From 62eb66ca7d3404f9977acdf73f815f4920fb964d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 16 Sep 2024 18:14:56 -0400
Subject: fixed doc decorations from crashing when dockingView is selected.
 cleaned up tab doc view.

---
 src/client/views/collections/TabDocView.tsx | 175 ++++++++++++----------------
 1 file changed, 72 insertions(+), 103 deletions(-)

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

diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 31b6be927..def1ea731 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -57,6 +57,7 @@ interface TabMiniThumbProps {
     miniLeft: () => number;
 }
 
+export type TabHTMLElement = HTMLDivElement & { InitTab?: (tab: object) => void };
 @observer
 class TabMiniThumb extends React.Component<TabMiniThumbProps> {
     render() {
@@ -193,8 +194,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             .filter(tv => tv._document)
             .map(tv => tv._document!);
     }
-    _mainCont: HTMLDivElement | null = null;
+    _mainCont: TabHTMLElement | null = null;
     _tabReaction: IReactionDisposer | undefined;
+    _lastSelection = 0; // time when view was last selected - used to re-select views that get invalidated when selected
 
     /**
      * Adds a document to the presentation view
@@ -273,19 +275,24 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
     }
 
+    // Flag indicating that when a tab is activated, it should not select it's document.
+    // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected)
+    public static DontSelectOnActivate = 'dontSelectOnActivate';
+
+    public static IsSelected = (doc?: Doc) => {
+        return DocumentView.getViews(doc).some(dv => dv?.IsSelected);
+    };
+
     static Activate = (tabDoc: Doc) => {
         const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
         tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         return tab !== undefined;
     };
-    // static ActivateTabView(doc: Doc) {
-    //     const tabView = Array.from(TabDocView._allTabs).find(view => view._document === doc);
-    //     if (!tabView?._activated && tabView?._document) {
-    //         TabDocView.Activate(tabView?._document);
-    //         return tabView;
-    //     }
-    //     return undefined;
-    // }
+
+    get stack()           { return this._props.glContainer.parent.parent; } // prettier-ignore
+    get tab()             { return this._props.glContainer.tab;  } // prettier-ignore
+    get view()            { return this._view;  } // prettier-ignore
+
     constructor(props: TabDocViewProps) {
         super(props);
         makeObservable(this);
@@ -299,37 +306,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     @observable _hovering = false;
     @observable _isActive: boolean = false;
     @observable _isAnyChildContentActive = false;
-    public static IsSelected = (doc?: Doc) => {
-        if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) {
-            return true;
-        }
-        return false;
-    };
-    @computed get _isUserActivated() {
-        return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive;
-    }
-    get _isContentActive() {
-        return this._isUserActivated || this._hovering;
-    }
     @observable _document: Doc | undefined = undefined;
     @observable _view: DocumentView | undefined = undefined;
+    @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver.  this hack allows the resize observer to trigger a react update
 
-    @computed get layoutDoc() {
-        return this._document && Doc.Layout(this._document);
-    }
-
-    get stack() {
-        return this._props.glContainer.parent.parent;
-    }
-    get tab() {
-        return this._props.glContainer.tab;
-    }
-    get view() {
-        return this._view;
-    }
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    _lastTab: any;
-    _lastView: DocumentView | undefined;
+    @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore
+    @computed get isUserActivated() {  return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore
+    @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore
 
     @action
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -413,7 +396,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                             color === variant
                                 ? DashColor(color)
                                       .fade(
-                                          this._isUserActivated
+                                          this.isUserActivated
                                               ? 0
                                               : this._hovering
                                                 ? 0.25
@@ -522,19 +505,14 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         this._props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged);
     }
 
-    // Flag indicating that when a tab is activated, it should not select it's document.
-    // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected)
-    public static DontSelectOnActivate = 'dontSelectOnActivate';
-
-    @action.bound
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    private onActiveContentItemChanged(contentItem: any) {
+    onActiveContentItemChanged = (contentItem: any) => {
         if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
             this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
             if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false));
             !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
         }
-    }
+    };
 
     // adds a tab to the layout based on the locaiton parameter which can be:
     //  close[:{left,right,top,bottom}]  - e.g., "close" will close the tab, "close:left" will close the left tab,
@@ -551,7 +529,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
         const panelName = whereFields.length > 1 ? whereFields.lastElement() : '';
         if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]);
-        // prettier-ignore
         switch (whereFields[0]) {
             case undefined:
             case OpenWhere.lightbox:   return LightboxView.Instance.AddDocTab(docs[0], location);
@@ -559,7 +536,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
             case OpenWhere.replace:    return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue);
             case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue);
             case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue);
-        }
+        } // prettier-ignore
     };
     remDocTab = (doc: Doc | Doc[]) => {
         if (doc === this._document) {
@@ -571,8 +548,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     };
 
     getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame);
-
-    @action
     focusFunc = () => {
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
             this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
@@ -580,7 +555,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
         return undefined;
     };
     active = () => this._isActive;
-    @observable _forceInvalidateScreenToLocal = 0;
     ScreenToLocalTransform = () => {
         this._forceInvalidateScreenToLocal;
         const { translateX, translateY } = ClientUtils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement);
@@ -594,47 +568,44 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     whenChildContentActiveChanges = (isActive: boolean) => {
         this._isAnyChildContentActive = isActive;
     };
-    isContentActive = () => this._isContentActive;
+    isContentActiveFunc = () => this.isContentActive;
     waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined);
-    @computed get docView() {
-        return !this._activated || !this._document ? null : (
-            <>
-                <DocumentView
-                    key={this._document[Id]}
-                    ref={action((r: DocumentView) => {
-                        this._lastView && DocumentView.removeView(this._lastView);
-                        this._view = r;
-                        this._lastView = this._view;
-                    })}
-                    renderDepth={0}
-                    LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined}
-                    hideTitle={this._props.keyValue}
-                    Document={this._document}
-                    TemplateDataDocument={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined}
-                    waitForDoubleClickToClick={this.waitForDoubleClick}
-                    isContentActive={this.isContentActive}
-                    isDocumentActive={returnFalse}
-                    PanelWidth={this.PanelWidth}
-                    PanelHeight={this.PanelHeight}
-                    styleProvider={DefaultStyleProvider}
-                    childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter}
-                    childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter}
-                    searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
-                    addDocument={undefined}
-                    removeDocument={this.remDocTab}
-                    addDocTab={this.addDocTab}
-                    suppressSetHeight={!!this._document._layout_fitWidth}
-                    ScreenToLocalTransform={this.ScreenToLocalTransform}
-                    dontCenter="y"
-                    whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
-                    focus={this.focusFunc}
-                    containerViewPath={returnEmptyDocViewList}
-                    pinToPres={TabDocView.PinDoc}
-                />
-                {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />}
-            </>
-        );
-    }
+    renderDocView = (doc: Doc) => (
+        <DocumentView
+            key={doc[Id]}
+            ref={action((r: DocumentView) => {
+                const now = Date.now();
+                this._lastSelection = this._view?.IsSelected ? now : this._lastSelection;
+                if (this._view) DocumentView.removeView(this._view);
+                this._view = r;
+                if (this._view && now - this._lastSelection < 1000) this._view.select(false);
+            })}
+            renderDepth={0}
+            LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined}
+            hideTitle={this._props.keyValue}
+            Document={doc}
+            TemplateDataDocument={!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined}
+            waitForDoubleClickToClick={this.waitForDoubleClick}
+            isContentActive={this.isContentActiveFunc}
+            isDocumentActive={returnFalse}
+            PanelWidth={this.PanelWidth}
+            PanelHeight={this.PanelHeight}
+            styleProvider={DefaultStyleProvider}
+            childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter}
+            childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter}
+            searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist}
+            addDocument={undefined}
+            removeDocument={this.remDocTab}
+            addDocTab={this.addDocTab}
+            suppressSetHeight={!!doc._layout_fitWidth}
+            ScreenToLocalTransform={this.ScreenToLocalTransform}
+            dontCenter="y"
+            whenChildContentsActiveChanged={this.whenChildContentActiveChanges}
+            focus={this.focusFunc}
+            containerViewPath={returnEmptyDocViewList}
+            pinToPres={TabDocView.PinDoc}
+        />
+    );
 
     render() {
         return (
@@ -647,23 +618,21 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
                 onPointerLeave={action(() => { this._hovering = false; })} // prettier-ignore
                 onDragOver={action(() =>     { this._hovering = true; })} // prettier-ignore
                 onDragLeave={action(() =>    { this._hovering = false; })} // prettier-ignore
-                ref={ref => {
+                ref={(ref: TabHTMLElement) => {
+                    // "add" an InitTab function to this div to call from tabCreated in CollectionDockingView when div is reused
                     this._mainCont = ref;
                     if (this._mainCont) {
-                        if (this._lastTab) {
-                            this._view && DocumentView.removeView(this._view);
-                        }
-                        this._lastTab = this.tab;
-                        (this._mainCont as { InitTab?: (tab: object) => void }).InitTab = (tab: object) => this.init(tab, this._document);
-                        DocServer.GetRefField(this._props.documentId).then(
-                            action(doc => {
-                                doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
-                            })
-                        );
-                        ref && new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref);
+                        this._mainCont.InitTab = (tab: object) => this.init(tab, this._document);
+                        DocServer.GetRefField(this._props.documentId).then(action(doc => {
+                            doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document);
+                        })); // prettier-ignore
+                        new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(this._mainCont);
                     }
                 }}>
-                {this.docView}
+                {!this._activated || !this._document ? null : this.renderDocView(this._document)}
+                {this.disableMinimap() || !this._document ? null : (
+                    <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />
+                )}
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


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/CollectionCarousel3DView.tsx |   1 +
 .../views/collections/CollectionCarouselView.scss  |  15 ++
 .../views/collections/CollectionCarouselView.tsx   | 223 ++++++++++++---------
 src/client/views/collections/TabDocView.tsx        |   4 +-
 4 files changed, 151 insertions(+), 92 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index c799eb3c8..54cc02825 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -78,6 +78,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
                 NativeWidth={returnZero}
                 NativeHeight={returnZero}
                 fitWidth={undefined}
+                containerViewPath={this.childContainerViewPath}
                 onDoubleClickScript={this.onChildDoubleClick}
                 renderDepth={this._props.renderDepth + 1}
                 LayoutTemplate={this._props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss
index f115bb40a..01b20d6d3 100644
--- a/src/client/views/collections/CollectionCarouselView.scss
+++ b/src/client/views/collections/CollectionCarouselView.scss
@@ -12,6 +12,21 @@
         user-select: none;
     }
 }
+.collectionCarouselView-addFlashcards {
+    justify-content: center;
+    align-items: center;
+    height: 100%;
+    z-index: -1;
+    pointer-events: none;
+}
+.collectionCarouselView-recentlyMissed {
+    color: red;
+    z-index: 999;
+    position: relative;
+    left: 10px;
+    top: 10px;
+    pointer-events: none;
+}
 .carouselView-back,
 .carouselView-fwd,
 .carouselView-star,
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>
         );
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index def1ea731..f56ea9d76 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -506,13 +506,13 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
     }
 
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    onActiveContentItemChanged = (contentItem: any) => {
+    onActiveContentItemChanged = action((contentItem: any) => {
         if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) {
             this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab;
             if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false));
             !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one.
         }
-    };
+    });
 
     // adds a tab to the layout based on the locaiton parameter which can be:
     //  close[:{left,right,top,bottom}]  - e.g., "close" will close the tab, "close:left" will close the left tab,
-- 
cgit v1.2.3-70-g09d2

