diff options
Diffstat (limited to 'src')
4 files changed, 151 insertions, 92 deletions
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index c799eb3c8..54cc02825 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -78,6 +78,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={undefined} + containerViewPath={this.childContainerViewPath} onDoubleClickScript={this.onChildDoubleClick} renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index f115bb40a..01b20d6d3 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -12,6 +12,21 @@ user-select: none; } } +.collectionCarouselView-addFlashcards { + justify-content: center; + align-items: center; + height: 100%; + z-index: -1; + pointer-events: none; +} +.collectionCarouselView-recentlyMissed { + color: red; + z-index: 999; + position: relative; + left: 10px; + top: 10px; + pointer-events: none; +} .carouselView-back, .carouselView-fwd, .carouselView-star, diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 2f8d5adaf..ad868fd1e 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,12 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { computed, makeObservable } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc, Opt } from '../../../fields/Doc'; -import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { ContextMenu } from '../ContextMenu'; @@ -32,13 +31,34 @@ export class CollectionCarouselView extends CollectionSubView() { get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore get starField() { return "star"; } // prettier-ignore + _fadeTimer: NodeJS.Timeout | undefined; + _resetter: IReactionDisposer | undefined; + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } + @observable _last_index = this.carouselIndex; + @observable _last_opacity = 1; + + componentDidMount() { + this._resetter = reaction( + // automatically reset practice fields when all cards have been marked as correct + () => this.carouselItems.length, + itemsCount => { + if (this.layoutDoc.filterOp === cardMode.PRACTICE && !itemsCount) { + this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode + this.carouselItems.forEach(item => { // reset all the practice values + item[this.practiceField] = undefined; + }); + } + } // prettier-ignore + ); + } componentWillUnmount() { this._dropDisposer?.(); + this._resetter?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { @@ -48,43 +68,24 @@ export class CollectionCarouselView extends CollectionSubView() { } }; + @computed get marginX() { 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.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); - } - @computed get marginX() { - return NumCast(this.layoutDoc.caption_xMargin, 50); + return DocListCast(this.childDocList) + .filter(doc => doc.type !== DocumentType.LINK) + .filter(doc => { + switch (StrCast(this.layoutDoc.filterOp)) { + case cardMode.STAR: return !!doc[this.starField]; // show only cards that are starred + case cardMode.PRACTICE: return doc[this.practiceField] !== practiceVal.CORRECT;// show only cards that aren't marked as correct + default: return true; + } // prettier-ignore + }); } - move = (dir: number) => { - const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => { - let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length; - while (!match(this.carouselItems?.[startInd].layout) && (startInd + dir + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) { - startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length; - } - if (match(this.carouselItems?.[startInd].layout)) { - this.layoutDoc._carousel_index = startInd; - return true; - } - return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout); - }; - switch (StrCast(this.layoutDoc.filterOp)) { - case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't - if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) { - this.layoutDoc.filterOp = undefined; // if there aren't any starred, show all cards - } - break; - case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct - if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) { - this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode - - this.carouselItems.forEach(item => { // reset all the practice values - item.layout[this.practiceField] = undefined; - }); - } - break; - default: moveToCardWithField(returnTrue); - } // prettier-ignore - }; + move = action((dir: number) => { + this._last_index = this.carouselIndex; + this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length; + }); /** * Goes to the next Doc in the stack subject to the currently selected filter option. @@ -107,8 +108,8 @@ export class CollectionCarouselView extends CollectionSubView() { */ star = (e: React.MouseEvent) => { e.stopPropagation(); - const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)]; - curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true; + const curDoc = this.carouselItems[this.carouselIndex]; + curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true); }; /* @@ -116,8 +117,8 @@ export class CollectionCarouselView extends CollectionSubView() { */ setPracticeVal = (e: React.MouseEvent, val: string) => { e.stopPropagation(); - const curDoc = this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]; - curDoc.layout[this.practiceField] = val; + const curDoc = this.carouselItems[this.carouselIndex]; + curDoc && (curDoc[this.practiceField] = val); this.advance(e); }; @@ -132,7 +133,6 @@ export class CollectionCarouselView extends CollectionSubView() { captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; specificMenu = (): void => { const cm = ContextMenu.Instance; - const revealOptions = cm.findByDescription('Filter Flashcards'); const revealItems = revealOptions?.subitems ?? []; revealItems.push({description: 'All', event: () => {this.layoutDoc.filterOp = undefined;}, icon: 'layer-group',}); // prettier-ignore @@ -141,32 +141,78 @@ export class CollectionCarouselView extends CollectionSubView() { revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); }; + + isChildContentActive = () => + this._props.isContentActive?.() === false + ? false + : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined; + + renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { + return ( + <DocumentView + {...this._props} + ref={overlayFunc} + Document={doc} + NativeWidth={returnZero} + NativeHeight={returnZero} + fitWidth={undefined} + 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={showCaptions} + renderDepth={this._props.renderDepth + 1} + LayoutTemplate={this._props.childLayoutTemplate} + LayoutTemplateString={this._props.childLayoutString} + TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} + PanelHeight={this.panelHeight} + /> + ); + }; + /** + * Display an overlay of the previous card that crossfades to the next card + */ + @computed get overlay() { + 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` }}> + {this.renderDoc( + lastDoc, + false, // hide captions if the carousel is configured to show the captions + action((r: DocumentView | null) => { + if (r) { + this._fadeTimer && clearTimeout(this._fadeTimer); + this._last_opacity = 0; + this._fadeTimer = setTimeout( + action(() => { + this._last_index = -1; + this._last_opacity = 1; + }), + fadeTime + ); + } + }) + )} + </div> + ); + } @computed get content() { - const index = NumCast(this.layoutDoc._carousel_index); + 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?.layout instanceof Doc) ? null : ( + return !curDoc ? null : ( <> <div className="collectionCarouselView-image" key="image"> - <DocumentView - {...this._props} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} - setContentViewBox={undefined} - onDoubleClickScript={this.onContentDoubleClick} - onClickScript={this.onContentClick} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} - isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction} - hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions - renderDepth={this._props.renderDepth + 1} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - Document={curDoc.layout} - TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)} - PanelHeight={this.panelHeight} - /> + {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.overlay} </div> {!carouselShowsCaptions ? null : ( <div @@ -179,14 +225,14 @@ export class CollectionCarouselView extends CollectionSubView() { marginLeft: this.marginX, width: `calc(100% - ${this.marginX * 2}px)`, }}> - <FormattedTextBox key={index} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} TemplateDataDocument={undefined} /> + <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} /> </div> )} </> ); } @computed get buttons() { - if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null; + if (!this.carouselItems?.[this.carouselIndex]) return null; return ( <> <div key="back" className="carouselView-back" onClick={this.goback}> @@ -196,7 +242,7 @@ export class CollectionCarouselView extends CollectionSubView() { <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> <div key="star" className="carouselView-star" onClick={this.star}> - <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> + <FontAwesomeIcon icon="star" color={this.carouselItems?.[this.carouselIndex][this.starField] ? 'yellow' : 'gray'} size="1x" /> </div> <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> <FontAwesomeIcon icon="xmark" color="red" size="1x" /> @@ -208,6 +254,24 @@ export class CollectionCarouselView extends CollectionSubView() { ); } + /** + * Prompts user to add more flashcaards if they are in practice mode but there are no flashcards + */ + renderAddFlashcards = () => <p + className="collectionCarouselView-addFlashcards" + style={{display: !this.carouselItems?.[this.carouselIndex] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none'}}> + Add flashcards! + </p> // prettier-ignore + + /** + * Displays message that a flashcard was recently missed if it had previously been marked as wrong. + * */ + renderRecentlyMissed = () => <p + className="collectionCarouselView-recentlyMissed" + style={{display: this.carouselItems?.[this.carouselIndex]?.[this.practiceField] === practiceVal.MISSED ? 'block' : 'none'}}> + Recently missed! + </p> // prettier-ignore + render() { return ( <div @@ -219,29 +283,8 @@ export class CollectionCarouselView extends CollectionSubView() { color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, }}> {this.content} - {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */} - <p - style={{ - display: !this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - zIndex: '-1', - }}> - Add flashcards! - </p> - {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */} - <p - style={{ - color: 'red', - zIndex: '999', - position: 'relative', - left: '10px', - top: '10px', - display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] ? (this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED ? 'block' : 'none') : 'none', - }}> - Recently missed! - </p> + {this.renderAddFlashcards()} + {this.renderRecentlyMissed()} {this.Document._chromeHidden ? null : this.buttons} </div> ); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index def1ea731..f56ea9d76 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -506,13 +506,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { } // eslint-disable-next-line @typescript-eslint/no-explicit-any - onActiveContentItemChanged = (contentItem: any) => { + onActiveContentItemChanged = action((contentItem: any) => { if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) { this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab; if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false)); !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } - }; + }); // adds a tab to the layout based on the locaiton parameter which can be: // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, |