aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authoreleanor-park <eleanor_park@brown.edu>2024-10-30 19:39:46 -0400
committereleanor-park <eleanor_park@brown.edu>2024-10-30 19:39:46 -0400
commitc11c760db62f78a07b624b98b209e6ee86036c8e (patch)
treec9587b50042a5115373e91ba8ecf9b76913cd321 /src/client/views/collections
parentb5944e87f9d4f3149161de4de0d76db486461c76 (diff)
parent4c768162e0436115a05b9c8b0e4d837d626d45ba (diff)
Merge branch 'master' into eleanor-gptdraw
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionCardDeckView.scss24
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx390
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx82
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx94
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx2
-rw-r--r--src/client/views/collections/CollectionStackedTimeline.tsx5
-rw-r--r--src/client/views/collections/CollectionSubView.tsx19
-rw-r--r--src/client/views/collections/CollectionTimeView.tsx4
-rw-r--r--src/client/views/collections/CollectionView.tsx6
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.scss6
-rw-r--r--src/client/views/collections/FlashcardPracticeUI.tsx59
-rw-r--r--src/client/views/collections/TabDocView.tsx1
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx6
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx94
-rw-r--r--src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx23
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaRowBox.tsx2
18 files changed, 490 insertions, 335 deletions
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 286df30aa..0ba6b679c 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -4,8 +4,8 @@ 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 { DocData } from '../../../fields/DocSymbols';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { Animation, DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { ScriptField } from '../../../fields/ScriptField';
@@ -13,14 +13,17 @@ import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } fr
import { URLField } from '../../../fields/URLField';
import { gptImageLabel } from '../../apis/gpt/GPT';
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 { Transform } from '../../util/Transform';
import { undoable } from '../../util/UndoManager';
+import { PinDocView } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { TagItem } from '../TagsView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup';
import './CollectionCardDeckView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -46,15 +49,15 @@ 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' })!;
+ private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!;
@observable _forceChildXf = 0;
@observable _hoveredNodeIndex = -1;
@observable _docRefs = new ObservableMap<Doc, DocumentView>();
@observable _maxRowCount = 10;
@observable _docDraggedIndex: number = -1;
- @observable _curDoc: Doc | undefined = undefined;
constructor(props: SubCollectionViewProps) {
super(props);
@@ -66,6 +69,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
@@ -91,7 +98,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;
}
}
);
@@ -114,7 +121,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);
}
/**
@@ -132,107 +157,57 @@ 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
*/
- quizMode = action(() => {
+ quizMode = () => {
const randomIndex = Math.floor(Math.random() * this.childDocs.length);
- this._curDoc = this.childDocs[randomIndex];
- });
-
- /**
- * Number of rows of cards to be rendered
- */
- @computed get numRows() {
- return Math.ceil(this.sortedDocs.length / this._maxRowCount);
- }
+ this.layoutDoc._card_curDoc = this.childDocs[randomIndex];
+ };
- @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
* @param mouseY mouses' y location
* @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;
@@ -241,11 +216,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);
+ }
};
/**
@@ -264,7 +242,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)) {
@@ -280,15 +258,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
@@ -336,11 +305,20 @@ 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
? false
- : this._props.isDocumentActive?.() && this._curDoc === doc
+ : this._props.isDocumentActive?.() && this.curDoc() === doc
? true
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
@@ -354,74 +332,100 @@ 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}
- onClickScript={this._curDoc === doc ? undefined : this._clickScript}
+ 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)}
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);
};
/**
@@ -488,7 +492,7 @@ export class CollectionCardView extends CollectionSubView() {
}
if (questionType === '6') {
- this.Document.cardSort = 'chat';
+ this.Document.card_sort = 'chat';
}
listItems.forEach((item, index) => {
@@ -544,7 +548,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();
@@ -555,64 +559,91 @@ 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.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
+ }
});
+ /**
+ * 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) {
+ 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')
+ }
+ });
+
+ 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) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() });
+ PinDocView(anchor, { 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.childCards.length) {
- return null;
- }
-
// 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, calcRowIndex, doc === this._curDoc, amCards);
+ const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc());
+
+ 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 (
<div
key={doc[Id]}
- className={`card-item${doc === this._curDoc ? '-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(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)
- scale(${doc === this._curDoc ? `${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)}>
@@ -621,6 +652,7 @@ export class CollectionCardView extends CollectionSubView() {
);
});
}
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
docViewProps = (): DocumentViewProps => ({
@@ -629,27 +661,20 @@ export class CollectionCardView extends CollectionSubView() {
isContentActive: emptyFunction,
ScreenToLocalTransform: this.contentScreenToLocalXf,
});
- answered = action(() => {
- this._curDoc = this.curDoc ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(this.curDoc) + 1) % (this.filteredChildDocs().length || 1)] : undefined;
- });
- curDoc = () => this._curDoc;
+ 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.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))}
@@ -659,16 +684,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 f2ba90c78..c080ba27e 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -1,6 +1,7 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
+import { computedFn } from 'mobx-utils';
import * as React from 'react';
import { returnZero } from '../../../ClientUtils';
import { Utils } from '../../../Utils';
@@ -8,8 +9,10 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc';
import { Id } from '../../../fields/FieldSymbols';
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 { Transform } from '../../util/Transform';
+import { PinDocView } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FocusViewOptions } from '../nodes/FocusViewOptions';
@@ -22,12 +25,16 @@ const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = requi
@observer
export class CollectionCarousel3DView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
+ private _oldWheel: HTMLElement | null = null;
constructor(props: SubCollectionViewProps) {
super(props);
makeObservable(this);
}
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ }
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -37,6 +44,10 @@ export class CollectionCarousel3DView 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 });
};
@computed get scrollSpeed() {
@@ -48,45 +59,61 @@ export class CollectionCarousel3DView extends CollectionSubView() {
centerScale = Number(CAROUSEL3D_CENTER_SCALE);
sideScale = Number(CAROUSEL3D_SIDE_SCALE);
- panelWidth = () => this._props.PanelWidth() / 3;
- panelHeight = () => this._props.PanelHeight() * this.sideScale;
+ panelWidth = () => this._props.PanelWidth() / 3 / this.nativeScaling();
+ panelHeight = () => (this._props.PanelHeight() * this.sideScale) / this.nativeScaling();
onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive();
- 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;
- contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1);
+ : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ ? true
+ : this._props.isContentActive?.() && this.curDoc() === doc
+ ? true
+ : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
+ ? false
+ : undefined
+ );
+ contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().translate(0, (-(Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) / this.nativeScaling());
childScreenLeftToLocal = () =>
this.contentScreenToLocalXf()
- .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .translate(
+ (-this.panelWidth() * (1 - this.sideScale)) / 2, //
+ (-this.panelHeight() * (1 - this.sideScale)) / 2
+ )
.scale(1 / this.sideScale);
childScreenRightToLocal = () =>
this.contentScreenToLocalXf()
- .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight())
+ .translate(
+ -2 * this.panelWidth() - (this.panelWidth() * (1 - this.sideScale)) / 2, //
+ (-this.panelHeight() * (1 - this.sideScale)) / 2
+ )
.scale(1 / this.sideScale);
childCenterScreenToLocal = () =>
this.contentScreenToLocalXf()
.translate(
-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3
- -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2
- ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2
+ ((this.centerScale - 1) * this.panelHeight()) / 2
+ )
.scale(1 / this.centerScale);
focus = (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.layoutDoc._carousel_index = index);
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
+ }
return undefined;
};
-
+ getAnchor = (addAsAnnotation: boolean) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number });
+ PinDocView(anchor, { 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;
@computed get content() {
const currentIndex = NumCast(this.layoutDoc._carousel_index);
const displayDoc = (child: Doc, dxf: () => Transform) => (
@@ -105,7 +132,7 @@ export class CollectionCarousel3DView extends CollectionSubView() {
LayoutTemplateString={this._props.childLayoutString}
focus={this.focus}
ScreenToLocalTransform={dxf}
- isContentActive={this.isChildContentActive}
+ isContentActive={this.isChildContentActive(child)}
isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive}
PanelWidth={this.panelWidth}
PanelHeight={this.panelHeight}
@@ -120,7 +147,6 @@ export class CollectionCarousel3DView extends CollectionSubView() {
}
changeSlide = (direction: number) => {
- DocumentView.DeselectAll();
this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1);
};
@@ -194,14 +220,16 @@ export class CollectionCarousel3DView extends CollectionSubView() {
return this.panelWidth() * (1 - index);
}
+ 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,
- isContentActive: this.isChildContentActive,
+ isContentActive: this._props.isContentActive,
ScreenToLocalTransform: this.contentScreenToLocalXf,
});
+ nativeScaling = () => this._props.NativeDimScaling?.() || 1;
render() {
return (
<div
@@ -210,6 +238,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
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,
+ transformOrigin: 'top left',
+ transform: `scale(${this.nativeScaling()})`,
+ width: `${100 / this.nativeScaling()}%`,
+ height: `${100 / this.nativeScaling()}%`,
}}>
<div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}>
{this.content}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index aa447c7bf..f714e2a00 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -3,12 +3,16 @@ import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
-import { Doc, Opt } from '../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
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 { PinDocView } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
+import { FocusViewOptions } from '../nodes/FocusViewOptions';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
@@ -17,6 +21,7 @@ import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
export class CollectionCarouselView extends CollectionSubView() {
private _dropDisposer?: DragManager.DragDropDisposer;
+ _oldWheel: HTMLElement | null = null;
_fadeTimer: NodeJS.Timeout | undefined;
@observable _last_index = this.carouselIndex;
@observable _last_opacity = 1;
@@ -26,6 +31,9 @@ export class CollectionCarouselView extends CollectionSubView() {
makeObservable(this);
}
+ componentDidMount(): void {
+ this._props.setContentViewBox?.(this);
+ }
componentWillUnmount() {
this._dropDisposer?.();
}
@@ -35,6 +43,10 @@ export class CollectionCarouselView 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 });
};
@computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore
@@ -53,44 +65,56 @@ export class CollectionCarouselView extends CollectionSubView() {
/**
* Goes to the next Doc in the stack subject to the currently selected filter option.
*/
- advance = (e?: React.MouseEvent) => {
- e?.stopPropagation();
- this.move(1);
- };
+ advance = () => this.move(1);
/**
* Goes to the previous Doc in the stack subject to the currently selected filter option.
*/
- goback = (e: React.MouseEvent) => {
- e.stopPropagation();
- this.move(-1);
- };
+ goback = () => this.move(-1);
curDoc = () => this.carouselItems[this.carouselIndex]?.layout;
+ focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => {
+ const docs = DocListCast(this.Document[this.fieldKey]);
+ if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) {
+ const newIndex = anchor.config_carousel_index ?? docs.getIndex(DocCast(anchor.annotationOn, anchor));
+ options.didMove = newIndex !== this.layoutDoc._carousel_index;
+ options.didMove && (this.layoutDoc._carousel_index = newIndex);
+ }
+ return undefined;
+ };
+
+ getAnchor = (addAsAnnotation: boolean) => {
+ const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex });
+ PinDocView(anchor, { 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;
+
captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string) => {
// first look for properties on the document in the carousel, then fallback to properties on the container
const childValue = doc?.['caption_' + property] ? this._props.styleProvider?.(doc, captionProps, property) : undefined;
return childValue ?? this._props.styleProvider?.(this.layoutDoc, captionProps, property);
};
- contentPanelWidth = () => this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin);
- contentPanelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin);
+ contentPanelWidth = () => (this._props.PanelWidth() - 2 * NumCast(this.layoutDoc.xMargin)) / this.nativeScaling();
+ contentPanelHeight = () => (this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0) - 2 * NumCast(this.layoutDoc.yMargin)) / this.nativeScaling();
onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick);
onContentClick = () => ScriptCast(this.layoutDoc.onChildClick);
captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX;
contentScreenToLocalXf = () =>
this._props
- .ScreenToLocalTransform()
- .translate(-NumCast(this.layoutDoc.xMargin), -NumCast(this.layoutDoc.yMargin))
- .scale(this._props.NativeDimScaling?.() || 1);
+ .ScreenToLocalTransform() //
+ .translate(-NumCast(this.layoutDoc.xMargin) / this.nativeScaling(), -NumCast(this.layoutDoc.yMargin) / this.nativeScaling());
isChildContentActive = () =>
this._props.isContentActive?.() === false
? false
- : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive))
+ : this._props.isContentActive()
? true
: this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false
? false
- : undefined;
+ : undefined; // prettier-ignore
+ onPassiveWheel = (e: WheelEvent) => e.stopPropagation();
renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => {
return (
<DocumentView
@@ -114,6 +138,7 @@ export class CollectionCarouselView extends CollectionSubView() {
LayoutTemplateString={this._props.childLayoutString}
TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)}
childFilters={this.childDocFilters}
+ focus={this.focus}
hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)}
addDocument={this._props.addDocument}
ScreenToLocalTransform={this.contentScreenToLocalXf}
@@ -196,30 +221,37 @@ export class CollectionCarouselView extends CollectionSubView() {
);
}
+ nativeScaling = () => this._props.NativeDimScaling?.() || 1;
+
docViewProps = () => ({
...this._props, //
isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive,
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 (
- <div
- className="collectionCarouselView-outer"
- ref={this.createDashEventsTarget}
- 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,
- width: `calc(100% - ${NumCast(this.layoutDoc._xMargin)}px)`,
- height: `calc(100% - ${NumCast(this.layoutDoc._yMargin)}px)`,
- left: NumCast(this.layoutDoc._xMargin),
- top: NumCast(this.layoutDoc._yMargin),
- }}>
- {this.content}
- {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
- {this.navButtons}
+ <div>
+ <div
+ className="collectionCarouselView-outer"
+ ref={this.createDashEventsTarget}
+ 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,
+ left: NumCast(this.layoutDoc._xMargin),
+ top: NumCast(this.layoutDoc._yMargin),
+ transformOrigin: 'top left',
+ transform: `scale(${this.nativeScaling()})`,
+ width: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._xMargin)) / this.nativeScaling()}px)`,
+ height: `calc(${100 / this.nativeScaling()}% - ${(2 * NumCast(this.layoutDoc._yMargin)) / this.nativeScaling()}px)`,
+ position: 'relative',
+ }}>
+ {this.content}
+ {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)}
+ {this.navButtons}
+ </div>
</div>
);
}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e1786d2c9..3e8138390 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -453,7 +453,7 @@ export class CollectionDockingView extends CollectionSubView() {
}
}
}
- if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
+ if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && Doc.ActiveTool !== InkTool.Ink) {
e.stopPropagation();
}
};
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 486c826b6..c3047e5fb 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable no-use-before-define */
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -59,7 +58,6 @@ export enum TrimScope {
@observer
export class CollectionStackedTimeline extends CollectionSubView<CollectionStackedTimelineProps>() {
- // eslint-disable-next-line no-use-before-define
public static SelectingRegions: Set<CollectionStackedTimeline> = new Set();
public static StopSelecting() {
this.SelectingRegions.forEach(
@@ -171,7 +169,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
makeDocUnfiltered = (doc: Doc) => this.childDocList?.some(item => item === doc);
- getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+ getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
new Promise<Opt<DocumentView>>(res => {
if (doc.hidden) options.didMove = !(doc.hidden = false);
const findDoc = (finish: (dv: DocumentView) => void) => DocumentView.addViewRenderedCb(doc, dv => finish(dv));
@@ -600,7 +598,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
pointerEvents: 'none',
}}>
<StackedTimelineAnchor
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
mark={d.anchor}
containerViewPath={this._props.containerViewPath}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index f85b0b433..0c059f729 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -9,7 +9,7 @@ import { Id } from '../../../fields/FieldSymbols';
import { List } from '../../../fields/List';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types';
import { WebField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { GestureUtils } from '../../../pen-gestures/GestureUtils';
@@ -27,6 +27,7 @@ import { ViewBoxBaseComponent } from '../DocComponent';
import { FieldViewProps } from '../nodes/FieldView';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import { FlashcardPracticeUI } from './FlashcardPracticeUI';
+import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere';
export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
@@ -130,6 +131,17 @@ export function CollectionSubView<X>() {
@computed get childDocList() {
return Cast(this.dataField, listSpec(Doc));
}
+
+ addLinkedDocTab = (docsIn: Doc | Doc[], location: OpenWhere) => {
+ const doc = toList(docsIn).lastElement();
+ const where = location.split(':')[0];
+ if (where === OpenWhere.lightbox && (this.childDocList?.includes(doc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(doc))) {
+ if (doc.hidden) doc.hidden = false;
+ if (!location.includes(OpenWhereMod.always)) return true;
+ }
+ return this._props.addDocTab(docsIn, location);
+ };
+
collectionFilters = () => this._focusFilters ?? StrListCast(this.Document._childFilters);
collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.Document._childFiltersByRanges, listSpec('string'), []);
// child filters apply to the descendants of the documents in this collection
@@ -519,17 +531,18 @@ export function CollectionSubView<X>() {
};
protected _sideBtnWidth = 35;
+ protected _sideBtnMaxPanelPct = 0.15;
@observable _filterFunc: ((doc: Doc) => boolean) | undefined = undefined;
/**
* How much the content of the collection is being scaled based on its nesting and its fit-to-width settings
*/
- @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale * (this._props.NativeDimScaling?.() ?? 1); } // prettier-ignore
+ @computed get contentScaling() { return this.ScreenToLocalBoxXf().Scale; } // prettier-ignore
/**
* The maximum size a UI widget can be in collection coordinates based on not wanting the widget to visually obscure too much of the collection
* This takes the desired screen space size and converts into collection coordinates. It then returns the smaller of the converted
* size or a fraction of the collection view.
*/
- @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, 0.25 * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore
+ @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth * this.contentScaling, (this._props.fitWidth?.(this.Document) && this._props.PanelWidth() > NumCast(this.layoutDoc._width)? 1: this._sideBtnMaxPanelPct) * NumCast(this.layoutDoc.width, 1)); } // prettier-ignore
/**
* This computes a scale factor for UI elements so that they shrink and grow as the collection does in screen space.
* Note, the scale factor does not allow for elements to grow larger than their native screen space size.
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 8a24db330..d75c633ac 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -52,7 +52,7 @@ export class CollectionTimeView extends CollectionSubView() {
title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as unknown as string, // title can take a functiono or a string
annotationOn: this.Document,
});
- PinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.Document);
+ PinDocView(anchor, { pinData: { collectionType: true, pivot: true, filters: true } }, this.Document);
if (addAsAnnotation) {
// when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered
@@ -147,7 +147,6 @@ export class CollectionTimeView extends CollectionSubView() {
return (
<div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this._props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
<CollectionFreeFormView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }}
fitContentsToBox={returnTrue}
@@ -257,7 +256,6 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou
const pivotView = DocumentView.getDocumentView(pivotDoc);
if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
- // eslint-disable-next-line prefer-destructuring
pivotDoc._pivotField = filterVals[0];
}
}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7418d4360..6f0833a22 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -83,7 +83,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
return viewField;
}
- screenToLocalTransform = () => (this._props.renderDepth ? this.ScreenToLocalBoxXf() : this.ScreenToLocalBoxXf().scale(this._props.PanelWidth() / this.bodyPanelWidth()));
+ screenToLocalTransform = this.ScreenToLocalBoxXf;
// prettier-ignore
private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
TraceMobx();
@@ -202,8 +202,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
}
};
- bodyPanelWidth = () => this._props.PanelWidth();
-
childLayoutTemplate = () => this._props.childLayoutTemplate?.() || Cast(this.Document.childLayoutTemplate, Doc, null);
isContentActive = () => this._isContentActive;
@@ -221,7 +219,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr
removeDocument: this.removeDocument,
isContentActive: this.isContentActive,
isAnyChildContentActive: this.isAnyChildContentActive,
- PanelWidth: this.bodyPanelWidth,
+ PanelWidth: this._props.PanelWidth,
PanelHeight: this._props.PanelHeight,
ScreenToLocalTransform: this.screenToLocalTransform,
childLayoutTemplate: this.childLayoutTemplate,
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 7697d308b..c298bff22 100644
--- a/src/client/views/collections/FlashcardPracticeUI.tsx
+++ b/src/client/views/collections/FlashcardPracticeUI.tsx
@@ -1,19 +1,19 @@
+import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
+import { MultiToggle, Type } from 'browndash-components';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnFalse, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
import { Doc, DocListCast } from '../../../fields/Doc';
import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import { SnappingManager } from '../../util/SnappingManager';
import { Transform } from '../../util/Transform';
import { ObservableReactComponent } from '../ObservableReactComponent';
import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
import './FlashcardPracticeUI.scss';
-import { IconButton, MultiToggle, Type } from 'browndash-components';
-import { SnappingManager } from '../../util/SnappingManager';
-import { IconProp } from '@fortawesome/fontawesome-svg-core';
-import { emptyFunction } from '../../../Utils';
export enum practiceMode {
PRACTICE = 'practice',
@@ -24,6 +24,11 @@ enum practiceVal {
CORRECT = 'correct',
}
+export enum flashcardRevealOp {
+ FLIP = 'flip',
+ SLIDE = 'slide',
+}
+
interface PracticeUIProps {
fieldKey: string;
layoutDoc: Doc;
@@ -53,7 +58,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
get practiceField() { return this._props.fieldKey + "_practice"; } // prettier-ignore
@computed get filterDoc() { return DocListCast(Doc.MyContextMenuBtns.data).find(doc => doc.title === 'Filter'); } // prettier-ignore
- @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
+ @computed get practiceMode() { return this._props.allChildDocs().some(doc => doc._flashcardType) ? StrCast(this._props.layoutDoc.practiceMode) : ''; } // prettier-ignore
btnHeight = () => NumCast(this.filterDoc?.height) * Math.min(1, this._props.ScreenToLocalBoxXf().Scale);
btnWidth = () => (!this.filterDoc ? 1 : (this.btnHeight() * NumCast(this.filterDoc._width)) / NumCast(this.filterDoc._height));
@@ -99,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() ? (
@@ -122,11 +127,11 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
const setColor = (mode: practiceMode) => (StrCast(this.practiceMode) === mode ? 'white' : 'lightgray');
const togglePracticeMode = (mode: practiceMode) => this.setPracticeMode(mode === this.practiceMode ? undefined : mode);
- return !this._props.allChildDocs().some(doc => doc._layout_isFlashcard) ? null : (
+ return !this._props.allChildDocs().some(doc => doc._layout_flashcardType) ? null : (
<div
className="FlashcardPracticeUI-practiceModes"
style={{
- transform: `translateY(${(this.btnHeight() * (1 - Math.min(1, this._props.uiBtnScaling))) / this._props.ScreenToLocalBoxXf().Scale}px)`,
+ transform: this._props.ScreenToLocalBoxXf().Scale >= 1 ? undefined : `translateY(${this.btnHeight() / this._props.ScreenToLocalBoxXf().Scale - this.btnHeight()}px)`,
}}>
<MultiToggle
tooltip="Practice flashcards one at a time"
@@ -148,28 +153,36 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
selectedItems={this.practiceMode}
onSelectionChange={(val: (string | number) | (string | number)[]) => togglePracticeMode(val as practiceMode)}
/>
- <IconButton
- tooltip="hover over card to reveal answer"
- type={Type.TERT}
- text={StrCast(this._props.layoutDoc.revealOp)}
+ <MultiToggle
+ tooltip="How to reveal flashcard answer"
+ type={Type.PRIM}
color={SnappingManager.userColor}
background={SnappingManager.userVariantColor}
- icon={<FontAwesomeIcon color={SnappingManager.userColor} icon={this._props.layoutDoc.revealOp === 'hover' ? 'hand-point-up' : 'question'} size="sm" />}
- label={StrCast(this._props.layoutDoc.revealOp)}
- onPointerDown={e =>
- setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => {
- this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === 'hover' ? 'flip' : 'hover';
- this._props.layoutDoc.childDocumentsActive = this._props.layoutDoc.revealOp === 'hover' ? true : undefined;
- })
- }
+ multiSelect={false}
+ isToggle={false}
+ toggleStatus={!!this.practiceMode}
+ label={StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)}
+ items={[
+ ['reveal', StrCast(this._props.layoutDoc.revealOp) === flashcardRevealOp.SLIDE ? 'expand' : 'question', StrCast(this._props.layoutDoc.revealOp, flashcardRevealOp.FLIP)],
+ ['trigger', this._props.layoutDoc.revealOp_hover ? 'hand-point-up' : 'hand', this._props.layoutDoc.revealOp_hover ? 'show on hover' : 'show on click'],
+ ].map(([item, icon, tooltip]) => ({
+ icon: <FontAwesomeIcon className={`FlashcardPracticeUI-${item}`} color={setColor(item as practiceMode)} icon={icon as IconProp} size="sm" />,
+ tooltip: tooltip,
+ val: item,
+ }))}
+ selectedItems={this._props.layoutDoc.revealOp_hover ? ['reveal', 'trigger'] : 'reveal'}
+ onSelectionChange={(val: (string | number) | (string | number)[]) => {
+ if (val === 'reveal') this._props.layoutDoc.revealOp = this._props.layoutDoc.revealOp === flashcardRevealOp.SLIDE ? flashcardRevealOp.FLIP : flashcardRevealOp.SLIDE;
+ if (val === 'trigger') this._props.layoutDoc.revealOp_hover = !this._props.layoutDoc.revealOp_hover;
+ }}
/>
</div>
);
}
- 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
+ tryFilterOut = (doc: Doc) => (this.practiceMode && BoolCast(doc?._flashcardType) && 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 : (
@@ -195,7 +208,7 @@ export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProp
{this.practiceModesMenu}
</div>
)}
- </>
+ </div>
);
}
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 5bfdee1f5..60728877a 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -285,6 +285,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> {
static Activate = (tabDoc: Doc) => {
const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue);
+ if (tab && tab.header.parent._activeContentItem === tab.contentItem) return false;
tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
return tab !== undefined;
};
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
index 6ad67a864..8530f7a23 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts
@@ -1,4 +1,4 @@
-import { action, observable } from 'mobx';
+import { action, observable, untracked } from 'mobx';
import { CollectionFreeFormView } from '.';
import { intersectRect } from '../../../../Utils';
import { Doc, Opt } from '../../../../fields/Doc';
@@ -179,7 +179,9 @@ export class CollectionFreeFormClusters {
};
styleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
- if (doc && this.childDocs?.includes(doc))
+ // without untracked, every inquired style property for any Doc will be invalidated if a change is made to the collection's childDocs.
+ // this prevents that by assuming that a Doc is generally always (or never) a member of childDocs - if it's removed or added, then all of its properties get updated anyway.
+ if (doc && untracked(() => this.childDocs)?.includes(doc))
switch (property.split(':')[0]) {
case StyleProp.BackgroundColor:
{
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
index 5d8373fc7..8b9a3e0ec 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoUI.tsx
@@ -29,7 +29,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
}
_firstDocPos = { x: 0, y: 0 };
- constructor(props: any) {
+ constructor(props: CollectionFreeFormInfoUIProps) {
super(props);
makeObservable(this);
this._currState = this.setupStates();
@@ -163,7 +163,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
return presentDocs;
}],
// eslint-disable-next-line no-use-before-define
- activePen: [() => activeTool() === InkTool.Pen, () => penMode],
+ activePen: [() => activeTool() === InkTool.Ink, () => penMode],
},
'documentation.png',
() => TopBar.Instance.FlipDocumentationIcon()
@@ -187,7 +187,7 @@ export class CollectionFreeFormInfoUI extends ObservableReactComponent<Collectio
const penMode = InfoState('You\'re in pen mode. Click and drag to draw your first masterpiece.', {
// activePen: [() => activeTool() === InkTool.Eraser, () => eraserMode],
- activePen: [() => activeTool() !== InkTool.Pen, () => viewedLink],
+ activePen: [() => activeTool() !== InkTool.Ink, () => viewedLink],
}); // prettier-ignore
// const eraserMode = InfoState('You\'re in eraser mode. Say goodbye to your first masterpiece.', {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8d73601d1..65b2702c6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -10,7 +10,7 @@ import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc';
import { DocData, Height, Width } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
-import { InkData, InkField, InkTool, Segment } from '../../../../fields/InkField';
+import { InkData, InkEraserTool, InkField, InkInkTool, InkTool, Segment } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { RichTextField } from '../../../../fields/RichTextField';
import { listSpec } from '../../../../fields/Schema';
@@ -36,11 +36,24 @@ import { ContextMenu } from '../../ContextMenu';
import { InkingStroke } from '../../InkingStroke';
import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp';
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView';
+import {
+ ActiveInkArrowEnd,
+ ActiveInkArrowStart,
+ ActiveInkDash,
+ ActiveEraserWidth,
+ ActiveInkFillColor,
+ ActiveInkBezierApprox,
+ ActiveInkColor,
+ ActiveInkWidth,
+ ActiveIsInkMask,
+ DocumentView,
+ SetActiveInkColor,
+ SetActiveInkWidth,
+} from '../../nodes/DocumentView';
import { FieldViewProps } from '../../nodes/FieldView';
import { FocusViewOptions } from '../../nodes/FocusViewOptions';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere';
+import { OpenWhere } from '../../nodes/OpenWhere';
import { PinDocView, PinProps } from '../../PinFuncs';
import { StickerPalette } from '../../smartdraw/StickerPalette';
import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
@@ -389,7 +402,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return undefined;
};
- getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
+ getView = (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> =>
new Promise<Opt<DocumentView>>(res => {
if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false);
if (doc === this.Document) {
@@ -497,13 +510,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// prettier-ignore
const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
switch (Doc.ActiveTool) {
- case InkTool.Highlighter:
- case InkTool.Write:
- case InkTool.Pen:
+ case InkTool.Ink:
break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
- case InkTool.StrokeEraser:
- case InkTool.SegmentEraser:
- case InkTool.RadiusEraser:
+ case InkTool.Eraser:
this._batch = UndoManager.StartBatch('collectionErase');
this._eraserPts.length = 0;
setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
@@ -543,7 +552,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const { points } = ge;
const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
const inkDoc = this.createInkDoc(points, B);
- if (Doc.ActiveTool === InkTool.Write) {
+ if (Doc.ActiveInk === InkInkTool.Highlight) inkDoc[DocData].backgroundColor = 'transparent';
+ if (Doc.ActiveInk === InkInkTool.Write) {
this.unprocessedDocs.push(inkDoc);
CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
}
@@ -606,7 +616,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
const currPoint = { X: e.clientX, Y: e.clientY };
this._eraserPts.push([currPoint.X, currPoint.Y]);
this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
- if (Doc.ActiveTool === InkTool.RadiusEraser) {
+ if (Doc.ActiveEraser === InkEraserTool.Radius) {
const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
strokeMap.forEach((intersects, stroke) => {
if (!this._deleteList.includes(stroke)) {
@@ -614,13 +624,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
const segments = this.radiusErase(stroke, intersects.sort());
- segments?.forEach(segment =>
- this.forceStrokeGesture(
- e,
- Gestures.Stroke,
- segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
- )
- );
+ segments?.forEach(segment => {
+ const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+ const bounds = InkField.getBounds(points);
+ const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkDoc = this.createInkDoc(points, B);
+ ['color', 'fillColor', 'stroke_width', 'stroke_dash', 'stroke_bezier'].forEach(field => {
+ inkDoc[DocData][field] = stroke.dataDoc[field];
+ });
+ this.addDocument(inkDoc);
+ });
}
stroke.layoutDoc.opacity = 0;
stroke.layoutDoc.dontIntersect = true;
@@ -632,7 +645,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
// create a new curve by appending all curves of the current segment together in order to render a single new stroke.
- if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+ if (Doc.ActiveEraser !== InkEraserTool.Stroke) {
// this._eraserLock++;
const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
const newStrokes = segments?.map(segment => {
@@ -1195,14 +1208,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
y: B.y - inkWidth / 2,
_width: B.width + inkWidth,
_height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: !BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
ActiveInkColor(),
ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ ActiveInkFillColor(),
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
};
@@ -1235,14 +1248,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
y: B.y - inkWidth / 2,
_width: B.width + inkWidth,
_height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeHideTextLabels)}, // prettier-ignore
inkWidth,
opts.autoColor ? stroke[1] : ActiveInkColor(),
ActiveInkBezierApprox(),
- stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
+ stroke[2] === 'none' ? ActiveInkFillColor() : stroke[2],
+ ActiveInkArrowStart(),
+ ActiveInkArrowEnd(),
+ ActiveInkDash(),
ActiveIsInkMask()
);
this._drawing.push(inkDoc);
@@ -1596,21 +1609,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
case undefined:
case OpenWhere.lightbox:
- {
- const firstDoc = docs[0];
- if (this.layoutDoc._isLightbox) {
- this._lightboxDoc = firstDoc;
- return true;
- }
- if (firstDoc === this.Document || this.childDocList?.includes(firstDoc) || this.childLayoutPairs.map(pair => pair.layout)?.includes(firstDoc)) {
- if (firstDoc.hidden) firstDoc.hidden = false;
- if (!location.includes(OpenWhereMod.always)) return true;
- }
+ if (this.layoutDoc._isLightbox) {
+ this._lightboxDoc = docs[0];
+ return true;
}
- break;
+ return this.addLinkedDocTab(docsIn, location);
default:
}
- return this._props.addDocTab(docs, location);
+ return this._props.addDocTab(docsIn, location);
});
getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData {
const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
@@ -1750,7 +1756,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
});
PinDocView(
anchor,
- { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } },
+ { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ? { ...pinProps.pinData, poslayoutview: pinProps.pinData.dataview } : {}), pannable: !this.Document.isGroup, collectionType: true, filters: true } },
this.Document
);
@@ -2202,7 +2208,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
width: `${100 / this.nativeDimScaling}%`,
height: this._props.getScrollHeight?.() ?? `${100 / this.nativeDimScaling}%`,
}}>
- {Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle && (
+ {Doc.ActiveTool === InkTool.Eraser && Doc.ActiveEraser === InkEraserTool.Radius && this._showEraserCircle && (
<div
onPointerMove={this.onCursorMove}
style={{
diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
index 534f67927..6d51ecac6 100644
--- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
+++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx
@@ -8,7 +8,7 @@ import { observer } from 'mobx-react';
import React from 'react';
import { DivHeight, lightOrDark, returnTrue, setupMoveUpEvents } from '../../../../ClientUtils';
import { emptyFunction } from '../../../../Utils';
-import { Doc, Opt } from '../../../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
import { DocData } from '../../../../fields/DocSymbols';
import { List } from '../../../../fields/List';
import { DocCast, ImageCast, NumCast, StrCast } from '../../../../fields/Types';
@@ -54,7 +54,7 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
@observable _headerRef: HTMLDivElement | null = null;
@observable _listRef: HTMLDivElement | null = null;
- observer = new ResizeObserver(a => {
+ observer = new ResizeObserver(() => {
this._props.setHeight?.(
(this.props.Document._face_showImages ? 20 : 0) + //
(!this._headerRef ? 0 : DivHeight(this._headerRef)) +
@@ -97,9 +97,9 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
const faceMatcher = new FaceMatcher([labeledFaceDescriptor], 1);
const faceAnno =
FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).reduce(
- (prev, faceAnno) => {
- const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(faceAnno.faceDescriptor as List<number>)));
- return match.distance < prev.dist ? { dist: match.distance, faceAnno } : prev;
+ (prev, fAnno) => {
+ const match = faceMatcher.matchDescriptor(new Float32Array(Array.from(fAnno.faceDescriptor as List<number>)));
+ return match.distance < prev.dist ? { dist: match.distance, faceAnno: fAnno } : prev;
},
{ dist: 1, faceAnno: undefined as Opt<Doc> }
).faceAnno ?? imgDoc;
@@ -108,10 +108,18 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
if (faceAnno) {
faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face));
FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document);
- faceAnno.face = this.Document;
+ faceAnno[DocData].face = this.Document[DocData];
}
}
});
+ de.complete.docDragData?.droppedDocuments
+ ?.filter(doc => DocCast(doc.face)?.type === DocumentType.UFACE)
+ .forEach(faceAnno => {
+ const imgDoc = faceAnno;
+ faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, DocCast(faceAnno.face));
+ FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document);
+ faceAnno[DocData].face = this.Document[DocData];
+ });
e.stopPropagation();
return true;
}
@@ -189,7 +197,8 @@ export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() {
this,
e,
() => {
- DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([doc], dropActionType.embed), e.clientX, e.clientY);
+ const dragDoc = DocListCast(doc.data_annotations).find(a => a.face === this.Document[DocData]) ?? this.Document;
+ DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([dragDoc], dropActionType.embed), e.clientX, e.clientY);
return true;
},
emptyFunction,
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index c865c681d..ddc50871d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -690,7 +690,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
}}
style={{
overflow: StrCast(this._props.Document._overflow),
- cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
+ cursor: Doc.ActiveTool === InkTool.Ink || this._visible ? 'crosshair' : 'pointer',
}}
onDragOver={e => e.preventDefault()}
onScroll={e => {
diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
index 6ffb0865a..7f519b065 100644
--- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx
@@ -188,7 +188,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() {
selectedCells={this.selectedCells}
selectedCol={this.selectedCol}
setColumnValues={this.setColumnValues}
- oneLine={BoolCast(this.schemaDoc?._singleLine)}
+ oneLine={BoolCast(this.schemaDoc?._schema_singleLine)}
menuTarget={this.schemaView.MenuTarget}
transform={() => {
const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index;