From 4f2ee4a8642a93fb399b979750078374b317af32 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 17 Sep 2024 12:29:48 -0400
Subject: fixed carouselfView to fade.  cleaned up code a bit

---
 .../views/collections/CollectionCarouselView.tsx   | 223 ++++++++++++---------
 1 file changed, 133 insertions(+), 90 deletions(-)

(limited to 'src/client/views/collections/CollectionCarouselView.tsx')

diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 4bec2d963..5d71177c3 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 this.fieldKey + "_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>
         );
-- 
cgit v1.2.3-70-g09d2

