aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--package.json7
-rw-r--r--src/client/util/DragManager.ts3
-rw-r--r--src/client/views/DocumentDecorations.tsx4
-rw-r--r--src/client/views/collections/CollectionNoteTakingView.tsx220
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewColumn.tsx53
-rw-r--r--src/client/views/collections/CollectionNoteTakingViewDivider.tsx41
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx2
-rw-r--r--src/client/views/nodes/trails/PresElementBox.tsx5
-rw-r--r--src/fields/SchemaHeaderField.ts92
-rw-r--r--src/fields/util.ts9
10 files changed, 219 insertions, 217 deletions
diff --git a/package.json b/package.json
index 0e0744a69..ccbc73a79 100644
--- a/package.json
+++ b/package.json
@@ -66,17 +66,17 @@
"@types/prosemirror-view": "^1.23.1",
"@types/rc-switch": "^1.9.2",
"@types/react": "^18.0.15",
- "@types/react-icons": "^3.0.0",
- "@types/react-reconciler": "^0.26.4",
- "@types/react-transition-group": "^4.4.5",
"@types/react-autosuggest": "^9.3.14",
"@types/react-color": "^2.17.6",
"@types/react-datepicker": "^3.1.8",
"@types/react-dom": "^18.0.6",
"@types/react-grid-layout": "^1.3.2",
+ "@types/react-icons": "^3.0.0",
"@types/react-measure": "^2.0.8",
+ "@types/react-reconciler": "^0.26.4",
"@types/react-select": "^3.1.2",
"@types/react-table": "^6.8.9",
+ "@types/react-transition-group": "^4.4.5",
"@types/request": "^2.48.8",
"@types/request-promise": "^4.1.48",
"@types/rimraf": "^2.0.5",
@@ -84,6 +84,7 @@
"@types/shelljs": "^0.8.11",
"@types/socket.io": "^2.1.13",
"@types/socket.io-client": "^1.4.36",
+ "@types/socket.io-parser": "^3.0.0",
"@types/typescript": "^2.0.0",
"@types/uuid": "^3.4.10",
"@types/valid-url": "^1.0.3",
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..b359ef420 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'));
+ setTimeout(() => 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 6c6cb64da..71834607c 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]);
}),