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; @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll private changeListenerDisposer: Opt; // 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; // listens for when the reset button is clicked constructor(props: Readonly) { 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 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; 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(
ContextMenu.Instance.addItem({ description: "test", event: () => console.log("test"), icon: "rainbow" })} > {this.getDisplayDoc(layout, dxf, width, height)}
); } 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) => { 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 (
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 ?
: null }
{ if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll; else this._scroll = e.currentTarget.scrollTop; })} onWheel={e => e.stopPropagation()} >
); } }