aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/collections
diff options
context:
space:
mode:
authorLionel Han <47760119+IGoByJoe@users.noreply.github.com>2020-06-15 19:27:59 -0700
committerLionel Han <47760119+IGoByJoe@users.noreply.github.com>2020-06-15 19:27:59 -0700
commit099a1942440efc2784bc9b2c5896777cf85842ec (patch)
tree56a598398fab001fb6625e20fb8c9797f71c733d /src/client/views/collections
parent3cb0b3aac05d9525d6deb4683328ce1309beee32 (diff)
parent163e1f579d5a6b111645953b12e665839694585c (diff)
pull from master and cleaning up code
Diffstat (limited to 'src/client/views/collections')
-rw-r--r--src/client/views/collections/CollectionSchemaCells.tsx42
-rw-r--r--src/client/views/collections/CollectionSchemaMovableTableHOC.tsx13
-rw-r--r--src/client/views/collections/CollectionSchemaView.tsx10
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx45
-rw-r--r--src/client/views/collections/CollectionView.tsx7
-rw-r--r--src/client/views/collections/CollectionViewChromes.scss103
-rw-r--r--src/client/views/collections/CollectionViewChromes.tsx189
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx11
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx25
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx2
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.scss160
-rw-r--r--src/client/views/collections/collectionGrid/CollectionGridView.tsx307
-rw-r--r--src/client/views/collections/collectionGrid/Grid.tsx53
13 files changed, 876 insertions, 91 deletions
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 62aed67ed..baf9d4156 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -1,5 +1,5 @@
import React = require("react");
-import { action, observable } from "mobx";
+import { action, observable, trace } from "mobx";
import { observer } from "mobx-react";
import { CellInfo } from "react-table";
import "react-table/react-table.css";
@@ -23,6 +23,7 @@ import { faExpand } from '@fortawesome/free-solid-svg-icons';
import { SchemaHeaderField } from "../../../fields/SchemaHeaderField";
import { undoBatch } from "../../util/UndoManager";
import { SnappingManager } from "../../util/SnappingManager";
+import { ComputedField } from "../../../fields/ScriptField";
library.add(faExpand);
@@ -57,7 +58,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
componentDidMount() {
document.addEventListener("keydown", this.onKeyDown);
-
}
componentWillUnmount() {
@@ -70,7 +70,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
document.removeEventListener("keydown", this.onKeyDown);
this._isEditing = true;
this.props.setIsEditing(true);
-
}
}
@@ -217,7 +216,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
// <div className="collectionSchemaView-cellContents-docExpander" onPointerDown={this.expandDoc} >
// <FontAwesomeIcon icon="expand" size="sm" />
// </div>
- // );
+ // );
+ trace();
return (
<div className="collectionSchemaView-cellContainer" style={{ cursor: fieldIsDoc ? "grab" : "auto" }} ref={dragRef} onPointerDown={this.onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
@@ -231,23 +231,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
height={"auto"}
maxHeight={Number(MAX_ROW_HEIGHT)}
GetValue={() => {
- const field = props.Document[props.fieldKey];
- if (Field.IsField(field)) {
- return Field.toScriptString(field);
- }
- return "";
- }
- }
- SetValue={(value: string) => {
+ const cfield = ComputedField.WithoutComputed(() => FieldValue(props.Document[props.fieldKey]));
+ const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
+ const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1];
+ const val = cscript !== undefined ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` :
+ Field.IsField(cfield) ? Field.toScriptString(cfield) : "";
+ return val;
+ }}
+ SetValue={action((value: string) => {
+ let retVal = false;
if (value.startsWith(":=")) {
- return this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
+ retVal = this.props.setComputed(value.substring(2), props.Document, this.props.rowProps.column.id!, this.props.row, this.props.col);
+ } else {
+ const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+ if (script.compiled) {
+ retVal = this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
+ }
}
- const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
- if (!script.compiled) {
- return false;
+ if (retVal) {
+ this._isEditing = false; // need to set this here. otherwise, the assignment of the field will invalidate & cause render() to be called with the wrong value for 'editing'
+ this.props.setIsEditing(false);
}
- return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
- }}
+ return retVal;
+ })}
OnFillDown={async (value: string) => {
const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
if (script.compiled) {
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index 6f1e8ac1f..b206765e8 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -66,8 +66,9 @@ export class MovableColumn extends React.Component<MovableColumnProps> {
const rect = this._header!.current!.getBoundingClientRect();
const bounds = this.props.ScreenToLocalTransform().transformPoint(rect.left + ((rect.right - rect.left) / 2), rect.top);
const before = x[0] < bounds[0];
- if (de.complete.columnDragData) {
- this.props.reorderColumns(de.complete.columnDragData.colKey, this.props.columnValue, before, this.props.allColumns);
+ const colDragData = de.complete.columnDragData;
+ if (colDragData) {
+ this.props.reorderColumns(colDragData.colKey, this.props.columnValue, before, this.props.allColumns);
return true;
}
return false;
@@ -164,13 +165,14 @@ export class MovableRow extends React.Component<MovableRowProps> {
}
createRowDropTarget = (ele: HTMLDivElement) => {
- this._rowDropDisposer && this._rowDropDisposer();
+ this._rowDropDisposer?.();
if (ele) {
this._rowDropDisposer = DragManager.MakeDropTarget(ele, this.rowDrop.bind(this));
}
}
rowDrop = (e: Event, de: DragManager.DropEvent) => {
+ this.onPointerLeave(e as any);
const rowDoc = FieldValue(Cast(this.props.rowInfo.original, Doc));
if (!rowDoc) return false;
@@ -203,10 +205,7 @@ export class MovableRow extends React.Component<MovableRowProps> {
@action
move: DragManager.MoveFunction = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDoc) => {
const targetView = targetCollection && DocumentManager.Instance.getDocumentView(targetCollection);
- if (targetView && targetView.props.ContainingCollectionDoc) {
- return doc !== targetCollection && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
- }
- return doc !== targetCollection && this.props.removeDoc(doc) && addDoc(doc);
+ return doc !== targetCollection && doc !== targetView?.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
}
render() {
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 35f892d65..6dbee217a 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -28,7 +28,7 @@ import { CollectionSubView } from "./CollectionSubView";
import { CollectionView } from "./CollectionView";
import { ContentFittingDocumentView } from "../nodes/ContentFittingDocumentView";
import { setupMoveUpEvents, emptyFunction, returnZero, returnOne, returnFalse } from "../../../Utils";
-import { DocumentView } from "../nodes/DocumentView";
+import { SnappingManager } from "../../util/SnappingManager";
library.add(faCog, faPlus, faSortUp, faSortDown);
library.add(faTable);
@@ -186,7 +186,11 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
}
render() {
- return <div className="collectionSchemaView-container">
+ return <div className="collectionSchemaView-container"
+ style={{
+ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined,
+ width: this.props.PanelWidth() || "100%", height: this.props.PanelHeight() || "100%"
+ }} >
<div className="collectionSchemaView-tableContainer" style={{ width: `calc(100% - ${this.previewWidth()}px)` }} onPointerDown={this.onPointerDown} onWheel={e => this.props.active(true) && e.stopPropagation()} onDrop={e => this.onExternalDrop(e, {})} ref={this.createTarget}>
{this.schemaTable}
</div>
@@ -651,7 +655,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
resized={this.resized}
onResizedChange={this.onResizedChange}
SubComponent={!hasCollectionChild ? undefined : row => (row.original.type !== "collection") ? (null) :
- <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} childDocs={undefined} /></div>}
+ <div className="reactTable-sub"><SchemaTable {...this.props} Document={row.original} dataDoc={undefined} childDocs={undefined} /></div>}
/>;
}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index e891c4274..180bcdd02 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -123,7 +123,7 @@ class TreeView extends React.Component<TreeViewProps> {
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this._treedropDisposer?.();
- ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this)), this.props.document);
+ ele && (this._treedropDisposer = DragManager.MakeDropTarget(ele, this.treeDrop.bind(this), undefined, this.preTreeDrop.bind(this)), this.props.document);
}
onPointerEnter = (e: React.PointerEvent): void => {
@@ -187,33 +187,36 @@ class TreeView extends React.Component<TreeViewProps> {
})}
/>)
+ preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ const dragData = de.complete.docDragData;
+ dragData && (dragData.dropAction = this.props.treeViewId[Id] === dragData.treeViewId ? "same" : dragData.dropAction);
+ }
+
@undoBatch
treeDrop = (e: Event, de: DragManager.DropEvent) => {
const pt = [de.x, de.y];
const rect = this._header!.current!.getBoundingClientRect();
const before = pt[1] < rect.top + rect.height / 2;
const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && DocListCast(this.dataDoc[this.fieldKey]).length);
- if (de.complete.linkDragData) {
- const sourceDoc = de.complete.linkDragData.linkSourceDocument;
+ const complete = de.complete;
+ if (complete.linkDragData) {
+ const sourceDoc = complete.linkDragData.linkSourceDocument;
const destDoc = this.props.document;
DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, "tree link");
e.stopPropagation();
}
- if (de.complete.docDragData) {
+ const docDragData = complete.docDragData;
+ if (docDragData) {
e.stopPropagation();
- if (de.complete.docDragData.draggedDocuments[0] === this.props.document) return true;
- let addDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
+ if (docDragData.draggedDocuments[0] === this.props.document) return true;
+ const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
+ let addDoc = parentAddDoc;
if (inside) {
addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce(
- ((flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc)), true) || addDoc(doc);
+ (flg: boolean, doc) => flg && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc), true) || parentAddDoc(doc);
}
- const movedDocs = (de.complete.docDragData.treeViewId === this.props.treeViewId[Id] ? de.complete.docDragData.draggedDocuments : de.complete.docDragData.droppedDocuments);
- const move = de.complete.docDragData.dropAction === "move" || de.complete.docDragData.dropAction;
- return ((!move && (de.complete.docDragData.treeViewId !== this.props.treeViewId[Id])) || de.complete.docDragData.userDropAction) ?
- de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d) || added, false)
- : de.complete.docDragData.moveDocument ?
- movedDocs.reduce((added, d) => de.complete.docDragData?.moveDocument?.(d, undefined, addDoc) || added, false)
- : de.complete.docDragData.droppedDocuments.reduce((added, d) => addDoc(d), false);
+ const move = (!docDragData.dropAction || docDragData.dropAction === "move" || docDragData.dropAction === "same") && docDragData.moveDocument;
+ return docDragData.droppedDocuments.reduce((added, d) => (move ? docDragData.moveDocument?.(d, undefined, addDoc) : addDoc(d)) || added, false);
}
return false;
}
@@ -662,7 +665,16 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
protected createTreeDropTarget = (ele: HTMLDivElement) => {
this.treedropDisposer?.();
if (this._mainEle = ele) {
- this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.props.Document);
+ this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.props.Document, this.onInternalPreDrop.bind(this));
+ }
+ }
+
+ protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
+ const dragData = de.complete.docDragData;
+ if (dragData) {
+ if (targetAction && !dragData.draggedDocuments.some(d => d.context === this.props.Document && this.childDocs.includes(d))) {
+ dragData.dropAction = targetAction;
+ } else dragData.dropAction = this.props.Document[Id] === dragData?.treeViewId ? "same" : dragData.dropAction;
}
}
@@ -788,7 +800,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
background: this.props.backgroundColor?.(this.props.Document),
paddingLeft: `${NumCast(this.props.Document._xPadding, 10)}px`,
paddingRight: `${NumCast(this.props.Document._xPadding, 10)}px`,
- paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px`
+ paddingTop: `${NumCast(this.props.Document._yPadding, 20)}px`,
+ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined
}}
onKeyPress={this.onKeyPress}
onContextMenu={this.onContextMenu}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 00a1d27f0..9f5308ce8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -42,6 +42,7 @@ import { CollectionStaffView } from './CollectionStaffView';
import { SubCollectionViewProps } from './CollectionSubView';
import { CollectionTimeView } from './CollectionTimeView';
import { CollectionTreeView } from "./CollectionTreeView";
+import { CollectionGridView } from './collectionGrid/CollectionGridView';
import './CollectionView.scss';
import { CollectionViewBaseChrome } from './CollectionViewChromes';
const higflyout = require("@hig/flyout");
@@ -67,6 +68,7 @@ export enum CollectionViewType {
Linear = "linear",
Staff = "staff",
Map = "map",
+ Grid = "grid",
Pile = "pileup"
}
export interface CollectionViewCustomProps {
@@ -91,7 +93,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;
@@ -194,6 +196,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} />); }
}
@@ -231,6 +234,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: "th-list" });
if (addExtras && this.props.Document._viewType === CollectionViewType.Freeform) {
subItems.push({ description: "Custom", icon: "fingerprint", event: AddCustomFreeFormLayout(this.props.Document, this.props.fieldKey) });
}
@@ -240,7 +244,6 @@ 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
-
this.setupViewTypes("Add a Perspective...", vtype => {
const newRendition = Doc.MakeAlias(this.props.Document);
newRendition._viewType = vtype;
diff --git a/src/client/views/collections/CollectionViewChromes.scss b/src/client/views/collections/CollectionViewChromes.scss
index 03bd9a01a..f85cbfee2 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,53 @@
}
}
-
.collectionStackingViewChrome-cont,
.collectionTreeViewChrome-cont {
display: flex;
justify-content: space-between;
}
+ .collectionGridViewChrome-cont {
+ display: flex;
+ margin-left: 10;
+
+ .collectionGridViewChrome-viewPicker {
+ font-size: 75%;
+ //text-transform: uppercase;
+ //letter-spacing: 2px;
+ background: rgb(238, 238, 238);
+ color: grey;
+ outline-color: black;
+ border: none;
+ //padding: 12px 10px 11px 10px;
+ }
+
+ .collectionGridViewChrome-viewPicker:active {
+ outline-color: black;
+ }
+
+ .grid-control {
+ align-self: center;
+ display: flex;
+ flex-direction: row;
+ margin-right: 5px;
+
+ .grid-icon {
+ margin-right: 5px;
+ align-self: center;
+ }
+
+ .flexLabel {
+ margin-bottom: 0;
+ }
+ }
+
+ .collectionGridViewChrome-entryBox {
+ width: 50%;
+ }
+ }
+
+
.collectionStackingViewChrome-sort,
.collectionTreeViewChrome-sort {
display: flex;
@@ -199,13 +244,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 +260,7 @@
input,
.editableView-container-editing-oneLine,
.editableView-container-editing {
- margin:auto;
+ margin: auto;
border: 0px;
color: grey;
text-align: center;
@@ -236,6 +281,7 @@
.collectionTreeViewChrome-pivotField:hover {
cursor: text;
}
+
}
}
@@ -244,7 +290,10 @@
display: flex;
position: relative;
align-items: center;
- .fwdKeyframe, .numKeyframe, .backKeyframe {
+
+ .fwdKeyframe,
+ .numKeyframe,
+ .backKeyframe {
cursor: pointer;
position: absolute;
width: 20;
@@ -253,26 +302,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 +388,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 3dc740c25..48810f1e9 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -1,8 +1,8 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { action, computed, observable, runInAction, Lambda } from "mobx";
import { observer } from "mobx-react";
import * as React from "react";
-import { Doc, DocListCast } from "../../../fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../fields/Doc";
import { Id } from "../../../fields/FieldSymbols";
import { List } from "../../../fields/List";
import { listSpec } from "../../../fields/Schema";
@@ -16,7 +16,6 @@ import { CollectionViewType } from "./CollectionView";
import { CollectionView } from "./CollectionView";
import "./CollectionViewChromes.scss";
import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-const datepicker = require('js-datepicker');
interface CollectionViewChromeProps {
CollectionView: CollectionView;
@@ -201,6 +200,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;
}
}
@@ -220,8 +220,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
@undoBatch
@action
protected drop(e: Event, de: DragManager.DropEvent): boolean {
- if (de.complete.docDragData && de.complete.docDragData.draggedDocuments.length) {
- this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(de.complete.docDragData?.draggedDocuments || []));
+ const docDragData = de.complete.docDragData;
+ if (docDragData?.draggedDocuments.length) {
+ this._buttonizableCommands.filter(c => c.title === this._currentKey).map(c => c.immediate(docDragData.draggedDocuments || []));
e.stopPropagation();
}
return true;
@@ -562,3 +563,181 @@ export class CollectionTreeViewChrome extends React.Component<CollectionViewChro
}
}
+/**
+ * Chrome for grid view.
+ */
+@observer
+export class CollectionGridViewChrome extends React.Component<CollectionViewChromeProps> {
+
+ private clicked: boolean = false;
+ private entered: boolean = false;
+ private decrementLimitReached: boolean = false;
+ @observable private resize = false;
+ private resizeListenerDisposer: Opt<Lambda>;
+
+ componentDidMount() {
+
+ runInAction(() => this.resize = this.props.CollectionView.props.PanelWidth() < 700);
+
+ // listener to reduce text on chrome resize (panel resize)
+ this.resizeListenerDisposer = computed(() => this.props.CollectionView.props.PanelWidth()).observe(({ newValue }) => {
+ runInAction(() => this.resize = newValue < 700);
+ });
+ }
+
+ componentWillUnmount() {
+ this.resizeListenerDisposer?.();
+ }
+
+ get numCols() { return NumCast(this.props.CollectionView.props.Document.gridNumCols, 10); }
+
+ /**
+ * 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.gridNumCols = e.currentTarget.valueAsNumber;
+ }
+
+ }
+ }
+
+ /**
+ * 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.
+ */
+ @undoBatch
+ toggleFlex = () => {
+ this.props.CollectionView.props.Document.gridFlex = !BoolCast(this.props.CollectionView.props.Document.gridFlex, true);
+ }
+
+ /**
+ * Increments the value of numCols on button click
+ */
+ onIncrementButtonClick = () => {
+ this.clicked = true;
+ this.entered && (this.props.CollectionView.props.Document.gridNumCols as number)--;
+ undoBatch(() => this.props.CollectionView.props.Document.gridNumCols = this.numCols + 1)();
+ this.entered = false;
+ }
+
+ /**
+ * Decrements the value of numCols on button click
+ */
+ onDecrementButtonClick = () => {
+ this.clicked = true;
+ if (!this.decrementLimitReached) {
+ this.entered && (this.props.CollectionView.props.Document.gridNumCols as number)++;
+ undoBatch(() => this.props.CollectionView.props.Document.gridNumCols = this.numCols - 1)();
+ }
+ this.entered = false;
+ }
+
+ /**
+ * Increments the value of numCols on button hover
+ */
+ incrementValue = () => {
+ this.entered = true;
+ if (!this.clicked && !this.decrementLimitReached) {
+ this.props.CollectionView.props.Document.gridNumCols = this.numCols + 1;
+ }
+ this.decrementLimitReached = false;
+ this.clicked = false;
+ }
+
+ /**
+ * Decrements the value of numCols on button hover
+ */
+ decrementValue = () => {
+ this.entered = true;
+ if (!this.clicked) {
+ if (this.numCols !== 1) {
+ this.props.CollectionView.props.Document.gridNumCols = this.numCols - 1;
+ }
+ else {
+ this.decrementLimitReached = true;
+ }
+ }
+
+ this.clicked = false;
+ }
+
+ /**
+ * Toggles the value of preventCollision
+ */
+ toggleCollisions = () => {
+ this.props.CollectionView.props.Document.gridPreventCollision = !this.props.CollectionView.props.Document.gridPreventCollision;
+ }
+
+ /**
+ * Changes the value of the compactType
+ */
+ changeCompactType = (e: React.ChangeEvent<HTMLSelectElement>) => {
+ // need to change startCompaction so that this operation will be undoable.
+ this.props.CollectionView.props.Document.gridStartCompaction = e.target.selectedOptions[0].value;
+ }
+
+ render() {
+ return (
+ <div className="collectionGridViewChrome-cont" >
+ <span className="grid-control" style={{ width: this.resize ? "25%" : "30%" }}>
+ <span className="grid-icon">
+ <FontAwesomeIcon icon="columns" size="1x" />
+ </span>
+ <input className="collectionGridViewChrome-entryBox" type="number" placeholder={this.numCols.toString()} onKeyDown={this.onNumColsEnter} onClick={(e: React.MouseEvent<HTMLInputElement, MouseEvent>) => { e.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
+ <input className="columnButton" onClick={this.onIncrementButtonClick} onMouseEnter={this.incrementValue} onMouseLeave={this.decrementValue} type="button" value="↑" />
+ <input className="columnButton" style={{ marginRight: 5 }} onClick={this.onDecrementButtonClick} onMouseEnter={this.decrementValue} onMouseLeave={this.incrementValue} type="button" value="↓" />
+ </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.stopPropagation(); e.preventDefault(); e.currentTarget.focus(); }} />
+ </span> */}
+ <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}>
+ <input type="checkbox" style={{ marginRight: 5 }} onChange={this.toggleCollisions} checked={!this.props.CollectionView.props.Document.gridPreventCollision} />
+ <label className="flexLabel">{this.resize ? "Coll" : "Collisions"}</label>
+ </span>
+
+ <select className="collectionGridViewChrome-viewPicker"
+ style={{ marginRight: 5 }}
+ onPointerDown={stopPropagation}
+ onChange={this.changeCompactType}
+ value={StrCast(this.props.CollectionView.props.Document.gridStartCompaction, StrCast(this.props.CollectionView.props.Document.gridCompaction))}>
+ {["vertical", "horizontal", "none"].map(type =>
+ <option className="collectionGridViewChrome-viewOption"
+ onPointerDown={stopPropagation}
+ value={type}>
+ {this.resize ? type[0].toUpperCase() + type.substring(1) : "Compact: " + type}
+ </option>
+ )}
+ </select>
+
+ <span className="grid-control" style={{ width: this.resize ? "12%" : "20%" }}>
+ <input style={{ marginRight: 5 }} type="checkbox" onChange={this.toggleFlex}
+ checked={BoolCast(this.props.CollectionView.props.Document.gridFlex, true)} />
+ <label className="flexLabel">{this.resize ? "Flex" : "Flexible"}</label>
+ </span>
+
+ <button onClick={() => this.props.CollectionView.props.Document.gridResetLayout = true}>
+ {!this.resize ? "Reset" :
+ <FontAwesomeIcon icon="redo-alt" size="1x" />}
+ </button>
+
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f3fc04752..6cac39f77 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -46,10 +46,15 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const bfield = afield === "anchor1" ? "anchor2" : "anchor1";
// really hacky stuff to make the LinkAnchorBox display where we want it to:
- // if there's an element in the DOM with the id of the opposite anchor, then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
+ // if there's an element in the DOM with a classname containing the link's id and a targetids attribute containing the other end of the link,
+ // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right
// otherwise, we just use the computed nearest point on the document boundary to the target Document
- const targetAhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][afield] as Doc)[Id]);
- const targetBhyperlink = window.document.getElementById(this.props.LinkDocs[0][Id] + (this.props.LinkDocs[0][bfield] as Doc)[Id]);
+ const linkId = this.props.LinkDocs[0][Id]; // this link's Id
+ const AanchorId = (this.props.LinkDocs[0][afield] as Doc)[Id]; // anchor a's id
+ const BanchorId = (this.props.LinkDocs[0][bfield] as Doc)[Id]; // anchor b's id
+ const linkEles = Array.from(window.document.getElementsByClassName(linkId));
+ const targetAhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(AanchorId));
+ const targetBhyperlink = linkEles.find((ele: any) => ele.getAttribute("targetids")?.includes(BanchorId));
if (!targetBhyperlink) {
this.props.A.props.Document[afield + "_x"] = (apt.point.x - abounds.left) / abounds.width * 100;
this.props.A.props.Document[afield + "_y"] = (apt.point.y - abounds.top) / abounds.height * 100;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aee158a2a..cddac01ad 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -52,7 +52,6 @@ library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressAr
export const panZoomSchema = createSchema({
_panX: "number",
_panY: "number",
- scale: "number",
currentTimecode: "number",
displayTimecode: "number",
currentFrame: "number",
@@ -76,6 +75,7 @@ export type collectionFreeformViewProps = {
forceScaling?: boolean; // whether to force scaling of content (needed by ImageBox)
viewDefDivClick?: ScriptField;
childPointerEvents?: boolean;
+ scaleField?: string;
};
@observer
@@ -108,6 +108,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@computed get nativeWidth() { return this.fitToContent ? 0 : NumCast(this.Document._nativeWidth, this.props.NativeWidth()); }
@computed get nativeHeight() { return this.fitToContent ? 0 : NumCast(this.Document._nativeHeight, this.props.NativeHeight()); }
private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
+ private get scaleFieldKey() { return this.props.scaleField || "scale"; }
private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
private easing = () => this.props.Document.panTransformType === "Ease";
private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0;
@@ -115,14 +116,14 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ?
Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y),
this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) :
- this.Document.scale || 1)
+ NumCast(this.Document[this.scaleFieldKey], 1))
@computed get cachedCenteringShiftX(): number {
- const scaling = this.fitToContent ? 1 : this.contentScaling;
+ const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0; // shift so pan position is at center of window for non-overlay collections
}
@computed get cachedCenteringShiftY(): number {
- const scaling = this.fitToContent ? 1 : this.contentScaling;
+ const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling;
return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections
}
@computed get cachedGetLocalTransform(): Transform {
@@ -779,7 +780,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) {
const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40);
- this.props.Document.scale = Math.abs(safeScale);
+ this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
}
}
@@ -795,7 +796,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (!e.ctrlKey && MarqueeView.DragMarquee) this.setPan(this.panX() + e.deltaX, this.panY() + e.deltaY, "None", true);
else this.zoom(e.clientX, e.clientY, e.deltaY);
}
- this.props.Document.targetScale = NumCast(this.props.Document.scale);
+ this.props.Document.targetScale = NumCast(this.props.Document[this.scaleFieldKey]);
}
@action
@@ -852,7 +853,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
scaleAtPt(docpt: number[], scale: number) {
const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
this.Document.panTransformType = "Ease";
- this.layoutDoc.scale = scale;
+ this.layoutDoc[this.scaleFieldKey] = scale;
const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] };
const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
@@ -896,7 +897,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
HistoryUtil.pushState(newState);
- const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document.scale, pt: this.Document.panTransformType };
+ const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document.panTransformType };
// if (!willZoom && DocumentView._focusHack.length) {
// Doc.BrushDoc(this.props.Document);
@@ -915,7 +916,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (afterFocus?.()) {
this.Document._panX = savedState.px;
this.Document._panY = savedState.py;
- this.Document.scale = savedState.s;
+ this.Document[this.scaleFieldKey] = savedState.s;
this.Document.panTransformType = savedState.pt;
}
}, 500);
@@ -924,7 +925,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
setScaleToZoom = (doc: Doc, scale: number = 0.75) => {
- this.Document.scale = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
+ this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height));
}
@computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; }
@@ -1125,7 +1126,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}));
if (this.props.isAnnotationOverlay) {
- this.props.Document.scale = Math.max(1, NumCast(this.props.Document.scale));
+ this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey]));
}
return elements;
@@ -1206,7 +1207,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
const options = ContextMenu.Instance.findByDescription("Options...");
const optionItems: ContextMenuProps[] = options && "subitems" in options ? options.subitems : [];
- optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
+ optionItems.push({ description: "reset view", event: () => { this.props.Document._panX = this.props.Document._panY = 0; this.props.Document[this.scaleFieldKey] = 1; }, icon: "compress-arrows-alt" });
optionItems.push({ description: "toggle snap line display", event: () => Doc.UserDoc().showSnapLines = !Doc.UserDoc().showSnapLines, icon: "compress-arrows-alt" });
optionItems.push({ description: "Reset default note style", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 085625e69..5f09fa0ee 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -122,7 +122,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
FormattedTextBox.SelectOnLoadChar = FormattedTextBox.DefaultLayout ? e.key : "";
const tbox = Docs.Create.TextDocument("", {
_width: 200, _height: 100, x: x, y: y, _autoHeight: true, _fontSize: NumCast(Doc.UserDoc().fontSize),
- _fontFamily: StrCast(Doc.UserDoc().fontFamily), _backgroundColor: StrCast(Doc.UserDoc().backgroundColor),
+ _fontFamily: StrCast(Doc.UserDoc().fontFamily),
title: "-typed text-"
});
const template = FormattedTextBox.DefaultLayout;
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.scss b/src/client/views/collections/collectionGrid/CollectionGridView.scss
new file mode 100644
index 000000000..9c2d5cbff
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.scss
@@ -0,0 +1,160 @@
+.collectionGridView-contents {
+ display: flex;
+ overflow: hidden;
+ width: 100%;
+ height: 100%;
+ flex-direction: column;
+
+ .collectionGridView-gridContainer {
+ height: 100%;
+ overflow-y: auto;
+ background-color: white;
+ overflow-x: hidden;
+
+ display: flex;
+ flex-direction: row;
+
+ .imageBox-cont img {
+ height: auto;
+ width: auto;
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ .react-grid-layout {
+ width : 100%;
+ }
+
+ .react-grid-item>.react-resizable-handle {
+ z-index: 4; // doesn't work on prezi otherwise
+ }
+
+ .react-grid-item>.react-resizable-handle::after {
+ // grey so it can be seen on the audiobox
+ border-right: 2px solid slategrey;
+ border-bottom: 2px solid slategrey;
+ }
+
+ .rowHeightSlider {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 100%;
+ height: 15px;
+ background: #d3d3d3;
+
+ position: absolute;
+ height: 3;
+ left: 5;
+ top: 30;
+ transform-origin: left;
+ transform: rotate(90deg);
+ outline: none;
+ opacity: 0.7;
+ }
+
+ .rowHeightSlider:hover {
+ opacity: 1;
+ }
+
+ .rowHeightSlider::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ appearance: none;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: darkgrey;
+ opacity: 1;
+ }
+
+ .rowHeightSlider::-moz-range-thumb {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: darkgrey;
+ opacity: 1;
+ }
+ }
+
+ .collectionGridView-addDocumentButton {
+ display: flex;
+ overflow: hidden;
+ margin: auto;
+ width: 90%;
+ cursor: text;
+ min-height: 30px;
+ max-height: 30px;
+ 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 {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+ height: 20px;
+
+ width: 100%;
+ color: grey;
+ padding: 10px;
+
+ 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;
+// }
+
+
+/* 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..a5d355abc
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -0,0 +1,307 @@
+import { action, computed, Lambda, observable, reaction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from "react";
+import { Doc, Opt } from '../../../../fields/Doc';
+import { documentSchema } from '../../../../fields/documentSchemas';
+import { Id } from '../../../../fields/FieldSymbols';
+import { makeInterface } from '../../../../fields/Schema';
+import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DragManager } from '../../../util/DragManager';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { Transform } from '../../../util/Transform';
+import { undoBatch } from '../../../util/UndoManager';
+import { ContextMenu } from '../../ContextMenu';
+import { ContextMenuProps } from '../../ContextMenuItem';
+import { ContentFittingDocumentView } from '../../nodes/ContentFittingDocumentView';
+import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
+import { CollectionSubView } from '../CollectionSubView';
+import "./CollectionGridView.scss";
+import Grid, { Layout } from "./Grid";
+
+type GridSchema = makeInterface<[typeof documentSchema]>;
+const GridSchema = makeInterface(documentSchema);
+
+@observer
+export class CollectionGridView extends CollectionSubView(GridSchema) {
+ private _containerRef: React.RefObject<HTMLDivElement> = React.createRef();
+ private _changeListenerDisposer: Opt<Lambda>; // listens for changes in this.childLayoutPairs
+ private _resetListenerDisposer: Opt<Lambda>; // listens for when the reset button is clicked
+ @observable private _rowHeight: Opt<number>; // temporary store of row height to make change undoable
+ @observable private _scroll: number = 0; // required to make sure the decorations box container updates on scroll
+
+ @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
+
+ @computed get numCols() { return NumCast(this.props.Document.gridNumCols, 10); }
+ @computed get rowHeight() { return this._rowHeight === undefined ? NumCast(this.props.Document.gridRowHeight, 100) : this._rowHeight; }
+ // sets the default width and height of the grid nodes
+ @computed get defaultW() { return NumCast(this.props.Document.gridDefaultW, 2); }
+ @computed get defaultH() { return NumCast(this.props.Document.gridDefaultH, 2); }
+
+ @computed get colWidthPlusGap() { return (this.props.PanelWidth() - this.margin) / this.numCols; }
+ @computed get rowHeightPlusGap() { return this.rowHeight + this.margin; }
+
+ @computed get margin() { return NumCast(this.props.Document.margin, 10); } // sets the margin between grid nodes
+
+ @computed get flexGrid() { return BoolCast(this.props.Document.gridFlex, true); } // is grid static/flexible i.e. whether nodes be moved around and resized
+ @computed get compaction() { return StrCast(this.props.Document.gridStartCompaction, StrCast(this.props.Document.gridCompaction, "vertical")); } // is grid static/flexible i.e. whether nodes be moved around and resized
+
+ componentDidMount() {
+ this._changeListenerDisposer = reaction(() => this.childLayoutPairs, (pairs) => {
+ const newLayouts: Layout[] = [];
+ const oldLayouts = this.savedLayoutList;
+ pairs.forEach((pair, i) => {
+ const existing = oldLayouts.find(l => l.i === pair.layout[Id]);
+ if (existing) newLayouts.push(existing);
+ else this.addLayoutItem(newLayouts, this.makeLayoutItem(pair.layout, this.unflexedPosition(i), !this.flexGrid));
+ });
+ pairs?.length && this.setLayoutList(newLayouts);
+ }, { fireImmediately: true });
+
+ // updates the layouts if the reset button has been clicked
+ this._resetListenerDisposer = reaction(() => this.props.Document.gridResetLayout, (reset) => {
+ if (reset && this.flexGrid) {
+ this.setLayout(this.childLayoutPairs.map((pair, index) => this.makeLayoutItem(pair.layout, this.unflexedPosition(index))));
+ }
+ this.props.Document.gridResetLayout = false;
+ });
+ }
+
+ componentWillUnmount() {
+ this._changeListenerDisposer?.();
+ this._resetListenerDisposer?.();
+ }
+
+ unflexedPosition(index: number): Omit<Layout, "i"> {
+ return {
+ x: (index % Math.floor(this.numCols / this.defaultW)) * this.defaultW,
+ y: Math.floor(index / Math.floor(this.numCols / this.defaultH)) * this.defaultH,
+ w: this.defaultW,
+ h: this.defaultH,
+ static: true
+ };
+ }
+
+ screenToCell(sx: number, sy: number) {
+ const pt = this.props.ScreenToLocalTransform().transformPoint(sx, sy);
+ const x = Math.floor(pt[0] / this.colWidthPlusGap);
+ const y = Math.floor((pt[1] + this._scroll) / this.rowHeight);
+ return { x, y };
+ }
+
+ makeLayoutItem = (doc: Doc, pos: { x: number, y: number }, Static: boolean = false, w: number = this.defaultW, h: number = this.defaultH) => {
+ return ({ i: doc[Id], w, h, x: pos.x, y: pos.y, static: Static });
+ }
+
+ addLayoutItem = (layouts: Layout[], layout: Layout) => {
+ const f = layouts.findIndex(l => l.i === layout.i);
+ f !== -1 && layouts.splice(f, 1);
+ layouts.push(layout);
+ return layouts;
+ }
+ /**
+ * @returns the transform that will correctly place the document decorations box.
+ */
+ private lookupIndividualTransform = (layout: Layout) => {
+ const xypos = this.flexGrid ? layout : this.unflexedPosition(this.renderedLayoutList.findIndex(l => l.i === layout.i));
+ const pos = { x: xypos.x * this.colWidthPlusGap + this.margin, y: xypos.y * this.rowHeightPlusGap + this.margin - this._scroll };
+
+ return this.props.ScreenToLocalTransform().translate(-pos.x, -pos.y);
+ }
+
+ /**
+ * @returns the layout list converted from JSON
+ */
+ get savedLayoutList() {
+ return (this.props.Document.gridLayoutString ? JSON.parse(StrCast(this.props.Document.gridLayoutString)) : []) as Layout[];
+ }
+
+ /**
+ * Stores the layout list on the Document as JSON
+ */
+ setLayoutList(layouts: Layout[]) {
+ this.props.Document.gridLayoutString = JSON.stringify(layouts);
+ }
+
+ /**
+ *
+ * @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}
+ 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={StrCast(this.props.Document.display, "contents")} // sets the css display type of the ContentFittingDocumentView component
+ />;
+ }
+
+ /**
+ * Saves the layouts received from the Grid to the Document.
+ * @param layouts `Layout[]`
+ */
+ @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
+ if (this.flexGrid) {
+ const savedLayouts = this.savedLayoutList;
+ this.childLayoutPairs.forEach(({ layout: doc }) => {
+ const gridLayout = savedLayouts.find(gridLayout => gridLayout.i === doc[Id]);
+ gridLayout && Object.assign(gridLayout, layoutArray.find(layout => layout.i === doc[Id]) || gridLayout);
+ });
+
+ if (this.props.Document.gridStartCompaction) {
+ undoBatch(() => {
+ this.props.Document.gridCompaction = this.props.Document.gridStartCompaction;
+ this.setLayoutList(savedLayouts);
+ })();
+ this.props.Document.gridStartCompaction = undefined;
+ } else {
+ undoBatch(() => this.setLayoutList(savedLayouts))();
+ }
+ }
+ }
+
+ /**
+ * @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 collector: JSX.Element[] = [];
+ if (this.renderedLayoutList.length === this.childLayoutPairs.length) {
+ this.renderedLayoutList.forEach(l => {
+ const child = this.childLayoutPairs.find(c => c.layout[Id] === l.i);
+ const dxf = () => this.lookupIndividualTransform(l);
+ const width = () => (this.flexGrid ? l.w : this.defaultW) * this.colWidthPlusGap - this.margin;
+ const height = () => (this.flexGrid ? l.h : this.defaultH) * this.rowHeightPlusGap - this.margin;
+ child && collector.push(
+ <div key={child.layout[Id]} className={"document-wrapper" + (this.flexGrid && this.props.isSelected() ? "" : " static")} >
+ {this.getDisplayDoc(child.layout, dxf, width, height)}
+ </div >
+ );
+ });
+ }
+ return collector;
+ }
+
+ /**
+ * @returns a list of `Layout` objects with attributes depending on whether the grid is flexible or static
+ */
+ @computed get renderedLayoutList(): Layout[] {
+ return this.flexGrid ?
+ this.savedLayoutList.map(({ i, x, y, w, h }) => ({
+ i, y, h,
+ x: x + w > this.numCols ? 0 : x, // handles wrapping around of nodes when numCols decreases
+ w: Math.min(w, this.numCols), // reduces width if greater than numCols
+ static: BoolCast(this.childLayoutPairs.find(({ layout }) => layout[Id] === i)?.layout.lockedPosition, false) // checks if the lock position item has been selected in the context menu
+ })) :
+ this.savedLayoutList.map((layout, index) => Object.assign(layout, this.unflexedPosition(index)));
+ }
+
+ @action
+ onInternalDrop = (e: Event, de: DragManager.DropEvent) => {
+ const savedLayouts = this.savedLayoutList;
+ const dropped = de.complete.docDragData?.droppedDocuments;
+ if (dropped && super.onInternalDrop(e, de) && savedLayouts.length !== this.childDocs.length) {
+ dropped.forEach(doc => this.addLayoutItem(savedLayouts, this.makeLayoutItem(doc, this.screenToCell(de.x, de.y)))); // shouldn't place all docs in the same cell;
+ this.setLayoutList(savedLayouts);
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Handles the change in the value of the rowHeight slider.
+ */
+ @action
+ onSliderChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+ this._rowHeight = event.currentTarget.valueAsNumber;
+ }
+ @action
+ onSliderDown = (e: React.PointerEvent) => {
+ this._rowHeight = this.rowHeight; // uses _rowHeight during dragging and sets doc's rowHeight when finished so that operation is undoable
+ setupMoveUpEvents(this, e, returnFalse, action(() => {
+ undoBatch(() => this.props.Document.gridRowHeight = this._rowHeight)();
+ this._rowHeight = undefined;
+ }), emptyFunction, false, false);
+ e.stopPropagation();
+ }
+ /**
+ * Adds the display option to change the css display attribute of the `ContentFittingDocumentView`s
+ */
+ onContextMenu = () => {
+ const displayOptionsMenu: ContextMenuProps[] = [];
+ displayOptionsMenu.push({ description: "Contents", event: () => this.props.Document.display = "contents", icon: "copy" });
+ displayOptionsMenu.push({ description: "Undefined", event: () => this.props.Document.display = undefined, icon: "exclamation" });
+ ContextMenu.Instance.addItem({ description: "Display", subitems: displayOptionsMenu, icon: "tv" });
+ }
+
+ onPointerDown = (e: React.PointerEvent) => {
+ if (this.props.active(true)) {
+ setupMoveUpEvents(this, e, returnFalse, returnFalse,
+ (e: PointerEvent, doubleTap?: boolean) => {
+ if (doubleTap) {
+ undoBatch(action(() => {
+ const text = Docs.Create.TextDocument("", { _width: 150, _height: 50 });
+ FormattedTextBox.SelectOnLoad = text[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
+ Doc.AddDocToList(this.props.Document, this.props.fieldKey, text);
+ this.setLayoutList(this.addLayoutItem(this.savedLayoutList, this.makeLayoutItem(text, this.screenToCell(e.clientX, e.clientY))));
+ }))();
+ }
+ },
+ false);
+ if (this.props.isSelected(true)) e.stopPropagation();
+ }
+ }
+
+ render() {
+ return (
+ <div className="collectionGridView-contents" ref={this.createDashEventsTarget}
+ style={{ pointerEvents: !this.props.active() && !SnappingManager.GetIsDragging() ? "none" : undefined }}
+ onContextMenu={this.onContextMenu}
+ onPointerDown={e => this.onPointerDown(e)} >
+ <div className="collectionGridView-gridContainer" ref={this._containerRef}
+ onWheel={e => e.stopPropagation()}
+ onScroll={action(e => {
+ if (!this.props.isSelected()) e.currentTarget.scrollTop = this._scroll;
+ else this._scroll = e.currentTarget.scrollTop;
+ })} >
+ <Grid
+ width={this.props.PanelWidth()}
+ nodeList={this.contents.length ? this.contents : null}
+ layout={this.contents.length ? this.renderedLayoutList : undefined}
+ childrenDraggable={this.props.isSelected() ? true : false}
+ numCols={this.numCols}
+ rowHeight={this.rowHeight}
+ setLayout={this.setLayout}
+ transformScale={this.props.ScreenToLocalTransform().Scale}
+ compactType={this.compaction} // determines whether nodes should remain in position, be bound to the top, or to the left
+ preventCollision={BoolCast(this.props.Document.gridPreventCollision)}// determines whether nodes should move out of the way (i.e. collide) when other nodes are dragged over them
+ margin={this.margin}
+ />
+ <input className="rowHeightSlider" type="range"
+ style={{ width: this.props.PanelHeight() - 30 }}
+ min={1} value={this.rowHeight} max={this.props.PanelHeight() - 30}
+ onPointerDown={this.onSliderDown} onChange={this.onSliderChange} />
+ </div>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/collections/collectionGrid/Grid.tsx b/src/client/views/collections/collectionGrid/Grid.tsx
new file mode 100644
index 000000000..3d2ed0cf9
--- /dev/null
+++ b/src/client/views/collections/collectionGrid/Grid.tsx
@@ -0,0 +1,53 @@
+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: (layout: Layout[]) => void;
+ transformScale: number;
+ childrenDraggable: boolean;
+ preventCollision: boolean;
+ compactType: string;
+ margin: number;
+}
+
+/**
+ * Wrapper around the actual GridLayout of `react-grid-layout`.
+ */
+@observer
+export default class Grid extends React.Component<GridProps> {
+ render() {
+ const compactType = this.props.compactType === "vertical" || this.props.compactType === "horizontal" ? this.props.compactType : null;
+ return (
+ <GridLayout className="layout"
+ layout={this.props.layout}
+ cols={this.props.numCols}
+ rowHeight={this.props.rowHeight}
+ width={this.props.width}
+ compactType={compactType}
+ isDroppable={true}
+ isDraggable={this.props.childrenDraggable}
+ isResizable={this.props.childrenDraggable}
+ useCSSTransforms={true}
+ onLayoutChange={this.props.setLayout}
+ preventCollision={this.props.preventCollision}
+ transformScale={1 / this.props.transformScale} // still doesn't work :(
+ margin={[this.props.margin, this.props.margin]}
+ >
+ {this.props.nodeList}
+ </GridLayout>
+ );
+ }
+}