diff options
-rw-r--r-- | src/client/views/PropertiesView.tsx | 7 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarousel3DView.scss | 1 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarousel3DView.tsx | 70 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.scss | 86 | ||||
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 316 | ||||
-rw-r--r-- | src/client/views/collections/FlashcardPracticeUI.scss | 64 | ||||
-rw-r--r-- | src/client/views/collections/FlashcardPracticeUI.tsx | 172 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.scss | 15 | ||||
-rw-r--r-- | src/client/views/nodes/ComparisonBox.tsx | 92 | ||||
-rw-r--r-- | src/client/views/nodes/FieldView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.scss | 6 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 16 |
12 files changed, 475 insertions, 371 deletions
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 371d34173..442dab671 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -139,6 +139,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps Object.values(this._disposers).forEach(disposer => disposer?.()); } + @computed get isText() { + return this.selectedDoc?.type === DocumentType.RTF; + } @computed get isInk() { return this.selectedDoc?.type === DocumentType.INK; } @@ -1199,8 +1202,8 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps // prettier-ignore <div className="transform-editor"> {!this.isStack ? null : this.getNumber('Gap', ' px', 0, 200, NumCast(this.selectedDoc!.gridGap), this.setVal((doc: Doc, val: number) => { doc.gridGap = val; })) } - {!this.isStack ? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) } - {!this.isStack ? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) } + {!this.isStack && !this.isText? null : this.getNumber('xMargin', ' px', 0, 500, NumCast(this.selectedDoc!.xMargin), this.setVal((doc: Doc, val: number) => { doc.xMargin = val; })) } + {!this.isStack && !this.isText? null : this.getNumber('yMargin', ' px', 0, 500, NumCast(this.selectedDoc!.yMargin), this.setVal((doc: Doc, val: number) => { doc.yMargin = val; })) } {!this.isGroup ? null : this.getNumber('Padding', ' px', 0, 500, NumCast(this.selectedDoc!.xPadding), this.setVal((doc: Doc, val: number) => { doc.xPadding = doc.yPadding = val; })) } {this.isInk ? this.controlPointsButton : null} {this.getNumber('Width', ' px', 0, Math.max(1000, this.shapeWid), this.shapeWid, this.setVal((doc: Doc, val:number) => {this.shapeWid = val}), 1000, 1)} diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index a556d0fa7..42e112906 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -4,6 +4,7 @@ position: relative; background-color: white; overflow: hidden; + display: flex; } .carousel-wrapper { diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index c5da8e037..1583f0e0c 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { computed, makeObservable } from 'mobx'; +import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnZero } from '../../../ClientUtils'; @@ -9,12 +9,13 @@ import { Id } from '../../../fields/FieldSymbols'; import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; +import { Transform } from '../../util/Transform'; import { StyleProp } from '../StyleProp'; import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -import { Transform } from '../../util/Transform'; +import { FlashcardPracticeUI } from './FlashcardPracticeUI'; // eslint-disable-next-line @typescript-eslint/no-require-imports const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @@ -24,6 +25,9 @@ export class CollectionCarousel3DView extends CollectionSubView() { @computed get scrollSpeed() { return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed } + _sideBtnWidth = 35; + @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined; + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); @@ -43,7 +47,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { }; @computed get carouselItems() { - return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); + return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK).filter(pair => !this._filterFunc?.(pair.layout)); } centerScale = Number(CAROUSEL3D_CENTER_SCALE); @@ -53,22 +57,17 @@ export class CollectionCarousel3DView extends CollectionSubView() { onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => !!this.isContentActive(); + contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); childScreenLeftToLocal = () => - this._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) + this.contentScreenToLocalXf() .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) .scale(1 / this.sideScale); childScreenRightToLocal = () => - this._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) + this.contentScreenToLocalXf() .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) .scale(1 / this.sideScale); childCenterScreenToLocal = () => - this._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) + this.contentScreenToLocalXf() .translate( -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3 -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2 @@ -119,7 +118,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { changeSlide = (direction: number) => { DocumentView.DeselectAll(); - this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % this.carouselItems.length; + this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1); }; onArrowClick = (direction: number) => { @@ -192,6 +191,35 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.panelWidth() * (1 - index); } + /** + * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore + + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetScale() { + const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1)); + return Math.max(maxWidgetSize / this._sideBtnWidth, 1); + } + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaleTransform() { return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore + screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale; + + docViewProps = () => ({ + ...this._props, // + isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, + isContentActive: this.isChildContentActive, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + carouselItemsFunc = () => this.carouselItems.map(pair => pair.layout); + @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore + answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1); + curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout; + render() { return ( <div @@ -206,6 +234,22 @@ export class CollectionCarousel3DView extends CollectionSubView() { </div> {this.buttons} <div className="dot-bar">{this.dots}</div> + <FlashcardPracticeUI + setFilterFunc={this.setFilterFunc} + fieldKey={this.fieldKey} + sideBtnWidth={this._sideBtnWidth} + carouselItems={this.carouselItemsFunc} + childDocs={this.childDocs} + advance={this.answered} + curDoc={this.curDoc} + practiceBtnOffset={this._sideBtnWidth * 4} + layoutDoc={this.layoutDoc} + maxWidgetScale={this.maxWidgetScale} + uiBtnScaleTransform={this.uiBtnScaleTransform} + ScreenToLocalBoxXf={this.ScreenToLocalBoxXf} + renderDepth={this._props.renderDepth} + docViewProps={this.docViewProps} + /> </div> ); } diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 97952822e..757072453 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -12,23 +12,10 @@ display: inline-block; width: 100%; user-select: none; + position: absolute; + top: 0; + left: 0; } - .message { - justify-content: center; - align-items: center; - display: flex; - height: 60%; - z-index: -1; - // margin: 15px; - } -} - -.collectionCarouselView-addFlashcards { - justify-content: center; - align-items: center; - height: 100%; - z-index: -1; - pointer-events: none; } .collectionCarouselView-recentlyMissed { color: red; @@ -39,10 +26,7 @@ pointer-events: none; } .carouselView-back, -.carouselView-fwd, -.carouselView-remove, -.carouselView-check, -.carouselView-add { +.carouselView-fwd { position: absolute; display: flex; width: 30; @@ -66,68 +50,6 @@ left: 0; transform-origin: top left; } -.carouselView-add { - position: absolute; - bottom: 0; - left: 0; -} -.carouselView-remove { - left: 52%; -} -.carouselView-check { - right: 52%; -} -.carouselView-menu { - position: absolute; - flex-direction: column; - align-items: center; - display: flex; - top: 0px; - left: 0px; - width: 30; - transform-origin: top left; - border-radius: 5px; - color: rgba(255, 255, 255, 0.5); - background: rgba(0, 0, 0, 0.1); - .carouselView-practiceModes { - width: 100%; - display: flex; - flex-direction: column; - top: 0; - position: relative; - .carouselView-quiz { - position: relative; - display: flex; - height: 20px; - align-items: center; - margin: auto; - &:hover { - color: white; - } - & > svg { - height: 100%; - width: 100%; - } - } - - .carouselView-practice { - position: relative; - display: flex; - flex-direction: column; - height: 20px; - align-items: center; - margin: auto; - &:hover { - color: white; - } - & > svg { - height: 100%; - width: 100%; - } - } - } -} - .carouselView-back:hover, .carouselView-fwd:hover { background: lightgray; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 559dcfe2a..64ddaac79 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,52 +1,35 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { StyleProp } from '../StyleProp'; -import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { FlashcardPracticeUI } from './FlashcardPracticeUI'; -enum cardMode { - STAR = 'star', - ALL = 'all', -} -enum practiceMode { - PRACTICE = 'practice', - QUIZ = 'quiz', -} -enum practiceVal { - MISSED = 'missed', - CORRECT = 'correct', -} @observer export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore - get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore - get starField() { return "#star"; } // prettier-ignore - - _sideBtnWidth = 35; _fadeTimer: NodeJS.Timeout | undefined; + _sideBtnWidth = 35; + @observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined; + @observable _last_index = this.carouselIndex; + @observable _last_opacity = 1; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } - @observable _last_index = this.carouselIndex; - @observable _last_opacity = 1; - componentWillUnmount() { this._dropDisposer?.(); } @@ -58,39 +41,32 @@ export class CollectionCarouselView extends CollectionSubView() { } }; - @computed get practiceMode() { - return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : ''; - } - @computed get practiceMessage() { - const cardCount = this.carouselItems.length; - if (this.practiceMode) { - if (!Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) { - return 'Finished! Click here to view all flashcards.'; - } - } - return ''; - } - - @computed get filterMessage() { - const cardCount = this.carouselItems.length; - if (!this.practiceMessage) { - if (Doc.hasDocFilter(this.layoutDoc, 'tags', Doc.FilterAny) && !cardCount) { - return 'No tagged items. Click here to view all flash cards.'; - } - if (this.practiceMode) { - if (!cardCount) return 'No flashcards to show! Click here to leave practice mode'; - } - } - return ''; - } - @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore + @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore @computed get carouselItems() { return this.childDocs .filter(doc => doc.type !== DocumentType.LINK) - .filter(doc => !this.practiceMode || (BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] !== practiceVal.CORRECT))// show only cards that aren't marked as correct + .filter(doc => !this._filterFunc?.(doc)) } // prettier-ignore /** + * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore + + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetScale() { + const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.layoutDoc.width, 1)); + return Math.max(maxWidgetSize / this._sideBtnWidth, 1); + } + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaleTransform() { return this.maxWidgetScale * Math.min(1, this.contentScaling); } // prettier-ignore + screenXPadding = () => (this.uiBtnScaleTransform * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale; + + /** * Move forward or backward the specified number of Docs * @param dir signed number indicating Docs to move forward or backward */ @@ -102,8 +78,8 @@ export class CollectionCarouselView extends CollectionSubView() { /** * Goes to the next Doc in the stack subject to the currently selected filter option. */ - advance = (e: React.MouseEvent) => { - e.stopPropagation(); + advance = (e?: React.MouseEvent) => { + e?.stopPropagation(); this.move(1); }; @@ -115,55 +91,23 @@ export class CollectionCarouselView extends CollectionSubView() { this.move(-1); }; - /* - * Toggles whether the 'star' metadata field is set on the current Doc - */ - toggleStar = (e: React.MouseEvent) => { - e.stopPropagation(); - const curDoc = this.carouselItems[this.carouselIndex]; - if (curDoc) { - if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField); - else TagItem.addTagToDoc(curDoc, this.starField); - } - }; - - /* - * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. - */ - setPracticeVal = (e: React.MouseEvent, val: string) => { - e.stopPropagation(); - const curDoc = this.carouselItems[this.carouselIndex]; - curDoc && (curDoc[this.practiceField] = val); - this.advance(e); - }; - - /** - * Sets the practice mode answer style for flashcards - * @param mode practiceMode or undefined for no practice - */ - setPracticeMode = (mode: practiceMode | undefined) => { - this.layoutDoc.practiceMode = mode; - this.carouselItems?.map(doc => (doc[this.practiceField] = undefined)); - if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined)); - }; + curDoc = () => this.carouselItems[this.carouselIndex]; captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => { // first look for properties on the document in the carousel, then fallback to properties on the container const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined; return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property); }; + contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); - captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; + captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX; contentScreenToLocalXf = () => this._props .ScreenToLocalTransform() .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)) .scale(this._props.NativeDimScaling?.() || 1); - - contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); - isChildContentActive = () => this._props.isContentActive?.() === false ? false @@ -172,10 +116,7 @@ export class CollectionCarouselView extends CollectionSubView() { : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; - renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { - const screenScale = this.ScreenToLocalBoxXf().Scale; - const fitWidthScale = (NumCast(this.Document.width, 1) / NumCast(this.carouselItems[this.carouselIndex]?._width)) * (this._props.NativeDimScaling?.() || 1); return ( <DocumentView {...this._props} @@ -203,7 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() { ScreenToLocalTransform={this.contentScreenToLocalXf} PanelWidth={this.contentPanelWidth} PanelHeight={this.contentPanelHeight} - xPadding={(this._sideBtnWidth * Math.min(this.maxWidgetScale, screenScale * screenScale)) / fitWidthScale} // padding shrinks based on screenScale to maintain its size, and then again by screenSize to get smaller + screenXPadding={this.screenXPadding} /> ); }; @@ -214,7 +155,7 @@ export class CollectionCarouselView extends CollectionSubView() { const fadeTime = 500; const lastDoc = this.carouselItems?.[this._last_index]; return !lastDoc || this.carouselIndex === this._last_index ? null : ( - <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}> + <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}> {this.renderDoc( lastDoc, false, // hide captions if the carousel is configured to show the captions @@ -235,15 +176,18 @@ export class CollectionCarouselView extends CollectionSubView() { </div> ); } + @computed get renderedDoc() { + const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); + return this.renderDoc(this.curDoc(), !!carouselShowsCaptions); + } + @computed get content() { - const index = this.carouselIndex; - const curDoc = this.carouselItems?.[index]; const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined }; const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); - return !curDoc ? null : ( + return !this.curDoc() ? null : ( <> <div className="collectionCarouselView-image" key="image"> - {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.renderedDoc} {this.overlay} </div> {!carouselShowsCaptions ? null : ( @@ -253,158 +197,70 @@ export class CollectionCarouselView extends CollectionSubView() { onWheel={StopEvent} style={{ borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string, - marginRight: this.marginX, - marginLeft: this.marginX, - width: `calc(100% - ${this.marginX * 2}px)`, + marginRight: this.captionMarginX, + marginLeft: this.captionMarginX, + width: `calc(100% - ${this.captionMarginX * 2}px)`, }}> - <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} /> + <FormattedTextBox xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={this.curDoc()} TemplateDataDocument={undefined} /> </div> )} </> ); } - togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode); - toggleFilterMode = () => Doc.setDocFilter(this.Document, 'tags', this.starField, 'check', true); - setColor = (mode: practiceMode | cardMode, which: string) => (which === mode ? 'white' : 'light gray'); - - @computed get filterDoc() { - return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); - } - filterHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this.ScreenToLocalBoxXf().Scale); - filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); - - /** - * How much the content of the carousel view is being scaled based on its nesting and its fit-to-width settings - */ - @computed get contentScaling() { - return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); - } - - /** - * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. - */ - @computed get maxWidgetScale() { - const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.1 * NumCast(this.Document.width, 1)); - return Math.max(maxWidgetSize / this._sideBtnWidth, 1); - } - /** - * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content - */ - @computed get uiBtnScaleTransform() { - return `scale(${this.maxWidgetScale * Math.min(1, this.contentScaling)})`; - } - @computed get menu() { - const curDoc = this.carouselItems?.[this.carouselIndex]; - return ( - <div className="carouselView-menu" style={{ height: this.filterHeight(), width: this.filterHeight(), transform: this.uiBtnScaleTransform }}> - {!this.filterDoc ? null : ( - <DocumentView - {...this._props} - Document={this.filterDoc} - TemplateDataDocument={undefined} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - renderDepth={this._props.renderDepth + 1} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} - showTags={false} - hideFilterStatus={true} - containerViewPath={this.childContainerViewPath} - setContentViewBox={undefined} - onDoubleClickScript={this.onContentDoubleClick} - onClickScript={this.onContentClick} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} - isContentActive={this.isChildContentActive} - hideCaptions={true} - childFilters={this.childDocFilters} - hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} - addDocument={this._props.addDocument} - ScreenToLocalTransform={this.contentScreenToLocalXf} - PanelWidth={this.filterWidth} - PanelHeight={this.filterHeight} - /> - )} - <div - className="carouselView-practiceModes" - style={{ - transformOrigin: `0px ${-this.filterHeight()}px`, - transform: `scale(${Math.max(1, 1 / this.ScreenToLocalBoxXf().Scale / this.maxWidgetScale)})`, - display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none', - }}> - <Tooltip title="Practice flashcards using GPT"> - <div key="back" className="carouselView-quiz" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}> - <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" /> - </div> - </Tooltip> - <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> - <div key="back" className="carouselView-practice" style={{ width: this.filterWidth(), height: this.filterHeight() }} onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}> - <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" /> - </div> - </Tooltip> - </div> - </div> - ); - } - @computed get buttons() { - return ( + @computed get navButtons() { + return this.Document._chromeHidden || !this.curDoc() ? null : ( <> - <div key="back" className="carouselView-back" style={{ transform: this.uiBtnScaleTransform }} onClick={this.goback}> + <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> - <div key="fwd" className="carouselView-fwd" style={{ transform: this.uiBtnScaleTransform }} onClick={this.advance}> + <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaleTransform})` }} onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - {this.practiceMode == practiceMode.PRACTICE ? ( - <div style={{ transform: this.uiBtnScaleTransform, bottom: `${this._sideBtnWidth}px`, height: `${this._sideBtnWidth}px`, position: 'absolute', width: `100%` }}> - <Tooltip title="Incorrect. View again later."> - <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}> - <FontAwesomeIcon icon="xmark" color="red" size="1x" /> - </div> - </Tooltip> - <Tooltip title="Correct"> - <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)}> - <FontAwesomeIcon icon="check" color="green" size="1x" /> - </div> - </Tooltip> - </div> - ) : null} </> ); } + docViewProps = () => ({ + ...this._props, // + isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, + isContentActive: this.isChildContentActive, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + carouselItemsFunc = () => this.carouselItems; + answered = () => this.advance(); + @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore + render() { return ( - <div> - <div - className="collectionCarouselView-outer" - ref={this.createDashEventsTarget} - style={{ - background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, - width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`, - height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`, - left: NumCast(this.layoutDoc._xMargin), - top: NumCast(this.layoutDoc._yMargin), - }}> - {!this.practiceMessage && !this.filterMessage ? ( - this.content - ) : ( - <p - className="message" - onClick={() => { - if (this.filterMessage || this.practiceMessage) { - this.setPracticeMode(undefined); - Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove'); - } - }}> - {this.filterMessage || this.practiceMessage} - </p> - )} - </div> - {!this.Document._chromeHidden ? this.menu : null} - {!this.Document._chromeHidden && this.carouselItems?.[this.carouselIndex] ? this.buttons : null} + <div + className="collectionCarouselView-outer" + ref={this.createDashEventsTarget} + style={{ + background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, + color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, + width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`, + height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`, + left: NumCast(this.layoutDoc._xMargin), + top: NumCast(this.layoutDoc._yMargin), + }}> + {this.content} + <FlashcardPracticeUI + setFilterFunc={this.setFilterFunc} + fieldKey={this.fieldKey} + sideBtnWidth={this._sideBtnWidth} + carouselItems={this.carouselItemsFunc} + childDocs={this.childDocs} + advance={this.answered} + curDoc={this.curDoc} + layoutDoc={this.layoutDoc} + maxWidgetScale={this.maxWidgetScale} + uiBtnScaleTransform={this.uiBtnScaleTransform} + ScreenToLocalBoxXf={this.ScreenToLocalBoxXf} + renderDepth={this._props.renderDepth} + docViewProps={this.docViewProps} + /> + {this.navButtons} </div> ); } diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss new file mode 100644 index 000000000..53c26ad34 --- /dev/null +++ b/src/client/views/collections/FlashcardPracticeUI.scss @@ -0,0 +1,64 @@ +.FlashcardPracticeUI-remove, +.FlashcardPracticeUI-check { + position: absolute; + display: flex; + width: 30; + height: 30; + align-items: center; + border-radius: 5px; + justify-content: center; + color: rgba(255, 255, 255, 0.5); + background: rgba(0, 0, 0, 0.1); + &:hover { + color: white; + } +} +.FlashcardPracticeUI-remove { + left: 52%; +} +.FlashcardPracticeUI-check { + right: 52%; +} +.FlashcardPracticeUI-menu { + position: absolute; + flex-direction: column; + align-items: center; + display: flex; + top: 0px; + left: 0px; + width: 30; + transform-origin: top left; + border-radius: 5px; + color: rgba(255, 255, 255, 0.5); + background: rgba(0, 0, 0, 0.1); + .FlashcardPracticeUI-practiceModes { + width: 100%; + display: flex; + flex-direction: column; + top: 0; + position: relative; + .FlashcardPracticeUI-quiz, + .FlashcardPracticeUI-practice { + position: relative; + display: flex; + height: 20px; + align-items: center; + margin: auto; + padding: 3px; + &:hover { + color: white; + } + & > svg { + height: 100%; + width: 100%; + } + } + } +} +.FlashcardPracticeUI-message { + z-index: 100; + position: relative; + margin: auto; + align-content: center; + width: max-content; +} diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx new file mode 100644 index 000000000..032a405bf --- /dev/null +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -0,0 +1,172 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@mui/material'; +import { computed, makeObservable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { returnZero } from '../../../ClientUtils'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { Transform } from '../../util/Transform'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import './FlashcardPracticeUI.scss'; + +enum practiceMode { + PRACTICE = 'practice', + QUIZ = 'quiz', +} +enum practiceVal { + MISSED = 'missed', + CORRECT = 'correct', +} + +interface PracticeUIProps { + fieldKey: string; + layoutDoc: Doc; + carouselItems: () => Doc[]; + childDocs: Doc[]; + curDoc: () => Doc; + advance: (correct: boolean) => void; + renderDepth: number; + sideBtnWidth: number; + uiBtnScaleTransform: number; + ScreenToLocalBoxXf: () => Transform; + maxWidgetScale: number; + docViewProps: () => DocumentViewProps; + setFilterFunc: (func?: (doc: Doc) => boolean) => void; + practiceBtnOffset?: number; +} +@observer +export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> { + constructor(props: PracticeUIProps) { + super(props); + makeObservable(this); + this._props.setFilterFunc(this.tryFilterOut); + } + + componentWillUnmount(): void { + this._props.setFilterFunc(undefined); + } + + get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore + + @computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore + @computed get practiceMode() { return this._props.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore + + btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale); + btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); + + /** + * Sets the practice mode answer style for flashcards + * @param mode practiceMode or undefined for no practice + */ + setPracticeMode = (mode: practiceMode | undefined) => { + this._props.layoutDoc.practiceMode = mode; + this._props.carouselItems().map(doc => (doc[this.practiceField] = undefined)); + }; + + @computed get emptyMessage() { + const cardCount = this._props.carouselItems().length; + const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !this._props.carouselItems().length ? 'Finished! Click here to view all flashcards.' : ''; + const filterMessage = practiceMessage + ? '' + : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount + ? 'No tagged items. Click here to view all flash cards.' + : this.practiceMode && !cardCount + ? 'No flashcards to show! Click here to leave practice mode' + : ''; + return !practiceMessage && !filterMessage ? null : ( + <p + className="FlashcardPracticeUI-message" + onClick={() => { + if (filterMessage || practiceMessage) { + this.setPracticeMode(undefined); + Doc.setDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny, 'remove'); + } + }}> + {filterMessage || practiceMessage} + </p> + ); + } + + @computed get practiceButtons() { + /* + * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. + */ + const setPracticeVal = (e: React.MouseEvent, val: string) => { + e.stopPropagation(); + const curDoc = this._props.curDoc(); + curDoc && (curDoc[this.practiceField] = val); + this._props.advance?.(val === practiceVal.CORRECT); + }; + + return this.practiceMode == practiceMode.PRACTICE ? ( + <div style={{ transform: `scale(${this._props.uiBtnScaleTransform})`, bottom: `${this._props.practiceBtnOffset ?? this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px`, position: 'absolute', width: `100%` }}> + <Tooltip title="Incorrect. View again later."> + <div key="remove" className="FlashcardPracticeUI-remove" onClick={e => setPracticeVal(e, practiceVal.MISSED)}> + <FontAwesomeIcon icon="xmark" color="red" size="1x" /> + </div> + </Tooltip> + <Tooltip title="Correct"> + <div key="check" className="FlashcardPracticeUI-check" onClick={e => setPracticeVal(e, practiceVal.CORRECT)}> + <FontAwesomeIcon icon="check" color="green" size="1x" /> + </div> + </Tooltip> + </div> + ) : null; + } + @computed get practiceModesMenu() { + const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'light gray'); + const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode); + + return !this._props.curDoc()?._layout_isFlashcard ? null : ( + <div + className="FlashcardPracticeUI-practiceModes" + style={{ + transformOrigin: `0px ${-this.btnHeight()}px`, + transform: `scale(${Math.max(1, 1 / this._props.ScreenToLocalBoxXf().Scale / this._props.maxWidgetScale)})`, + }}> + <Tooltip title="Practice flashcards using GPT"> + <div key="back" className="FlashcardPracticeUI-quiz" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.QUIZ)}> + <FontAwesomeIcon icon="file-pen" color={setColor(practiceMode.QUIZ)} size="sm" /> + </div> + </Tooltip> + <Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> + <div key="back" className="FlashcardPracticeUI-practice" style={{ width: this.btnWidth(), height: this.btnHeight() }} onClick={() => togglePracticeMode(practiceMode.PRACTICE)}> + <FontAwesomeIcon icon="check" color={setColor(practiceMode.PRACTICE)} size="sm" /> + </div> + </Tooltip> + </div> + ); + } + tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct + render() { + return ( + <> + {this.emptyMessage} + {this.practiceButtons} + <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaleTransform})` }}> + {!this.filterDoc || this._props.layoutDoc._chromeHidden ? null : ( + <DocumentView + {...this._props.docViewProps()} + Document={this.filterDoc} + TemplateDataDocument={undefined} + PanelWidth={this.btnWidth} + PanelHeight={this.btnHeight} + NativeWidth={returnZero} + NativeHeight={returnZero} + hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)} + hideCaptions={true} + hideFilterStatus={true} + renderDepth={this._props.renderDepth + 1} + fitWidth={undefined} + showTags={false} + setContentViewBox={undefined} + /> + )} + {this.practiceModesMenu} + </div> + </> + ); + } +} diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss index b7307f3a3..8156c50f6 100644 --- a/src/client/views/nodes/ComparisonBox.scss +++ b/src/client/views/nodes/ComparisonBox.scss @@ -296,3 +296,18 @@ } } } +.comparisonBox-bottomMenu { + transform-origin: bottom right; + width: max-content; + justify-content: space-between; + height: max-content; + position: absolute; + bottom: 0; + right: 2; + flex-direction: row-reverse; + display: flex; + cursor: pointer; + .comparisonBox-button { + padding-right: 8px; + } +} diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index ef66c2b11..81e223028 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -68,7 +68,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() return ( <Tooltip title={<div className="dash-tooltip">flip</div>}> <div - className="formattedTextBox-alternateButton" + className="comparisonBox-alternateButton ccomparisonBox-button" onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => { if (!this.revealOp || this.revealOp === 'flip') { @@ -81,42 +81,74 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white', display: 'inline-block', }}> - <div key="alternate" className="formattedTextBox-flip"> - <FontAwesomeIcon icon="turn-up" size="1x" /> - </div> + <FontAwesomeIcon icon="turn-up" size="xl" /> </div> </Tooltip> ); } + _sideBtnWidth = 30; + /** + * How much the content of the view is being scaled based on its nesting and its fit-to-width settings + */ + @computed get contentScaling() { + return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); + } + /** + * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. + */ + @computed get maxWidgetScale() { + const maxWidgetSize = Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * Math.min(NumCast(this.Document.width), NumCast(this.Document.height))); + return Math.max(maxWidgetSize / this._sideBtnWidth, 1); + } + /** + * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content + */ + @computed get uiBtnScaleTransform() { + return this.maxWidgetScale * Math.min(1, this.contentScaling); + } + @computed get flashcardMenu() { return ( - <div> - <Tooltip - title={this._frontSide ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}> - <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={() => (!this._frontSide ? this.findImageTags() : null)}> - <FontAwesomeIcon icon="lightbulb" size="xl" /> - </div> - </Tooltip> - {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : ( - <div> - <Tooltip title={<div>Create a flashcard pile</div>}> - <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={() => this.createFlashcardPile([this.Document], false)}> - <FontAwesomeIcon icon="folder-plus" size="xl" /> - </div> - </Tooltip> - <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> - <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={() => this.gptFlashcardPile()}> - <FontAwesomeIcon icon="layer-group" size="xl" /> + <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaleTransform})` }}> + {this.overlayAlternateIcon} + {!this._props.isContentActive() ? null : ( + <> + {' '} + {!this._frontSide ? null : ( + <Tooltip + title={ + <div className="dash-tooltip">{ + !this._frontSide ? "Flip to front side to use GPT": + "Ask GPT to create an answer on the back side of the flashcard based on your question on the front"} + </div> // prettier-ignore + }> + <div className="comparisonBox-button" onPointerDown={() => (this._frontSide ? this.findImageTags() : null)}> + <FontAwesomeIcon icon="lightbulb" size="xl" /> + </div> + </Tooltip> + )} + {DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : ( + <> + <Tooltip title={<div>Create a flashcard pile</div>}> + <div className="comparisonBox-button" onPointerDown={() => this.createFlashcardPile([this.Document], false)}> + <FontAwesomeIcon icon="folder-plus" size="xl" /> + </div> + </Tooltip> + <Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}> + <div className="comparisonBox-button" onClick={this.gptFlashcardPile}> + <FontAwesomeIcon icon="layer-group" size="xl" /> + </div> + </Tooltip> + </> + )} + <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}> + <div className="comparisonBox-button" onClick={this.handleHover}> + <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" /> </div> </Tooltip> - </div> + </> )} - <Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}> - <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={() => this.handleHover()}> - <FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" /> - </div> - </Tooltip> </div> ); } @@ -478,7 +510,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() this._loading = false; return; } - this.flipFlashcard(); } try { console.log(queryText); @@ -655,6 +686,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() childActiveFunc = () => this._childActive; + contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); render() { const clearButton = (which: string) => ( <Tooltip title={<div className="dash-tooltip">remove</div>}> @@ -687,6 +719,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2} NativeWidth={returnZero} NativeHeight={returnZero} + ScreenToLocalTransform={this.contentScreenToLocalXf} isContentActive={this.childActiveFunc} isDocumentActive={returnFalse} dontSelect={returnTrue} @@ -808,8 +841,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>() <ReactLoading type="spin" height={30} width={30} color={'blue'} /> </div> ) : null} - {this._props.isContentActive() ? this.flashcardMenu : null} - {this.overlayAlternateIcon} + {this.flashcardMenu} </div> ); } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 170966471..c81631baa 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -51,6 +51,7 @@ export interface FieldViewSharedProps { LayoutTemplate?: () => Opt<Doc>; renderDepth: number; scriptContext?: unknown; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document + screenXPadding?: () => number; // padding in screen space coordinates (used by text box to reflow around UI buttons in carouselView) xPadding?: number; yPadding?: number; dontRegisterView?: boolean; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index d6f13d9ee..f1ae1151f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -101,12 +101,6 @@ audiotag:hover { height: 22; cursor: default; } -.formattedTextBox-flip { - align-items: center; - position: absolute; - right: 2px; - bottom: 4px; -} .formattedTextBox-outer { position: relative; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index c89737e1e..93153b453 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -763,10 +763,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { - const localDelta = this._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) - .transformDirection(delta[0], delta[1]); + const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta; const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; @@ -1264,7 +1261,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._cachedLinks = Doc.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( - () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }), + () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }), autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction( @@ -2088,8 +2085,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const scale = this._props.NativeDimScaling?.() || 1; const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); - const paddingX = Math.max(this._props.xPadding ?? 0, NumCast(this.layoutDoc._xMargin)); - const paddingY = Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin)); + + const scrSize = (which: number, view = this._props.docViewPath().slice(-which)[0]) => + [view._props.PanelWidth() / view.screenToLocalScale(), view._props.PanelHeight() / view.screenToLocalScale()]; // prettier-ignore + const scrMargin = [Math.max(0, (scrSize(2)[0] - scrSize(1)[0]) / 2), Math.max(0, (scrSize(2)[1] - scrSize(1)[1]) / 2)]; + const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), this._props.xPadding ?? 0, 0, ((this._props.screenXPadding?.() ?? 0) - scrMargin[0]) * this.ScreenToLocalBoxXf().Scale); + const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, ((this._props.yPadding ?? 0) - scrMargin[1]) * this.ScreenToLocalBoxXf().Scale); const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > return styleFromLayout?.height === '0px' ? null : ( <div @@ -2176,7 +2177,6 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, { _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, - _layout_noSidebar: true, defaultDoubleClick: 'ignore', systemIcon: 'BsFileEarmarkTextFill', }, |