import { action, computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DivHeight, lightOrDark, returnZero, smoothScroll } from '../../../ClientUtils'; import { Doc, Field, FieldType, Opt } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Copy, Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction } from '../../../Utils'; import { Docs } from '../../documents/Documents'; import { DocUtils } from '../../documents/DocUtils'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoable, undoBatch } from '../../util/UndoManager'; import { ContextMenu } from '../ContextMenu'; import { ContextMenuProps } from '../ContextMenuItem'; import { FieldsDropdown } from '../FieldsDropdown'; import { Colors } from '../global/globalEnums'; import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import { StyleProp } from '../StyleProp'; import './CollectionNoteTakingView.scss'; import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn'; import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider'; 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) * 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() { _disposers: { [key: string]: IReactionDisposer } = {}; _masonryGridRef: HTMLDivElement | null = null; _draggerRef = React.createRef(); @computed get notetakingCategoryField() { return StrCast(this.dataDoc.notetaking_column, StrCast(this.layoutDoc.pivotField, 'notetaking_column')); } public DividerWidth = 16; @observable docsDraggedRowCol: number[] = []; @observable _scroll = 0; @observable _refList: HTMLElement[] = []; constructor(props: SubCollectionViewProps) { super(props); makeObservable(this); } @computed get chromeHidden() { return BoolCast(this.layoutDoc.chromeHidden) || SnappingManager.ExploreMode; } // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns @computed get colHeaderData() { const colHeaderData = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !colHeaderData?.find(sh => sh.heading === 'unset')); if (needsUnsetCategory || colHeaderData === undefined || colHeaderData.length === 0) { setTimeout(() => { const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []); if (needsUnsetCategory || columnHeaders.length === 0) { columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); this.resizeColumns(columnHeaders); } }); } return colHeaderData ?? ([] as SchemaHeaderField[]); } @computed get headerMargin() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number; } @computed get xMargin() { return NumCast(this.layoutDoc._xMargin, 5); } @computed get yMargin() { return NumCast(this.layoutDoc._yMargin, 5); } @computed get gridGap() { return NumCast(this.layoutDoc._gridGap, 10); } // numGroupColumns returns the number of columns @computed get numGroupColumns() { return this.colHeaderData.length; } // PanelWidth returns the size of the total available space the view occupies @computed get PanelWidth() { return this._props.PanelWidth(); } // maxColWidth returns the maximum column width, which is slightly less than the total available space. @computed get maxColWidth() { return this._props.PanelWidth(); } // 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.numGroupColumns - 1; return this.maxColWidth - numDividers * this.DividerWidth - 2 * NumCast(this.layoutDoc.xMargin); } // 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(); return docs.map(d => { const height = () => this.getDocHeight(d); const width = () => this.getDocWidth(d); const style = { width: width(), marginTop: this.gridGap, height: height() }; return (
{this.getDisplayDoc(d, width)}
); }); }; // 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.colHeaderData; // 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(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); const rowCol = this.docsDraggedRowCol; // this will sort the docs into the correct columns (minus the ones you're currently dragging) docs.forEach(d => { const sectionValue = (d[this.notetakingCategoryField] as object) ?? `unset`; // 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 && columnHeaders.length > rowCol[1]) { const offset = 0; sections.get(columnHeaders[rowCol[1]])?.splice(rowCol[0] - offset, 0, ...DragManager.docsBeingDragged); } return sections; } removeDocDragHighlight = () => { setTimeout( action(() => { this.docsDraggedRowCol.length = 0; }), 100 ); }; @computed get allFieldValues() { return new Set(this.childDocs.map(doc => StrCast(doc[this.notetakingCategoryField]))); } componentDidMount() { super.componentDidMount?.(); document.addEventListener('pointerup', this.removeDocDragHighlight, true); this._disposers.autoColumns = reaction( () => (this.layoutDoc._notetaking_columns_autoCreate ? Array.from(this.allFieldValues) : undefined), columns => undoable(() => columns?.filter(col => !this.colHeaderData.some(h => h.heading === col)).forEach(col => this.addColumn(col)), 'adding columns')(), { fireImmediately: true } ); this._disposers.refList = reaction( () => ({ refList: this._refList.slice(), autoHeight: this.layoutDoc._layout_autoHeight && !DocumentView.LightboxContains(this.DocumentView?.()) }), ({ refList, autoHeight }) => { if (autoHeight) { refList.forEach(r => this.observer.observe(r)); this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight))); } else this.observer.disconnect(); }, { fireImmediately: true } ); } componentWillUnmount() { this.observer.disconnect(); document.removeEventListener('pointerup', this.removeDocDragHighlight, true); super.componentWillUnmount(); Object.keys(this._disposers).forEach(key => this._disposers[key]()); } moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean) => !!(this._props.removeDocument?.(doc) && addDocument?.(doc)); createRef = (ele: HTMLDivElement | null) => { this._masonryGridRef = ele; this.createDashEventsTarget(ele!); }; @computed get onChildClickHandler() { return () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); } @computed get onChildDoubleClickHandler() { return () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); } scrollToBottom = () => { smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease'); }; // 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 => node.id === doc[Id]); if (found) { const { top } = found.getBoundingClientRect(); const localTop = this.ScreenToLocalBoxXf().transformPoint(0, top); if (Math.floor(localTop[1]) !== 0 && Math.ceil(this._props.PanelHeight()) < (this._mainCont?.scrollHeight || 0)) { const focusSpeed = options.zoomTime ?? 500; smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc); return focusSpeed; } } return undefined; }; styleProvider = (doc: Doc | undefined, props: Opt, property: string) => { switch (property) { case StyleProp.BoxShadow: if (doc && DragManager.docsBeingDragged.includes(doc)) { return `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}`; } break; case StyleProp.Opacity: if (doc && this._props.childOpacity) { return this._props.childOpacity(); } break; default: } return this._props.styleProvider?.(doc, props, property); }; isContentActive = () => this._props.isContentActive(); blockPointerEventsWhenDragging = () => (this.docsDraggedRowCol.length ? 'none' : undefined); // getDisplayDoc returns the rules for displaying a document in this view (ie. DocumentView) getDisplayDoc(doc: Doc, width: () => number) { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this._props.TemplateDataDocument; const height = () => this.getDocHeight(doc); let dref: Opt; const noteTakingDocTransform = () => this.getDocTransform(doc, dref); return ( { dref = r || undefined; }} Document={doc} TemplateDataDocument={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} pointerEvents={this.blockPointerEventsWhenDragging} renderDepth={this._props.renderDepth + 1} PanelWidth={width} PanelHeight={height} styleProvider={this.styleProvider} containerViewPath={this.childContainerViewPath} fitWidth={this._props.childLayoutFitWidth} isContentActive={emptyFunction} onKey={this.onKeyDown} // TODO: change this from a prop to a parameter passed into a function dontHideOnDrag isDocumentActive={this.isContentActive} LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} NativeWidth={this._props.childIgnoreNativeSize ? returnZero : this._props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !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) || (doc._layout_fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} dontCenter={this._props.childIgnoreNativeSize ? 'xy' : undefined} dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this._props.dontRegisterView)} rootSelected={this.rootSelected} showTitle={this._props.childlayout_showTitle} dragAction={StrCast(this.layoutDoc.childDragAction) as dropActionType} onClickScript={this.onChildClickHandler} onDoubleClickScript={this.onChildDoubleClickHandler} ScreenToLocalTransform={noteTakingDocTransform} focus={this.focusDocument} childFilters={this.childDocFilters} hideDecorationTitle={this._props.childHideDecorationTitle} hideResizeHandles={this._props.childHideResizeHandles} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} addDocument={this._props.addDocument} moveDocument={this._props.moveDocument} removeDocument={this._props.removeDocument} contentPointerEvents={StrCast(this.layoutDoc.childContentPointerEvents) as Property.PointerEvents} whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} addDocTab={this._props.addDocTab} pinToPres={this._props.pinToPres} /> ); } // 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) { this._scroll; // required for document decorations to update when the text box container is scrolled const { translateX, translateY } = ClientUtils.GetScreenTransform(dref?.ContentDiv || undefined); // 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); } // how to get the width of a document. Currently returns the width of the column (minus margins) // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) getDocWidth = (d: Doc) => { const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as FieldType); const 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) { if (!d || d.hidden) return 0; const childLayoutDoc = Doc.Layout(d, this._props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this._props.TemplateDataDocument; const maxHeight = (lim => (lim === 0 ? this._props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d)) ? NumCast(d._width) : 0); const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d)) ? NumCast(d._height) : 0); if (nw && nh) { const docWid = this.getDocWidth(d); return Math.min(maxHeight, (docWid * nh) / nw); } const childHeight = NumCast(childLayoutDoc._height); const panelHeight = childLayoutDoc._layout_fitWidth || this._props.childLayoutFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this._props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); } // 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 = (headers: SchemaHeaderField[]) => { const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0); const scaleFactor = 1 / curWidths; this.dataDoc[this.fieldKey + '_columnHeaders'] = new List( headers.map(h => { h.setWidth(Math.abs(h.width) * scaleFactor); return h; }) ); return true; }; // 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) => { 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); // get the index for where you need to insert the doc you are currently dragging const clientY = this.ScreenToLocalBoxXf().transformPoint(ex, ey)[1]; let dropInd = -1; let pos0 = (this._refList.lastElement() as HTMLDivElement).children[0].getBoundingClientRect().height + this.yMargin * 2; colDocs.forEach((doc, i) => { let pos1 = this.getDocHeight(doc) + 2 * this.gridGap; pos1 += pos0; // updating drop position based on y coordinates const yCoordInBetween = clientY > pos0 && clientY < pos1; if (yCoordInBetween || (clientY < pos0 && i === 0)) { dropInd = i; } else if (i === colDocs.length - 1 && dropInd === -1) { dropInd = !colDocs.includes(DragManager.docsBeingDragged.lastElement()) ? i + 1 : i; } pos0 = pos1; }); // we alter the pivot fields of the docs in case they are moved to a new column. const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); DragManager.docsBeingDragged .map(doc => doc[DocData]) .forEach(d => { d[this.notetakingCategoryField] = colHeader; }); // used to notify sections to re-render this.docsDraggedRowCol.length = 0; const columnFromCoord = this.getColumnFromXCoord(xCoord); columnFromCoord !== undefined && this.docsDraggedRowCol.push(dropInd, columnFromCoord); } }; // 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 | undefined => { let colIndex: number | undefined; const numColumns = this.colHeaderData.length; const coords = []; let colStartXCoord = 0; for (let i = 0; i < numColumns; i++) { coords.push(colStartXCoord); colStartXCoord += this.colHeaderData[i].width * this.availableWidth + this.DividerWidth; } coords.push(this.PanelWidth); for (let i = 0; i < numColumns; i++) { if (xCoord > coords[i] && xCoord < coords[i + 1]) { colIndex = i; break; } } return colIndex; }; // getDocsFromXCoord returns the docs of a column based on the x-coordinate provided. getDocsFromXCoord = (xCoord: number): Doc[] => { const docsMatchingHeader: Doc[] = []; const colIndex = this.getColumnFromXCoord(xCoord); const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); this.childDocs?.forEach(d => { if (d instanceof Promise) return; const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; if (sectionValue.toString() === colHeader) { docsMatchingHeader.push(d); } }); return docsMatchingHeader; }; @undoBatch onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { if ((e.ctrlKey || fieldProps.Document._createDocOnCR) && ['Enter'].includes(e.key)) { e.stopPropagation?.(); const newDoc = Doc.MakeCopy(fieldProps.Document, true); newDoc[DocData].text = undefined; Doc.SetSelectOnLoad(newDoc); return this.addDocument?.(newDoc); } return undefined; }; // 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 onInternalDrop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.docDragData) { if (super.onInternalDrop(e, de)) { // filter out the currently dragged docs from the child docs, since we will insert them later const rowCol = this.docsDraggedRowCol; const droppedDocs = this.childDocs.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.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 if (rowCol[0] <= 0) { docs.splice(0, 0, ...newDocs); } else { const colDocs = this.getDocsFromXCoord(this.ScreenToLocalBoxXf().transformPoint(de.x, de.y)[0]); const previousDoc = colDocs[rowCol[0] - 1]; const previousDocIndex = docs.indexOf(previousDoc); docs.splice(previousDocIndex + 1, 0, ...newDocs); } } return true; } } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.Document && CollectionFreeFormDocumentView.from(de.complete.linkDragData?.linkDragView)) { const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); if (!this._props.addDocument?.(source)) e.preventDefault(); de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); return true; } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) { return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); } return false; }; @undoBatch internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData) { const dropCreator = annoDragData.dropDocCreator; annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => dropCreator(annotationOn) || this.Document; return true; } // onExternalDrop is used when dragging a document out from a CollectionNoteTakingView // to another tab/view/collection onExternalDrop = async (e: React.DragEvent): Promise => { const targInd = this.docsDraggedRowCol?.[0] || 0; const colInd = this.docsDraggedRowCol?.[1] || 0; super.onExternalDrop( e, {}, undoable( action(docus => { this.onPointerMove(true, e.clientX, e.clientY); docus?.map((doc: Doc) => this.addDocument(doc)); const newDoc = this.childDocs.lastElement(); const colHeader = colInd === undefined ? 'unset' : StrCast(this.colHeaderData[colInd].heading); newDoc[this.notetakingCategoryField] = colHeader; const docs = this.childDocList; if (docs && targInd !== -1) { docs.splice(docs.length - 1, 1); docs.splice(targInd, 0, newDoc); } this.removeDocDragHighlight(); }), 'drop into note view' ) ); }; headings = () => Array.from(this.Sections); editableViewProps = () => ({ GetValue: () => '', SetValue: this.addColumn, contents: '+ Column', }); refList = () => this._refList; backgroundColor = () => this.DocumentView?.()?.backgroundColor(); // sectionNoteTaking returns a CollectionNoteTakingViewColumn (which is an individual column) sectionNoteTaking = (heading: SchemaHeaderField | undefined, docList: Doc[]) => ( ); @undoBatch remColumn = (value: SchemaHeaderField) => { const colHdrData = Array.from(Cast(this._props.Document[this._props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); if (value) { const index = colHdrData.indexOf(value); index !== -1 && colHdrData.splice(index, 1); this.resizeColumns(colHdrData); } }; // 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 addColumn = (value: string) => { this.colHeaderData.forEach(header => { if (header.heading === value) { alert('You cannot use an existing column name. Please try a new column name'); return value; } return undefined; }); const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); const newColWidth = 1 / (this.numGroupColumns + 1); columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)); value && this.resizeColumns(columnHeaders); return true; }; removeEmptyColumns = undoable(() => { this.colHeaderData.filter(h => !this.allFieldValues.has(h.heading)).forEach(this.remColumn); }, 'remove empty Columns'); onContextMenu = (e: React.MouseEvent): void => { // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.layoutDoc._notetaking_columns_autoCreate ? 'Manually' : 'Automatically'} Create columns`, event: () => { this.layoutDoc._notetaking_columns_autoCreate = !this.layoutDoc._notetaking_columns_autoCreate; }, icon: 'computer', }); subItems.push({ description: 'Remove Empty Columns', event: this.removeEmptyColumns, icon: 'computer' }); subItems.push({ description: `${this.layoutDoc._notetaking_columns_autoSize ? 'Variable Size' : 'Autosize'} Columns`, event: () => { this.layoutDoc._notetaking_columns_autoSize = !this.layoutDoc._notetaking_columns_autoSize; }, icon: 'plus', }); subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => { this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; }, icon: 'plus', }); subItems.push({ description: 'Clear All', event: () => { this.dataDoc.data = new List([]); }, icon: 'times', }); ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' }); } }; // setColumnStartXCoords is used to update column widths when using the drag handlers between columns @action setColumnStartXCoords = (movementXScreen: number, colIndex: number) => { const movementX = this.ScreenToLocalBoxXf().transformDirection(movementXScreen, 0)[0]; const leftHeader = this.colHeaderData[colIndex]; const rightHeader = this.colHeaderData[colIndex + 1]; leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); const headers = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); headers.splice(headers.indexOf(leftHeader), 1, leftHeader[Copy]()); }; // 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(); const sections = Array.from(this.Sections.entries()); return sections.reduce((list, sec, i) => { list.push(this.sectionNoteTaking(sec[0], sec[1])); i !== sections.length - 1 && list.push(); return list; }, [] as JSX.Element[]); } @computed get nativeWidth() { return Doc.NativeWidth(this.layoutDoc); } @computed get nativeHeight() { return Doc.NativeHeight(this.layoutDoc); } @computed get scaling() { return !this.nativeWidth ? 1 : this._props.PanelHeight() / this.nativeHeight; } @computed get backgroundEvents() { return this.isContentActive() === false ? 'none' : undefined; } observer = new ResizeObserver(() => this._props.setHeight?.(this.headerMargin + Math.max(...this._refList.map(DivHeight)))); render() { TraceMobx(); return (
{ this._scroll = e.currentTarget.scrollTop; })} onPointerLeave={action(() => { this.docsDraggedRowCol.length = 0; })} onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} onDrop={this.onExternalDrop.bind(this)} onContextMenu={this.onContextMenu} onWheel={e => this._props.isContentActive() && e.stopPropagation()}> {this.renderedSections}
{ this.layoutDoc._pivotField = fieldKey; this.removeEmptyColumns(); }, 'change pivot field')} placeholder={StrCast(this.layoutDoc._pivotField)} />
); } }