diff options
author | bobzel <zzzman@gmail.com> | 2024-10-08 22:51:46 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-10-08 22:51:46 -0400 |
commit | 972839216c14baa5c9eaf80e1fb2fb2694bbb72c (patch) | |
tree | ebd73624983ad563134a6c17e8bce04a8a4bd38e /src | |
parent | caceff7f37b4e49621bc3495bf1d51fcc3a79957 (diff) |
modified how buttons are laid out on carousel and comparison views so that text boxes can reflow around them. extracted flashcard pratice into its own component and applied it to carousel3D and carousel
Diffstat (limited to 'src')
-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', }, |