From 76abb174684f2cd231a0dd9f6b71484c16e0498a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 11 Oct 2024 15:55:19 -0400
Subject: fixes for quiz mode - comparisonbox renderSide fixes. scrolling
 doesn't propagate out of carousel or card views.   fix for text with image
 Doc - now gets saved to UPDATE_CACHE working  set.

---
 src/client/views/collections/CollectionCardDeckView.tsx | 6 ++++++
 1 file changed, 6 insertions(+)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 286df30aa..14ce9d2af 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -46,6 +46,7 @@ export class CollectionCardView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [key: string]: IReactionDisposer } = {};
     private _textToDoc = new Map<string, Doc>();
+    private _oldWheel: HTMLElement | null = null;
     private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center)
     private _clickScript = () => ScriptField.MakeScript('scriptContext._curDoc=this', { scriptContext: 'any' })!;
 
@@ -66,6 +67,10 @@ export class CollectionCardView 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 });
     };
     /**
      * Callback to ensure gpt's text versions of the child docs are updated
@@ -621,6 +626,7 @@ export class CollectionCardView extends CollectionSubView() {
             );
         });
     }
+    onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
 
     contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
     docViewProps = (): DocumentViewProps => ({
-- 
cgit v1.2.3-70-g09d2


From 9779182e74e427d3a8a2004d0efd01fac8387d55 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Oct 2024 17:31:12 -0400
Subject: major fixes to cardDeck view to simplify code and to make arch follow
 a true circle arc and to fix doc sizing when fitwidth/lightbox/etc.  fixes to
 flashcard UI for advancing to next Doc in cardView and carousel3D.

---
 src/client/documents/Documents.ts                  |   4 +-
 src/client/views/DocumentDecorations.tsx           |   4 +-
 .../views/collections/CollectionCardDeckView.scss  |  24 +-
 .../views/collections/CollectionCardDeckView.tsx   | 336 +++++++++++----------
 .../views/collections/CollectionCarousel3DView.tsx |   2 +-
 .../views/collections/CollectionCarouselView.tsx   |   2 +-
 .../views/collections/FlashcardPracticeUI.scss     |   6 +
 .../views/collections/FlashcardPracticeUI.tsx      |   6 +-
 src/client/views/global/globalScripts.ts           |  26 +-
 src/client/views/nodes/ComparisonBox.tsx           |  25 +-
 src/client/views/nodes/DocumentContentsView.tsx    |   1 -
 11 files changed, 230 insertions(+), 206 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 99af1f1a9..e539e3c65 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -508,8 +508,8 @@ export class DocumentOptions {
     userBackgroundColor?: STRt = new StrInfo('background color associated with a Dash user (seen in header fields of shared documents)');
     userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)');
 
-    cardSort?: STRt = new StrInfo('way cards are sorted in deck view');
-    cardSort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
+    card_sort?: STRt = new StrInfo('way cards are sorted in deck view');
+    card_sort_isDesc?: BOOLt = new BoolInfo('whether the cards are sorted ascending or descending');
 }
 
 export const DocOptions = new DocumentOptions();
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 62f2de776..5a48b6c62 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -10,7 +10,7 @@ import { lightOrDark, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
 import { Utils, emptyFunction, numberValue } from '../../Utils';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, HierarchyMapping, ReverseHierarchyMap } from '../../fields/Doc';
-import { AclAdmin, AclAugment, AclEdit, DocData } from '../../fields/DocSymbols';
+import { AclAdmin, AclAugment, AclEdit, Animation, DocData } from '../../fields/DocSymbols';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { ScriptField } from '../../fields/ScriptField';
@@ -650,7 +650,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
         this._forceRender;
         const { b, r, x, y } = this.Bounds;
         const seldocview = DocumentView.Selected().lastElement();
-        if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || SnappingManager.HideDecorations || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
+        if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || seldocview?.Document[Animation] || SnappingManager.HideDecorations || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) {
             setTimeout(
                 action(() => {
                     this._editingTitle = false;
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss
index 0637cd4e9..5283601bf 100644
--- a/src/client/views/collections/CollectionCardDeckView.scss
+++ b/src/client/views/collections/CollectionCardDeckView.scss
@@ -7,23 +7,31 @@
     background-color: white;
     overflow: hidden;
     display: flex;
+    .collectionCardView-inner {
+        display: flex;
+        transform-origin: top left;
+        align-items: center;
+    }
 
     button {
         border-radius: 50%;
     }
 }
 
-.card-wrapper {
-    display: grid;
-    grid-template-columns: repeat(10, 1fr);
+.collectionCardView-flashcardUI {
+    top: 0;
+    position: absolute;
+    width: 100%;
+    height: 100%;
     transform-origin: top left;
+}
 
-    position: absolute;
+.collectionCardView-cardwrapper {
+    display: grid;
+    grid-template-columns: repeat(10, 1fr);
+    transform-origin: left 50%;
     align-items: center;
-    justify-items: center;
-    justify-content: center;
-
-    transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955);
+    z-index: 0; // so that setting z-index of active card doesn't make it land on top of things outside of the card-wrapper
 }
 
 .no-card-span {
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 14ce9d2af..5faabacf4 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
 import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { ScriptField } from '../../../fields/ScriptField';
@@ -96,7 +96,7 @@ export class CollectionCardView extends CollectionSubView() {
                 if (isVis) {
                     this.openChatPopup();
                 } else {
-                    this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort;
+                    this.Document.card_sort = this.cardSort === cardSortings.Chat ? '' : this.Document.card_sort;
                 }
             }
         );
@@ -119,7 +119,25 @@ export class CollectionCardView extends CollectionSubView() {
     }
 
     @computed get cardSort() {
-        return StrCast(this.Document.cardSort) as cardSortings;
+        return StrCast(this.Document.card_sort) as cardSortings;
+    }
+    /**
+     * Number of rows of cards to be rendered
+     */
+    @computed get numRows() {
+        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+    }
+    /**
+     * Circle arc size, in radians, to layout cards
+     */
+    @computed get archAngle() {
+        return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childCards.length < this._maxRowCount ? this.childCards.length / this._maxRowCount : 1);
+    }
+    /**
+     * Spacing card rows as a percent of Doc size.   100 means rows spread out to fill 100% of the Doc vertically.  Default is 60%
+     */
+    @computed get cardSpacing() {
+        return NumCast(this.layoutDoc.card_spacing, 60);
     }
 
     /**
@@ -137,6 +155,10 @@ export class CollectionCardView extends CollectionSubView() {
         return (this.childPanelWidth() * length) / this._props.PanelWidth();
     }
 
+    @computed get nativeScaling() {
+        return this._props.NativeDimScaling?.() || 1;
+    }
+
     /**
      * When in quiz mode, randomly selects a document
      */
