diff options
author | bobzel <zzzman@gmail.com> | 2022-08-24 18:21:21 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2022-08-24 18:21:21 -0400 |
commit | cc1f967b88816f99b131e8d965bc13e16cd26acf (patch) | |
tree | 49371b6727d9164815b731e2e863a0beb828258e /src/client/views/collections/CollectionNoteTakingView.tsx | |
parent | 94dbcc40067cb6637f7a535ff305d9452a3f40d1 (diff) | |
parent | 749fd6b1fff8ee8392de466ac2433f17ed093dec (diff) |
Merge branch 'parker'
Diffstat (limited to 'src/client/views/collections/CollectionNoteTakingView.tsx')
-rw-r--r-- | src/client/views/collections/CollectionNoteTakingView.tsx | 220 |
1 files changed, 110 insertions, 110 deletions
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 5c8b10ae1..81ca4dd98 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -1,6 +1,6 @@ import React = require('react'); import { CursorProperty } from 'csstype'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; @@ -37,84 +37,82 @@ export type collectionNoteTakingViewProps = { NativeHeight?: () => number; }; -//TODO: somehow need to update the mapping and then have everything else rerender. Maybe with a refresh boolean like -// in Hypermedia? - +/** + * CollectionNoteTakingView is a column-based view for displaying documents. In this view, the user can (1) + * add and remove columns (2) change column sizes and (3) move documents within and between columns. This + * view is reminiscent of Kanban-style web apps like Trello, or the 'Board' view in Notion. Each column is + * headed by a SchemaHeaderField followed by the column's documents. SchemaHeaderFields are NOT present in + * the rest of Dash, so it may be worthwhile to transition the headers to simple documents. + */ @observer export class CollectionNoteTakingView extends CollectionSubView<Partial<collectionNoteTakingViewProps>>() { _disposers: { [key: string]: IReactionDisposer } = {}; _masonryGridRef: HTMLDivElement | null = null; - _draggerRef = React.createRef<HTMLDivElement>(); // change to relative widths for deleting. change storage from columnStartXCoords to columnHeaders (schemaHeaderFields has a widgth alrady) - @observable columnStartXCoords: number[] = []; // columnHeaders -- SchemaHeaderField -- widht + _draggerRef = React.createRef<HTMLDivElement>(); @observable docsDraggedRowCol: number[] = []; @observable _cursor: CursorProperty = 'grab'; - @observable _scroll = 0; // used to force the document decoration to update when scrolling + @observable _scroll = 0; @computed get chromeHidden() { return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } + // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns @computed get columnHeaders() { - const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null)); + const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null); const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders.find(sh => sh.heading === 'unset')); - - // @#Oberable draggedColIndex = ... - //@observable cloneDivXYcoords - // @observable clonedDiv... - - // render() { - // { this.clonedDiv ? <div clone styule={{transform: clonedDivXYCoords}} : null} - // } - - // in NoteatakinView Column code, add a poinerDown handler that calls setupMoveUpEvents() which will clone the column div - // and re-render it under the cursor during move events. - // that move move event will update 2 observales -- the draggedColIndex up above, and the location of the clonedDiv so that the render in this view will know where to render the cloned div - // add observable variable that tells drag column to rnder in a different location than where the schemaHeaderFiel sa y ot. - // if (col 1 is where col 3) { - // return 3 2 1 4 56 - // } if (needsUnsetCategory) { - columnHeaders.push(new SchemaHeaderField('unset')); + columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); } return columnHeaders; } + // notetakingCategoryField returns the key to accessing a document's column value @computed get notetakingCategoryField() { return 'NotetakingCategory'; } - @computed get filteredChildren() { - return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout); - } @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 2 * Math.min(this.gridGap, 0.05 * this.props.PanelWidth())); } + // dividerWidth returns the width of a CollectionNoteTakingViewDivider + @computed get dividerWidth() { + return 32; + } @computed get yMargin() { return this.props.yPadding || NumCast(this.layoutDoc._yMargin, 5); - } // 2 * this.gridGap)); } + } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } + // numGroupColumns returns the number of columns @computed get numGroupColumns() { return this.columnHeaders.length; } + // PanelWidth returns the size of the total available space the view occupies @computed get PanelWidth() { return this.props.PanelWidth(); } - @computed get maxColWdith() { + // maxColWidth returns the maximum column width, which is slightly less than the total available space. + @computed get maxColWidth() { return this.props.PanelWidth() - 2 * this.xMargin; } + // availableWidth is the total amount of non-divider width. Since widths are stored relatively, + // we use availableWidth to convert from a percentage to a pixel count. + @computed get availableWidth() { + const numDividers = this.columnHeaders.length - 1; + return this.maxColWidth - numDividers * this.dividerWidth; + } - // If the user has not yet created any docs (in another view), this will create a single column. Otherwise, - // it will adjust according to the + // Documents should NOT have column category fields until entering this view, so the contructor creates the 'New Column' + // category for the user to then edit later. constructor(props: any) { super(props); if (this.columnHeaders === undefined) { - this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column')]); - // add all of the docs that have not been added to a column to this new column + this.dataDoc.columnHeaders = new List<SchemaHeaderField>([new SchemaHeaderField('New Column', undefined, undefined, 1)]); } } - // passed as a prop to the NoteTakingField, which uses this function + // children is passed as a prop to the NoteTakingField, which uses this function // to render the docs you see within an individual column. children = (docs: Doc[]) => { TraceMobx(); @@ -130,39 +128,30 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti }); }; + // Sections is one of the more important functions in this file, rendering the the documents + // for the UI. It properly renders documents being dragged between columns. // [CAVEATS] (1) keep track of the offsetting // (2) documentView gets unmounted as you remove it from the list @computed get Sections() { TraceMobx(); const columnHeaders = this.columnHeaders; - let docs = this.childDocs; + // filter out the currently dragged docs from the child docs, since we will insert them later + const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d)); const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); const rowCol = this.docsDraggedRowCol; - - // filter out the currently dragged docs from the child docs, since we will insert them later - if (rowCol.length && DragManager.docsBeingDragged.length) { - const docIdsToRemove = new Set(); - DragManager.docsBeingDragged.forEach(d => docIdsToRemove.add(d[Id])); - docs = docs.filter(d => !docIdsToRemove.has(d[Id])); - } - // this will sort the docs into the correct columns (minus the ones you're currently dragging) docs.map(d => { const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; - // look for if header exists already const existingHeader = columnHeaders.find(sh => sh.heading === sectionValue.toString()); if (existingHeader) { sections.get(existingHeader)!.push(d); } }); - // now we add back in the docs that we're dragging - if (rowCol.length && DragManager.docsBeingDragged.length) { - const colHeader = columnHeaders[rowCol[1]]; - // TODO: get the actual offset that occurs if the docs were in that column + if (rowCol.length) { const offset = 0; - sections.get(colHeader)?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); + sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); } return sections; } @@ -173,6 +162,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti 100 ); }; + componentDidMount() { super.componentDidMount?.(); document.addEventListener('pointerup', this.removeDocDragHighlight, true); @@ -180,11 +170,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti () => this.layoutDoc._autoHeight, autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))))) ); - this._disposers.headers = reaction( - () => this.columnHeaders.slice(), - headers => this.resizeColumns(headers.length), - { fireImmediately: true } - ); } componentWillUnmount() { @@ -206,6 +191,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti @computed get onChildClickHandler() { return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); } + @computed get onChildDoubleClickHandler() { return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } @@ -225,7 +211,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti // let's dive in and get the actual document we want to drag/move around focusDocument = (doc: Doc, options?: DocFocusOptions) => { Doc.BrushDoc(doc); - let focusSpeed = 0; const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]); if (found) { @@ -260,7 +245,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti isContentActive = () => this.props.isSelected() || this.props.isContentActive(); - // rules for displaying the documents + // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView) getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc; const height = () => this.getDocHeight(doc); @@ -316,7 +301,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti ); } - // This is used to get the coordinates of a document when we go from a view like freeform to columns + // getDocTransform is used to get the coordinates of a document when we go from a view like freeform to columns getDocTransform(doc: Doc, dref?: DocumentView) { const y = this._scroll; // required for document decorations to update when the text box container is scrolled const { scale, translateX, translateY } = Utils.GetScreenTransform(dref?.ContentDiv || undefined); @@ -329,9 +314,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti getDocWidth(d: Doc) { const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field); const existingHeader = this.columnHeaders.find(sh => sh.heading === heading); - const index = existingHeader ? this.columnHeaders.indexOf(existingHeader) : 0; - const endColValue = index === this.columnHeaders.length - 1 || index > this.columnStartXCoords.length - 1 ? this.PanelWidth : this.columnStartXCoords[index + 1]; - const maxWidth = index > this.columnStartXCoords.length - 1 ? this.PanelWidth : endColValue - this.columnStartXCoords[index] - 3 * this.xMargin; + const existingWidth = existingHeader?.width ? existingHeader.width : 0; + const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth - 2 * this.xMargin : this.maxColWidth - 2 * this.xMargin; if (d.type === DocumentType.RTF) { return maxWidth; } @@ -358,28 +342,36 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti return Math.min(childHeight, maxHeight, panelHeight); } - // called when a column is either added or deleted. This function creates n evenly spaced columns + // resizeColumns is called whenever a user adds or removes a column. When removing, + // this function renormalizes the column widths to fill the newly available space + // in the panel. When adding, this function renormalizes the existing columns to take up + // (n - 1)/n space, since the new column will be allocated 1/n of the total space. + // Column widths are relative (portion of available space) and stored in the 'width' + // field of SchemaHeaderFields. + // + // Removing example: column widths are [0.5, 0.30, 0.20] --> user deletes the final column --> column widths are [0.625, 0.375]. + // Adding example: column widths are [0.6, 0.4] --> user adds column at end --> column widths are [0.4, 0.267, 0.33] @action - resizeColumns = (n: number) => { - const totalWidth = this.PanelWidth; - const dividerWidth = 32; - const totaldividerWidth = (n - 1) * dividerWidth; - const colWidth = (totalWidth - totaldividerWidth) / n; - const newColXCoords: number[] = []; - let colStart = 0; - for (let i = 0; i < n; i++) { - newColXCoords.push(colStart); - colStart += colWidth + dividerWidth; + resizeColumns = (isAdd: boolean, colWidth: number, colIndex: number) => { + const n = this.columnHeaders.length; + if (n == 1) { + this.columnHeaders[0].setWidth(1); + return true; } - this.columnStartXCoords = newColXCoords; + const scaleFactor = isAdd ? 1 - colWidth : 1 / (1 - colWidth); + this.columnHeaders.forEach((h, i) => { + if (!(isAdd && i == colIndex)) { + h.width < 0 ? h.setWidth(1 / n) : h.setWidth(h.width * scaleFactor); + } + }); + return true; }; - // This function is used to preview where a document will drop in a column once a drag is complete. + // 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 && (this.childDocList.includes(DragManager.DocDragData?.draggedDocuments.lastElement()!) || force || this.isContentActive())) { // get the current docs for the column based on the mouse's x coordinate - // will use again later, which is why we're saving as local const xCoord = this.props.ScreenToLocalTransform().transformPoint(ex, ey)[0] - 2 * this.gridGap; const colDocs = this.getDocsFromXCoord(xCoord); // get the index for where you need to insert the doc you are currently dragging @@ -408,10 +400,16 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }; - // returns the column index for a given x-coordinate + // getColumnFromXCoord returns the column index for a given x-coordinate (currently always the client's mouse coordinate). + // This function is used to know which document a column SHOULD be in while it is being dragged. getColumnFromXCoord = (xCoord: number): number => { const numColumns = this.columnHeaders.length; - const coords = this.columnStartXCoords.slice(); + const coords = []; + let colStartXCoord = 0; + for (let i = 0; i < numColumns; i++) { + coords.push(colStartXCoord); + colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.dividerWidth; + } coords.push(this.PanelWidth); let colIndex = 0; for (let i = 0; i < numColumns; i++) { @@ -423,7 +421,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti return colIndex; }; - // returns the docs of a column based on the x-coordinate provided. + // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided. getDocsFromXCoord = (xCoord: number): Doc[] => { const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = StrCast(this.columnHeaders[colIndex].heading); @@ -455,6 +453,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }; + // onInternalDrop is used when dragging and dropping a document within the view, such as dragging + // a document to a new column or changing its order within the column. @undoBatch @action onInternalDrop = (e: Event, de: DragManager.DropEvent) => { @@ -464,14 +464,11 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti const rowCol = this.docsDraggedRowCol; const droppedDocs = this.childDocs.slice().filter((d: Doc, ind: number) => ind >= this.childDocs.length); // if the drop operation adds something to the end of the list, then use that as the new document (may be different than what was dropped e.g., in the case of a button which is dropped but which creates say, a note). const newDocs = droppedDocs.length ? droppedDocs : de.complete.docDragData.droppedDocuments; - - // const docs = this.childDocs const docs = this.childDocList; if (docs && newDocs.length) { // remove the dragged documents from the childDocList newDocs.filter(d => docs.indexOf(d) !== -1).forEach(d => docs.splice(docs.indexOf(d), 1)); // if the doc starts a columnm (or the drop index is undefined), we can just push it to the front. Otherwise we need to add it to the column properly - //TODO: you need to update childDocList instead. It seems that childDocs is a copy of the actual array we want to modify if (rowCol[0] <= 0) { docs.splice(0, 0, ...newDocs); } else { @@ -482,8 +479,7 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } } } - } // it seems like we're creating a link here. Weird. I didn't know that you could establish links by dragging - else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); this.props.addDocument?.(source); de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed @@ -502,7 +498,8 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti return true; } - // when dropping outside of the current noteTaking context (like a new tab, freeform view, etc...) + // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView + // to another tab/view/collection onExternalDrop = async (e: React.DragEvent): Promise<void> => { const targInd = this.docsDraggedRowCol?.[0] || 0; const colInd = this.docsDraggedRowCol?.[1] || 0; @@ -530,12 +527,14 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti headings = () => Array.from(this.Sections); refList: any[] = []; + editableViewProps = () => ({ GetValue: () => '', SetValue: this.addGroup, contents: '+ New Column', }); + // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => { const type = 'number'; return ( @@ -558,8 +557,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }} addDocument={this.addDocument} - // docsByColumnHeader={this._docsByColumnHeader} - // setDocsForColHeader={this.setDocsForColHeader} chromeHidden={this.chromeHidden} columnHeaders={this.columnHeaders} Document={this.props.Document} @@ -569,8 +566,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti numGroupColumns={this.numGroupColumns} gridGap={this.gridGap} pivotField={this.notetakingCategoryField} - columnStartXCoords={this.columnStartXCoords} - maxColWidth={this.maxColWdith} + dividerWidth={this.dividerWidth} + maxColWidth={this.maxColWidth} + availableWidth={this.availableWidth} PanelWidth={this.PanelWidth} key={heading?.heading ?? ''} headings={this.headings} @@ -586,19 +584,20 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti ); }; - // called when adding a new columnHeader + // addGroup is called when adding a new columnHeader, adding a SchemaHeaderField to our list of + // columnHeaders and resizing the existing columns to make room for our new one. @undoBatch @action addGroup = (value: string) => { + for (const header of this.columnHeaders) { + if (header.heading == value) { + alert('You cannot use an existing column name. Please try a new column name'); + return value; + } + } const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); - return value && columnHeaders?.push(new SchemaHeaderField(value)) ? true : false; - }; - - sortFunc = (a: [SchemaHeaderField, Doc[]], b: [SchemaHeaderField, Doc[]]): 1 | -1 => { - const descending = StrCast(this.layoutDoc._columnsSort) === 'descending'; - const firstEntry = descending ? b : a; - const secondEntry = descending ? a : b; - return firstEntry[0].heading > secondEntry[0].heading ? 1 : -1; + const newColWidth = 1 / (this.numGroupColumns + 1); + return value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1) ? true : false; }; onContextMenu = (e: React.MouseEvent): void => { @@ -612,29 +611,29 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti } }; - // used to reset column sizes when using the drag handlers + // setColumnStartXCoords is used to update column widths when using the drag handlers between columns @action - setColumnStartXCoords = (movementX: number, colIndex: number) => { - const coords = [...this.columnStartXCoords]; - coords[colIndex] += movementX; - this.columnStartXCoords = coords; + setColumnStartXCoords = (movementXScreen: number, colIndex: number) => { + const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0]; + const leftHeader = this.columnHeaders[colIndex]; + const rightHeader = this.columnHeaders[colIndex + 1]; + leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); + rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); }; + // renderedSections returns a list of all of the JSX elements used (columns and dividers). If the view + // has more than one column, those columns will be separated by a CollectionNoteTakingViewDivider that + // allows the user to adjust the column widths. @computed get renderedSections() { TraceMobx(); - // let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]]; - // if (this.pivotField) { - // const entries = Array.from(this.Sections.entries()); - // sections = this.layoutDoc._columnsSort ? entries.sort(this.sortFunc) : entries; - // } const entries = Array.from(this.Sections.entries()); - const sections = entries; //.sort(this.sortFunc); + const sections = entries; const eles: JSX.Element[] = []; for (let i = 0; i < sections.length; i++) { const col = this.sectionNoteTaking(sections[i][0], sections[i][1]); eles.push(col); if (i < sections.length - 1) { - eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i + 1} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); + eles.push(<CollectionNoteTakingViewDivider key={`divider${i}`} index={i} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />); } } return eles; @@ -642,7 +641,6 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti @computed get buttonMenu() { const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); - // TODO:glr Allow support for multiple buttons if (menuDoc) { const width: number = NumCast(menuDoc._width, 30); const height: number = NumCast(menuDoc._height, 30); @@ -693,7 +691,9 @@ export class CollectionNoteTakingView extends CollectionSubView<Partial<collecti @computed get backgroundEvents() { return SnappingManager.GetIsDragging(); } + observer: any; + render() { TraceMobx(); const buttonMenu = this.rootDoc.buttonMenu; |