From f63aa4c65f6336c7708c50ced83867702a310240 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 4 Oct 2024 18:39:15 -0400
Subject: cleaning up CardDeckView so that animations work smoothly

---
 .../views/collections/CollectionCardDeckView.tsx   | 199 +++++++++------------
 1 file changed, 84 insertions(+), 115 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 101cc8082..a9ab9de26 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -2,7 +2,6 @@ import { IReactionDisposer, ObservableMap, action, computed, makeObservable, obs
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
-import { emptyFunction } from '../../../Utils';
 import { Doc } from '../../../fields/Doc';
 import { DocData } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
@@ -13,7 +12,6 @@ import { gptImageLabel } from '../../apis/gpt/GPT';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
 import { undoable } from '../../util/UndoManager';
@@ -23,12 +21,12 @@ import { DocumentView } from '../nodes/DocumentView';
 import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
 import './CollectionCardDeckView.scss';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
+import { computedFn } from 'mobx-utils';
 
 enum cardSortings {
     Time = 'time',
     Type = 'type',
     Color = 'color',
-    Custom = 'custom',
     Chat = 'chat',
     Tag = 'tag',
     None = '',
@@ -46,14 +44,13 @@ export class CollectionCardView extends CollectionSubView() {
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [key: string]: IReactionDisposer } = {};
     private _textToDoc = new Map<string, Doc>();
-    private _dropped = false; // indicate when a card doc has just moved;
+    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)
 
     @observable _forceChildXf = 0;
     @observable _hoveredNodeIndex = -1;
     @observable _docRefs = new ObservableMap<Doc, DocumentView>();
     @observable _maxRowCount = 10;
     @observable _docDraggedIndex: number = -1;
-    @observable overIndex: number = -1;
 
     static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
         try {
@@ -101,7 +98,6 @@ export class CollectionCardView extends CollectionSubView() {
 
     componentDidMount() {
         this._props.setContentViewBox?.(this);
-        // Reaction to cardSort changes
         this._disposers.sort = reaction(
             () => GPTPopup.Instance.visible,
             isVis => {
@@ -135,8 +131,7 @@ export class CollectionCardView extends CollectionSubView() {
     }
 
     /**
-     * The child documents to be rendered-- either all of them except the Links or the docs in the currently active
-     * custom group
+     * The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's)
      */
     @computed get childDocsWithoutLinks() {
         return this.childDocs.filter(l => !l.layout_isSvg);
@@ -155,8 +150,7 @@ export class CollectionCardView extends CollectionSubView() {
      */
     quizMode = () => {
         const randomIndex = Math.floor(Math.random() * this.childDocs.length);
-        SelectionManager.DeselectAll();
-        DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false);
+        DocumentView.getDocumentView(this.childDocs[randomIndex])?.select(false);
     };
 
     /**
@@ -168,29 +162,15 @@ export class CollectionCardView extends CollectionSubView() {
 
     @action
     setHoveredNodeIndex = (index: number) => {
-        if (!DocumentView.SelectedDocs().includes(this.childDocs[index])) {
-            this._hoveredNodeIndex = index;
-        }
+        if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index;
     };
-    /**
-     * Translates the hovered node to the center of the screen
-     * @param index
-     * @returns
-     */
-    translateHover = (index: number) => (this._hoveredNodeIndex === index && !DocumentView.SelectedDocs().includes(this.childDocs[index]) ? -50 : 0);
-
-    isSelected = (index: number) => DocumentView.SelectedDocs().includes(this.childDocs[index]);
-
-    /**
-     * Returns all the documents except the one that's currently selected
-     */
-    inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d));
 
+    isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected;
     childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2);
     childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale;
     onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
-    isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
-    isChildContentActive = () => !!this.isContentActive();
+    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
@@ -202,13 +182,14 @@ export class CollectionCardView extends CollectionSubView() {
         if (amCards == 1) return 0;
 
         const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2));
-        const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2)));
-
-        if (amCards % 2 === 0 && possRotate === 0) {
-            return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2)));
-        }
-        if (amCards % 2 === 0 && index > (amCards + 1) / 2) {
-            return possRotate + stepMag;
+        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;
@@ -274,21 +255,12 @@ export class CollectionCardView extends CollectionSubView() {
 
     /**
      * Checks to see if a card is being dragged and calls the appropriate methods if so
-     * @param e the current pointer event
      */
-
     @action
     onPointerMove = (x: number, y: number) => {
         this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1;
     };
 
-    /**
-     * Handles external drop of images/PDFs etc from outside Dash.
-     */
-    onExternalDrop = async (e: React.DragEvent): Promise<void> => {
-        super.onExternalDrop(e, {});
-    };
-
     /**
      * Resets all the doc dragging vairables once a card is dropped
      * @param e
@@ -326,7 +298,7 @@ export class CollectionCardView extends CollectionSubView() {
     }
 
     /**
-     * Used to determine how to sort cards based on tags. The lestmost tags are given lower values while cards to the right are
+     * 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
      * @param doc the doc whose value is being determined
      * @returns its value based on its tags
@@ -352,24 +324,14 @@ export class CollectionCardView extends CollectionSubView() {
             docs.sort((docA, docB) => {
                 const [typeA, typeB] = (() => {
                     switch (sortType) {
-                        case cardSortings.Time:
-                            return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()];
-                        case cardSortings.Color: {
-                            const d1 = DashColor(StrCast(docA.backgroundColor));
-                            const d2 = DashColor(StrCast(docB.backgroundColor));
-                            return [d1.hsv().hue(), d2.hsv().hue()];
-                        }
-                        case cardSortings.Tag:
-                            return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999];
-                        case cardSortings.Chat:
-                            return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999];
-                        default:
-                            return [StrCast(docA.type), StrCast(docB.type)];
+                        default:                
+                        case cardSortings.Type: return [StrCast(docA.type),                                   StrCast(docB.type)];
+                        case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999),                        NumCast(docB.chatIndex,9999)];
+                        case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(),       DateCast(docB.author_date)?.date ?? Date.now()];
+                        case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()];
                     }
-                })();
-
-                const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0;
-                return isDesc ? out : -out;
+                })(); //prettier-ignore
+                return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
             });
         if (dragIndex !== -1) {
             const draggedDoc = DragManager.docsBeingDragged[0];
@@ -382,6 +344,15 @@ export class CollectionCardView extends CollectionSubView() {
         return docs;
     };
 
+    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;
+
     displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => (
         <DocumentView
             {...this._props}
@@ -396,13 +367,14 @@ export class CollectionCardView extends CollectionSubView() {
             LayoutTemplateString={this._props.childLayoutString}
             containerViewPath={this.childContainerViewPath}
             ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot
-            isContentActive={emptyFunction}
             isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
             PanelWidth={this.childPanelWidth}
             PanelHeight={this.childPanelHeight}
             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}
             dontHideOnDrag
         />
     );
@@ -416,13 +388,11 @@ export class CollectionCardView extends CollectionSubView() {
         if (this.sortedDocs.length < this._maxRowCount) {
             return this.sortedDocs.length;
         }
-        // 13 - 3 = 10
         const totalCards = this.sortedDocs.length;
         // if 9 or less
         if (index < totalCards - (totalCards % this._maxRowCount)) {
             return this._maxRowCount;
         }
-        // (3)
         return totalCards % this._maxRowCount;
     };
     /**
@@ -443,17 +413,17 @@ export class CollectionCardView extends CollectionSubView() {
     /**
      * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected
      * @param isHovered
-     * @param isSelected
+     * @param isActive
      * @param realIndex
      * @param amCards
      * @param calcRowIndex
      * @returns
      */
-    calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
+    calculateTranslateY = (isHovered: boolean, isActive: boolean, realIndex: number, amCards: number, calcRowIndex: number) => {
         const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows;
         const rowIndex = Math.trunc(realIndex / this._maxRowCount);
         const rowToCenterShift = this.numRows / 2 - rowIndex;
-        if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2;
+        if (isActive) return rowToCenterShift * rowHeight - rowHeight / 2;
         if (amCards == 1) return 50 * this.fitContentScale;
         return this.translateY(amCards, calcRowIndex, realIndex);
     };
@@ -576,15 +546,43 @@ export class CollectionCardView extends CollectionSubView() {
         await this.childPairStringListAndUpdateSortDesc();
     };
 
+    childScreenToLocal = computedFn((doc: Doc, index: number, calcRowIndex: number, isSelected: boolean, amCards: number) => () => {
+        // need to explicitly trigger an invalidation since we're reading everything from the Dom
+        this._forceChildXf;
+        this._props.ScreenToLocalTransform();
+
+        const dref = this._docRefs.get(doc);
+        const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
+        if (!scale) return new Transform(0, 0, 0);
+
+        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
+    });
+
+    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.isSelected(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.SetIsResizing(doc[Id]);
+            setTimeout(
+                action(() => {
+                    SnappingManager.SetIsResizing(undefined);
+                    this._forceChildXf++;
+                }),
+                1000
+            );
+        }
+    });
+
     /**
      * Actually renders all the cards
      */
     @computed get renderCards() {
-        const sortedDocs = this.sortedDocs;
-        const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc));
-        const isEmpty = this.childDocsWithoutLinks.length === 0;
-
-        if (isEmpty) {
+        if (!this.childDocsWithoutLinks.length) {
             return (
                 <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}>
                     Sorry ! There are no cards in this group
@@ -593,31 +591,18 @@ export class CollectionCardView extends CollectionSubView() {
         }
 
         // Map sorted documents to their rendered components
-        return sortedDocs.map((doc, index) => {
-            const realIndex = sortedDocs.indexOf(doc);
+        return this.sortedDocs.map((doc, index) => {
+            const realIndex = this.sortedDocs.indexOf(doc);
             const calcRowIndex = this.overflowIndexCalc(realIndex);
             const amCards = this.overflowAmCardsCalc(realIndex);
             const view = DocumentView.getDocumentView(doc, this.DocumentView?.());
-            const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false;
 
-            const childScreenToLocal = () => {
-                // need to explicitly trigger an invalidation since we're reading everything from the Dom
-                this._forceChildXf;
-                this._props.ScreenToLocalTransform();
-
-                const dref = this._docRefs.get(doc);
-                const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
-                if (!scale) return new Transform(0, 0, 0);
-
-                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
-            };
+            const childScreenToLocal = this.childScreenToLocal(doc, index, calcRowIndex, !!view?.IsContentActive, amCards);
 
             const translateIfSelected = () => {
                 const indexInRow = index % this._maxRowCount;
                 const rowIndex = Math.trunc(index / this._maxRowCount);
-                const rowCenterIndex = Math.min(this._maxRowCount, sortedDocs.length - rowIndex * this._maxRowCount) / 2;
+                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);
@@ -627,33 +612,18 @@ export class CollectionCardView extends CollectionSubView() {
             return (
                 <div
                     key={doc[Id]}
-                    className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`}
-                    onPointerUp={action(() => {
-                        // if a card doc has just moved, or a card is selected and in front, then ignore this event
-                        if (DocumentView.SelectedDocs().includes(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.SetIsResizing(doc[Id]);
-                            setTimeout(
-                                action(() => {
-                                    SnappingManager.SetIsResizing(undefined);
-                                    this._forceChildXf++;
-                                }),
-                                1000
-                            );
-                        }
-                    })}
+                    className={`card-item${view?.IsContentActive ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
+                    onPointerUp={() => this.cardPointerUp(doc)}
                     style={{
                         width: this.childPanelWidth(),
                         height: 'max-content',
-                        transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px)
-                                    translateX(calc(${isSelected ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px))
-                                    rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg)
-                                    scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`,
+                        transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, !!view?.IsContentActive, realIndex, amCards, calcRowIndex)}px)
+                                    translateX(calc(${view?.IsContentActive ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px))
+                                    rotate(${!view?.IsContentActive ? this.rotate(amCards, calcRowIndex) : 0}deg)
+                                    scale(${view?.IsContentActive ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`,
                     }} // prettier-ignore
-                    onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}>
+                    onPointerEnter={() => this.setHoveredNodeIndex(index)}
+                    onPointerLeave={() => this.setHoveredNodeIndex(-1)}>
                     {this.displayDoc(doc, childScreenToLocal)}
                 </div>
             );
@@ -680,8 +650,7 @@ export class CollectionCardView extends CollectionSubView() {
                         ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
                         ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }),
                         gridAutoRows: `${100 / this.numRows}%`,
-                    }}
-                    onMouseLeave={() => this.setHoveredNodeIndex(-1)}>
+                    }}>
                     {this.renderCards}
                 </div>
             </div>
-- 
cgit v1.2.3-70-g09d2

