import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 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 { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; 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'; @observer export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; _fadeTimer: NodeJS.Timeout | undefined; @observable _last_index = this.carouselIndex; @observable _last_opacity = 1; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } componentDidMount(): void { this._props.setContentViewBox?.(this); } componentWillUnmount() { this._dropDisposer?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } this.fixWheelEvents(ele, this._props.isContentActive); }; @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.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.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 = () => this.move(1); /** * Goes to the previous Doc in the stack subject to the currently selected filter option. */ goback = () => this.move(-1); curDoc = () => this.carouselItems[this.carouselIndex]?.layout; focus = (anchor: Doc, options: FocusViewOptions): Opt => { const docs = DocListCast(this.Document[this.fieldKey]); if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) { const annoOn = DocCast(anchor.annotationOn, anchor); const newIndex = NumCast(anchor.config_carousel_index, (annoOn && docs.getIndex(annoOn)) ?? 0); options.didMove = newIndex !== this.layoutDoc._carousel_index; options.didMove && (this.layoutDoc._carousel_index = newIndex); } return undefined; }; 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, 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)) / 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.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.isContentActive() ? true : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; // prettier-ignore 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]?.layout; 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 renderedDoc() { const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); return this.renderDoc(this.curDoc(), !!carouselShowsCaptions); } @computed get content() { const captionProps = { ...this._props, // NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined, noSidebar: true, }; const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); return !this.curDoc() ? null : ( <>
{this.renderedDoc} {this.overlay}
{!carouselShowsCaptions ? null : (
)} ); } @computed get navButtons() { return !this.curDoc() ? null : ( <>
); } nativeScaling = () => this._props.NativeDimScaling?.() || 1; 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 (
{this.content} {this.navButtons}
{this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
); } }