import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } 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 { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { StyleProp } from '../StyleProp'; 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 { TagItem } from '../TagsView'; import { tickStep } from 'd3'; 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 _fadeTimer: NodeJS.Timeout | undefined; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @observable _last_index = this.carouselIndex; @observable _last_opacity = 1; componentWillUnmount() { this._dropDisposer?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } }; @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 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 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.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); }; /** * Goes to the previous Doc in the stack subject to the currently selected filter option. */ goback = (e: React.MouseEvent) => { e.stopPropagation(); 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)); }; captionStyleProvider = (doc: Doc | undefined, captionProps: Opt, 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); }; 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; 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; childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( ); }; /** * 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 : (
{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 ); } }) )}
); } @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 : ( <>
{this.renderDoc(curDoc, !!carouselShowsCaptions)} {this.overlay}
{!carouselShowsCaptions ? null : (
)} ); } addFlashcard() { const newDoc = Docs.Create.ComparisonDocument('', { _layout_isFlashcard: true, _width: 300, _height: 300 }); this.addDocument?.(newDoc); } @computed get buttons() { if (!this.carouselItems?.[this.carouselIndex]) return null; return ( <>
{/*
*/}
{this.practiceMode == practiceMode.PRACTICE ? (
this.setPracticeVal(e, practiceVal.MISSED)}>
this.setPracticeVal(e, practiceVal.CORRECT)}>
) : null} ); } 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); filterWidth = () => (!this.filterDoc ? 1 : (this.filterHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height)); @computed get menu() { const curDoc = this.carouselItems?.[this.carouselIndex]; return (
{!this.filterDoc ? null : (
)}
this.togglePracticeMode(practiceMode.QUIZ)}>
this.togglePracticeMode(practiceMode.PRACTICE)}>
); } render() { return (
{!this.practiceMessage && !this.filterMessage ? ( this.content ) : (

{ if (this.filterMessage || this.practiceMessage) { this.setPracticeMode(undefined); Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove'); } }}> {this.filterMessage || this.practiceMessage}

)}
{!this.Document._chromeHidden ? this.menu : null} {!this.Document._chromeHidden ? this.buttons : null}
); } }