diff options
Diffstat (limited to 'src/client/views/collections/CollectionCarouselView.tsx')
-rw-r--r-- | src/client/views/collections/CollectionCarouselView.tsx | 243 |
1 files changed, 102 insertions, 141 deletions
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 8b3a699ed..a7d217076 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,64 +1,41 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +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, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; -import { ContextMenu } from '../ContextMenu'; +import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; -import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; +import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -enum cardMode { - PRACTICE = 'practice', - STAR = 'star', - 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 starField() { return "#star"; } // prettier-ignore + _oldWheel: HTMLElement | null = null; _fadeTimer: NodeJS.Timeout | undefined; - _resetter: IReactionDisposer | 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; - - 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 - ); + componentDidMount(): void { + this._props.setContentViewBox?.(this); } componentWillUnmount() { this._dropDisposer?.(); - this._resetter?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { @@ -66,96 +43,78 @@ export class CollectionCarouselView extends CollectionSubView() { if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }; - @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 => { - 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 - }); - } + @computed get carouselItems() { return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg); } // prettier-ignore + /** + * Move forward or backward the specified number of Docs + * @param dir signed number indicating Docs to move forward or backward + */ move = action((dir: number) => { this._last_index = this.carouselIndex; - this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length; + 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. */ - advance = (e: React.MouseEvent) => { - e.stopPropagation(); - this.move(1); - }; + advance = () => this.move(1); /** * Goes to the previous Doc in the stack subject to the currently selected filter option. */ - goback = (e: React.MouseEvent) => { - e.stopPropagation(); - this.move(-1); - }; + goback = () => this.move(-1); - /* - * Stars the document when the star button is pressed. - */ - star = (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); + curDoc = () => this.carouselItems[this.carouselIndex]?.layout; + + focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => { + const docs = DocListCast(this.Document[this.fieldKey]); + if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) { + const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor)); + options.didMove = newIndex !== this.layoutDoc._carousel_index; + options.didMove && (this.layoutDoc._carousel_index = newIndex); } + return undefined; }; - /* - * 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); + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex }); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document); + addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered + return anchor; }; + addDocTab = this.addLinkedDocTab; 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); }; - panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0); + contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling(); + contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling(); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); - 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 - revealItems.push({description: 'Star', event: () => {this.layoutDoc.filterOp = cardMode.STAR;}, icon: 'star',}); // prettier-ignore - revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = cardMode.PRACTICE;}, icon: 'check',}); // prettier-ignore - 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' }); - }; - + captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX; + contentScreenToLocalXf = () => + this._props + .ScreenToLocalTransform() // + .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling()); isChildContentActive = () => this._props.isContentActive?.() === false ? false - : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + : this._props.isContentActive() ? true : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false - : undefined; - - childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); - + : undefined; // prettier-ignore + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( <DocumentView @@ -165,10 +124,10 @@ export class CollectionCarouselView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={this._props.childLayoutFitWidth} + hideFilterStatus={true} showTags={BoolCast(this.layoutDoc.showChildTags)} containerViewPath={this.childContainerViewPath} setContentViewBox={undefined} - ScreenToLocalTransform={this.childScreenToLocalXf} onDoubleClickScript={this.onContentDoubleClick} onClickScript={this.onContentClick} isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} @@ -178,8 +137,14 @@ export class CollectionCarouselView extends CollectionSubView() { LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} - xPadding={35} - PanelHeight={this.panelHeight} + childFilters={this.childDocFilters} + focus={this.focus} + hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} + addDocument={this._props.addDocument} + ScreenToLocalTransform={this.contentScreenToLocalXf} + PanelWidth={this.contentPanelWidth} + PanelHeight={this.contentPanelHeight} + screenXPadding={this.screenXPadding} /> ); }; @@ -188,9 +153,9 @@ export class CollectionCarouselView extends CollectionSubView() { */ @computed get overlay() { const fadeTime = 500; - const lastDoc = this.carouselItems?.[this._last_index]; + const lastDoc = this.carouselItems?.[this._last_index]?.layout; 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 @@ -211,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 : ( @@ -229,68 +197,61 @@ 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> )} </> ); } - @computed get buttons() { - if (!this.carouselItems?.[this.carouselIndex]) return null; - return ( + + @computed get navButtons() { + return !this.curDoc() ? null : ( <> - <div key="back" className="carouselView-back" onClick={this.goback}> + <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> - <div key="fwd" className="carouselView-fwd" onClick={this.advance}> + <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </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" /> - </div> - <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> - <FontAwesomeIcon icon="check" color="green" size="1x" /> - </div> </> ); } - /** - * 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 + nativeScaling = () => this._props.NativeDimScaling?.() || 1; - /** - * 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 + docViewProps = () => ({ + ...this._props, // + isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, + isContentActive: this.isChildContentActive, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.advance(); render() { return ( - <div - className="collectionCarouselView-outer" - ref={this.createDashEventsTarget} - onContextMenu={this.specificMenu} - 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, - }}> - {this.content} - {this.renderAddFlashcards()} - {this.renderRecentlyMissed()} - {this.Document._chromeHidden ? null : this.buttons} + <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, + left: NumCast(this.layoutDoc._xMargin), + top: NumCast(this.layoutDoc._yMargin), + transformOrigin: 'top left', + transform: `scale(${this.nativeScaling()})`, + width: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._xMargin)) / this.nativeScaling()}px)`, + height: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._yMargin)) / this.nativeScaling()}px)`, + position: 'relative', + }}> + {this.content} + {this.navButtons} + </div> + {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} </div> ); } |