diff options
| author | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2025-03-04 04:32:50 -0500 |
|---|---|---|
| committer | Nathan-SR <144961007+Nathan-SR@users.noreply.github.com> | 2025-03-04 04:32:50 -0500 |
| commit | 95abdada5a275fc258fa72781f7f3c40c0b306ea (patch) | |
| tree | 6d729cebe0937ae81108005de9895b5398d1f475 /src/client/views/collections | |
| parent | 0a8f3739cf5c30852f18751a4c05d81e0dabe928 (diff) | |
| parent | 215ad40efa2e343e290d18bffbc55884829f1a0d (diff) | |
Merge branch 'master' of https://github.com/brown-dash/Dash-Web into Merge
Diffstat (limited to 'src/client/views/collections')
49 files changed, 1595 insertions, 1098 deletions
diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss index d1731c244..e6cc398af 100644 --- a/src/client/views/collections/CollectionCardDeckView.scss +++ b/src/client/views/collections/CollectionCardDeckView.scss @@ -1,28 +1,45 @@ -@import '../global/globalCssVariables.module.scss'; - .collectionCardView-outer { height: 100%; width: 100%; position: relative; background-color: white; overflow: hidden; + display: flex; + .collectionCardView-inner { + display: flex; + transform-origin: top left; + align-items: center; + position: relative; + } 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; + 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 +} +.collectionCardView-cardSizeDragger { + position: absolute; + top: 0; + width: 28px; + height: 28px; + > svg { + width: 100%; + height: 100%; + } } .no-card-span { diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index a9ab9de26..756b37f99 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,36 +1,32 @@ -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import * as CSS from 'csstype'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; +import { computedFn } from 'mobx-utils'; import * as React from 'react'; -import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils'; -import { Doc } from '../../../fields/Doc'; -import { DocData } from '../../../fields/DocSymbols'; +import { ClientUtils, returnFalse, returnNever, returnZero, setupMoveUpEvents } from '../../../ClientUtils'; +import { emptyFunction } from '../../../Utils'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Animation } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; -import { BoolCast, DateCast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { URLField } from '../../../fields/URLField'; -import { gptImageLabel } from '../../apis/gpt/GPT'; +import { ScriptField } from '../../../fields/ScriptField'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; +import { Docs } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; +import { SettingsManager } from '../../util/SettingsManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoable } from '../../util/UndoManager'; +import { undoable, UndoManager } from '../../util/UndoManager'; +import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { TagItem } from '../TagsView'; -import { DocumentView } from '../nodes/DocumentView'; -import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCardDeckView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -import { computedFn } from 'mobx-utils'; - -enum cardSortings { - Time = 'time', - Type = 'type', - Color = 'color', - Chat = 'chat', - Tag = 'tag', - None = '', -} /** * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily @@ -43,73 +39,41 @@ enum cardSortings { export class CollectionCardView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [key: string]: IReactionDisposer } = {}; - private _textToDoc = new Map<string, Doc>(); + private _oldWheel: HTMLElement | null = null; private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center) + private _setCurDocScript = () => ScriptField.MakeScript('scriptContext.layoutDoc._card_curDoc=this', { scriptContext: 'any' })!; + private _draggerRef = React.createRef<HTMLDivElement>(); @observable _forceChildXf = 0; @observable _hoveredNodeIndex = -1; @observable _docRefs = new ObservableMap<Doc, DocumentView>(); - @observable _maxRowCount = 10; - @observable _docDraggedIndex: number = -1; - - static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { - try { - const response = await fetch(imageUrl); - const blob = await response.blob(); - - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.readAsDataURL(blob); - reader.onloadend = () => resolve(reader.result as string); - reader.onerror = error => reject(error); - }); - } catch (error) { - console.error('Error:', error); - throw error; - } - }; + @observable _cursor: CSS.Property.Cursor = 'ew-resize'; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); - this.setRegenerateCallback(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc); } + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = ele; + // prevent wheel events from passively propagating up through containers and prevents containers from preventDefault which would block scrolling + ele?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }; - /** - * Callback to ensure gpt's text versions of the child docs are updated - */ - setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc); - - /** - * update's gpt's doc-text list and initializes callbacks - */ - @action - childPairStringListAndUpdateSortDesc = async () => { - const sortDesc = await this.childPairStringList(); // Await the promise to get the string result - GPTPopup.Instance.setSortDesc(sortDesc.join()); - GPTPopup.Instance.onSortComplete = (sortResult: string, questionType: string, tag?: string) => this.processGptOutput(sortResult, questionType, tag); - GPTPopup.Instance.onQuizRandom = () => this.quizMode(); - }; + @computed get cardWidth() { + return NumCast(this.layoutDoc._cardWidth, 50); + } + @computed get _maxRowCount() { + return Math.ceil(this.cardDeckWidth / this.cardWidth); + } componentDidMount() { this._props.setContentViewBox?.(this); - this._disposers.sort = reaction( - () => GPTPopup.Instance.visible, - isVis => { - if (isVis) { - this.openChatPopup(); - } else { - this.Document.cardSort = this.cardSort === cardSortings.Chat ? '' : this.Document.cardSort; - } - } - ); // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles - // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the + // when inquired from the dom (below in childScreenToLocal). When the doc is actually rendered, we need to act like the // dash data just changed and trigger a React involidation with the correct data (read from the dom). this._disposers.child = reaction( () => [this.Document.x, this.Document.y], @@ -119,6 +83,12 @@ export class CollectionCardView extends CollectionSubView() { } } ); + this._disposers.select = reaction( + () => this.childDocs.find(d => this._docRefs.get(d)?.IsSelected), + selected => { + selected && (this.layoutDoc._card_curDoc = selected); + } + ); } componentWillUnmount() { @@ -126,126 +96,95 @@ export class CollectionCardView extends CollectionSubView() { this._dropDisposer?.(); } - @computed get cardSort() { - return StrCast(this.Document.cardSort) as cardSortings; + /** + * Number of rows of cards to be rendered + */ + @computed get numRows() { + return Math.ceil(this.childDocs.length / this._maxRowCount); + } + /** + * Circle arc size, in radians, to layout cards + */ + @computed get archAngle() { + return NumCast(this.layoutDoc.card_arch, 90) * (Math.PI / 180) * (this.childDocsNoInk.length < this._maxRowCount ? this.childDocsNoInk.length / this._maxRowCount : 1); + } + /** + * Spacing card rows as a percent of Doc size. 100 means rows spread out to fill 100% of the Doc vertically. Default is 60% + */ + @computed get cardSpacing() { + return NumCast(this.layoutDoc.card_spacing, 60); } /** * The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's) */ - @computed get childDocsWithoutLinks() { - return this.childDocs.filter(l => !l.layout_isSvg); + @computed get childDocsNoInk() { + return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg); } /** * how much to scale down the contents of the view so that everything will fit */ @computed get fitContentScale() { - const length = Math.min(this.childDocsWithoutLinks.length, this._maxRowCount); - return (this.childPanelWidth() * length) / this._props.PanelWidth(); + const length = Math.min(this.childDocsNoInk.length, this._maxRowCount); + return (this.childPanelWidth() * length) / (this._props.PanelWidth() - 2 * this.xMargin); } - /** - * When in quiz mode, randomly selects a document - */ - quizMode = () => { - const randomIndex = Math.floor(Math.random() * this.childDocs.length); - DocumentView.getDocumentView(this.childDocs[randomIndex])?.select(false); - }; + @computed get nativeScaling() { + return this._props.NativeDimScaling?.() || 1; + } - /** - * Number of rows of cards to be rendered - */ - @computed get numRows() { - return Math.ceil(this.sortedDocs.length / this._maxRowCount); + @computed get xMargin() { + return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth())); } - @action - setHoveredNodeIndex = (index: number) => { + @computed get yMargin() { + return this._props.yPadding || NumCast(this.layoutDoc._yMargin, Math.min(5, 0.05 * this._props.PanelWidth())); + } + + @computed get cardDeckWidth() { + return this._props.PanelWidth() - 2 * this.xMargin; + } + + setHoveredNodeIndex = action((index: number) => { if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index; - }; + }); isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected; - childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2); + childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, Math.max(150, this._props.PanelWidth() / (this.childDocsNoInk.length > this._maxRowCount ? this._maxRowCount : this.childDocsNoInk.length) / this.nativeScaling)); childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive(); isAnyChildContentActive = this._props.isAnyChildContentActive; /** - * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row - * @param amCards - * @param index - * @returns - */ - rotate = (amCards: number, index: number) => { - if (amCards == 1) return 0; - - const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2)); - if (amCards % 2 === 0) { - if (possRotate === 0) { - return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2))); - } - if (index > (amCards + 1) / 2) { - const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2))); - return possRotate + stepMag; - } - } - - return possRotate; - }; - /** - * Returns the degree to which a card should be translated in the y direction for the arch effect - */ - translateY = (amCards: number, index: number, realIndex: number) => { - const evenOdd = amCards % 2; - const apex = (amCards - evenOdd) / 2; - const Magnitude = this.childPanelWidth() / 2; // 400 - const stepMag = Magnitude / 2 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25); - - let rowOffset = 0; - if (realIndex > this._maxRowCount - 1) { - rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount); - } - if (evenOdd === 1 || index < apex - 1) { - return Math.abs(stepMag * (apex - index)) - rowOffset; - } - if (index === apex || index === apex - 1) { - return 0 - rowOffset; - } - - return Math.abs(stepMag * (apex - index - 1)) - rowOffset; - }; - - /** * When dragging a card, determines the index the card should be set to if dropped * @param mouseX mouse's x location * @param mouseY mouses' y location * @returns the card's new index */ findCardDropIndex = (mouseX: number, mouseY: number) => { - const amCardsTotal = this.sortedDocs.length; + const cardCount = this.childDocs.length; let index = 0; - const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount; + const cardWidth = cardCount < this._maxRowCount ? this._props.PanelWidth() / cardCount : this._props.PanelWidth() / this._maxRowCount; // Calculate the adjusted X position accounting for the initial offset let adjustedX = mouseX; - const amRows = Math.ceil(amCardsTotal / this._maxRowCount); - const rowHeight = this._props.PanelHeight() / amRows; + const rowHeight = this._props.PanelHeight() / this.numRows; const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0 if (adjustedX < 0) { return 0; // Before the first column } - if (amCardsTotal < this._maxRowCount) { + if (cardCount < this._maxRowCount) { index = Math.floor(adjustedX / cardWidth); - } else if (currRow != amRows - 1) { + } else if (currRow != this.numRows - 1) { index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount; } else { - const rowAmCards = amCardsTotal - currRow * this._maxRowCount; - const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth; + const cardsInRow = cardCount - currRow * this._maxRowCount; + const offset = ((this._maxRowCount - cardsInRow) / 2) * cardWidth; adjustedX = mouseX - offset; index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount; @@ -254,11 +193,14 @@ export class CollectionCardView extends CollectionSubView() { }; /** - * Checks to see if a card is being dragged and calls the appropriate methods if so + * if pointer moves over cardDeck while dragging a Doc that is in the Deck or that can be dropped in the deck, + * then this sets the card index where the dragged card would be added. */ @action onPointerMove = (x: number, y: number) => { - this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1; + if (DragManager.docsBeingDragged.some(doc => this.childDocs.includes(doc)) || SnappingManager.CanEmbed) { + this.docDraggedIndex = this.findCardDropIndex(x, y); + } }; /** @@ -270,14 +212,14 @@ export class CollectionCardView extends CollectionSubView() { onInternalDrop = undoable( action((e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { - const dragIndex = this._docDraggedIndex; + const dragIndex = this.docDraggedIndex; const draggedDoc = DragManager.docsBeingDragged[0]; if (dragIndex > -1 && draggedDoc) { - this._docDraggedIndex = -1; - const sorted = this.sortedDocs; + this.docDraggedIndex = -1; + const sorted = this.childDocs; const originalIndex = sorted.findIndex(doc => doc === draggedDoc); - this.Document.cardSort = ''; + this.Document[this._props.fieldKey + '_sort'] = ''; originalIndex !== -1 && sorted.splice(originalIndex, 1); sorted.splice(dragIndex, 0, draggedDoc); if (de.complete.docDragData.removeDocument?.(draggedDoc)) { @@ -293,10 +235,6 @@ export class CollectionCardView extends CollectionSubView() { '' ); - @computed get sortedDocs() { - return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.Document.cardSort_isDesc), this._docDraggedIndex); - } - /** * Used to determine how to sort cards based on tags. The leftmost tags are given lower values while cards to the right are * given higher values. Decimals are used to determine placement for cards with multiple tags @@ -310,48 +248,16 @@ export class CollectionCardView extends CollectionSubView() { .map(({ i }) => i) .join('.'); - /** - * Called in the sortedDocsType method. Compares the cards' value in regards to the desired sort type-- earlier cards are move to the - * front, latter cards to the back - * @param docs - * @param sortType - * @param isDesc - * @returns - */ - sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => { - const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching - sortType && - docs.sort((docA, docB) => { - const [typeA, typeB] = (() => { - switch (sortType) { - default: - case cardSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; - case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)]; - case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; - case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; - } - })(); //prettier-ignore - return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1); - }); - if (dragIndex !== -1) { - const draggedDoc = DragManager.docsBeingDragged[0]; - const originalIndex = docs.findIndex(doc => doc === draggedDoc); - - originalIndex !== -1 && docs.splice(originalIndex, 1); - draggedDoc && docs.splice(dragIndex, 0, draggedDoc); - } - - return docs; - }; - - isChildContentActive = () => - this._props.isContentActive?.() === false - ? false - : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) - ? true - : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + isChildContentActive = computedFn( + (doc: Doc) => () => + this._props.isContentActive?.() === false ? false - : undefined; + : this._props.isDocumentActive?.() && this.curDoc() === doc + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined + ); // prettier-ignore displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => ( <DocumentView @@ -360,267 +266,218 @@ export class CollectionCardView extends CollectionSubView() { Document={doc} NativeWidth={returnZero} NativeHeight={returnZero} - fitWidth={returnFalse} - onDoubleClickScript={this.onChildDoubleClick} + PanelWidth={this.childPanelWidth} + PanelHeight={this.childPanelHeight} renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} containerViewPath={this.childContainerViewPath} ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} - PanelWidth={this.childPanelWidth} - PanelHeight={this.childPanelHeight} + isContentActive={this.isChildContentActive(doc)} + fitWidth={returnFalse} + waitForDoubleClickToClick={returnNever} + scriptContext={this} + focus={this.focus} + onDoubleClickScript={this.onChildDoubleClick} + onClickScript={this.curDoc() === doc ? undefined : this._setCurDocScript} dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice. dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType} - showTags={BoolCast(this.layoutDoc.showChildTags)} + showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} - isContentActive={this.isChildContentActive} dontHideOnDrag /> ); /** * Determines how many cards are in the row of a card at a specific index - * @param index - * @returns + * @param index numerical index of card in total list of all cards + * @returns number of cards in row that contains index */ - overflowAmCardsCalc = (index: number) => { - if (this.sortedDocs.length < this._maxRowCount) { - return this.sortedDocs.length; + cardsInRowThatIncludesCardIndex = (index: number) => { + if (this.childDocsNoInk.length < this._maxRowCount) { + return this.childDocsNoInk.length; } - const totalCards = this.sortedDocs.length; - // if 9 or less + const totalCards = this.childDocsNoInk.length; if (index < totalCards - (totalCards % this._maxRowCount)) { return this._maxRowCount; } return totalCards % this._maxRowCount; }; /** - * Determines the index a card is in in a row - * @param realIndex - * @returns - */ - overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount; - /** - * Translates the cards in the second rows and beyond over to the right - * @param realIndex - * @param calcIndex - * @param calcRowCards - * @returns - */ - translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2)); - - /** - * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected - * @param isHovered - * @param isActive - * @param realIndex - * @param amCards - * @param calcRowIndex - * @returns + * Determines the index a card is in in a row. If the row is not full, then the cards + * are centered within the row (as if unrendered cards had been added to the start and end + * of the row) and the retuned index is the index the card in this virtual full row. + * @param index numerical index of card in total list of all cards + * @returns index of card in its row, normalized to a full size row */ - calculateTranslateY = (isHovered: boolean, isActive: boolean, realIndex: number, amCards: number, calcRowIndex: number) => { - const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows; - const rowIndex = Math.trunc(realIndex / this._maxRowCount); - const rowToCenterShift = this.numRows / 2 - rowIndex; - if (isActive) return rowToCenterShift * rowHeight - rowHeight / 2; - if (amCards == 1) return 50 * this.fitContentScale; - return this.translateY(amCards, calcRowIndex, realIndex); + centeredIndexOfCardInRow = (index: number) => { + const cardsInRow = this.cardsInRowThatIncludesCardIndex(index); + const lineIndex = index % this._maxRowCount; + if (cardsInRow === this._maxRowCount) return lineIndex; + return lineIndex + (this._maxRowCount - cardsInRow) / 2; }; - /** - * A list of the text content of all the child docs. RTF documents will have just their text and pdf documents will have the first 50 words. - * Image documents are converted to bse64 and gpt generates a description for them. all other documents use their title. This string is - * inputted into the gpt prompt to sort everything together - * @returns + * Returns the rotation of a card in radians based on its horizontal location (and thus m apping to a circle arc). + * The amount of rotation is goverend by the Doc's card_arch field which specifies, in degrees, the range of a circle + * arc that cards should cover -- by default, -45 to 45 degrees. + * @param index numerical index of card in total list of all cards + * @returns angle of rotation in radians */ - childPairStringList = () => { - const docToText = (doc: Doc) => { - switch (doc.type) { - case DocumentType.PDF: return StrCast(doc.text).split(/\s+/).slice(0, 50).join(' '); // first 50 words of pdf text - case DocumentType.IMG: return this.getImageDesc(doc); - case DocumentType.RTF: return StrCast(RTFCast(doc.text).Text); - default: return StrCast(doc.title); - } // prettier-ignore - }; - const docTextPromises = this.childDocsWithoutLinks.map(async doc => { - const docText = (await docToText(doc)) ?? ''; - doc.gptInputText = docText; - this._textToDoc.set(docText.replace(/\n/g, ' ').trim(), doc); - return `======${docText.replace(/\n/g, ' ').trim()}======`; - }); - return Promise.all<string>(docTextPromises); + rotate = (index: number) => { + const cardsInRow = this.cardsInRowThatIncludesCardIndex(index); + const centeredIndexInRow = (cardsInRow < this._maxRowCount ? index + (this._maxRowCount - cardsInRow) / 2 : index) % this._maxRowCount; + const rowIndexMax = this._maxRowCount - 1; + return ((this.archAngle / 2) * (centeredIndexInRow - rowIndexMax / 2)) / (rowIndexMax / 2); }; - /** - * Calls the gpt API to generate descriptions for the images in the view - * @param image - * @returns + * Provides a vertical adjustment to a card's grid position so that it will lie along an arch. + * @param index numerical index of card in total list of all cards */ - getImageDesc = async (image: Doc) => { - if (StrCast(image.description)) return StrCast(image.description); // Return existing description - const { href } = (image.data as URLField).url; - const hrefParts = href.split('.'); - const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; - try { - const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete); - const response = await gptImageLabel(hrefBase64); - image[DocData].description = response.trim(); - return response; // Return the response from gptImageLabel - } catch (error) { - console.log(error); - } - return ''; + translateY = (index: number) => { + const Magnitude = ((this._props.PanelHeight() * this.fitContentScale) / 2) * Math.sqrt(((this.archAngle * (180 / Math.PI)) / 60) * 4); + return Magnitude * (1 - Math.sin(this.rotate(index) + Math.PI / 2) - (1 - Math.sin(this.archAngle / 2 + Math.PI / 2)) / 2); }; - /** - * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to - * usable code - * @param gptOutput + * When the card index is for a row (not the first row) that is not full, this returns a horizontal adjustment that centers the row + * @param index index of card from start of deck + * @param cardsInRow number of cards in the row containing the indexed card + * @returns horizontal pixel translation */ - @action - processGptOutput = undoable((gptOutput: string, questionType: string, tag?: string) => { - // Split the string into individual list items - const listItems = gptOutput.split('======').filter(item => item.trim() !== ''); - - if (questionType === '2' || questionType === '4') { - this.childDocs.forEach(d => { - d.chatFilter = false; - }); - } - - if (questionType === '6') { - this.Document.cardSort = 'chat'; - } - - listItems.forEach((item, index) => { - const normalizedItem = item.trim(); - // find the corresponding Doc in the textToDoc map - const doc = this._textToDoc.get(normalizedItem); - - if (doc) { - switch (questionType) { - case '6': - doc.chatIndex = index; - break; - case '1': { - const allHotKeys = Doc.MyFilterHotKeys; - - let myTag = ''; - - if (tag) { - for (let i = 0; i < allHotKeys.length; i++) { - // bcz: CHECK THIS CODE OUT -- SOMETHING CHANGED - const keyTag = StrCast(allHotKeys[i].toolType); - if (tag.includes(keyTag)) { - myTag = keyTag; - break; - } - } - - if (myTag != '') { - doc[myTag] = true; - } - } - break; - } - case '2': - case '4': - doc.chatFilter = true; - Doc.setDocFilter(DocCast(this.Document.embedContainer), 'chatFilter', true, 'match'); - break; - } - } else { - console.warn(`No matching document found for item: ${normalizedItem}`); - } - }); - }, ''); + horizontalAdjustmentForPartialRows = (index: number, cardsInRow: number) => (index < this._maxRowCount ? 0 : (this._maxRowCount - cardsInRow) * (this.childPanelWidth() / 2)); /** - * Opens up the chat popup and starts the process for smart sorting. + * Adjusts the vertical placement of the card from its grid position so that it will either line on a + * circular arc if the card isn't active, or so that it will be centered otherwise. + * @param isActive whether the card is focused for interaction + * @param index index of card from start of deck + * @returns vertical pixel translation */ - openChatPopup = async () => { - GPTPopup.Instance.setVisible(true); - GPTPopup.Instance.setMode(GPTPopupMode.CARD); - GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded - await this.childPairStringListAndUpdateSortDesc(); + adjustCardYtoFitArch = (isActive: boolean, index: number) => { + const rowHeight = this._props.PanelHeight() / this.numRows; + const rowIndex = Math.floor(index / this._maxRowCount); + const rowToCenterShift = this.numRows / 2 - rowIndex; + return isActive + ? (rowToCenterShift * rowHeight - rowHeight / 2) * ((this.cardSpacing * this.fitContentScale) / 100) // + : this.translateY(index); }; - childScreenToLocal = computedFn((doc: Doc, index: number, calcRowIndex: number, isSelected: boolean, amCards: number) => () => { + childScreenToLocal = computedFn((doc: Doc, index: number, isSelected: boolean) => () => { // need to explicitly trigger an invalidation since we're reading everything from the Dom this._forceChildXf; this._props.ScreenToLocalTransform(); const dref = this._docRefs.get(doc); const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); - if (!scale) return new Transform(0, 0, 0); + if (!scale) return new Transform(0, 0, 1); return new Transform(-translateX + (dref?.centeringX || 0) * scale, -translateY + (dref?.centeringY || 0) * scale, 1) - .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore + .scale(1 / scale).rotate(!isSelected ? -this.rotate(this.centeredIndexOfCardInRow(index)) : 0); // prettier-ignore + }); + + /** + * Releases the currently focused Doc by deselecting it and returning it to its location on the arch, and selecting the + * cardDeck itself. + * This will also force the Doc to recompute its layout transform when the animation completes. + * In addition, this sets an animating flag on the Doc so that it will receive no poiner events when animating, such as hover + * events that would trigger a flashcard to flip. + * @param doc doc that will be animated away from center focus + */ + releaseCurDoc = action(() => { + const selDoc = this.curDoc(); + this.layoutDoc._card_curDoc = undefined; + const cardDocView = DocumentView.getDocumentView(selDoc, this.DocumentView?.()); + if (cardDocView && selDoc) { + DocumentView.DeselectView(cardDocView); + this._props.select(false); + selDoc[Animation] = selDoc; // turns off pointer events & doc decorations while animating - useful for flashcards that reveal back on hover + setTimeout(action(() => { + selDoc[Animation] = undefined; + this._forceChildXf++; + }), 350); // prettier-ignore + } }); + cardSizerDown = (e: React.PointerEvent) => { + runInAction(() => { + this._cursor = 'grabbing'; + }); + const batch = UndoManager.StartBatch('card view size'); + setupMoveUpEvents( + this, + e, + (emove: PointerEvent) => { + this.layoutDoc._cardWidth = Math.max(10, this.ScreenToLocalBoxXf().transformPoint(emove.clientX, 0)[0] - this.xMargin); + return false; + }, + action(() => { + this._cursor = 'ew-resize'; + batch.end(); + }), + emptyFunction + ); + }; + + /** + * turns off the _dropped flag at the end of a drag/drop, or releases the focused Doc if a different Doc is clicked + */ cardPointerUp = action((doc: Doc) => { - // if a card doc has just moved, or a card is selected and in front, then ignore this event - if (this.isSelected(doc) || this._dropped) { + if (this.curDoc() === doc || this._dropped) { this._dropped = false; } else { - // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts. - // Turn them back on when the animation has completed and the render and backend structures are in synch - SnappingManager.SetIsResizing(doc[Id]); - setTimeout( - action(() => { - SnappingManager.SetIsResizing(undefined); - this._forceChildXf++; - }), - 1000 + this.releaseCurDoc(); // NOTE: the onClick script for the card will select the new card (ie, 'doc') + } + }); + + focus = action((anchor: Doc, options: FocusViewOptions): Opt<number> => { + const docs = DocListCast(this.Document[this.fieldKey]); + if (anchor.type === DocumentType.CONFIG || docs.includes(anchor)) { + const foundDoc = DocCast( + anchor.config_card_curDoc, + docs.find(doc => doc === DocCast(anchor.annotationOn, anchor)) ); + options.didMove = foundDoc !== this.curDoc() ? true : false; + options.didMove && (this.layoutDoc._card_curDoc = foundDoc); } + return undefined; }); + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_card_curDoc: this.curDoc() }); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document); + addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered + return anchor; + }; + addDocTab = this.addLinkedDocTab; /** * Actually renders all the cards */ @computed get renderCards() { - if (!this.childDocsWithoutLinks.length) { - return ( - <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}> - Sorry ! There are no cards in this group - </span> - ); - } - // Map sorted documents to their rendered components - return this.sortedDocs.map((doc, index) => { - const realIndex = this.sortedDocs.indexOf(doc); - const calcRowIndex = this.overflowIndexCalc(realIndex); - const amCards = this.overflowAmCardsCalc(realIndex); - const view = DocumentView.getDocumentView(doc, this.DocumentView?.()); - - const childScreenToLocal = this.childScreenToLocal(doc, index, calcRowIndex, !!view?.IsContentActive, amCards); - - const translateIfSelected = () => { - const indexInRow = index % this._maxRowCount; - const rowIndex = Math.trunc(index / this._maxRowCount); - const rowCenterIndex = Math.min(this._maxRowCount, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2; - return (rowCenterIndex - indexInRow) * 100 - 50; - }; + return this.childDocs.map((doc, index) => { + const cardsInRow = this.cardsInRowThatIncludesCardIndex(index); + + const childScreenToLocal = this.childScreenToLocal(doc, index, doc === this.curDoc()); + + const translateToCenterIfActive = () => (doc === this.curDoc() ? (cardsInRow / 2 - (index % this._maxRowCount)) * 100 - 50 : 0); + const aspect = NumCast(doc.height) / NumCast(doc.width, 1); - const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), + const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale * this.nativeScaling) / (aspect * this.childPanelWidth()), (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore - const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size + const hscale = Math.min(this.childDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size return ( <div key={doc[Id]} - className={`card-item${view?.IsContentActive ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`} + className={`card-item${doc === this.curDoc() ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`} onPointerUp={() => this.cardPointerUp(doc)} style={{ width: this.childPanelWidth(), height: 'max-content', - transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, !!view?.IsContentActive, realIndex, amCards, calcRowIndex)}px) - translateX(calc(${view?.IsContentActive ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px)) - rotate(${!view?.IsContentActive ? this.rotate(amCards, calcRowIndex) : 0}deg) - scale(${view?.IsContentActive ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`, + transform: `translateY(${this.adjustCardYtoFitArch(doc === this.curDoc(), index)}px) + translateX(calc(${translateToCenterIfActive()}% + ${this.horizontalAdjustmentForPartialRows(index, cardsInRow)}px)) + rotate(${doc !== this.curDoc()? this.rotate(index) : 0}rad) + scale(${doc === this.curDoc()? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`, }} // prettier-ignore onPointerEnter={() => this.setHoveredNodeIndex(index)} onPointerLeave={() => this.setHoveredNodeIndex(-1)}> @@ -629,29 +486,70 @@ export class CollectionCardView extends CollectionSubView() { ); }); } + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); + + contentScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); + docViewProps = (): DocumentViewProps => ({ + ...this._props, // + isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, + isContentActive: emptyFunction, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + answered = () => { + this.layoutDoc._card_curDoc = this.curDoc() ? this.filteredChildDocs()[(this.filteredChildDocs().findIndex(d => d === this.curDoc()) + 1) % (this.filteredChildDocs().length || 1)] : undefined; + }; + curDoc = () => DocCast(this.layoutDoc._card_curDoc); render() { - const isEmpty = this.childDocsWithoutLinks.length === 0; - + const fitContentScale = this.childDocsNoInk.length === 0 ? 1 : this.fitContentScale; return ( <div className="collectionCardView-outer" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} - onPointerLeave={action(() => (this._docDraggedIndex = -1))} + onPointerDown={e => e.button !== 2 && !e.ctrlKey && this.releaseCurDoc()} + onPointerLeave={action(() => (this.docDraggedIndex = -1))} onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))} onDrop={this.onExternalDrop.bind(this)} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, + paddingLeft: this.xMargin, + paddingRight: this.xMargin, }}> <div - className="card-wrapper" + className="collectionCardView-inner" style={{ - ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }), - ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }), - gridAutoRows: `${100 / this.numRows}%`, + transform: `scale(${1 / fitContentScale})`, + height: `${100 * fitContentScale}%`, + width: `${100 * fitContentScale}%`, + top: this.yMargin, }}> - {this.renderCards} + <div + className="collectionCardView-cardwrapper" + style={{ + gridTemplateColumns: `repeat(${this._maxRowCount}, 1fr)`, + gridAutoRows: `${100 / this.numRows}%`, + height: `${this.cardSpacing}%`, + }}> + {this.renderCards} + </div> + <div + className="collectionCardView-flashcardUI" + style={{ + pointerEvents: this.childDocsNoInk.length === 0 ? undefined : 'none', + height: `${100 / this.nativeScaling / fitContentScale}%`, + width: `${100 / this.nativeScaling / fitContentScale}%`, + transform: `scale(${this.nativeScaling * fitContentScale})`, + }}></div> + </div> + + {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} + <div + className="collectionCardView-cardSizeDragger" + onPointerDown={this.cardSizerDown} + ref={this._draggerRef} + style={{ display: this._props.isContentActive() ? undefined : 'none', cursor: this._cursor, color: SettingsManager.userColor, left: `${this.cardWidth + this.xMargin}px` }}> + <FontAwesomeIcon icon="arrows-alt-h" /> </div> </div> ); diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index a556d0fa7..13e6b54c2 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -1,16 +1,17 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionCarousel3DView-outer { height: 100%; position: relative; background-color: white; overflow: hidden; + display: flex; } .carousel-wrapper { display: flex; position: absolute; - top: $CAROUSEL3D_TOP * 1%; - height: ($CAROUSEL3D_SIDE_SCALE * 100) * 1%; + top: global.$CAROUSEL3D_TOP * 1%; + height: (global.$CAROUSEL3D_SIDE_SCALE * 100) * 1%; align-items: center; transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); @@ -23,7 +24,7 @@ pointer-events: none; opacity: 0.5; z-index: 1; - transform: scale($CAROUSEL3D_SIDE_SCALE); + transform: scale(global.$CAROUSEL3D_SIDE_SCALE); user-select: none; } @@ -31,7 +32,7 @@ pointer-events: unset; opacity: 1; z-index: 2; - transform: scale($CAROUSEL3D_CENTER_SCALE); + transform: scale(global.$CAROUSEL3D_CENTER_SCALE); } } @@ -79,7 +80,7 @@ .carousel3DView-back { top: 0; background: transparent; - width: calc((1 - #{$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%); + width: calc((1 - #{global.$CAROUSEL3D_CENTER_SCALE} * 0.33) / 2 * 100%); height: 100%; } diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index c5da8e037..9c8ef5519 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -1,36 +1,40 @@ 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'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; -import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +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, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -import { Transform } from '../../util/Transform'; // eslint-disable-next-line @typescript-eslint/no-require-imports const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @observer export class CollectionCarousel3DView extends CollectionSubView() { - @computed get scrollSpeed() { - return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed - } + private _dropDisposer?: DragManager.DragDropDisposer; + private _oldWheel: HTMLElement | null = null; + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } - private _dropDisposer?: DragManager.DragDropDisposer; - + componentDidMount(): void { + this._props.setContentViewBox?.(this); + } componentWillUnmount() { this._dropDisposer?.(); } @@ -40,58 +44,83 @@ 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() { + return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed + } @computed get carouselItems() { - return this.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); + return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg); } 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.isContentActive(); + isChildContentActive = computedFn( + (doc: Doc) => () => + this._props.isContentActive?.() === false + ? false + : 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._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) - .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + this.contentScreenToLocalXf() + .translate( + (-this.panelWidth() * (1 - this.sideScale)) / 2, // + (-this.panelHeight() * (1 - this.sideScale)) / 2 + ) .scale(1 / this.sideScale); childScreenRightToLocal = () => - this._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) - .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()) + this.contentScreenToLocalXf() + .translate( + -2 * this.panelWidth() - (this.panelWidth() * (1 - this.sideScale)) / 2, // + (-this.panelHeight() * (1 - this.sideScale)) / 2 + ) .scale(1 / this.sideScale); childCenterScreenToLocal = () => - this._props - .ScreenToLocalTransform() - .scale(this._props.NativeDimScaling?.() || 1) + 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, pinProps?: PinProps) => { + const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.layoutDoc._carousel_index as number }); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document); + addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered + return anchor; + }; + addDocTab = this.addLinkedDocTab; @computed get content() { const currentIndex = NumCast(this.layoutDoc._carousel_index); - const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => ( + const displayDoc = (child: Doc, dxf: () => Transform) => ( <DocumentView {...this._props} - Document={childPair.layout} - TemplateDataDocument={childPair.data} + Document={child} + TemplateDataDocument={undefined} // suppressSetHeight={true} NativeWidth={returnZero} NativeHeight={returnZero} @@ -103,23 +132,23 @@ 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} + showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)} /> ); - return this.carouselItems.map((childPair, index) => ( - <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> - {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)} + return this.carouselItems.map((child, index) => ( + <div key={child.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> + {displayDoc(child.layout, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)} </div> )); } changeSlide = (direction: number) => { - DocumentView.DeselectAll(); - this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % this.carouselItems.length; + this.layoutDoc._carousel_index = !this.curDoc() ? 0 : (NumCast(this.layoutDoc._carousel_index) + direction + this.carouselItems.length) % (this.carouselItems.length || 1); }; onArrowClick = (direction: number) => { @@ -192,6 +221,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() || 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._props.isContentActive, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + nativeScaling = () => this._props.NativeDimScaling?.() || 1; render() { return ( <div @@ -200,12 +239,19 @@ 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} </div> {this.buttons} - <div className="dot-bar">{this.dots}</div> + <div className="dot-bar" style={{ transform: `scale(${this.uiBtnScaling})` }}> + {this.dots} + </div> + {this.flashCardUI(this.curDoc, this.docViewProps, this.answered)} </div> ); } diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index 01b20d6d3..544b3e262 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -1,5 +1,8 @@ .collectionCarouselView-outer { height: 100%; + position: relative; + overflow: hidden; + display: flex; .collectionCarouselView-caption { height: 50; display: inline-block; @@ -10,15 +13,11 @@ display: inline-block; width: 100%; user-select: none; + position: absolute; + top: 0; + left: 0; } } -.collectionCarouselView-addFlashcards { - justify-content: center; - align-items: center; - height: 100%; - z-index: -1; - pointer-events: none; -} .collectionCarouselView-recentlyMissed { color: red; z-index: 999; @@ -28,15 +27,11 @@ pointer-events: none; } .carouselView-back, -.carouselView-fwd, -.carouselView-star, -.carouselView-remove, -.carouselView-check { +.carouselView-fwd { position: absolute; display: flex; - top: 42.5%; width: 30; - height: 15%; + height: 30; align-items: center; border-radius: 5px; justify-content: center; @@ -47,24 +42,15 @@ } } .carouselView-fwd { - right: 20; + top: calc(50% - 15px); + right: 0; + transform-origin: right top; } .carouselView-back { - left: 20; -} -.carouselView-star { - top: 0; - right: 20; -} -.carouselView-remove { - top: 80%; - left: 52%; -} -.carouselView-check { - top: 80%; - right: 52%; + top: calc(50% - 15px); + left: 0; + transform-origin: top left; } - .carouselView-back:hover, .carouselView-fwd:hover { background: lightgray; diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 8b3a699ed..87c6e3e5c 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,64 +1,41 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +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 { ContextMenu } from '../ContextMenu'; +import { PinDocView, PinProps } from '../PinFuncs'; import { StyleProp } from '../StyleProp'; -import { TagItem } from '../TagsView'; 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'; -enum cardMode { - PRACTICE = 'practice', - STAR = 'star', - QUIZ = 'quiz', -} -enum practiceVal { - MISSED = 'missed', - CORRECT = 'correct', -} @observer export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore - get starField() { return "#star"; } // prettier-ignore + _oldWheel: HTMLElement | null = null; _fadeTimer: NodeJS.Timeout | undefined; - _resetter: IReactionDisposer | undefined; + @observable _last_index = this.carouselIndex; + @observable _last_opacity = 1; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } - @observable _last_index = this.carouselIndex; - @observable _last_opacity = 1; - - componentDidMount() { - this._resetter = reaction( - // automatically reset practice fields when all cards have been marked as correct - () => this.carouselItems.length, - itemsCount => { - if (this.layoutDoc.filterOp === cardMode.PRACTICE && !itemsCount) { - this.layoutDoc.filterOp = undefined; // if all of the cards are correct, show all cards and exit practice mode - this.carouselItems.forEach(item => { // reset all the practice values - item[this.practiceField] = undefined; - }); - } - } // prettier-ignore - ); + componentDidMount(): void { + this._props.setContentViewBox?.(this); } componentWillUnmount() { this._dropDisposer?.(); - this._resetter?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { @@ -66,96 +43,78 @@ 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 marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore + @computed get captionMarginX(){ return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore - @computed get carouselItems() { - return this.childDocs - .filter(doc => doc.type !== DocumentType.LINK) - .filter(doc => { - switch (StrCast(this.layoutDoc.filterOp)) { - case cardMode.STAR: return !!doc[this.starField]; // show only cards that are starred - case cardMode.PRACTICE: return doc[this.practiceField] !== practiceVal.CORRECT;// show only cards that aren't marked as correct - default: return true; - } // prettier-ignore - }); - } + @computed get carouselItems() { return this.childLayoutPairs.filter(pair => !pair.layout.layout_isSvg); } // prettier-ignore + /** + * Move forward or backward the specified number of Docs + * @param dir signed number indicating Docs to move forward or backward + */ move = action((dir: number) => { this._last_index = this.carouselIndex; - this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length; + this.layoutDoc._carousel_index = this.carouselItems.length ? (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length : 0; }); /** * 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); - /* - * Stars the document when the star button is pressed. - */ - star = (e: React.MouseEvent) => { - e.stopPropagation(); - const curDoc = this.carouselItems[this.carouselIndex]; - if (curDoc) { - if (TagItem.docHasTag(curDoc, this.starField)) TagItem.removeTagFromDoc(curDoc, this.starField); - else TagItem.addTagToDoc(curDoc, this.starField); + 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; }; - /* - * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. - */ - setPracticeVal = (e: React.MouseEvent, val: string) => { - e.stopPropagation(); - const curDoc = this.carouselItems[this.carouselIndex]; - curDoc && (curDoc[this.practiceField] = val); - this.advance(e); + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { + const anchor = Docs.Create.ConfigDocument({ annotationOn: this.Document, config_carousel_index: this.carouselIndex }); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { collectionType: true, filters: true } }, this.Document); + addAsAnnotation && Doc.AddDocToList(this.dataDoc, this.fieldKey + '_annotations', anchor); // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered + return anchor; }; + addDocTab = this.addLinkedDocTab; 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); }; - panelHeight = () => this._props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0); + 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.marginX; - specificMenu = (): void => { - const cm = ContextMenu.Instance; - const revealOptions = cm.findByDescription('Filter Flashcards'); - const revealItems = revealOptions?.subitems ?? []; - revealItems.push({description: 'All', event: () => {this.layoutDoc.filterOp = undefined;}, icon: 'layer-group',}); // prettier-ignore - revealItems.push({description: 'Star', event: () => {this.layoutDoc.filterOp = cardMode.STAR;}, icon: 'star',}); // prettier-ignore - revealItems.push({description: 'Practice Mode', event: () => {this.layoutDoc.filterOp = cardMode.PRACTICE;}, icon: 'check',}); // prettier-ignore - revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore - !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); - }; - + captionWidth = () => this._props.PanelWidth() - 2 * this.captionMarginX; + contentScreenToLocalXf = () => + this._props + .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; - - childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); - + : undefined; // prettier-ignore + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( <DocumentView @@ -165,10 +124,10 @@ export class CollectionCarouselView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={this._props.childLayoutFitWidth} + hideFilterStatus={true} showTags={BoolCast(this.layoutDoc.showChildTags)} containerViewPath={this.childContainerViewPath} setContentViewBox={undefined} - ScreenToLocalTransform={this.childScreenToLocalXf} onDoubleClickScript={this.onContentDoubleClick} onClickScript={this.onContentClick} isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} @@ -178,8 +137,14 @@ export class CollectionCarouselView extends CollectionSubView() { LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} - xPadding={35} - PanelHeight={this.panelHeight} + childFilters={this.childDocFilters} + focus={this.focus} + hideDecorations={BoolCast(this.layoutDoc.layout_hideDecorations)} + addDocument={this._props.addDocument} + ScreenToLocalTransform={this.contentScreenToLocalXf} + PanelWidth={this.contentPanelWidth} + PanelHeight={this.contentPanelHeight} + screenXPadding={this.screenXPadding} /> ); }; @@ -188,9 +153,9 @@ export class CollectionCarouselView extends CollectionSubView() { */ @computed get overlay() { const fadeTime = 500; - const lastDoc = this.carouselItems?.[this._last_index]; + const lastDoc = this.carouselItems?.[this._last_index]?.layout; return !lastDoc || this.carouselIndex === this._last_index ? null : ( - <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, position: 'absolute', top: 0, left: 0, transition: `opacity ${fadeTime}ms` }}> + <div className="collectionCarouselView-image" style={{ opacity: this._last_opacity, transition: `opacity ${fadeTime}ms` }}> {this.renderDoc( lastDoc, false, // hide captions if the carousel is configured to show the captions @@ -211,15 +176,18 @@ export class CollectionCarouselView extends CollectionSubView() { </div> ); } + @computed get renderedDoc() { + const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); + return this.renderDoc(this.curDoc(), !!carouselShowsCaptions); + } + @computed get content() { - const index = this.carouselIndex; - const curDoc = this.carouselItems?.[index]; const captionProps = { ...this._props, NativeScaling: returnOne, PanelWidth: this.captionWidth, fieldKey: 'caption', setHeight: undefined, setContentView: undefined }; const carouselShowsCaptions = StrCast(this.layoutDoc._layout_showCaption); - return !curDoc ? null : ( + return !this.curDoc() ? null : ( <> <div className="collectionCarouselView-image" key="image"> - {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.renderedDoc} {this.overlay} </div> {!carouselShowsCaptions ? null : ( @@ -229,68 +197,61 @@ export class CollectionCarouselView extends CollectionSubView() { onWheel={StopEvent} style={{ borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string, - marginRight: this.marginX, - marginLeft: this.marginX, - width: `calc(100% - ${this.marginX * 2}px)`, + marginRight: this.captionMarginX, + marginLeft: this.captionMarginX, + width: `calc(100% - ${this.captionMarginX * 2}px)`, }}> - <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} /> + <FormattedTextBox xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={this.curDoc()} TemplateDataDocument={undefined} /> </div> )} </> ); } - @computed get buttons() { - if (!this.carouselItems?.[this.carouselIndex]) return null; - return ( + + @computed get navButtons() { + return !this.curDoc() ? null : ( <> - <div key="back" className="carouselView-back" onClick={this.goback}> + <div key="back" className="carouselView-back" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.goback}> <FontAwesomeIcon icon="chevron-left" size="2x" /> </div> - <div key="fwd" className="carouselView-fwd" onClick={this.advance}> + <div key="fwd" className="carouselView-fwd" style={{ transform: `scale(${this.uiBtnScaling})` }} onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> - <FontAwesomeIcon icon="xmark" color="red" size="1x" /> - </div> - <div key="check" className="carouselView-check" onClick={e => this.setPracticeVal(e, practiceVal.CORRECT)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> - <FontAwesomeIcon icon="check" color="green" size="1x" /> - </div> </> ); } - /** - * Prompts user to add more flashcaards if they are in practice mode but there are no flashcards - */ - renderAddFlashcards = () => <p - className="collectionCarouselView-addFlashcards" - style={{display: !this.carouselItems?.[this.carouselIndex] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none'}}> - Add flashcards! - </p> // prettier-ignore + nativeScaling = () => this._props.NativeDimScaling?.() || 1; - /** - * Displays message that a flashcard was recently missed if it had previously been marked as wrong. - * */ - renderRecentlyMissed = () => <p - className="collectionCarouselView-recentlyMissed" - style={{display: this.carouselItems?.[this.carouselIndex]?.[this.practiceField] === practiceVal.MISSED ? 'block' : 'none'}}> - Recently missed! - </p> // prettier-ignore + docViewProps = () => ({ + ...this._props, // + isDocumentActive: this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive, + isContentActive: this.isChildContentActive, + ScreenToLocalTransform: this.contentScreenToLocalXf, + }); + 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} - onContextMenu={this.specificMenu} - 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, - }}> - {this.content} - {this.renderAddFlashcards()} - {this.renderRecentlyMissed()} - {this.Document._chromeHidden ? null : this.buttons} + <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.scss b/src/client/views/collections/CollectionDockingView.scss index a747ef45f..7c19d39da 100644 --- a/src/client/views/collections/CollectionDockingView.scss +++ b/src/client/views/collections/CollectionDockingView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .lm_root { position: relative; @@ -285,7 +285,7 @@ background: transparent; border: solid 0px transparent; cursor: grab; - color: $black; + color: global.$black; } .collectiondockingview-container .lm_splitter { opacity: 0.2; @@ -378,7 +378,7 @@ ul.lm_tabs::before { z-index: 1; text-align: center; font-size: 18; - color: $dark-gray; + color: global.$dark-gray; img { position: relative; @@ -491,7 +491,7 @@ ul.lm_tabs::before { } .lm_content { - background: $white; + background: global.$white; } .lm_controls { @@ -557,7 +557,7 @@ ul.lm_tabs::before { } .flexlayout__splitter { - background-color: $dark-gray; + background-color: global.$dark-gray; } .flexlayout__splitter:hover { @@ -626,7 +626,7 @@ ul.lm_tabs::before { position: absolute; box-sizing: border-box; background-color: #222; - color: $dark-gray; + color: global.$dark-gray; } .flexlayout__tab_button { @@ -709,7 +709,7 @@ ul.lm_tabs::before { } .flexlayout__tab_header_outer { - background-color: $dark-gray; + background-color: global.$dark-gray; position: absolute; left: 0; right: 0; @@ -769,28 +769,28 @@ ul.lm_tabs::before { } .flexlayout__border_top { - background-color: $dark-gray; + background-color: global.$dark-gray; border-bottom: 1px solid #ddd; box-sizing: border-box; overflow: hidden; } .flexlayout__border_bottom { - background-color: $dark-gray; + background-color: global.$dark-gray; border-top: 1px solid #333; box-sizing: border-box; overflow: hidden; } .flexlayout__border_left { - background-color: $dark-gray; + background-color: global.$dark-gray; border-right: 1px solid #333; box-sizing: border-box; overflow: hidden; } .flexlayout__border_right { - background-color: $dark-gray; + background-color: global.$dark-gray; border-left: 1px solid #333; box-sizing: border-box; overflow: hidden; diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index d1304b8f4..539b49c86 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -31,17 +31,17 @@ import { ScriptingRepl } from '../ScriptingRepl'; import { UndoStack } from '../UndoStack'; import './CollectionDockingView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -import { TabHTMLElement } from './TabDocView'; +import { TabDocView, TabHTMLElement } from './TabDocView'; @observer export class CollectionDockingView extends CollectionSubView() { - static tabClass: unknown = null; + static tabClass?: typeof TabDocView; /** * Initialize by assigning the add split method to DocumentView and by * configuring golden layout to render its documents using the specified React component * @param ele - typically would be set to TabDocView */ - public static Init(ele: unknown) { + public static Init(ele: typeof TabDocView) { this.tabClass = ele; DocumentView.addSplit = CollectionDockingView.AddSplit; } @@ -382,7 +382,7 @@ export class CollectionDockingView extends CollectionSubView() { } catch { /* empty */ } - this._goldenLayout?.destroy(); + setTimeout(() => this._goldenLayout?.destroy()); window.removeEventListener('resize', this.onResize); window.removeEventListener('mouseup', this.onPointerUp); @@ -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(); } }; @@ -544,7 +544,7 @@ export class CollectionDockingView extends CollectionSubView() { tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => { this.tabMap.add(tab); // InitTab is added to the tab's HTMLElement in TabDocView - const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as TabHTMLElement; + const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as TabHTMLElement; tabdocviewContent?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content) }; diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 710c00841..a0d5f9f70 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -258,7 +258,6 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF const stackPad = showChrome ? `0px ${this._props.parent.xMargin}px` : `${this._props.parent.yMargin}px ${this._props.parent.xMargin}px 0px ${this._props.parent.xMargin}px `; return this.collapsed ? null : ( <div style={{ position: 'relative' }}> - {this._props.showHandle && this._props.parent._props.isContentActive() ? this._props.parent.columnDragger : null} {showChrome ? ( <div className="collectionStackingView-addDocumentButton" diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 45d9394ed..11fce720c 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -1,13 +1,13 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionMenu-container { display: flex; position: relative; align-content: center; justify-content: space-between; - background-color: $dark-gray; + background-color: global.$dark-gray; height: 40px; - border-bottom: $standard-border; + border-bottom: global.$standard-border; padding: 0 10px; align-items: center; overflow-x: auto; diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index dab1298d5..de999c91a 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -2,7 +2,7 @@ /* eslint-disable react/sort-comp */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { Toggle, ToggleType, Type } from 'browndash-components'; +import { Toggle, ToggleType, Type } from '@dash/components'; import { Lambda, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; diff --git a/src/client/views/collections/CollectionNoteTakingView.scss b/src/client/views/collections/CollectionNoteTakingView.scss index 95fda7b0a..0d24a56b5 100644 --- a/src/client/views/collections/CollectionNoteTakingView.scss +++ b/src/client/views/collections/CollectionNoteTakingView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionNoteTakingView-DocumentButtons { opacity: 0; @@ -58,7 +58,7 @@ .documentButtonMenu { position: relative; height: fit-content; - border-bottom: $standard-border; + border-bottom: global.$standard-border; display: flex; justify-content: center; flex-direction: column; @@ -70,11 +70,11 @@ width: 90%; margin: 5px; font-size: 11px; - background-color: $light-blue; - color: $medium-blue; + background-color: global.$light-blue; + color: global.$medium-blue; padding: 10px; border-radius: 10px; - border: solid 2px $medium-blue; + border: solid 2px global.$medium-blue; } } @@ -146,9 +146,9 @@ padding: 10px; height: 2vw; width: 100%; - font-family: $sans-serif; - background: $dark-gray; - color: $white; + font-family: global.$sans-serif; + background: global.$dark-gray; + color: global.$white; } .collectionNoteTakingView-columnDragger { @@ -206,7 +206,7 @@ margin-left: 2px; margin-right: 2px; margin-top: 2px; - background: $medium-gray; + background: global.$medium-gray; height: 5px; &.active { @@ -258,7 +258,7 @@ text-align: center; margin: auto; margin-bottom: 10px; - background: $medium-gray; + background: global.$medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong .editableView-input:hover, @@ -279,7 +279,7 @@ display: flex; align-items: center; justify-content: center; - color: $dark-gray; + color: global.$dark-gray; .editableView-container-editing-oneLine, .editableView-container-editing { diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index ac8e37358..4c23d4c48 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -2,7 +2,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils'; -import { Doc, Field, FieldType, Opt } from '../../../fields/Doc'; +import { Doc, Opt, StrListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -48,6 +48,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { @computed get notetakingCategoryField() { return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column')); } + toHeader = (d: Doc) => (d[this.notetakingCategoryField] instanceof List ? StrListCast(d[this.notetakingCategoryField]).join('.') : (d[this.notetakingCategoryField] ?? 'unset')); public DividerWidth = 16; @observable docsDraggedRowCol: number[] = []; @observable _scroll = 0; @@ -136,7 +137,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const rowCol = this.docsDraggedRowCol; // this will sort the docs into the correct columns (minus the ones you're currently dragging) docs.forEach(d => { - const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; + const sectionValue = this.toHeader(d); // look for if header exists already const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); if (existingHeader) { @@ -161,7 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { }; @computed get allFieldValues() { - return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField]))); + return new Set(this.childDocs.map(doc => (doc[this.notetakingCategoryField] instanceof List ? StrListCast(doc[this.notetakingCategoryField]).join('.') : StrCast(doc[this.notetakingCategoryField])))); } componentDidMount() { @@ -313,7 +314,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // how to get the width of a document. Currently returns the width of the column (minus margins) // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) getDocWidth = (d: Doc) => { - const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType); + const heading = this.toHeader(d); const existingHeader = this.colHeaderData.find(sh => sh.heading === heading); const existingWidth = this.layoutDoc._notetaking_columns_autoSize ? 1 / (this.colHeaderData.length ?? 1) : existingHeader?.width ? existingHeader.width : 0; const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; @@ -427,7 +428,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); this.childDocs?.forEach(d => { if (d instanceof Promise) return; - const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; + const sectionValue = this.toHeader(d); if (sectionValue.toString() === colHeader) { docsMatchingHeader.push(d); } diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 8c6a6b551..226d06f37 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, computed, makeObservable, observable } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { lightOrDark, returnEmptyString } from '../../../ClientUtils'; @@ -87,12 +87,16 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV }; componentDidMount(): void { - this._ele && this.props.refList.push(this._ele); + runInAction(() => { + this._ele && this.props.refList.push(this._ele); + }); } componentWillUnmount() { - this._ele && this.props.refList.splice(this._props.refList.indexOf(this._ele), 1); - this._ele = null; + runInAction(() => { + this._ele && this.props.refList.splice(this._props.refList.indexOf(this._ele), 1); + this._ele = null; + }); } @undoBatch @@ -248,10 +252,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents="+ Node" menuCallback={this.menuCallback} /> </div> <div className="collectionNoteTakingView-addDocumentButton" style={{ color: lightOrDark(this._props.backgroundColor?.()) }}> - { - // eslint-disable-next-line react/jsx-props-no-spreading - <EditableView {...this._props.editableViewProps()} /> - } + <EditableView {...this._props.editableViewProps()} /> </div> </div> ) : null} diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss index 0ced3f9e3..d05c0ffde 100644 --- a/src/client/views/collections/CollectionStackedTimeline.scss +++ b/src/client/views/collections/CollectionStackedTimeline.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionStackedTimeline-timelineContainer { height: 100%; @@ -6,7 +6,7 @@ overflow-x: auto; overflow-y: hidden; border: none; - background-color: $white; + background-color: global.$white; border-width: 0 2px 0 2px; &:hover { @@ -28,7 +28,7 @@ .collectionStackedTimeline { position: absolute; - background: $off-white; + background: global.$off-white; z-index: 1000; height: 100%; overflow: hidden; @@ -36,7 +36,7 @@ .collectionStackedTimeline-trim-shade { position: absolute; height: 100%; - background-color: $dark-gray; + background-color: global.$dark-gray; opacity: 0.3; top: 0; } @@ -45,7 +45,7 @@ height: 100%; position: absolute; box-sizing: border-box; - border: 2px solid $medium-blue; + border: 2px solid global.$medium-blue; display: flex; justify-content: space-between; max-width: 100%; @@ -53,7 +53,7 @@ left: 0; .collectionStackedTimeline-trim-handle { - background-color: $medium-blue; + background-color: global.$medium-blue; height: 100%; width: 5px; cursor: ew-resize; @@ -65,12 +65,12 @@ width: 10px; top: 2.5%; height: 95%; - background: $light-blue; + background: global.$light-blue; border-radius: 3px; opacity: 0.3; z-index: 500; border-style: solid; - border-color: $medium-blue; + border-color: global.$medium-blue; border-width: 1px; } @@ -84,12 +84,12 @@ } .collectionStackedTimeline-current { - background-color: $pink; + background-color: global.$pink; } .collectionStackedTimeline-hover { display: none; - background-color: $medium-blue; + background-color: global.$medium-blue; } .collectionStackedTimeline-marker-timeline { @@ -97,14 +97,14 @@ top: 2.5%; height: 95%; border-radius: 4px; - //background: $light-gray; + //background: global.$light-gray; &:hover { opacity: 1; } .collectionStackedTimeline-left-resizer, .collectionStackedTimeline-resizer { - background: $dark-gray; + background: global.$dark-gray; position: absolute; top: 0; height: 100%; @@ -141,7 +141,7 @@ .hoverTime { position: absolute; - color: $dark-gray; + color: global.$dark-gray; transform: translate(0, -100%); font-weight: bold; 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/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 6400a0a8e..05ac52ff9 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionMasonryView { display: inline; @@ -12,120 +12,124 @@ position: relative; height: 100%; width: 100%; -} - -// TODO:glr Turn this into a seperate class -.documentButtonMenu { - position: relative; - height: fit-content; - border-bottom: $standard-border; - display: flex; - justify-content: center; - flex-direction: column; - align-items: center; - align-content: center; - padding: 5px 0 5px 0; - .documentExplanation { - width: 90%; - margin: 5px; - font-size: 11px; - color: $medium-blue; - padding: 10px; - border-radius: 5px; - border: solid 0.5px $medium-blue; + .collectionStackingView-columnDragger { + width: 28; + height: 28; + position: absolute; + margin-left: -5; + z-index: 10; + > svg { + width: 100%; + height: 100%; + } } -} -.collectionStackingView, -.collectionMasonryView { - height: 100%; - width: 100%; - position: absolute; - top: 0; - overflow-y: auto; - overflow-x: hidden; - flex-wrap: wrap; - transition: top 0.5s; - - > div { + // TODO:glr Turn this into a seperate class + .documentButtonMenu { position: relative; - display: block; - } - - .collectionStackingViewFieldColumn { + height: fit-content; + border-bottom: global.$standard-border; display: flex; + justify-content: center; flex-direction: column; + align-items: center; + align-content: center; + padding: 5px 0 5px 0; + + .documentExplanation { + width: 90%; + margin: 5px; + font-size: 11px; + color: global.$medium-blue; + padding: 10px; + border-radius: 5px; + border: solid 0.5px global.$medium-blue; + } } - .collectionSchemaView-previewDoc { + .collectionStackingView, + .collectionMasonryView { height: 100%; + width: 100%; position: absolute; - } + top: 0; + overflow-y: auto; + overflow-x: hidden; + flex-wrap: wrap; + transition: top 0.5s; - .collectionStackingView-docView-container { - width: 45%; - margin: 5% 2.5%; - padding-left: 2.5%; - height: auto; - } + > div { + position: relative; + display: block; + } - .collectionStackingView-flexCont { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - } + .collectionStackingViewFieldColumn { + display: flex; + flex-direction: column; + } - .collectionStackingView-masonrySingle, - .collectionStackingView-masonryGrid { - width: 100%; - display: grid; - top: 0; - left: 0; - } + .collectionSchemaView-previewDoc { + height: 100%; + position: absolute; + } - .collectionStackingView-masonrySingle { - height: 100%; - position: absolute; - } + .collectionStackingView-docView-container { + width: 45%; + margin: 5% 2.5%; + padding-left: 2.5%; + height: auto; + } - .collectionStackingView-masonryGrid { - margin: auto; - height: max-content; - position: relative; - grid-auto-rows: 0px; - } + .collectionStackingView-flexCont { + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + } - .collectionStackingView-masonrySingle { - width: 100%; - height: 100%; - position: absolute; - display: flex; - flex-direction: column; - top: 0; - left: 0; - width: 100%; - position: absolute; - } + .collectionStackingView-masonrySingle, + .collectionStackingView-masonryGrid { + width: 100%; + display: grid; + top: 0; + left: 0; + } - .collectionStackingView-description { - font-size: 100%; - margin-bottom: 1vw; - padding: 10px; - height: 2vw; - width: 100%; - font-family: $sans-serif; - background: $dark-gray; - color: $white; - } + .collectionStackingView-masonrySingle { + height: 100%; + position: absolute; + } - .collectionStackingView-columnDragger { - width: 15; - height: 15; - position: absolute; - margin-left: -5; - z-index: 10; + .collectionStackingView-masonryGrid { + margin: auto; + height: max-content; + position: relative; + grid-auto-rows: 0px; + } + + .collectionStackingView-masonrySingle { + width: 100%; + height: 100%; + position: absolute; + display: flex; + flex-direction: column; + top: 0; + left: 0; + width: 100%; + position: absolute; + } + + .collectionStackingView-description { + font-size: 100%; + margin-bottom: 1vw; + padding: 10px; + height: 2vw; + width: 100%; + font-family: global.$sans-serif; + background: global.$dark-gray; + color: global.$white; + } } // Documents in stacking view @@ -149,7 +153,7 @@ .collectionStackingView-collapseBar { margin-top: 2px; - background: $medium-gray; + background: global.$medium-gray; height: 5px; width: 100%; display: none; @@ -207,11 +211,11 @@ text-align: center; margin: auto; margin-bottom: 10px; - background: $medium-gray; + background: global.$medium-gray; // overflow: hidden; overflow is visible so the color menu isn't hidden -ftong .editableView-input { - color: $dark-gray; + color: global.$dark-gray; } .editableView-input:hover, @@ -232,7 +236,7 @@ display: flex; align-items: center; justify-content: center; - color: $dark-gray; + color: global.$dark-gray; .editableView-container-editing-oneLine, .editableView-container-editing { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 1ac0b6d70..f06e0b551 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -110,7 +110,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // columnWidth handles the margin on the left and right side of the documents @computed get columnWidth() { - return Math.min(this._props.PanelWidth() - 2 * this.xMargin, this.isStackingView ? Number.MAX_VALUE : this.layoutDoc._columnWidth === -1 ? this._props.PanelWidth() - 2 * this.xMargin : NumCast(this.layoutDoc._columnWidth, 250)); + const availableWidth = this._props.PanelWidth() - 2 * this.xMargin; + const cwid = availableWidth / NumCast(this.Document._layout_columnCount, 1); + return Math.min(availableWidth, this.isStackingView ? Number.MAX_VALUE : cwid - this.gridGap); } @computed get NodeWidth() { @@ -146,7 +148,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // assuming we need to get rowSpan because we might be dealing with many columns. Grid gap makes sense if multiple columns const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap); // just getting the style - const style = this.isStackingView ? { margin: undefined, transition: this.getDocTransition(d)(), width: this.columnWidth, marginTop: i ? this.gridGap : 0, height: this.getDocHeight(d)() } : { gridRowEnd: `span ${rowSpan}` }; + const style = this.isStackingView + ? { + // + margin: undefined, + transition: this.getDocTransition(d)(), + width: this.columnWidth, + marginTop: i ? this.gridGap : 0, + height: this.getDocHeight(d)(), + zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0, + } + : { gridRowEnd: `span ${rowSpan}`, zIndex: DocumentView.getFirstDocumentView(d)?.IsSelected ? 1000 : 0 }; // So we're choosing whether we're going to render a column or a masonry doc return ( <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}> @@ -211,6 +223,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return fields; } + setAutoHeight = () => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : 2 * this.yMargin + this._refList.reduce((p, r) => p + DivHeight(r), 0))); + observer = new ResizeObserver(this.setAutoHeight); + componentDidMount() { super.componentDidMount?.(); this._props.setContentViewBox?.(this); @@ -222,10 +237,21 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this.dataDoc['_' + this.fieldKey + '_columnHeaders'] = new List(); } ); + // reset section headers when a new filter is inputted + this._disposers.width = reaction( + () => [this._props.PanelWidth() - 2 * this.xMargin, NumCast(this.Document._layout_columnWidth, 250)], + ([pw, cw]) => { + if (!this.isStackingView) { + this.layoutDoc._layout_columnCount = Math.round(pw / cw); + } + } + ); + this._disposers.autoHeight = reaction( - () => this.layoutDoc._layout_autoHeight, - layoutAutoHeight => layoutAutoHeight && this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0))) + () => [this.layoutDoc._layout_autoHeight, this.yMargin], + ([autoH]) => autoH && this.setAutoHeight() ); + this._disposers.refList = reaction( () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }), ({ refList, autoHeight }) => { @@ -344,6 +370,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} showTitle={this._props.childlayout_showTitle} + showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)} dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} @@ -424,8 +451,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); }; @action - onDividerMove = (e: PointerEvent, down: number[], delta: number[]) => { - this.layoutDoc._columnWidth = Math.max(10, this.columnWidth + delta[0]); + onDividerMove = (e: PointerEvent) => { + this.Document._layout_columnWidth = Math.max(10, (this._props.DocumentView?.().screenToViewTransform().transformPoint(e.clientX, 0)[0] ?? 0) - this.xMargin); return false; }; @@ -435,7 +462,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection className="collectionStackingView-columnDragger" onPointerDown={this.columnDividerDown} ref={this._draggerRef} - style={{ cursor: this._cursor, color: SettingsManager.userColor, left: `${this.columnWidth + this.xMargin}px`, top: `${Math.max(0, this.yMargin - 9)}px` }}> + style={{ cursor: this._cursor, color: SettingsManager.userColor, left: `${NumCast(this.Document._layout_columnWidth) + this.xMargin}px` }}> <FontAwesomeIcon icon="arrows-alt-h" /> </div> ); @@ -579,24 +606,29 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))))); return ( - <CollectionMasonryViewFieldRow - showHandle={first} - Document={this.Document} - chromeHidden={this.chromeHidden} - pivotField={this.pivotField} - refList={this._refList} - key={heading ? heading.heading : ''} - rows={rows} - headings={this.headings} - heading={heading ? heading.heading : ''} - headingObject={heading} - docList={docList} - parent={this} - type={type} - createDropTarget={this.createDashEventsTarget} - screenToLocalTransform={this.ScreenToLocalBoxXf} - setDocHeight={this.setDocHeight} - /> + <div key={(heading?.heading ?? '') + 'head'}> + {this._props.isContentActive() && !this.isStackingView ? this.columnDragger : null} + <div style={{ top: this.yMargin }}> + <CollectionMasonryViewFieldRow + showHandle={first} + Document={this.Document} + chromeHidden={this.chromeHidden} + pivotField={this.pivotField} + refList={this._refList} + key={heading ? heading.heading : ''} + rows={rows} + headings={this.headings} + heading={heading ? heading.heading : ''} + headingObject={heading} + docList={docList} + parent={this} + type={type} + createDropTarget={this.createDashEventsTarget} + screenToLocalTransform={this.ScreenToLocalBoxXf} + setDocHeight={this.setDocHeight} + /> + </div> + </div> ); }; @@ -688,8 +720,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this._props.isContentActive() === false ? 'none' : undefined; } - observer = new ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))); - onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); render() { TraceMobx(); diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 581201a20..655894e40 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,7 +1,7 @@ import { action, computed, makeObservable, observable } from 'mobx'; import * as React from 'react'; import * as rp from 'request-promise'; -import { ClientUtils, returnFalse } from '../../../ClientUtils'; +import { ClientUtils, DashColor, returnFalse } from '../../../ClientUtils'; import CursorField from '../../../fields/CursorField'; import { Doc, DocListCast, GetDocFromUrl, GetHrefFromHTML, Opt, RTFIsFragment, StrListCast } from '../../../fields/Doc'; import { AclPrivate, DocData } from '../../../fields/DocSymbols'; @@ -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, ScriptCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DateCast, NumCast, ScriptCast, StrCast, toList } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; @@ -25,7 +25,21 @@ import { SnappingManager } from '../../util/SnappingManager'; import { UndoManager } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldViewProps } from '../nodes/FieldView'; -import { DocumentView } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { FlashcardPracticeUI } from './FlashcardPracticeUI'; +import { OpenWhere, OpenWhereMod } from '../nodes/OpenWhere'; +import { Upload } from '../../../server/SharedMediaTypes'; + +export enum docSortings { + Time = 'time', + Type = 'type', + Color = 'color', + Chat = 'chat', + Tag = 'tag', + None = '', +} + +export const ChatSortField = 'chat_sortIndex'; export interface CollectionViewProps extends React.PropsWithChildren<FieldViewProps> { isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc) @@ -111,6 +125,7 @@ export function CollectionSubView<X>() { return this.dataDoc[this._props.fieldKey]; // this used to be 'layoutDoc', but then template fields will get ignored since the template is not a proto of the layout. hopefully nothing depending on the previous code. } + hasChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); @computed get childLayoutPairs(): { layout: Doc; data: Doc }[] { const { Document, TemplateDataDocument } = this._props; const validPairs = this.childDocs @@ -119,7 +134,8 @@ export function CollectionSubView<X>() { pair => // filter out any documents that have a proto that we don't have permissions to !pair.layout?.hidden && pair.layout && (!pair.layout.proto || (pair.layout.proto instanceof Doc && GetEffectiveAcl(pair.layout.proto) !== AclPrivate)) - ); + ) + .filter(pair => !this._filterFunc?.(pair.layout!)); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } /** @@ -128,6 +144,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 @@ -136,6 +163,8 @@ export function CollectionSubView<X>() { unrecursiveDocFilters = () => [...(this._props.childFilters?.().filter(f => !ClientUtils.IsRecursiveFilter(f)) || [])]; childDocRangeFilters = () => [...(this._props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this._props.searchFilterDocs?.() ?? DocListCast(this.Document._searchFilterDocs); + + @observable docDraggedIndex = -1; @computed.struct get childDocs() { TraceMobx(); let rawdocs: (Doc | Promise<Doc>)[] = []; @@ -152,8 +181,10 @@ export function CollectionSubView<X>() { const templateRoot = this._props.TemplateDataDocument; rawdocs = templateRoot && !this._props.isAnnotationOverlay ? [Doc.GetProto(templateRoot)] : []; } - - const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc); + const childDocs = this.childSortedDocs( + rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this._props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc), + this.docDraggedIndex + ); const childDocFilters = this.childDocFilters(); const childFiltersByRanges = this.childDocRangeFilters(); @@ -200,6 +231,33 @@ export function CollectionSubView<X>() { return docsforFilter; } + childSortedDocs = (docsIn: Doc[], dragIndex: number) => { + const sortType = StrCast(this.Document[this._props.fieldKey + '_sort']) as docSortings; + const isDesc = BoolCast(this.Document[this._props.fieldKey + '_sort_reverse']); + const docs = docsIn.slice(); + sortType && docs.sort((docA, docB) => { + const [typeA, typeB] = (() => { + switch (sortType) { + default: + case docSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; + case docSortings.Chat: return [NumCast(docA[ChatSortField], 9999), NumCast(docB[ChatSortField], 9999)]; + case docSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; + case docSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; + case docSortings.Tag: return [StrListCast(docA.tags).join(""), StrListCast(docB.tags).join("")]; + } + })(); + return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? -1 : 1); + }); //prettier-ignore + if (dragIndex !== -1) { + const draggedDoc = DragManager.docsBeingDragged[0]; + const originalIndex = docs.findIndex(doc => doc === draggedDoc); + + originalIndex !== -1 && docs.splice(originalIndex, 1); + draggedDoc && docs.splice(dragIndex, 0, draggedDoc); + } + return docs; + }; + @action protected async setCursorPosition(position: [number, number]) { let ind; @@ -350,7 +408,7 @@ export function CollectionSubView<X>() { const imgSrc = img.split('src="')[1].split('"')[0]; const imgOpts = { ...options, _width: 300 }; if (imgSrc.startsWith('data:image') && imgSrc.includes('base64')) { - const result = (await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })).lastElement(); + const result = ((await Networking.PostToServer('/uploadRemoteImage', { sources: [imgSrc] })) as Upload.ImageInformation[]).lastElement(); const newImgSrc = result.accessPaths.agnostic.client.indexOf('dashblobstore') === -1 // ? ClientUtils.prepend(result.accessPaths.agnostic.client) @@ -483,12 +541,17 @@ export function CollectionSubView<X>() { DocUtils.uploadYoutubeVideoLoading(files, {}, loading); } else { generatedDocuments.push( - ...files.map(file => { - const loading = Docs.Create.LoadingDocument(file, options); - Doc.addCurrentlyLoading(loading); - DocUtils.uploadFileToDoc(file, {}, loading); - return loading; - }) + ...(await Promise.all( + files.map(async file => { + if (file.name.endsWith('svg')) { + return (await DocUtils.openSVGfile(file, options)) as Doc; + } + const loading = Docs.Create.LoadingDocument(file, options); + Doc.addCurrentlyLoading(loading); + DocUtils.uploadFileToDoc(file, {}, loading); + return loading; + }) + )) ); } if (generatedDocuments.length) { @@ -515,6 +578,49 @@ export function CollectionSubView<X>() { alert('Document upload failed - possibly an unsupported file type.'); } }; + + 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; } // 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, (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. + */ + @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore + + screenXPadding = () => (this.uiBtnScaling * this._sideBtnWidth - NumCast(this.layoutDoc.xMargin)) / this._props.ScreenToLocalTransform().Scale; + filteredChildDocs = () => this.childLayoutPairs.map(pair => pair.layout); + childDocsFunc = () => this.childDocs; + @action setFilterFunc = (func?: (doc: Doc) => boolean) => { this._filterFunc = func; }; // prettier-ignore + + public flashCardUI = (curDoc: () => Doc | undefined, docViewProps: () => DocumentViewProps, answered?: (correct: boolean) => void) => { + return ( + <FlashcardPracticeUI + setFilterFunc={this.setFilterFunc} + fieldKey={this.fieldKey} + sideBtnWidth={this._sideBtnWidth} + allChildDocs={this.childDocsFunc} + filteredChildDocs={this.filteredChildDocs} + advance={answered} + curDoc={curDoc} + layoutDoc={this.layoutDoc} + uiBtnScaling={this.uiBtnScaling} + ScreenToLocalBoxXf={this.ScreenToLocalBoxXf} + renderDepth={this._props.renderDepth} + docViewProps={docViewProps} + /> + ); + }; } return CollectionSubViewInternal; 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/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss index bbbef78b4..2a03ea708 100644 --- a/src/client/views/collections/CollectionTreeView.scss +++ b/src/client/views/collections/CollectionTreeView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionTreeView-container { transform-origin: top left; @@ -12,7 +12,7 @@ width: 100%; position: relative; top: 0; - // background: $light-gray; + // background: global.$light-gray; font-size: 13px; overflow: auto; user-select: none; @@ -21,7 +21,7 @@ ul { list-style: none; - padding-left: $TREE_BULLET_WIDTH; + padding-left: global.$TREE_BULLET_WIDTH; margin-bottom: 1px; // otherwise vertical scrollbars may pop up for no apparent reason.... > .contentFittingDocumentView { width: unset; @@ -47,7 +47,7 @@ } .delete-button { - color: $medium-gray; + color: global.$medium-gray; // float: right; margin-left: 15px; // margin-top: 3px; @@ -90,7 +90,7 @@ .collectionTreeView-subtitle { font-style: italic; font-size: 8pt; - color: $medium-gray; + color: global.$medium-gray; } .docContainer { diff --git a/src/client/views/collections/CollectionView.scss b/src/client/views/collections/CollectionView.scss index de53a2c62..06c324bd0 100644 --- a/src/client/views/collections/CollectionView.scss +++ b/src/client/views/collections/CollectionView.scss @@ -1,10 +1,10 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .collectionView { border-width: 0; - border-color: $light-gray; + border-color: global.$light-gray; border-style: solid; - border-radius: 0 0 $border-radius $border-radius; + border-radius: 0 0 global.$border-radius global.$border-radius; box-sizing: border-box; border-radius: inherit; width: 100%; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 7418d4360..a4900e9d7 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -83,12 +83,14 @@ 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(); if (type === undefined) return null; switch (type) { + default: + case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />; case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />; case CollectionViewType.Calendar: return <CalendarBox key="collview" {...props} />; case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />; @@ -105,8 +107,6 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr case CollectionViewType.Time: return <CollectionTimeView key="collview" {...props} />; case CollectionViewType.Grid: return <CollectionGridView key="collview" {...props} />; case CollectionViewType.Card: return <CollectionCardView key="collview" {...props} />; - case CollectionViewType.Freeform: - default: return <CollectionFreeFormView key="collview" {...props} />; } }; @@ -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 new file mode 100644 index 000000000..210c6798f --- /dev/null +++ b/src/client/views/collections/FlashcardPracticeUI.scss @@ -0,0 +1,78 @@ +.FlashcardPracticeUI { + width: 100%; + height: 100%; + display: flex; + align-items: center; +} +.FlashcardPracticeUI-remove, +.FlashcardPracticeUI-check { + position: absolute; + display: flex; + width: 30; + height: 30; + align-items: center; + border-radius: 5px; + justify-content: center; + color: rgba(255, 255, 255, 0.5); + background: rgba(0, 0, 0, 0.1); + &:hover { + color: white; + } +} +.FlashcardPracticeUI-practice { + position: absolute; + width: 100%; + pointer-events: none; + .FlashcardPracticeUI-remove { + left: 52%; + pointer-events: all; + } + .FlashcardPracticeUI-check { + right: 52%; + pointer-events: all; + } +} +.FlashcardPracticeUI-menu { + position: absolute; + flex-direction: column; + align-items: center; + display: flex; + top: 0px; + left: 0px; + width: 30; + transform-origin: top left; + border-radius: 5px; + color: rgba(255, 255, 255, 0.5); + pointer-events: all; + background: rgba(0, 0, 0, 0.1); + .FlashcardPracticeUI-practiceModes { + width: 100%; + display: flex; + flex-direction: column; + top: 0; + position: relative; + .FlashcardPracticeUI-quiz, + .FlashcardPracticeUI-practice { + position: relative; + display: flex; + height: 20px; + align-items: center; + margin: auto; + padding: 3px; + &:hover { + color: white; + } + & > svg { + height: 100%; + width: 100%; + } + } + } +} +.FlashcardPracticeUI-message { + z-index: 100; + position: relative; + margin: auto; + align-content: center; + width: max-content; +} diff --git a/src/client/views/collections/FlashcardPracticeUI.tsx b/src/client/views/collections/FlashcardPracticeUI.tsx new file mode 100644 index 000000000..c071c5fb8 --- /dev/null +++ b/src/client/views/collections/FlashcardPracticeUI.tsx @@ -0,0 +1,214 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip } from '@mui/material'; +import { MultiToggle, Type } from '@dash/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'; + +export enum practiceMode { + PRACTICE = 'practice', + QUIZ = 'quiz', +} +enum practiceVal { + MISSED = 'missed', + CORRECT = 'correct', +} + +export enum flashcardRevealOp { + FLIP = 'flip', + SLIDE = 'slide', +} + +interface PracticeUIProps { + fieldKey: string; + layoutDoc: Doc; + filteredChildDocs: () => Doc[]; + allChildDocs: () => Doc[]; + curDoc: () => Doc | undefined; + advance?: (correct: boolean) => void; + renderDepth: number; + sideBtnWidth: number; + uiBtnScaling: number; + ScreenToLocalBoxXf: () => Transform; + docViewProps: () => DocumentViewProps; + setFilterFunc: (func?: (doc: Doc) => boolean) => void; +} +@observer +export class FlashcardPracticeUI extends ObservableReactComponent<PracticeUIProps> { + constructor(props: PracticeUIProps) { + super(props); + makeObservable(this); + this._props.setFilterFunc(this.tryFilterOut); + } + + componentWillUnmount(): void { + this._props.setFilterFunc(undefined); + } + + 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_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)); + + /** + * Sets the practice mode answer style for flashcards + * @param mode practiceMode or undefined for no practice + */ + setPracticeMode = (mode: practiceMode | undefined) => { + this._props.layoutDoc.practiceMode = mode; + this._props.allChildDocs().map(doc => (doc[this.practiceField] = undefined)); + }; + + @computed get emptyMessage() { + const cardCount = this._props.filteredChildDocs().length; + const practiceMessage = this.practiceMode && !Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount ? 'Finished! Click here to view all flashcards.' : ''; + const filterMessage = practiceMessage + ? '' + : Doc.hasDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny) && !cardCount + ? 'No tagged items. Click here to view all flash cards.' + : this.practiceMode && !cardCount + ? 'No flashcards to show! Click here to leave practice mode' + : ''; + return !practiceMessage && !filterMessage ? null : ( + <p + className="FlashcardPracticeUI-message" + style={{ transform: `scale(${this._props.uiBtnScaling})` }} + onClick={() => { + if (filterMessage || practiceMessage) { + this.setPracticeMode(undefined); + Doc.setDocFilter(this._props.layoutDoc, 'tags', Doc.FilterAny, 'remove'); + } + }}> + {filterMessage || practiceMessage} + </p> + ); + } + + @computed get practiceButtons() { + /* + * Sets a flashcard to either missed or correct depending on if they got the question right in practice mode. + */ + const setPracticeVal = (e: React.MouseEvent, val: string) => { + e.stopPropagation(); + const curDoc = this._props.curDoc(); + this._props.advance?.(val === practiceVal.CORRECT); + curDoc && (curDoc[this.practiceField] = val); + }; + + return this.practiceMode == practiceMode.PRACTICE && this._props.curDoc() ? ( + <div className="FlashcardPracticeUI-practice" style={{ transform: `scale(${this._props.uiBtnScaling})`, bottom: `${this._props.sideBtnWidth}px`, height: `${this._props.sideBtnWidth}px` }}> + <Tooltip title="Incorrect. View again later."> + <div key="remove" className="FlashcardPracticeUI-remove" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => setPracticeVal(e, practiceVal.MISSED))}> + <FontAwesomeIcon icon="xmark" color="red" size="1x" /> + </div> + </Tooltip> + <Tooltip title="Correct"> + <div key="check" className="FlashcardPracticeUI-check" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => setPracticeVal(e, practiceVal.CORRECT))}> + <FontAwesomeIcon icon="check" color="green" size="1x" /> + </div> + </Tooltip> + </div> + ) : null; + } + @computed get practiceModesMenu() { + 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_flashcardType) ? null : ( + <div + className="FlashcardPracticeUI-practiceModes" + style={{ + 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" + type={Type.PRIM} + color={SnappingManager.userColor} + background={SnappingManager.userVariantColor} + multiSelect={false} + isToggle={false} + toggleStatus={!!this.practiceMode} + label="Practice" + items={[ + [practiceMode.QUIZ, 'file-pen', 'Practice flashcards using GPT'], + [practiceMode.PRACTICE, 'check', this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'], + ].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.practiceMode} + onSelectionChange={(val: (string | number) | (string | number)[]) => togglePracticeMode(val as practiceMode)} + /> + <MultiToggle + tooltip="How to reveal flashcard answer" + type={Type.PRIM} + color={SnappingManager.userColor} + background={SnappingManager.userVariantColor} + 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 && doc?._layout_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 : ( + <div className="FlashcardPracticeUI-menu" style={{ height: this.btnHeight(), width: this.btnHeight(), transform: `scale(${this._props.uiBtnScaling})` }}> + {!this.filterDoc ? null : ( + <DocumentView + {...this._props.docViewProps()} + Document={this.filterDoc} + TemplateDataDocument={undefined} + PanelWidth={this.btnWidth} + PanelHeight={this.btnHeight} + NativeWidth={returnZero} + NativeHeight={returnZero} + hideDecorations={BoolCast(this._props.layoutDoc.layout_hideDecorations)} + hideCaptions={true} + hideFilterStatus={true} + renderDepth={this._props.renderDepth + 1} + fitWidth={undefined} + showTags={false} + setContentViewBox={undefined} + /> + )} + {this.practiceModesMenu} + </div> + )} + </div> + ); + } +} diff --git a/src/client/views/collections/TabDocView.scss b/src/client/views/collections/TabDocView.scss index dd4c0b881..397e35ca9 100644 --- a/src/client/views/collections/TabDocView.scss +++ b/src/client/views/collections/TabDocView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .tabDocView-content { height: 100%; diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index f56ea9d76..cc56a8ff9 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -1,6 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Popup, Type } from 'browndash-components'; +import { Popup, Type } from '@dash/components'; import { clamp } from 'lodash'; import { IReactionDisposer, ObservableSet, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -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; }; @@ -478,7 +479,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { componentDidMount() { new ResizeObserver( action(entries => { - // eslint-disable-next-line no-restricted-syntax for (const entry of entries) { this._panelWidth = entry.contentRect.width; this._panelHeight = entry.contentRect.height; diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss index 2ab1a5ac1..78794d112 100644 --- a/src/client/views/collections/TreeView.scss +++ b/src/client/views/collections/TreeView.scss @@ -1,4 +1,4 @@ -@import '../global/globalCssVariables.module.scss'; +@use '../global/globalCssVariables.module.scss' as global; .treeView-label { max-height: 1.5em; @@ -14,7 +14,7 @@ .bullet-outline { position: relative; width: fit-content; - color: $medium-gray; + color: global.$medium-gray; transform: scale(0.5); display: inline-flex; align-items: center; @@ -66,7 +66,7 @@ min-height: 20px; min-width: 15px; margin-right: 3px; - color: $medium-gray; + color: global.$medium-gray; border: #80808030 1px solid; border-radius: 5px; z-index: 1; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 5444a7a57..ab4d8b060 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1,6 +1,6 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IconButton, Size } from 'browndash-components'; +import { IconButton, Size } from '@dash/components'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -38,7 +38,7 @@ import { CollectionView } from './CollectionView'; import { TreeSort } from './TreeSort'; import './TreeView.scss'; -// eslint-disable-next-line @typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-require-imports const { TREE_BULLET_WIDTH } = require('../global/globalCssVariables.module.scss'); // prettier-ignore export interface TreeViewProps { @@ -500,9 +500,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { !(key in ids) && doc[key] !== ComputedField.undefined && (ids[key] = key); }); - // eslint-disable-next-line no-restricted-syntax for (const key of Object.keys(ids).slice().sort()) { - // eslint-disable-next-line no-continue if (this._props.skipFields?.includes(key) || key === 'title' || key === 'treeView_Open') continue; const contents = doc[key]; let contentElement: (JSX.Element | null)[] | JSX.Element = []; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts b/src/client/views/collections/collectionFreeForm/CollectionFreeFormClusters.ts index 6ad67a864..3838852dd 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,18 +179,20 @@ 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: { const cluster = NumCast(doc?.layout_cluster); - if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) { + if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG && !doc.layout_isSvg) { if (this._clusterSets.length <= cluster) { setTimeout(() => doc && this.addDocument(doc)); } else { const palette = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)']; - // override palette cluster color with an explicitly set cluster doc color - return this._clusterSets[cluster]?.reduce((b, s) => StrCast(s.backgroundColor, b), palette[cluster % palette.length]); + // override palette cluster color with an explicitly set cluster doc color ONLY if doc color matches the current default text color + return this._clusterSets[cluster]?.reduce((b, s) => (s.backgroundColor !== Doc.UserDoc().textBackgroundColor ? StrCast(s.backgroundColor, b) : b), palette[cluster % palette.length]); } } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx index c17371151..437888ef2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx @@ -1,4 +1,4 @@ -import { IconButton, Size, Type } from 'browndash-components'; +import { IconButton, Size, Type } from '@dash/components'; import { IReactionDisposer, action, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -46,7 +46,6 @@ export function InfoState( gif?: string, entryFunc?: () => unknown ) { - // eslint-disable-next-line new-cap return new infoState(msg, arcs, gif, entryFunc); } 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/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index 79aad0ef2..bebdbd731 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -44,6 +44,7 @@ export interface PoolData { transition?: string; highlight?: boolean; pointerEvents?: string; + showTags?: boolean; } export interface ViewDefResult { @@ -102,7 +103,6 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc replica: '', }); }); - // eslint-disable-next-line no-use-before-define return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []); } @@ -272,7 +272,6 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do payload: pivotColumnGroups.get(key)?.filters, })); groupNames.push(...dividers); - // eslint-disable-next-line no-use-before-define return normalizeResults(panelDim, maxText, docMap, poolData, viewDefsToJSX, groupNames, 0, []); } @@ -347,7 +346,6 @@ export function computeTimelineLayout(poolData: Map<string, PoolData>, pivotDoc: if (!stack && (curTime === undefined || Math.abs(x - (curTime - minTime) * scaling) > pivotAxisWidth)) { groupNames.push({ type: 'text', text: toLabel(key), x: x, y: stack * 25, height: fontHeight, fontSize, payload: undefined }); } - // eslint-disable-next-line no-use-before-define layoutDocsAtTime(keyDocs, key); }); if (sortedKeys.length && curTime !== undefined && curTime > sortedKeys[sortedKeys.length - 1]) { @@ -428,6 +426,7 @@ function normalizeResults( opacity: newPosRaw.opacity, color: newPosRaw.color, pair: ele[1].pair, + showTags: newPosRaw.showTags, }; if (newPosRaw.transition) newPos.transition = newPosRaw.transition; poolData.set(newPos.pair.layout[Id] + (newPos.replica || ''), { transition: 'all 1s', ...newPos }); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss index 2c94446fb..cce0ff684 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; .collectionfreeformview-none { position: inherit; @@ -32,9 +32,9 @@ .collectionfreeformview-mask-empty, .collectionfreeformview-mask { z-index: 5000; - width: $INK_MASK_SIZE; - height: $INK_MASK_SIZE; - transform: translate($INK_MASK_SIZE_HALF, $INK_MASK_SIZE_HALF); + width: global.$INK_MASK_SIZE; + height: global.$INK_MASK_SIZE; + transform: translate(global.$INK_MASK_SIZE_HALF, global.$INK_MASK_SIZE_HALF); pointer-events: none; position: absolute; background-color: transparent; @@ -211,11 +211,11 @@ //nested freeform views // .collectionfreeformview-container { - // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px), - // linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px); + // background-image: linear-gradient(to right, global.$light-color-secondary 1px, transparent 1px), + // linear-gradient(to bottom, global.$light-color-secondary 1px, transparent 1px); // background-size: 30px 30px; // } - border: 0px solid $light-gray; + border: 0px solid global.$light-gray; border-radius: inherit; box-sizing: border-box; position: absolute; @@ -233,7 +233,7 @@ box-sizing: border-box; width: 98%; height: 98%; - border-radius: $border-radius; + border-radius: global.$border-radius; } //this is an animation for the blinking cursor! @@ -304,3 +304,65 @@ display: none; } } + +.collectionFreeformView-aiView { + text-align: center; + font-weight: bold; + width: 100%; + + .collectionfreeformview-aiView-prompt { + height: 25px; + width: 65%; + } + + .collectionFreeFormView-aiView-strength { + text-align: center; + align-items: center; + display: flex; + width: 25%; + .collectionFreeFormView-aiView-similarity { + max-width: 65px; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + } + } + + .collectionFreeForView-aiView-send { + width: 10%; + .button-container { + width: 100% !important; + justify-content: left !important; + } + } + + .collectionFreeformView-aiView-options-container, + .collectionFreeFormView-aiView-regenerate-container { + text-align: start; + font-weight: normal; + width: 100%; + display: flex; + .collectionFreeformView-aiView-subtitle { + margin: auto; + width: 40px; + } + } + .collectionFreeformView-aiView-options, + .collectionFreeFormView-aiView-regenerate { + display: flex; + flex-direction: row; + align-items: center; + align-items: center; + width: 100%; + gap: 10px; + .collectionFreeformView-aiView-input { + width: 100%; + } + .collectionFreeFormView-aiView-regenBtn { + width: 10%; + .button-container { + width: 100% !important; + } + } + } +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index d2bc8f2c2..b3d908da4 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,5 +1,5 @@ import { Bezier } from 'bezier-js'; -import { Colors } from 'browndash-components'; +import { Button, Colors, Type } from '@dash/components'; import { Property } from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -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'; @@ -30,19 +30,32 @@ import { CompileScript } from '../../../util/Scripting'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; -import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, UndoManager } from '../../../util/UndoManager'; import { Timeline } from '../../animationtimeline/Timeline'; 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 { AnnotationPalette } from '../../smartdraw/AnnotationPalette'; +import { StickerPalette } from '../../smartdraw/StickerPalette'; import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { StyleProp } from '../../StyleProp'; import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; @@ -54,6 +67,11 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; +import ReactLoading from 'react-loading'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { Slider } from '@mui/material'; +import { AiOutlineSend } from 'react-icons/ai'; +import { DrawingFillHandler } from '../../smartdraw/DrawingFillHandler'; @observer class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { @@ -85,6 +103,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const parent = CollectionFreeFormDocumentView.from(dv)?._props.reactParent; return parent instanceof CollectionFreeFormView ? parent : undefined; } + /** + * The Freeformview below the cursor at the start of a gesture (that receives the pointerDown event). Used by GestureOverlay to determine the doc a gesture should apply to. + */ + // eslint-disable-next-line no-use-before-define + public static DownFfview: CollectionFreeFormView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to. private _clusters = new CollectionFreeFormClusters(this); private _oldWheel: HTMLDivElement | null = null; @@ -209,7 +232,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) { - return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true); + return DocumentView.SetViewTransition(docs, 'all', duration, timer, true); } changeKeyFrame = (back = false) => { const currentFrame = Cast(this.Document._currentFrame, 'number', null); @@ -389,7 +412,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) { @@ -464,11 +487,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // do nothing if link is dropped into any freeform view parent of dragged document const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' }); const added = !!this._props.addDocument?.(source); - de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); - if (de.complete.linkDocument) { - de.complete.linkDocument.layout_isSvg = true; - this.addDocument(de.complete.linkDocument); - } + de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { layout_isSvg: true, link_relationship: 'annotated by:annotation of' }); + de.complete.linkDocument && this.addDocument(de.complete.linkDocument); e.stopPropagation(); !added && e.preventDefault(); return added; @@ -487,6 +507,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onPointerDown = (e: React.PointerEvent): void => { + if (!CollectionFreeFormView.DownFfview) CollectionFreeFormView.DownFfview = this; + this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; this._downTime = Date.now(); @@ -497,13 +519,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 +561,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 +625,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 +633,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 +654,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,60 +1217,28 @@ 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() ); }; @action - showSmartDraw = (x: number, y: number) => { - SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; - SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - SmartDrawHandler.Instance.displaySmartDrawHandler(x, y); + showSmartDraw = (x: number, y: number, regenerate?: boolean) => { + const sm = SmartDrawHandler.Instance; + sm.RemoveDrawing = this.removeDrawing; + sm.AddDrawing = this.addDrawing; + (regenerate ? sm.displayRegenerate : sm.displaySmartDrawHandler)(x, y, NumCast(this.layoutDoc[this.scaleFieldKey])); }; _drawing: Doc[] = []; _drawingContainer: Doc | undefined = undefined; - /** - * Function that creates a drawing--a group of ink strokes--to go with the smart draw function. - */ - @undoBatch - createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions) => { - this._drawing = []; - const xf = this.screenToFreeformContentsXf; - strokeData.forEach((stroke: [InkData, string, string]) => { - const bounds = InkField.getBounds(stroke[0]); - const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); - const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; - const inkDoc = Docs.Create.InkDocument( - stroke[0], - { title: 'stroke', - x: B.x - inkWidth / 2, - y: B.y - inkWidth / 2, - _width: B.width + inkWidth, - _height: B.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore - inkWidth, - opts.autoColor ? stroke[1] : ActiveInkColor(), - ActiveInkBezierApprox(), - stroke[2] === 'none' ? ActiveFillColor() : stroke[2], - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), - ActiveIsInkMask() - ); - this._drawing.push(inkDoc); - }); - return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 }); - }; /** * Part of regenerating a drawing--deletes the old drawing. @@ -1269,16 +1259,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /** * Adds the created drawing to the freeform canvas and sets the metadata. */ - addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => { + addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string, x?: number, y?: number) => { const docData = doc[DocData]; - docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; - docData.width = opts.size; - docData.drawingInput = opts.text; - docData.drawingComplexity = opts.complexity; - docData.drawingColored = opts.autoColor; - docData.drawingSize = opts.size; - docData.drawingData = gptRes; + docData.title = opts.text; + docData._width = opts.size; + docData.ai_drawing_input = opts.text; + docData.ai_drawing_complexity = opts.complexity; + docData.ai_drawing_colored = opts.autoColor; + docData.ai_drawing_size = opts.size; + docData.ai_drawing_data = gptRes; + docData.ai = 'gpt'; this._drawingContainer = doc; + if (x !== undefined && y !== undefined) { + [doc.x, doc.y] = this.screenToFreeformContentsXf.transformPoint(x, y); + } this.addDocument(doc); this._batch?.end(); }; @@ -1307,6 +1301,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.Document[this.scaleFieldKey] = Math.abs(safeScale); this.setPan(-localTransform.TranslateX / safeScale, (this._props.originTopLeft ? undefined : NumCast(this.Document.layout_scrollTop) * safeScale) || -localTransform.TranslateY / safeScale, undefined, allowScroll); } + SmartDrawHandler.Instance.hideSmartDrawHandler(); }; @action @@ -1596,21 +1591,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); @@ -1641,6 +1629,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: _width, height: _height, transition: StrCast(childDocLayout.dataTransition), + showTags: BoolCast(childDocLayout.showTags) || BoolCast(this.Document.showChildTags) || BoolCast(this.Document._layout_showTags), pointerEvents: Cast(childDoc.pointerEvents, 'string', null), pair, replica: '', @@ -1750,7 +1739,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 ); @@ -1854,14 +1843,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } updateIcon = (usePanelDimensions?: boolean) => { - const contentDiv = this.DocumentView?.().ContentDiv; + const contentDiv = this._mainCont; return !contentDiv ? new Promise<void>(res => res()) : UpdateIcon( this.layoutDoc[Id] + '_icon_' + new Date().getTime(), contentDiv, - usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), - usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + usePanelDimensions || true ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + usePanelDimensions || true ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), this._props.PanelWidth(), this._props.PanelHeight(), 0, @@ -1981,20 +1970,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }), icon: 'eye', }); + this.layoutDoc.drawingData != undefined && + optionItems.push({ + description: 'Regenerate AI Drawing', + event: action(() => { + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); + }), + icon: 'pen-to-square', + }); optionItems.push({ - description: 'Show Drawing Editor', - event: action(() => { - SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; - SmartDrawHandler.Instance.AddDrawing = this.addDrawing; - SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; - !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); - }), - icon: 'pen-to-square', - }); - optionItems.push({ - description: this.Document.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette', - event: action(undoable(async () => await AnnotationPalette.addToPalette(this.Document), 'save to palette')), - icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down', + description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', + event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); this._props.renderDepth && optionItems.push({ @@ -2173,6 +2162,102 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection </div> ); } + + @observable private _regenInput = ''; + @observable private _drawingFillInput = ''; + @observable private _regenLoading = false; + @observable private _drawingFillLoading = false; + @observable private _fireflyRefStrength = 50; + + componentAIView = () => { + return ( + <div className="collectionfreeformview-aiView" onPointerDown={e => e.stopPropagation()}> + <div className="collectionfreeformview-aiView-options-container"> + <span className="collectionfreeformview-aiView-subtitle">Firefly:</span> + <div className="collectionfreeformview-aiView-options"> + <input + className="collectionfreeformview-aiView-prompt" + placeholder={this._drawingFillInput || StrCast(this.Document.title) || 'Describe image'} + type="text" + value={this._drawingFillInput} + onChange={action(e => { + this._drawingFillInput = e.target.value; + })} + /> + <div className="collectionFreeFormView-aiView-strength"> + <span className="collectionFreeFormView-aiView-similarity">Similarity</span> + <Slider + className="collectionfreeformview-aiView-slider" + sx={{ + '& .MuiSlider-track': { color: SettingsManager.userVariantColor }, + '& .MuiSlider-rail': { color: SettingsManager.userBackgroundColor }, + '& .MuiSlider-thumb': { color: SettingsManager.userVariantColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } }, + }} + min={1} + max={100} + step={1} + size="small" + value={this._fireflyRefStrength} + onChange={action((e, val) => (this._fireflyRefStrength = val as number))} + valueLabelDisplay="auto" + /> + </div> + <div className="collectionFreeFormView-aiView-send"> + <Button + text="Send" + type={Type.SEC} + icon={this._drawingFillLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + onClick={undoable( + action(() => { + this._drawingFillLoading = true; + DrawingFillHandler.drawingToImage(this.props.Document, this._fireflyRefStrength, this._drawingFillInput || StrCast(this.Document.title))?.then( + action(() => { + this._drawingFillLoading = false; + }) + ); + }), + 'create image' + )} + /> + </div> + </div> + </div> + <div className="collectionfreeformview-aiView-regenerate-container"> + <span className="collectionfreeformview-aiView-subtitle">Regenerate</span> + <div className="collectionfreeformview-aiView-regenerate"> + <input + className="collectionfreeformview-aiView-input" + aria-label="Edit instructions input" + type="text" + value={this._regenInput} + onChange={action(e => { + this._regenInput = e.target.value; + })} + placeholder="..under development.." + /> + <div className="collectionFreeFormView-aiView-regenBtn"> + <Button + text="Regenerate" + type={Type.SEC} + icon={this._regenLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + // onClick={action(async () => { + // this._regenLoading = true; + // SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; + // SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + // SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + // await SmartDrawHandler.Instance.regenerate([this.Document], undefined, undefined, this._regenInput, true); + // this._regenLoading = false; + // })} + /> + </div> + </div> + </div> + </div> + ); + }; + render() { TraceMobx(); return ( @@ -2202,7 +2287,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..b9f8b13a7 100644 --- a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IconButton, Size } from 'browndash-components'; +import { IconButton, Size } from '@dash/components'; import * as faceapi from 'face-api.js'; import { FaceMatcher } from 'face-api.js'; import 'ldrs/ring'; @@ -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/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx index 033d1590d..a3d9641da 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -1,11 +1,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Colors, IconButton } from 'browndash-components'; +import { Colors, IconButton } from '@dash/components'; import similarity from 'compute-cosine-similarity'; import { ring } from 'ldrs'; import 'ldrs/ring'; import { action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; +import { imageUrlToBase64 } from '../../../../ClientUtils'; import { Utils, numberRange } from '../../../../Utils'; import { Doc, NumListCast, Opt } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; @@ -22,13 +23,13 @@ import { MainView } from '../../MainView'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { OpenWhere } from '../../nodes/OpenWhere'; -import { CollectionCardView } from '../CollectionCardDeckView'; import './ImageLabelBox.scss'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; export class ImageInformationItem {} export class ImageLabelBoxData { + // eslint-disable-next-line no-use-before-define static _instance: ImageLabelBoxData; @observable _docs: Doc[] = []; @observable _labelGroups: string[] = []; @@ -47,8 +48,8 @@ export class ImageLabelBoxData { }; @action - addLabel = (label: string) => { - label = label.toUpperCase().trim(); + addLabel = (labelIn: string) => { + const label = labelIn.toUpperCase().trim(); if (label.length > 0) { if (!this._labelGroups.includes(label)) { this._labelGroups = [...this._labelGroups, label.startsWith('#') ? label : '#' + label]; @@ -68,9 +69,10 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageLabelBox, fieldKey); } + // eslint-disable-next-line no-use-before-define + public static Instance: ImageLabelBox; private _dropDisposer?: DragManager.DragDropDisposer; - public static Instance: ImageLabelBox; private _inputRef = React.createRef<HTMLInputElement>(); @observable _loading: boolean = false; private _currentLabel: string = ''; @@ -99,7 +101,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { } @observable _displayImageInformation: boolean = false; - constructor(props: any) { + constructor(props: FieldViewProps) { super(props); makeObservable(this); ring.register(); @@ -165,9 +167,9 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { const imageInfos = this._selectedImages.map(async doc => { if (!doc[DocData].tags_chat) { const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); - return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => + return imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => !hrefBase64 ? undefined : - gptImageLabel(hrefBase64).then(labels => + gptImageLabel(hrefBase64,'Give three labels to describe this image.').then(labels => ({ doc, labels }))) ; // prettier-ignore } }); @@ -178,13 +180,13 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { const labels = imageInfo.labels.split('\n'); labels.forEach(label => { - label = + const hashLabel = '#' + label .replace(/^\d+\.\s*|-|f\*/, '') .replace(/^#/, '') .trim(); - (imageInfo.doc[DocData].tags_chat as List<string>).push(label); + (imageInfo.doc[DocData].tags_chat as List<string>).push(hashLabel); }); } }); @@ -214,7 +216,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { // most similar one. this._selectedImages.forEach(doc => { const embedLists = numberRange((doc[DocData].tags_chat as List<string>).length).map(n => Array.from(NumListCast(doc[DocData][`tags_embedding_${n + 1}`]))); - const bestEmbedScore = (embedding: Opt<number[]>) => Math.max(...embedLists.map((l, index) => (embedding && similarity(Array.from(embedding), l)!) || 0)); + const bestEmbedScore = (embedding: Opt<number[]>) => Math.max(...embedLists.map(l => (embedding && similarity(Array.from(embedding), l)!) || 0)); const {label: mostSimilarLabelCollect} = this._labelGroups.map(label => ({ label, similarityScore: bestEmbedScore(labelToEmbedding.get(label)) })) .reduce((prev, cur) => cur.similarityScore < 0.3 || cur.similarityScore <= prev.similarityScore ? prev: cur, @@ -243,7 +245,7 @@ export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { if (this._selectedImages.length === 0) { return ( <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} ref={ele => this.createDropTarget(ele!)}> - <p style={{ fontSize: 'large' }}>In order to classify and sort images, marquee select the desired images and press the 'Classify and Sort Images' button. Then, add the desired groups for the images to be put in.</p> + <p style={{ fontSize: 'large' }}>In order to classify and sort images, marquee select the desired images and press the 'Classify and Sort Images' button. Then, add the desired groups for the images to be put in.</p> </div> ); } diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx index 73befb205..1a44e094d 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IconButton } from 'browndash-components'; +import { IconButton } from '@dash/components'; import { action, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import React from 'react'; @@ -9,7 +9,8 @@ import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './ImageLabelHandler.scss'; @observer -export class ImageLabelHandler extends ObservableReactComponent<{}> { +export class ImageLabelHandler extends ObservableReactComponent<object> { + // eslint-disable-next-line no-use-before-define static Instance: ImageLabelHandler; @observable _display: boolean = false; @@ -19,11 +20,10 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> { @observable _currentLabel: string = ''; @observable _labelGroups: string[] = []; - constructor(props: any) { + constructor(props: object) { super(props); makeObservable(this); ImageLabelHandler.Instance = this; - console.log('Instantiated label handler!'); } @action @@ -41,8 +41,8 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> { }; @action - addLabel = (label: string) => { - label = label.toUpperCase().trim(); + addLabel = (labelIn: string) => { + const label = labelIn.toUpperCase().trim(); if (label.length > 0) { if (!this._labelGroups.includes(label)) { this._labelGroups = [...this._labelGroups, label]; @@ -96,10 +96,10 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> { <div> {this._labelGroups.map(group => { return ( - <div> + <div key={group}> <p>{group}</p> <IconButton - tooltip={'Remove Label'} + tooltip="Remove Label" onPointerDown={() => { this.removeLabel(group); }} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index de65b240f..abd828945 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IconButton } from 'browndash-components'; +import { IconButton } from '@dash/components'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 10709cc00..9cfb0416c 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -436,14 +436,14 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps * Classifies images and assigns the labels as document fields. */ @undoBatch - classifyImages = action(async () => { + classifyImages = async () => { const groupButton = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MyImageGrouper); if (groupButton) { this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG); ImageLabelBoxData.Instance.setData(this._selectedDocs); MainView.Instance.expandFlyout(groupButton); } - }); + }; /** * Groups images to most similar labels. @@ -461,6 +461,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps const newColDim = 900; for (const label of labelGroups) { const newCollection = MarqueeView.getCollection([], undefined, false, this.Bounds); + newCollection[DocData].title = label + ' Collection'; newCollection._x = this.Bounds.left + x_offset; newCollection._y = this.Bounds.top + y_offset; newCollection._width = newColDim; @@ -586,7 +587,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps /** * When this is called, returns the list of documents that have been selected by the marquee box. */ - marqueeSelect(selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) { + marqueeSelect = (selectBackgrounds: boolean = false, docType: DocumentType | undefined = undefined) => { const selection: Doc[] = []; const selectFunc = (doc: Doc) => { const layoutDoc = Doc.Layout(doc); @@ -619,7 +620,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps .filter(doc => doc.z !== undefined) .map(selectFunc); return selection; - } + }; @computed get marqueeDiv() { const cpt = this._lassoFreehand || !this._visible ? [0, 0] : [this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY]; @@ -690,7 +691,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/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 5c41fee37..6dffb80f1 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -186,21 +186,23 @@ export class CollectionGridView extends CollectionSubView() { getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) { return ( <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} - NativeWidth={returnZero} - NativeHeight={returnZero} - setContentViewBox={emptyFunction} Document={layout} TemplateDataDocument={layout.resolvedDataDoc as Doc} + NativeWidth={returnZero} + NativeHeight={returnZero} + fitWidth={this._props.childLayoutFitWidth} + containerViewPath={this.childContainerViewPath} + renderDepth={this._props.renderDepth + 1} isContentActive={this.isChildContentActive} PanelWidth={width} PanelHeight={height} ScreenToLocalTransform={dxf} + setContentViewBox={emptyFunction} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} onClickScript={this.onChildClickHandler} - renderDepth={this._props.renderDepth + 1} dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'} + showTags={BoolCast(this.layoutDoc.showChildTags) || BoolCast(this.Document._layout_showTags)} /> ); } diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.scss b/src/client/views/collections/collectionLinear/CollectionLinearView.scss index b8ceec139..0dfaed38a 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.scss +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.scss @@ -1,12 +1,12 @@ -@import '../../global/globalCssVariables.module.scss'; -@import '../../_nodeModuleOverrides'; +@use '../../global/globalCssVariables.module.scss' as global; +// bcz fix @import '../../_nodeModuleOverrides'; .collectionLinearView { width: 100%; } .collectionLinearView-label { color: black; - background-color: $light-gray; + background-color: global.$light-gray; width: 100%; } .collectionLinearView-outer { @@ -32,8 +32,8 @@ } > span { - background: $dark-gray; - color: $white; + background: global.$dark-gray; + color: global.$white; border-radius: 18px; margin-right: 6px; cursor: pointer; @@ -44,7 +44,7 @@ } .bottomPopup-background { - background: $medium-blue; + background: global.$medium-blue; display: flex; border-radius: 10px; height: 35; @@ -55,7 +55,7 @@ } .bottomPopup-text { - color: $white; + color: global.$white; display: inline; white-space: nowrap; padding-left: 8px; @@ -72,7 +72,7 @@ padding-left: 8px; padding-right: 8px; vertical-align: middle; - background-color: $light-gray; + background-color: global.$light-gray; border-radius: 3px; color: black; margin-right: 5px; @@ -86,7 +86,7 @@ padding-left: 8px; padding-right: 8px; vertical-align: middle; - background-color: $close-red; + background-color: global.$close-red; border-radius: 3px; color: black; } @@ -94,13 +94,13 @@ > label { pointer-events: all; cursor: pointer; - background-color: $medium-blue; + background-color: global.$medium-blue; padding: 5; border-radius: 2px; height: 100%; min-width: 25; margin: 0; - color: $white; + color: global.$white; display: flex; font-weight: 100; transition: transform 0.2s; diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index ceae43c04..80116dd2f 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { Toggle, ToggleType, Type } from 'browndash-components'; +import { Toggle, ToggleType, Type } from '@dash/components'; import { Property } from 'csstype'; import { IReactionDisposer, action, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index d67e10c0b..8aae24df0 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,6 +1,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { Button, IconButton } from 'browndash-components'; +import { Button, IconButton } from '@dash/components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index c32661214..0bf78f57c 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -1,4 +1,4 @@ -@import '../../global/globalCssVariables.module.scss'; +@use '../../global/globalCssVariables.module.scss' as global; .collectionSchemaView { cursor: default; @@ -7,7 +7,7 @@ flex-direction: row; .schema-table { - background-color: $white; + background-color: global.$white; cursor: grab; width: 100%; @@ -49,10 +49,10 @@ .schema-column-menu, .schema-filter-menu { - background: $light-gray; + background: global.$light-gray; position: absolute; - border: 1px solid $medium-gray; - border-bottom: 2px solid $medium-gray; + border: 1px solid global.$medium-gray; + border-bottom: 2px solid global.$medium-gray; max-height: 201px; display: flex; overflow: hidden; @@ -66,7 +66,7 @@ width: 100%; &:hover { - background-color: $medium-gray; + background-color: global.$medium-gray; } .schema-search-result-type { font-family: 'Courier New', Courier, monospace; @@ -74,8 +74,8 @@ .schema-search-result-type, .schema-search-result-desc { - color: $dark-gray; - font-size: $body-text; + color: global.$dark-gray; + font-size: global.$body-text; } .schema-search-result-desc { font-style: italic; @@ -120,9 +120,9 @@ .schema-column-menu-button { cursor: pointer; padding: 2px 5px; - background: $medium-blue; + background: global.$medium-blue; border-radius: 9999px; - color: $white; + color: global.$white; width: fit-content; margin: 5px; align-self: center; @@ -141,7 +141,7 @@ } .schema-column-header { - background-color: $light-gray; + background-color: global.$light-gray; font-weight: bold; display: flex; flex-direction: row; @@ -149,7 +149,7 @@ align-items: center; padding: 0; z-index: 1; - border: 1px solid $medium-gray; + border: 1px solid global.$medium-gray; .schema-column-title { flex-grow: 2; @@ -175,7 +175,7 @@ cursor: ew-resize; &:hover { - background-color: $light-blue; + background-color: global.$light-blue; } } @@ -188,7 +188,7 @@ min-width: 5px; transform: translate(-3px, 0px); align-self: flex-start; - background-color: $medium-gray; + background-color: global.$medium-gray; }*/ // creates awkward thick gray borders between colheaders } @@ -202,7 +202,7 @@ } .schema-header-row { - background-color: $light-gray; + background-color: global.$light-gray; overflow: hidden; } @@ -226,7 +226,7 @@ .schema-table-cell, .row-menu { - border: 1px solid $medium-gray; + border: 1px solid global.$medium-gray; overflow-x: hidden; overflow-y: auto; display: inline-flex; @@ -264,7 +264,7 @@ .row-menu-infos { position: absolute; top: 3; - left: 3; + left: 3; z-index: 1; display: flex; justify-content: flex-end; @@ -278,7 +278,7 @@ .schema-row-button, .schema-header-button { - color: $dark-gray; + color: global.$dark-gray; margin: 3px; cursor: pointer; display: flex; @@ -294,7 +294,7 @@ width: 17px; height: 17px; border-radius: 30%; - background-color: $dark-gray; + background-color: global.$dark-gray; color: white; margin: 3px; cursor: pointer; @@ -309,6 +309,5 @@ .schemaField-editing { outline: none; + height: 100%; } - - diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index aef97e723..5a5cc3622 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { IconButton, Size } from 'browndash-components'; +import { IconButton, Size } from '@dash/components'; import { IReactionDisposer, Lambda, ObservableMap, action, computed, makeObservable, observable, observe, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 9ffdd812f..81a2d8e64 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -17,7 +17,7 @@ import { DocCast } from '../../../../fields/Types'; import { computedFn } from 'mobx-utils'; import { CollectionSchemaView } from './CollectionSchemaView'; import { undoable } from '../../../util/UndoManager'; -import { IconButton, Size } from 'browndash-components'; +import { IconButton, Size } from '@dash/components'; export enum SchemaFieldType { Header, @@ -122,45 +122,53 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea @computed get editableView() { const { color, fieldProps, pointerEvents } = this.renderProps(this._props); - return <div className='schema-column-edit-wrapper' onPointerUp={() => { - SchemaColumnHeader.isDefaultField(this.fieldKey) && this.openKeyDropdown(); - this._props.schemaView.deselectAllCells(); - }} - style={{ - color, - width: '100%', - pointerEvents, - }}> - <EditableView - ref={r => {this._inputRef = r; this._props.autoFocus && r?.setIsFocused(true)}} - oneLine={true} - allowCRs={false} - contents={''} - onClick={this.openKeyDropdown} - fieldContents={fieldProps} - editing={undefined} - placeholder={'Add key'} - updateAlt={this.updateAlt} // alternate title to display - updateSearch={this.updateKeyDropdown} - inputString={true} - inputStringPlaceholder={'Add key'} - GetValue={() => { - if (SchemaColumnHeader.isDefaultField(this.fieldKey)) return ''; - else if (this._altTitle) return this._altTitle; - else return this.fieldKey; + return ( + <div + className="schema-column-edit-wrapper" + onPointerUp={() => { + SchemaColumnHeader.isDefaultField(this.fieldKey) && this.openKeyDropdown(); + this._props.schemaView.deselectAllCells(); }} - SetValue={undoable((value: string, shiftKey?: boolean, enterKey?: boolean) => { - if (enterKey) { - // if shift & enter, set value of each cell in column - this.setColumnValues(value, ''); - this._altTitle = undefined; + style={{ + color, + width: '100%', + pointerEvents, + }}> + <EditableView + ref={r => { + this._inputRef = r; + this._props.autoFocus && r?.setIsFocused(true); + }} + oneLine={true} + allowCRs={false} + contents={''} + onClick={this.openKeyDropdown} + fieldContents={fieldProps} + editing={undefined} + placeholder={'Add key'} + updateAlt={this.updateAlt} // alternate title to display + updateSearch={this.updateKeyDropdown} + inputString={true} + inputStringPlaceholder={'Add key'} + GetValue={() => { + if (SchemaColumnHeader.isDefaultField(this.fieldKey)) return ''; + else if (this._altTitle) return this._altTitle; + else return this.fieldKey; + }} + SetValue={undoable((value: string, shiftKey?: boolean, enterKey?: boolean) => { + if (enterKey) { + // if shift & enter, set value of each cell in column + this.setColumnValues(value, ''); + this._altTitle = undefined; + this._props.finishEdit?.(); + return true; + } this._props.finishEdit?.(); return true; - } - this._props.finishEdit?.(); - return true; - }, 'edit column header')}/> + }, 'edit column header')} + /> </div> + ); } public static isDefaultField = (key: string) => { diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index 6ffb0865a..da203abfa 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -1,4 +1,4 @@ -import { IconButton, Size } from 'browndash-components'; +import { IconButton, Size } from '@dash/components'; import { computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; @@ -100,9 +100,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { return infos; } - isolatedSelection = (doc: Doc) => { - return this.schemaView?.selectionOverlap(doc); - }; + isolatedSelection = (doc: Doc) => this.schemaView?.selectionOverlap(doc); setCursorIndex = (mouseY: number) => this.schemaView?.setRelCursorIndex(mouseY); selectedCol = () => this.schemaView._selectedCol; getFinfo = computedFn((fieldKey: string) => this.schemaView?.fieldInfos.get(fieldKey)); @@ -113,9 +111,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<SchemaRowBoxProps>() { columnWidth = computedFn((index: number) => () => this.schemaView?.displayColumnWidths[index] ?? CollectionSchemaView._minColWidth); computeRowIndex = () => this.schemaView?.rowIndex(this.Document); highlightCells = (text: string) => this.schemaView?.highlightCells(text); - selectReference = (doc: Doc, col: number) => { - this.schemaView.selectReference(doc, col); - }; + selectReference = (doc: Doc, col: number) => this.schemaView.selectReference(doc, col); eqHighlightFunc = (text: string) => { const info = this.schemaView.findCellRefs(text); const cells: HTMLDivElement[] = []; @@ -188,7 +184,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; diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index f036ff843..cd46ae824 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,6 +1,6 @@ /* eslint-disable no-use-before-define */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Popup, Size, Type } from 'browndash-components'; +import { Popup, Size, Type } from '@dash/components'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; |
