aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/CollectionCardDeckView.tsx
diff options
context:
space:
mode:
authorNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2025-03-11 17:43:05 +0100
committerNathan-SR <144961007+Nathan-SR@users.noreply.github.com>2025-03-11 17:43:05 +0100
commitfa937182bc93aa2c6faadda80ea998cdfd479b4e (patch)
treecba8e16edcccc6fd2932173484ac444cb79abea2 /src/client/views/collections/CollectionCardDeckView.tsx
parentcf91c46cfec6e3e36b9184764016f9c1b5c997d4 (diff)
parent04669ffeb163688c7aefd7b5face7998252abdca (diff)
Merge branch 'master' of https://github.com/brown-dash/Dash-Web into DocCreatorMenu-work
Diffstat (limited to 'src/client/views/collections/CollectionCardDeckView.tsx')
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx682
1 files changed, 290 insertions, 392 deletions
diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index a9ab9de26..756b37f99 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,36 +1,32 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import * as CSS from 'csstype';
+import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
import * as React from 'react';
-import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
-import { Doc } from '../../../fields/Doc';
-import { DocData } from '../../../fields/DocSymbols';
+import { ClientUtils, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Animation } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
-import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { URLField } from '../../../fields/URLField';
-import { gptImageLabel } from '../../apis/gpt/GPT';
+import { ScriptField } from '../../../fields/ScriptField';
+import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
+import { SettingsManager } from '../../util/SettingsManager';
import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
-import { undoable } from '../../util/UndoManager';
+import { undoable, UndoManager } from '../../util/UndoManager';
+import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
-import { DocumentView } from '../nodes/DocumentView';
-import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
+import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import './CollectionCardDeckView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { computedFn } from 'mobx-utils';
-
-enum cardSortings {
- Time = 'time',
- Type = 'type',
- Color = 'color',
- Chat = 'chat',
- Tag = 'tag',
- None = '',
-}
/**
* New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily
@@ -43,73 +39,41 @@ enum cardSortings {
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 _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
+ private _draggerRef = React.createRef<HTMLDivElement>();
@observable _forceChildXf = 0;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
- @observable _maxRowCount = 10;
- @observable _docDraggedIndex: number = -1;
-
- static imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
- try {
- const response = await fetch(imageUrl);
- const blob = await response.blob();
-
- return new Promise((resolve, reject) => {
- const reader = new FileReader();
- reader.readAsDataURL(blob);
- reader.onloadend = () => resolve(reader.result as string);
- reader.onerror = error => reject(error);
- });
- } catch (error) {
- console.error('Error:', error);
- throw error;
- }
- };
+ @observable _cursor: CSS.Property.Cursor = 'ew-resize';
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
- this.setRegenerateCallback();
}
protected createDashEventsTarget = (ele: HTMLDivElement | null) => {
this._dropDisposer?.();
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
- */
- setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc);
-
- /**
- * update's gpt's doc-text list and initializes callbacks
- */
- @action
- childPairStringListAndUpdateSortDesc = async () => {
- const sortDesc = await this.childPairStringList(); // Await the promise to get the string result
- GPTPopup.Instance.setSortDesc(sortDesc.join());
- GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag);
- GPTPopup.Instance.onQuizRandom = () => this.quizMode();
- };
+ @computed get cardWidth() {
+ return NumCast(this.layoutDoc._cardWidth, 50);
+ }
+ @computed get _maxRowCount() {
+ return Math.ceil(this.cardDeckWidth / this.cardWidth);
+ }
componentDidMount() {
this._props.setContentViewBox?.(this);
- this._disposers.sort = reaction(
- () => GPTPopup.Instance.visible,
- isVis => {
- if (isVis) {
- this.openChatPopup();
- } else {
- this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort;
- }
- }
- );
// if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles
- // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the
+ // when inquired from the dom (below in childScreenToLocal). When the doc is actually rendered, we need to act like the
// dash data just changed and trigger a React involidation with the correct data (read from the dom).
this._disposers.child = reaction(
() => [this.Document.x, this.Document.y],
@@ -119,6 +83,12 @@ export class CollectionCardView extends CollectionSubView() {
}
}
);
+ this._disposers.select = reaction(
+ () => this.childDocs.find(d => this._docRefs.get(d)?.IsSelected),
+ selected => {
+ selected && (this.layoutDoc._card_curDoc = selected);
+ }
+ );
}
componentWillUnmount() {
@@ -126,126 +96,95 @@ export class CollectionCardView extends CollectionSubView() {
this._dropDisposer?.();
}
- @computed get cardSort() {
- return StrCast(this.Document.cardSort) as cardSortings;
+ /**
+ * Number of rows of cards to be rendered
+ */
+ @computed get numRows() {
+ return Math.ceil(this.childDocs.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.childDocsNoInk.length < this._maxRowCount ? this.childDocsNoInk.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);
}
/**
* 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);
+ @computed get childDocsNoInk() {
+ return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg);
}
/**
* how much to scale down the contents of the view so that everything will fit
*/
@computed get fitContentScale() {
- const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount);
- return (this.childPanelWidth() * length) / this._props.PanelWidth();
+ const length = Math.min(this.childDocsNoInk.length, this._maxRowCount);
+ return (this.childPanelWidth() * length) / (this._props.PanelWidth() - 2 * this.xMargin);
}
- /**
- * When in quiz mode, randomly selects a document
- */
- quizMode = () => {
- const randomIndex = Math.floor(Math.random() * this.childDocs.length);
- DocumentView.getDocumentView(this.childDocs[randomIndex])?.select(false);
- };
+ @computed get nativeScaling() {
+ return this._props.NativeDimScaling?.() || 1;
+ }
- /**
- * Number of rows of cards to be rendered
- */
- @computed get numRows() {
- return Math.ceil(this.sortedDocs.length / this._maxRowCount);
+ @computed get xMargin() {
+ return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth()));
}
- @action
- setHoveredNodeIndex = (index: number) => {
+ @computed get yMargin() {
+ return this._props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this._props.PanelWidth()));
+ }
+
+ @computed get cardDeckWidth() {
+ return this._props.PanelWidth() - 2 * this.xMargin;
+ }
+
+ 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.childDocsNoInk.length > this._maxRowCount ? this._maxRowCount : this.childDocsNoInk.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
* @param mouseY mouses' y location
* @returns the card's new index
*/
findCardDropIndex = (mouseX: number, mouseY: number) => {
- const amCardsTotal = this.sortedDocs.length;
+ const cardCount = this.childDocs.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;
@@ -254,11 +193,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.childDocs.includes(doc)) || SnappingManager.CanEmbed) {
+ this.docDraggedIndex = this.findCardDropIndex(x, y);
+ }
};
/**
@@ -270,14 +212,14 @@ export class CollectionCardView extends CollectionSubView() {
onInternalDrop = undoable(
action((e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
- const dragIndex = this._docDraggedIndex;
+ const dragIndex = this.docDraggedIndex;
const draggedDoc = DragManager.docsBeingDragged[0];
if (dragIndex > -1 && draggedDoc) {
- this._docDraggedIndex = -1;
- const sorted = this.sortedDocs;
+ this.docDraggedIndex = -1;
+ const sorted = this.childDocs;
const originalIndex = sorted.findIndex(doc => doc === draggedDoc);
- this.Document.cardSort = '';
+ this.Document[this._props.fieldKey + '_sort'] = '';
originalIndex !== -1 && sorted.splice(originalIndex, 1);
sorted.splice(dragIndex, 0, draggedDoc);
if (de.complete.docDragData.removeDocument?.(draggedDoc)) {
@@ -293,10 +235,6 @@ export class CollectionCardView extends CollectionSubView() {
''
);
- @computed get sortedDocs() {
- return this.sort(this.childDocsWithoutLinks, 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
@@ -310,48 +248,16 @@ export class CollectionCardView extends CollectionSubView() {
.map(({ i }) => i)
.join('.');
- /**
- * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the
- * front, latter cards to the back
- * @param docs
- * @param sortType
- * @param isDesc
- * @returns
- */
- sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => {
- const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching
- sortType &&
- docs.sort((docA, docB) => {
- const [typeA, typeB] = (() => {
- switch (sortType) {
- 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()];
- }
- })(); //prettier-ignore
- return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1);
- });
- if (dragIndex !== -1) {
- const draggedDoc = DragManager.docsBeingDragged[0];
- const originalIndex = docs.findIndex(doc => doc === draggedDoc);
-
- originalIndex !== -1 && docs.splice(originalIndex, 1);
- draggedDoc && docs.splice(dragIndex, 0, draggedDoc);
- }
-
- 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
+ isChildContentActive = computedFn(
+ (doc: Doc) => () =>
+ this._props.isContentActive?.() === false
? false
- : undefined;
+ : this._props.isDocumentActive?.() && this.curDoc() === doc
+ ? true
+ : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+ ? false
+ : undefined
+ ); // prettier-ignore
displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => (
<DocumentView
@@ -360,267 +266,218 @@ 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}
+ focus={this.focus}
+ onDoubleClickScript={this.onChildDoubleClick}
+ onClickScript={this.curDoc() === doc ? undefined : this._setCurDocScript}
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)}
+ showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)}
whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
- isContentActive={this.isChildContentActive}
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.childDocsNoInk.length < this._maxRowCount) {
+ return this.childDocsNoInk.length;
}
- const totalCards = this.sortedDocs.length;
- // if 9 or less
+ const totalCards = this.childDocsNoInk.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
- */
- overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount;
- /**
- * Translates the cards in the second rows and beyond over to the right
- * @param realIndex
- * @param calcIndex
- * @param calcRowCards
- * @returns
- */
- translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2));
-
- /**
- * 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 isActive
- * @param realIndex
- * @param amCards
- * @param calcRowIndex
- * @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
*/
- 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 (isActive) return rowToCenterShift * rowHeight - rowHeight / 2;
- if (amCards == 1) return 50 * this.fitContentScale;
- return this.translateY(amCards, calcRowIndex, realIndex);
+ 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;
};
-
/**
- * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words.
- * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is
- * inputted into the gpt prompt to sort everything together
- * @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
*/
- childPairStringList = () => {
- const docToText = (doc: Doc) => {
- switch (doc.type) {
- case DocumentType.PDF: return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text
- case DocumentType.IMG: return this.getImageDesc(doc);
- case DocumentType.RTF: return StrCast(RTFCast(doc.text).Text);
- default: return StrCast(doc.title);
- } // prettier-ignore
- };
- const docTextPromises = this.childDocsWithoutLinks.map(async doc => {
- const docText = (await docToText(doc)) ?? '';
- doc.gptInputText = docText;
- this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc);
- return `======${docText.replace(/\n/g, ' ').trim()}======`;
- });
- return Promise.all<string>(docTextPromises);
+ 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);
};
-
/**
- * Calls the gpt API to generate descriptions for the images in the view
- * @param image
- * @returns
+ * 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
*/
- getImageDesc = async (image: Doc) => {
- if (StrCast(image.description)) return StrCast(image.description); // Return existing description
- const { href } = (image.data as URLField).url;
- const hrefParts = href.split('.');
- const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
- try {
- const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
- const response = await gptImageLabel(hrefBase64);
- image[DocData].description = response.trim();
- return response; // Return the response from gptImageLabel
- } catch (error) {
- console.log(error);
- }
- return '';
+ 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);
};
-
/**
- * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to
- * usable code
- * @param gptOutput
+ * 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
*/
- @action
- processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => {
- // Split the string into individual list items
- const listItems = gptOutput.split('======').filter(item => item.trim() !== '');
-
- if (questionType === '2' || questionType === '4') {
- this.childDocs.forEach(d => {
- d.chatFilter = false;
- });
- }
-
- if (questionType === '6') {
- this.Document.cardSort = 'chat';
- }
-
- listItems.forEach((item, index) => {
- const normalizedItem = item.trim();
- // find the corresponding Doc in the textToDoc map
- const doc = this._textToDoc.get(normalizedItem);
-
- if (doc) {
- switch (questionType) {
- case '6':
- doc.chatIndex = index;
- break;
- case '1': {
- const allHotKeys = Doc.MyFilterHotKeys;
-
- let myTag = '';
-
- if (tag) {
- for (let i = 0; i < allHotKeys.length; i++) {
- // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED
- const keyTag = StrCast(allHotKeys[i].toolType);
- if (tag.includes(keyTag)) {
- myTag = keyTag;
- break;
- }
- }
-
- if (myTag != '') {
- doc[myTag] = true;
- }
- }
- break;
- }
- case '2':
- case '4':
- doc.chatFilter = true;
- Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match');
- break;
- }
- } else {
- console.warn(`No matching document found for item: ${normalizedItem}`);
- }
- });
- }, '');
+ horizontalAdjustmentForPartialRows = (index: number, cardsInRow: number) => (index < this._maxRowCount ? 0 : (this._maxRowCount - cardsInRow) * (this.childPanelWidth() / 2));
/**
- * Opens up the chat popup and starts the process for smart sorting.
+ * 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 index index of card from start of deck
+ * @returns vertical pixel translation
*/
- openChatPopup = async () => {
- GPTPopup.Instance.setVisible(true);
- GPTPopup.Instance.setMode(GPTPopupMode.CARD);
- GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded
- await this.childPairStringListAndUpdateSortDesc();
+ 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;
+ return isActive
+ ? (rowToCenterShift * rowHeight - rowHeight / 2) * ((this.cardSpacing * this.fitContentScale) / 100) //
+ : this.translateY(index);
};
- 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();
const dref = this._docRefs.get(doc);
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
- if (!scale) return new Transform(0, 0, 0);
+ if (!scale) return new Transform(0, 0, 1);
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.layoutDoc._card_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
+ }
});
+ cardSizerDown = (e: React.PointerEvent) => {
+ runInAction(() => {
+ this._cursor = 'grabbing';
+ });
+ const batch = UndoManager.StartBatch('card view size');
+ setupMoveUpEvents(
+ this,
+ e,
+ (emove: PointerEvent) => {
+ this.layoutDoc._cardWidth = Math.max(10, this.ScreenToLocalBoxXf().transformPoint(emove.clientX, 0)[0] - this.xMargin);
+ return false;
+ },
+ action(() => {
+ this._cursor = 'ew-resize';
+ batch.end();
+ }),
+ emptyFunction
+ );
+ };
+
+ /**
+ * 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.isSelected(doc) || this._dropped) {
+ 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.SetIsResizing(doc[Id]);
- setTimeout(
- action(() => {
- SnappingManager.SetIsResizing(undefined);
- this._forceChildXf++;
- }),
- 1000
+ this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc')
+ }
+ });
+
+ focus = action((anchor: Doc, options: FocusViewOptions): Opt<number> => {
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const foundDoc = DocCast(
+ anchor.config_card_curDoc,
+ docs.find(doc => doc === DocCast(anchor.annotationOn, anchor))
);
+ options.didMove = foundDoc !== this.curDoc() ? true : false;
+ options.didMove && (this.layoutDoc._card_curDoc = foundDoc);
}
+ return undefined;
});
+ getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() });
+ PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document);
+ addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
+ return anchor;
+ };
+ addDocTab = this.addLinkedDocTab;
/**
* Actually renders all the cards
*/
@computed get renderCards() {
- 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
- </span>
- );
- }
-
// Map sorted documents to their rendered components
- 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 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, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2;
- return (rowCenterIndex - indexInRow) * 100 - 50;
- };
+ return this.childDocs.map((doc, index) => {
+ const cardsInRow = this.cardsInRowThatIncludesCardIndex(index);
+
+ const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
+
+ const translateToCenterIfActive = () => (doc === this.curDoc() ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0);
+
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
+ const hscale = Math.min(this.childDocs.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 (
<div
key={doc[Id]}
- className={`card-item${view?.IsContentActive ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
+ className={`card-item${doc === this.curDoc() ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`}
onPointerUp={() => this.cardPointerUp(doc)}
style={{
width: this.childPanelWidth(),
height: 'max-content',
- 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})`,
+ 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)}
onPointerLeave={() => this.setHoveredNodeIndex(-1)}>
@@ -629,29 +486,70 @@ export class CollectionCardView extends CollectionSubView() {
);
});
}
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
+
+ contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+ docViewProps = (): DocumentViewProps => ({
+ ...this._props, //
+ isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
+ isContentActive: emptyFunction,
+ ScreenToLocalTransform: this.contentScreenToLocalXf,
+ });
+ answered = () => {
+ this.layoutDoc._card_curDoc = this.curDoc() ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
+ };
+ curDoc = () => DocCast(this.layoutDoc._card_curDoc);
render() {
- const isEmpty = this.childDocsWithoutLinks.length === 0;
-
+ const fitContentScale = this.childDocsNoInk.length === 0 ? 1 : this.fitContentScale;
return (
<div
className="collectionCardView-outer"
ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)}
- onPointerLeave={action(() => (this._docDraggedIndex = -1))}
+ onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()}
+ onPointerLeave={action(() => (this.docDraggedIndex = -1))}
onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))}
onDrop={this.onExternalDrop.bind(this)}
style={{
background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string,
color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string,
+ paddingLeft: this.xMargin,
+ paddingRight: this.xMargin,
}}>
<div
- className="card-wrapper"
+ className="collectionCardView-inner"
style={{
- ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }),
- ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }),
- gridAutoRows: `${100 / this.numRows}%`,
+ transform: `scale(${1 / fitContentScale})`,
+ height: `${100 * fitContentScale}%`,
+ width: `${100 * fitContentScale}%`,
+ top: this.yMargin,
}}>
- {this.renderCards}
+ <div
+ className="collectionCardView-cardwrapper"
+ style={{
+ gridTemplateColumns: `repeat(${this._maxRowCount}, 1fr)`,
+ gridAutoRows: `${100 / this.numRows}%`,
+ height: `${this.cardSpacing}%`,
+ }}>
+ {this.renderCards}
+ </div>
+ <div
+ className="collectionCardView-flashcardUI"
+ style={{
+ pointerEvents: this.childDocsNoInk.length === 0 ? undefined : 'none',
+ height: `${100 / this.nativeScaling / fitContentScale}%`,
+ width: `${100 / this.nativeScaling / fitContentScale}%`,
+ transform: `scale(${this.nativeScaling * fitContentScale})`,
+ }}></div>
+ </div>
+
+ {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+ <div
+ className="collectionCardView-cardSizeDragger"
+ onPointerDown={this.cardSizerDown}
+ ref={this._draggerRef}
+ style={{ display: this._props.isContentActive() ? undefined : 'none', cursor: this._cursor, color: SettingsManager.userColor, left: `${this.cardWidth + this.xMargin}px` }}>
+ <FontAwesomeIcon icon="arrows-alt-h" />
</div>
</div>
);