@@ -145,70 +167,17 @@ export class CollectionCardView extends CollectionSubView() {
         this._curDoc = this.childDocs[randomIndex];
     });
 
-    /**
-     * Number of rows of cards to be rendered
-     */
-    @computed get numRows() {
-        return Math.ceil(this.sortedDocs.length / this._maxRowCount);
-    }
-
-    @action
-    setHoveredNodeIndex = (index: number) => {
+    setHoveredNodeIndex = action((index: number) => {
         if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
-    };
+    });
 
     isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
-    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
+    childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childCards.length > this._maxRowCount ? this._maxRowCount : this.childCards.length) / this.nativeScaling));
     childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
     isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive();
     isAnyChildContentActive = this._props.isAnyChildContentActive;
 
-    /**
-     * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row
-     * @param amCards
-     * @param index
-     * @returns
-     */
-    rotate = (amCards: number, index: number) => {
-        if (amCards == 1) return 0;
-
-        const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
-        if (amCards % 2 === 0) {
-            if (possRotate === 0) {
-                return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2)));
-            }
-            if (index > (amCards + 1) / 2) {
-                const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
-                return possRotate + stepMag;
-            }
-        }
-
-        return possRotate;
-    };
-    /**
-     * Returns the degree to which a card should be translated in the y direction for the arch effect
-     */
-    translateY = (amCards: number, index: number, realIndex: number) => {
-        const evenOdd = amCards % 2;
-        const apex = (amCards - evenOdd) / 2;
-        const Magnitude = this.childPanelWidth() / 2; // 400
-        const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25);
-
-        let rowOffset = 0;
-        if (realIndex > this._maxRowCount - 1) {
-            rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount);
-        }
-        if (evenOdd === 1 || index < apex - 1) {
-            return Math.abs(stepMag * (apex - index)) - rowOffset;
-        }
-        if (index === apex || index === apex - 1) {
-            return 0 - rowOffset;
-        }
-
-        return Math.abs(stepMag * (apex - index - 1)) - rowOffset;
-    };
-
     /**
      * When dragging a card, determines the index the card should be set to if dropped
      * @param mouseX mouse's x location
@@ -216,28 +185,27 @@ export class CollectionCardView extends CollectionSubView() {
      * @returns the card's new index
      */
     findCardDropIndex = (mouseX: number, mouseY: number) => {
-        const amCardsTotal = this.sortedDocs.length;
+        const cardCount = this.sortedDocs.length;
         let index = 0;
-        const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount;
+        const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount;
 
         // Calculate the adjusted X position accounting for the initial offset
         let adjustedX = mouseX;
 
-        const amRows = Math.ceil(amCardsTotal / this._maxRowCount);
-        const rowHeight = this._props.PanelHeight() / amRows;
+        const rowHeight = this._props.PanelHeight() / this.numRows;
         const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0
 
         if (adjustedX < 0) {
             return 0; // Before the first column
         }
 
-        if (amCardsTotal < this._maxRowCount) {
+        if (cardCount < this._maxRowCount) {
             index = Math.floor(adjustedX / cardWidth);
-        } else if (currRow != amRows - 1) {
+        } else if (currRow != this.numRows - 1) {
             index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
         } else {
-            const rowAmCards = amCardsTotal - currRow * this._maxRowCount;
-            const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth;
+            const cardsInRow = cardCount - currRow * this._maxRowCount;
+            const offset = ((this._maxRowCount - cardsInRow) / 2) * cardWidth;
             adjustedX = mouseX - offset;
 
             index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount;
@@ -246,11 +214,14 @@ export class CollectionCardView extends CollectionSubView() {
     };
 
     /**
-     * Checks to see if a card is being dragged and calls the appropriate methods if so
+     * if pointer moves over cardDeck while dragging a Doc that is in the Deck or that can be dropped in the deck,
+     * then this sets the card index where the dragged card would be added.
      */
     @action
     onPointerMove = (x: number, y: number) => {
-        this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1;
+        if (DragManager.docsBeingDragged.some(doc => this.sortedDocs.includes(doc)) || SnappingManager.CanEmbed) {
+            this._docDraggedIndex = this.findCardDropIndex(x, y);
+        }
     };
 
     /**
@@ -269,7 +240,7 @@ export class CollectionCardView extends CollectionSubView() {
                     const sorted = this.sortedDocs;
                     const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
 
-                    this.Document.cardSort = '';
+                    this.Document.card_sort = '';
                     originalIndex !== -1 && sorted.splice(originalIndex, 1);
                     sorted.splice(dragIndex, 0, draggedDoc);
                     if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
@@ -285,15 +256,6 @@ export class CollectionCardView extends CollectionSubView() {
         ''
     );
 
-    @computed get sortedDocs() {
-        return this.sort(
-            this.childCards.map(card => card.layout),
-            this.cardSort,
-            BoolCast(this.Document.cardSort_isDesc),
-            this._docDraggedIndex
-        );
-    }
-
     /**
      * Used to determine how to sort cards based on tags. The leftmost tags are given lower values while cards to the right are
      * given higher values. Decimals are used to determine placement for cards with multiple tags
@@ -341,6 +303,15 @@ export class CollectionCardView extends CollectionSubView() {
         return docs;
     };
 
+    @computed get sortedDocs() {
+        return this.sort(
+            this.childCards.map(card => card.layout),
+            this.cardSort,
+            BoolCast(this.Document.card_sort_isDesc),
+            this._docDraggedIndex
+        );
+    }
+
     isChildContentActive = computedFn(
         (doc: Doc) => () =>
             this._props.isContentActive?.() === false
@@ -359,74 +330,99 @@ export class CollectionCardView extends CollectionSubView() {
             Document={doc}
             NativeWidth={returnZero}
             NativeHeight={returnZero}
-            fitWidth={returnFalse}
-            onDoubleClickScript={this.onChildDoubleClick}
+            PanelWidth={this.childPanelWidth}
+            PanelHeight={this.childPanelHeight}
             renderDepth={this._props.renderDepth + 1}
             LayoutTemplate={this._props.childLayoutTemplate}
             LayoutTemplateString={this._props.childLayoutString}
             containerViewPath={this.childContainerViewPath}
             ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
             isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
-            PanelWidth={this.childPanelWidth}
-            PanelHeight={this.childPanelHeight}
+            isContentActive={this.isChildContentActive(doc)}
+            fitWidth={returnFalse}
             waitForDoubleClickToClick={returnNever}
             scriptContext={this}
+            onDoubleClickScript={this.onChildDoubleClick}
             onClickScript={this._curDoc === doc ? undefined : this._clickScript}
             dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
             dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
             showTags={BoolCast(this.layoutDoc.showChildTags)}
             whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
-            isContentActive={this.isChildContentActive(doc)}
             dontHideOnDrag
         />
     );
 
     /**
      * Determines how many cards are in the row of a card at a specific index
-     * @param index
-     * @returns
+     * @param index numerical index of card in total list of all cards
+     * @returns number of cards in row that contains index
      */
-    overflowAmCardsCalc = (index: number) => {
-        if (this.sortedDocs.length < this._maxRowCount) {
-            return this.sortedDocs.length;
+    cardsInRowThatIncludesCardIndex = (index: number) => {
+        if (this.childCards.length < this._maxRowCount) {
+            return this.childCards.length;
         }
-        const totalCards = this.sortedDocs.length;
-        // if 9 or less
+        const totalCards = this.childCards.length;
         if (index < totalCards - (totalCards % this._maxRowCount)) {
             return this._maxRowCount;
         }
         return totalCards % this._maxRowCount;
     };
     /**
-     * Determines the index a card is in in a row
-     * @param realIndex
-     * @returns
+     * Determines the index a card is in in a row.  If the row is not full, then the cards
+     * are centered within the row (as if unrendered cards had been added to the start and end
+     * of the row) and the retuned index is the index the card in this virtual full row.
+     * @param index numerical index of card in total list of all cards
+     * @returns index of card in its row, normalized to a full size row
      */
-    overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
+    centeredIndexOfCardInRow = (index: number) => {
+        const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+        const lineIndex = index % this._maxRowCount;
+        if (cardsInRow === this._maxRowCount) return lineIndex;
+        return lineIndex + (this._maxRowCount - cardsInRow) / 2;
+    };
     /**
-     * Translates the cards in the second rows and beyond over to the right
-     * @param realIndex
-     * @param calcIndex
-     * @param calcRowCards
-     * @returns
+     * Returns the rotation of a card in radians based on its horizontal location (and thus m apping to a circle arc).
+     * The amount of rotation is goverend by the Doc's card_arch field which specifies, in degrees, the range of a circle
+     * arc that cards should cover -- by default, -45 to 45 degrees.
+     * @param index numerical index of card in total list of all cards
+     * @returns angle of rotation in radians
+     */
+    rotate = (index: number) => {
+        const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+        const centeredIndexInRow = (cardsInRow < this._maxRowCount ? index + (this._maxRowCount - cardsInRow) / 2 : index) % this._maxRowCount;
+        const rowIndexMax = this._maxRowCount - 1;
+        return ((this.archAngle / 2) * (centeredIndexInRow - rowIndexMax / 2)) / (rowIndexMax / 2);
+    };
+    /**
+     * Provides a vertical adjustment to a card's grid position so that it will lie along an arch.
+     * @param index numerical index of card in total list of all cards
+     */
+    translateY = (index: number) => {
+        const Magnitude = ((this._props.PanelHeight() * this.fitContentScale) / 2) * Math.sqrt(((this.archAngle * (180 / Math.PI)) / 60) * 4);
+        return Magnitude * (1 - Math.sin(this.rotate(index) + Math.PI / 2) - (1 - Math.sin(this.archAngle / 2 + Math.PI / 2)) / 2);
+    };
+    /**
+     * When the card index is for a row (not the first row) that is not full, this returns a horizontal adjustment that centers the row
+     * @param index index of card from start of deck
+     * @param cardsInRow number of cards in the row containing the indexed card
+     * @returns horizontal pixel translation
      */
-    translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));
+    horizontalAdjustmentForPartialRows = (index: number, cardsInRow: number) => (index < this._maxRowCount ? 0 : (this._maxRowCount - cardsInRow) * (this.childPanelWidth() / 2));
 
     /**
-     * Determines how far to translate a card in the y direction depending on its index and if it's selected
+     * Adjusts the vertical placement of the card from its grid position so that it will either line on a
+     * circular arc if the card isn't active, or so that it will be centered otherwise.
      * @param isActive whether the card is focused for interaction
-     * @param realIndex index of card from start of deck
-     * @param amCards ??
-     * @param calcRowIndex index of card from start of row
-     * @returns Y translation of card
+     * @param index index of card from start of deck
+     * @returns vertical pixel translation
      */
-    calculateTranslateY = (isActive: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
-        const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
-        const rowIndex = Math.trunc(realIndex / this._maxRowCount);
+    adjustCardYtoFitArch = (isActive: boolean, index: number) => {
+        const rowHeight = this._props.PanelHeight() / this.numRows;
+        const rowIndex = Math.floor(index / this._maxRowCount);
         const rowToCenterShift = this.numRows / 2 - rowIndex;
-        if (isActive) return rowToCenterShift * rowHeight - rowHeight / 2;
-        if (amCards == 1) return 50 * this.fitContentScale;
-        return this.translateY(amCards, calcRowIndex, realIndex);
+        return isActive
+            ? (rowToCenterShift * rowHeight - rowHeight / 2) * ((this.cardSpacing * this.fitContentScale) / 100) //
+            : this.translateY(index);
     };
 
     /**
@@ -493,7 +489,7 @@ export class CollectionCardView extends CollectionSubView() {
         }
 
         if (questionType === '6') {
-            this.Document.cardSort = 'chat';
+            this.Document.card_sort = 'chat';
         }
 
         listItems.forEach((item, index) => {
@@ -549,7 +545,7 @@ export class CollectionCardView extends CollectionSubView() {
         await this.childPairStringListAndUpdateSortDesc();
     };
 
-    childScreenToLocal = computedFn((doc: Doc, index: number, calcRowIndex: number, isSelected: boolean, amCards: number) => () => {
+    childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => {
         // need to explicitly trigger an invalidation since we're reading everything from the Dom
         this._forceChildXf;
         this._props.ScreenToLocalTransform();
@@ -560,24 +556,40 @@ export class CollectionCardView extends CollectionSubView() {
 
         return new Transform(-translateX + (dref?.centeringX || 0) * scale, 
                              -translateY + (dref?.centeringY || 0) * scale, 1)
-                  .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
+                  .scale(1 / scale).rotate(!isSelected ? -this.rotate(this.centeredIndexOfCardInRow(index)) : 0); // prettier-ignore
     });
 
+    /**
+     * Releases the currently focused Doc by deselecting it and returning it to its location on the arch, and selecting the
+     * cardDeck itself.
+     * This will also force the Doc to recompute its layout transform when the animation completes.
+     * In addition, this sets an animating flag on the Doc so that it will receive no poiner events when animating, such as hover
+     * events that would trigger a flashcard to flip.
+     * @param doc doc that will be animated away from center focus
+     */
+    releaseCurDoc = action(() => {
+        const selDoc = this._curDoc;
+        this._curDoc = undefined;
+        const cardDocView = DocumentView.getDocumentView(selDoc, this.DocumentView?.());
+        if (cardDocView && selDoc) {
+            DocumentView.DeselectView(cardDocView);
+            this._props.select(false);
+            selDoc[Animation] = selDoc; // turns off pointer events & doc decorations while animating - useful for flashcards that reveal back on hover
+            setTimeout(action(() => {
+                selDoc[Animation] = undefined;
+                this._forceChildXf++;
+            }), 350); // prettier-ignore
+        }
+    });
+
+    /**
+     * turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked
+     */
     cardPointerUp = action((doc: Doc) => {
-        // if a card doc has just moved, or a card is selected and in front, then ignore this event
         if (this._curDoc === doc || this._dropped) {
             this._dropped = false;
         } else {
-            // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts.
-            // Turn them back on when the animation has completed and the render and backend structures are in synch
-            SnappingManager.SetHideDecorations(true);
-            setTimeout(
-                action(() => {
-                    SnappingManager.SetHideDecorations(false);
-                    this._forceChildXf++;
-                }),
-                1000
-            );
+            this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc')
         }
     });
 
@@ -585,25 +597,17 @@ export class CollectionCardView extends CollectionSubView() {
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        if (!this.childCards.length) {
-            return null;
-        }
-
+        console.log('CHILDPw = ' + this.childPanelWidth());
         // Map sorted documents to their rendered components
         return this.sortedDocs.map((doc, index) => {
-            const calcRowIndex = this.overflowIndexCalc(index);
-            const amCards = this.overflowAmCardsCalc(index);
+            const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+
+            const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this._curDoc);
 
-            const childScreenToLocal = this.childScreenToLocal(doc, index, calcRowIndex, doc === this._curDoc, amCards);
+            const translateToCenterIfActive = () => (doc === this._curDoc ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
 
-            const translateIfSelected = () => {
-                const indexInRow = index % this._maxRowCount;
-                const rowIndex = Math.trunc(index / this._maxRowCount);
-                const rowCenterIndex = Math.min(this._maxRowCount, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2;
-                return (rowCenterIndex - indexInRow) * 100 - 50;
-            };
             const aspect = NumCast(doc.height) / NumCast(doc.width, 1);
-            const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), 
+            const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), 
                                     (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore
             const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size
             return (
@@ -614,9 +618,9 @@ export class CollectionCardView extends CollectionSubView() {
                     style={{
                         width: this.childPanelWidth(),
                         height: 'max-content',
-                        transform: `translateY(${this.calculateTranslateY(doc === this._curDoc, index, amCards, calcRowIndex)}px)
-                                    translateX(calc(${doc === this._curDoc ? translateIfSelected() : 0}% + ${this.translateOverflowX(index, amCards)}px))
-                                    rotate(${doc !== this._curDoc ? this.rotate(amCards, calcRowIndex) : 0}deg)
+                        transform: `translateY(${this.adjustCardYtoFitArch(doc === this._curDoc, index)}px)
+                                    translateX(calc(${translateToCenterIfActive()}% + ${this.horizontalAdjustmentForPartialRows(index, cardsInRow)}px))
+                                    rotate(${doc !== this._curDoc ? this.rotate(index) : 0}rad)
                                     scale(${doc === this._curDoc ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
                     }} // prettier-ignore
                     onPointerEnter={() => this.setHoveredNodeIndex(index)}
@@ -636,26 +640,19 @@ export class CollectionCardView extends CollectionSubView() {
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
     answered = action(() => {
-        this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(this.curDoc) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
+        this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
     });
     curDoc = () => this._curDoc;
 
     render() {
-        const isEmpty = this.childCards.length === 0;
+        const fitContentScale = this.childCards.length === 0 ? 1 : this.fitContentScale;
         return (
             <div
                 className="collectionCardView-outer"
                 ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
-                onPointerDown={action(() => {
-                    this._curDoc = undefined;
-                    SnappingManager.SetHideDecorations(true);
-                    setTimeout(
-                        action(() => {
-                            SnappingManager.SetHideDecorations(false);
-                            this._forceChildXf++;
-                        }),
-                        1000
-                    );
+                onPointerDown={action(e => {
+                    if (e.button === 2 || e.ctrlKey) return;
+                    this.releaseCurDoc();
                 })}
                 onPointerLeave={action(() => (this._docDraggedIndex = -1))}
                 onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
@@ -665,16 +662,31 @@ export class CollectionCardView extends CollectionSubView() {
                     color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
                 }}>
                 <div
-                    className="card-wrapper"
+                    className="collectionCardView-inner"
                     style={{
-                        ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
-                        ...{ height: `${100 * (isEmpty ? 1 : this.fitContentScale)}%` },
-                        ...{ width: `${100 * (isEmpty ? 1 : this.fitContentScale)}%` },
-                        gridAutoRows: `${100 / this.numRows}%`,
+                        transform: `scale(${1 / fitContentScale})`,
+                        height: `${100 * fitContentScale}%`,
+                        width: `${100 * fitContentScale}%`,
                     }}>
-                    {this.renderCards}
+                    <div
+                        className="collectionCardView-cardwrapper"
+                        style={{
+                            gridAutoRows: `${100 / this.numRows}%`,
+                            height: `${this.cardSpacing}%`,
+                        }}>
+                        {this.renderCards}
+                    </div>
+                    <div
+                        className="collectionCardView-flashcardUI"
+                        style={{
+                            pointerEvents: this.childCards.length === 0 ? undefined : 'none',
+                            height: `${100 / this.nativeScaling / fitContentScale}%`,
+                            width: `${100 / this.nativeScaling / fitContentScale}%`,
+                            transform: `scale(${this.nativeScaling * fitContentScale})`,
+                        }}>
+                        {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+                    </div>
                 </div>
-                {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index e9ace733e..a71cc43ba 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -205,7 +205,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
 
     onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
     curDoc = () => this.carouselItems[NumCast(this.layoutDoc._carousel_index)]?.layout;
-    answered = (correct: boolean) => (!correct || !this.curDoc()) && this.changeSlide(1);
+    answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.changeSlide(1);
     docViewProps = () => ({
         ...this._props, //
         isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index ff587b199..1f2bc908f 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -203,7 +203,7 @@ export class CollectionCarouselView extends CollectionSubView() {
         isContentActive: this.isChildContentActive,
         ScreenToLocalTransform: this.contentScreenToLocalXf,
     });
-    answered = () => this.advance();
+    answered = (correct: boolean) => (!correct || !this.curDoc() || NumCast(this.layoutDoc._carousel_index) === this.carouselItems.length - 1) && this.advance();
 
     render() {
         return (
diff --git a/src/client/views/collections/FlashcardPracticeUI.scss b/src/client/views/collections/FlashcardPracticeUI.scss
index 4ed27793d..210c6798f 100644
--- a/src/client/views/collections/FlashcardPracticeUI.scss
+++ b/src/client/views/collections/FlashcardPracticeUI.scss
@@ -1,3 +1,9 @@
+.FlashcardPracticeUI {
+    width: 100%;
+    height: 100%;
+    display: flex;
+    align-items: center;
+}
 .FlashcardPracticeUI-remove,
 .FlashcardPracticeUI-check {
     position: absolute;
diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx
index 79eb7f107..ec892ee3d 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -104,8 +104,8 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
         const setPracticeVal = (e: React.MouseEvent, val: string) => {
             e.stopPropagation();
             const curDoc = this._props.curDoc();
-            curDoc && (curDoc[this.practiceField] = val);
             this._props.advance?.(val === practiceVal.CORRECT);
+            curDoc && (curDoc[this.practiceField] = val);
         };
 
         return this.practiceMode == practiceMode.PRACTICE && this._props.curDoc() ? (
@@ -182,7 +182,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
     tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._layout_isFlashcard) && doc[this.practiceField] === practiceVal.CORRECT ? true : false); // show only cards that aren't marked as correct
     render() {
         return (
-            <>
+            <div className="FlashcardPracticeUI">
                 {this.emptyMessage}
                 {this.practiceButtons}
                 {this._props.layoutDoc._chromeHidden ? null : (
@@ -208,7 +208,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
                         {this.practiceModesMenu}
                     </div>
                 )}
-            </>
+            </div>
         );
     }
 }
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 903e04ad7..954c79f7d 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -189,38 +189,38 @@ ScriptingGlobals.add(function showFreeform(
             setDoc: (doc: Doc, dv: DocumentView) => { Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards}, // prettier-ignore
         }],
         ['time', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "time",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "time" ? doc.cardSort = '' : doc.cardSort = 'time'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "time",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "time" ? doc.card_sort = '' : doc.card_sort = 'time'}, // prettier-ignore
         }],
         ['docType', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "type",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "type" ? doc.cardSort = '' : doc.cardSort = 'type'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "type",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "type" ? doc.card_sort = '' : doc.card_sort = 'type'}, // prettier-ignore
         }],
         ['color', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "color",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "color" ? doc.cardSort = '' : doc.cardSort = 'color'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "color",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "color" ? doc.card_sort = '' : doc.card_sort = 'color'}, // prettier-ignore
         }],
         ['tag', {
-            checkResult: (doc: Doc) => StrCast(doc?.cardSort) === "tag",
-            setDoc: (doc: Doc, dv: DocumentView) => { doc.cardSort === "tag" ? doc.cardSort = '' : doc.cardSort = 'tag'}, // prettier-ignore
+            checkResult: (doc: Doc) => StrCast(doc?.card_sort) === "tag",
+            setDoc: (doc: Doc, dv: DocumentView) => { doc.card_sort === "tag" ? doc.card_sort = '' : doc.card_sort = 'tag'}, // prettier-ignore
         }],
         ['up', {
-            checkResult: (doc: Doc) => BoolCast(!doc?.cardSort_isDesc),
+            checkResult: (doc: Doc) => BoolCast(!doc?.card_sort_isDesc),
             setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.cardSort_isDesc = false;
+                doc.card_sort_isDesc = false;
             },
         }],
         ['down', {
-            checkResult: (doc: Doc) => BoolCast(doc?.cardSort_isDesc),
+            checkResult: (doc: Doc) => BoolCast(doc?.card_sort_isDesc),
             setDoc: (doc: Doc, dv: DocumentView) => {
-                doc.cardSort_isDesc = true;
+                doc.card_sort_isDesc = true;
             },
         }],
         ['toggle-chat', {
             checkResult: (doc: Doc) => GPTPopup.Instance.visible,
             setDoc: (doc: Doc, dv: DocumentView) => {
                 if (GPTPopup.Instance.visible){
-                    doc.cardSort = ''
+                    doc.card_sort = ''
                     GPTPopup.Instance.setVisible(false);
    
                 } else {
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 38ce5f2f7..3c126ea4a 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -8,7 +8,7 @@ import ReactLoading from 'react-loading';
 import { imageUrlToBase64, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
 import { Doc, Opt } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { Animation, DocData } from '../../../fields/DocSymbols';
 import { RichTextField } from '../../../fields/RichTextField';
 import { BoolCast, DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
 import { nullAudio } from '../../../fields/URLField';
@@ -18,7 +18,6 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { SnappingManager } from '../../util/SnappingManager';
 import { undoable } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
@@ -235,7 +234,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
     @computed get uiBtnScaling() { return Math.max(this.maxWidgetSize / this._sideBtnWidth, 1) * Math.min(1, this.viewScaling)* (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
 
     @computed get flashcardMenu() {
-        return SnappingManager.HideDecorations ? null : (
+        return (
             <div className="comparisonBox-bottomMenu" style={{ transform: `scale(${this.uiBtnScaling})` }}>
                 {this.revealOpHover || !this._props.isSelected() ? null : this.overlayAlternateIcon}
                 {!this._props.isSelected() || this._renderSide === this.frontKey ? null : (
@@ -666,20 +665,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             <>
                 <DocumentView
                     {...this._props}
-                    showTags={undefined}
-                    fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
-                    ignoreUsePath={layoutString ? true : undefined}
-                    renderDepth={this.props.renderDepth + 1}
-                    LayoutTemplateString={layoutString}
                     Document={layoutString ? this.Document : targetDoc}
-                    containerViewPath={this._props.docViewPath}
-                    moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
-                    removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
                     NativeWidth={returnZero}
                     NativeHeight={returnZero}
+                    renderDepth={this.props.renderDepth + 1}
+                    LayoutTemplateString={layoutString}
+                    containerViewPath={this._props.docViewPath}
                     ScreenToLocalTransform={this.contentScreenToLocalXf}
-                    isContentActive={this.childActiveFunc}
                     isDocumentActive={returnFalse}
+                    isContentActive={this.childActiveFunc}
+                    showTags={undefined}
+                    fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
+                    ignoreUsePath={layoutString ? true : undefined}
+                    moveDocument={whichSlot === this.frontKey ? this.moveDocFront : this.moveDocBack}
+                    removeDocument={whichSlot === this.frontKey ? this.remDocFront : this.remDocBack}
                     dontSelect={returnTrue}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
@@ -795,7 +794,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
             [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
         if (this.isQuizMode) this.renderAsQuiz(this.frontText);
         return (
-            <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
+            <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() && !this.Document[Animation] ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
                 {renderMode.get(this.revealOp)?.() ?? null}
                 {this.loading ? (
                     <div className="loading-spinner">
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 9aa000ba7..aab8a183a 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/require-default-props */
 import { computed, makeObservable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-- 
cgit v1.2.3-70-g09d2


From dd93f5175064850c6c0e47f025cd7bbba1f23106 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 17 Oct 2024 17:19:11 -0400
Subject: fixed quizMode to actually render and to get pointer events.

---
 src/client/views/collections/CollectionCardDeckView.tsx | 14 +++++++++++++-
 src/client/views/nodes/ComparisonBox.scss               |  3 +++
 src/client/views/nodes/ComparisonBox.tsx                |  5 +++--
 3 files changed, 19 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 5faabacf4..b86dad9d7 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -4,7 +4,7 @@ import { computedFn } from 'mobx-utils';
 import * as React from 'react';
 import { ClientUtils, DashColor, imageUrlToBase64, returnFalse, returnNever, returnZero } from '../../../ClientUtils';
 import { emptyFunction } from '../../../Utils';
-import { Doc } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Animation, DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -24,6 +24,7 @@ import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
 
 enum cardSortings {
     Time = 'time',
@@ -342,6 +343,7 @@ export class CollectionCardView extends CollectionSubView() {
             fitWidth={returnFalse}
             waitForDoubleClickToClick={returnNever}
             scriptContext={this}
+            focus={this.focus}
             onDoubleClickScript={this.onChildDoubleClick}
             onClickScript={this._curDoc === doc ? undefined : this._clickScript}
             dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
@@ -593,6 +595,16 @@ export class CollectionCardView extends CollectionSubView() {
         }
     });
 
+    focus = action((anchor: Doc, options: FocusViewOptions): Opt<number> => {
+        const docs = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]);
+        if (anchor.type !== DocumentType.CONFIG && !docs.includes(anchor)) return undefined;
+        options.didMove = true;
+        const target = DocCast(anchor.annotationOn) ?? anchor;
+        const index = docs.indexOf(target);
+        index !== -1 && (this._curDoc = target);
+        return undefined;
+    });
+
     /**
      * Actually renders all the cards
      */
diff --git a/src/client/views/nodes/ComparisonBox.scss b/src/client/views/nodes/ComparisonBox.scss
index d1cc48051..d2ba9796b 100644
--- a/src/client/views/nodes/ComparisonBox.scss
+++ b/src/client/views/nodes/ComparisonBox.scss
@@ -236,6 +236,9 @@
         }
     }
 }
+.comparisonBox-interactive {
+    pointer-events: all;
+}
 
 .comparisonBox-explain {
     position: absolute;
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 3c126ea4a..f6c33d6ba 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -792,8 +792,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
         const renderMode = new Map<flashcardRevealOp, () => JSX.Element>([
             [flashcardRevealOp.FLIP, this.renderAsFlip],
             [flashcardRevealOp.SLIDE, this.renderAsBeforeAfter]]); // prettier-ignore
-        if (this.isQuizMode) this.renderAsQuiz(this.frontText);
-        return (
+        return this.isQuizMode ? (
+            this.renderAsQuiz(this.frontText)
+        ) : (
             <div className="comparisonBox" style={{ pointerEvents: this._props.isContentActive() && !this.Document[Animation] ? 'unset' : undefined }} onContextMenu={this.flashcardContextMenu}>
                 {renderMode.get(this.revealOp)?.() ?? null}
                 {this.loading ? (
-- 
cgit v1.2.3-70-g09d2

