diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/util/DragManager.ts | 3 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 4 | ||||
-rw-r--r-- | src/client/views/collections/CollectionNoteTakingView.tsx | 220 | ||||
-rw-r--r-- | src/client/views/collections/CollectionNoteTakingViewColumn.tsx | 53 | ||||
-rw-r--r-- | src/client/views/collections/CollectionNoteTakingViewDivider.tsx | 41 | ||||
-rw-r--r-- | src/client/views/collections/CollectionStackingView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/nodes/trails/PresElementBox.tsx | 5 | ||||
-rw-r--r-- | src/fields/SchemaHeaderField.ts | 92 | ||||
-rw-r--r-- | src/fields/util.ts | 9 |
9 files changed, 215 insertions, 214 deletions
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index d781a87ab..6386c87a0 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -457,8 +457,7 @@ export namespace DragManager { document.removeEventListener('pointerup', upHandler, true); SnappingManager.SetIsDragging(false); SnappingManager.clearSnapLines(); - const ended = batch.end(); - if (undo && ended) UndoManager.Undo(); + if (batch.end() && undo) UndoManager.Undo(); docsBeingDragged.length = 0; }); var startWindowDragTimer: any; diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index ab77af0f4..3589e014a 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -358,8 +358,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onPointerDown = (e: React.PointerEvent): void => { - DragManager.docsBeingDragged.push(...SelectionManager.Views().map(dv => dv.rootDoc)); - this._inkDragDocs = DragManager.docsBeingDragged + const views = SelectionManager.Views().map(dv => dv.rootDoc); + this._inkDragDocs = views .filter(doc => doc.type === DocumentType.INK) .map(doc => { if (InkStrokeProperties.Instance._lock) { 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; diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 624beca96..4610da4e3 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -5,11 +5,12 @@ import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { RichTextField } from '../../../fields/RichTextField'; +import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; -import { ScriptField } from '../../../fields/ScriptField'; +import { Cast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyString, setupMoveUpEvents } from '../../../Utils'; +import { returnEmptyString } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; @@ -21,13 +22,10 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { EditableView } from '../EditableView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionNoteTakingView.scss'; -import { listSpec } from '../../../fields/Schema'; -import { Cast } from '../../../fields/Types'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; -// So this is how we are storing a column interface CSVFieldColumnProps { Document: Doc; DataDoc: Opt<Doc>; @@ -38,7 +36,6 @@ interface CSVFieldColumnProps { columnHeaders: SchemaHeaderField[] | undefined; headingObject: SchemaHeaderField | undefined; yMargin: number; - // columnWidth: number; numGroupColumns: number; gridGap: number; type: 'string' | 'number' | 'bigint' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'function' | undefined; @@ -49,32 +46,32 @@ interface CSVFieldColumnProps { screenToLocalTransform: () => Transform; observeHeight: (myref: any) => void; unobserveHeight: (myref: any) => void; - //setDraggedCol:(clonedDiv:any, header:SchemaHeaderField, xycoors: ) editableViewProps: () => any; - resizeColumns: (n: number) => void; - columnStartXCoords: number[]; + resizeColumns: (isAdd: boolean, colWidth: number, colIndex: number) => boolean; PanelWidth: number; maxColWidth: number; - // docsByColumnHeader: Map<string, Doc[]> - // setDocsForColHeader: (key: string, docs: Doc[]) => void + dividerWidth: number; + availableWidth: number; } +/** + * CollectionNoteTakingViewColumn represents an individual column rendered in CollectionNoteTakingView. The + * majority of functions here are for rendering styles. + */ @observer export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColumnProps> { @observable private _background = 'inherit'; + // columnWidth returns the width of a column in absolute pixels @computed get columnWidth() { - // base cases - if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length == 1) { + if (!this.props.columnHeaders || !this.props.headingObject) { return this.props.maxColWidth; } - const i = this.props.columnHeaders.indexOf(this.props.headingObject); - if (i < 0 || i > this.props.columnStartXCoords.length - 1) { + if (this.props.columnHeaders.length == 1) { return this.props.maxColWidth; } - const endColValue = i == this.props.numGroupColumns - 1 ? this.props.PanelWidth : this.props.columnStartXCoords[i + 1]; - // TODO make the math work here. 35 is half of 70, which is the current width of the divider - return endColValue - this.props.columnStartXCoords[i] - 30; + const i = this.props.columnHeaders.indexOf(this.props.headingObject); + return this.props.columnHeaders[i].width * this.props.availableWidth; } private dropDisposer?: DragManager.DragDropDisposer; @@ -84,8 +81,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @observable _color = this.props.headingObject ? this.props.headingObject.color : '#f1efeb'; _ele: HTMLElement | null = null; - // This is likely similar to what we will be doing. Why do we need to make these refs? - // is that the only way to have drop targets? createColumnDropRef = (ele: HTMLDivElement | null) => { this.dropDisposer?.(); if (ele) { @@ -134,6 +129,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @action pointerLeave = () => (this._background = 'inherit'); textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + // addNewTextDoc is called when a user starts typing in a column to create a new node @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; @@ -146,14 +142,22 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu return this.props.addDocument?.(newDoc) || false; }; + // deleteColumn is called when a user deletes a column using the 'trash' icon in the button area. + // If the user deletes the first column, the documents get moved to the second column. Otherwise, + // all docs are added to the column directly to the left. @undoBatch @action deleteColumn = () => { const columnHeaders = Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null); if (columnHeaders && this.props.headingObject) { const index = columnHeaders.indexOf(this.props.headingObject); - this.props.docList.forEach(d => (d[this.props.pivotField] = 'unset')); + const newColIndex = index > 0 ? index - 1 : 1; + const newColHeader = this.props.columnHeaders ? this.props.columnHeaders[newColIndex] : undefined; + const newHeading = newColHeader ? newColHeader.heading : 'unset'; + this.props.docList.forEach(d => (d[this.props.pivotField] = newHeading)); + const colWidth = this.props.columnHeaders ? this.props.columnHeaders[index].width : 0; columnHeaders.splice(index, 1); + this.props.resizeColumns(false, colWidth, index); } }; @@ -255,7 +259,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu </div> </div> ) : null; - // const templatecols = `${this.props.columnWidth / this.props.numGroupColumns}px `; const templatecols = `${this.columnWidth}px `; const type = this.props.Document.type; return ( @@ -280,10 +283,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu </div> {!this.props.chromeHidden && type !== DocumentType.PRES ? ( - <div - className="collectionNoteTakingView-DocumentButtons" - // style={{ width: this.props.columnWidth / this.props.numGroupColumns, marginBottom: 10 }}> - style={{ width: this.columnWidth - 20, marginBottom: 10 }}> + <div className="collectionNoteTakingView-DocumentButtons" style={{ width: this.columnWidth - 20, marginBottom: 10 }}> <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> </div> @@ -311,7 +311,6 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu className={'collectionNoteTakingViewFieldColumn' + (SnappingManager.GetIsDragging() ? 'Dragging' : '')} key={heading} style={{ - //TODO: change this so that it's based on the column width width: this.columnWidth, background: this._background, }} diff --git a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx index 7d31b3193..8d659f790 100644 --- a/src/client/views/collections/CollectionNoteTakingViewDivider.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewDivider.tsx @@ -1,5 +1,7 @@ import { action, observable } from 'mobx'; import * as React from 'react'; +import { emptyFunction, setupMoveUpEvents } from '../../../Utils'; +import { UndoManager } from '../../util/UndoManager'; interface DividerProps { index: number; @@ -7,34 +9,35 @@ interface DividerProps { setColumnStartXCoords: (movementX: number, colIndex: number) => void; } +/** + * CollectionNoteTakingViewDivider are dividers between CollectionNoteTakingViewColumns, + * which only appear when there is more than 1 column in CollectionNoteTakingView. Dividers + * are two simple vertical lines that allow the user to alter the widths of CollectionNoteTakingViewColumns. + */ export class CollectionNoteTakingViewDivider extends React.Component<DividerProps> { @observable private isHoverActive = false; @observable private isResizingActive = false; @action private registerResizing = (e: React.PointerEvent<HTMLDivElement>) => { - e.stopPropagation(); - e.preventDefault(); - window.removeEventListener('pointermove', this.onPointerMove); - window.removeEventListener('pointerup', this.onPointerUp); - window.addEventListener('pointermove', this.onPointerMove); - window.addEventListener('pointerup', this.onPointerUp); + const batch = UndoManager.StartBatch('resizing'); + setupMoveUpEvents( + this, + e, + (e, down, delta) => { + this.props.setColumnStartXCoords(delta[0], this.props.index); + return false; + }, + action(() => { + this.isResizingActive = false; + this.isHoverActive = false; + batch.end(); + }), + emptyFunction + ); this.isResizingActive = true; }; - @action - private onPointerUp = () => { - this.isResizingActive = false; - this.isHoverActive = false; - window.removeEventListener('pointermove', this.onPointerMove); - window.removeEventListener('pointerup', this.onPointerUp); - }; - - @action - onPointerMove = ({ movementX }: PointerEvent) => { - this.props.setColumnStartXCoords(movementX, this.props.index); - }; - render() { return ( <div diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index d4efef47a..cf781b54a 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -455,8 +455,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection newDocs.filter(ndoc => docs.indexOf(ndoc) !== -1).forEach(ndoc => docs.splice(docs.indexOf(ndoc), 1)); docs.splice(insertInd - offset, 0, ...newDocs); } - // reset drag manager docs, because we just dropped - DragManager.docsBeingDragged.length = 0; } } 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' }); diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 7888d0841..91196ca21 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -256,10 +256,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerMove = (e: PointerEvent) => { const slide = this._itemRef.current!; - let dragIsPresItem: boolean = DragManager.docsBeingDragged.length > 0 ? true : false; - for (const doc of DragManager.docsBeingDragged) { - if (!doc.presentationTargetDoc) dragIsPresItem = false; - } + const dragIsPresItem = DragManager.docsBeingDragged.some(d => d.presentationTargetDoc); if (slide && dragIsPresItem) { const rect = slide.getBoundingClientRect(); const y = e.clientY - rect.top; //y position within the element. diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts index 3b02d0cfe..1321bc327 100644 --- a/src/fields/SchemaHeaderField.ts +++ b/src/fields/SchemaHeaderField.ts @@ -1,60 +1,60 @@ -import { Deserializable } from "../client/util/SerializationHelper"; -import { serializable, primitive } from "serializr"; -import { ObjectField } from "./ObjectField"; -import { Copy, ToScriptString, ToString, OnUpdate } from "./FieldSymbols"; -import { scriptingGlobal } from "../client/util/ScriptingGlobals"; -import { ColumnType } from "../client/views/collections/collectionSchema/CollectionSchemaView"; +import { Deserializable } from '../client/util/SerializationHelper'; +import { serializable, primitive } from 'serializr'; +import { ObjectField } from './ObjectField'; +import { Copy, ToScriptString, ToString, OnUpdate } from './FieldSymbols'; +import { scriptingGlobal } from '../client/util/ScriptingGlobals'; +import { ColumnType } from '../client/views/collections/collectionSchema/CollectionSchemaView'; export const PastelSchemaPalette = new Map<string, string>([ // ["pink1", "#FFB4E8"], - ["pink2", "#ff9cee"], - ["pink3", "#ffccf9"], - ["pink4", "#fcc2ff"], - ["pink5", "#f6a6ff"], - ["purple1", "#b28dff"], - ["purple2", "#c5a3ff"], - ["purple3", "#d5aaff"], - ["purple4", "#ecd4ff"], + ['pink2', '#ff9cee'], + ['pink3', '#ffccf9'], + ['pink4', '#fcc2ff'], + ['pink5', '#f6a6ff'], + ['purple1', '#b28dff'], + ['purple2', '#c5a3ff'], + ['purple3', '#d5aaff'], + ['purple4', '#ecd4ff'], // ["purple5", "#fb34ff"], - ["purple6", "#dcd3ff"], - ["purple7", "#a79aff"], - ["purple8", "#b5b9ff"], - ["purple9", "#97a2ff"], - ["bluegreen1", "#afcbff"], - ["bluegreen2", "#aff8db"], - ["bluegreen3", "#c4faf8"], - ["bluegreen4", "#85e3ff"], - ["bluegreen5", "#ace7ff"], + ['purple6', '#dcd3ff'], + ['purple7', '#a79aff'], + ['purple8', '#b5b9ff'], + ['purple9', '#97a2ff'], + ['bluegreen1', '#afcbff'], + ['bluegreen2', '#aff8db'], + ['bluegreen3', '#c4faf8'], + ['bluegreen4', '#85e3ff'], + ['bluegreen5', '#ace7ff'], // ["bluegreen6", "#6eb5ff"], - ["bluegreen7", "#bffcc6"], - ["bluegreen8", "#dbffd6"], - ["yellow1", "#f3ffe3"], - ["yellow2", "#e7ffac"], - ["yellow3", "#ffffd1"], - ["yellow4", "#fff5ba"], + ['bluegreen7', '#bffcc6'], + ['bluegreen8', '#dbffd6'], + ['yellow1', '#f3ffe3'], + ['yellow2', '#e7ffac'], + ['yellow3', '#ffffd1'], + ['yellow4', '#fff5ba'], // ["red1", "#ffc9de"], - ["red2", "#ffabab"], - ["red3", "#ffbebc"], - ["red4", "#ffcbc1"], - ["orange1", "#ffd5b3"], - ["gray", "#f1efeb"] + ['red2', '#ffabab'], + ['red3', '#ffbebc'], + ['red4', '#ffcbc1'], + ['orange1', '#ffd5b3'], + ['gray', '#f1efeb'], ]); export const RandomPastel = () => Array.from(PastelSchemaPalette.values())[Math.floor(Math.random() * PastelSchemaPalette.size)]; export const DarkPastelSchemaPalette = new Map<string, string>([ - ["pink2", "#c932b0"], - ["purple4", "#913ad6"], - ["bluegreen1", "#3978ed"], - ["bluegreen7", "#2adb3e"], - ["bluegreen5", "#21b0eb"], - ["yellow4", "#edcc0c"], - ["red2", "#eb3636"], - ["orange1", "#f2740f"], + ['pink2', '#c932b0'], + ['purple4', '#913ad6'], + ['bluegreen1', '#3978ed'], + ['bluegreen7', '#2adb3e'], + ['bluegreen5', '#21b0eb'], + ['yellow4', '#edcc0c'], + ['red2', '#eb3636'], + ['orange1', '#f2740f'], ]); @scriptingGlobal -@Deserializable("schemaheader") +@Deserializable('schemaheader') export class SchemaHeaderField extends ObjectField { @serializable(primitive()) heading: string; @@ -69,7 +69,7 @@ export class SchemaHeaderField extends ObjectField { @serializable(primitive()) desc: boolean | undefined; // boolean determines sort order, undefined when no sort - constructor(heading: string = "", color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { + constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) { super(); this.heading = heading; @@ -111,7 +111,7 @@ export class SchemaHeaderField extends ObjectField { } [Copy]() { - return new SchemaHeaderField(this.heading, this.color, this.type); + return new SchemaHeaderField(this.heading, this.color, this.type, this.width, this.desc, this.collapsed); } [ToScriptString]() { @@ -120,4 +120,4 @@ export class SchemaHeaderField extends ObjectField { [ToString]() { return `SchemaHeaderField`; } -}
\ No newline at end of file +} diff --git a/src/fields/util.ts b/src/fields/util.ts index d87bb6656..b3cbbe241 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -445,8 +445,13 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any undo: action(() => { // console.log("undo $add: " + prop, diff.items) // bcz: uncomment to log undo diff.items.forEach((item: any) => { - const ind = receiver[prop].indexOf(item.value ? item.value() : item); - ind !== -1 && receiver[prop].splice(ind, 1); + if (item instanceof SchemaHeaderField) { + const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading); + ind !== -1 && receiver[prop].splice(ind, 1); + } else { + const ind = receiver[prop].indexOf(item.value ? item.value() : item); + ind !== -1 && receiver[prop].splice(ind, 1); + } }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), |