aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections/collectionGrid/CollectionGridView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/collections/collectionGrid/CollectionGridView.tsx')
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx426
1 files changed, 426 insertions, 0 deletions
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
new file mode 100644
index 000000000..01ad44a2d
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -0,0 +1,426 @@
+import { computed, observable, Lambda, action, autorun } from 'mobx';
+import * as React from "react";
+import { Doc, Opt } from '../../../../fields/Doc';
+import { documentSchema } from '../../../../fields/documentSchemas';
+import { makeInterface } from '../../../../fields/Schema';
+import { BoolCast, NumCast, StrCast, ScriptCast } from '../../../../fields/Types';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
+import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
+import { CollectionSubView } from '../CollectionSubView';
+import { SubCollectionViewProps } from '../CollectionSubView';
+import { returnZero, returnFalse } from '../../../../Utils';
+import Grid, { Layout } from "./Grid";
+import { Id } from '../../../../fields/FieldSymbols';
+import { observer } from 'mobx-react';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { Docs } from '../../../documents/Documents';
+import { EditableView, EditableProps } from '../../EditableView';
+import "./CollectionGridView.scss";
+import { ContextMenu } from '../../ContextMenu';
+import { ScriptField } from '../../../../fields/ScriptField';
+
+
+type GridSchema = makeInterface<[typeof documentSchema]>;
+const GridSchema = makeInterface(documentSchema);
+
+@observer
+export class CollectionGridView extends CollectionSubView(GridSchema) {
+ private containerRef: React.RefObject<HTMLDivElement>;
+ @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll
+ private changeListenerDisposer: Opt<Lambda>; // listens for changes in this.childLayoutPairs
+ private rowHeight: number = 0; // temporary store of row height to make change undoable
+ private mounted: boolean = false; // hack to fix the issue of not rerendering when mounting
+ private resetListenerDisposer: Opt<Lambda>; // listens for when the reset button is clicked
+
+ constructor(props: Readonly<SubCollectionViewProps>) {
+ super(props);
+
+ this.props.Document.numCols = NumCast(this.props.Document.numCols, 10);
+ this.props.Document.rowHeight = NumCast(this.props.Document.rowHeight, 100);
+
+ // determines whether the grid is static/flexible i.e. whether can nodes be moved around and resized or not
+ this.props.Document.flexGrid = BoolCast(this.props.Document.flexGrid, true);
+
+ // determines whether nodes should remain in position, be bound to the top, or to the left
+ this.props.Document.compactType = StrCast(this.props.Document.compactType, "vertical");
+
+ // determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them
+ this.props.Document.preventCollision = BoolCast(this.props.Document.preventCollision, false);
+
+ this.setLayout = this.setLayout.bind(this);
+ this.onSliderChange = this.onSliderChange.bind(this);
+
+ this.containerRef = React.createRef();
+ }
+
+ componentDidMount() {
+
+ console.log("mounting");
+ this.mounted = true;
+
+ this.changeListenerDisposer = computed(() => this.childLayoutPairs).observe(({ oldValue, newValue }) => {
+
+ const layouts: Layout[] = this.parsedLayoutList;
+
+ // if grid view has been opened and then exited and a document has been deleted
+ // this deletes the layout of that document from the layouts list
+ if (!oldValue && newValue.length) {
+ layouts.forEach(({ i }, index) => {
+ const targetId = i;
+ if (!newValue.find(({ layout: preserved }) => preserved[Id] === targetId)) {
+ layouts.splice(index, 1);
+ }
+ });
+ }
+
+ if (!oldValue || newValue.length > oldValue.length) {
+ // for each document that was added, add a corresponding grid layout object
+
+ newValue.forEach(({ layout }, i) => {
+ const targetId = layout[Id];
+ if (!layouts.find((gridLayout: Layout) => gridLayout.i === targetId)) {
+ layouts.push({
+ i: targetId,
+ w: 2,
+ h: 2,
+ x: 2 * (i % Math.floor(NumCast(this.props.Document.numCols) / 2)),
+ y: 2 * Math.floor(i / Math.floor(NumCast(this.props.Document.numCols) / 2)),
+ static: !this.props.Document.flexGrid
+ });
+ }
+ });
+ } else {
+ // for each document that was removed, remove its corresponding grid layout object
+ oldValue.forEach(({ layout }) => {
+ const targetId = layout[Id];
+ if (!newValue.find(({ layout: preserved }) => preserved[Id] === targetId)) {
+ const index = layouts.findIndex((gridLayout: Layout) => gridLayout.i === targetId);
+ index !== -1 && layouts.splice(index, 1);
+ }
+ });
+ }
+ this.unStringifiedLayoutList = layouts;
+ }, true);
+
+ // updates the layouts if the reset button has been clicked
+ this.resetListenerDisposer = autorun(() => {
+ if (this.props.Document.resetLayout) {
+ if (this.props.Document.flexGrid) {
+ console.log("Resetting layout");
+ const layouts: Layout[] = this.parsedLayoutList;
+
+ this.setLayout(
+ layouts.map(({ i }, index) => ({
+ i: i,
+ x: 2 * (index % Math.floor(NumCast(this.props.Document.numCols) / 2)),
+ y: 2 * Math.floor(index / Math.floor(NumCast(this.props.Document.numCols) / 2)),
+ w: 2,
+ h: 2,
+ })));
+ }
+
+ this.props.Document.resetLayout = false;
+ }
+ });
+ }
+
+ componentWillUnmount() {
+ console.clear();
+ this.mounted = false;
+ this.changeListenerDisposer && this.changeListenerDisposer();
+ this.resetListenerDisposer?.();
+ }
+
+ /**
+ * @returns the transform that will correctly place the document decorations box.
+ */
+ private lookupIndividualTransform = (layout: Layout) => {
+
+ console.log("lookup");
+
+ const index = this.childLayoutPairs.findIndex(({ layout: layoutDoc }) => layoutDoc[Id] === layout.i);
+
+ // translations depend on whether the grid is flexible or static
+ const yTranslation = (this.props.Document.flexGrid ? NumCast(layout.y) : 2 * Math.floor(index / Math.floor(NumCast(this.props.Document.numCols) / 2))) * this.rowHeightPlusGap + 10 - this._scroll + 30; // 30 is the height of the add text doc bar
+ const xTranslation = (this.props.Document.flexGrid ? NumCast(layout.x) : 2 * (index % Math.floor(NumCast(this.props.Document.numCols) / 2))) * this.colWidthPlusGap + 10;
+
+ return this.props.ScreenToLocalTransform().translate(-xTranslation, -yTranslation);
+ }
+ // is this needed? it seems to never be called
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+
+ @computed get colWidthPlusGap() { return (this.props.PanelWidth() - 10) / NumCast(this.props.Document.numCols); }
+ @computed get rowHeightPlusGap() { return NumCast(this.props.Document.rowHeight) + 10; }
+
+ /**
+ * @returns the layout list converted from JSON
+ */
+ get parsedLayoutList() {
+ console.log("parsedlayoutlist");
+ return this.props.Document.gridLayoutString ? JSON.parse(StrCast(this.props.Document.gridLayoutString)) : [];
+ }
+
+ /**
+ * Stores the layout list on the Document as JSON
+ */
+ set unStringifiedLayoutList(layouts: Layout[]) {
+
+ // sometimes there are issues with rendering when you switch from a different view
+ // where the nodes are all squeezed together on the left hand side of the screen
+ // until you click on the screen or close the chrome or interact with it in some way
+ // this seems to fix that though it isn't very elegant
+
+ console.log("setting unstringified")
+ this.mounted && (this.props.Document.gridLayoutString = "");
+ this.props.Document.gridLayoutString = JSON.stringify(layouts);
+ this.mounted = false;
+ }
+
+
+ /**
+ * Sets the width of the decorating box.
+ * @param layout
+ */
+ @observable private width = (layout: Layout) => (this.props.Document.flexGrid ? layout.w : 2) * this.colWidthPlusGap - 10;
+
+ /**
+ * Sets the height of the decorating box.
+ * @param layout
+ */
+ @observable private height = (layout: Layout) => (this.props.Document.flexGrid ? layout.h : 2) * this.rowHeightPlusGap - 10;
+
+ contextMenuItems = (layoutDoc: Doc) => {
+ const layouts: Layout[] = this.parsedLayoutList;
+ const freezeScript = ScriptField.MakeFunction(
+ // "layouts.find(({ i }) => i === layoutDoc[Id]).static=true;" +
+ // "this.unStringifiedLayoutList = layouts;" +
+ "console.log(doc)", { doc: Doc.name }
+ );
+
+ // const layouts: Layout[] = this.parsedLayoutList;
+
+ // const layoutToChange = layouts.find(({ i }) => i === layoutDoc[Id]);
+ // layoutToChange!.static = !layoutToChange!.static;
+
+ // this.unStringifiedLayoutList = layouts;
+
+ return [{ script: freezeScript!, label: "testing" }];
+ }
+
+ /**
+ *
+ * @param layout
+ * @param dxf the x- and y-translations of the decorations box as a transform i.e. this.lookupIndividualTransform
+ * @param width
+ * @param height
+ * @returns the `ContentFittingDocumentView` of the node
+ */
+ getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
+ return <ContentFittingDocumentView
+ {...this.props}
+ Document={layout}
+ DataDoc={layout.resolvedDataDoc as Doc}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ addDocTab={returnFalse}
+ backgroundColor={this.props.backgroundColor}
+ ContainingCollectionDoc={this.props.Document}
+ PanelWidth={width}
+ PanelHeight={height}
+ ScreenToLocalTransform={dxf}
+ onClick={this.onChildClickHandler}
+ renderDepth={this.props.renderDepth + 1}
+ parentActive={this.props.active}
+ display={"contents"}
+ contextMenuItems={() => this.contextMenuItems(layout)}
+ />;
+ }
+
+ /**
+ * Saves the layouts received from the Grid to the Document.
+ * @param layouts `Layout[]`
+ */
+ @undoBatch
+ @action
+ setLayout(layoutArray: Layout[]) {
+ // for every child in the collection, check to see if there's a corresponding grid layout object and
+ // updated layout object. If both exist, which they should, update the grid layout object from the updated object
+
+ console.log("settinglayout");
+
+ if (this.props.Document.flexGrid) {
+ const layouts: Layout[] = this.parsedLayoutList;
+ this.childLayoutPairs.forEach(({ layout: doc }) => {
+ let update: Opt<Layout>;
+ const targetId = doc[Id];
+ const gridLayout = layouts.find(gridLayout => gridLayout.i === targetId);
+ if (gridLayout && (update = layoutArray.find(layout => layout.i === targetId))) {
+ gridLayout.x = update.x;
+ gridLayout.y = update.y;
+ gridLayout.w = update.w;
+ gridLayout.h = update.h;
+ }
+ });
+
+ this.unStringifiedLayoutList = layouts;
+ }
+ }
+
+ /**
+ * @returns a list of `ContentFittingDocumentView`s inside wrapper divs.
+ * The key of the wrapper div must be the same as the `i` value of the corresponding layout.
+ */
+ @computed
+ private get contents(): JSX.Element[] {
+
+ console.log("getting contents");
+
+ const { childLayoutPairs } = this;
+ const collector: JSX.Element[] = [];
+ const layouts: Layout[] = this.parsedLayoutList;
+ if (!layouts.length || layouts.length !== childLayoutPairs.length) {
+ return [];
+ }
+
+ for (let i = 0; i < childLayoutPairs.length; i++) {
+ const { layout } = childLayoutPairs[i];
+ const gridLayout = layouts[i];
+ const dxf = () => this.lookupIndividualTransform(gridLayout);
+ const width = () => this.width(gridLayout);
+ const height = () => this.height(gridLayout);
+ collector.push(
+ <div className={this.props.Document.flexGrid && (this.props.isSelected() ? true : false) ? "document-wrapper" : "document-wrapper static"}
+ key={gridLayout.i}
+ // onContextMenu={() => ContextMenu.Instance.addItem({ description: "test", event: () => console.log("test"), icon: "rainbow" })}
+ >
+ {this.getDisplayDoc(layout, dxf, width, height)}
+ </div >
+ );
+ }
+
+ return collector;
+ }
+
+ /**
+ * @returns a list of `Layout` objects with attributes depending on whether the grid is flexible or static
+ */
+ get layoutList(): Layout[] {
+
+ console.log("getting layoutlist");
+ const layouts: Layout[] = this.parsedLayoutList;
+
+ return this.props.Document.flexGrid ?
+ layouts.map(({ i, x, y, w, h, static: staticVal }) => ({
+ i: i,
+ x: x + w > NumCast(this.props.Document.numCols) ? 0 : x, // handles wrapping around of nodes when numCols decreases
+ y: y,
+ w: w > NumCast(this.props.Document.numCols) ? NumCast(this.props.Document.numCols) : w, // reduces width if greater than numCols
+ h: h,
+ static: staticVal // only needed if we implement freeze in place
+ }))
+ : layouts.map(({ i }, index) => ({
+ i: i,
+ x: 2 * (index % Math.floor(NumCast(this.props.Document.numCols) / 2)),
+ y: 2 * Math.floor(index / Math.floor(NumCast(this.props.Document.numCols) / 2)),
+ w: 2,
+ h: 2,
+ static: true
+ }));
+ }
+
+ /**
+ * Handles the change in the value of the rowHeight slider.
+ */
+ onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this.props.Document.rowHeight = event.currentTarget.valueAsNumber;
+ }
+
+ /**
+ * Saves the rowHeight in a temporary variable to make it undoable later.
+ */
+ onSliderDown = () => {
+ this.rowHeight = NumCast(this.props.Document.rowHeight);
+ }
+
+ /**
+ * Uses the stored rowHeight to make the rowHeight change undoable.
+ */
+ onSliderUp = () => {
+ const tempVal = this.props.Document.rowHeight;
+ this.props.Document.rowHeight = this.rowHeight;
+ undoBatch(() => this.props.Document.rowHeight = tempVal)();
+ }
+
+ /**
+ * Creates a text document and adds it to the grid.
+ */
+ @undoBatch @action addTextDocument = (value: string) => this.props.addDocument(Docs.Create.TextDocument(value, { title: value }));
+
+ render() {
+
+ console.log("and render");
+ // for the add text document EditableView
+ const newEditableViewProps: EditableProps = {
+ GetValue: () => "",
+ SetValue: this.addTextDocument,
+ contents: "+ ADD TEXT DOCUMENT",
+ };
+
+ const childDocumentViews: JSX.Element[] = this.contents;
+ const chromeStatus = this.props.Document._chromeStatus;
+ const showChrome = (chromeStatus !== 'view-mode' && chromeStatus !== 'disabled');
+
+ return (
+ <div className="collectionGridView-contents"
+ style={{
+ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined
+ }}
+ // onContextMenu={() => ContextMenu.Instance.addItem({ description: "test", event: () => console.log("test"), icon: "rainbow" })}
+ ref={this.createDashEventsTarget}
+ onPointerDown={e => {
+ if (this.props.active(true)) {
+ if (this.props.isSelected(true)) {
+ e.stopPropagation();
+ }
+ }
+ // is the following section needed? it prevents the slider from being easily used and I'm not sure what it's preventing
+
+ // if (this.props.isSelected(true)) {
+ // !((e.target as any)?.className.includes("react-resizable-handle")) && e.preventDefault();
+ // }
+
+ }} // the grid doesn't stopPropagation when its widgets are hit, so we need to otherwise the outer documents will respond
+ >
+ {showChrome ?
+ <div className="collectionGridView-addDocumentButton">
+ <EditableView {...newEditableViewProps} />
+ </div> : null
+ }
+ <div className="collectionGridView-gridContainer"
+ ref={this.containerRef}
+ onScroll={action(e => {
+ if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll;
+ else this._scroll = e.currentTarget.scrollTop;
+ })}
+ onWheel={e => e.stopPropagation()}
+ >
+ <input className="rowHeightSlider" type="range" value={NumCast(this.props.Document.rowHeight)} onPointerDown={this.onSliderDown} onPointerUp={this.onSliderUp} onChange={this.onSliderChange} style={{ width: this.props.PanelHeight() - 40 }} min={1} max={this.props.PanelHeight() - 40} />
+ <Grid
+ width={this.props.PanelWidth()}
+ nodeList={childDocumentViews.length ? childDocumentViews : null}
+ layout={childDocumentViews.length ? this.layoutList : undefined}
+ childrenDraggable={this.props.isSelected() ? true : false}
+ numCols={NumCast(this.props.Document.numCols)}
+ rowHeight={NumCast(this.props.Document.rowHeight)}
+ setLayout={this.setLayout}
+ transformScale={this.props.ScreenToLocalTransform().Scale}
+ compactType={StrCast(this.props.Document.compactType)}
+ preventCollision={BoolCast(this.props.Document.preventCollision)}
+ />
+
+ </div>
+ </div >
+ );
+ }
+}