diff options
| author | eleanor-park <eleanor_park@brown.edu> | 2024-10-30 19:39:46 -0400 |
|---|---|---|
| committer | eleanor-park <eleanor_park@brown.edu> | 2024-10-30 19:39:46 -0400 |
| commit | c11c760db62f78a07b624b98b209e6ee86036c8e (patch) | |
| tree | c9587b50042a5115373e91ba8ecf9b76913cd321 /src/client/views/collections | |
| parent | b5944e87f9d4f3149161de4de0d76db486461c76 (diff) | |
| parent | 4c768162e0436115a05b9c8b0e4d837d626d45ba (diff) | |
Merge branch 'master' into eleanor-gptdraw
Diffstat (limited to 'src/client/views/collections')
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; |
