diff options
Diffstat (limited to 'src/client/views/collections')
44 files changed, 2121 insertions, 1199 deletions
diff --git a/src/client/views/collections/CollectionCalendarView.tsx b/src/client/views/collections/CollectionCalendarView.tsx index a08a7c7c1..0ea9f8ebc 100644 --- a/src/client/views/collections/CollectionCalendarView.tsx +++ b/src/client/views/collections/CollectionCalendarView.tsx @@ -6,11 +6,11 @@ import { dateRangeStrToDates, returnTrue } from '../../../ClientUtils'; import { Doc, DocListCast } from '../../../fields/Doc'; import { StrCast } from '../../../fields/Types'; import { CollectionStackingView } from './CollectionStackingView'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; @observer export class CollectionCalendarView extends CollectionSubView() { - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -38,8 +38,8 @@ export class CollectionCalendarView extends CollectionSubView() { const aDateRangeStr = StrCast(DocListCast(calendarA.data).lastElement()?.date_range); const bDateRangeStr = StrCast(DocListCast(calendarB.data).lastElement()?.date_range); - const [aFromDate, aToDate] = dateRangeStrToDates(aDateRangeStr); - const [bFromDate, bToDate] = dateRangeStrToDates(bDateRangeStr); + const { start: aFromDate, end: aToDate } = dateRangeStrToDates(aDateRangeStr); + const { start: bFromDate, end: bToDate } = dateRangeStrToDates(bDateRangeStr); if (aFromDate > bFromDate) { return -1; // a comes first @@ -82,6 +82,7 @@ export class CollectionCalendarView extends CollectionSubView() { isAnnotationOverlay={false} // select={emptyFunction} What does this mean? isAnyChildContentActive={returnTrue} // ?? + dontCenter="y" // childDocumentsActive={} // whenChildContentsActiveChanged={} childHideDecorationTitle={false} diff --git a/src/client/views/collections/CollectionCardDeckView.scss b/src/client/views/collections/CollectionCardDeckView.scss index a089b248d..d1731c244 100644 --- a/src/client/views/collections/CollectionCardDeckView.scss +++ b/src/client/views/collections/CollectionCardDeckView.scss @@ -6,12 +6,15 @@ position: relative; background-color: white; overflow: hidden; + + button { + border-radius: 50%; + } } .card-wrapper { display: grid; grid-template-columns: repeat(10, 1fr); - // width: 100%; transform-origin: top left; position: absolute; @@ -22,54 +25,18 @@ transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); } -.card-button-container { - display: flex; - padding: 3px; - // width: 300px; - background-color: rgb(218, 218, 218); /* Background color of the container */ - border-radius: 50px; /* Rounds the corners of the container */ - transform: translateY(75px); - // box-shadow: 0 4px 8px rgba(0,0,0,0.1); /* Optional: Adds shadow for depth */ - align-items: center; /* Centers buttons vertically */ - justify-content: start; /* Centers buttons horizontally */ -} - -button { - width: 35px; - height: 35px; - border-radius: 50%; - background-color: $dark-gray; - // border-color: $medium-blue; - margin: 5px; // transform: translateY(-50px); +.no-card-span { + position: relative; + width: fit-content; + text-align: center; + font-size: 65px; } -// button:hover { -// transform: translateY(-50px); -// } - -// .card-wrapper::after { -// content: ""; -// width: 100%; /* Forces wrapping */ -// } - -// .card-wrapper > .card-item:nth-child(10n)::after { -// content: ""; -// width: 100%; /* Forces wrapping after every 10th item */ -// } - -// .card-row{ -// display: flex; -// position: absolute; -// align-items: center; -// transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); - -// } - .card-item-inactive, .card-item-active, .card-item { position: relative; - transition: transform 0.5s ease-in-out; + transition: transform 0.3s ease-in-out; display: flex; flex-direction: column; } @@ -79,6 +46,5 @@ button { } .card-item-active { - position: absolute; z-index: 100; } diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index de46180e6..e5a6ebc7f 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,48 +1,59 @@ -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils'; -import { numberRange } from '../../../Utils'; -import { Doc, NumListCast } from '../../../fields/Doc'; +import { emptyFunction } from '../../../Utils'; +import { Doc } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; -import { BoolCast, Cast, DateCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types'; +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 { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; +import { dropActionType } from '../../util/DropActionTypes'; +import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoable } from '../../util/UndoManager'; import { StyleProp } from '../StyleProp'; +import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; import './CollectionCardDeckView.scss'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; enum cardSortings { Time = 'time', Type = 'type', Color = 'color', Custom = 'custom', + Chat = 'chat', + Tag = 'tag', None = '', } + +/** + * New view type specifically for studying more dynamically. Allows you to reorder docs however you see fit, easily + * sort and filter using presets, and customize your experience with chat gpt. + * + * This file contains code as to how the docs are to be rendered (there place geographically and also in regards to sorting), + * and callback functions for the gpt popup + */ @observer export class CollectionCardView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; - private _childDocumentWidth = 600; // target width of a Doc... private _disposers: { [key: string]: IReactionDisposer } = {}; private _textToDoc = new Map<string, Doc>(); + private _dropped = false; // indicate when a card doc has just moved; - @observable _forceChildXf = false; - @observable _isLoading = false; + @observable _forceChildXf = 0; @observable _hoveredNodeIndex = -1; @observable _docRefs = new ObservableMap<Doc, DocumentView>(); @observable _maxRowCount = 10; - - static getButtonGroup(groupFieldKey: 'chat' | 'star' | 'idea' | 'like', doc: Doc): number | undefined { - return Cast(doc[groupFieldKey], 'number', null); - } + @observable _docDraggedIndex: number = -1; + @observable overIndex: number = -1; static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { try { @@ -61,22 +72,56 @@ export class CollectionCardView extends CollectionSubView() { } }; + 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); } }; + /** + * Callback to ensure gpt's text versions of the child docs are updated + */ + setRegenerateCallback = () => GPTPopup.Instance.setRegenerateCallback(this.childPairStringListAndUpdateSortDesc); - constructor(props: any) { - super(props); - makeObservable(this); - } + /** + * 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(); + }; - componentDidMount(): void { + componentDidMount() { + this._props.setContentViewBox?.(this); + // Reaction to cardSort changes this._disposers.sort = reaction( - () => ({ cardSort: this.cardSort, field: this.cardSort_customField }), - ({ cardSort, field }) => (cardSort === cardSortings.Custom && field === 'chat' ? this.openChatPopup() : GPTPopup.Instance.setVisible(false)) + () => 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 + // 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], + () => { + if (!Array.from(this._docRefs.values()).every(dv => dv.ContentDiv?.getBoundingClientRect().width)) { + setTimeout(action(() => this._forceChildXf++)); + } + } ); } @@ -85,56 +130,40 @@ export class CollectionCardView extends CollectionSubView() { this._dropDisposer?.(); } - @computed get cardSort_customField() { - return StrCast(this.Document.cardSort_customField) as any as 'chat' | 'star' | 'idea' | 'like'; + @computed get cardSort() { + return StrCast(this.Document.cardSort) as cardSortings; } - @computed get cardSort() { - return StrCast(this.Document.cardSort) as any as cardSortings; + /** + * The child documents to be rendered-- either all of them except the Links or the docs in the currently active + * custom group + */ + @computed get childDocsWithoutLinks() { + return this.childDocs.filter(l => l.type !== DocumentType.LINK); } + /** * 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._childDocumentWidth * length) / this._props.PanelWidth(); - } - - @computed get translateWrapperX() { - let translate = 0; - - if (this.inactiveDocs().length !== this.childDocsWithoutLinks.length && this.inactiveDocs().length < 10) { - translate += this.panelWidth() / 2; - } - return translate; + return (this.childPanelWidth() * length) / this._props.PanelWidth(); } /** - * The child documents to be rendered-- either all of them except the Links or the docs in the currently active - * custom group + * When in quiz mode, randomly selects a document */ - @computed get childDocsWithoutLinks() { - const regularDocs = this.childDocs.filter(l => l.type !== DocumentType.LINK); - const activeGroups = NumListCast(this.Document.cardSort_visibleSortGroups); - - if (activeGroups.length > 0 && this.cardSort === cardSortings.Custom) { - return regularDocs.filter(doc => { - // Get the group number for the current index - const groupNumber = CollectionCardView.getButtonGroup(this.cardSort_customField, doc); - // Check if the group number is in the active groups - return groupNumber !== undefined && activeGroups.includes(groupNumber); - }); - } - - // Default return for non-custom cardSort or other cases, filtering out links - return regularDocs; - } + quizMode = () => { + const randomIndex = Math.floor(Math.random() * this.childDocs.length); + SelectionManager.DeselectAll(); + DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false); + }; /** - * Determines the order in which the cards will be rendered depending on the current sort type + * Number of rows of cards to be rendered */ - @computed get sortedDocs() { - return this.sort(this.childDocsWithoutLinks, this.cardSort, BoolCast(this.layoutDoc.sortDesc)); + @computed get numRows() { + return Math.ceil(this.sortedDocs.length / this._maxRowCount); } @action @@ -157,8 +186,8 @@ export class CollectionCardView extends CollectionSubView() { */ inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d)); - panelWidth = () => this._childDocumentWidth; - panelHeight = (layout: Doc) => () => (this.panelWidth() * NumCast(layout._height)) / NumCast(layout._width); + childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2); + childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => !!this.isContentActive(); @@ -170,6 +199,8 @@ export class CollectionCardView extends CollectionSubView() { * @returns */ rotate = (amCards: number, index: number) => { + if (amCards == 1) return 0; + const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2)); const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2))); @@ -188,11 +219,12 @@ export class CollectionCardView extends CollectionSubView() { translateY = (amCards: number, index: number, realIndex: number) => { const evenOdd = amCards % 2; const apex = (amCards - evenOdd) / 2; - const stepMag = 200 / ((amCards - evenOdd) / 2) + Math.abs((apex - index) * 25); + 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 = 400 * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount); + rowOffset = Magnitude * ((realIndex - (realIndex % this._maxRowCount)) / this._maxRowCount); } if (evenOdd === 1 || index < apex - 1) { return Math.abs(stepMag * (apex - index)) - rowOffset; @@ -205,27 +237,108 @@ export class CollectionCardView extends CollectionSubView() { }; /** - * Translates the selected node to the middle fo the screen - * @param index - * @returns + * 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 */ - translateSelected = (index: number): number => { - // if (this.isSelected(index)) { - const middleOfPanel = this._props.PanelWidth() / 2; - const scaledNodeWidth = this.panelWidth() * 1.25; + findCardDropIndex = (mouseX: number, mouseY: number) => { + const amCardsTotal = this.sortedDocs.length; + let index = 0; + const cardWidth = amCardsTotal < this._maxRowCount ? this._props.PanelWidth() / amCardsTotal : this._props.PanelWidth() / this._maxRowCount; - // Calculate the position of the node's left edge before scaling - const nodeLeftEdge = index * this.panelWidth(); - // Find the center of the node after scaling - const scaledNodeCenter = nodeLeftEdge + scaledNodeWidth / 2; + // Calculate the adjusted X position accounting for the initial offset + let adjustedX = mouseX; - // Calculate the translation needed to align the scaled node's center with the panel's center - const translation = middleOfPanel - scaledNodeCenter - scaledNodeWidth - scaledNodeWidth / 4; + const amRows = Math.ceil(amCardsTotal / this._maxRowCount); + const rowHeight = this._props.PanelHeight() / amRows; + const currRow = Math.floor((mouseY - 100) / rowHeight); //rows start at 0 + + if (adjustedX < 0) { + return 0; // Before the first column + } - return translation; + if (amCardsTotal < this._maxRowCount) { + index = Math.floor(adjustedX / cardWidth); + } else if (currRow != amRows - 1) { + index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount; + } else { + const rowAmCards = amCardsTotal - currRow * this._maxRowCount; + const offset = ((this._maxRowCount - rowAmCards) / 2) * cardWidth; + adjustedX = mouseX - offset; + + index = Math.floor(adjustedX / cardWidth) + currRow * this._maxRowCount; + } + return index; }; /** + * Checks to see if a card is being dragged and calls the appropriate methods if so + * @param e the current pointer event + */ + + @action + onPointerMove = (x: number, y: number) => { + this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1; + }; + + /** + * Handles external drop of images/PDFs etc from outside Dash. + */ + onExternalDrop = async (e: React.DragEvent): Promise<void> => { + super.onExternalDrop(e, {}); + }; + + /** + * Resets all the doc dragging vairables once a card is dropped + * @param e + * @param de drop event + * @returns true if a card has been dropped, falls if not + */ + onInternalDrop = undoable( + action((e: Event, de: DragManager.DropEvent) => { + if (de.complete.docDragData) { + const dragIndex = this._docDraggedIndex; + const draggedDoc = DragManager.docsBeingDragged[0]; + if (dragIndex > -1 && draggedDoc) { + this._docDraggedIndex = -1; + const sorted = this.sortedDocs; + const originalIndex = sorted.findIndex(doc => doc === draggedDoc); + + this.Document.cardSort = ''; + originalIndex !== -1 && sorted.splice(originalIndex, 1); + sorted.splice(dragIndex, 0, draggedDoc); + if (de.complete.docDragData.removeDocument?.(draggedDoc)) { + this.dataDoc[this.fieldKey] = new List<Doc>(sorted); + } + this._dropped = true; + } + e.stopPropagation(); + return true; + } + return false; + }), + '' + ); + + @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 lestmost tags are given lower values while cards to the right are + * given higher values. Decimals are used to determine placement for cards with multiple tags + * @param doc the doc whose value is being determined + * @returns its value based on its tags + */ + + tagValue = (doc: Doc) => + Doc.MyFilterHotKeys.map((key, i) => ({ has: TagItem.docHasTag(doc, StrCast(key.toolType)), i })) + .filter(({ has }) => has) + .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 @@ -233,37 +346,46 @@ export class CollectionCardView extends CollectionSubView() { * @param isDesc * @returns */ - sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean) => { - if (sortType === cardSortings.None) return docs; - docs.sort((docA, docB) => { - const [typeA, typeB] = (() => { - switch (sortType) { - case cardSortings.Time: - return [DateCast(docA.author_date)?.date ?? Date.now(), - DateCast(docB.author_date)?.date ?? Date.now()]; - case cardSortings.Color: - return [DashColor(StrCast(docA.backgroundColor)).hsv().toString(), // If docA.type is undefined, use an empty string - DashColor(StrCast(docB.backgroundColor)).hsv().toString()]; // If docB.type is undefined, use an empty string - case cardSortings.Custom: - return [CollectionCardView.getButtonGroup(this.cardSort_customField, docA)??0, - CollectionCardView.getButtonGroup(this.cardSort_customField, docB)??0]; - default: return [StrCast(docA.type), // If docA.type is undefined, use an empty string - StrCast(docB.type)]; // If docB.type is undefined, use an empty string - } // prettier-ignore - })(); - - const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; - return isDesc ? -out : out; // Reverse the sort order if descending is true - }); + 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) { + case cardSortings.Time: + return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; + case cardSortings.Color: { + const d1 = DashColor(StrCast(docA.backgroundColor)); + const d2 = DashColor(StrCast(docB.backgroundColor)); + return [d1.hsv().hue(), d2.hsv().hue()]; + } + case cardSortings.Tag: + return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999]; + case cardSortings.Chat: + return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999]; + default: + return [StrCast(docA.type), StrCast(docB.type)]; + } + })(); + + const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; + return isDesc ? out : -out; + }); + 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; }; displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => ( <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} - ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))} + ref={action((r: DocumentView) => (!r?.ContentDiv ? this._docRefs.delete(doc) : this._docRefs.set(doc, r)))} Document={doc} NativeWidth={returnZero} NativeHeight={returnZero} @@ -272,11 +394,16 @@ export class CollectionCardView extends CollectionSubView() { 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 - isContentActive={this.isChildContentActive} + isContentActive={emptyFunction} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} - PanelWidth={this.panelWidth} - PanelHeight={this.panelHeight(doc)} + PanelWidth={this.childPanelWidth} + PanelHeight={this.childPanelHeight} + dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice. + dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType} + showTags={true} + dontHideOnDrag /> ); @@ -286,24 +413,24 @@ export class CollectionCardView extends CollectionSubView() { * @returns */ overflowAmCardsCalc = (index: number) => { - if (this.inactiveDocs().length < this._maxRowCount) { - return this.inactiveDocs().length; + if (this.sortedDocs.length < this._maxRowCount) { + return this.sortedDocs.length; } // 13 - 3 = 10 - const totalCards = this.inactiveDocs().length; + const totalCards = this.sortedDocs.length; // if 9 or less - if (index < totalCards - (totalCards % 10)) { + if (index < totalCards - (totalCards % this._maxRowCount)) { return this._maxRowCount; } // (3) - return totalCards % 10; + return totalCards % this._maxRowCount; }; /** * Determines the index a card is in in a row * @param realIndex * @returns */ - overflowIndexCalc = (realIndex: number) => realIndex % 10; + overflowIndexCalc = (realIndex: number) => realIndex % this._maxRowCount; /** * Translates the cards in the second rows and beyond over to the right * @param realIndex @@ -311,7 +438,7 @@ export class CollectionCardView extends CollectionSubView() { * @param calcRowCards * @returns */ - translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (10 - calcRowCards) * (this.panelWidth() / 2)); + 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 @@ -323,22 +450,15 @@ export class CollectionCardView extends CollectionSubView() { * @returns */ calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => { - if (isSelected) return 50 * this.fitContentScale; - const trans = isHovered ? this.translateHover(realIndex) : 0; - return trans + this.translateY(amCards, calcRowIndex, realIndex); + const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows; + const rowIndex = Math.trunc(realIndex / this._maxRowCount); + const rowToCenterShift = this.numRows / 2 - rowIndex; + if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2; + if (amCards == 1) return 50 * this.fitContentScale; + return this.translateY(amCards, calcRowIndex, realIndex); }; /** - * Toggles the buttons between on and off when creating custom sort groupings/changing those created by gpt - * @param childPairIndex - * @param buttonID - * @param doc - */ - toggleButton = undoable((buttonID: number, doc: Doc) => { - this.cardSort_customField && (doc[this.cardSort_customField] = buttonID); - }, 'toggle custom button'); - - /** * 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 @@ -355,6 +475,7 @@ export class CollectionCardView extends CollectionSubView() { }; 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()}======`; }); @@ -377,82 +498,113 @@ export class CollectionCardView extends CollectionSubView() { image[DocData].description = response.trim(); return response; // Return the response from gptImageLabel } catch (error) { - console.log('bad things have happened'); + console.log(error); } return ''; }; /** - * Converts the gpt output into a hashmap that can be used for sorting. lists are seperated by ==== while elements within the list are seperated by ~~~~~~ + * Processes gpt's output depending on the type of question the user asked. Converts gpt's string output to + * usable code * @param gptOutput */ - processGptOutput = (gptOutput: string) => { + @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) => { - // Split the item by '~~~~~~' to get all descriptors - const parts = item.split('~~~~~~').map(part => part.trim()); - - parts.forEach(part => { - // Find the corresponding Doc in the textToDoc map - const doc = this._textToDoc.get(part); - if (doc) { - doc.chat = 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}`); + } }); - }; + }, ''); + /** * Opens up the chat popup and starts the process for smart sorting. */ openChatPopup = async () => { GPTPopup.Instance.setVisible(true); - GPTPopup.Instance.setMode(GPTPopupMode.SORT); - const sortDesc = await this.childPairStringList(); // Await the promise to get the string result + GPTPopup.Instance.setMode(GPTPopupMode.CARD); GPTPopup.Instance.setCardsDoneLoading(true); // Set dataDoneLoading to true after data is loaded - GPTPopup.Instance.setSortDesc(sortDesc.join()); - GPTPopup.Instance.onSortComplete = (sortResult: string) => this.processGptOutput(sortResult); + await this.childPairStringListAndUpdateSortDesc(); }; /** - * Renders the buttons to customize sorting depending on which group the card belongs to and the amount of total groups - * @param childPairIndex - * @param doc - * @returns - */ - renderButtons = (doc: Doc, cardSort: cardSortings) => { - if (cardSort !== cardSortings.Custom) return ''; - const amButtons = Math.max(4, this.childDocs?.reduce((set, d) => this.cardSort_customField && set.add(NumCast(d[this.cardSort_customField])), new Set<number>()).size ?? 0); - const activeButtonIndex = CollectionCardView.getButtonGroup(this.cardSort_customField, doc); - const totalWidth = amButtons * 35 + amButtons * 2 * 5 + 6; - return ( - <div className="card-button-container" style={{ width: `${totalWidth}px` }}> - {numberRange(amButtons).map(i => ( - // eslint-disable-next-line jsx-a11y/control-has-associated-label - <button - key={i} - type="button" - style={{ backgroundColor: activeButtonIndex === i ? '#4476f7' : '#323232' }} // - onClick={() => this.toggleButton(i, doc)} - /> - ))} - </div> - ); - }; - /** * Actually renders all the cards */ - renderCards = () => { + @computed get renderCards() { + const sortedDocs = this.sortedDocs; const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc)); + const isEmpty = this.childDocsWithoutLinks.length === 0; + + if (isEmpty) { + 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.filter(sortDoc => !DocumentView.SelectedDocs().includes(sortDoc)).indexOf(doc); + return sortedDocs.map((doc, index) => { + const realIndex = sortedDocs.indexOf(doc); const calcRowIndex = this.overflowIndexCalc(realIndex); const amCards = this.overflowAmCardsCalc(realIndex); - const isSelected = DocumentView.SelectedDocs().includes(doc); + const view = DocumentView.getDocumentView(doc, this.DocumentView?.()); + const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false; const childScreenToLocal = () => { + // need to explicitly trigger an invalidation since we're reading everything from the Dom this._forceChildXf; + this._props.ScreenToLocalTransform(); + const dref = this._docRefs.get(doc); const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); return new Transform(-translateX + (dref?.centeringX || 0) * scale, @@ -460,53 +612,75 @@ export class CollectionCardView extends CollectionSubView() { .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore }; + const translateIfSelected = () => { + const indexInRow = index % this._maxRowCount; + const rowIndex = Math.trunc(index / this._maxRowCount); + const rowCenterIndex = Math.min(this._maxRowCount, sortedDocs.length - rowIndex * this._maxRowCount) / 2; + return (rowCenterIndex - indexInRow) * 100 - 50; + }; + const aspect = NumCast(doc.height) / NumCast(doc.width, 1); + const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), + (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore + const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size return ( <div key={doc[Id]} className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`} - onPointerUp={() => { - // this turns off documentDecorations during a transition, then turns them back on afterward. - SnappingManager.SetIsResizing(this.Document[Id]); - setTimeout( - action(() => { - SnappingManager.SetIsResizing(undefined); - this._forceChildXf = !this._forceChildXf; - }), - 700 - ); - }} + onPointerUp={action(() => { + // if a card doc has just moved, or a card is selected and in front, then ignore this event + if (DocumentView.SelectedDocs().includes(doc) || this._dropped) { + this._dropped = false; + } else { + // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts. + // Turn them back on when the animation has completed and the render and backend structures are in synch + SnappingManager.SetIsResizing(doc[Id]); + setTimeout( + action(() => { + SnappingManager.SetIsResizing(undefined); + this._forceChildXf++; + }), + 600 + ); + } + })} style={{ - width: this.panelWidth(), - height: 'max-content', // this.panelHeight(childPair.layout)(), + width: this.childPanelWidth(), + height: 'max-content', transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px) - translateX(${isSelected ? this.translateSelected(calcRowIndex) : this.translateOverflowX(realIndex, amCards)}px) + translateX(calc(${isSelected ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px)) rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg) - scale(${isSelected ? 1.25 : 1})`, - }} - onMouseEnter={() => this.setHoveredNodeIndex(index)}> + scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`, + }} // prettier-ignore + onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}> {this.displayDoc(doc, childScreenToLocal)} - {this.renderButtons(doc, this.cardSort)} </div> ); }); - }; + } + render() { + const isEmpty = this.childDocsWithoutLinks.length === 0; + return ( <div className="collectionCardView-outer" - ref={this.createDashEventsTarget} + ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} + 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), - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color), + background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, + color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, }}> <div className="card-wrapper" style={{ - transform: ` scale(${1 / this.fitContentScale}) translateX(${this.translateWrapperX}px)`, - height: `${100 * this.fitContentScale}%`, + ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }), + ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }), + gridAutoRows: `${100 / this.numRows}%`, }} onMouseLeave={() => this.setHoveredNodeIndex(-1)}> - {this.renderCards()} + {this.renderCards} </div> </div> ); diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 38f681e87..139aebb02 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; @@ -15,7 +13,8 @@ import { StyleProp } from '../StyleProp'; import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { Transform } from '../../util/Transform'; // eslint-disable-next-line @typescript-eslint/no-var-requires const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @@ -25,7 +24,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { @computed get scrollSpeed() { return this.layoutDoc._autoScrollSpeed ? NumCast(this.layoutDoc._autoScrollSpeed) : 1000; // default scroll speed } - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -48,15 +47,32 @@ export class CollectionCarousel3DView extends CollectionSubView() { } centerScale = Number(CAROUSEL3D_CENTER_SCALE); + sideScale = Number(CAROUSEL3D_SIDE_SCALE); panelWidth = () => this._props.PanelWidth() / 3; - panelHeight = () => this._props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE); + panelHeight = () => this._props.PanelHeight() * this.sideScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => !!this.isContentActive(); - childScreenToLocal = () => - this._props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1) - .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor. - .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2) + 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()) + .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()) + .scale(1 / this.sideScale); + childCenterScreenToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .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 .scale(1 / this.centerScale); focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => { @@ -68,9 +84,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { index !== -1 && (this.layoutDoc._carousel_index = index); return undefined; }; + @computed get content() { const currentIndex = NumCast(this.layoutDoc._carousel_index); - const displayDoc = (childPair: { layout: Doc; data: Doc }) => ( + const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => ( <DocumentView // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} @@ -80,12 +97,13 @@ export class CollectionCarousel3DView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={undefined} + containerViewPath={this.childContainerViewPath} onDoubleClickScript={this.onChildDoubleClick} renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} focus={this.focus} - ScreenToLocalTransform={this.childScreenToLocal} + ScreenToLocalTransform={dxf} isContentActive={this.isChildContentActive} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} @@ -95,7 +113,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.carouselItems.map((childPair, index) => ( <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> - {displayDoc(childPair)} + {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)} </div> )); } @@ -181,8 +199,8 @@ export class CollectionCarousel3DView extends CollectionSubView() { className="collectionCarousel3DView-outer" ref={this.createDashEventsTarget} style={{ - background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor), - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color), + background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, + color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string, }}> <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}> {this.content} diff --git a/src/client/views/collections/CollectionCarouselView.scss b/src/client/views/collections/CollectionCarouselView.scss index f115bb40a..01b20d6d3 100644 --- a/src/client/views/collections/CollectionCarouselView.scss +++ b/src/client/views/collections/CollectionCarouselView.scss @@ -12,6 +12,21 @@ user-select: none; } } +.collectionCarouselView-addFlashcards { + justify-content: center; + align-items: center; + height: 100%; + z-index: -1; + pointer-events: none; +} +.collectionCarouselView-recentlyMissed { + color: red; + z-index: 999; + position: relative; + left: 10px; + top: 10px; + pointer-events: none; +} .carouselView-back, .carouselView-fwd, .carouselView-star, diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 2adad68e0..6ea263215 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -1,24 +1,20 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { computed, makeObservable } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; -import { Doc, Opt } from '../../../fields/Doc'; -import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { ContextMenu } from '../ContextMenu'; -import { ContextMenuProps } from '../ContextMenuItem'; import { StyleProp } from '../StyleProp'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { TagItem } from '../TagsView'; enum cardMode { PRACTICE = 'practice', @@ -33,15 +29,36 @@ enum practiceVal { export class CollectionCarouselView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; get practiceField() { return this.fieldKey + "_practice"; } // prettier-ignore - get starField() { return this.fieldKey + "_star"; } // prettier-ignore + get starField() { return "#star"; } // prettier-ignore - constructor(props: any) { + _fadeTimer: NodeJS.Timeout | undefined; + _resetter: IReactionDisposer | undefined; + + 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 + ); + } componentWillUnmount() { this._dropDisposer?.(); + this._resetter?.(); } protected createDashEventsTarget = (ele: HTMLDivElement | null) => { @@ -51,43 +68,24 @@ export class CollectionCarouselView extends CollectionSubView() { } }; + @computed get marginX() { 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.childLayoutPairs.filter(pair => pair.layout.type !== DocumentType.LINK); - } - @computed get marginX() { - return NumCast(this.layoutDoc.caption_xMargin, 50); + 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 + }); } - move = (dir: number) => { - const moveToCardWithField = (match: (doc: Doc) => boolean): boolean => { - let startInd = (NumCast(this.layoutDoc._carousel_index) + dir) % this.carouselItems.length; - while (!match(this.carouselItems?.[startInd].layout) && (startInd + dir + this.carouselItems.length) % this.carouselItems.length !== this.layoutDoc._carousel_index) { - startInd = (startInd + dir + this.carouselItems.length) % this.carouselItems.length; - } - if (match(this.carouselItems?.[startInd].layout)) { - this.layoutDoc._carousel_index = startInd; - return true; - } - return match(this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout); - }; - switch (StrCast(this.layoutDoc.filterOp)) { - case cardMode.STAR: // go to a flashcard that is starred, skip the ones that aren't - if (!moveToCardWithField((doc: Doc) => !!doc[this.starField])) { - this.layoutDoc.filterOp = undefined; // if there aren't any starred, show all cards - } - break; - case cardMode.PRACTICE: // go to a new index that is missed, skip the ones that are correct - if (!moveToCardWithField((doc: Doc) => doc[this.practiceField] !== practiceVal.CORRECT)) { - 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.layout[this.practiceField] = undefined; - }); - } - break; - default: moveToCardWithField(returnTrue); - } // prettier-ignore - }; + move = action((dir: number) => { + this._last_index = this.carouselIndex; + this.layoutDoc._carousel_index = (this.carouselIndex + dir + this.carouselItems.length) % this.carouselItems.length; + }); /** * Goes to the next Doc in the stack subject to the currently selected filter option. @@ -110,8 +108,11 @@ export class CollectionCarouselView extends CollectionSubView() { */ star = (e: React.MouseEvent) => { e.stopPropagation(); - const curDoc = this.carouselItems[NumCast(this.layoutDoc._carousel_index)]; - curDoc.layout[this.starField] = curDoc.layout[this.starField] ? undefined : true; + 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); + } }; /* @@ -119,12 +120,12 @@ export class CollectionCarouselView extends CollectionSubView() { */ setPracticeVal = (e: React.MouseEvent, val: string) => { e.stopPropagation(); - const curDoc = this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]; - curDoc.layout[this.practiceField] = val; + const curDoc = this.carouselItems[this.carouselIndex]; + curDoc && (curDoc[this.practiceField] = val); this.advance(e); }; - captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<FieldViewProps>, property: string): any => { + 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); @@ -135,41 +136,90 @@ export class CollectionCarouselView extends CollectionSubView() { captionWidth = () => this._props.PanelWidth() - 2 * this.marginX; specificMenu = (): void => { const cm = ContextMenu.Instance; - const revealOptions = cm.findByDescription('Filter Flashcards'); - const revealItems: ContextMenuProps[] = revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; + 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' }); }; + + isChildContentActive = () => + this._props.isContentActive?.() === false + ? false + : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined; + + childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); + + renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { + return ( + <DocumentView + {...this._props} + ref={overlayFunc} + Document={doc} + NativeWidth={returnZero} + NativeHeight={returnZero} + fitWidth={undefined} + showTags={true} + containerViewPath={this.childContainerViewPath} + setContentViewBox={undefined} + ScreenToLocalTransform={this.childScreenToLocalXf} + onDoubleClickScript={this.onContentDoubleClick} + onClickScript={this.onContentClick} + isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} + isContentActive={this.isChildContentActive} + hideCaptions={showCaptions} + renderDepth={this._props.renderDepth + 1} + LayoutTemplate={this._props.childLayoutTemplate} + LayoutTemplateString={this._props.childLayoutString} + TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} + PanelHeight={this.panelHeight} + /> + ); + }; + /** + * Display an overlay of the previous card that crossfades to the next card + */ + @computed get overlay() { + const fadeTime = 500; + const lastDoc = this.carouselItems?.[this._last_index]; + 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` }}> + {this.renderDoc( + lastDoc, + false, // hide captions if the carousel is configured to show the captions + action((r: DocumentView | null) => { + if (r) { + this._fadeTimer && clearTimeout(this._fadeTimer); + this._last_opacity = 0; + this._fadeTimer = setTimeout( + action(() => { + this._last_index = -1; + this._last_opacity = 1; + }), + fadeTime + ); + } + }) + )} + </div> + ); + } @computed get content() { - const index = NumCast(this.layoutDoc._carousel_index); + 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?.layout instanceof Doc) ? null : ( + return !curDoc ? null : ( <> <div className="collectionCarouselView-image" key="image"> - <DocumentView - {...this._props} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} - setContentViewBox={undefined} - onDoubleClickScript={this.onContentDoubleClick} - onClickScript={this.onContentClick} - isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} - isContentActive={this._props.childContentsActive ?? this._props.isContentActive() === false ? returnFalse : emptyFunction} - hideCaptions={!!carouselShowsCaptions} // hide captions if the carousel is configured to show the captions - renderDepth={this._props.renderDepth + 1} - LayoutTemplate={this._props.childLayoutTemplate} - LayoutTemplateString={this._props.childLayoutString} - Document={curDoc.layout} - TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)} - PanelHeight={this.panelHeight} - /> + {this.renderDoc(curDoc, !!carouselShowsCaptions)} + {this.overlay} </div> {!carouselShowsCaptions ? null : ( <div @@ -177,19 +227,19 @@ export class CollectionCarouselView extends CollectionSubView() { key="caption" onWheel={StopEvent} style={{ - borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding), + borderRadius: this._props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding) as string, marginRight: this.marginX, marginLeft: this.marginX, width: `calc(100% - ${this.marginX * 2}px)`, }}> - <FormattedTextBox key={index} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} TemplateDataDocument={undefined} /> + <FormattedTextBox key={index} xPadding={10} yPadding={10} {...captionProps} fieldKey={carouselShowsCaptions} styleProvider={this.captionStyleProvider} Document={curDoc} TemplateDataDocument={undefined} /> </div> )} </> ); } @computed get buttons() { - if (!this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)]) return null; + if (!this.carouselItems?.[this.carouselIndex]) return null; return ( <> <div key="back" className="carouselView-back" onClick={this.goback}> @@ -198,9 +248,6 @@ export class CollectionCarouselView extends CollectionSubView() { <div key="fwd" className="carouselView-fwd" onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - <div key="star" className="carouselView-star" onClick={this.star}> - <FontAwesomeIcon icon="star" color={this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.starField] ? 'yellow' : 'gray'} size="1x" /> - </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> @@ -211,6 +258,24 @@ export class CollectionCarouselView extends CollectionSubView() { ); } + /** + * 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 + + /** + * 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 + render() { return ( <div @@ -218,33 +283,12 @@ export class CollectionCarouselView extends CollectionSubView() { ref={this.createDashEventsTarget} onContextMenu={this.specificMenu} style={{ - background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor), - color: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color), + 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} - {/* Displays a message to the user to add more flashcards if they are in practice mode and no flashcards are there. */} - <p - style={{ - display: !this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] && this.layoutDoc.filterOp === cardMode.PRACTICE ? 'flex' : 'none', - justifyContent: 'center', - alignItems: 'center', - height: '100%', - zIndex: '-1', - }}> - Add flashcards! - </p> - {/* Displays a message to the user that a flashcard was recently missed if they had previously gotten it wrong. */} - <p - style={{ - color: 'red', - zIndex: '999', - position: 'relative', - left: '10px', - top: '10px', - display: this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)] ? (this.carouselItems?.[NumCast(this.layoutDoc._carousel_index)].layout[this.practiceField] === practiceVal.MISSED ? 'block' : 'none') : 'none', - }}> - Recently missed! - </p> + {this.renderAddFlashcards()} + {this.renderRecentlyMissed()} {this.Document._chromeHidden ? null : this.buttons} </div> ); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 2a36e96bf..028133a6e 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -2,12 +2,14 @@ import { action, IReactionDisposer, makeObservable, observable, reaction } from import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; +import ResizeObserver from 'resize-observer-polyfill'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivHeight, DivWidth, incrementTitleCopy, returnTrue, UpdateIcon } from '../../../ClientUtils'; import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; import { AclAdmin, AclEdit, DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { FieldType } from '../../../fields/ObjectField'; import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { GetEffectiveAcl, inheritParentAcls, SetPropSetterCb } from '../../../fields/util'; @@ -28,19 +30,18 @@ import { OverlayView } from '../OverlayView'; import { ScriptingRepl } from '../ScriptingRepl'; import { UndoStack } from '../UndoStack'; import './CollectionDockingView.scss'; -import { CollectionSubView } from './CollectionSubView'; - -const _global = (window /* browser */ || global) /* node */ as any; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { TabHTMLElement } from './TabDocView'; @observer export class CollectionDockingView extends CollectionSubView() { - static tabClass: JSX.Element | null = null; + static tabClass: unknown = null; /** * 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: any) { + public static Init(ele: unknown) { this.tabClass = ele; DocumentView.addSplit = CollectionDockingView.AddSplit; } @@ -53,20 +54,22 @@ export class CollectionDockingView extends CollectionSubView() { private _flush: UndoManager.Batch | undefined; private _unmounting = false; private _ignoreStateChange = ''; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _goldenLayout: any = null; + // eslint-disable-next-line @typescript-eslint/no-explicit-any public tabMap: Set<any> = new Set(); public get HasFullScreen() { return this._goldenLayout._maximisedItem !== null; } - private _goldenLayout: any = null; static _highlightStyleSheet = addStyleSheet(); - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); if (this._props.renderDepth < 0) CollectionDockingView.Instance = this; // Why is this here? - (window as any).React = React; - (window as any).ReactDOM = ReactDOM; + (window as unknown as { React: unknown }).React = React; + (window as unknown as { ReactDOM: unknown }).ReactDOM = ReactDOM; DragManager.StartWindowDrag = this.StartOtherDrag; this.Document.myTrails; // this is equivalent to having a prefetchProxy for myTrails which is needed for the My Trails button in the UI which assumes that Doc.ActiveDashboard.myTrails is legit... } @@ -88,10 +91,11 @@ export class CollectionDockingView extends CollectionSubView() { }; tabItemDropped = () => DragManager.CompleteWindowDrag?.(false); + // eslint-disable-next-line @typescript-eslint/no-explicit-any tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => { this._flush = this._flush ?? UndoManager.StartBatch('tab move'); - const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc; - dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc])); + //const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc; + //dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc])); DragManager.CompleteWindowDrag = (aborted: boolean) => { if (aborted) { proxy._dragListener.AbortDrag(); @@ -129,12 +133,13 @@ export class CollectionDockingView extends CollectionSubView() { } @undoBatch + // eslint-disable-next-line @typescript-eslint/no-explicit-any public static ReplaceTab(document: Doc, mods: OpenWhereMod, stack: any, panelName: string, addToSplit?: boolean, keyValue?: boolean): boolean { const instance = CollectionDockingView.Instance; if (!instance) return false; const newConfig = DashboardView.makeDocumentConfig(document, panelName, undefined, keyValue); if (!panelName && stack) { - const activeContentItemIndex = stack.contentItems.findIndex((item: any) => item.config === stack._activeContentItem.config); + const activeContentItemIndex = stack.contentItems.findIndex((item: { config: unknown }) => item.config === stack._activeContentItem.config); const newContentItem = stack.layoutManager.createContentItem(newConfig, instance._goldenLayout); stack.addChild(newContentItem.contentItems[0], undefined); stack.contentItems[activeContentItemIndex].remove(); @@ -154,6 +159,7 @@ export class CollectionDockingView extends CollectionSubView() { } @undoBatch + // eslint-disable-next-line @typescript-eslint/no-explicit-any public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { return Array.from(CollectionDockingView.Instance?.tabMap.keys() ?? []).findIndex(tab => tab.DashDoc === doc) !== -1 ? CollectionDockingView.CloseSplit(doc) : CollectionDockingView.AddSplit(doc, location, stack, panelName, keyValue); } @@ -162,6 +168,7 @@ export class CollectionDockingView extends CollectionSubView() { // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // @action + // eslint-disable-next-line @typescript-eslint/no-explicit-any public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { if (document?._type_collection === CollectionViewType.Docking && !keyValue) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; @@ -320,7 +327,7 @@ export class CollectionDockingView extends CollectionSubView() { * @param target * @param title */ - titleChanged = (target: any, value: any) => { + titleChanged = (target: Doc, value: FieldType) => { const title = Field.toString(value); if (title.startsWith('@') && !title.substring(1).match(/[()[\]@]/) && title.length > 1) { const embedding = DocListCast(target.proto_embeddings).lastElement(); @@ -339,7 +346,7 @@ export class CollectionDockingView extends CollectionSubView() { () => DocumentView.LightboxDoc(), doc => setTimeout(() => !doc && this.onResize()) ); - new _global.ResizeObserver(this.onResize).observe(this._containerRef.current); + new ResizeObserver(this.onResize).observe(this._containerRef.current); this._reactionDisposer = reaction( () => StrCast(this.Document.dockingConfig), config => { @@ -428,7 +435,7 @@ export class CollectionDockingView extends CollectionSubView() { @action onPointerDown = (e: React.PointerEvent): void => { let hitFlyout = false; - for (let par = e.target as any; !hitFlyout && par; par = par.parentElement) { + for (let par = e.target as HTMLElement | null; !hitFlyout && par; par = par.parentElement) { hitFlyout = par.className === 'dockingViewButtonSelector'; } if (!hitFlyout) { @@ -513,6 +520,7 @@ export class CollectionDockingView extends CollectionSubView() { return changesMade; }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any tabDestroyed = (tab: any) => { this._flush = this._flush ?? UndoManager.StartBatch('tab movement'); const dashDoc = tab.DashDoc; @@ -530,18 +538,21 @@ export class CollectionDockingView extends CollectionSubView() { const { fieldKey } = CollectionDockingView.Instance.props; Doc.RemoveDocFromList(dview, fieldKey, dashDoc); this.tabMap.delete(tab); - tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + tab._disposers && Object.values(tab._disposers).forEach(disposer => (disposer as () => void)()); this.stateChanged(); } }; - tabCreated = (tab: any) => { + tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => { this.tabMap.add(tab); - tab.contentItem.element[0]?.firstChild?.firstChild?.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) + // InitTab is added to the tab's HTMLElement in TabDocView + const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown 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) }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any stackCreated = (stackIn: any) => { const stack = stackIn.header ? stackIn : stackIn.origin; - stack.header?.element.on('mousedown', (e: any) => { + stack.header?.element.on('mousedown', (e: MouseEvent) => { const dashboard = Doc.ActiveDashboard; if (dashboard && e.target === stack.header?.element[0] && e.button === 2) { dashboard.pane_count = NumCast(dashboard.pane_count) + 1; @@ -594,7 +605,7 @@ export class CollectionDockingView extends CollectionSubView() { }) ); - stack.element.click((e: any) => { + stack.element.click((e: { originalEvent: MouseEvent }) => { if (stack.contentItems.length === 0 && Array.from(document.elementsFromPoint(e.originalEvent.x, e.originalEvent.y)).some(ele => ele?.className === 'empty-tabs-message')) { addNewDoc(); } @@ -632,7 +643,7 @@ export class CollectionDockingView extends CollectionSubView() { ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback - function openInLightbox(doc: any) { + function openInLightbox(doc: Doc) { CollectionDockingView.Instance?._props.addDocTab(doc, OpenWhere.lightboxAlways); }, 'opens up document in a lightbox', @@ -640,33 +651,22 @@ ScriptingGlobals.add( ); ScriptingGlobals.add( // eslint-disable-next-line prefer-arrow-callback - function openDoc(doc: any, where: OpenWhere) { + function openDoc(doc: Doc | string, where: OpenWhere) { switch (where) { case OpenWhere.addRight: - return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); + return doc instanceof Doc && CollectionDockingView.AddSplit(doc, OpenWhereMod.right); case OpenWhere.overlay: default: - // prettier-ignore switch (doc) { case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); case "<UndoStack />": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Undo stack' }); - default: - } - Doc.AddToMyOverlay(doc); - return true; + default: return doc instanceof Doc && Doc.AddToMyOverlay(doc); + } // prettier-ignore } }, 'opens up document in location specified', '(doc: any)' ); -ScriptingGlobals.add( - // eslint-disable-next-line prefer-arrow-callback - function openRepl() { - return 'openRepl'; - }, - 'opens up document in screen overlay layer', - '(doc: any)' -); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(async function snapshotDashboard() { const batch = UndoManager.StartBatch('snapshot'); diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index 9a6f1e2eb..710c00841 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -1,6 +1,3 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; @@ -16,7 +13,7 @@ import { DragManager } from '../../util/DragManager'; import { CompileScript } from '../../util/Scripting'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; +import { undoBatch, undoable } from '../../util/UndoManager'; import { EditableView } from '../EditableView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; @@ -37,13 +34,13 @@ interface CMVFieldRowProps { createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; setDocHeight: (key: string, thisHeight: number) => void; - refList: any[]; + refList: Element[]; showHandle: boolean; } @observer export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVFieldRowProps> { - constructor(props: any) { + constructor(props: CMVFieldRowProps) { super(props); makeObservable(this); } @@ -73,7 +70,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF private _dropDisposer?: DragManager.DragDropDisposer; private _headerRef: React.RefObject<HTMLDivElement> = React.createRef(); private _contRef: React.RefObject<HTMLDivElement> = React.createRef(); - private _ele: any; + private _ele: HTMLDivElement | null = null; createRowDropRef = (ele: HTMLDivElement | null) => { this._dropDisposer?.(); @@ -118,7 +115,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF return false; }); - getValue = (value: string): any => { + getValue = (value: string) => { const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; if (value.toLowerCase().indexOf('true') > -1) return true; @@ -173,7 +170,7 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF return docs ? !!docs.splice(0, 0, newDoc) : this._props.parent._props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list) }; - deleteRow = undoBatch( + deleteRow = undoable( action(() => { this._createEmbeddingSelected = false; const key = this._props.pivotField; @@ -182,11 +179,12 @@ export class CollectionMasonryViewFieldRow extends ObservableReactComponent<CMVF const index = this._props.parent.colHeaderData.indexOf(this._props.headingObject); this._props.parent.colHeaderData.splice(index, 1); } - }) + }), + 'delete row' ); @action - collapseSection = (e: any) => { + collapseSection = (e: PointerEvent) => { this._createEmbeddingSelected = false; this.toggleVisibility(); e.stopPropagation(); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index b2f0280a5..dab1298d5 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -1,7 +1,3 @@ -/* eslint-disable jsx-a11y/label-has-associated-control */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/control-has-associated-label */ /* eslint-disable react/no-unused-class-component-methods */ /* eslint-disable react/sort-comp */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -10,9 +6,9 @@ import { Toggle, ToggleType, Type } from 'browndash-components'; import { Lambda, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; +import { ClientUtils, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -23,7 +19,7 @@ import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch } from '../../util/UndoManager'; +import { undoBatch, undoable } from '../../util/UndoManager'; import { AntimodeMenu } from '../AntimodeMenu'; import { EditableView } from '../EditableView'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider'; @@ -185,7 +181,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu params: ['target', 'source'], title: 'item view', script: 'this.target.childLayoutTemplate = getDocTemplate(this.source?.[0])', - immediate: undoBatch((source: Doc[]) => { + immediate: undoable((source: Doc[]) => { let formatStr = source.length && Cast(source[0].text, RichTextField, null)?.Text; try { formatStr && JSON.parse(formatStr); @@ -200,25 +196,25 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu Doc.SetInPlace(this.target, 'childLayoutString', undefined, true); Doc.SetInPlace(this.target, 'childLayoutTemplate', undefined, true); } - }), + }, ''), initialize: emptyFunction, }; _narrativeCommand = { params: ['target', 'source'], title: 'child click view', script: 'this.target.childClickedOpenTemplateView = getDocTemplate(this.source?.[0])', - immediate: undoBatch((source: Doc[]) => { + immediate: undoable((source: Doc[]) => { source.length && (this.target.childClickedOpenTemplateView = Doc.getDocTemplate(source?.[0])); - }), + }, 'narrative command'), initialize: emptyFunction, }; _contentCommand = { params: ['target', 'source'], title: 'set content', script: 'getProto(this.target).data = copyField(this.source);', - immediate: undoBatch((source: Doc[]) => { + immediate: undoable((source: Doc[]) => { this.target[DocData].data = new List<Doc>(source); - }), + }, ''), initialize: emptyFunction, }; _onClickCommand = { @@ -229,19 +225,19 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu getProto(this.proxy[0]).target = this.target.target; getProto(this.proxy[0]).source = copyField(this.target.source); }}`, - immediate: undoBatch(() => {}), + immediate: undoable(() => {}, ''), initialize: emptyFunction, }; _viewCommand = { params: ['target'], title: 'bookmark view', script: "this.target._freeform_panX = this.target_freeform_panX; this.target._freeform_panY = this['target-freeform_panY']; this.target._freeform_scale = this['target_freeform_scale']; gotoFrame(this.target, this['target-currentFrame']);", - immediate: undoBatch(() => { + immediate: undoable(() => { this.target._freeform_panX = 0; this.target._freeform_panY = 0; this.target._freeform_scale = 1; this.target._currentFrame = this.target._currentFrame === undefined ? undefined : 0; - }), + }, ''), initialize: (button: Doc) => { button['target-panX'] = this.target._freeform_panX; button['target-panY'] = this.target._freeform_panY; @@ -253,18 +249,18 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu params: ['target'], title: 'fit content', script: 'this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox;', - immediate: undoBatch(() => { + immediate: undoable(() => { this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox; - }), + }, ''), initialize: emptyFunction, }; _fitContentCommand = { params: ['target'], title: 'toggle clusters', script: 'this.target._freeform_useClusters = !this.target._freeform_useClusters;', - immediate: undoBatch(() => { + immediate: undoable(() => { this.target._freeform_useClusters = !this.target._freeform_useClusters; - }), + }, ''), initialize: emptyFunction, }; _saveFilterCommand = { @@ -272,10 +268,10 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu title: 'save filter', script: `this.target._childFilters = compareLists(this.target_childFilters,this.target._childFilters) ? undefined : copyField(this.target_childFilters); this.target._searchFilterDocs = compareLists(this.target_searchFilterDocs,this.target._searchFilterDocs) ? undefined: copyField(this.target_searchFilterDocs);`, - immediate: undoBatch(() => { + immediate: undoable(() => { this.target._childFilters = undefined; this.target._searchFilterDocs = undefined; - }), + }, ''), initialize: (button: Doc) => { const activeDash = Doc.ActiveDashboard; if (activeDash) { @@ -598,9 +594,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu */ onNumColsChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.currentTarget.valueAsNumber > 0) - undoBatch(() => { + undoable(() => { this.document.gridNumCols = e.currentTarget.valueAsNumber; - })(); + }, '')(); }; /** @@ -629,9 +625,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu onIncrementButtonClick = () => { this.clicked = true; this.entered && (this.document.gridNumCols as number)--; - undoBatch(() => { + undoable(() => { this.document.gridNumCols = this.numCols + 1; - })(); + }, '')(); this.entered = false; }; @@ -642,9 +638,9 @@ export class CollectionGridViewChrome extends React.Component<CollectionViewMenu this.clicked = true; if (this.numCols > 1 && !this.decrementLimitReached) { this.entered && (this.document.gridNumCols as number)++; - undoBatch(() => { + undoable(() => { this.document.gridNumCols = this.numCols - 1; - })(); + }, '')(); if (this.numCols === 1) this.decrementLimitReached = true; } this.entered = false; diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 16c474996..ac8e37358 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -30,9 +30,8 @@ import { StyleProp } from '../StyleProp'; import './CollectionNoteTakingView.scss'; import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn'; import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider'; -import { CollectionSubView } from './CollectionSubView'; - -const _global = (window /* browser */ || global) /* node */ as any; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { Property } from 'csstype'; /** * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1) @@ -52,9 +51,9 @@ export class CollectionNoteTakingView extends CollectionSubView() { public DividerWidth = 16; @observable docsDraggedRowCol: number[] = []; @observable _scroll = 0; - @observable _refList: any[] = []; + @observable _refList: HTMLElement[] = []; - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -78,7 +77,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { return colHeaderData ?? ([] as SchemaHeaderField[]); } @computed get headerMargin() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number; } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 5); @@ -216,7 +215,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // let's dive in and get the actual document we want to drag/move around focusDocument = (doc: Doc, options: FocusViewOptions) => { Doc.BrushDoc(doc); - const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find(node => node.id === doc[Id]); if (found) { const { top } = found.getBoundingClientRect(); const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top); @@ -295,7 +294,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { addDocument={this._props.addDocument} moveDocument={this._props.moveDocument} removeDocument={this._props.removeDocument} - contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as any} + contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as Property.PointerEvents} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} addDocTab={this._props.addDocTab} pinToPres={this._props.pinToPres} @@ -313,14 +312,14 @@ 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) { + getDocWidth = (d: Doc) => { const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType); 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; const width = d.layout_fitWidth ? maxWidth : NumCast(d._width); return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); - } + }; // how to get the height of a document. Nothing special here. getDocHeight(d?: Doc) { @@ -364,7 +363,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { // onPointerMove is used to preview where a document will drop in a column once a drag is complete. @action onPointerMove = (force: boolean, ex: number, ey: number) => { - if (this.childDocList?.includes(DragManager.DocDragData?.draggedDocuments?.lastElement() as any) || force || SnappingManager.CanEmbed) { + const dragDoc = DragManager.DraggedDocs?.lastElement(); + if ((dragDoc && this.childDocList?.includes(dragDoc)) || force || SnappingManager.CanEmbed) { // get the current docs for the column based on the mouse's x coordinate const xCoord = this.ScreenToLocalBoxXf().transformPoint(ex, ey)[0] - 2 * this.gridGap; const colDocs = this.getDocsFromXCoord(xCoord); @@ -500,7 +500,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { super.onExternalDrop( e, {}, - undoBatch( + undoable( action(docus => { this.onPointerMove(true, e.clientX, e.clientY); docus?.map((doc: Doc) => this.addDocument(doc)); @@ -513,7 +513,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { docs.splice(targInd, 0, newDoc); } this.removeDocDragHighlight(); - }) + }), + 'drop into note view' ) ); }; @@ -652,7 +653,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { const sections = Array.from(this.Sections.entries()); return sections.reduce((list, sec, i) => { list.push(this.sectionNoteTaking(sec[0], sec[1])); - // eslint-disable-next-line react/no-array-index-key i !== sections.length - 1 && list.push(<CollectionNoteTakingViewDivider key={`divider${i}`} isContentActive={this.isContentActive} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); return list; }, [] as JSX.Element[]); @@ -673,7 +673,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { return this.isContentActive() === false ? 'none' : undefined; } - observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)))); + observer = new ResizeObserver(() => this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)))); render() { TraceMobx(); diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 44ab1968d..8c6a6b551 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; @@ -16,7 +15,7 @@ import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoBatch, undoable } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; -import { EditableView } from '../EditableView'; +import { EditableProps, EditableView } from '../EditableView'; import { ObservableReactComponent } from '../ObservableReactComponent'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionNoteTakingView.scss'; @@ -24,7 +23,7 @@ import './CollectionNoteTakingView.scss'; interface CSVFieldColumnProps { Document: Doc; TemplateDataDocument: Opt<Doc>; - backgroundColor?: (() => string) | undefined; + backgroundColor?: () => string | undefined; docList: Doc[]; heading: string; pivotField: string; @@ -35,15 +34,15 @@ interface CSVFieldColumnProps { yMargin: number; numGroupColumns: number; gridGap: number; - headings: () => object[]; + headings: () => [SchemaHeaderField, Doc[]][]; select: (ctrlPressed: boolean) => void; isContentActive: () => boolean | undefined; renderChildren: (docs: Doc[]) => JSX.Element[]; addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; - refList: any[]; - editableViewProps: () => any; + refList: HTMLElement[]; + editableViewProps: () => EditableProps; resizeColumns: (headers: SchemaHeaderField[]) => boolean; maxColWidth: number; dividerWidth: number; @@ -103,7 +102,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV return true; }; - getValue = (value: string): any => { + getValue = (value: string) => { const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; if (value.toLowerCase().indexOf('true') > -1) return true; @@ -272,7 +271,7 @@ export class CollectionNoteTakingViewColumn extends ObservableReactComponent<CSV style={{ width: this.columnWidth, background: this._hover && SnappingManager.IsDragging ? '#b4b4b4' : 'inherit', - marginLeft: this._props.headings().findIndex((h: any) => h[0] === this._props.headingObject) === 0 ? NumCast(this._props.Document.xMargin) : 0, + marginLeft: this._props.headings().findIndex(h => h[0] === this._props.headingObject) === 0 ? NumCast(this._props.Document.xMargin) : 0, }}> <div className="collectionNoteTakingViewFieldColumn" key={this._heading} ref={this.createColumnDropRef}> {this.innards} diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 5b3f625db..eea128803 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,10 +1,8 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, IReactionDisposer, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { returnFalse, setupMoveUpEvents } from '../../../ClientUtils'; -import { Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, FieldResult } from '../../../fields/Doc'; import { ScriptField } from '../../../fields/ScriptField'; import { NumCast, StrCast, toList } from '../../../fields/Types'; import { emptyFunction } from '../../../Utils'; @@ -15,15 +13,15 @@ import { OpenWhere } from '../nodes/OpenWhere'; import { computePassLayout, computeStarburstLayout } from './collectionFreeForm'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; import './CollectionPileView.scss'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { DocumentView } from '../nodes/DocumentView'; @observer export class CollectionPileView extends CollectionSubView() { - _originalChrome: any = ''; + _originalChrome: FieldResult = ''; _disposers: { [name: string]: IReactionDisposer } = {}; - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index b03f0cffa..486c826b6 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -1,14 +1,11 @@ /* eslint-disable react/jsx-props-no-spreading */ -/* eslint-disable jsx-a11y/alt-text */ /* eslint-disable no-use-before-define */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; -import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../ClientUtils'; -import { Doc, Opt } from '../../../fields/Doc'; +import { returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../ClientUtils'; +import { Doc, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -34,7 +31,7 @@ import { LabelBox } from '../nodes/LabelBox'; import { OpenWhere } from '../nodes/OpenWhere'; import { ObservableReactComponent } from '../ObservableReactComponent'; import './CollectionStackedTimeline.scss'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; export type CollectionStackedTimelineProps = { Play: () => void; @@ -72,7 +69,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack ); this.SelectingRegions.clear(); } - constructor(props: any) { + constructor(props: SubCollectionViewProps & CollectionStackedTimelineProps) { super(props); makeObservable(this); } @@ -182,7 +179,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack }); anchorStart = (anchor: Doc) => NumCast(anchor._timecodeToShow, NumCast(anchor[this._props.startTag])); - anchorEnd = (anchor: Doc, val: any = null) => NumCast(anchor._timecodeToHide, NumCast(anchor[this._props.endTag], val) ?? null); + anchorEnd = (anchor: Doc, val?: number) => NumCast(anchor._timecodeToHide, NumCast(anchor[this._props.endTag], val) ?? null); // converts screen pixel offset to time // prettier-ignore @@ -192,13 +189,13 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack @computed get rangeClick() { // prettier-ignore return ScriptField.MakeFunction('stackedTimeline.clickAnchor(this, clientX)', - { stackedTimeline: 'any', clientX: 'number' }, { stackedTimeline: this as any } + { stackedTimeline: 'any', clientX: 'number' }, { stackedTimeline: 'string' /* should be CollectionStackedTimeline */ } )!; } @computed get rangePlay() { // prettier-ignore return ScriptField.MakeFunction('stackedTimeline.playOnClick(this, clientX)', - { stackedTimeline: 'any', clientX: 'number' }, { stackedTimeline: this as any })!; + { stackedTimeline: 'any', clientX: 'number' }, { stackedTimeline: 'string' /* should be CollectionStackedTimeline */})!; } rangeClickScript = () => this.rangeClick; rangePlayScript = () => this.rangePlay; @@ -426,7 +423,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack const anchor = docAnchor ?? Docs.Create.LabelDocument({ - title: ComputedField.MakeFunction(`this["${endTag}"] ? "#" + formatToTime(this["${startTag}"]) + "-" + formatToTime(this["${endTag}"]) : "#" + formatToTime(this["${startTag}"])`) as any, + title: ComputedField.MakeFunction(`this["${endTag}"] ? "#" + formatToTime(this["${startTag}"]) + "-" + formatToTime(this["${endTag}"]) : "#" + formatToTime(this["${startTag}"])`) as unknown as string, // title can take a function or a string _label_minFontSize: 12, _label_maxFontSize: 24, _dragOnlyWithinContainer: true, @@ -777,8 +774,8 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch @action onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => { const newTime = (timeDownEv: PointerEvent) => { - const rect = (timeDownEv.target as any).getBoundingClientRect(); - return this._props.toTimeline(timeDownEv.clientX - rect.x, rect.width); + const rect = (timeDownEv.target as HTMLElement).getBoundingClientRect?.(); + return !rect ? 0 : this._props.toTimeline(timeDownEv.clientX - rect.x, rect.width); }; const changeAnchor = (time: number | undefined) => { const timelineOnly = Cast(anchor[this._props.startTag], 'number', null) !== undefined; @@ -892,7 +889,7 @@ class StackedTimelineAnchor extends ObservableReactComponent<StackedTimelineAnch } } // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function formatToTime(time: number): any { +ScriptingGlobals.add(function formatToTime(time: number): string { return formatTime(time); }); // eslint-disable-next-line prefer-arrow-callback diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index 6225cc52a..6400a0a8e 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -54,11 +54,8 @@ } .collectionStackingViewFieldColumn { - height: max-content; - } - - .collectionStackingViewFieldColumnDragging { - height: 100%; + display: flex; + flex-direction: column; } .collectionSchemaView-previewDoc { diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index 56d2a6c9c..1ac0b6d70 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -1,11 +1,9 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -// eslint-disable-next-line import/no-extraneous-dependencies import * as CSS from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, DivHeight, returnEmptyDoclist, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; +import { ClientUtils, DivHeight, returnNone, returnZero, setupMoveUpEvents, smoothScroll } from '../../../ClientUtils'; import { Doc, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -31,12 +29,12 @@ import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { StyleProp } from '../StyleProp'; +import { returnEmptyDocViewList } from '../StyleProvider'; import { CollectionMasonryViewFieldRow } from './CollectionMasonryViewFieldRow'; import './CollectionStackingView.scss'; import { CollectionStackingViewFieldColumn } from './CollectionStackingViewFieldColumn'; -import { CollectionSubView } from './CollectionSubView'; - -const _global = (window /* browser */ || global) /* node */ as any; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { computedFn } from 'mobx-utils'; export type collectionStackingViewProps = { sortFunc?: (a: Doc, b: Doc) => number; @@ -57,8 +55,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; // Doesn't look like this field is being used anywhere. Obsolete? _columnStart: number = 0; + _oldWheel: HTMLElement | null = null; - @observable _refList: any[] = []; + @observable _refList: HTMLElement[] = []; // map of node headers to their heights. Used in Masonry @observable _heightMap = new Map<string, number>(); // Assuming that this is the current css cursor style @@ -85,7 +84,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // how much margin we give the header @computed get headerMargin() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number; } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, Math.max(3, 0.05 * this._props.PanelWidth())); @@ -99,7 +98,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // are we stacking or masonry? @computed get isStackingView() { - return (this._props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking; + return (this._props.type_collection ?? this.layoutDoc._type_collection) !== CollectionViewType.Masonry; } // this is the number of StackingViewFieldColumns that we have @computed get numGroupColumns() { @@ -118,7 +117,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this._props.PanelWidth() - this.gridGap; } - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); if (this.colHeaderData === undefined) { @@ -128,6 +127,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } } + columnWidthFn = () => this.columnWidth; + columnDocHeightFn = (doc: Doc) => () => (this.isStackingView ? this.getDocHeight(doc)() : Math.min(this.getDocHeight(doc)(), this._props.PanelHeight())); + // TODO: plj - these are the children children = (docs: Doc[]) => { // TODO: can somebody explain me to what exactly TraceMobX is? @@ -141,17 +143,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }) ); return docs.map((d, i) => { - const height = () => this.getDocHeight(d); - const width = () => this.getDocWidth(d); - const trans = () => this.getDocTransition(d); // 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((height() + this.gridGap) / this.gridGap); + const rowSpan = Math.ceil((this.getDocHeight(d)() + this.gridGap) / this.gridGap); // just getting the style - const style = this.isStackingView ? { margin: this.Document._stacking_alignCenter ? 'auto' : undefined, transition: trans(), width: width(), marginTop: i ? this.gridGap : 0, height: height() } : { 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)() } : { gridRowEnd: `span ${rowSpan}` }; // 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}> - {this.getDisplayDoc(d, width, trans, i)} + {this.getDisplayDoc(d, this.getDocTransition(d), i)} </div> ); }); @@ -260,7 +259,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection focusDocument = (doc: Doc, options: FocusViewOptions) => { Doc.BrushDoc(doc); - const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); + const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find(node => node.id === doc[Id]); if (found) { const { top } = found.getBoundingClientRect(); const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top); @@ -312,26 +311,23 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined; + isChildButtonContentActive = () => (this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false ? false : undefined); @observable docRefs = new ObservableMap<Doc, DocumentView>(); childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); // this is what renders the document that you see on the screen // called in Children: this actually adds a document to our children list - getDisplayDoc(doc: Doc, width: () => number, trans: () => string, count: number) { + getDisplayDoc(doc: Doc, trans: () => string, count: number) { const dataDoc = doc.isTemplateDoc || doc.isTemplateForField ? this._props.TemplateDataDocument : undefined; - const height = () => this.getDocHeight(doc); - const panelHeight = () => (this.isStackingView ? height() : Math.min(height(), this._props.PanelHeight())); - const panelWidth = () => (this.isStackingView ? width() : this.columnWidth); - const stackedDocTransform = () => this.getDocTransform(doc); - this._docXfs.push({ stackedDocTransform, width, height }); + this._docXfs.push({ stackedDocTransform: this.getDocTransform(doc), width: this.getDocWidth(doc), height: this.getDocHeight(doc) }); return count > this._renderCount ? null : ( <DocumentView ref={action((r: DocumentView) => r?.ContentDiv && this.docRefs.set(doc, r))} Document={doc} TemplateDataDocument={dataDoc} renderDepth={this._props.renderDepth + 1} - PanelWidth={panelWidth} - PanelHeight={panelHeight} + PanelWidth={this.columnWidthFn} + PanelHeight={this.columnDocHeightFn(doc)} pointerEvents={this.DocumentView?.()._props.onClickScript?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView) styleProvider={this.styleProvider} containerViewPath={this.childContainerViewPath} @@ -342,20 +338,21 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} - NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox - NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? height : undefined} - dontCenter={this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as any)} + NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeWidth(doc)) ? this.getDocWidth(doc) : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (this.childFitWidth(doc) && !Doc.NativeHeight(doc)) ? this.getDocHeight(doc) : undefined} + dontCenter={this.dontCenter} 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} dragAction={(this.layoutDoc.childDragAction ?? this._props.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} - ScreenToLocalTransform={stackedDocTransform} + ScreenToLocalTransform={this.getDocTransform(doc)} focus={this.focusDocument} childFilters={this.childDocFilters} hideDecorationTitle={this._props.childHideDecorationTitle} hideResizeHandles={this._props.childHideResizeHandles} + hideDecorations={this._props.childHideDecorations} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} xPadding={NumCast(this.layoutDoc._childXPadding, this._props.childXPadding)} @@ -363,7 +360,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection addDocument={this._props.addDocument} moveDocument={this._props.moveDocument} removeDocument={this._props.removeDocument} - contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as any} + contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as CSS.Property.PointerEvents | undefined} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} addDocTab={this._props.addDocTab} pinToPres={this._props.pinToPres} @@ -371,14 +368,18 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection ); } - getDocTransform(doc: Doc) { + getDocTransform = computedFn((doc: Doc) => () => { + // these must be referenced for document decorations to update when the text box container is scrolled + this._scroll; + this._props.ScreenToLocalTransform(); + const dref = this.docRefs.get(doc); - this._scroll; // must be referenced for document decorations to update when the text box container is scrolled - const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv); - // the document view may center its contents and if so, will prepend that onto the screenToLocalTansform. so we have to subtract that off - return new Transform(-translateX + (dref?.centeringX || 0), -translateY + (dref?.centeringY || 0), 1).scale(this.ScreenToLocalBoxXf().Scale); - } - getDocWidth(d?: Doc) { + const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); + return new Transform(-translateX + (dref?.centeringX || 0) * scale, + -translateY + (dref?.centeringY || 0) * scale, 1) + .scale(1 / scale); // prettier-ignore + }); + getDocWidth = computedFn((d?: Doc) => () => { if (!d) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; @@ -386,12 +387,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return Math.min(NumCast(d._width), maxWidth); } return maxWidth; - } - getDocTransition(d?: Doc) { - if (!d) return ''; - return StrCast(d.dataTransition); - } - getDocHeight(d?: Doc) { + }); + getDocTransition = computedFn((d?: Doc) => () => StrCast(d?.dataTransition)); + getDocHeight = computedFn((d?: Doc) => () => { if (!d || d.hidden) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument; @@ -400,13 +398,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!this.childFitWidth(childLayoutDoc) ? NumCast(d._height) : 0); if (nw && nh) { const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); - const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); + const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d)(), colWid); return Math.min(maxHeight, (docWid * nh) / nw); } const childHeight = NumCast(childLayoutDoc._height); const panelHeight = this.childFitWidth(childLayoutDoc) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); - } + }); // This following three functions must be from the view Mehek showed columnDividerDown = (e: React.PointerEvent) => { @@ -530,6 +528,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } }); }; + @computed get dontCenter() { + return this._props.dontCenter ?? (this._props.childIgnoreNativeSize ? 'xy' : (StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy')); + } headings = () => Array.from(this.Sections); // what a section looks like if we're in stacking view sectionStacking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { @@ -538,7 +539,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (this.pivotField) { const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - // eslint-disable-next-line prefer-destructuring type = types[0]; } } @@ -564,6 +564,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection type={type} createDropTarget={this.createDashEventsTarget} screenToLocalTransform={this.ScreenToLocalBoxXf} + dontCenter={this.dontCenter} /> ); }; @@ -574,7 +575,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection let type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; const types = docList.length ? docList.map(d => typeof d[key]) : this.filteredChildren.map(d => typeof d[key]); if (types.map((i, idx) => types.indexOf(i) === idx).length === 1) { - // eslint-disable-next-line prefer-destructuring type = types[0]; } const rows = () => (!this.isStackingView ? 1 : Math.max(1, Math.min(docList.length, Math.floor((this._props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))))); @@ -623,7 +623,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (!e.isPropagationStopped()) { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + const optionItems: ContextMenuProps[] = options?.subitems ?? []; optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => { this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill; }, icon: 'plus' }); // prettier-ignore optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => { this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; }, icon: 'plus' }); // prettier-ignore optionItems.push({ description: 'Clear All', event: () => { this.dataDoc[this.fieldKey ?? 'data'] = new List([]); } , icon: 'times' }); // prettier-ignore @@ -663,7 +663,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection renderDepth={this._props.renderDepth} focus={emptyFunction} styleProvider={this._props.styleProvider} - containerViewPath={returnEmptyDoclist} + containerViewPath={returnEmptyDocViewList} whenChildContentsActiveChanged={emptyFunction} childFilters={this._props.childFilters} childFiltersByRanges={this._props.childFiltersByRanges} @@ -688,10 +688,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this._props.isContentActive() === false ? 'none' : undefined; } - observer = new _global.ResizeObserver(() => this._props.setHeight?.(this.headerMargin + (this.isStackingView ? Math.max(...this._refList.map(DivHeight)) : this._refList.reduce((p, r) => p + DivHeight(r), 0)))); + 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(); - _oldWheel: any; render() { TraceMobx(); const editableViewProps = { @@ -722,8 +721,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }} style={{ overflowY: this.isContentActive() ? 'auto' : 'hidden', - background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor), - pointerEvents: (this._props.pointerEvents?.() as any) ?? this.backgroundEvents, + background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string, + pointerEvents: this._props.pointerEvents?.() ?? this.backgroundEvents, }} onScroll={action(e => { this._scroll = e.currentTarget.scrollTop; diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index e2ad5b31d..ed0cabd0a 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -1,6 +1,3 @@ -/* eslint-disable jsx-a11y/control-has-associated-label */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -44,6 +41,7 @@ interface CSVFieldColumnProps { columnWidth: number; numGroupColumns: number; gridGap: number; + dontCenter: 'x' | 'xy' | 'y'; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; headings: () => object[]; // I think that stacking view actually has a single column and then supposedly you can add more columns? Unsure @@ -51,7 +49,7 @@ interface CSVFieldColumnProps { addDocument: (doc: Doc | Doc[]) => boolean; createDropTarget: (ele: HTMLDivElement) => void; screenToLocalTransform: () => Transform; - refList: any[]; + refList: HTMLElement[]; } @observer @@ -64,7 +62,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< @observable _heading = ''; @observable _color = ''; - constructor(props: any) { + constructor(props: CSVFieldColumnProps) { super(props); makeObservable(this); this._heading = this._props.headingObject ? this._props.headingObject.heading : this._props.heading; @@ -118,7 +116,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< this._props.pivotField && drop.docs?.forEach(d => Doc.SetInPlace(d, this._props.pivotField, drop.val, false)); return true; }); - getValue = (value: string): any => { + getValue = (value: string) => { const parsed = parseInt(value); if (!isNaN(parsed)) return parsed; if (value.toLowerCase().indexOf('true') > -1) return true; @@ -212,7 +210,7 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< <div className="colorOptions"> {colors.map(col => { const palette = PastelSchemaPalette.get(col); - return <div className={'colorPicker' + (selected === palette ? ' active' : '')} style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; + return <div key={col} className={'colorPicker' + (selected === palette ? ' active' : '')} style={{ backgroundColor: palette }} onClick={() => this.changeColumnColor(palette!)} />; })} </div> </div> @@ -348,15 +346,6 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< <button type="button" className="collectionStackingView-sectionDelete" onClick={this.deleteColumn}> <FontAwesomeIcon icon="trash" size="lg" /> </button> - {/* {evContents === noValueHeader ? null : ( - <div className="collectionStackingView-sectionOptions"> - <Flyout anchorPoint={anchorPoints.TOP_RIGHT} content={this.renderMenu()}> - <button className="collectionStackingView-sectionOptionButton"> - <FontAwesomeIcon icon="ellipsis-v" size="lg"></FontAwesomeIcon> - </button> - </Flyout> - </div> - )} */} </div> <div className={'collectionStackingView-collapseBar' + (this._props.headingObject.collapsed === true ? ' active' : '')} @@ -371,14 +360,20 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< <> {this._props.Document._columnsHideIfEmpty ? null : headingView} {this.collapsed ? null : ( - <div> + <div + style={{ + margin: 'auto', + marginTop: this._props.dontCenter.includes('y') ? undefined : 'auto', + marginBottom: this._props.dontCenter.includes('y') ? undefined : 'auto', + width: this._props.columnWidth / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1), + }}> <div key={`${heading}-stack`} className="collectionStackingView-masonrySingle" style={{ padding: `${columnYMargin}px ${0}px ${this._props.yMargin}px ${0}px`, - margin: 'auto', - width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, + margin: this._props.dontCenter.includes('x') ? undefined : 'auto', + // width: 'max-content', // singleColumn ? undefined : `${cols * (style.columnWidth + style.gridGap) + 2 * style.xMargin - style.gridGap}px`, height: 'max-content', position: 'relative', gridGap: this._props.gridGap, @@ -422,11 +417,11 @@ export class CollectionStackingViewFieldColumn extends ObservableReactComponent< const uniqueHeadings = headings.map((i, idx) => headings.indexOf(i) === idx); return ( <div - className={'collectionStackingViewFieldColumn' + (SnappingManager.IsDragging ? 'Dragging' : '')} + className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / (uniqueHeadings.length + (this._props.chromeHidden ? 0 : 1) || 1)}%`, - height: undefined, // DraggingManager.GetIsDragging() ? "100%" : undefined, + height: undefined, background: this._background, }} ref={this.createColumnDropRef} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index e250d7a90..5d32482c3 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -22,7 +22,7 @@ import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { ImageUtils } from '../../util/Import & Export/ImageUtils'; import { SnappingManager } from '../../util/SnappingManager'; -import { UndoManager, undoBatch } from '../../util/UndoManager'; +import { UndoManager } from '../../util/UndoManager'; import { ViewBoxBaseComponent } from '../DocComponent'; import { FieldViewProps } from '../nodes/FieldView'; import { DocumentView } from '../nodes/DocumentView'; @@ -45,6 +45,7 @@ export interface CollectionViewProps extends React.PropsWithChildren<FieldViewPr childLayoutTemplate?: () => Doc | undefined; // specify a layout Doc template to use for children of the collection childHideDecorationTitle?: boolean; childHideResizeHandles?: boolean; + childHideDecorations?: boolean; childDragAction?: dropActionType; childXPadding?: number; childYPadding?: number; @@ -67,7 +68,7 @@ export function CollectionSubView<X>() { private gestureDisposer?: GestureUtils.GestureEventDisposer; protected _mainCont?: HTMLDivElement; - constructor(props: any) { + constructor(props: X & SubCollectionViewProps) { super(props); makeObservable(this); } @@ -121,6 +122,9 @@ export function CollectionSubView<X>() { ); return validPairs.map(({ data, layout }) => ({ data: data as Doc, layout: layout! })); // this mapping is a bit of a hack to coerce types } + /** + * This is the raw, stored list of children on a collection. If you modify this list, the database will be updated + */ @computed get childDocList() { return Cast(this.dataField, listSpec(Doc)); } @@ -217,7 +221,6 @@ export function CollectionSubView<X>() { if (!cursors) { proto.cursors = cursors = new List<CursorField>(); } - // eslint-disable-next-line no-cond-assign if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.data.metadata.id === id)) > -1) { cursors[ind].setPosition(pos); } else { @@ -227,7 +230,6 @@ export function CollectionSubView<X>() { } } - @undoBatch // eslint-disable-next-line @typescript-eslint/no-unused-vars protected onGesture(e: Event, ge: GestureUtils.GestureEvent) {} @@ -251,7 +253,7 @@ export function CollectionSubView<X>() { protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { const { docDragData } = de.complete; - if (docDragData) { + if (docDragData && !docDragData.draggedDocuments.includes(this.Document)) { let added; const dropAction = docDragData.dropAction || docDragData.userDropAction; const targetDocments = DocListCast(this.dataDoc[this._props.fieldKey]); @@ -294,7 +296,6 @@ export function CollectionSubView<X>() { return false; } - @undoBatch protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: (docs: Doc[]) => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl @@ -386,7 +387,7 @@ export function CollectionSubView<X>() { addDocument(htmlDoc); if (srcWeb) { const iframe = DocumentView.Selected()[0].ContentDiv?.getElementsByTagName('iframe')?.[0]; - const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any; + const focusNode = iframe?.contentDocument?.getSelection()?.focusNode; if (focusNode) { const anchor = srcWeb?.ComponentView?.getAnchor?.(true); anchor && DocUtils.MakeLink(htmlDoc, anchor, {}); @@ -465,23 +466,6 @@ export function CollectionSubView<X>() { if (item.kind === 'file') { const file = item.getAsFile(); file?.type && files.push(file); - - file?.type === 'application/json' && - ClientUtils.readUploadedFileAsText(file).then(result => { - const json = JSON.parse(result as string); - addDocument( - Docs.Create.TreeDocument( - json['rectangular-puzzle'].crossword.clues[0].clue.map((c: any) => { - const label = Docs.Create.LabelDocument({ title: c['#text'], _width: 120, _height: 20 }); - const proto = Doc.GetProto(label); - proto._width = 120; - proto._height = 20; - return proto; - }), - { _width: 150, _height: 600, title: 'across', backgroundColor: 'white', _createDocOnCR: true } - ) - ); - }); } } this.slowLoadDocuments(files, options, generatedDocuments, text, completed, addDocument).then(batch.end); diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 0369e4a2a..8a24db330 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -18,7 +16,7 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { FieldsDropdown } from '../FieldsDropdown'; import { PinDocView } from '../PinFuncs'; import { DocumentView } from '../nodes/DocumentView'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import './CollectionTimeView.scss'; import { ViewDefBounds, computePivotLayout, computeTimelineLayout } from './collectionFreeForm/CollectionFreeFormLayoutEngines'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; @@ -32,7 +30,7 @@ export class CollectionTimeView extends CollectionSubView() { @observable _viewDefDivClick: Opt<ScriptField> = undefined; @observable _focusPivotField: Opt<string> = undefined; - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -51,7 +49,7 @@ export class CollectionTimeView extends CollectionSubView() { getAnchor = (addAsAnnotation: boolean) => { const anchor = Docs.Create.ConfigDocument({ - title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, + 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); diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 285598600..a60cd98ac 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,10 +1,9 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DivHeight, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; -import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import ResizeObserver from 'resize-observer-polyfill'; +import { DivHeight, returnAll, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; +import { Doc, DocListCast, Opt, returnEmptyDoclist, StrListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { listSpec } from '../../../fields/Schema'; @@ -19,21 +18,20 @@ import { dropActionType } from '../../util/DropActionTypes'; import { ScriptingGlobals } from '../../util/ScriptingGlobals'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { DocumentView } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import { StyleProp } from '../StyleProp'; +import { returnEmptyDocViewList } from '../StyleProvider'; import { CollectionFreeFormView } from './collectionFreeForm'; -import { CollectionSubView } from './CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import './CollectionTreeView.scss'; import { TreeViewType } from './CollectionTreeViewType'; import { TreeView } from './TreeView'; -const _global = (window /* browser */ || global) /* node */ as any; - export type collectionTreeViewProps = { treeViewExpandedView?: 'fields' | 'layout' | 'links' | 'data'; treeViewOpen?: boolean; @@ -55,10 +53,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree private _titleRef?: HTMLDivElement | HTMLInputElement | null; private _disposers: { [name: string]: IReactionDisposer } = {}; private _isDisposing = false; // notes that instance is in process of being disposed - private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes - private observer: any; // observer for monitoring tree view items. + private refList: Set<HTMLElement> = new Set(); // list of tree view items to monitor for height changes + private observer: ResizeObserver | undefined; // observer for monitoring tree view items. - constructor(props: any) { + constructor(props: SubCollectionViewProps & collectionTreeViewProps) { super(props); makeObservable(this); } @@ -113,14 +111,14 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree !this._props.dontRegisterView && this._props.setHeight?.(bodyHeight + titleHeight); } }; - unobserveHeight = (ref: any) => { + unobserveHeight = (ref: HTMLElement) => { this.refList.delete(ref); this.layoutDoc.layout_autoHeight && this.computeHeight(); }; - observeHeight = (ref: any) => { + observeHeight = (ref: HTMLElement) => { if (ref) { this.refList.add(ref); - this.observer = new _global.ResizeObserver(() => { + this.observer = new ResizeObserver(() => { if (this.layoutDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.IsDragging) { this.computeHeight(); } @@ -216,7 +214,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' }); if (!Doc.noviceMode) { const existingOnClick = ContextMenu.Instance.findByDescription('OnClick...'); - const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; + const onClicks: ContextMenuProps[] = existingOnClick?.subitems ?? []; onClicks.push({ description: 'Edit onChecked Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onCheckedClick'), 'edit onCheckedClick'), icon: 'edit' }); !existingOnClick && ContextMenu.Instance.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); } @@ -229,16 +227,16 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree get editableTitle() { return ( <EditableView - contents={this.dataDoc.title} + contents={StrCast(this.dataDoc.title)} display="block" maxHeight={72} height="auto" GetValue={() => StrCast(this.dataDoc.title)} - SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => { + SetValue={undoable((value: string, shift: boolean, enter: boolean) => { if (enter && this.Document.treeView_Type === TreeViewType.outline) this.makeTextCollection(this.treeChildren); this.dataDoc.title = value; return true; - })} + }, 'set doc title')} /> ); } @@ -285,7 +283,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _renderCount = 1; @computed get treeViewElements() { TraceMobx(); - const dragAction = StrCast(this.Document.childDragAction) as any as dropActionType; + const dragAction = StrCast(this.Document.childDragAction) as dropActionType; const treeAddDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before); const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this._props.moveDocument?.(d, target, addDoc) || false; if (this._renderCount < this.treeChildren.length) @@ -333,9 +331,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return this.dataDoc === null ? null : ( <div className="collectionTreeView-titleBar" - ref={action((r: any) => { - (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale); - })} + ref={r => + runInAction(() => { + (this._titleRef = r) && (this._titleHeight = r.getBoundingClientRect().height * this.ScreenToLocalBoxXf().Scale); + }) + } key={this.Document[Id]} style={!this.outlineMode ? { marginLeft: this.marginX(), paddingTop: this.marginTop() } : {}}> {this.outlineMode ? this.documentTitle : this.editableTitle} @@ -370,7 +370,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree renderDepth={this._props.renderDepth + 1} focus={emptyFunction} styleProvider={this._props.styleProvider} - containerViewPath={returnEmptyDoclist} + containerViewPath={returnEmptyDocViewList} whenChildContentsActiveChanged={emptyFunction} childFilters={this._props.childFilters} childFiltersByRanges={this._props.childFiltersByRanges} @@ -410,8 +410,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @observable _headerHeight = 0; @computed get content() { - const background = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor); - const color = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.Color); + const background = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; + const color = () => this._props.styleProvider?.(this.Document, this._props, StyleProp.Color) as string; const pointerEvents = () => (this._props.isContentActive() === false ? 'none' : undefined); const titleBar = this._props.treeViewHideTitle || this.Document.treeView_HideTitle ? null : this.titleBar; return ( diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index 5c304b4a9..c9e934448 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -17,6 +17,7 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView } from '../nodes/FieldView'; import { OpenWhere } from '../nodes/OpenWhere'; import { CollectionCalendarView } from './CollectionCalendarView'; +import { CollectionCardView } from './CollectionCardDeckView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; import { CollectionDockingView } from './CollectionDockingView'; @@ -33,7 +34,7 @@ import { CollectionLinearView } from './collectionLinear'; import { CollectionMulticolumnView } from './collectionMulticolumn/CollectionMulticolumnView'; import { CollectionMultirowView } from './collectionMulticolumn/CollectionMultirowView'; import { CollectionSchemaView } from './collectionSchema/CollectionSchemaView'; -import { CollectionCardView } from './CollectionCardDeckView'; +import { CalendarBox } from '../nodes/calendarBox/CalendarBox'; @observer export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewProps>() { @@ -48,7 +49,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr private reactionDisposer: IReactionDisposer | undefined; @observable _isContentActive: boolean | undefined = undefined; - constructor(props: any) { + constructor(props: CollectionViewProps) { super(props); makeObservable(this); this._annotationKeySuffix = returnEmptyString; @@ -72,7 +73,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr } get collectionViewType(): CollectionViewType | undefined { - const viewField = StrCast(this.layoutDoc._type_collection) as any as CollectionViewType; + const viewField = StrCast(this.layoutDoc._type_collection) as CollectionViewType; if (CollectionView._safeMode) { switch (viewField) { case CollectionViewType.Freeform: @@ -91,7 +92,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr if (type === undefined) return null; switch (type) { case CollectionViewType.Schema: return <CollectionSchemaView key="collview" {...props} />; - case CollectionViewType.Calendar: return <CollectionCalendarView key="collview" {...props} />; + case CollectionViewType.Calendar: return <CalendarBox key="collview" {...props} />; case CollectionViewType.Docking: return <CollectionDockingView key="collview" {...props} />; case CollectionViewType.Tree: return <CollectionTreeView key="collview" {...props} />; case CollectionViewType.Multicolumn: return <CollectionMulticolumnView key="collview" {...props} />; @@ -126,13 +127,14 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr { description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' }, { description: 'Carousel', event: () => func(CollectionViewType.Carousel), icon: 'columns' }, { description: '3D Carousel', event: () => func(CollectionViewType.Carousel3D), icon: 'columns' }, + { description: 'Calendar', event: () => func(CollectionViewType.Calendar), icon: 'columns' }, { description: 'Pivot/Time', event: () => func(CollectionViewType.Time), icon: 'columns' }, { description: 'Map', event: () => func(CollectionViewType.Map), icon: 'globe-americas' }, { description: 'Grid', event: () => func(CollectionViewType.Grid), icon: 'th-list' }, ]; const existingVm = ContextMenu.Instance.findByDescription(category); - const catItems = existingVm && 'subitems' in existingVm ? existingVm.subitems : []; + const catItems = existingVm?.subitems ?? []; catItems.push({ description: 'Add a Perspective...', addDivider: true, noexpand: true, subitems: subItems, icon: 'eye' }); !existingVm && ContextMenu.Instance.addItem({ description: category, subitems: catItems, icon: 'eye' }); } @@ -151,7 +153,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr }); const options = cm.findByDescription('Options...'); - const optionItems = options && 'subitems' in options ? options.subitems : []; + const optionItems = options?.subitems ?? []; !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.Document.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => {this.Document.forceActive = !this.Document.forceActive}, icon: 'project-diagram' }) : null; // prettier-ignore if (this.Document.childLayout instanceof Doc) { optionItems.push({ description: 'View Child Layout', event: () => this._props.addDocTab(this.Document.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' }); @@ -165,7 +167,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr if (!Doc.noviceMode && !this.Document.annotationOn && !this._props.hideClickBehaviors) { const existingOnClick = cm.findByDescription('OnClick...'); - const onClicks = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; + const onClicks = existingOnClick?.subitems ?? []; const funcs = [ { key: 'onChildClick', name: 'On Child Clicked' }, { key: 'onChildDoubleClick', name: 'On Child Double Clicked' }, @@ -195,7 +197,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<CollectionViewPr if (!Doc.noviceMode) { const more = cm.findByDescription('More...'); - const moreItems = more && 'subitems' in more ? more.subitems : []; + const moreItems = more?.subitems ?? []; moreItems.push({ description: 'Export Image Hierarchy', icon: 'columns', event: () => ImageUtils.ExportHierarchyToFileSystem(this.Document) }); !more && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'hand-point-right' }); } diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 46f61290e..f56ea9d76 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -6,15 +6,16 @@ import { IReactionDisposer, ObservableSet, action, computed, makeObservable, obs import { observer } from 'mobx-react'; import * as React from 'react'; import * as ReactDOM from 'react-dom/client'; -import { ClientUtils, DashColor, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; +import ResizeObserver from 'resize-observer-polyfill'; +import { ClientUtils, DashColor, lightOrDark, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; -import { Doc, Opt } from '../../../fields/Doc'; +import { Doc, Opt, returnEmptyDoclist } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, DocCast, NumCast, StrCast, toList } from '../../../fields/Types'; +import { Cast, NumCast, StrCast, toList } from '../../../fields/Types'; import { DocServer } from '../../DocServer'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -41,8 +42,6 @@ import { CollectionView } from './CollectionView'; import './TabDocView.scss'; import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView'; -const _global = (window /* browser */ || global) /* node */ as any; - interface TabMinimapViewProps { document: Doc; tabView: () => DocumentView | undefined; @@ -58,6 +57,7 @@ interface TabMiniThumbProps { miniLeft: () => number; } +export type TabHTMLElement = HTMLDivElement & { InitTab?: (tab: object) => void }; @observer class TabMiniThumb extends React.Component<TabMiniThumbProps> { render() { @@ -67,7 +67,7 @@ class TabMiniThumb extends React.Component<TabMiniThumbProps> { } @observer export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps> { - static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string): any => { + static miniStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => { if (doc) { switch (property.split(':')[0]) { case StyleProp.PointerEvents: return 'none'; @@ -158,8 +158,8 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps addDocTab={this._props.addDocTab} // eslint-disable-next-line no-use-before-define pinToPres={TabDocView.PinDoc} - childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter} searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} fitContentsToBox={returnTrue} xPadding={this.xPadding} @@ -183,6 +183,7 @@ export class TabMinimapView extends ObservableReactComponent<TabMinimapViewProps interface TabDocViewProps { documentId: FieldId; keyValue?: boolean; + // eslint-disable-next-line @typescript-eslint/no-explicit-any glContainer: any; } @observer @@ -193,8 +194,9 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { .filter(tv => tv._document) .map(tv => tv._document!); } - _mainCont: HTMLDivElement | null = null; + _mainCont: TabHTMLElement | null = null; _tabReaction: IReactionDisposer | undefined; + _lastSelection = 0; // time when view was last selected - used to re-select views that get invalidated when selected /** * Adds a document to the presentation view @@ -273,20 +275,25 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs } + // Flag indicating that when a tab is activated, it should not select it's document. + // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected) + public static DontSelectOnActivate = 'dontSelectOnActivate'; + + public static IsSelected = (doc?: Doc) => { + return DocumentView.getViews(doc).some(dv => dv?.IsSelected); + }; + static Activate = (tabDoc: Doc) => { - const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue); + const tab = Array.from(CollectionDockingView.Instance?.tabMap ?? []).find(findTab => findTab.DashDoc === tabDoc && !findTab.contentItem.config.props.keyValue); 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; }; - // static ActivateTabView(doc: Doc) { - // const tabView = Array.from(TabDocView._allTabs).find(view => view._document === doc); - // if (!tabView?._activated && tabView?._document) { - // TabDocView.Activate(tabView?._document); - // return tabView; - // } - // return undefined; - // } - constructor(props: any) { + + get stack() { return this._props.glContainer.parent.parent; } // prettier-ignore + get tab() { return this._props.glContainer.tab; } // prettier-ignore + get view() { return this._view; } // prettier-ignore + + constructor(props: TabDocViewProps) { super(props); makeObservable(this); DocumentView.activateTabView = TabDocView.Activate; @@ -299,38 +306,16 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { @observable _hovering = false; @observable _isActive: boolean = false; @observable _isAnyChildContentActive = false; - public static IsSelected = (doc?: Doc) => { - if (DocumentView.getViews(doc).some(dv => dv?.IsSelected)) { - return true; - } - return false; - }; - @computed get _isUserActivated() { - return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; - } - get _isContentActive() { - return this._isUserActivated || this._hovering; - } @observable _document: Doc | undefined = undefined; @observable _view: DocumentView | undefined = undefined; + @observable _forceInvalidateScreenToLocal = 0; // screentolocal is computed outside of react using a dom resize ovbserver. this hack allows the resize observer to trigger a react update - @computed get layoutDoc() { - return this._document && Doc.Layout(this._document); - } - - get stack() { - return this._props.glContainer.parent.parent; - } - get tab() { - return this._props.glContainer.tab; - } - get view() { - return this._view; - } - _lastTab: any; - _lastView: DocumentView | undefined; + @computed get layoutDoc() { return this._document && Doc.Layout(this._document); } // prettier-ignore + @computed get isUserActivated() { return TabDocView.IsSelected(this._document) || this._isAnyChildContentActive; } // prettier-ignore + @computed get isContentActive() { return this.isUserActivated || this._hovering; } // prettier-ignore @action + // eslint-disable-next-line @typescript-eslint/no-explicit-any init = (tab: any, doc: Opt<Doc>) => { if (tab.contentItem === tab.header.parent.getActiveContentItem()) this._activated = true; if (tab.DashDoc !== doc && doc && tab.contentItem?.config.type !== 'stack') { @@ -357,10 +342,11 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; titleEle.onkeydown = (e: KeyboardEvent) => e.stopPropagation(); - titleEle.onchange = (e: any) => { + titleEle.onchange = (e: InputEvent) => { undoable(() => { - titleEle.size = e.currentTarget.value.length + 3; - doc[DocData].title = e.currentTarget.value; + const target = e.currentTarget as unknown as { value: string }; + titleEle.size = target?.value.length + 3; + doc[DocData].title = target?.value ?? ''; }, 'edit tab title')(); }; @@ -399,9 +385,10 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { tab._disposers.color = reaction( () => ({ variant: SnappingManager.userVariantColor, degree: Doc.GetBrushStatus(doc), highlight: DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting) }), ({ variant, degree, highlight }) => { - const color = highlight?.highlightIndex === Doc.DocBrushStatus.highlighted ? highlight.highlightColor : degree ? ['transparent', variant, variant, 'orange'][degree] : variant; + const { highlightIndex, highlightColor } = (highlight as { highlightIndex: number; highlightColor: string }) ?? { highlightIndex: undefined, highlightColor: undefined }; + const color = highlightIndex === Doc.DocBrushStatus.highlighted ? highlightColor : degree ? ['transparent', variant, variant, 'orange'][degree] : variant; - const textColor = color === variant ? SnappingManager.userColor ?? '' : lightOrDark(color); + const textColor = color === variant ? (SnappingManager.userColor ?? '') : lightOrDark(color); titleEle.style.color = textColor; iconWrap.style.color = textColor; closeWrap.style.color = textColor; @@ -409,7 +396,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { color === variant ? DashColor(color) .fade( - this._isUserActivated + this.isUserActivated ? 0 : this._hovering ? 0.25 @@ -448,8 +435,8 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { }; // select the tab document when the tab is directly clicked and activate the tab whenver the tab document is selected - titleEle.onpointerdown = action((e: any) => { - if (e.target.className !== 'lm_iconWrap') { + titleEle.onpointerdown = action((e: PointerEvent) => { + if ((e.target as HTMLElement)?.className !== 'lm_iconWrap') { if (this.view) DocumentView.SelectView(this.view, false); else this._activated = true; if (Date.now() - titleEle.lastClick < 1000) titleEle.select(); @@ -481,7 +468,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { tab.closeElement .off('click') // unbind the current click handler .click(() => { - Object.values(tab._disposers).forEach((disposer: any) => disposer?.()); + Object.values(tab._disposers).forEach(disposer => (disposer as () => void)()); DocumentView.DeselectAll(); UndoManager.RunInBatch(() => tab.contentItem.remove(), 'delete tab'); }); @@ -489,8 +476,8 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { }; componentDidMount() { - new _global.ResizeObserver( - action((entries: any) => { + new ResizeObserver( + action(entries => { // eslint-disable-next-line no-restricted-syntax for (const entry of entries) { this._panelWidth = entry.contentRect.width; @@ -518,18 +505,14 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { this._props.glContainer.layoutManager.off('activeContentItemChanged', this.onActiveContentItemChanged); } - // Flag indicating that when a tab is activated, it should not select it's document. - // this is used by the link properties menu when it wants to display the link target without selecting the target (which would make the link property window go away since it would no longer be selected) - public static DontSelectOnActivate = 'dontSelectOnActivate'; - - @action.bound - private onActiveContentItemChanged(contentItem: any) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onActiveContentItemChanged = action((contentItem: any) => { if (!contentItem || (this.stack === contentItem.parent && ((contentItem?.tab === this.tab && !this._isActive) || (contentItem?.tab !== this.tab && this._isActive)))) { this._activated = this._isActive = !contentItem || contentItem?.tab === this.tab; if (!this._view && this.tab?.contentItem?.config?.props?.panelName !== TabDocView.DontSelectOnActivate) setTimeout(() => DocumentView.SelectView(this._view, false)); !this._isActive && this._document && Doc.UnBrushDoc(this._document); // bcz: bad -- trying to simulate a pointer leave event when a new tab is opened up on top of an existing one. } - } + }); // adds a tab to the layout based on the locaiton parameter which can be: // close[:{left,right,top,bottom}] - e.g., "close" will close the tab, "close:left" will close the left tab, @@ -546,7 +529,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none; const panelName = whereFields.length > 1 ? whereFields.lastElement() : ''; if (docs[0]?.dockingConfig && !keyValue) return DashboardView.openDashboard(docs[0]); - // prettier-ignore switch (whereFields[0]) { case undefined: case OpenWhere.lightbox: return LightboxView.Instance.AddDocTab(docs[0], location); @@ -554,7 +536,7 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { case OpenWhere.replace: return CollectionDockingView.ReplaceTab(docs[0], whereMods, this.stack, panelName, undefined, keyValue); case OpenWhere.toggle: return CollectionDockingView.ToggleSplit(docs[0], whereMods, this.stack, TabDocView.DontSelectOnActivate, keyValue); case OpenWhere.add:default:return CollectionDockingView.AddSplit(docs[0], whereMods, this.stack, undefined, keyValue); - } + } // prettier-ignore }; remDocTab = (doc: Doc | Doc[]) => { if (doc === this._document) { @@ -566,8 +548,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { }; getCurrentFrame = () => NumCast(Cast(PresBox.Instance.activeItem.presentation_targetDoc, Doc, null)._currentFrame); - - @action focusFunc = () => { if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) { this.tab.header.parent.setActiveContentItem(this.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) @@ -575,7 +555,6 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { return undefined; }; active = () => this._isActive; - @observable _forceInvalidateScreenToLocal = 0; ScreenToLocalTransform = () => { this._forceInvalidateScreenToLocal; const { translateX, translateY } = ClientUtils.GetScreenTransform(this._mainCont?.children?.[0] as HTMLElement); @@ -589,47 +568,44 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { whenChildContentActiveChanges = (isActive: boolean) => { this._isAnyChildContentActive = isActive; }; - isContentActive = () => this._isContentActive; + isContentActiveFunc = () => this.isContentActive; waitForDoubleClick = () => (SnappingManager.ExploreMode ? 'never' : undefined); - @computed get docView() { - return !this._activated || !this._document ? null : ( - <> - <DocumentView - key={this._document[Id]} - ref={action((r: DocumentView) => { - this._lastView && DocumentView.removeView(this._lastView); - this._view = r; - this._lastView = this._view; - })} - renderDepth={0} - LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined} - hideTitle={this._props.keyValue} - Document={this._document} - TemplateDataDocument={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} - waitForDoubleClickToClick={this.waitForDoubleClick} - isContentActive={this.isContentActive} - isDocumentActive={returnFalse} - PanelWidth={this.PanelWidth} - PanelHeight={this.PanelHeight} - styleProvider={DefaultStyleProvider} - childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} - searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} - addDocument={undefined} - removeDocument={this.remDocTab} - addDocTab={this.addDocTab} - suppressSetHeight={!!this._document._layout_fitWidth} - ScreenToLocalTransform={this.ScreenToLocalTransform} - dontCenter="y" - whenChildContentsActiveChanged={this.whenChildContentActiveChanges} - focus={this.focusFunc} - containerViewPath={returnEmptyDoclist} - pinToPres={TabDocView.PinDoc} - /> - {this.disableMinimap() ? null : <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} />} - </> - ); - } + renderDocView = (doc: Doc) => ( + <DocumentView + key={doc[Id]} + ref={action((r: DocumentView) => { + const now = Date.now(); + this._lastSelection = this._view?.IsSelected ? now : this._lastSelection; + if (this._view) DocumentView.removeView(this._view); + this._view = r; + if (this._view && now - this._lastSelection < 1000) this._view.select(false); + })} + renderDepth={0} + LayoutTemplateString={this._props.keyValue ? KeyValueBox.LayoutString() : undefined} + hideTitle={this._props.keyValue} + Document={doc} + TemplateDataDocument={!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined} + waitForDoubleClickToClick={this.waitForDoubleClick} + isContentActive={this.isContentActiveFunc} + isDocumentActive={returnFalse} + PanelWidth={this.PanelWidth} + PanelHeight={this.PanelHeight} + styleProvider={DefaultStyleProvider} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyFilter} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyFilter} + searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} + addDocument={undefined} + removeDocument={this.remDocTab} + addDocTab={this.addDocTab} + suppressSetHeight={!!doc._layout_fitWidth} + ScreenToLocalTransform={this.ScreenToLocalTransform} + dontCenter="y" + whenChildContentsActiveChanged={this.whenChildContentActiveChanges} + focus={this.focusFunc} + containerViewPath={returnEmptyDocViewList} + pinToPres={TabDocView.PinDoc} + /> + ); render() { return ( @@ -642,23 +618,21 @@ export class TabDocView extends ObservableReactComponent<TabDocViewProps> { onPointerLeave={action(() => { this._hovering = false; })} // prettier-ignore onDragOver={action(() => { this._hovering = true; })} // prettier-ignore onDragLeave={action(() => { this._hovering = false; })} // prettier-ignore - ref={ref => { + ref={(ref: TabHTMLElement) => { + // "add" an InitTab function to this div to call from tabCreated in CollectionDockingView when div is reused this._mainCont = ref; if (this._mainCont) { - if (this._lastTab) { - this._view && DocumentView.removeView(this._view); - } - this._lastTab = this.tab; - (this._mainCont as any).InitTab = (tab: any) => this.init(tab, this._document); - DocServer.GetRefField(this._props.documentId).then( - action(doc => { - doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document); - }) - ); - new _global.ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(ref); + this._mainCont.InitTab = (tab: object) => this.init(tab, this._document); + DocServer.GetRefField(this._props.documentId).then(action(doc => { + doc instanceof Doc && (this._document = doc) && this.tab && this.init(this.tab, this._document); + })); // prettier-ignore + new ResizeObserver(action(() => this._forceInvalidateScreenToLocal++)).observe(this._mainCont); } }}> - {this.docView} + {!this._activated || !this._document ? null : this.renderDocView(this._document)} + {this.disableMinimap() || !this._document ? null : ( + <TabMinimapView key="minimap" addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> + )} </div> ); } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index f69aea2a7..d2514dfd1 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -1,15 +1,12 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { IconButton, Size } from 'browndash-components'; -import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, lightOrDark, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; +import { ClientUtils, lightOrDark, return18, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, setupMoveUpEvents, simulateMouseClick } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; -import { Doc, DocListCast, Field, FieldResult, FieldType, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldType, Opt, StrListCast, returnEmptyDoclist } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; @@ -41,14 +38,15 @@ import { CollectionView } from './CollectionView'; import { TreeSort } from './TreeSort'; import './TreeView.scss'; +// eslint-disable-next-line @typescript-eslint/no-var-requires const { TREE_BULLET_WIDTH } = require('../global/globalCssVariables.module.scss'); // prettier-ignore export interface TreeViewProps { treeView: CollectionTreeView; // eslint-disable-next-line no-use-before-define parentTreeView: TreeView | CollectionTreeView | undefined; - observeHeight: (ref: any) => void; - unobserveHeight: (ref: any) => void; + observeHeight: (ref: HTMLDivElement) => void; + unobserveHeight: (ref: HTMLDivElement) => void; prevSibling?: Doc; Document: Doc; dataDoc?: Doc; @@ -188,7 +186,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { moving: boolean = false; @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { if (this.Document !== target && addDoc !== returnFalse) { - const canAdd1 = (this._props.parentTreeView as any).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this._props.parentTreeView?.Document.data)) instanceof ComputedField); + const canAdd1 = (this._props.parentTreeView as TreeView).dropping || !(ComputedField.WithoutComputed(() => FieldValue(this._props.parentTreeView?.Document.data)) instanceof ComputedField); // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse if (canAdd1 && this._props.removeDoc?.(doc) === true) { @@ -251,7 +249,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return []; } - const runningChildren: FieldResult[] = []; + const runningChildren: Doc[] = []; childList.forEach(child => { if (child.runProcess && TreeView.GetRunningChildren.get(child)) { if (child.runProcess) { @@ -263,7 +261,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return runningChildren; }; - static GetRunningChildren = new Map<Doc, any>(); + static GetRunningChildren = new Map<Doc, () => Doc[]>(); static ToggleChildrenRun = new Map<Doc, () => void>(); constructor(props: TreeViewProps) { super(props); @@ -285,7 +283,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { TreeView.GetRunningChildren.set(this.Document, () => this.getRunningChildren(this.childDocs)); } - _treeEle: any; + _treeEle: HTMLDivElement | null = null; protected createTreeDropTarget = (ele: HTMLDivElement) => { this._treedropDisposer?.(); ele && ((this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), this.Document, this.preTreeDrop.bind(this))), this.Document); @@ -472,11 +470,11 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { refTransform = (ref: HTMLElement | undefined | null) => { if (!ref) return this.ScreenToLocalTransform(); const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref); - return new Transform(-translateX, -translateY, 1).scale(1/scale); + return new Transform(-translateX, -translateY, 1).scale(1 / scale); }; docTransform = () => this.refTransform(this._dref?.ContentDiv); getTransform = () => this.refTransform(this._tref.current); - embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1); + embeddedPanelWidth = () => this._props.panelWidth() / (this.treeView._props.NativeDimScaling?.() || 1) - 3 /* paddingRight for bullet */; embeddedPanelHeight = () => { const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.Document))(this.treeView._props.childLayoutTemplate?.()) || this.layoutDoc; return Math.min( @@ -524,7 +522,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return toList(docs).reduce((flg, iDoc) => flg && innerAdd(iDoc), true as boolean); }; contentElement = TreeView.GetChildElements( - toList(contents as any), + contents instanceof Doc ? [contents] : DocListCast(contents), this.treeView, this, doc, @@ -572,9 +570,11 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { rows.push( <div style={{ display: 'flex', overflow: 'auto' }} key={key}> <span - ref={action((r: any) => { - if (r) leftOffset.width = r.getBoundingClientRect().width; - })} + ref={r => + runInAction(() => { + if (r) leftOffset.width = r.getBoundingClientRect().width; + }) + } style={{ fontWeight: 'bold' }}> {key + ':'} @@ -608,7 +608,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return rows; } - _renderTimer: any; + _renderTimer: NodeJS.Timeout | undefined; @observable _renderCount = 1; @computed get renderContent() { TraceMobx(); @@ -754,7 +754,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { } get onCheckedClick() { - return this.Document.type === DocumentType.COL ? undefined : this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick); + return this.Document.type === DocumentType.COL ? undefined : (this._props.onCheckedClick?.() ?? ScriptCast(this.Document.onCheckedClick)); } @action @@ -777,9 +777,9 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { @computed get renderBullet() { TraceMobx(); - const iconType = this.treeView._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':treeOpen' : !this.childDocs.length ? ':empty' : '')) || 'question'; + const iconType = (this.treeView._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':treeOpen' : !this.childDocs.length ? ':empty' : '')) as string) || 'question'; const color = SettingsManager.userColor; - const checked = this.onCheckedClick ? this.Document.treeView_Checked ?? 'unchecked' : undefined; + const checked = this.onCheckedClick ? (this.Document.treeView_Checked ?? 'unchecked') : undefined; return ( <div className={`bullet${this.treeView.outlineMode ? '-outline' : ''}`} @@ -789,7 +789,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { style={ this.treeView.outlineMode ? { - opacity: this.titleStyleProvider?.(this.Document, this.treeView._props, StyleProp.Opacity), + opacity: this.titleStyleProvider?.(this.Document, this.treeView._props, StyleProp.Opacity) as number, } : { pointerEvents: this._props.isContentActive() ? 'all' : undefined, @@ -829,7 +829,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { @action expandNextviewType = () => { if (this.treeViewOpen && !this.Document.isFolder && !this.treeView.outlineMode && !this.Document.treeView_ExpandedViewLock) { - const next = (modes: any[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length]; + const next = (modes: string[]) => modes[(modes.indexOf(StrCast(this.treeViewExpandedView)) + 1) % modes.length]; this.Document.treeView_ExpandedView = next(this.validExpandViewTypes); } this.treeViewOpen = true; @@ -897,13 +897,13 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { onChildDoubleClick = () => ScriptCast(this.treeView.Document.treeView_ChildDoubleClick, !this.treeView.outlineMode ? this._openScript?.() : null); refocus = () => this.treeView._props.focus(this.treeView.Document, {}); - ignoreEvent = (e: any) => { + ignoreEvent = (e: React.MouseEvent) => { if (this._props.isContentActive(true)) { e.stopPropagation(); e.preventDefault(); } }; - titleStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => { + titleStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { if (!doc || doc !== this.Document) return this._props?.treeView?._props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView const { treeView } = this; @@ -923,7 +923,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { style={{ // just render a title for a tree view label (identified by treeViewDoc being set in 'props') maxWidth: props?.PanelWidth() || undefined, - background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor), + background: props?.styleProvider?.(doc, props, StyleProp.BackgroundColor) as string, outline: SnappingManager.IsDragging ? undefined: `solid ${highlightColor} ${highlightIndex}px`, paddingLeft: NumCast(treeView.Document.childXPadding, NumCast(treeView._props.childXPadding, Doc.IsComicStyle(doc)?20:0)), paddingRight: NumCast(treeView.Document.childXPadding, NumCast(treeView._props.childXPadding, Doc.IsComicStyle(doc)?20:0)), @@ -938,7 +938,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { } return treeView._props.styleProvider?.(doc, props, property); }; - embeddedStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string): any => { + embeddedStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { if (property.startsWith(StyleProp.Decorations)) return null; return this._props?.treeView?._props.styleProvider?.(doc, props, property); // properties are inherited from the CollectionTreeView, not the hierarchical parent in the treeView }; @@ -990,28 +990,30 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { this._editTitle = e; })} GetValue={() => StrCast(this.Document.title)} - OnTab={undoBatch((shift?: boolean) => { + OnTab={undoable((shift?: boolean) => { if (!shift) this._props.indentDocument?.(true); else this._props.outdentDocument?.(true); - })} - OnEmpty={undoBatch(() => this.treeView.outlineMode && this._props.removeDoc?.(this.Document))} + }, 'create new tree Doc')} + OnEmpty={undoable(() => this.treeView.outlineMode && this._props.removeDoc?.(this.Document), 'remove tree doc')} OnFillDown={() => this.treeView.fileSysMode && this.makeFolder()} - SetValue={undoBatch((value: string, shiftKey: boolean, enterKey: boolean) => { + SetValue={undoable((value: string, shiftKey: boolean, enterKey: boolean) => { Doc.SetInPlace(this.Document, 'title', value, false); - this.treeView.outlineMode && enterKey && this.makeTextCollection(); - })} + return this.treeView.outlineMode && enterKey && this.makeTextCollection(); + }, 'set tree doc title')} /> ) : ( <DocumentView key="title" - ref={action((r: any) => { - this._docRef = r || undefined; - if (this._docRef && TreeView._editTitleOnLoad?.id === this.Document[Id] && TreeView._editTitleOnLoad.parent === this._props.parentTreeView) { - this._docRef.select(false); - this.setEditTitle(this._docRef); - TreeView._editTitleOnLoad = undefined; - } - })} + ref={r => + runInAction(() => { + this._docRef = r || undefined; + if (this._docRef && TreeView._editTitleOnLoad?.id === this.Document[Id] && TreeView._editTitleOnLoad.parent === this._props.parentTreeView) { + this._docRef.select(false); + this.setEditTitle(this._docRef); + TreeView._editTitleOnLoad = undefined; + } + }) + } Document={this.Document} fitWidth={returnTrue} scriptContext={this} @@ -1068,9 +1070,11 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { </div> <div className="treeView-rightButtons" - ref={action((r: any) => { - r && (this.headerEleWidth = r.getBoundingClientRect().width); - })}> + ref={r => + runInAction(() => { + r && (this.headerEleWidth = r.getBoundingClientRect().width); + }) + }> {this.titleButtons} </div> </> @@ -1090,7 +1094,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { this, e, () => { - (this._dref ?? this._docRef)?.startDragging(e.clientX, e.clientY, '' as any); + (this._dref ?? this._docRef)?.startDragging(e.clientX, e.clientY, undefined); return true; }, returnFalse, @@ -1179,7 +1183,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { @computed get renderBorder() { const sorting = StrCast(this.Document.treeView_SortCriterion, TreeSort.WhenAdded); - const sortings = (this._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewSortings) ?? {}) as { [key: string]: { color: string; label: string } }; + const sortings = (this._props.styleProvider?.(this.Document, this.treeView._props, StyleProp.TreeViewSortings) ?? {}) as { [key: string]: { color: string; icon: JSX.Element } }; return ( <div className={`treeView-border${this.treeView.outlineMode ? TreeViewType.outline : ''}`} style={{ borderColor: sortings[sorting]?.color }}> {!this.treeViewOpen ? null : this.renderContent} @@ -1272,8 +1276,8 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { firstLevel: boolean, whenChildContentsActiveChanged: (isActive: boolean) => void, dontRegisterView: boolean | undefined, - observerHeight: (ref: any) => void, - unobserveHeight: (ref: any) => void, + observerHeight: (ref: HTMLElement) => void, + unobserveHeight: (ref: HTMLElement) => void, contextMenuItems: { script: ScriptField; filter: ScriptField; label: string; icon: string }[], // TODO: [AL] add these AddToMap?: (treeViewDoc: Doc, index: number[]) => void, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx index fc39cafaa..c17371151 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx @@ -12,7 +12,7 @@ import './CollectionFreeFormView.scss'; * returns a truthy value */ // eslint-disable-next-line no-use-before-define -export type infoArc = [() => any, (res?: any) => infoState]; +export type infoArc = [() => unknown, (res?: unknown) => infoState]; export const StateMessage = Symbol('StateMessage'); export const StateMessageGIF = Symbol('StateMessageGIF'); @@ -20,9 +20,9 @@ export const StateEntryFunc = Symbol('StateEntryFunc'); export class infoState { [StateMessage]: string = ''; [StateMessageGIF]?: string = ''; - [StateEntryFunc]?: () => any; + [StateEntryFunc]?: () => unknown; [key: string]: infoArc; - constructor(message: string, arcs: { [key: string]: infoArc }, messageGif?: string, entryFunc?: () => any) { + constructor(message: string, arcs: { [key: string]: infoArc }, messageGif?: string, entryFunc?: () => unknown) { this[StateMessage] = message; Object.assign(this, arcs); this[StateMessageGIF] = messageGif; @@ -44,7 +44,7 @@ export function InfoState( msg: string, // arcs: { [key: string]: infoArc }, gif?: string, - entryFunc?: () => any + entryFunc?: () => unknown ) { // eslint-disable-next-line new-cap return new infoState(msg, arcs, gif, entryFunc); @@ -52,7 +52,7 @@ export function InfoState( export interface CollectionFreeFormInfoStateProps { infoState: infoState; - next: (state: infoState) => any; + next: (state: infoState) => unknown; close: () => void; } @@ -61,7 +61,7 @@ export class CollectionFreeFormInfoState extends ObservableReactComponent<Collec _disposers: IReactionDisposer[] = []; @observable _expanded = false; - constructor(props: any) { + constructor(props: CollectionFreeFormInfoStateProps) { super(props); makeObservable(this); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index de51cc73c..79aad0ef2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -9,7 +9,7 @@ import { aggregateBounds } from '../../../../Utils'; export interface ViewDefBounds { type: string; - payload: any; + payload: unknown; x: number; y: number; z?: number; @@ -72,11 +72,15 @@ function toLabel(target: FieldResult<FieldType>) { */ function getTextWidth(text: string, font: string): number { // re-use canvas object for better performance - const canvas = (getTextWidth as any).canvas || ((getTextWidth as any).canvas = document.createElement('canvas')); + const selfStoreHack = getTextWidth as unknown as { canvas: Element }; + const canvas = (selfStoreHack.canvas = (selfStoreHack.canvas as unknown as HTMLCanvasElement) ?? document.createElement('canvas')); const context = canvas.getContext('2d'); - context.font = font; - const metrics = context.measureText(text); - return metrics.width; + if (context) { + context.font = font; + const metrics = context.measureText(text); + return metrics.width; + } + return 0; } interface PivotColumn { @@ -131,13 +135,13 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc return normalizeResults(burstDiam, 12, docMap, poolData, viewDefsToJSX, [], 0, [divider]); } -export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) { +export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: unknown) { const docMap = new Map<string, PoolData>(); const fieldKey = 'data'; const pivotColumnGroups = new Map<FieldResult<FieldType>, PivotColumn>(); let nonNumbers = 0; - const pivotFieldKey = toLabel(engineProps?.pivotField ?? pivotDoc._pivotField) || 'author'; + const pivotFieldKey = toLabel((engineProps as { pivotField?: string })?.pivotField ?? pivotDoc._pivotField) || 'author'; childPairs.forEach(pair => { const listValue = Cast(pair.layout[pivotFieldKey], listSpec('string'), null); @@ -265,7 +269,7 @@ export function computePivotLayout(poolData: Map<string, PoolData>, pivotDoc: Do y: -maxColHeight + pivotAxisWidth, width: pivotAxisWidth * numCols * expander, height: maxColHeight, - payload: pivotColumnGroups.get(key)!.filters, + payload: pivotColumnGroups.get(key)?.filters, })); groupNames.push(...dividers); // eslint-disable-next-line no-use-before-define diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx index e543b4008..bc9dd022c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormPannableContents.tsx @@ -54,8 +54,8 @@ export class CollectionFreeFormPannableContents extends ObservableReactComponent <div className={'collectionfreeformview' + (this._props.viewDefDivClick ? '-viewDef' : '-none')} onScroll={e => { - const target = e.target as any; - if (getComputedStyle(target)?.overflow === 'visible') { + const { target } = e; + if (target instanceof Element && getComputedStyle(target)?.overflow === 'visible') { target.scrollTop = target.scrollLeft = 0; // if collection is visible, scrolling messes things up since there are no scroll bars } }} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 5b7f09be3..f106eba26 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,16 +1,13 @@ -/* eslint-disable react/jsx-props-no-spreading */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { Bezier } from 'bezier-js'; import { Colors } from 'browndash-components'; +import { Property } from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Field, FieldType, Opt } from '../../../../fields/Doc'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetActiveInkColor, SetActiveInkWidth } from '../../nodes/DocumentView'; +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'; @@ -33,20 +30,20 @@ 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 { ActiveFillColor, DocumentView } from '../../nodes/DocumentView'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveEraserWidth, ActiveFillColor, 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 { PinDocView, PinProps } from '../../PinFuncs'; import { StyleProp } from '../../StyleProp'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeViewType'; import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid'; import { CollectionFreeFormClusters } from './CollectionFreeFormClusters'; @@ -71,7 +68,7 @@ export interface collectionFreeformViewProps { childPointerEvents?: () => string | undefined; viewField?: string; noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) - engineProps?: any; + engineProps?: unknown; getScrollHeight?: () => number | undefined; } @@ -83,7 +80,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection public unprocessedDocs: Doc[] = []; public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>(); public static from(dv?: DocumentView): CollectionFreeFormView | undefined { - const parent = CollectionFreeFormDocumentView.from(dv)?._props.parent; + const parent = CollectionFreeFormDocumentView.from(dv)?._props.reactParent; return parent instanceof CollectionFreeFormView ? parent : undefined; } @@ -123,14 +120,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _marqueeViewRef = React.createRef<MarqueeView>(); @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined = undefined; // highlighted region of freeform canvas used by presentations to indicate a region @observable GroupChildDrag: boolean = false; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. - @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined = undefined; + @observable _childPointerEvents: Property.PointerEvents | undefined = undefined; @observable _lightboxDoc: Opt<Doc> = undefined; @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, ''); @observable _keyframeEditing = false; @observable _eraserX: number = 0; @observable _eraserY: number = 0; @observable _showEraserCircle: boolean = false; // to determine whether the radius eraser should show - constructor(props: collectionFreeformViewProps) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -140,12 +137,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @computed get childPointerEvents() { return SnappingManager.IsResizing ? 'none' - : this._props.childPointerEvents?.() ?? + : (this._props.childPointerEvents?.() ?? (this._props.viewDefDivClick || // (this.layoutEngine === computePassLayout.name && !this._props.isSelected()) || this.isContentActive() === false ? 'none' - : this._props.pointerEvents?.()); + : this._props.pointerEvents?.())); } @computed get contentViews() { const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele); @@ -185,7 +182,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .transform(this.panZoomXf); } @computed get backgroundColor() { - return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor); + return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) as string; } @computed get fitWidth() { return this._props.fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth; @@ -357,7 +354,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * @param options * @returns */ - focus = (anchor: Doc, options: FocusViewOptions): any => { + focus = (anchor: Doc, options: FocusViewOptions) => { if (anchor.isGroup && !options.docTransform && options.contextPath?.length) { // don't focus on group if there's a context path because we're about to focus on a group item // which will override any group focus. (If we allowed the group to focus, it would mark didMove even if there were no net movement) @@ -374,14 +371,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !DocumentView.LightboxDoc()); - const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale ?? 0.75 : undefined); + const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? (options?.zoomScale ?? 0.75) : undefined); // focus on the document in the collection const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale); if (didMove) options.didMove = true; // glr: freeform transform speed can be set by adjusting presentation_transition field - needs a way of knowing when presentation is not active... if (didMove) { - const focusTime = options?.instant ? 0 : options.zoomTime ?? 500; + const focusTime = options?.instant ? 0 : (options.zoomTime ?? 500); (options.zoomScale ?? options.willZoomCentered) && scale && (this.Document[this.scaleFieldKey] = scale); this.setPan(panX, panY, focusTime); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow return focusTime; @@ -443,8 +440,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return true; } - @undoBatch - internalAnchorAnnoDrop(e: Event, de: DragManager.DropEvent, annoDragData: DragManager.AnchorAnnoDragData) { + internalAnchorAnnoDrop = undoable((e: Event, de: DragManager.DropEvent, annoDragData: DragManager.AnchorAnnoDragData) => { const dropCreator = annoDragData.dropDocCreator; const [xp, yp] = this.screenToFreeformContentsXf.transformPoint(de.x, de.y); annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { @@ -457,10 +453,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return dropDoc || this.Document; }; return true; - } + }, 'anchor drop'); - @undoBatch - internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData) { + internalLinkDrop = undoable((e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData) => { if (this.DocumentView?.() && linkDragData.linkDragView.containerViewPath?.().includes(this.DocumentView())) { const [x, y] = this.screenToFreeformContentsXf.transformPoint(de.x, de.y); // do nothing if link is dropped into any freeform view parent of dragged document @@ -476,9 +471,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return added; } return false; - } + }, 'link drop'); - onInternalDrop = (e: Event, de: DragManager.DropEvent) => { + onInternalDrop = (e: Event, de: DragManager.DropEvent): boolean => { if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData); if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); @@ -524,8 +519,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; - @undoBatch - onGesture = (e: Event, ge: GestureUtils.GestureEvent) => { + onGesture = undoable((e: Event, ge: GestureUtils.GestureEvent) => { switch (ge.gesture) { case Gestures.Text: if (ge.text) { @@ -568,7 +562,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); } } - }; + }, 'gesture'); @action onEraserUp = (): void => { this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document)); @@ -1178,6 +1172,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection // call in a test for linearity bintersects = (curve: Bezier, otherCurve: Bezier) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((curve as any)._linear) { // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] }); @@ -1187,6 +1182,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return intT ? [intT] : []; } } + // eslint-disable-next-line @typescript-eslint/no-explicit-any if ((otherCurve as any)._linear) { return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] }); } @@ -1214,7 +1210,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (let j = 0; j < otherCtrlPts.length - 3; j += 4) { const neighboringSegment = i === j || i === j - 4 || i === j + 4; // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. - // eslint-disable-next-line no-continue if (ink?.Document === otherInk.Document && neighboringSegment) continue; const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y }))); @@ -1478,17 +1473,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return ret; }; childPointerEventsFunc = () => this._childPointerEvents; - childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); + childContentsActive = () => ((this._props.childContentsActive ?? this.isContentActive() === false) ? returnFalse : emptyFunction)(); getChildDocView(entry: PoolData) { const childLayout = entry.pair.layout; const childData = entry.pair.data; return ( <CollectionFreeFormDocumentView - // eslint-disable-next-line react/jsx-props-no-spreading - {...OmitKeys(entry, ['replica', 'pair']).omit} + {...(OmitKeys(entry, ['replica', 'pair']).omit as { x: number; y: number; z: number; width: number; height: number })} key={childLayout[Id] + (entry.replica || '')} Document={childLayout} - parent={this} + reactParent={this} containerViewPath={this.DocumentView?.().docViewPath} styleProvider={this._clusters.styleProvider} TemplateDataDocument={childData} @@ -1603,7 +1597,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } - onViewDefDivClick = (e: React.MouseEvent, payload: any) => { + onViewDefDivClick = (e: React.MouseEvent, payload: unknown) => { (this._props.viewDefDivClick || ScriptCast(this.Document.onViewDefDivClick))?.script.run({ this: this.Document, payload }); e.stopPropagation(); }; @@ -1637,7 +1631,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ele: ( <div className="collectionFreeform-customDiv" - title={viewDef.payload?.join(' ')} + title={StrListCast(viewDef.payload as string).join(' ')} key={'div' + x + y + z + viewDef.payload} onClick={e => this.onViewDefDivClick(e, viewDef)} style={{ width, height, backgroundColor: color, transform }} @@ -1658,7 +1652,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection doEngineLayout( poolData: Map<string, PoolData>, - engine: (poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) => ViewDefResult[] + engine: (poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: unknown) => ViewDefResult[] ) { return engine(poolData, this.Document, this.childLayoutPairs, [this._props.PanelWidth(), this._props.PanelHeight()], this.viewDefsToJSX, this._props.engineProps); } @@ -1688,7 +1682,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .forEach(entry => elements.push({ ele: this.getChildDocView(entry[1]), - bounds: (entry[1].opacity === 0 ? { payload:undefined, type:"", ...entry[1], width: 0, height: 0 } : { payload:undefined, type:"",...entry[1] }), + bounds: entry[1].opacity === 0 ? { payload: undefined, type: '', ...entry[1], width: 0, height: 0 } : { payload: undefined, type: '', ...entry[1] }, inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, }) ); @@ -1771,7 +1765,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.pointerevents = reaction( () => this.childPointerEvents, pointerevents => { - this._childPointerEvents = pointerevents as any; + this._childPointerEvents = pointerevents as Property.PointerEvents | undefined; }, { fireImmediately: true } ); @@ -1789,7 +1783,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (!code.includes('dashDiv')) { const script = CompileScript(code, { params: { docView: 'any' }, typecheck: false, editable: true }); if (script.compiled) script.run({ this: this.DocumentView?.() }); - // eslint-disable-next-line no-eval } else code && !first && eval?.(code); }, { fireImmediately: true } @@ -1812,24 +1805,25 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection updateIcon = () => { const contentDiv = this.DocumentView?.().ContentDiv; - contentDiv && UpdateIcon( - this.layoutDoc[Id] + '-icon' + new Date().getTime(), - contentDiv, - NumCast(this.layoutDoc._width), - NumCast(this.layoutDoc._height), - this._props.PanelWidth(), - this._props.PanelHeight(), - 0, - 1, - false, - '', - (iconFile, nativeWidth, nativeHeight) => { - this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc.icon_nativeWidth = nativeWidth; - this.dataDoc.icon_nativeHeight = nativeHeight; - } - ); - } + contentDiv && + UpdateIcon( + this.layoutDoc[Id] + '_icon_' + new Date().getTime(), + contentDiv, + NumCast(this.layoutDoc._width), + NumCast(this.layoutDoc._height), + this._props.PanelWidth(), + this._props.PanelHeight(), + 0, + 1, + false, + '', + (iconFile, nativeWidth, nativeHeight) => { + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; + } + ); + }; @action onCursorMove = (e: React.PointerEvent) => { @@ -1848,8 +1842,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._showEraserCircle = true; }; - @undoBatch - promoteCollection = () => { + promoteCollection = undoable(() => { const childDocs = this.childDocs.slice(); childDocs.forEach(docIn => { const doc = docIn; @@ -1858,10 +1851,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection doc.y = scr?.[1]; }); this._props.addDocTab(childDocs, OpenWhere.inParentFromScreen); - }; + }, 'promote collection'); - @undoBatch - layoutDocsInGrid = () => { + layoutDocsInGrid = undoable(() => { const docs = this.childLayoutPairs.map(pair => pair.layout); const width = Math.max(...docs.map(doc => NumCast(doc._width))) + 20; const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20; @@ -1871,40 +1863,37 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2; doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2; }); - }; + }, 'layout docs in grid'); - @undoBatch - toggleNativeDimensions = () => Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight); + toggleNativeDimensions = undoable(() => Doc.toggleNativeDimensions(this.layoutDoc, 1, this.nativeWidth, this.nativeHeight), 'toggle native dimensions'); /// /// resetView restores a freeform collection to unit scale and centered at (0,0) UNLESS /// the view is a group, in which case this does nothing (since Groups calculate their own scale and center) /// - @undoBatch - resetView = () => { + resetView = undoable(() => { this.layoutDoc[this.panXFieldKey] = NumCast(this.dataDoc[this.panXFieldKey + '_reset']); this.layoutDoc[this.panYFieldKey] = NumCast(this.dataDoc[this.panYFieldKey + '_reset']); this.layoutDoc[this.scaleFieldKey] = NumCast(this.dataDoc[this.scaleFieldKey + '_reset'], 1); - }; + }, 'reset view'); /// /// resetView restores a freeform collection to unit scale and centered at (0,0) UNLESS /// the view is a group, in which case this does nothing (since Groups calculate their own scale and center) /// - @undoBatch - toggleResetView = () => { + toggleResetView = undoable(() => { this.dataDoc[this.autoResetFieldKey] = !this.dataDoc[this.autoResetFieldKey]; if (this.dataDoc[this.autoResetFieldKey]) { this.dataDoc[this.panXFieldKey + '_reset'] = this.layoutDoc[this.panXFieldKey]; this.dataDoc[this.panYFieldKey + '_reset'] = this.layoutDoc[this.panYFieldKey]; this.dataDoc[this.scaleFieldKey + '_reset'] = this.layoutDoc[this.scaleFieldKey]; } - }; + }, 'toggle reset view'); onContextMenu = () => { if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription('Appearance...'); - const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; + const appearanceItems = appearance?.subitems ?? []; !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' }); !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' }); if (this._props.setContentViewBox === emptyFunction) { @@ -1931,7 +1920,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = ContextMenu.Instance.findByDescription('Options...'); - const optionItems = options && 'subitems' in options ? options.subitems : []; + const optionItems = options?.subitems ?? []; !this._props.isAnnotationOverlay && !Doc.noviceMode && optionItems.push({ @@ -1955,12 +1944,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const mores = ContextMenu.Instance.findByDescription('More...'); - const moreItems = mores && 'subitems' in mores ? mores.subitems : []; + const moreItems = mores?.subitems ?? []; !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' }); }; - @undoBatch - transcribeStrokes = () => { + transcribeStrokes = undoable(() => { if (this.Document.isGroup && this.Document.transcription) { const text = StrCast(this.Document.transcription); const lines = text.split('\n'); @@ -1968,7 +1956,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.addDocument(Docs.Create.TextDocument(text, { title: lines[0], x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) + 20, y: NumCast(this.layoutDoc.y), _width: 200, _height: height })); } - }; + }, 'transcribe strokes'); @action dragEnding = () => { @@ -2134,7 +2122,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : (this._props.pointerEvents?.() as any), + pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(), textAlign: this.isAnnotationOverlay ? 'initial' : undefined, transform: `scale(${this.nativeDimScaling})`, width: `${100 / this.nativeDimScaling}%`, diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss new file mode 100644 index 000000000..0a001d84c --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.scss @@ -0,0 +1,104 @@ +.face-document-item { + display: flex; + height: max-content; + flex-direction: column; + top: 0; + position: absolute; + width: 100%; + height: 100%; + + h1 { + color: white; + font-size: 24px; + text-align: center; + .face-document-name { + text-align: center; + background: transparent; + width: 80%; + border: transparent; + } + } + + .face-collection-buttons { + position: absolute; + top: 0px; + right: 10px; + } + .face-collection-toggle { + position: absolute; + top: 0px; + left: 10px; + } + .face-document-top { + position: relative; + top: 0; + width: 100%; + left: 0; + } + + .face-document-image-container { + display: flex; + justify-content: center; + flex-wrap: wrap; + overflow-x: hidden; + overflow-y: auto; + position: relative; + padding: 10px; + + .image-wrapper { + position: relative; + width: 70px; + height: 70px; + margin: 10px; + display: flex; + align-items: center; // Center vertically + justify-content: center; // Center horizontally + + img { + width: 100%; + height: 100%; + object-fit: cover; // This ensures the image covers the container without stretching + border-radius: 5px; + border: 2px solid white; + transition: border-color 0.4s; + + &:hover { + border-color: orange; // Change this to your desired hover border color + } + } + + .remove-item { + position: absolute; + bottom: -5; + right: -5; + background-color: rgba(0, 0, 0, 0.5); // Optional: to add a background behind the icon for better visibility + border-radius: 30%; + width: 10px; // Adjust size as needed + height: 10px; // Adjust size as needed + display: flex; + align-items: center; + justify-content: center; + } + } + + // img { + // max-width: 60px; + // margin: 10px; + // border-radius: 5px; + // border: 2px solid white; + // transition: 0.4s; + + // &:hover { + // border-color: orange; + // } + // } + } +} + +.faceCollectionBox { + width: 100%; + height: 100%; + top: 0; + left: 0; + position: absolute; +} diff --git a/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx new file mode 100644 index 000000000..534f67927 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/FaceCollectionBox.tsx @@ -0,0 +1,282 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { IconButton, Size } from 'browndash-components'; +import * as faceapi from 'face-api.js'; +import { FaceMatcher } from 'face-api.js'; +import 'ldrs/ring'; +import { IReactionDisposer, action, makeObservable, observable, reaction } from 'mobx'; +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 { DocData } from '../../../../fields/DocSymbols'; +import { List } from '../../../../fields/List'; +import { DocCast, ImageCast, NumCast, 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 { undoable } from '../../../util/UndoManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +import { DocumentView } from '../../nodes/DocumentView'; +import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { FaceRecognitionHandler } from '../../search/FaceRecognitionHandler'; +import { CollectionStackingView } from '../CollectionStackingView'; +import './FaceCollectionBox.scss'; +import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; + +/** + * This code is used to render the sidebar collection of unique recognized faces, where each + * unique face in turn displays the set of images that correspond to the face. + */ + +/** + * Viewer for unique face Doc collections. + * + * This both displays a collection of images corresponding tp a unique face, and + * allows for editing the face collection by removing an image, or drag-and-dropping + * an image that was not recognized. + */ +@observer +export class UniqueFaceBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(UniqueFaceBox, fieldKey); + } + private _dropDisposer?: DragManager.DragDropDisposer; + private _disposers: { [key: string]: IReactionDisposer } = {}; + private _lastHeight = 0; + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + @observable _headerRef: HTMLDivElement | null = null; + @observable _listRef: HTMLDivElement | null = null; + + observer = new ResizeObserver(a => { + this._props.setHeight?.( + (this.props.Document._face_showImages ? 20 : 0) + // + (!this._headerRef ? 0 : DivHeight(this._headerRef)) + + (!this._listRef ? 0 : DivHeight(this._listRef)) + ); + }); + + componentDidMount(): void { + this._disposers.refList = reaction( + () => ({ refList: [this._headerRef, this._listRef], autoHeight: this.layoutDoc._layout_autoHeight }), + ({ refList, autoHeight }) => { + this.observer.disconnect(); + if (autoHeight) refList.filter(r => r).forEach(r => this.observer.observe(r!)); + }, + { fireImmediately: true } + ); + } + + componentWillUnmount(): void { + this.observer.disconnect(); + Object.keys(this._disposers).forEach(key => this._disposers[key]()); + } + + protected createDropTarget = (ele: HTMLDivElement) => { + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.Document)); + }; + + protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { + de.complete.docDragData?.droppedDocuments + ?.filter(doc => doc.type === DocumentType.IMG) + .forEach(imgDoc => { + // If the current Face Document has no faces, and the doc has more than one face descriptor, don't let the user add the document first. Or should we just use the first face ? + if (FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).length === 0 && FaceRecognitionHandler.ImageDocFaceAnnos(imgDoc).length > 1) { + alert('Cannot add a document with multiple faces as the first item!'); + } else { + // Loop through the documents' face descriptors and choose the face in the iage with the smallest distance (most similar to the face colleciton) + const faceDescriptorsAsFloat32Array = FaceRecognitionHandler.UniqueFaceDescriptors(this.Document).map(fd => new Float32Array(Array.from(fd))); + const labeledFaceDescriptor = new faceapi.LabeledFaceDescriptors(FaceRecognitionHandler.UniqueFaceLabel(this.Document), faceDescriptorsAsFloat32Array); + 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; + }, + { dist: 1, faceAnno: undefined as Opt<Doc> } + ).faceAnno ?? imgDoc; + + // assign the face in the image that's closest to the face collection's face + if (faceAnno) { + faceAnno.face && FaceRecognitionHandler.UniqueFaceRemoveFaceImage(faceAnno, DocCast(faceAnno.face)); + FaceRecognitionHandler.UniqueFaceAddFaceImage(faceAnno, this.Document); + faceAnno.face = this.Document; + } + } + }); + e.stopPropagation(); + return true; + } + + /** + * Toggles whether a Face Document displays its associated docs. This saves and restores the last height of the Doc since + * toggling the associated Documentss overwrites the Doc height. + */ + onDisplayClick() { + this.Document._face_showImages && (this._lastHeight = NumCast(this.Document.height)); + this.Document._face_showImages = !this.Document._face_showImages; + setTimeout(action(() => (!this.Document.layout_autoHeight || !this.Document._face_showImages) && (this.Document.height = this.Document._face_showImages ? this._lastHeight : 60))); + } + + /** + * Removes a unique face Doc from the colelction of unique faces. + */ + deleteUniqueFace = undoable(() => { + FaceRecognitionHandler.DeleteUniqueFace(this.Document); + }, 'delete face'); + + /** + * Removes a face image Doc from a unique face's list of images. + * @param imgDoc - image Doc to remove + */ + removeFaceImageFromUniqueFace = undoable((imgDoc: Doc) => { + FaceRecognitionHandler.UniqueFaceRemoveFaceImage(imgDoc, this.Document); + }, 'remove doc from face'); + + /** + * This stops scroll wheel events when they are used to scroll the face collection. + */ + onPassiveWheel = (e: WheelEvent) => e.stopPropagation(); + + render() { + return ( + <div className="face-document-item" ref={ele => this.createDropTarget(ele!)}> + <div className="face-collection-buttons"> + <IconButton tooltip="Delete Face From Collection" onPointerDown={this.deleteUniqueFace} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} /> + </div> + <div className="face-document-top" ref={action((r: HTMLDivElement | null) => (this._headerRef = r))}> + <h1 style={{ color: lightOrDark(StrCast(this.Document.backgroundColor)) }}> + <input className="face-document-name" type="text" onChange={e => FaceRecognitionHandler.SetUniqueFaceLabel(this.Document, e.currentTarget.value)} value={FaceRecognitionHandler.UniqueFaceLabel(this.Document)} /> + </h1> + </div> + <div className="face-collection-toggle"> + <IconButton + tooltip="See image information" + onPointerDown={() => this.onDisplayClick()} + icon={<FontAwesomeIcon icon={this.Document._face_showImages ? 'caret-up' : 'caret-down'} />} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> + </div> + {this.props.Document._face_showImages ? ( + <div + className="face-document-image-container" + style={{ + pointerEvents: this._props.isContentActive() ? undefined : 'none', + }} + ref={action((ele: HTMLDivElement | null) => { + this._listRef?.removeEventListener('wheel', this.onPassiveWheel); + this._listRef = 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 }); + })}> + {FaceRecognitionHandler.UniqueFaceImages(this.Document).map((doc, i) => { + const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)])?.url.href.split('.') ?? ['-missing-', '.png']; + return ( + <div + className="image-wrapper" + key={i} + onPointerDown={e => + setupMoveUpEvents( + this, + e, + () => { + DragManager.StartDocumentDrag([e.target as HTMLElement], new DragManager.DocumentDragData([doc], dropActionType.embed), e.clientX, e.clientY); + return true; + }, + emptyFunction, + emptyFunction + ) + }> + <img onClick={() => DocumentView.showDocument(doc, { willZoomCentered: true })} style={{ maxWidth: '60px', margin: '10px' }} src={`${name}_o.${type}`} /> + <div className="remove-item"> + <IconButton tooltip={'Remove Doc From Face Collection'} onPointerDown={() => this.removeFaceImageFromUniqueFace(doc)} icon={'x'} style={{ width: '4px' }} size={Size.XSMALL} /> + </div> + </div> + ); + })} + </div> + ) : null} + </div> + ); + } +} + +/** + * This renders the sidebar collection of the unique faces that have been recognized. + * + * Since the collection of recognized faces is stored on the active dashboard, this class + * does not itself store any Docs, but accesses the myUniqueFaces field of the current + * dashboard. (This should probably go away as Doc type in favor of it just being a + * stacking collection of uniqueFace docs) + */ +@observer +export class FaceCollectionBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(FaceCollectionBox, fieldKey); + } + + constructor(props: FieldViewProps) { + super(props); + makeObservable(this); + } + + moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); + addDocument = (doc: Doc | Doc[], annotationKey?: string) => { + const uniqueFaceDoc = doc instanceof Doc ? doc : doc[0]; + const added = uniqueFaceDoc.type === DocumentType.UFACE; + if (added) { + Doc.SetContainer(uniqueFaceDoc, Doc.MyFaceCollection); + Doc.ActiveDashboard && Doc.AddDocToList(Doc.ActiveDashboard[DocData], 'myUniqueFaces', uniqueFaceDoc); + } + return added; + }; + /** + * this changes style provider requests that target the dashboard to requests that target the face collection box which is what's actually being rendered. + * This is needed, for instance, to get the default background color from the face collection, not the dashboard. + */ + stackingStyleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => { + if (doc === Doc.ActiveDashboard) return this._props.styleProvider?.(this.Document, this._props, property); + return this._props.styleProvider?.(doc, this._props, property); + }; + + render() { + return !Doc.ActiveDashboard ? null : ( + <div className="faceCollectionBox"> + <div className="documentButtonMenu"> + <div className="documentExplanation" onClick={action(() => (Doc.UserDoc().recognizeFaceImages = !Doc.UserDoc().recognizeFaceImages))}>{`Face Recgognition is ${Doc.UserDoc().recognizeFaceImages ? 'on' : 'off'}`}</div> + </div> + <CollectionStackingView + {...this._props} // + styleProvider={this.stackingStyleProvider} + Document={Doc.ActiveDashboard} + fieldKey="myUniqueFaces" + moveDocument={this.moveDocument} + addDocument={this.addDocument} + isContentActive={returnTrue} + isAnyChildContentActive={returnTrue} + childHideDecorations={true} + dontCenter="y" + /> + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.FACECOLLECTION, { + layout: { view: FaceCollectionBox, dataField: 'data' }, + options: { acl: '', _width: 400, dropAction: dropActionType.embed }, +}); + +Docs.Prototypes.TemplateMap.set(DocumentType.UFACE, { + layout: { view: UniqueFaceBox, dataField: 'face_images' }, + options: { acl: '', _width: 400, _height: 400 }, +}); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.scss b/src/client/views/collections/collectionFreeForm/ImageLabelBox.scss new file mode 100644 index 000000000..819c72760 --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.scss @@ -0,0 +1,85 @@ +.image-box-container { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-size: 10px; + line-height: 1; + background: none; + z-index: 1000; + padding: 0px; + overflow: auto; + cursor: default; +} + +.image-label-list { + display: flex; + flex-direction: column; + align-items: center; // Centers the content vertically in the flex container + width: 100%; + + > div { + display: flex; + justify-content: space-between; // Puts the content and delete button on opposite ends + align-items: center; + width: 100%; + margin-top: 8px; // Adds space between label rows + background-color: black; + + p { + text-align: center; // Centers the text of the paragraph + font-size: large; + vertical-align: middle; + margin-left: 10px; + } + + .IconButton { + // Styling for the delete button + margin-left: auto; // Pushes the button to the far right + } + } +} + +.image-information-list { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + margin-top: 10px; +} + +.image-information { + border: 1px solid; + width: 100%; + display: inline-flex; + flex-direction: column; + justify-content: center; + align-items: center; + overflow: hidden; + padding: 2px; + overflow-x: auto; + overflow-y: auto; + + img { + max-width: 200px; + max-height: 200px; + width: auto; + height: auto; + } +} + +.image-information-labels { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + .image-label { + margin-top: 5px; + margin-bottom: 5px; + padding: 3px; + border-radius: 2px; + border: solid 1px; + } +} diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx new file mode 100644 index 000000000..033d1590d --- /dev/null +++ b/src/client/views/collections/collectionFreeForm/ImageLabelBox.tsx @@ -0,0 +1,346 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Colors, IconButton } from 'browndash-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 { Utils, numberRange } from '../../../../Utils'; +import { Doc, NumListCast, Opt } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; +import { List } from '../../../../fields/List'; +import { ImageCast } from '../../../../fields/Types'; +import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; +import { DocumentType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; +import { DragManager } from '../../../util/DragManager'; +import { SettingsManager } from '../../../util/SettingsManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { ViewBoxBaseComponent } from '../../DocComponent'; +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 { + static _instance: ImageLabelBoxData; + @observable _docs: Doc[] = []; + @observable _labelGroups: string[] = []; + + constructor() { + makeObservable(this); + ImageLabelBoxData._instance = this; + } + public static get Instance() { + return ImageLabelBoxData._instance ?? new ImageLabelBoxData(); + } + + @action + public setData = (docs: Doc[]) => { + this._docs = docs; + }; + + @action + addLabel = (label: string) => { + label = label.toUpperCase().trim(); + if (label.length > 0) { + if (!this._labelGroups.includes(label)) { + this._labelGroups = [...this._labelGroups, label.startsWith('#') ? label : '#' + label]; + } + } + }; + + @action + removeLabel = (label: string) => { + const labelUp = label.toUpperCase(); + this._labelGroups = this._labelGroups.filter(group => group !== labelUp); + }; +} + +@observer +export class ImageLabelBox extends ViewBoxBaseComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(ImageLabelBox, fieldKey); + } + + private _dropDisposer?: DragManager.DragDropDisposer; + public static Instance: ImageLabelBox; + private _inputRef = React.createRef<HTMLInputElement>(); + @observable _loading: boolean = false; + private _currentLabel: string = ''; + + protected createDropTarget = (ele: HTMLDivElement) => { + this._dropDisposer?.(); + ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.layoutDoc)); + }; + + protected onInternalDrop(e: Event, de: DragManager.DropEvent): boolean { + const { docDragData } = de.complete; + if (docDragData) { + ImageLabelBoxData.Instance.setData(ImageLabelBoxData.Instance._docs.concat(docDragData.droppedDocuments)); + return false; + } + return false; + } + + @computed get _labelGroups() { + return ImageLabelBoxData.Instance._labelGroups; + } + + @computed get _selectedImages() { + // return DocListCast(this.dataDoc.data); + return ImageLabelBoxData.Instance._docs; + } + @observable _displayImageInformation: boolean = false; + + constructor(props: any) { + super(props); + makeObservable(this); + ring.register(); + ImageLabelBox.Instance = this; + } + + // ImageLabelBox.Instance.setData() + /** + * This method is called when the SearchBox component is first mounted. When the user opens + * the search panel, the search input box is automatically selected. This allows the user to + * type in the search input box immediately, without needing clicking on it first. + */ + componentDidMount() { + this.classifyImagesInBox(); + reaction( + () => this._selectedImages, + () => this.classifyImagesInBox() + ); + } + + @action + groupImages = () => { + this.groupImagesInBox(); + }; + + @action + startLoading = () => { + this._loading = true; + }; + + @action + endLoading = () => { + this._loading = false; + }; + + @action + toggleDisplayInformation = () => { + this._displayImageInformation = !this._displayImageInformation; + if (this._displayImageInformation) { + this._selectedImages.forEach(doc => (doc._layout_showTags = true)); + } else { + this._selectedImages.forEach(doc => (doc._layout_showTags = false)); + } + }; + + @action + submitLabel = () => { + const input = document.getElementById('new-label') as HTMLInputElement; + ImageLabelBoxData.Instance.addLabel(this._currentLabel); + this._currentLabel = ''; + input.value = ''; + }; + + onInputChange = action((e: React.ChangeEvent<HTMLInputElement>) => { + this._currentLabel = e.target.value; + }); + + classifyImagesInBox = async () => { + this.startLoading(); + + // Converts the images into a Base64 format, afterwhich the information is sent to GPT to label them. + + 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 => + !hrefBase64 ? undefined : + gptImageLabel(hrefBase64).then(labels => + ({ doc, labels }))) ; // prettier-ignore + } + }); + + (await Promise.all(imageInfos)).forEach(imageInfo => { + if (imageInfo) { + imageInfo.doc[DocData].tags_chat = (imageInfo.doc[DocData].tags_chat as List<string>) ?? new List<string>(); + + const labels = imageInfo.labels.split('\n'); + labels.forEach(label => { + label = + '#' + + label + .replace(/^\d+\.\s*|-|f\*/, '') + .replace(/^#/, '') + .trim(); + (imageInfo.doc[DocData].tags_chat as List<string>).push(label); + }); + } + }); + + this.endLoading(); + }; + + /** + * Groups images to most similar labels. + */ + groupImagesInBox = action(async () => { + this.startLoading(); + + for (const doc of this._selectedImages) { + for (let index = 0; index < (doc[DocData].tags_chat as List<string>).length; index++) { + const label = (doc[DocData].tags_chat as List<string>)[index]; + const embedding = await gptGetEmbedding(label); + doc[DocData][`tags_embedding_${index + 1}`] = new List<number>(embedding); + } + } + + const labelToEmbedding = new Map<string, number[]>(); + // Create embeddings for the labels. + await Promise.all(this._labelGroups.map(async label => gptGetEmbedding(label).then(labelEmbedding => labelToEmbedding.set(label, labelEmbedding)))); + + // For each image, loop through the labels, and calculate similarity. Associate it with the + // 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 {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, + { label: '', similarityScore: 0, }); // prettier-ignore + doc[DocData].data_label = mostSimilarLabelCollect; // The label most similar to the image's contents. + }); + + this.endLoading(); + + if (this._selectedImages) { + MarqueeOptionsMenu.Instance.groupImages(); + } + + MainView.Instance.closeFlyout(); + }); + + render() { + if (this._loading) { + return ( + <div className="image-box-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}> + <l-ring size="60" color="white" /> + </div> + ); + } + + 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> + </div> + ); + } + + return ( + <div className="searchBox-container" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }} ref={ele => this.createDropTarget(ele!)}> + <div className="searchBox-bar" style={{ pointerEvents: 'all', color: SnappingManager.userColor, background: SnappingManager.userBackgroundColor }}> + <IconButton + tooltip={'See image information'} + onPointerDown={this.toggleDisplayInformation} + icon={this._displayImageInformation ? <FontAwesomeIcon icon="caret-up" /> : <FontAwesomeIcon icon="caret-down" />} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> + <input + defaultValue="" + autoComplete="off" + onChange={this.onInputChange} + onKeyDown={e => { + e.key === 'Enter' ? this.submitLabel() : null; + e.stopPropagation(); + }} + type="text" + placeholder="Input groups for images to be put into..." + aria-label="label-input" + id="new-label" + className="searchBox-input" + style={{ width: '100%', borderRadius: '5px' }} + ref={this._inputRef} + /> + <IconButton + tooltip={'Add a label'} + onPointerDown={() => { + const input = document.getElementById('new-label') as HTMLInputElement; + ImageLabelBoxData.Instance.addLabel(this._currentLabel); + this._currentLabel = ''; + input.value = ''; + }} + icon={<FontAwesomeIcon icon="plus" />} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '19px' }} + /> + {this._labelGroups.length > 0 ? <IconButton tooltip={'Group Images'} onPointerDown={this.groupImages} icon={<FontAwesomeIcon icon="object-group" />} color={Colors.MEDIUM_BLUE} style={{ width: '19px' }} /> : <div></div>} + </div> + <div> + <div className="image-label-list"> + {this._labelGroups.map(group => { + return ( + <div key={Utils.GenerateGuid()}> + <p style={{ color: MarqueeOptionsMenu.Instance.userColor }}>{group}</p> + <IconButton + tooltip={'Remove Label'} + onPointerDown={() => { + ImageLabelBoxData.Instance.removeLabel(group); + }} + icon={'x'} + color={MarqueeOptionsMenu.Instance.userColor} + style={{ width: '8px' }} + /> + </div> + ); + })} + </div> + </div> + {this._displayImageInformation ? ( + <div className="image-information-list"> + {this._selectedImages.map(doc => { + const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); + return ( + <div className="image-information" style={{ borderColor: SettingsManager.userColor }} key={Utils.GenerateGuid()}> + <img + src={`${name}_o.${type}`} + onClick={async () => { + await DocumentView.showDocument(doc, { willZoomCentered: true }); + }}></img> + <div className="image-information-labels" onClick={() => this._props.addDocTab(doc, OpenWhere.addRightKeyvalue)}> + {(doc[DocData].tags_chat as List<string>).map(label => { + return ( + <div key={Utils.GenerateGuid()} className="image-label" style={{ backgroundColor: SettingsManager.userVariantColor, borderColor: SettingsManager.userColor }}> + {label} + </div> + ); + })} + </div> + </div> + ); + })} + </div> + ) : ( + <div></div> + )} + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.IMAGEGROUPER, { + layout: { view: ImageLabelBox, dataField: 'data' }, + options: { acl: '', _width: 400 }, +}); diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx index 7f27c6b5c..73befb205 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx +++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.tsx @@ -77,7 +77,7 @@ export class ImageLabelHandler extends ObservableReactComponent<{}> { }}> <div> <IconButton tooltip={'Cancel'} onPointerDown={this.hideLabelhandler} icon={<FontAwesomeIcon icon="eye-slash" />} color={MarqueeOptionsMenu.Instance.userColor} style={{ width: '19px' }} /> - <input aria-label="label-input" id="new-label" type="text" style={{ color: 'black' }} /> + <input aria-label="label-input" id="new-label" type="text" placeholder="Input a classification" style={{ color: 'black' }} /> <IconButton tooltip={'Add Label'} onPointerDown={() => { diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index f02cd9d45..44c916ab9 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -18,10 +18,10 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { public showMarquee: () => void = unimplementedFunction; public hideMarquee: () => void = unimplementedFunction; public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; - public classifyImages: (e: React.MouseEvent | undefined) => void = unimplementedFunction; + public classifyImages: () => void = unimplementedFunction; public groupImages: () => void = unimplementedFunction; public isShown = () => this._opacity > 0; - constructor(props: any) { + constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); MarqueeOptionsMenu.Instance = this; @@ -39,7 +39,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { <IconButton tooltip="Summarize Documents" onPointerDown={this.summarize} icon={<FontAwesomeIcon icon="compress-arrows-alt" />} color={this.userColor} /> <IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} /> <IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} /> - <IconButton tooltip="Classify Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} /> + <IconButton tooltip="Classify and Sort Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} /> </> ); return this.getElement(buttons); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index dc15c83c5..ccb6bc9be 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,28 +1,24 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -import similarity from 'compute-cosine-similarity'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils'; -import { intersectRect, numberRange } from '../../../../Utils'; -import { Doc, NumListCast, Opt } from '../../../../fields/Doc'; +import { intersectRect } from '../../../../Utils'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { InkData, InkField, InkTool } from '../../../../fields/InkField'; +import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { RichTextField } from '../../../../fields/RichTextField'; -import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; +import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; -import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; -import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { DocUtils } from '../../../documents/DocUtils'; -import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { DocumentType } from '../../../documents/DocumentTypes'; import { Docs, DocumentOptions } from '../../../documents/Documents'; import { SnappingManager, freeformScrollMode } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { UndoManager, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; +import { MainView } from '../../MainView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { MarqueeViewBounds } from '../../PinFuncs'; import { PreviewCursor } from '../../PreviewCursor'; @@ -30,10 +26,8 @@ import { DocumentView } from '../../nodes/DocumentView'; import { OpenWhere } from '../../nodes/OpenWhere'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; -import { CollectionCardView } from '../CollectionCardDeckView'; import { SubCollectionViewProps } from '../CollectionSubView'; -import { CollectionFreeFormView } from './CollectionFreeFormView'; -import { ImageLabelHandler } from './ImageLabelHandler'; +import { ImageLabelBoxData } from './ImageLabelBox'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; @@ -53,6 +47,9 @@ interface MarqueeViewProps { slowLoadDocuments: (files: File[] | string, options: DocumentOptions, generatedDocuments: Doc[], text: string, completed: ((doc: Doc[]) => void) | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => Promise<void>; } +/** + * A component that deals with the marquee select in the freeform canvas. + */ @observer export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps & MarqueeViewProps> { public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) { @@ -60,9 +57,12 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps }; } - constructor(props: any) { + static Instance: MarqueeView; + + constructor(props: SubCollectionViewProps & MarqueeViewProps) { super(props); makeObservable(this); + MarqueeView.Instance = this; } private _commandExecuted = false; @@ -156,11 +156,12 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps } else if (e.key === 'b' && e.ctrlKey) { document.body.focus(); // so that we can access the clipboard without an error setTimeout(() => + // eslint-disable-next-line @typescript-eslint/no-explicit-any pasteImageBitmap((data: any, error: any) => { error && console.log(error); data && - ClientUtils.convertDataUri(data, this._props.Document[Id] + '-thumb-frozen').then(returnedfilename => { - this._props.Document['thumb-frozen'] = new ImageField(returnedfilename); + ClientUtils.convertDataUri(data, this._props.Document[Id] + '_icon_' + new Date().getTime()).then(returnedfilename => { + this._props.Document[DocData].icon = new ImageField(returnedfilename); }); }) ); @@ -430,32 +431,14 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps /** * Classifies images and assigns the labels as document fields. - * TODO: Turn into lists of labels instead of individual fields. */ @undoBatch - classifyImages = action(async (e: React.MouseEvent | undefined) => { - this._selectedDocs = this.marqueeSelect(false, DocumentType.IMG); - - const imageInfos = this._selectedDocs.map(async doc => { - const [name, type] = ImageCast(doc[Doc.LayoutFieldKey(doc)]).url.href.split('.'); - return CollectionCardView.imageUrlToBase64(`${name}_o.${type}`).then(hrefBase64 => - !hrefBase64 ? undefined : - gptImageLabel(hrefBase64).then(labels => - Promise.all(labels.split('\n').map(label => gptGetEmbedding(label))).then(embeddings => - ({ doc, embeddings, labels }))) ); // prettier-ignore - }); - - (await Promise.all(imageInfos)).forEach(imageInfo => { - if (imageInfo && Array.isArray(imageInfo.embeddings)) { - imageInfo.doc[DocData].data_labels = imageInfo.labels; - numberRange(3).forEach(n => { - imageInfo.doc[`data_labels_embedding_${n + 1}`] = new List<number>(imageInfo.embeddings[n]); - }); - } - }); - - if (e) { - ImageLabelHandler.Instance.displayLabelHandler(e.pageX, e.pageY); + classifyImages = action(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); } }); @@ -464,93 +447,44 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps */ @undoBatch groupImages = action(async () => { - const labelGroups = ImageLabelHandler.Instance._labelGroups; - const labelToEmbedding = new Map<string, number[]>(); - // Create embeddings for the labels. - await Promise.all(labelGroups.map(async label => gptGetEmbedding(label).then(labelEmbedding => labelToEmbedding.set(label, labelEmbedding)))); - - // For each image, loop through the labels, and calculate similarity. Associate it with the - // most similar one. - this._selectedDocs.forEach(doc => { - const embedLists = numberRange(3).map(n => Array.from(NumListCast(doc[`data_labels_embedding_${n + 1}`]))); - const bestEmbedScore = (embedding: Opt<number[]>) => Math.max(...embedLists.map(l => (embedding && similarity(Array.from(embedding), l)) || 0)); - const {label: mostSimilarLabelCollect} = - labelGroups.map(label => ({ label, similarityScore: bestEmbedScore(labelToEmbedding.get(label)) })) - .reduce((prev, cur) => cur.similarityScore < 0.3 || cur.similarityScore <= prev.similarityScore ? prev: cur, - { label: '', similarityScore: 0, }); // prettier-ignore - - numberRange(3).forEach(n => { - doc[`data_labels_embedding_${n + 1}`] = undefined; - }); - doc[DocData].data_label = mostSimilarLabelCollect; - }); - this._props.Document._type_collection = CollectionViewType.Time; - this._props.Document.pivotField = 'data_label'; - }); + const labelGroups: string[] = ImageLabelBoxData.Instance._labelGroups; + const labelToCollection: Map<string, Doc> = new Map(); + const selectedImages = ImageLabelBoxData.Instance._docs; + + // Create new collections associated with each label and get the embeddings for the labels. + let x_offset = 0; + let y_offset = 0; + let row_count = 0; + for (const label of labelGroups) { + const newCollection = this.getCollection([], undefined, false); + newCollection._width = 900; + newCollection._height = 900; + newCollection._x = this.Bounds.left; + newCollection._y = this.Bounds.top; + newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; + newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; + newCollection._x = (newCollection._x as number) + x_offset; + newCollection._y = (newCollection._y as number) + y_offset; + x_offset += (newCollection._width as number) + 40; + row_count += 1; + if (row_count == 3) { + y_offset += (newCollection._height as number) + 40; + x_offset = 0; + row_count = 0; + } + labelToCollection.set(label, newCollection); + this._props.addDocument?.(newCollection); + } - @undoBatch - syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => { - const selected = this.marqueeSelect(false); - if (e instanceof KeyboardEvent ? e.key === 'i' : true) { - const inks = selected.filter(s => s.type === DocumentType.INK); - const setDocs = selected.filter(s => s.type === DocumentType.RTF && s.color); - const sets = setDocs.map(sd => Cast(sd.data, RichTextField)?.Text as string); - const colors = setDocs.map(sd => FieldValue(sd.color) as string); - const wordToColor = new Map<string, string>(); - sets.forEach((st: string, i: number) => st.split(',').forEach(word => wordToColor.set(word, colors[i]))); - const strokes: InkData[] = []; - inks.filter(i => Cast(i.data, InkField)).forEach(i => { - const d = Cast(i.data, InkField, null); - const left = Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); - const top = Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); - strokes.push(d.inkData.map(pd => ({ X: pd.X + NumCast(i.x) - left, Y: pd.Y + NumCast(i.y) - top }))); - }); - CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => { - // const wordResults = results.filter((r: any) => r.category === "inkWord"); - // for (const word of wordResults) { - // const indices: number[] = word.strokeIds; - // indices.forEach(i => { - // if (wordToColor.has(word.recognizedText.toLowerCase())) { - // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); - // } - // else { - // for (const alt of word.alternates) { - // if (wordToColor.has(alt.recognizedString.toLowerCase())) { - // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); - // break; - // } - // } - // } - // }) - // } - // const wordResults = results.filter((r: any) => r.category === "inkWord"); - // for (const word of wordResults) { - // const indices: number[] = word.strokeIds; - // indices.forEach(i => { - // const otherInks: Doc[] = []; - // indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); - // inks[i].relatedInks = new List<Doc>(otherInks); - // const uniqueColors: string[] = []; - // Array.from(wordToColor.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); - // inks[i].alternativeColors = new List<string>(uniqueColors); - // if (wordToColor.has(word.recognizedText.toLowerCase())) { - // inks[i].color = wordToColor.get(word.recognizedText.toLowerCase()); - // } - // else if (word.alternates) { - // for (const alt of word.alternates) { - // if (wordToColor.has(alt.recognizedString.toLowerCase())) { - // inks[i].color = wordToColor.get(alt.recognizedString.toLowerCase()); - // break; - // } - // } - // } - // }); - // } - const lines = results.filter((r: any) => r.category === 'line'); - const text = lines.map((l: any) => l.recognizedText).join('\r\n'); - this._props.addDocument?.(Docs.Create.TextDocument(text, { _width: this.Bounds.width, _height: this.Bounds.height, x: this.Bounds.left + this.Bounds.width, y: this.Bounds.top, title: text })); - }); + for (const doc of selectedImages) { + if (doc[DocData].data_label) { + Doc.AddDocToList(labelToCollection.get(doc[DocData].data_label as string)!, undefined, doc); + this._props.removeDocument?.(doc); + } } + + //this._props.Document._type_collection = CollectionViewType.Time; // Change the collection view to a Time view. + //this._props.Document.pivotField = 'data_label'; // Sets the pivot to be the 'data_label'. }); @undoBatch @@ -582,13 +516,14 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps @action marqueeCommand = (e: KeyboardEvent) => { - if (this._commandExecuted || (e as any).propagationIsStopped) { + const ee = e as unknown as KeyboardEvent & { propagationIsStopped?: boolean }; + if (this._commandExecuted || ee.propagationIsStopped) { return; } if (e.key === 'Backspace' || e.key === 'Delete' || e.key === 'd' || e.key === 'h') { this._commandExecuted = true; e.stopPropagation(); - (e as any).propagationIsStopped = true; + ee.propagationIsStopped = true; this.delete(e, e.key === 'h'); e.stopPropagation(); } @@ -596,7 +531,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); - (e as any).propagationIsStopped = true; + ee.propagationIsStopped = true; if (e.key === 'g') this.collection(e, true); if (e.key === 'c' || e.key === 't') this.collection(e); if (e.key === 's' || e.key === 'S') this.summary(); @@ -697,8 +632,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps transform: `translate(${p[0]}px, ${p[1]}px)`, width: Math.abs(v[0]), height: Math.abs(v[1]), - color: lightOrDark(this._props.Document?.backgroundColor ?? 'white'), - borderColor: lightOrDark(this._props.Document?.backgroundColor ?? 'white'), + color: lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white'), + borderColor: lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white'), zIndex: 2000, }}> {' '} @@ -707,7 +642,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps <polyline // points={this._lassoPts.reduce((s, pt) => s + pt[0] + ',' + pt[1] + ' ', '')} fill="none" - stroke={lightOrDark(this._props.Document?.backgroundColor ?? 'white')} + stroke={lightOrDark((this._props.Document?.backgroundColor as string) ?? 'white')} strokeWidth="1" strokeDasharray="3" /> @@ -727,8 +662,9 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps */ @action onDragMovePause = (e: CustomEvent<React.DragEvent>) => { - if ((e as any).handlePan || this._props.isAnnotationOverlay) return; - (e as any).handlePan = true; + const ee = e as CustomEvent<React.DragEvent> & { handlePan?: boolean }; + if (ee.handlePan || this._props.isAnnotationOverlay) return; + ee.handlePan = true; const bounds = this.MarqueeRef?.getBoundingClientRect(); if (!this._props.Document._freeform_noAutoPan && !this._props.renderDepth && bounds) { @@ -746,10 +682,10 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps }; render() { return ( - // eslint-disable-next-line jsx-a11y/click-events-have-key-events <div className="marqueeView" ref={r => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any r?.addEventListener('dashDragMovePause', this.onDragMovePause as any); this.MarqueeRef = r; }} diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx index 2d9191dd7..5c41fee37 100644 --- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx +++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx @@ -9,11 +9,11 @@ import { emptyFunction } from '../../../../Utils'; import { Docs } from '../../../documents/Documents'; import { DragManager } from '../../../util/DragManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { DocumentView } from '../../nodes/DocumentView'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionGridView.scss'; import Grid, { Layout } from './Grid'; @@ -26,7 +26,7 @@ export class CollectionGridView extends CollectionSubView() { @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll private dropLocation: object = {}; // sets the drop location for external drops - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -200,7 +200,7 @@ export class CollectionGridView extends CollectionSubView() { whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} onClickScript={this.onChildClickHandler} renderDepth={this._props.renderDepth + 1} - dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as any} // 'y', 'x', 'xy' + dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'} /> ); } @@ -221,13 +221,13 @@ export class CollectionGridView extends CollectionSubView() { }); if (this.Document.gridStartCompaction) { - undoBatch(() => { + undoable(() => { this.Document.gridCompaction = this.Document.gridStartCompaction; this.setLayoutList(savedLayouts); - })(); + }, 'start grid compaction')(); this.Document.gridStartCompaction = undefined; } else { - undoBatch(() => this.setLayoutList(savedLayouts))(); + undoable(() => this.setLayoutList(savedLayouts), 'start grid compaction')(); } } }; @@ -315,9 +315,9 @@ export class CollectionGridView extends CollectionSubView() { e, returnFalse, action(() => { - undoBatch(() => { + undoable(() => { this.Document.gridRowHeight = this._rowHeight; - })(); + }, 'changing row height')(); this._rowHeight = undefined; }), emptyFunction, @@ -360,13 +360,14 @@ export class CollectionGridView extends CollectionSubView() { returnFalse, (clickEv: PointerEvent, doubleTap?: boolean) => { if (doubleTap && !clickEv.button) { - undoBatch( + undoable( action(() => { const text = Docs.Create.TextDocument('', { _width: 150, _height: 50 }); Doc.SetSelectOnLoad(text); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed Doc.AddDocToList(this.Document, this._props.fieldKey, text); this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(clickEv.clientX, clickEv.clientY)))); - }) + }), + 'create grid text' )(); } }, diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index eac0dc0e1..ceae43c04 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -1,8 +1,7 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Toggle, ToggleType, Type } from 'browndash-components'; +import { Property } from 'csstype'; import { IReactionDisposer, action, makeObservable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -22,7 +21,7 @@ import { UndoStack } from '../../UndoStack'; import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; import { DocumentView } from '../../nodes/DocumentView'; import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionLinearView.scss'; /** @@ -39,7 +38,7 @@ export class CollectionLinearView extends CollectionSubView() { private _widthDisposer?: IReactionDisposer; private _selectedDisposer?: IReactionDisposer; - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -239,7 +238,7 @@ export class CollectionLinearView extends CollectionSubView() { className="collectionLinearView-content" style={{ height: this.dimension(), - flexDirection: flexDir as any, + flexDirection: flexDir as Property.FlexDirection, gap: flexGap, }}> {this.childLayoutPairs.map(pair => this.getDisplayDoc(pair.layout))} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss index f983fd815..9ed247d50 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.scss @@ -1,43 +1,51 @@ -.collectionMulticolumnView_contents { - display: flex; - //overflow: hidden; // bcz: turned of to allow highlighting to appear when there is no border (e.g, for a component of the slide template) - width: 100%; +.collectionMulticolumnView_drop { height: 100%; + width: 100%; + top: 0; + left: 0; + position: absolute; - .document-wrapper { + .collectionMulticolumnView_contents { display: flex; - flex-direction: column; + //overflow: hidden; // bcz: turned of to allow highlighting to appear when there is no border (e.g, for a component of the slide template) width: 100%; - align-items: center; - position: relative; - > .iconButton-container { - top: 0; - left: 0; - position: absolute; - } + height: 100%; - .contentFittingDocumentView { - margin: auto; - } - - .label-wrapper { + .document-wrapper { display: flex; - flex-direction: row; - justify-content: center; - height: 20px; + flex-direction: column; + width: 100%; + align-items: center; + position: relative; + > .iconButton-container { + top: 0; + left: 0; + position: absolute; + } + + .contentFittingDocumentView { + margin: auto; + } + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } } - } - .multiColumnResizer { - cursor: ew-resize; - transition: 0.5s opacity ease; - display: flex; - flex-direction: column; + .multiColumnResizer { + cursor: ew-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: column; - .multiColumnResizer-hdl { - width: 100%; - height: 100%; - transition: 0.5s background-color ease; + .multiColumnResizer-hdl { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } } } } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index b8509a005..d67e10c0b 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -1,5 +1,3 @@ -/* eslint-disable jsx-a11y/no-static-element-interactions */ -/* eslint-disable jsx-a11y/click-events-have-key-events */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Button, IconButton } from 'browndash-components'; @@ -12,13 +10,14 @@ import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch, undoable } from '../../../util/UndoManager'; +import { undoable } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionMulticolumnView.scss'; import ResizeBar from './MulticolumnResizer'; import WidthLabel from './MulticolumnWidthLabel'; import { dropActionType } from '../../../util/DropActionTypes'; +import { SnappingManager } from '../../../util/SnappingManager'; interface WidthSpecifier { magnitude: number; @@ -42,7 +41,7 @@ const resizerWidth = 8; export class CollectionMulticolumnView extends CollectionSubView() { @observable _startIndex = 0; - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -198,30 +197,28 @@ export class CollectionMulticolumnView extends CollectionSubView() { * documents before the target. */ private lookupIndividualTransform = (layout: Doc) => { - const { columnUnitLength } = this; - if (columnUnitLength === undefined) { + if (this.columnUnitLength === undefined) { return Transform.Identity(); // we're still waiting on promises to resolve } let offset = 0; // eslint-disable-next-line no-restricted-syntax for (const { layout: candidate } of this.childLayoutPairs) { if (candidate === layout) { - return this.ScreenToLocalBoxXf().translate(0, -offset / (this._props.NativeDimScaling?.() || 1)); + return this.ScreenToLocalBoxXf().translate(-offset / (this._props.NativeDimScaling?.() || 1), 0); } offset += this.lookupPixels(candidate) + resizerWidth; } return Transform.Identity(); }; - @undoBatch onInternalDrop = (e: Event, de: DragManager.DropEvent) => { let dropInd = -1; - if (de.complete.docDragData && this._mainCont) { + if (de.complete.docDragData && this._contRef.current) { let curInd = -1; de.complete.docDragData?.droppedDocuments.forEach(d => { curInd = this.childDocs.indexOf(d); }); - Array.from(this._mainCont.children).forEach((child, index) => { + Array.from(this._contRef.current.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.x < de.x && brect.x + brect.width > de.x) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { @@ -305,7 +302,7 @@ export class CollectionMulticolumnView extends CollectionSubView() { whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} addDocTab={this._props.addDocTab} pinToPres={this._props.pinToPres} - dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as any} // 'y', 'x', 'xy' + dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'x' | 'y' | 'xy'} /> ); }; @@ -319,11 +316,11 @@ export class CollectionMulticolumnView extends CollectionSubView() { this.childLayouts.forEach((layout, i) => { collector.push( // eslint-disable-next-line react/no-array-index-key - <Tooltip title={'Tab: ' + StrCast(layout.title)} key={'wrapper' + i}> + <Tooltip title={'Doc: ' + StrCast(layout.title)} key={'wrapper' + i}> <div className="document-wrapper" style={{ flexDirection: 'column', width: this.lookupPixels(layout) }}> {this.getDisplayDoc(layout)} {this.layoutDoc._chromeHidden ? null : ( - <Button tooltip="Remove document from header bar" icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={undoable(() => this._props.removeDocument?.(layout), 'close doc')} color={SettingsManager.userColor} /> + <Button tooltip="Remove document" icon={<FontAwesomeIcon icon="times" size="lg" />} onClick={undoable(() => this._props.removeDocument?.(layout), 'close doc')} color={SettingsManager.userColor} /> )} <WidthLabel layout={layout} collectionDoc={this.Document} /> </div> @@ -345,49 +342,53 @@ export class CollectionMulticolumnView extends CollectionSubView() { return collector; } + _contRef = React.createRef<HTMLDivElement>(); render() { return ( - <div - className="collectionMulticolumnView_contents" - ref={this.createDashEventsTarget} - style={{ - width: `calc(100% - ${2 * NumCast(this.Document._xMargin)}px)`, - height: `calc(100% - ${2 * NumCast(this.Document._yMargin)}px)`, - marginLeft: NumCast(this.Document._xMargin), - marginRight: NumCast(this.Document._xMargin), - marginTop: NumCast(this.Document._yMargin), - marginBottom: NumCast(this.Document._yMargin), - }}> - {this.contents} - {!this._startIndex ? null : ( - <Tooltip title="scroll back"> - <div - style={{ position: 'absolute', bottom: 0, left: 0, background: SettingsManager.userVariantColor }} - onClick={action(() => { - this._startIndex = Math.min(this.childLayoutPairs.length - 1, this._startIndex + this.maxShown); - })}> - <Button - tooltip="Scroll back" - icon={<FontAwesomeIcon icon="chevron-left" size="lg" />} + <div className="collectionMulticolumnView_drop" ref={this.createDashEventsTarget}> + <div + className="collectionMulticolumnView_contents" + ref={this._contRef} + style={{ + pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : this._props.pointerEvents?.(), + width: `calc(100% - ${2 * NumCast(this.Document._xMargin)}px)`, + height: `calc(100% - ${2 * NumCast(this.Document._yMargin)}px)`, + marginLeft: NumCast(this.Document._xMargin), + marginRight: NumCast(this.Document._xMargin), + marginTop: NumCast(this.Document._yMargin), + marginBottom: NumCast(this.Document._yMargin), + }}> + {this.contents} + {!this._startIndex ? null : ( + <Tooltip title="scroll back"> + <div + style={{ position: 'absolute', bottom: 0, left: 0, background: SettingsManager.userVariantColor }} + onClick={action(() => { + this._startIndex = Math.min(this.childLayoutPairs.length - 1, this._startIndex + this.maxShown); + })}> + <Button + tooltip="Scroll back" + icon={<FontAwesomeIcon icon="chevron-left" size="lg" />} + onClick={action(() => { + this._startIndex = Math.max(0, this._startIndex - this.maxShown); + })} + color={SettingsManager.userColor} + /> + </div> + </Tooltip> + )} + {this._startIndex > this.childLayoutPairs.length - 1 || !this.maxShown ? null : ( + <Tooltip title="scroll forward"> + <div + style={{ position: 'absolute', bottom: 0, right: 0, background: SettingsManager.userVariantColor }} onClick={action(() => { - this._startIndex = Math.max(0, this._startIndex - this.maxShown); - })} - color={SettingsManager.userColor} - /> - </div> - </Tooltip> - )} - {this._startIndex > this.childLayoutPairs.length - 1 || !this.maxShown ? null : ( - <Tooltip title="scroll forward"> - <div - style={{ position: 'absolute', bottom: 0, right: 0, background: SettingsManager.userVariantColor }} - onClick={action(() => { - this._startIndex = Math.min(this.childLayoutPairs.length - 1, this._startIndex + this.maxShown); - })}> - <IconButton icon={<FaChevronRight />} color={SettingsManager.userColor} /> - </div> - </Tooltip> - )} + this._startIndex = Math.min(this.childLayoutPairs.length - 1, this._startIndex + this.maxShown); + })}> + <IconButton icon={<FaChevronRight />} color={SettingsManager.userColor} /> + </div> + </Tooltip> + )} + </div> </div> ); } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss index f44eacb2a..91779065d 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.scss @@ -1,34 +1,42 @@ -.collectionMultirowView_contents { - display: flex; - //overflow: hidden; // bcz: turned of to allow highlighting to appear when there is no border (e.g, for a component of the slide template) - width: 100%; +.collectionMultirowView_drop { height: 100%; - flex-direction: column; + width: 100%; + top: 0; + left: 0; + position: absolute; - .document-wrapper { + .collectionMultirowView_contents { display: flex; - flex-direction: row; + //overflow: hidden; // bcz: turned of to allow highlighting to appear when there is no border (e.g, for a component of the slide template) + width: 100%; height: 100%; - align-items: center; + flex-direction: column; - .label-wrapper { + .document-wrapper { display: flex; flex-direction: row; - justify-content: center; - height: 20px; + height: 100%; + align-items: center; + + .label-wrapper { + display: flex; + flex-direction: row; + justify-content: center; + height: 20px; + } } - } - .multiRowResizer { - cursor: ns-resize; - transition: 0.5s opacity ease; - display: flex; - flex-direction: row; + .multiRowResizer { + cursor: ns-resize; + transition: 0.5s opacity ease; + display: flex; + flex-direction: row; - .multiRowResizer-hdl { - width: 100%; - height: 100%; - transition: 0.5s background-color ease; + .multiRowResizer-hdl { + width: 100%; + height: 100%; + transition: 0.5s background-color ease; + } } } } diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 3fe3d5343..bda8e91ac 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -6,9 +6,8 @@ import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; import { DocumentView } from '../../nodes/DocumentView'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionMultirowView.scss'; import HeightLabel from './MultirowHeightLabel'; import ResizeBar from './MultirowResizer'; @@ -33,7 +32,7 @@ const resizerHeight = 8; @observer export class CollectionMultirowView extends CollectionSubView() { - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @@ -193,15 +192,14 @@ export class CollectionMultirowView extends CollectionSubView() { return Transform.Identity(); // type coersion, this case should never be hit }; - @undoBatch onInternalDrop = (e: Event, de: DragManager.DropEvent) => { let dropInd = -1; - if (de.complete.docDragData && this._mainCont) { + if (de.complete.docDragData && this._contRef.current) { let curInd = -1; de.complete.docDragData?.droppedDocuments.forEach(d => { curInd = this.childDocs.indexOf(d); }); - Array.from(this._mainCont.children).forEach((child, index) => { + Array.from(this._contRef.current.children).forEach((child, index) => { const brect = child.getBoundingClientRect(); if (brect.y < de.y && brect.y + brect.height > de.y) { if (curInd !== -1 && curInd === Math.floor(index / 2)) { @@ -284,7 +282,7 @@ export class CollectionMultirowView extends CollectionSubView() { whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} addDocTab={this._props.addDocTab} pinToPres={this._props.pinToPres} - dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as any} // 'y', 'x', 'xy' + dontCenter={StrCast(this.layoutDoc.layout_dontCenter) as 'y' | 'x' | 'xy'} /> ); }; @@ -318,20 +316,23 @@ export class CollectionMultirowView extends CollectionSubView() { return collector; } + _contRef = React.createRef<HTMLDivElement>(); render() { return ( - <div - className="collectionMultirowView_contents" - style={{ - width: `calc(100% - ${2 * NumCast(this.Document._xMargin)}px)`, - height: `calc(100% - ${2 * NumCast(this.Document._yMargin)}px)`, - marginLeft: NumCast(this.Document._xMargin), - marginRight: NumCast(this.Document._xMargin), - marginTop: NumCast(this.Document._yMargin), - marginBottom: NumCast(this.Document._yMargin), - }} - ref={this.createDashEventsTarget}> - {this.contents} + <div className="collectionMultirowView_drop" ref={this.createDashEventsTarget}> + <div + ref={this._contRef} + className="collectionMultirowView_contents" + style={{ + width: `calc(100% - ${2 * NumCast(this.Document._xMargin)}px)`, + height: `calc(100% - ${2 * NumCast(this.Document._yMargin)}px)`, + marginLeft: NumCast(this.Document._xMargin), + marginRight: NumCast(this.Document._xMargin), + marginTop: NumCast(this.Document._yMargin), + marginBottom: NumCast(this.Document._yMargin), + }}> + {this.contents} + </div> </div> ); } diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx index 931e2c5e0..10a6fa2e9 100644 --- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx @@ -68,7 +68,7 @@ export default class ResizeBar extends React.Component<ResizerProps> { style={{ pointerEvents: this.props.isContentActive?.() ? 'all' : 'none', width: this.props.width, - backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor), + backgroundColor: !this.props.isContentActive?.() ? '' : (this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) as string), }}> <div className="multiColumnResizer-hdl" onPointerDown={e => this.registerResizing(e)} /> </div> diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx index cff0a8b4c..918365700 100644 --- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx +++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx @@ -66,7 +66,7 @@ export default class ResizeBar extends React.Component<ResizerProps> { style={{ pointerEvents: this.props.isContentActive?.() ? 'all' : 'none', height: this.props.height, - backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor), + backgroundColor: !this.props.isContentActive?.() ? '' : this.props.styleProvider?.(undefined, undefined, StyleProp.WidgetColor) as string, }}> <div className="multiRowResizer-hdl" onPointerDown={e => this.registerResizing(e)} /> </div> diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 51b3e1532..0076caaf8 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -4,7 +4,7 @@ import { IconButton, Popup, PopupTrigger, Size, Type } from 'browndash-component import { IReactionDisposer, ObservableMap, action, autorun, computed, makeObservable, observable, observe, override, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { ClientUtils, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils'; +import { ClientUtils, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { Doc, DocListCast, Field, FieldType, IdToDoc, NumListCast, Opt, StrListCast } from '../../../../fields/Doc'; import { DocData } from '../../../../fields/DocSymbols'; @@ -21,12 +21,12 @@ import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { StyleProp } from '../../StyleProp'; -import { DefaultStyleProvider } from '../../StyleProvider'; +import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; import { Colors } from '../../global/globalEnums'; import { DocumentView } from '../../nodes/DocumentView'; import { FieldViewProps } from '../../nodes/FieldView'; import { FocusViewOptions } from '../../nodes/FocusViewOptions'; -import { CollectionSubView } from '../CollectionSubView'; +import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; @@ -73,7 +73,7 @@ export class CollectionSchemaView extends CollectionSubView() { private _headerRefs: SchemaColumnHeader[] = []; private _eqHighlightColors: Array<[{r: number, g: number, b: number}, {r: number, g: number, b: number}]> = []; - constructor(props: any) { + constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); const lightenedColor = (r: number, g: number, b:number) => { const lightened = ClientUtils.lightenRGB(r, g, b, 165); return {r: lightened[0], g: lightened[1], b: lightened[2]}} // prettier-ignore @@ -105,7 +105,7 @@ export class CollectionSchemaView extends CollectionSubView() { @observable _columnMenuIndex: number | undefined = undefined; @observable _newFieldWarning: string = ''; @observable _makeNewField: boolean = false; - @observable _newFieldDefault: any = 0; + @observable _newFieldDefault: boolean | number | string | undefined = 0; @observable _newFieldType: ColumnType = ColumnType.Number; @observable _menuValue: string = ''; @observable _filterColumnIndex: number | undefined = undefined; @@ -200,11 +200,11 @@ export class CollectionSchemaView extends CollectionSubView() { Object.entries(this._documentOptions).forEach((pair: [string, FInfo]) => this.fieldInfos.set(pair[0], pair[1])); this._keysDisposer = observe( this.dataDoc[this.fieldKey ?? 'data'] as List<Doc>, - (change: any) => { + change => { switch (change.type) { case 'splice': // prettier-ignore - (change as any).added.forEach((doc: Doc) => // for each document added + change.added.filter(doc => doc instanceof Doc).map(doc => doc as Doc).forEach((doc: Doc) => // for each document added Doc.GetAllPrototypes(doc.value as Doc).forEach(proto => // for all of its prototypes (and itself) Object.keys(proto).forEach(action(key => // check if any of its keys are new, and add them !this.fieldInfos.get(key) && this.fieldInfos.set(key, new FInfo("-no description-", key === 'author')))))); @@ -835,7 +835,7 @@ export class CollectionSchemaView extends CollectionSubView() { }; scrollToDoc = (doc: Doc, options: FocusViewOptions) => { - const found = this._tableContentRef && Array.from(this._tableContentRef.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); + const found = this._tableContentRef && Array.from(this._tableContentRef.getElementsByClassName('documentView-node')).find(node => node.id === doc[Id]); if (found) { const rect = found.getBoundingClientRect(); const localRect = this.ScreenToLocalBoxXf().transformBounds(rect.left, rect.top, rect.width, rect.height); @@ -915,7 +915,7 @@ export class CollectionSchemaView extends CollectionSubView() { const fieldSortedAsc = (this.sortField === this.columnKeys[index] && !this.sortDesc); const fieldSortedDesc = (this.sortField === this.columnKeys[index] && this.sortDesc); const revealOptions = cm.findByDescription('Sort column') - const sortOptions: ContextMenuProps[] = revealOptions && revealOptions && 'subitems' in revealOptions ? revealOptions.subitems : []; + const sortOptions: ContextMenuProps[] = revealOptions && revealOptions && 'subitems' in revealOptions ? revealOptions.subitems ?? [] : []; sortOptions.push({ description: 'Sort A-Z', event: () => { @@ -1029,11 +1029,10 @@ export class CollectionSchemaView extends CollectionSubView() { onKeysPassiveWheel = (e: WheelEvent) => { // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - if (!this._oldKeysWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); + if (!this._oldKeysWheel?.scrollTop && e.deltaY <= 0) e.preventDefault(); e.stopPropagation(); }; - - _oldKeysWheel: any; + _oldKeysWheel: HTMLDivElement | null = null; @computed get keysDropdown() { return ( <div className="schema-key-search"> @@ -1046,6 +1045,7 @@ export class CollectionSchemaView extends CollectionSubView() { }}> {this._menuKeys.map(key => ( <div + key={key} className="schema-search-result" onPointerDown={e => { e.stopPropagation(); @@ -1116,7 +1116,7 @@ export class CollectionSchemaView extends CollectionSubView() { {this.renderFilterOptions} <div className="schema-column-menu-button" - onPointerDown={action((e: any) => { + onPointerDown={action(e => { e.stopPropagation(); this.closeFilterMenu(); })}> @@ -1404,7 +1404,7 @@ export class CollectionSchemaView extends CollectionSubView() { childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} styleProvider={DefaultStyleProvider} - containerViewPath={returnEmptyDoclist} + containerViewPath={returnEmptyDocViewList} moveDocument={this._props.moveDocument} addDocument={this.addRow} removeDocument={this._props.removeDocument} @@ -1429,7 +1429,7 @@ interface CollectionSchemaViewDocProps { @observer class CollectionSchemaViewDoc extends ObservableReactComponent<CollectionSchemaViewDocProps> { - constructor(props: any) { + constructor(props: CollectionSchemaViewDocProps) { super(props); makeObservable(this); } diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index cf4fe4ef4..e97d53edd 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -3,14 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; +import { returnEmptyFilter, returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import './CollectionSchemaView.scss'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; import { FieldViewProps } from '../../nodes/FieldView'; -import { Doc } from '../../../../fields/Doc'; +import { Doc, returnEmptyDoclist } from '../../../../fields/Doc'; import { dropActionType } from '../../../util/DropActionTypes'; import { Transform } from '../../../util/Transform'; import { SchemaTableCell } from './SchemaTableCell'; @@ -37,8 +37,8 @@ export interface SchemaColumnHeaderProps { setSort: (field: string | undefined, desc?: boolean) => void; removeColumn: (index: number) => void; rowHeight: () => number; - resizeColumn: (e: any, index: number, isRight: boolean) => void; - dragColumn: (e: any, index: number) => boolean; + resizeColumn: (e: React.PointerEvent, index: number) => void; + dragColumn: (e: PointerEvent, index: number) => boolean; openContextMenu: (x: number, y: number, index: number) => void; setColRef: (index: number, ref: HTMLDivElement) => void; rootSelected?: () => boolean; @@ -128,7 +128,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea ref={r => {this._inputRef = r; this._props.autoFocus && r?.setIsFocused(true)}} oneLine={true} allowCRs={false} - contents={undefined} + contents={''} onClick={this.openKeyDropdown} fieldContents={fieldProps} editing={undefined} @@ -231,7 +231,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea this._props.setColRef(this._props.columnIndex, col); } }}> - <div className="schema-column-resizer left" onPointerDown={e => this._props.resizeColumn(e, this._props.columnIndex, false)} /> + <div className="schema-column-resizer left" onPointerDown={e => this._props.resizeColumn(e, this._props.columnIndex)} /> <div className="schema-header-text">{this.editableView}</div> @@ -241,7 +241,7 @@ export class SchemaColumnHeader extends ObservableReactComponent<SchemaColumnHea </div> </div> - <div className="schema-column-resizer right" onPointerDown={e => this._props.resizeColumn(e, this._props.columnIndex, true)} /> + <div className="schema-column-resizer right" onPointerDown={e => this._props.resizeColumn(e, this._props.columnIndex)} /> </div> ); } diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 247739fd2..c05382ce0 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,4 +1,3 @@ -/* eslint-disable jsx-a11y/alt-text */ /* eslint-disable react/jsx-props-no-spreading */ /* eslint-disable no-use-before-define */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -10,10 +9,10 @@ import * as React from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import Select from 'react-select'; -import { ClientUtils, StopEvent, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero } from '../../../../ClientUtils'; +import { ClientUtils, StopEvent, returnEmptyFilter, returnFalse, returnZero } from '../../../../ClientUtils'; import { emptyFunction } from '../../../../Utils'; import { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Field, IdToDoc } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, IdToDoc, returnEmptyDoclist } from '../../../../fields/Doc'; import { RichTextField } from '../../../../fields/RichTextField'; import { ColumnType } from '../../../../fields/SchemaHeaderField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast, toList } from '../../../../fields/Types'; @@ -22,7 +21,7 @@ import { FInfo, FInfoFieldType } from '../../../documents/Documents'; import { dropActionType } from '../../../util/DropActionTypes'; import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch, undoable } from '../../../util/UndoManager'; +import { undoable } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DefaultStyleProvider, returnEmptyDocViewList } from '../../StyleProvider'; @@ -292,7 +291,7 @@ export class SchemaTableCell extends ObservableReactComponent<SchemaTableCellPro // mj: most of this is adapted from old schema code so I'm not sure what it does tbh @observer export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellProps> { - constructor(props: any) { + constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } @@ -359,7 +358,7 @@ export class SchemaImageCell extends ObservableReactComponent<SchemaTableCellPro @observer export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProps> { - constructor(props: any) { + constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } @@ -407,7 +406,7 @@ export class SchemaDateCell extends ObservableReactComponent<SchemaTableCellProp } @observer export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps> { - constructor(props: any) { + constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } @@ -426,7 +425,7 @@ export class SchemaRTFCell extends ObservableReactComponent<SchemaTableCellProps } @observer export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProps> { - constructor(props: any) { + constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } @@ -439,18 +438,19 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp style={{ marginRight: 4 }} type="checkbox" checked={BoolCast(this._props.Document[this._props.fieldKey])} - onChange={undoBatch((value: React.ChangeEvent<HTMLInputElement> | undefined) => { - if ((value?.nativeEvent as any).shiftKey) { + onChange={undoable((value: React.ChangeEvent<HTMLInputElement> | undefined) => { + if ((value?.nativeEvent as MouseEvent | PointerEvent).shiftKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); } else Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), (color === 'black' ? '=' : '') + (value?.target?.checked.toString() ?? '')); - })} + }, 'set bool cell')} /> + <EditableView - contents={undefined} + contents="" fieldContents={fieldProps} editing={selectedCell(this._props) ? undefined : false} GetValue={() => Field.toKeyValueString(this._props.Document, this._props.fieldKey)} - SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => { + SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this._props.setColumnValues(this._props.fieldKey.replace(/^_/, ''), value); this._props.finishEdit?.(); @@ -459,7 +459,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp const set = Doc.SetField(this._props.Document, this._props.fieldKey.replace(/^_/, ''), value, Doc.IsDataProto(this._props.Document) ? true : undefined); this._props.finishEdit?.(); return set; - })} + }, 'set bool cell')} /> </div> ); @@ -467,7 +467,7 @@ export class SchemaBoolCell extends ObservableReactComponent<SchemaTableCellProp } @observer export class SchemaEnumerationCell extends ObservableReactComponent<SchemaTableCellProps> { - constructor(props: any) { + constructor(props: SchemaTableCellProps) { super(props); makeObservable(this); } |
