diff options
| author | bobzel <zzzman@gmail.com> | 2024-09-17 18:31:09 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2024-09-17 18:31:09 -0400 |
| commit | 0653464370398188b23bb490c16b5a2ccf0300c3 (patch) | |
| tree | 3dff6b5a05b6e4f5d3ad489b3e34ece487b836ac /src/client/views/collections/CollectionCarouselView.tsx | |
| parent | 35d19c29c2f628792a379534df6d5760e49cfb8f (diff) | |
| parent | 4f2ee4a8642a93fb399b979750078374b317af32 (diff) | |
merged with master + cleanup of carousel code
Diffstat (limited to 'src/client/views/collections/CollectionCarouselView.tsx')
| -rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 359 |
1 files changed, 164 insertions, 195 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index a1c59d238..d9a99f47f 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,11 +1,10 @@ /* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, makeObservable, observable, trace } 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 { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; import { Doc, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -19,15 +18,12 @@ import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; enum cardMode { - // PRACTICE = 'practice', STAR = 'star', - // QUIZ = 'quiz', ALL = 'all', } enum practiceMode { PRACTICE = 'practice', QUIZ = 'quiz', - NORMAL = 'normal', } enum practiceVal { MISSED = 'missed', @@ -36,24 +32,20 @@ enum practiceVal { @observer export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - @observable private _practiceMessage: string | undefined; - @observable private _filterMessage: string | undefined; get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore - get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore + get sideField() { return "_" + this.fieldKey + "_usePath"; } // prettier-ignore get starField() { return "star"; } // prettier-ignore + _fadeTimer: NodeJS.Timeout | undefined; + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); - // this.setModes(); - this.layoutDoc.filterOp = cardMode.ALL; - Doc.setDocFilter(this.Document, 'star', undefined, 'match'); - this.layoutDoc.practiceMode = practiceMode.NORMAL; - this.layoutDoc._carousel_index = 0; - this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore - console.log(this.carouselItems.length); } + @observable _last_index = this.carouselIndex; + @observable _last_opacity = 1; + componentWillUnmount() { this._dropDisposer?.(); } @@ -65,65 +57,42 @@ export class CollectionCarouselView extends CollectionSubView() { } }; - @computed get carouselItems() { - this.childLayoutPairs.map(pair => { - pair.layout.embedContainer = this.Document; - }); - return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); + @computed get practiceMode() { + return this.childDocs.some(doc => doc._layout_isFlashcard) ? StrCast(this.layoutDoc.practiceMode) : ''; } - @computed get marginX() { - return NumCast(this.layoutDoc.caption_xMargin, 50); + @computed get practiceMessage() { + const cardCount = this.carouselItems.length; + if (this.practiceMode) { + if (!Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) { + return 'Finished! Click here to view all flashcards.'; + } + } + return ''; } - @action setPracticeMessage = (mes: string | undefined) => { - this._practiceMessage = mes; - }; - @action setFilterMessage = (mes: string | undefined) => { - this._filterMessage = mes; - }; - - setModes = () => { - this.layoutDoc.filterOp = cardMode.ALL; - Doc.setDocFilter(this.Document, 'data_star', undefined, 'match'); - this.layoutDoc.practiceMode = practiceMode.NORMAL; - this.layoutDoc._carousel_index = 0; - }; - - 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 + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) { - startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length; + @computed get filterMessage() { + const cardCount = this.carouselItems.length; + if (!this.practiceMessage) { + if (Doc.hasDocFilter(this.layoutDoc, 'star') && !cardCount) { + return 'No starred items. Click here to view all flash cards.'; } - if (match(this.carouselItems?.[startInd].layout)) { - this.layoutDoc._carousel_index = startInd; - return true; + if (this.practiceMode) { + if (!cardCount) return 'No flashcards to show! Click here to leave practice mode'; } - return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout); - }; - - switch (this.layoutDoc.practiceMode && this.layoutDoc.filterOp) { - case practiceMode.PRACTICE && cardMode.ALL: - if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) { - this._practiceMessage = 'Finished! Unselect practice mode to view all flashcards.'; - this.carouselItems.forEach(item => { item.layout[this.practiceField] = undefined}); //prettier-ignore - } - break; - case !practiceMode.PRACTICE && cardMode.STAR: - if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) { - this._filterMessage = 'No starred items. Unselect this view to see all flashcards and star them.'; - } - break; - case practiceMode.PRACTICE && cardMode.STAR: - if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT && doc[this.starField] === true)) { - this._filterMessage = 'No flashcards to show! Unselect mode to view all flashcards.'; - this._practiceMessage = undefined; - } - break; - default: - moveToCardWithField(returnTrue); } - }; + return ''; + } + @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.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 + } // prettier-ignore + + move = action((dir: number) => { + this._last_index = this.carouselIndex; + this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0; + }); /** * Goes to the next Doc in the stack subject to the currently selected filter option. @@ -146,10 +115,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; - // if (!curDoc.layout[this.starField]) this.move(1); - // this.layoutDoc._carousel_index = undefined; + const curDoc = this.carouselItems[this.carouselIndex]; + curDoc && (curDoc[this.starField] = curDoc[this.starField] ? undefined : true); }; /* @@ -157,8 +124,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); }; @@ -172,71 +139,92 @@ export class CollectionCarouselView extends CollectionSubView() { onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; - setPracticeMode = (mode: practiceMode) => { + setPracticeMode = (mode: practiceMode | undefined) => { this.layoutDoc.practiceMode = mode; - this.carouselItems?.map(doc => (doc.layout[this.practiceField] = undefined)); - switch (mode) { - case practiceMode.QUIZ: - this.carouselItems?.map(doc => (doc.layout[this.sideField] = undefined)); - break; - case practiceMode.NORMAL: - this.setPracticeMessage(undefined); - break; - } + this.carouselItems?.map(doc => (doc[this.practiceField] = undefined)); + if (mode === practiceMode.QUIZ) this.carouselItems?.map(doc => (doc[this.sideField] = undefined)); }; - setFilterMode = (mode: cardMode) => { - this.layoutDoc.filterOp = mode; - switch (mode) { - case cardMode.STAR: - // Doc.setDocFilter(this.Document, 'data_star', true, 'match'); - this.move(1); - break; - default: - this.setFilterMessage(undefined); // prettier-ignore - // Doc.setDocFilter(this.Document, 'data_star', true, 'remove'); - } - }; contentScreentToLocalXf = () => this._props.ScreenToLocalTransform().translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin)); contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin); + + 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)} + childFilters={this.childDocFilters} + hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} + addDocument={this._props.addDocument} + ScreenToLocalTransform={this.contentScreentToLocalXf} + PanelWidth={this.contentPanelWidth} + PanelHeight={this.contentPanelHeight} + /> + ); + }; + /** + * 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() { - trace(); - if (this.layoutDoc._carousel_index === this.carouselItems.length && this.layoutDoc._carousel_index !== 0) { - this.move(1); - } - 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" style={{ padding: `${NumCast(this.layoutDoc.yMargin)}px ${NumCast(this.layoutDoc.xMargin)}px ${NumCast(this.layoutDoc.yMargin)}px ${NumCast(this.layoutDoc.xMargin)}px` }}> - <DocumentView - {...this._props} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} - setContentViewBox={undefined} - childFilters={this.childDocFilters} - containerViewPath={this._props.docViewPath} - onDoubleClickScript={this.onContentDoubleClick} - onClickScript={this.onContentClick} - hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} - isContentActive={(this._props.childContentsActive ?? this._props.isContentActive() === false) ? returnFalse : emptyFunction} - addDocument={this._props.addDocument} - 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)} - ScreenToLocalTransform={this.contentScreentToLocalXf} - PanelWidth={this.contentPanelWidth} - PanelHeight={this.contentPanelHeight} - /> + <div className="collectionCarouselView-image" key="image"> + {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.overlay} </div> {!carouselShowsCaptions ? null : ( <div @@ -249,17 +237,13 @@ 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> )} </> ); } - containsDifTypes = (): boolean => { - return this.carouselItems.filter(doc => !doc.layout._layout_isFlashcard).length !== 0; - }; - addFlashcard() { const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 }); this.addDocument?.(newDoc); @@ -275,30 +259,28 @@ export class CollectionCarouselView extends CollectionSubView() { } @computed get buttons() { - if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null; + if (!this.carouselItems?.[this.carouselIndex]) return null; return ( <> + <div> + <Tooltip title="star"> + <div key="star" className="carouselView-star" onClick={this.star}> + <FontAwesomeIcon icon="star" color={this.carouselItems[this.carouselIndex]?.[this.starField] ? 'yellow' : 'gray'} size="1x" /> + </div> + </Tooltip> + {/* <Tooltip title="add new flashcard to pile"> + <div key="add" className="carouselView-add" onClick={this.addFlashcard}> + <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> + </div> + </Tooltip> */} + </div> <div key="back" className="carouselView-back" onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> <div key="fwd" className="carouselView-fwd" onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - {!this.containsDifTypes() ? ( - <div> - <Tooltip title="star"> - <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" /> - </div> - </Tooltip> - {/* <Tooltip title="add new flashcard to pile"> - <div key="add" className="carouselView-add" onClick={this.addFlashcard}> - <FontAwesomeIcon icon="plus" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> - </div> - </Tooltip> */} - </div> - ) : null} - {this.layoutDoc.practiceMode === practiceMode.PRACTICE ? ( + {this.practiceMode ? ( <div> <Tooltip title="Incorrect. View again later."> <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)}> @@ -316,33 +298,31 @@ export class CollectionCarouselView extends CollectionSubView() { ); } - togglePracticeMode = (mode: practiceMode) => { - if (mode === this.layoutDoc.practiceMode) { - this.setPracticeMode(practiceMode.NORMAL); - // this.setPracticeMessage("undefined"); - } else this.setPracticeMode(mode); - }; - toggleFilterMode = () => { this.layoutDoc.filterOp === cardMode.STAR ? this.setFilterMode(cardMode.ALL) : this.setFilterMode(cardMode.STAR)}; //prettier-ignore + togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.layoutDoc.practiceMode ? undefined : mode); + toggleFilterMode = () => Doc.setDocFilter(this.Document, 'star', true, 'match', true); setColor = (mode: practiceMode | cardMode, which: string) => { return which === mode ? 'white' : 'light gray'}; //prettier-ignore @computed get menu() { + const curDoc = this.carouselItems?.[this.carouselIndex]; return ( <div className="carouselView-menu"> - <Tooltip title="Practice flashcards using GPT"> - <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}> - <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" /> - </div> - </Tooltip> - <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> - <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}> - <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" /> - </div> - </Tooltip> - <Tooltip title={this.layoutDoc.filterOp === cardMode.STAR ? 'Show all cards' : 'Show only starred cards'}> + <Tooltip title={Doc.hasDocFilter(this.Document, 'star') ? 'Show all cards' : 'Show only starred cards'}> <div key="back" className="carouselView-starFilter" onClick={e => this.toggleFilterMode()}> - <FontAwesomeIcon icon="filter" color={this.setColor(cardMode.STAR, StrCast(this.layoutDoc.filterOp))} size="1x" /> + <FontAwesomeIcon icon="filter" color={Doc.hasDocFilter(this.Document, 'star') ? 'white' : 'lightgray'} size="1x" /> </div> </Tooltip> + <div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}> + <Tooltip title="Practice flashcards using GPT"> + <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}> + <FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.layoutDoc.practiceMode))} size="1x" /> + </div> + </Tooltip> + <Tooltip title={this.layoutDoc.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}> + <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}> + <FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.layoutDoc.practiceMode))} size="1x" /> + </div> + </Tooltip> + </div> </div> ); } @@ -356,36 +336,25 @@ export class CollectionCarouselView extends CollectionSubView() { background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, }}> - {!this._practiceMessage && !this._filterMessage ? ( + {!this.practiceMessage && !this.filterMessage ? ( this.content ) : ( - <p className="message"> - {this._filterMessage} - {'\n'} - {this._practiceMessage} + <p + className="message" + onClick={e => { + if (this.filterMessage) { + this.layoutDoc.practiceMode = undefined; + Doc.setDocFilter(this.layoutDoc, 'star', undefined, 'remove'); + } + this.childDocs.forEach(item => { + item[this.practiceField] = undefined; + }); + }}> + {this.filterMessage || this.practiceMessage} </p> )} - {!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.practiceMode === practiceMode.PRACTICE ? <p className="message">Add flashcards </p> : null} - <p - className="missed-message" - style={{ - color: 'red', - fontWeight: 'bold', - zIndex: '999', - position: 'relative', - left: '10px', - top: '10px', - width: '10px', - display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] - ? this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED && this.layoutDoc.practiceMode === practiceMode.PRACTICE && !this._practiceMessage - ? 'block' - : 'none' - : 'none', - }}> - Recently missed! - </p> - {!this.containsDifTypes() && this.carouselItems.length !== 0 ? this.menu : null} - {this.Document._chromeHidden || (!this._filterMessage && !this._practiceMessage) ? this.buttons : null} + {!this.Document._chromeHidden ? this.menu : null} + {!this.Document._chromeHidden ? this.buttons : null} </div> ); } |
