aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/CollectionNoteTakingView.tsx
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2022-09-13 10:11:49 -0400
committerbobzel <zzzman@gmail.com>2022-09-13 10:11:49 -0400
commit36b17b5b0878eeb2eb23fd4c5078e06fcc002aaf (patch)
tree33c1a3fb50381d09bf89bd43d869544a3c52c7b6 /src/client/views/collections/CollectionNoteTakingView.tsx
parent7696d85b7b737a29cab189f4c65f395c5de132c7 (diff)
parentbb9f0d4dec849bdaf2d358d060707b2ed1ed677d (diff)
Merge branch 'sharing-jenny' of https://github.com/brown-dash/Dash-Web into sharing-jenny
Diffstat (limited to 'src/client/views/collections/CollectionNoteTakingView.tsx')
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx707
1 files changed, 707 insertions, 0 deletions
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
new file mode 100644
index 000000000..92c0bc341
--- /dev/null
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -0,0 +1,707 @@
+import React = require('react');
+import { CursorProperty } from 'csstype';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+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, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
+import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DragManager, dropActionType } from '../../util/DragManager';
+import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { LightboxView } from '../LightboxView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { FieldViewProps } from '../nodes/FieldView';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
+import { StyleProp } from '../StyleProvider';
+import './CollectionNoteTakingView.scss';
+import { CollectionNoteTakingViewColumn } from './CollectionNoteTakingViewColumn';
+import { CollectionNoteTakingViewDivider } from './CollectionNoteTakingViewDivider';
+import { CollectionSubView } from './CollectionSubView';
+const _global = (window /* browser */ || global) /* node */ as any;
+
+/**
+ * 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<HTMLDivElement>();
+ notetakingCategoryField = 'NotetakingCategory';
+ public DividerWidth = 16;
+ @observable docsDraggedRowCol: number[] = [];
+ @observable _cursor: CursorProperty = 'grab';
+ @observable _scroll = 0;
+ @computed get chromeHidden() {
+ return 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 = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
+ const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
+ if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
+ setTimeout(() => {
+ const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null);
+ const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset'));
+ if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) {
+ if (columnHeaders) columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1));
+ else this.dataDoc.columnHeaders = new List<SchemaHeaderField>();
+ }
+ });
+ }
+ return columnHeaders ?? ([] as SchemaHeaderField[]);
+ }
+ @computed get headerMargin() {
+ return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin);
+ }
+ @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.columnHeaders.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;
+ }
+
+ // 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, i) => {
+ const height = () => this.getDocHeight(d);
+ const width = () => this.getDocWidth(d);
+ const style = { width: width(), marginTop: this.gridGap, height: height() };
+ return (
+ <div className={`collectionNoteTakingView-columnDoc`} key={d[Id]} style={style}>
+ {this.getDisplayDoc(d, width)}
+ </div>
+ );
+ });
+ };
+
+ // 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;
+ // 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;
+ // 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 && 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
+ );
+ };
+
+ componentDidMount() {
+ super.componentDidMount?.();
+ document.addEventListener('pointerup', this.removeDocDragHighlight, true);
+ this._disposers.autoHeight = reaction(
+ () => 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', ''))))))
+ );
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('pointerup', this.removeDocDragHighlight, true);
+ super.componentWillUnmount();
+ Object.keys(this._disposers).forEach(key => this._disposers[key]());
+ }
+
+ @action
+ moveDocument = (doc: Doc, targetCollection: Doc | undefined, addDocument: (document: Doc) => boolean): boolean => {
+ return this.props.removeDocument?.(doc) && addDocument?.(doc) ? true : false;
+ };
+
+ createRef = (ele: HTMLDivElement | null) => {
+ this._masonryGridRef = ele;
+ this.createDashEventsTarget(ele!); //so the whole grid is the drop target?
+ };
+
+ @computed get onChildClickHandler() {
+ return () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
+ }
+
+ @computed get onChildDoubleClickHandler() {
+ return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
+ }
+
+ addDocTab = (doc: Doc, where: string) => {
+ if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
+ this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
+ return true;
+ }
+ return this.props.addDocTab(doc, where);
+ };
+
+ scrollToBottom = () => {
+ smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+ };
+
+ // 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) {
+ const top = found.getBoundingClientRect().top;
+ const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
+ if (Math.floor(localTop[1]) !== 0) {
+ smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+ }
+ }
+ const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
+ this.props.focus(this.rootDoc, {
+ willZoom: options?.willZoom,
+ scale: options?.scale,
+ afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
+ });
+ };
+
+ styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
+ if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) {
+ return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`;
+ }
+ if (property === StyleProp.Opacity && doc) {
+ if (this.props.childOpacity) {
+ return this.props.childOpacity();
+ }
+ }
+ return this.props.styleProvider?.(doc, props, property);
+ };
+
+ isContentActive = () => this.props.isSelected() || this.props.isContentActive();
+
+ // 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);
+ let dref: Opt<DocumentView>;
+ const noteTakingDocTransform = () => this.getDocTransform(doc, dref);
+ return (
+ <DocumentView
+ ref={r => (dref = r || undefined)}
+ Document={doc}
+ DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])}
+ renderDepth={this.props.renderDepth + 1}
+ PanelWidth={width}
+ PanelHeight={height}
+ styleProvider={this.styleProvider}
+ docViewPath={this.props.docViewPath}
+ fitWidth={this.props.childFitWidth}
+ isContentActive={emptyFunction}
+ onKey={this.onKeyDown}
+ //TODO: change this from a prop to a parameter passed into a function
+ dontHideOnDrag={true}
+ isDocumentActive={this.isContentActive}
+ LayoutTemplate={this.props.childLayoutTemplate}
+ LayoutTemplateString={this.props.childLayoutString}
+ NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox
+ NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._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.childShowTitle}
+ dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
+ onClick={this.onChildClickHandler}
+ onDoubleClick={this.onChildDoubleClickHandler}
+ ScreenToLocalTransform={noteTakingDocTransform}
+ focus={this.focusDocument}
+ docFilters={this.childDocFilters}
+ hideDecorationTitle={this.props.childHideDecorationTitle?.()}
+ hideResizeHandles={this.props.childHideResizeHandles?.()}
+ hideTitle={this.props.childHideTitle?.()}
+ docRangeFilters={this.childDocRangeFilters}
+ searchFilterDocs={this.searchFilterDocs}
+ ContainingCollectionDoc={this.props.CollectionView?.props.Document}
+ ContainingCollectionView={this.props.CollectionView}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ removeDocument={this.props.removeDocument}
+ contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
+ whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
+ addDocTab={this.addDocTab}
+ bringToFront={returnFalse}
+ scriptContext={this.props.scriptContext}
+ 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) {
+ 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);
+ // 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.props.ScreenToLocalTransform().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 Field);
+ const existingHeader = this.columnHeaders.find(sh => sh.heading === heading);
+ const existingWidth = existingHeader?.width ? existingHeader.width : 0;
+ const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth;
+ const width = d.fitWidth ? maxWidth : d[WidthSym]();
+ 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 && !d.PARAMS ? undefined : this.props.DataDoc;
+ const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1));
+ const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0);
+ const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0);
+ if (nw && nh) {
+ const docWid = this.getDocWidth(d);
+ return Math.min(maxHeight, (docWid * nh) / nw);
+ }
+ const childHeight = NumCast(childLayoutDoc._height);
+ const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(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 = (isAdd: boolean, colWidth: number, colIndex: number) => {
+ const n = this.columnHeaders.length;
+ if (n == 1) {
+ this.columnHeaders[0].setWidth(1);
+ return true;
+ }
+ 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;
+ };
+
+ // 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
+ 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
+ const clientY = this.props.ScreenToLocalTransform().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.columnHeaders[colIndex].heading);
+ DragManager.docsBeingDragged.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 = undefined;
+ const numColumns = this.columnHeaders.length;
+ 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);
+ 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.columnHeaders[colIndex].heading);
+ this.childDocs?.map(d => {
+ if (d instanceof Promise) return;
+ const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset';
+ if (sectionValue.toString() == colHeader) {
+ docsMatchingHeader.push(d);
+ }
+ });
+ return docsMatchingHeader;
+ };
+
+ @undoBatch
+ @action
+ onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => {
+ const docView = fieldProps.DocumentView?.();
+ if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) {
+ e.stopPropagation?.();
+ const newDoc = Doc.MakeCopy(docView.rootDoc, true);
+ Doc.GetProto(newDoc).text = undefined;
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ return this.addDocument?.(newDoc);
+ }
+ };
+
+ // 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) => {
+ 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.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.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.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]);
+ const previousDoc = colDocs[rowCol[0] - 1];
+ const previousDocIndex = docs.indexOf(previousDoc);
+ docs.splice(previousDocIndex + 1, 0, ...newDocs);
+ }
+ }
+ }
+ } 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
+ e.stopPropagation();
+ } 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) => {
+ const dropDoc = dropCreator(annotationOn);
+ return dropDoc || this.rootDoc;
+ };
+ return true;
+ }
+
+ // 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;
+ super.onExternalDrop(
+ e,
+ {},
+ undoBatch(
+ 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.columnHeaders[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();
+ })
+ )
+ );
+ };
+
+ 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 (
+ <CollectionNoteTakingViewColumn
+ unobserveHeight={ref => this.refList.splice(this.refList.indexOf(ref), 1)}
+ observeHeight={ref => {
+ if (ref) {
+ this.refList.push(ref);
+ this.observer = new _global.ResizeObserver(
+ action((entries: any) => {
+ if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) {
+ const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))));
+ if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) {
+ this.props.setHeight?.(height);
+ }
+ }
+ })
+ );
+ this.observer.observe(ref);
+ }
+ }}
+ addDocument={this.addDocument}
+ chromeHidden={this.chromeHidden}
+ columnHeaders={this.columnHeaders}
+ Document={this.props.Document}
+ DataDoc={this.props.DataDoc}
+ resizeColumns={this.resizeColumns}
+ renderChildren={this.children}
+ numGroupColumns={this.numGroupColumns}
+ gridGap={this.gridGap}
+ pivotField={this.notetakingCategoryField}
+ dividerWidth={this.DividerWidth}
+ maxColWidth={this.maxColWidth}
+ availableWidth={this.availableWidth}
+ PanelWidth={this.PanelWidth}
+ key={heading?.heading ?? 'unset'}
+ headings={this.headings}
+ heading={heading?.heading ?? 'unset'}
+ headingObject={heading}
+ docList={docList}
+ yMargin={this.yMargin}
+ type={type}
+ createDropTarget={this.createDashEventsTarget}
+ screenToLocalTransform={this.props.ScreenToLocalTransform}
+ editableViewProps={this.editableViewProps}
+ />
+ );
+ };
+
+ // 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) => {
+ if (this.columnHeaders) {
+ 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);
+ const newColWidth = 1 / (this.numGroupColumns + 1);
+ value && columnHeaders?.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)) && this.resizeColumns(true, newColWidth, this.columnHeaders.length - 1);
+ this.dataDoc.columnHeaders = new List<SchemaHeaderField>(columnHeaders.map(header => header[Copy]()));
+ return true;
+ };
+
+ 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._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+ subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._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.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();
+ const entries = Array.from(this.Sections.entries());
+ 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} setColumnStartXCoords={this.setColumnStartXCoords} xMargin={this.xMargin} />);
+ }
+ }
+ return eles;
+ }
+
+ @computed get buttonMenu() {
+ const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ if (menuDoc) {
+ const width = NumCast(menuDoc._width, 30);
+ const height = NumCast(menuDoc._height, 30);
+ return (
+ <div className="buttonMenu-docBtn" style={{ width, height }}>
+ <DocumentView
+ Document={menuDoc}
+ DataDoc={menuDoc}
+ isContentActive={this.props.isContentActive}
+ isDocumentActive={returnTrue}
+ addDocument={this.props.addDocument}
+ moveDocument={this.props.moveDocument}
+ addDocTab={this.props.addDocTab}
+ pinToPres={emptyFunction}
+ rootSelected={this.props.isSelected}
+ removeDocument={this.props.removeDocument}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={() => 35}
+ PanelHeight={() => 35}
+ renderDepth={this.props.renderDepth}
+ focus={emptyFunction}
+ styleProvider={this.props.styleProvider}
+ docViewPath={returnEmptyDoclist}
+ whenChildContentsActiveChanged={emptyFunction}
+ bringToFront={emptyFunction}
+ docFilters={this.props.docFilters}
+ docRangeFilters={this.props.docRangeFilters}
+ searchFilterDocs={this.props.searchFilterDocs}
+ ContainingCollectionView={undefined}
+ ContainingCollectionDoc={undefined}
+ />
+ </div>
+ );
+ }
+ }
+
+ @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 SnappingManager.GetIsDragging();
+ }
+
+ observer: any;
+
+ render() {
+ TraceMobx();
+ const buttonMenu = this.rootDoc.buttonMenu;
+ const noviceExplainer = StrCast(this.rootDoc.explainer);
+ return (
+ <>
+ {buttonMenu || noviceExplainer ? (
+ <div className="documentButtonMenu" key="buttons">
+ {buttonMenu ? this.buttonMenu : null}
+ {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null}
+ </div>
+ ) : null}
+ <div
+ className="collectionNoteTakingView"
+ ref={this.createRef}
+ key="notes"
+ style={{
+ overflowY: this.props.isContentActive() ? 'auto' : 'hidden',
+ background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor),
+ pointerEvents: this.backgroundEvents ? 'all' : undefined,
+ }}
+ onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))}
+ onPointerLeave={action(e => (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(true) && e.stopPropagation()}>
+ {this.renderedSections}
+ </div>
+ </>
+ );
+ }
+}