aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.vscode/settings.json6
-rw-r--r--package-lock.json44
-rw-r--r--package.json3
-rw-r--r--src/client/views/collections/CollectionView.tsx37
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss81
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx68
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.scss100
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx309
-rw-r--r--src/client/views/collections/collectionGrid/Grid.tsx65
-rw-r--r--src/client/views/nodes/ContentFittingDocumentView.tsx38
-rw-r--r--src/client/views/nodes/DocumentView.tsx6
11 files changed, 727 insertions, 30 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0b45c3f46..e636c9d73 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -11,5 +11,9 @@
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false,
"typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true,
"search.usePCRE2": true,
- "typescript.tsdk": "node_modules/typescript/lib"
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "python.testing.promptToConfigure": false,
+ "python.testing.pytestEnabled": false,
+ "python.testing.unittestEnabled": false,
+ "python.testing.nosetestsEnabled": false
} \ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 842553659..adf71ee58 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -963,6 +963,15 @@
"@types/react": "*"
}
},
+ "@types/react-grid-layout": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-0.17.1.tgz",
+ "integrity": "sha512-1ssQjX3X2A89jx94jECJ0Ze2EHFRYlBHjRh2pnlwjJj1WaEijXUNvwKnUzKwgNFnyZ91Pzqu9Z3V7Atzi9ge7A==",
+ "dev": true,
+ "requires": {
+ "@types/react": "*"
+ }
+ },
"@types/react-measure": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/react-measure/-/react-measure-2.0.6.tgz",
@@ -7992,6 +8001,11 @@
"resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk="
},
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+ },
"lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
@@ -14046,6 +14060,27 @@
"scheduler": "^0.19.1"
}
},
+ "react-draggable": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.3.1.tgz",
+ "integrity": "sha512-m8QeV+eIi7LhD5mXoLqDzLbokc6Ncwa0T34fF6uJzWSs4vc4fdZI/XGqHYoEn91T8S6qO+BSXslONh7Jz9VPQQ==",
+ "requires": {
+ "classnames": "^2.2.5",
+ "prop-types": "^15.6.0"
+ }
+ },
+ "react-grid-layout": {
+ "version": "0.18.3",
+ "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-0.18.3.tgz",
+ "integrity": "sha512-lHkrk941Tk5nTwZPa9uj6ttHBT0VehSHwEhWbINBJKvM1GRaFNOefvjcuxSyuCI5JWjVUP+Qm3ARt2470AlxMA==",
+ "requires": {
+ "classnames": "2.x",
+ "lodash.isequal": "^4.0.0",
+ "prop-types": "^15.0.0",
+ "react-draggable": "^4.0.0",
+ "react-resizable": "^1.9.0"
+ }
+ },
"react-image-lightbox-with-rotate": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/react-image-lightbox-with-rotate/-/react-image-lightbox-with-rotate-5.1.1.tgz",
@@ -14114,6 +14149,15 @@
}
}
},
+ "react-resizable": {
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-1.10.1.tgz",
+ "integrity": "sha512-Jd/bKOKx6+19NwC4/aMLRu/J9/krfxlDnElP41Oc+oLiUWs/zwV1S9yBfBZRnqAwQb6vQ/HRSk3bsSWGSgVbpw==",
+ "requires": {
+ "prop-types": "15.x",
+ "react-draggable": "^4.0.3"
+ }
+ },
"react-table": {
"version": "6.11.5",
"resolved": "https://registry.npmjs.org/react-table/-/react-table-6.11.5.tgz",
diff --git a/package.json b/package.json
index d1d7935af..74b7bf9d1 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"@types/react": "^16.9.19",
"@types/react-autosuggest": "^9.3.13",
"@types/react-color": "^2.17.3",
+ "@types/react-grid-layout": "^0.17.1",
"@types/react-measure": "^2.0.6",
"@types/react-table": "^6.8.6",
"@types/request": "^2.48.4",
@@ -204,6 +205,8 @@
"react-color": "^2.18.0",
"react-compound-slider": "^2.5.0",
"react-dom": "^16.12.0",
+ "react-grid-layout": "^0.18.3",
+ "react-resizable": "^1.10.1",
"react-image-lightbox-with-rotate": "^5.1.1",
"react-jsx-parser": "^1.21.0",
"react-measure": "^2.2.4",
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index fecba32c5..bcb190de4 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -43,6 +43,7 @@ import { ScriptField, ComputedField } from '../../../fields/ScriptField';
import { InteractionUtils } from '../../util/InteractionUtils';
import { ObjectField } from '../../../fields/ObjectField';
import CollectionMapView from './CollectionMapView';
+import { CollectionGridView } from './collectionGrid/CollectionGridView';
import { CollectionPileView } from './CollectionPileView';
const higflyout = require("@hig/flyout");
export const { anchorPoints } = higflyout;
@@ -66,6 +67,7 @@ export enum CollectionViewType {
Linear = "linear",
Staff = "staff",
Map = "map",
+ Grid = "grid",
Pile = "pileup"
}
export interface CollectionViewCustomProps {
@@ -90,7 +92,7 @@ export interface CollectionRenderProps {
export class CollectionView extends Touchable<FieldViewProps & CollectionViewCustomProps> {
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
- private _isChildActive = false; //TODO should this be observable?
+ _isChildActive = false; //TODO should this be observable?
get _isLightboxOpen() { return BoolCast(this.props.Document.isLightboxOpen); }
set _isLightboxOpen(value) { this.props.Document.isLightboxOpen = value; }
@observable private _curLightboxImg = 0;
@@ -192,6 +194,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView key="collview" {...props} />); }
case CollectionViewType.Time: { return (<CollectionTimeView key="collview" {...props} />); }
case CollectionViewType.Map: return (<CollectionMapView key="collview" {...props} />);
+ case CollectionViewType.Grid: return (<CollectionGridView key="gridview" {...props} />);
case CollectionViewType.Freeform:
default: { this.props.Document._freeformLayoutEngine = undefined; return (<CollectionFreeFormView key="collview" {...props} />); }
}
@@ -229,6 +232,7 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
subItems.push({ description: "Carousel", event: () => func(CollectionViewType.Carousel), icon: "columns" });
subItems.push({ description: "Pivot/Time", event: () => func(CollectionViewType.Time), icon: "columns" });
subItems.push({ description: "Map", event: () => func(CollectionViewType.Map), icon: "globe-americas" });
+ subItems.push({ description: "Grid", event: () => func(CollectionViewType.Grid), icon: "border-all" });
if (addExtras && this.props.Document._viewType === CollectionViewType.Freeform) {
subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
}
@@ -238,6 +242,37 @@ export class CollectionView extends Touchable<FieldViewProps & CollectionViewCus
onContextMenu = (e: React.MouseEvent): void => {
if (!e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+ const existingVm = ContextMenu.Instance.findByDescription("View Modes...");
+ const subItems = existingVm && "subitems" in existingVm ? existingVm.subitems : [];
+ subItems.push({ description: "Freeform", event: () => { this.props.Document._viewType = CollectionViewType.Freeform; }, icon: "signature" });
+ if (CollectionView._safeMode) {
+ ContextMenu.Instance.addItem({ description: "Test Freeform", event: () => this.props.Document._viewType = CollectionViewType.Invalid, icon: "project-diagram" });
+ }
+ subItems.push({ description: "Schema", event: () => this.props.Document._viewType = CollectionViewType.Schema, icon: "th-list" });
+ subItems.push({ description: "Treeview", event: () => this.props.Document._viewType = CollectionViewType.Tree, icon: "tree" });
+ subItems.push({ description: "Stacking", event: () => this.props.Document._viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
+ subItems.push({
+ description: "Stacking (AutoHeight)", event: () => {
+ this.props.Document._viewType = CollectionViewType.Stacking;
+ this.props.Document._autoHeight = true;
+ }, icon: "ellipsis-v"
+ });
+ subItems.push({ description: "Staff", event: () => this.props.Document._viewType = CollectionViewType.Staff, icon: "music" });
+ subItems.push({ description: "Multicolumn", event: () => this.props.Document._viewType = CollectionViewType.Multicolumn, icon: "columns" });
+ subItems.push({ description: "Multirow", event: () => this.props.Document._viewType = CollectionViewType.Multirow, icon: "columns" });
+ subItems.push({ description: "Masonry", event: () => this.props.Document._viewType = CollectionViewType.Masonry, icon: "columns" });
+ subItems.push({ description: "Carousel", event: () => this.props.Document._viewType = CollectionViewType.Carousel, icon: "columns" });
+ subItems.push({ description: "Pivot/Time", event: () => this.props.Document._viewType = CollectionViewType.Time, icon: "columns" });
+ subItems.push({ description: "Map", event: () => this.props.Document._viewType = CollectionViewType.Map, icon: "globe-americas" });
+ subItems.push({ description: "Grid", event: () => this.props.Document._viewType = CollectionViewType.Grid, icon: "rainbow" });
+ switch (this.props.Document._viewType) {
+ case CollectionViewType.Freeform: {
+ subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
+ break;
+ }
+ }
+ subItems.push({ description: "lightbox", event: action(() => this._isLightboxOpen = true), icon: "eye" });
+ !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
this.setupViewTypes("Add a Perspective...", vtype => {
const newRendition = Doc.MakeAlias(this.props.Document);
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 03bd9a01a..42ec35853 100644
--- a/src/client/views/collections/CollectionViewChromes.scss
+++ b/src/client/views/collections/CollectionViewChromes.scss
@@ -3,7 +3,7 @@
.collectionViewChrome-cont {
position: absolute;
- width:100%;
+ width: 100%;
opacity: 0.9;
z-index: 9001;
transition: top .5s;
@@ -13,7 +13,7 @@
.collectionViewChrome {
display: flex;
padding-bottom: 1px;
- height:32px;
+ height: 32px;
border-bottom: .5px solid rgb(180, 180, 180);
overflow: hidden;
@@ -35,7 +35,7 @@
outline-color: black;
}
- .collectionViewBaseChrome-button{
+ .collectionViewBaseChrome-button {
font-size: 75%;
text-transform: uppercase;
letter-spacing: 2px;
@@ -46,6 +46,7 @@
padding: 12px 10px 11px 10px;
margin-left: 10px;
}
+
.collectionViewBaseChrome-cmdPicker {
margin-left: 3px;
margin-right: 0px;
@@ -54,15 +55,17 @@
border: none;
color: grey;
}
+
.commandEntry-outerDiv {
pointer-events: all;
background-color: gray;
display: flex;
flex-direction: row;
- height:30px;
+ height: 30px;
+
.commandEntry-drop {
- color:white;
- width:25px;
+ color: white;
+ width: 25px;
margin-top: auto;
margin-bottom: auto;
}
@@ -76,15 +79,17 @@
pointer-events: all;
// margin-top: 10px;
}
+
.collectionViewBaseChrome-template,
.collectionViewBaseChrome-viewModes {
display: grid;
background: rgb(238, 238, 238);
- color:grey;
- margin-top:auto;
- margin-bottom:auto;
+ color: grey;
+ margin-top: auto;
+ margin-bottom: auto;
margin-left: 5px;
}
+
.collectionViewBaseChrome-viewModes {
margin-left: 25px;
}
@@ -92,7 +97,7 @@
.collectionViewBaseChrome-viewSpecs {
margin-left: 5px;
display: grid;
-
+
.collectionViewBaseChrome-filterIcon {
position: relative;
display: flex;
@@ -163,13 +168,31 @@
}
}
-
.collectionStackingViewChrome-cont,
.collectionTreeViewChrome-cont {
display: flex;
justify-content: space-between;
}
+ .collectionGridViewChrome-cont {
+ display: flex;
+ margin-left: 10;
+
+ .grid-control {
+ align-self: center;
+ width: 30%;
+
+ .grid-icon {
+ margin-right: 5px;
+ }
+ }
+
+ .collectionGridViewChrome-entryBox {
+ width: 50%;
+ }
+ }
+
+
.collectionStackingViewChrome-sort,
.collectionTreeViewChrome-sort {
display: flex;
@@ -199,13 +222,13 @@
.collectionTreeViewChrome-pivotField-label {
vertical-align: center;
padding-left: 10px;
- margin:auto;
+ margin: auto;
}
.collectionStackingViewChrome-pivotField,
.collectionTreeViewChrome-pivotField {
color: white;
- width:100%;
+ width: 100%;
min-width: 100px;
display: flex;
align-items: center;
@@ -215,7 +238,7 @@
input,
.editableView-container-editing-oneLine,
.editableView-container-editing {
- margin:auto;
+ margin: auto;
border: 0px;
color: grey;
text-align: center;
@@ -236,6 +259,7 @@
.collectionTreeViewChrome-pivotField:hover {
cursor: text;
}
+
}
}
@@ -244,7 +268,10 @@
display: flex;
position: relative;
align-items: center;
- .fwdKeyframe, .numKeyframe, .backKeyframe {
+
+ .fwdKeyframe,
+ .numKeyframe,
+ .backKeyframe {
cursor: pointer;
position: absolute;
width: 20;
@@ -253,26 +280,31 @@
background: gray;
display: flex;
align-items: center;
- color:white;
+ color: white;
}
+
.backKeyframe {
- left:0;
+ left: 0;
+
svg {
- display:block;
- margin:auto;
+ display: block;
+ margin: auto;
}
}
+
.numKeyframe {
- left:20;
+ left: 20;
display: flex;
flex-direction: column;
padding: 5px;
}
+
.fwdKeyframe {
- left:40;
+ left: 40;
+
svg {
- display:block;
- margin:auto;
+ display: block;
+ margin: auto;
}
}
}
@@ -334,8 +366,9 @@
flex-direction: column;
height: 40px;
}
+
.commandEntry-inputArea {
- display:flex;
+ display: flex;
flex-direction: row;
width: 150px;
margin: auto auto auto auto;
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 29a3e559a..3d712e4f5 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
-import { Doc, DocListCast } from "../../../fields/Doc";
+import { Doc, DocListCast, HeightSym } from "../../../fields/Doc";
import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec } from "../../../fields/Schema";
@@ -201,6 +201,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
case CollectionViewType.Schema: return (<CollectionSchemaViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Tree: return (<CollectionTreeViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
case CollectionViewType.Masonry: return (<CollectionStackingViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
+ case CollectionViewType.Grid: return (<CollectionGridViewChrome key="collchrome" PanelWidth={this.props.PanelWidth} CollectionView={this.props.CollectionView} type={this.props.type} />);
default: return null;
}
}
@@ -562,3 +563,68 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
}
}
+/**
+ * Chrome for grid view.
+ */
+@observer
+export class CollectionGridViewChrome extends React.Component<CollectionViewChromeProps> {
+
+ /**
+ * Sets the value of `numCols` on the grid's Document to the value entered.
+ */
+ @undoBatch
+ onNumColsEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter" || e.key === "Tab") {
+ if (e.currentTarget.valueAsNumber > 0 && this.props.CollectionView.props.Document.numCols as number !== e.currentTarget.valueAsNumber) {
+ this.props.CollectionView.props.Document.numCols = e.currentTarget.valueAsNumber;
+ //this.props.CollectionView.forceUpdate(); // to be used if CollectionGridView is not an observer
+ }
+
+ }
+ }
+
+ /**
+ * Sets the value of `rowHeight` on the grid's Document to the value entered.
+ */
+ @undoBatch
+ onRowHeightEnter = (e: React.KeyboardEvent<HTMLInputElement>) => {
+ if (e.key === "Enter" || e.key === "Tab") {
+ if (e.currentTarget.valueAsNumber > 0 && this.props.CollectionView.props.Document.rowHeight as number !== e.currentTarget.valueAsNumber) {
+ this.props.CollectionView.props.Document.rowHeight = e.currentTarget.valueAsNumber;
+ }
+ }
+ }
+
+ /**
+ * Sets whether the grid is flexible or not on the grid's Document.
+ */
+ toggleFlex = () => {
+ this.props.CollectionView.props.Document.flexGrid = !this.props.CollectionView.props.Document.flexGrid;
+ }
+
+ render() {
+ return (
+ <div className="collectionGridViewChrome-cont">
+ <span className={"grid-control"}>
+ <span className="grid-icon">
+ <FontAwesomeIcon icon="columns" size="1x" />
+ </span>
+ <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.numCols as string} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => e.currentTarget.focus()} />
+ </span>
+ <span className={"grid-control"}>
+ <span className="grid-icon">
+ <FontAwesomeIcon icon="text-height" size="1x" />
+ </span>
+ <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.props.CollectionView.props.Document.rowHeight as string} onKeyDown={this.onRowHeightEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => e.currentTarget.focus()} />
+ </span>
+ <span className={"grid-control"}>
+ <input style={{ marginRight: 5 }} type="checkbox" onClick={this.toggleFlex} defaultChecked={this.props.CollectionView.props.Document.flexGrid as boolean} />
+ <span className="grid-icon">
+ <FontAwesomeIcon icon="arrow-up" size="1x" />
+ </span>
+ <label>Flexible</label>
+ </span>
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
new file mode 100644
index 000000000..94b0d958c
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -0,0 +1,100 @@
+.collectionGridView-contents {
+ display: flex;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+
+ .collectionGridView-gridContainer {
+ height: 100%;
+ overflow-y: auto;
+ background-color: white;
+ }
+
+ .collectionGridView-addDocumentButton {
+ display: flex;
+ overflow: hidden;
+ margin: auto;
+ width: 90%;
+ cursor: text;
+
+ font-size: 75%;
+ letter-spacing: 2px;
+
+ .editableView-input {
+ outline-color: black;
+ letter-spacing: 2px;
+ color: grey;
+ border: 0px;
+ padding: 12px 10px 11px 10px;
+ }
+
+ .editableView-container-editing,
+ .editableView-container-editing-oneLine {
+ min-height: 20px;
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ height: auto;
+
+ color: grey;
+ padding: 10px;
+ width: 100%;
+
+ span::before,
+ span::after {
+ content: "";
+ width: 50%;
+ position: relative;
+ display: inline-block;
+ }
+
+ span::before {
+ margin-right: 10px;
+ }
+
+ span::after {
+ margin-left: 10px;
+ }
+
+ span {
+ position: relative;
+ text-align: center;
+ white-space: nowrap;
+ overflow: visible;
+ display: flex;
+ color: gray;
+ align-items: center;
+ }
+ }
+
+
+ }
+
+}
+
+
+// .documentDecorations-container .documentDecorations-resizer {
+// pointer-events: none;
+// }
+
+// #documentDecorations-bottomRightResizer,
+// #documentDecorations-bottomLeftResizer,
+// #documentDecorations-topRightResizer,
+// #documentDecorations-topLeftResizer {
+// visibility: collapse;
+// }
+
+// .collectionGridView-contents
+
+/* Chrome, Safari, Edge, Opera */
+input::-webkit-outer-spin-button,
+input::-webkit-inner-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+/* Firefox */
+input[type=number] {
+ -moz-appearance: textfield;
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
new file mode 100644
index 000000000..c56d6ee53
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -0,0 +1,309 @@
+import { computed, observable, Lambda, action } 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, 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 { List } from '../../../../fields/List';
+import { returnZero } from '../../../../Utils';
+import Grid, { Layout } from "./Grid";
+import { Id } from '../../../../fields/FieldSymbols';
+import { observer } from 'mobx-react';
+import { SnappingManager } from '../../../util/SnappingManager';
+import "./CollectionGridView.scss";
+import { Docs } from '../../../documents/Documents';
+import { EditableView, EditableProps } from '../../EditableView';
+
+
+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;
+ private changeListenerDisposer: Opt<Lambda>;
+
+ 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);
+ this.props.Document.flexGrid = BoolCast(this.props.Document.flexGrid, true);
+
+ this.setLayout = this.setLayout.bind(this);
+ // this.addTextDoc = this.addTextDoc.bind(this);
+
+ this.containerRef = React.createRef();
+ }
+
+ componentDidMount() {
+
+ this.changeListenerDisposer = computed(() => this.childLayoutPairs).observe(({ oldValue, newValue }) => {
+
+ const layouts: Layout[] = this.parsedLayoutList;
+ if (!oldValue || newValue.length > oldValue.length) {
+ // for each document that was added, add a corresponding grid layout document
+ newValue.forEach(({ layout }, i) => {
+ const targetId = layout[Id];
+ if (!layouts.find((gridLayout: Layout) => gridLayout.i === targetId)) {
+ console.log("pushing")
+ layouts.push({
+ i: targetId,
+ w: 2,
+ h: 2,
+ x: 2 * (i % Math.floor(this.props.Document.numCols as number / 2)),
+ y: 2 * Math.floor(i / Math.floor(this.props.Document.numCols as number / 2))
+ });
+ }
+ });
+ } else {
+ // for each document that was removed, remove its corresponding grid layout document
+ 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;
+ // this.props.Document.gridLayoutString = JSON.stringify(layouts);
+ }, true);
+ }
+
+ componentWillUnmount() {
+ this.changeListenerDisposer && this.changeListenerDisposer();
+ }
+
+ /**
+ * Establishes the x and y properties of the @param layoutDoc, currently
+ * using the @param previousLength for the computations.
+ *
+ * However, this could also be more of a first fit algorithm, iterating through
+ * this.toLayoutList(DocListCast(this.props.Document.gridLayouts)) and finding the
+ * first gap in the layout structure that suits the width and height. It would be
+ * easiest to see that a column is free (for a given row, if two documents' x are separated
+ * by a value greater than the ratio width of the document you're trying to insert),
+ * but you would then have to ensure that the next row at that column has a y at least
+ * as big as the ratio height of the document you're trying to insert.
+ */
+ private findNextLayout(layoutDoc: Doc, previousLength: number) {
+ layoutDoc.x = 2 * (previousLength % Math.floor(this.props.Document.numCols as number / 2));
+ layoutDoc.y = 2 * Math.floor(previousLength / Math.floor(this.props.Document.numCols as number / 2));
+ }
+
+ /**
+ * @returns the transform that will correctly place
+ * the document decorations box, shifted to the right by
+ * the sum of all the resolved column widths of the
+ * documents before the target.
+ */
+ private lookupIndividualTransform = (layout: Layout) => {
+
+ const index = this.childLayoutPairs.findIndex(({ layout: layoutDoc }) => layoutDoc[Id] === layout.i);
+ const yTranslation = (this.props.Document.flexGrid ? NumCast(layout.y) : 2 * Math.floor(index / Math.floor(this.props.Document.numCols as number / 2))) * this.rowHeightPlusGap + 10 - this._scroll;
+ const xTranslation = (this.props.Document.flexGrid ? NumCast(layout.x) : 2 * (index % Math.floor(this.props.Document.numCols as number / 2))) * this.colWidthPlusGap + 10;
+
+ return this.props.ScreenToLocalTransform().translate(-xTranslation, -yTranslation);
+ }
+
+ @computed get colWidthPlusGap() { return (this.props.PanelWidth() - 10) / NumCast(this.props.Document.numCols); }
+ @computed get rowHeightPlusGap() { return NumCast(this.props.Document.rowHeight) + 10; }
+
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+
+ get parsedLayoutList() { return this.props.Document.gridLayoutString ? JSON.parse(this.props.Document.gridLayoutString as string) : []; }
+ set unStringifiedLayoutList(layouts: Layout[]) { this.props.Document.gridLayoutString = JSON.stringify(layouts); }
+
+
+ /**
+ * Sets the width of the decorating box.
+ * @param Doc doc
+ */
+ @observable private width = (layout: Layout) => (this.props.Document.flexGrid ? layout.w : 2) * this.colWidthPlusGap - 10;
+
+ /**
+ * Sets the height of the decorating box.
+ * @param doc `Doc`
+ */
+ @observable private height = (layout: Layout) => (this.props.Document.flexGrid ? layout.h : 2) * this.rowHeightPlusGap - 10;
+
+ 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);
+ }
+
+ 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={this.addDocTab}
+ 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"} // this causes an issue- this is the reason the decorations box is weird with images and web boxes
+ />;
+ }
+
+ /**
+ * 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 document and
+ // updated layout object. If both exist, which they should, update the grid layout document from the updated object
+ 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);
+ // const gridLayout = DocListCast(this.props.Document.gridLayouts).find(gridLayout => StrCast(gridLayout.i) === targetId);
+ if (this.props.Document.flexGrid && 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.props.Document.gridLayoutString = JSON.stringify(layouts);
+ 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[] {
+ 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 ? "document-wrapper" : "document-wrapper static"}
+ key={gridLayout.i}
+ >
+ {this.getDisplayDoc(layout, dxf, width, height)}
+ </div>
+ );
+ }
+
+ return collector;
+ }
+
+ /**
+ * @returns a list of Layouts from a list of Docs
+ * @param docLayoutList `Doc[]`
+ */
+ get layoutList(): Layout[] {
+ const layouts: Layout[] = this.parsedLayoutList;
+
+ return this.props.Document.flexGrid ?
+ layouts : layouts.map(({ i }, index) => ({
+ i: i,
+ x: 2 * (index % Math.floor(this.props.Document.numCols as number / 2)),
+ y: 2 * Math.floor(index / Math.floor(this.props.Document.numCols as number / 2)),
+ w: 2,
+ h: 2,
+ static: true
+ }));
+ }
+
+ @undoBatch @action addTextDocument = (value: string) => this.props.addDocument(Docs.Create.TextDocument(value, { title: value }));
+
+ /**
+ * DocListCast only includes *resolved* documents, i.e. filters out promises. So even if we have a nonzero
+ * number of documents in either of these Dash lists on the document, the DocListCast version may evaluate to empty
+ * if the corresponding documents are all promises, waiting to be fetched from the server. If we don't return early
+ * in the event that promises are encountered, we might feed inaccurate data to the grid since the corresponding gridLayout
+ * documents are unresolved (or the grid may misinterpret an empty array) which has the unfortunate byproduct of triggering
+ * the setLayout event, which makes these unintended changes permanent by writing them to the likely now resolved documents.
+ */
+ render() {
+
+ const newEditableViewProps: EditableProps = {
+ GetValue: () => "",
+ SetValue: this.addTextDocument,
+ contents: "+ NEW",
+ };
+
+ 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={{
+ marginLeft: NumCast(this.props.Document._xMargin), marginRight: NumCast(this.props.Document._xMargin),
+ marginTop: NumCast(this.props.Document._yMargin), marginBottom: NumCast(this.props.Document._yMargin),
+ pointerEvents: !this.props.isSelected() && this.props.renderDepth !== 0 && !this.props.ContainingCollectionView?._isChildActive && !SnappingManager.GetIsDragging() ? "none" : undefined
+ }}
+ ref={this.createDashEventsTarget}
+ onPointerDown={e => {
+ if (this.props.active(true)) {
+ if (this.props.isSelected(true)) {
+ e.stopPropagation();
+ }
+ }
+ 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: React.UIEvent<HTMLDivElement>) => this._scroll = e.currentTarget.scrollTop)}
+ >
+ <Grid
+ width={this.props.PanelWidth()}
+ nodeList={childDocumentViews.length ? childDocumentViews : null}
+ layout={childDocumentViews.length ? this.layoutList : undefined}
+ childrenDraggable={this.props.isSelected() ? true : false}
+ numCols={this.props.Document.numCols as number}
+ rowHeight={this.props.Document.rowHeight as number}
+ setLayout={this.setLayout}
+ //setLayout={(layout: Layout[]) => this.setLayout(layout)}
+ transformScale={this.props.ScreenToLocalTransform().Scale}
+ // flex={this.props.Document.flexGrid as boolean}
+ />
+ </div>
+ </div >
+ );
+ }
+}
diff --git a/src/client/views/collections/collectionGrid/Grid.tsx b/src/client/views/collections/collectionGrid/Grid.tsx
new file mode 100644
index 000000000..cae06c4e7
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/Grid.tsx
@@ -0,0 +1,65 @@
+import * as React from 'react';
+import { observer } from "mobx-react";
+
+
+import "../../../../../node_modules/react-grid-layout/css/styles.css";
+import "../../../../../node_modules/react-resizable/css/styles.css";
+
+import * as GridLayout from 'react-grid-layout';
+import { Layout } from 'react-grid-layout';
+export { Layout } from 'react-grid-layout';
+
+
+interface GridProps {
+ width: number;
+ nodeList: JSX.Element[] | null;
+ layout: Layout[] | undefined;
+ numCols: number;
+ rowHeight: number;
+ setLayout: Function;
+ transformScale: number;
+ childrenDraggable: boolean;
+}
+
+/**
+ * Wrapper around the actual GridLayout of `react-grid-layout`.
+ */
+@observer
+export default class Grid extends React.Component<GridProps> {
+
+ constructor(props: Readonly<GridProps>) {
+ super(props);
+ this.onLayoutChange = this.onLayoutChange.bind(this);
+ }
+ /**
+ * If there has been a change in layout, calls a method in CollectionGridView to set the layouts on the Document.
+ * @param layout `Layout[]`
+ */
+ onLayoutChange(layout: Layout[]) {
+ console.log("layout changed");
+ this.props.setLayout(layout);
+ }
+
+ render() {
+ console.log(this.props.transformScale);
+ return (
+ <GridLayout className="layout"
+ layout={this.props.layout}
+ cols={this.props.numCols}
+ rowHeight={this.props.rowHeight}
+ width={this.props.width}
+ compactType={null}
+ isDroppable={true}
+ // isDraggable={this.props.childrenDraggable}
+ useCSSTransforms={true}
+ margin={[10, 10]}
+ onLayoutChange={this.onLayoutChange}
+ preventCollision={false} // change this to true later
+ // transformScale={2} // 1.2/scale
+ style={{ zIndex: 5 }}
+ >
+ {this.props.nodeList}
+ </GridLayout >
+ );
+ }
+}
diff --git a/src/client/views/nodes/ContentFittingDocumentView.tsx b/src/client/views/nodes/ContentFittingDocumentView.tsx
index a90b4668e..77555061f 100644
--- a/src/client/views/nodes/ContentFittingDocumentView.tsx
+++ b/src/client/views/nodes/ContentFittingDocumentView.tsx
@@ -9,7 +9,42 @@ import { emptyFunction, returnOne } from "../../../Utils";
import '../DocumentDecorations.scss';
import { DocumentView, DocumentViewProps } from "../nodes/DocumentView";
import "./ContentFittingDocumentView.scss";
+import { dropActionType } from "../../util/DragManager";
+import { CollectionView } from "../collections/CollectionView";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Transform } from "nodemailer/lib/xoauth2";
+interface ContentFittingDocumentViewProps {
+ Document: Doc;
+ DataDocument?: Doc;
+ LayoutDoc?: () => Opt<Doc>;
+ NativeWidth?: () => number;
+ NativeHeight?: () => number;
+ FreezeDimensions?: boolean;
+ LibraryPath: Doc[];
+ renderDepth: number;
+ fitToBox?: boolean;
+ layoutKey?: string;
+ dropAction?: dropActionType;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ focus?: (doc: Doc) => void;
+ CollectionView?: CollectionView;
+ CollectionDoc?: Doc;
+ onClick?: ScriptField;
+ backgroundColor?: (doc: Doc) => string | undefined;
+ getTransform: () => Transform;
+ addDocument?: (document: Doc) => boolean;
+ moveDocument?: (document: Doc, target: Doc | undefined, addDoc: ((doc: Doc) => boolean)) => boolean;
+ removeDocument?: (document: Doc) => boolean;
+ active: (outsideReaction: boolean) => boolean;
+ whenActiveChanged: (isActive: boolean) => void;
+ addDocTab: (document: Doc, where: string) => boolean;
+ pinToPres: (document: Doc) => void;
+ dontRegisterView?: boolean;
+ rootSelected: (outsideReaction?: boolean) => boolean;
+ Display?: string;
+}
@observer
export class ContentFittingDocumentView extends React.Component<DocumentViewProps>{
@@ -47,7 +82,8 @@ export class ContentFittingDocumentView extends React.Component<DocumentViewProp
TraceMobx();
return (<div className="contentFittingDocumentView" style={{
width: Math.abs(this.centeringYOffset) > 0.001 ? "auto" : this.props.PanelWidth(),
- height: Math.abs(this.centeringOffset) > 0.0001 ? "auto" : this.props.PanelHeight()
+ height: Math.abs(this.centeringOffset) > 0.0001 ? "auto" : this.props.PanelHeight(),
+ display: this.props.display /* just added for grid */
}}>
{!this.props.Document || !this.props.PanelWidth ? (null) : (
<div className="contentFittingDocumentView-previewDoc"
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e245e045c..334033874 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -96,6 +96,7 @@ export interface DocumentViewProps {
dontRegisterView?: boolean;
layoutKey?: string;
radialMenu?: String[];
+ display?: string;
}
@observer
@@ -516,6 +517,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || InkingControl.Instance.selectedTool === InkTool.Highlighter || InkingControl.Instance.selectedTool === InkTool.Pen)) {
if (!InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
e.stopPropagation();
+ if (SelectionManager.IsSelected(this, true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
// TODO: check here for panning/inking
}
return;
@@ -529,8 +531,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
!e.ctrlKey &&
(e.button === 0 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) &&
!this.Document.inOverlay) {
- e.stopPropagation(); // events stop at the lowest document that is active. if right dragging, we let it go through though to allow for context menu clicks. PointerMove callbacks should remove themselves if the move event gets stopPropagated by a lower-level handler (e.g, marquee drag);
-
+ e.stopPropagation();
+ if (SelectionManager.IsSelected(this, true) && this.props.Document._viewType !== CollectionViewType.Docking) e.preventDefault(); // goldenlayout needs to be able to move its tabs, so can't preventDefault for it
}
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);