From 963f137f10e6f08cc449b181c920069dc7cfd394 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Tue, 19 Feb 2019 05:19:50 -0500
Subject: Changed some names, added some comments, and got rid of some unused
 stuff

---
 src/client/views/collections/CollectionViewBase.tsx | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 4cbafe950..5dd10722d 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -14,15 +14,14 @@ import { Transform } from "../../util/Transform";
 
 
 export interface CollectionViewProps {
-    CollectionFieldKey: Key;
-    DocumentForCollection: Document;
+    fieldKey: Key;
+    Document: Document;
     ContainingDocumentView: Opt<DocumentView>;
     ScreenToLocalTransform: () => Transform;
     isSelected: () => boolean;
     isTopMost: boolean;
     select: (ctrlPressed: boolean) => void;
     BackgroundView?: () => JSX.Element;
-    ParentScaling: number;
 }
 
 export const COLLECTION_BORDER_WIDTH = 2;
@@ -31,8 +30,8 @@ export const COLLECTION_BORDER_WIDTH = 2;
 export class CollectionViewBase extends React.Component<CollectionViewProps> {
 
     public static LayoutString(collectionType: string, fieldKey: string = "DataKey") {
-        return `<${collectionType} Scaling={Scaling} DocumentForCollection={Document}
-                    ScreenToLocalTransform={ScreenToLocalTransform} CollectionFieldKey={${fieldKey}} isSelected={isSelected} select={select}
+        return `<${collectionType} Document={Document}
+                    ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select}
                     isTopMost={isTopMost}
                     ContainingDocumentView={DocumentView} BackgroundView={BackgroundView} />`;
     }
@@ -46,14 +45,14 @@ export class CollectionViewBase extends React.Component<CollectionViewProps> {
     @action
     addDocument = (doc: Document): void => {
         //TODO This won't create the field if it doesn't already exist
-        const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>())
+        const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>())
         value.push(doc);
     }
 
     @action
     removeDocument = (doc: Document): boolean => {
         //TODO This won't create the field if it doesn't already exist
-        const value = this.props.DocumentForCollection.GetData(this.props.CollectionFieldKey, ListField, new Array<Document>())
+        const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>())
         let index = value.indexOf(doc);
         if (index !== -1) {
             value.splice(index, 1)
-- 
cgit v1.2.3-70-g09d2


From c69e80d7bc0fc1774f71bb58b759bb571eb6a085 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 19 Feb 2019 16:11:31 -0500
Subject: got rid of DocumentView as a prop

---
 src/client/views/collections/CollectionViewBase.tsx | 10 ++--------
 src/client/views/nodes/DocumentView.tsx             |  4 +---
 2 files changed, 3 insertions(+), 11 deletions(-)

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 0a90bd0f2..0acc890d8 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -1,22 +1,17 @@
 import { action, computed } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../fields/Document";
-import { Opt } from "../../../fields/Field";
 import { Key } from "../../../fields/Key";
 import { ListField } from "../../../fields/ListField";
 import { SelectionManager } from "../../util/SelectionManager";
 import { ContextMenu } from "../ContextMenu";
 import React = require("react");
-import { DocumentView } from "../nodes/DocumentView";
-import { CollectionDockingView } from "./CollectionDockingView";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
 import { Transform } from "../../util/Transform";
 
 
 export interface CollectionViewProps {
     fieldKey: Key;
     Document: Document;
-    ContainingDocumentView: Opt<DocumentView>;
     ScreenToLocalTransform: () => Transform;
     isSelected: () => boolean;
     isTopMost: boolean;
@@ -32,12 +27,11 @@ export class CollectionViewBase extends React.Component<CollectionViewProps> {
     public static LayoutString(collectionType: string, fieldKey: string = "DataKey") {
         return `<${collectionType} Document={Document}
                     ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select}
-                    isTopMost={isTopMost}
-                    ContainingDocumentView={DocumentView} BackgroundView={BackgroundView} />`;
+                    isTopMost={isTopMost} BackgroundView={BackgroundView} />`;
     }
     @computed
     public get active(): boolean {
-        var isSelected = (this.props.ContainingDocumentView && SelectionManager.IsSelected(this.props.ContainingDocumentView));
+        var isSelected = this.props.isSelected();
         var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this);
         var topMost = this.props.isTopMost;
         return isSelected || childSelected || topMost;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0ef8856b7..ac1996ac5 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -278,13 +278,11 @@ export class DocumentView extends React.Component<DocumentViewProps> {
             bindings.BackgroundView = backgroundView;
         }
 
-        bindings.DocumentView = this;
-
         var width = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
         var strwidth = width > 0 ? width.toString() + "px" : "100%";
         var height = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
         var strheight = height > 0 ? height.toString() + "px" : "100%";
-        var scaling = this.props.Scaling;// this.props.ScreenToLocalTransform().Scale;
+        var scaling = this.props.Scaling;
         return (
             <div className="documentView-node" ref={this._mainCont} style={{ width: strwidth, height: strheight, transformOrigin: "left top", transform: `scale(${scaling},${scaling})` }}
                 onContextMenu={this.onContextMenu}
-- 
cgit v1.2.3-70-g09d2


From 38ca0618dcdeb57d5a91557b392c1aff095868be Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 22 Feb 2019 03:01:30 -0500
Subject: Added CollectionView Switched sub-collections to not inherit from
 CollectionViewBase

---
 src/client/Server.ts                               |   5 +-
 src/client/documents/Documents.ts                  |  81 +++++---------
 src/client/views/Main.tsx                          |   8 +-
 .../views/collections/CollectionDockingView.tsx    |  11 +-
 .../views/collections/CollectionFreeFormView.tsx   |  25 ++---
 .../views/collections/CollectionSchemaView.tsx     |  13 ++-
 src/client/views/collections/CollectionView.tsx    | 116 +++++++++++++++++++++
 .../views/collections/CollectionViewBase.tsx       |  61 -----------
 src/client/views/nodes/DocumentView.tsx            |  13 ++-
 src/client/views/nodes/FieldView.tsx               |   3 +-
 src/fields/BasicField.ts                           |   6 +-
 src/fields/Document.ts                             |   9 ++
 src/fields/KeyStore.ts                             |   1 +
 src/fields/ListField.ts                            |  19 +++-
 14 files changed, 212 insertions(+), 159 deletions(-)
 create mode 100644 src/client/views/collections/CollectionView.tsx
 delete mode 100644 src/client/views/collections/CollectionViewBase.tsx

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/Server.ts b/src/client/Server.ts
index 3e61729ab..06ac22c61 100644
--- a/src/client/Server.ts
+++ b/src/client/Server.ts
@@ -60,13 +60,16 @@ export class Server {
         });
     }
 
-    public static GetDocumentField(doc: Document, key: Key) {
+    public static GetDocumentField(doc: Document, key: Key, callback?: (field: Field) => void) {
         let field = doc._proxies.get(key.Id);
         if (field) {
             this.GetField(field,
                 action((fieldfromserver: Opt<Field>) => {
                     if (fieldfromserver) {
                         doc.fields.set(key.Id, { key, field: fieldfromserver });
+                        if (callback) {
+                            callback(fieldfromserver);
+                        }
                     }
                 }));
         }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 920068273..bfa6cb7a9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -11,6 +11,7 @@ import { ImageField } from "../../fields/ImageField";
 import { ImageBox } from "../views/nodes/ImageBox";
 import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormView";
 import { FieldId } from "../../fields/Field";
+import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
 
 interface DocumentOptions {
     x?: number;
@@ -24,12 +25,10 @@ interface DocumentOptions {
 
 export namespace Documents {
     export function initProtos(callback: () => void) {
-        Server.GetFields([collectionProtoId, textProtoId, imageProtoId, schemaProtoId, dockProtoId], (fields) => {
+        Server.GetFields([collectionProtoId, textProtoId, imageProtoId], (fields) => {
             collectionProto = fields[collectionProtoId] as Document;
             imageProto = fields[imageProtoId] as Document;
             textProto = fields[textProtoId] as Document;
-            dockProto = fields[dockProtoId] as Document;
-            schemaProto = fields[schemaProtoId] as Document;
             callback()
         });
     }
@@ -83,52 +82,6 @@ export namespace Documents {
         return doc;
     }
 
-    let schemaProto: Document;
-    const schemaProtoId = "schemaProto";
-    function GetSchemaPrototype(): Document {
-        if (!schemaProto) {
-            schemaProto = new Document(schemaProtoId);
-            schemaProto.Set(KeyStore.X, new NumberField(0));
-            schemaProto.Set(KeyStore.Y, new NumberField(0));
-            schemaProto.Set(KeyStore.Width, new NumberField(300));
-            schemaProto.Set(KeyStore.Height, new NumberField(150));
-            schemaProto.Set(KeyStore.Layout, new TextField(CollectionSchemaView.LayoutString()));
-            schemaProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data]));
-        }
-        return schemaProto;
-    }
-
-    export function SchemaDocument(documents: Array<Document>, options: DocumentOptions = {}): Document {
-        let doc = GetSchemaPrototype().MakeDelegate();
-        setupOptions(doc, options);
-        doc.Set(KeyStore.Data, new ListField(documents));
-        return doc;
-    }
-
-
-    let dockProto: Document;
-    const dockProtoId = "dockProto";
-    function GetDockPrototype(): Document {
-        if (!dockProto) {
-            dockProto = new Document();
-            dockProto.Set(KeyStore.X, new NumberField(0));
-            dockProto.Set(KeyStore.Y, new NumberField(0));
-            dockProto.Set(KeyStore.Width, new NumberField(300));
-            dockProto.Set(KeyStore.Height, new NumberField(150));
-            dockProto.Set(KeyStore.Layout, new TextField(CollectionDockingView.LayoutString()));
-            dockProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data]));
-        }
-        return dockProto;
-    }
-
-    export function DockDocument(config: string, options: DocumentOptions = {}, id?: string): Document {
-        let doc = GetDockPrototype().MakeDelegate(id);
-        setupOptions(doc, options);
-        doc.SetText(KeyStore.Data, config);
-        return doc;
-    }
-
-
     let imageProto: Document;
     const imageProtoId = "imageProto";
     function GetImagePrototype(): Document {
@@ -141,7 +94,8 @@ export namespace Documents {
             imageProto.Set(KeyStore.NativeHeight, new NumberField(300));
             imageProto.Set(KeyStore.Width, new NumberField(300));
             imageProto.Set(KeyStore.Height, new NumberField(300));
-            imageProto.Set(KeyStore.Layout, new TextField(CollectionFreeFormView.LayoutString("AnnotationsKey")));
+            imageProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("AnnotationsKey")));
+            imageProto.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform)
             imageProto.Set(KeyStore.BackgroundLayout, new TextField(ImageBox.LayoutString()));
             // imageProto.SetField(KeyStore.Layout, new TextField('<div style={"background-image: " + {Data}} />'));
             imageProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data, KeyStore.Annotations]));
@@ -165,23 +119,36 @@ export namespace Documents {
     function GetCollectionPrototype(): Document {
         if (!collectionProto) {
             collectionProto = new Document(collectionProtoId);
-            collectionProto.Set(KeyStore.X, new NumberField(0));
-            collectionProto.Set(KeyStore.Y, new NumberField(0));
             collectionProto.Set(KeyStore.Scale, new NumberField(1));
             collectionProto.Set(KeyStore.PanX, new NumberField(0));
             collectionProto.Set(KeyStore.PanY, new NumberField(0));
-            collectionProto.Set(KeyStore.Width, new NumberField(300));
-            collectionProto.Set(KeyStore.Height, new NumberField(300));
-            collectionProto.Set(KeyStore.Layout, new TextField(CollectionFreeFormView.LayoutString("DataKey")));
+            collectionProto.Set(KeyStore.Layout, new TextField(CollectionView.LayoutString("DataKey")));
             collectionProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data]));
         }
         return collectionProto;
     }
 
-    export function CollectionDocument(documents: Array<Document>, options: DocumentOptions = {}, id?: string): Document {
+    export function CollectionDocument(data: Array<Document> | string, viewType: CollectionViewType, options: DocumentOptions = {}, id?: string): Document {
         let doc = GetCollectionPrototype().MakeDelegate(id);
         setupOptions(doc, options);
-        doc.Set(KeyStore.Data, new ListField(documents));
+        if (typeof data === "string") {
+            doc.SetText(KeyStore.Data, data);
+        } else {
+            doc.SetData(KeyStore.Data, data, ListField);
+        }
+        doc.SetNumber(KeyStore.ViewType, viewType);
         return doc;
     }
+
+    export function FreeformDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
+        return CollectionDocument(documents, CollectionViewType.Freeform, options, id)
+    }
+
+    export function SchemaDocument(documents: Array<Document>, options: DocumentOptions, id?: string) {
+        return CollectionDocument(documents, CollectionViewType.Schema, options, id)
+    }
+
+    export function DockDocument(config: string, options: DocumentOptions, id?: string) {
+        return CollectionDocument(config, CollectionViewType.Docking, options, id)
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index c7a6a44e8..fe1a999ec 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -61,16 +61,14 @@ Documents.initProtos(() => {
         let mainfreeform: Document;
         if (res) {
             mainContainer = ServerUtils.FromJson(res) as Document;
-            mainfreeform = mainContainer.Get(KeyStore.ActiveFrame) as Document;
-            if (!mainfreeform)
-                Server.GetField(mainContainer._proxies.get(KeyStore.ActiveFrame.Id)!, (field) => mainfreeform = field as Document);
+            mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document);
         }
         else {
             mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId);
             Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainContainer.ToJson()))
 
             setTimeout(() => {
-                mainfreeform = Documents.CollectionDocument([], { x: 0, y: 400, title: "mini collection" });
+                mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" });
                 Utils.Emit(Server.Socket, MessageStore.AddDocument, new DocumentTransfer(mainfreeform.ToJson()));
 
                 var docs = [mainfreeform].map(doc => CollectionDockingView.makeDocumentConfig(doc));
@@ -90,7 +88,7 @@ Documents.initProtos(() => {
             }));
         })
         let addColNode = action(() => {
-            mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.CollectionDocument([], {
+            mainfreeform.GetList<Document>(KeyStore.Data, []).push(Documents.FreeformDocument([], {
                 x: 0, y: 300, width: 200, height: 200, title: "added note"
             }));
         })
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 60dc24b5f..6d91aef5e 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -14,13 +14,12 @@ import { DragManager } from "../../util/DragManager";
 import { undoBatch } from "../../util/UndoManager";
 import { DocumentView } from "../nodes/DocumentView";
 import "./CollectionDockingView.scss";
-import { CollectionViewBase, CollectionViewProps, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase";
+import { CollectionViewProps, COLLECTION_BORDER_WIDTH, SubCollectionViewProps } from "./CollectionView";
 import React = require("react");
 
 @observer
-export class CollectionDockingView extends CollectionViewBase {
+export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
     public static Instance: CollectionDockingView;
-    public static LayoutString() { return CollectionViewBase.LayoutString("CollectionDockingView"); }
     public static makeDocumentConfig(document: Document) {
         return {
             type: 'react-component',
@@ -41,7 +40,7 @@ export class CollectionDockingView extends CollectionViewBase {
     private _containerRef = React.createRef<HTMLDivElement>();
     private _fullScreen: any = null;
 
-    constructor(props: CollectionViewProps) {
+    constructor(props: SubCollectionViewProps) {
         super(props);
         CollectionDockingView.Instance = this;
         (window as any).React = React;
@@ -187,7 +186,7 @@ export class CollectionDockingView extends CollectionViewBase {
     }
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.button === 2 && this.active) {
+        if (e.button === 2 && this.props.active()) {
             e.stopPropagation();
             e.preventDefault();
         } else {
@@ -195,7 +194,7 @@ export class CollectionDockingView extends CollectionViewBase {
             if (className == "lm_drag_handle" || className == "lm_close" || className == "lm_maximise" || className == "lm_minimise" || className == "lm_close_tab") {
                 this._flush = true;
             }
-            if (e.buttons === 1 && this.active) {
+            if (e.buttons === 1 && this.props.active()) {
                 e.stopPropagation();
             }
         }
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 986bcdcee..bd7ca5b6f 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -4,7 +4,7 @@ import { action, computed } from "mobx";
 import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
 import { DragManager } from "../../util/DragManager";
 import "./CollectionFreeFormView.scss";
-import { CollectionViewBase, COLLECTION_BORDER_WIDTH, CollectionViewProps } from "./CollectionViewBase";
+import { COLLECTION_BORDER_WIDTH, CollectionViewProps, SubCollectionViewProps } from "./CollectionView";
 import { KeyStore } from "../../../fields/KeyStore";
 import { Document } from "../../../fields/Document";
 import { ListField } from "../../../fields/ListField";
@@ -16,8 +16,7 @@ import { DocumentView } from "../nodes/DocumentView";
 import { undoBatch } from "../../util/UndoManager";
 
 @observer
-export class CollectionFreeFormView extends CollectionViewBase {
-    public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionFreeFormView", fieldKey); }
+export class CollectionFreeFormView extends React.Component<SubCollectionViewProps> {
     private _canvasRef = React.createRef<HTMLDivElement>();
     private _lastX: number = 0;
     private _lastY: number = 0;
@@ -38,7 +37,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
     @computed
     get resizeScaling() { return this.isAnnotationOverlay ? this.props.Document.GetNumber(KeyStore.Width, 0) / this.nativeWidth : 1; }
 
-    constructor(props: CollectionViewProps) {
+    constructor(props: SubCollectionViewProps) {
         super(props);
     }
 
@@ -46,9 +45,11 @@ export class CollectionFreeFormView extends CollectionViewBase {
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
         const doc: DocumentView = de.data["document"];
-        if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this) {
-            doc.props.ContainingCollectionView.removeDocument(doc.props.Document);
-            this.addDocument(doc.props.Document);
+        if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this.props.CollectionView) {
+            if (doc.props.RemoveDocument) {
+                doc.props.RemoveDocument(doc.props.Document);
+            }
+            this.props.addDocument(doc.props.Document);
         }
         const xOffset = de.data["xOffset"] as number || 0;
         const yOffset = de.data["yOffset"] as number || 0;
@@ -79,7 +80,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if ((e.button === 2 && this.active) ||
+        if ((e.button === 2 && this.props.active()) ||
             !e.defaultPrevented) {
             document.removeEventListener("pointermove", this.onPointerMove);
             document.addEventListener("pointermove", this.onPointerMove);
@@ -104,7 +105,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
 
     @action
     onPointerMove = (e: PointerEvent): void => {
-        if (!e.cancelBubble && this.active) {
+        if (!e.cancelBubble && this.props.active()) {
             e.preventDefault();
             e.stopPropagation();
             let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
@@ -248,13 +249,13 @@ export class CollectionFreeFormView extends CollectionViewBase {
                     {this.props.BackgroundView ? this.props.BackgroundView() : null}
                     {lvalue.Data.map(doc => {
                         return (<CollectionFreeFormDocumentView key={doc.Id} Document={doc}
-                            AddDocument={this.addDocument}
-                            RemoveDocument={this.removeDocument}
+                            AddDocument={this.props.addDocument}
+                            RemoveDocument={this.props.removeDocument}
                             ScreenToLocalTransform={this.getTransform}
                             isTopMost={false}
                             Scaling={1}
                             PanelSize={[doc.GetNumber(KeyStore.Width, 0), doc.GetNumber(KeyStore.Height, 0)]}
-                            ContainingCollectionView={this} />);
+                            ContainingCollectionView={this.props.CollectionView} />);
                     })}
                 </div>
             </div>
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 5ec288b13..9405c820f 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -13,12 +13,10 @@ import { EditableView } from "../EditableView";
 import { DocumentView } from "../nodes/DocumentView";
 import { FieldView, FieldViewProps } from "../nodes/FieldView";
 import "./CollectionSchemaView.scss";
-import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "./CollectionViewBase";
+import { COLLECTION_BORDER_WIDTH, CollectionViewProps, SubCollectionViewProps } from "./CollectionView";
 
 @observer
-export class CollectionSchemaView extends CollectionViewBase {
-    public static LayoutString(fieldKey: string = "DataKey") { return CollectionViewBase.LayoutString("CollectionSchemaView", fieldKey); }
-
+export class CollectionSchemaView extends React.Component<SubCollectionViewProps> {
     private _mainCont = React.createRef<HTMLDivElement>();
 
     private DIVIDER_WIDTH = 5;
@@ -34,6 +32,7 @@ export class CollectionSchemaView extends CollectionViewBase {
             doc: rowProps.value[0],
             fieldKey: rowProps.value[1],
             isSelected: () => false,
+            select: () => { },
             isTopMost: false
         }
         let contents = (
@@ -112,7 +111,7 @@ export class CollectionSchemaView extends CollectionViewBase {
         //     e.preventDefault();
         // } else 
         {
-            if (e.buttons === 1 && this.active) {
+            if (e.buttons === 1 && this.props.active()) {
                 e.stopPropagation();
             }
         }
@@ -145,12 +144,12 @@ export class CollectionSchemaView extends CollectionViewBase {
                 {({ measureRef }) =>
                     <div ref={measureRef}>
                         <DocumentView Document={selected}
-                            AddDocument={this.addDocument} RemoveDocument={this.removeDocument}
+                            AddDocument={this.props.addDocument} RemoveDocument={this.props.removeDocument}
                             ScreenToLocalTransform={this.getTransform}
                             Scaling={this._parentScaling}
                             isTopMost={false}
                             PanelSize={[this._panelWidth, this._panelHeight]}
-                            ContainingCollectionView={me} />
+                            ContainingCollectionView={this.props.CollectionView} />
                     </div>
                 }
             </Measure>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
new file mode 100644
index 000000000..651d85879
--- /dev/null
+++ b/src/client/views/collections/CollectionView.tsx
@@ -0,0 +1,116 @@
+import { action, computed } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../fields/Document";
+import { Key } from "../../../fields/Key";
+import { ListField } from "../../../fields/ListField";
+import { SelectionManager } from "../../util/SelectionManager";
+import { ContextMenu } from "../ContextMenu";
+import React = require("react");
+import { Transform } from "../../util/Transform";
+import { KeyStore } from "../../../fields/KeyStore";
+import { NumberField } from "../../../fields/NumberField";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import { CollectionDockingView } from "./CollectionDockingView";
+import { CollectionSchemaView } from "./CollectionSchemaView";
+import { Opt } from "../../../fields/Field";
+
+
+export interface CollectionViewProps {
+    fieldKey: Key;
+    Document: Document;
+    ScreenToLocalTransform: () => Transform;
+    isSelected: () => boolean;
+    isTopMost: boolean;
+    select: (ctrlPressed: boolean) => void;
+    BackgroundView?: () => JSX.Element;
+}
+
+export interface SubCollectionViewProps extends CollectionViewProps {
+    active: () => boolean;
+    addDocument: (doc: Document) => void;
+    removeDocument: (doc: Document) => boolean;
+    CollectionView: Opt<CollectionView>;
+}
+
+export enum CollectionViewType {
+    Invalid,
+    Freeform,
+    Schema,
+    Docking,
+}
+
+export const COLLECTION_BORDER_WIDTH = 2;
+
+@observer
+export class CollectionView extends React.Component<CollectionViewProps> {
+
+    public static LayoutString(fieldKey: string = "DataKey") {
+        return `<CollectionView Document={Document}
+                    ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select}
+                    isTopMost={isTopMost} BackgroundView={BackgroundView} />`;
+    }
+    public active = () => {
+        var isSelected = this.props.isSelected();
+        var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this);
+        var topMost = this.props.isTopMost;
+        return isSelected || childSelected || topMost;
+    }
+    @action
+    addDocument = (doc: Document): void => {
+        //TODO This won't create the field if it doesn't already exist
+        const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>())
+        value.push(doc);
+    }
+
+    @action
+    removeDocument = (doc: Document): boolean => {
+        //TODO This won't create the field if it doesn't already exist
+        const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>())
+        let index = value.indexOf(doc);
+        if (index !== -1) {
+            value.splice(index, 1)
+
+            SelectionManager.DeselectAll()
+            ContextMenu.Instance.clearItems()
+            return true;
+        }
+        return false
+    }
+
+    get collectionViewType(): CollectionViewType {
+        let Document = this.props.Document;
+        let viewField = Document.GetT(KeyStore.ViewType, NumberField);
+        if (viewField === "<Waiting>") {
+            return CollectionViewType.Invalid;
+        } else if (viewField) {
+            return viewField.Data;
+        } else {
+            return CollectionViewType.Freeform;
+        }
+    }
+
+    set collectionViewType(type: CollectionViewType) {
+        let Document = this.props.Document;
+        Document.SetData(KeyStore.ViewType, type, NumberField);
+    }
+
+    render() {
+        let viewType = this.collectionViewType;
+        switch (viewType) {
+            case CollectionViewType.Freeform:
+                return (<CollectionFreeFormView {...this.props}
+                    addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active}
+                    CollectionView={this} />)
+            case CollectionViewType.Schema:
+                return (<CollectionSchemaView {...this.props}
+                    addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active}
+                    CollectionView={this} />)
+            case CollectionViewType.Docking:
+                return (<CollectionDockingView {...this.props}
+                    addDocument={this.addDocument} removeDocument={this.removeDocument} active={this.active}
+                    CollectionView={this} />)
+            default:
+                return <div></div>
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
deleted file mode 100644
index 0acc890d8..000000000
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import { action, computed } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { Key } from "../../../fields/Key";
-import { ListField } from "../../../fields/ListField";
-import { SelectionManager } from "../../util/SelectionManager";
-import { ContextMenu } from "../ContextMenu";
-import React = require("react");
-import { Transform } from "../../util/Transform";
-
-
-export interface CollectionViewProps {
-    fieldKey: Key;
-    Document: Document;
-    ScreenToLocalTransform: () => Transform;
-    isSelected: () => boolean;
-    isTopMost: boolean;
-    select: (ctrlPressed: boolean) => void;
-    BackgroundView?: () => JSX.Element;
-}
-
-export const COLLECTION_BORDER_WIDTH = 2;
-
-@observer
-export class CollectionViewBase extends React.Component<CollectionViewProps> {
-
-    public static LayoutString(collectionType: string, fieldKey: string = "DataKey") {
-        return `<${collectionType} Document={Document}
-                    ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select}
-                    isTopMost={isTopMost} BackgroundView={BackgroundView} />`;
-    }
-    @computed
-    public get active(): boolean {
-        var isSelected = this.props.isSelected();
-        var childSelected = SelectionManager.SelectedDocuments().some(view => view.props.ContainingCollectionView == this);
-        var topMost = this.props.isTopMost;
-        return isSelected || childSelected || topMost;
-    }
-    @action
-    addDocument = (doc: Document): void => {
-        //TODO This won't create the field if it doesn't already exist
-        const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>())
-        value.push(doc);
-    }
-
-    @action
-    removeDocument = (doc: Document): boolean => {
-        //TODO This won't create the field if it doesn't already exist
-        const value = this.props.Document.GetData(this.props.fieldKey, ListField, new Array<Document>())
-        let index = value.indexOf(doc);
-        if (index !== -1) {
-            value.splice(index, 1)
-
-            SelectionManager.DeselectAll()
-            ContextMenu.Instance.clearItems()
-            return true;
-        }
-        return false
-    }
-
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7cf00a116..2114a5697 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -9,7 +9,7 @@ import { Utils } from "../../../Utils";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
 import { CollectionSchemaView } from "../collections/CollectionSchemaView";
-import { CollectionViewBase, COLLECTION_BORDER_WIDTH } from "../collections/CollectionViewBase";
+import { COLLECTION_BORDER_WIDTH, CollectionView, CollectionViewType } from "../collections/CollectionView";
 import { FormattedTextBox } from "../nodes/FormattedTextBox";
 import { ImageBox } from "../nodes/ImageBox";
 import "./DocumentView.scss";
@@ -22,7 +22,7 @@ import { TextField } from "../../../fields/TextField";
 const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
 
 export interface DocumentViewProps {
-    ContainingCollectionView: Opt<CollectionViewBase>;
+    ContainingCollectionView: Opt<CollectionView>;
 
     Document: Document;
     AddDocument?: (doc: Document) => void;
@@ -114,7 +114,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     @computed
     get active(): boolean {
         return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView ||
-            this.props.ContainingCollectionView.active;
+            this.props.ContainingCollectionView.active();
     }
 
     private _contextMenuCanOpen = false;
@@ -233,6 +233,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
             ContextMenu.Instance.addItem({ description: "Full Screen", event: this.fullScreenClicked })
             ContextMenu.Instance.addItem({ description: "Open Right", event: this.openRight })
             ContextMenu.Instance.addItem({ description: "Delete", event: this.deleteClicked })
+            ContextMenu.Instance.addItem({ description: "Freeform", event: () => { this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Freeform) } })
+            ContextMenu.Instance.addItem({ description: "Schema", event: () => { this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Schema) } })
+            ContextMenu.Instance.addItem({ description: "Docking", event: () => { this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Docking) } })
             ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
             SelectionManager.SelectDoc(this, e.ctrlKey);
         }
@@ -275,7 +278,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         let backgroundLayout = this.backgroundLayout;
         if (backgroundLayout) {
             let backgroundView = () => (<JsxParser
-                components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }}
+                components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
                 bindings={documentBindings}
                 jsx={this.backgroundLayout}
                 showWarnings={true}
@@ -294,7 +297,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                 onContextMenu={this.onContextMenu}
                 onPointerDown={this.onPointerDown} >
                 <JsxParser
-                    components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView }}
+                    components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
                     bindings={documentBindings}
                     jsx={this.layout}
                     showWarnings={true}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 918acff4c..821172d71 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -20,12 +20,13 @@ export interface FieldViewProps {
     fieldKey: Key;
     doc: Document;
     isSelected: () => boolean;
+    select: () => void;
     isTopMost: boolean;
 }
 
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} isSelected={isSelected} isTopMost={isTopMost} />`; }
+    public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; }
     @computed
     get field(): FieldValue<Field> {
         const { doc, fieldKey } = this.props;
diff --git a/src/fields/BasicField.ts b/src/fields/BasicField.ts
index 91977b243..a92c4a236 100644
--- a/src/fields/BasicField.ts
+++ b/src/fields/BasicField.ts
@@ -32,7 +32,7 @@ export abstract class BasicField<T> extends Field {
             return;
         }
         let oldValue = this.data;
-        this.data = value;
+        this.setData(value);
         UndoManager.AddEvent({
             undo: () => this.Data = oldValue,
             redo: () => this.Data = value
@@ -40,6 +40,10 @@ export abstract class BasicField<T> extends Field {
         Server.UpdateField(this);
     }
 
+    protected setData(value: T) {
+        this.data = value;
+    }
+
     @action
     TrySetValue(value: any): boolean {
         if (typeof value == typeof this.data) {
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index 6667485b6..ff13732b3 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -90,6 +90,15 @@ export class Document extends Field {
         return field;
     }
 
+    GetAsync(key: Key, callback: (field: Field) => void): boolean {
+        //This currently doesn't deal with prototypes
+        if (this._proxies.has(key.Id)) {
+            Server.GetDocumentField(this, key, callback);
+            return true;
+        }
+        return false;
+    }
+
     GetT<T extends Field = Field>(key: Key, ctor: { new(...args: any[]): T }, ignoreProto: boolean = false): FieldValue<T> {
         var getfield = this.Get(key, ignoreProto);
         if (getfield != FieldWaiting) {
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 7056886aa..6d6c6a546 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -16,6 +16,7 @@ export namespace KeyStore {
     export const ZIndex = new Key("ZIndex");
     export const Data = new Key("Data");
     export const Annotations = new Key("Annotations");
+    export const ViewType = new Key("ViewType");
     export const Layout = new Key("Layout");
     export const BackgroundLayout = new Key("BackgroundLayout");
     export const LayoutKeys = new Key("LayoutKeys");
diff --git a/src/fields/ListField.ts b/src/fields/ListField.ts
index 75c2eb343..700600804 100644
--- a/src/fields/ListField.ts
+++ b/src/fields/ListField.ts
@@ -1,4 +1,4 @@
-import { action, IArrayChange, IArraySplice, IObservableArray, observe } from "mobx";
+import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
 import { Server } from "../client/Server";
 import { UndoManager } from "../client/util/UndoManager";
 import { Types } from "../server/Message";
@@ -13,7 +13,12 @@ export class ListField<T extends Field> extends BasicField<T[]> {
         if (save) {
             Server.UpdateField(this);
         }
-        observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
+        this.observeList();
+    }
+
+    private observeDisposer: Lambda | undefined;
+    private observeList(): void {
+        this.observeDisposer = observe(this.Data as IObservableArray<T>, (change: IArrayChange<T> | IArraySplice<T>) => {
             this.updateProxies()
             if (change.type == "splice") {
                 UndoManager.AddEvent({
@@ -27,7 +32,15 @@ export class ListField<T extends Field> extends BasicField<T[]> {
                 })
             }
             Server.UpdateField(this);
-        })
+        });
+    }
+
+    protected setData(value: T[]) {
+        if (this.observeDisposer) {
+            this.observeDisposer()
+        }
+        this.data = observable(value);
+        this.observeList();
     }
 
     private updateProxies() {
-- 
cgit v1.2.3-70-g09d2


From 370ac4fde98f2ff25b2dedd6b0857c88c6d7f55b Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 22 Feb 2019 13:42:25 -0500
Subject: moved common code into CollectionViewBase.

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/util/DragManager.ts                     |  1 +
 .../views/collections/CollectionDockingView.tsx    |  3 +-
 .../views/collections/CollectionFreeFormView.tsx   | 59 ++--------------
 .../views/collections/CollectionSchemaView.tsx     | 13 ++--
 src/client/views/collections/CollectionView.tsx    | 20 +-----
 .../views/collections/CollectionViewBase.tsx       | 82 ++++++++++++++++++++++
 7 files changed, 102 insertions(+), 78 deletions(-)
 create mode 100644 src/client/views/collections/CollectionViewBase.tsx

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index bfa6cb7a9..156a09316 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -13,7 +13,7 @@ import { CollectionFreeFormView } from "../views/collections/CollectionFreeFormV
 import { FieldId } from "../../fields/Field";
 import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
 
-interface DocumentOptions {
+export interface DocumentOptions {
     x?: number;
     y?: number;
     width?: number;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 6d5fe12a7..eb4b3aeaa 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -44,6 +44,7 @@ export namespace DragManager {
         drop: (e: Event, de: DropEvent) => void;
     }
 
+
     export function MakeDropTarget(element: HTMLElement, options: DropOptions): DragDropDisposer {
         if ("canDrop" in element.dataset) {
             throw new Error("Element is already droppable, can't make it droppable again");
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 752007439..857af023d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -14,8 +14,9 @@ import { DragManager } from "../../util/DragManager";
 import { undoBatch } from "../../util/UndoManager";
 import { DocumentView } from "../nodes/DocumentView";
 import "./CollectionDockingView.scss";
-import { CollectionViewProps, COLLECTION_BORDER_WIDTH, SubCollectionViewProps } from "./CollectionView";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
 import React = require("react");
+import { SubCollectionViewProps } from "./CollectionViewBase";
 
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index b78b1a3b6..b8312aff7 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -4,20 +4,18 @@ import { action, computed, trace } from "mobx";
 import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
 import { DragManager } from "../../util/DragManager";
 import "./CollectionFreeFormView.scss";
-import { COLLECTION_BORDER_WIDTH, CollectionViewProps, SubCollectionViewProps } from "./CollectionView";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
 import { KeyStore } from "../../../fields/KeyStore";
 import { Document } from "../../../fields/Document";
 import { ListField } from "../../../fields/ListField";
-import { NumberField } from "../../../fields/NumberField";
-import { Documents } from "../../documents/Documents";
 import { FieldWaiting } from "../../../fields/Field";
 import { Transform } from "../../util/Transform";
 import { DocumentView } from "../nodes/DocumentView";
 import { undoBatch } from "../../util/UndoManager";
-import { jSXElement } from "babel-types";
+import { CollectionViewBase, SubCollectionViewProps } from "./CollectionViewBase";
 
 @observer
-export class CollectionFreeFormView extends React.Component<SubCollectionViewProps> {
+export class CollectionFreeFormView extends CollectionViewBase {
     private _canvasRef = React.createRef<HTMLDivElement>();
     private _lastX: number = 0;
     private _lastY: number = 0;
@@ -45,13 +43,8 @@ export class CollectionFreeFormView extends React.Component<SubCollectionViewPro
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
+        super.drop(e, de);
         const doc: DocumentView = de.data["document"];
-        if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this.props.CollectionView) {
-            if (doc.props.RemoveDocument) {
-                doc.props.RemoveDocument(doc.props.Document);
-            }
-            this.props.addDocument(doc.props.Document);
-        }
         const xOffset = de.data["xOffset"] as number || 0;
         const yOffset = de.data["yOffset"] as number || 0;
         //this should be able to use translate and scale methods on an Identity transform, no?
@@ -62,21 +55,6 @@ export class CollectionFreeFormView extends React.Component<SubCollectionViewPro
         doc.props.Document.SetNumber(KeyStore.X, x);
         doc.props.Document.SetNumber(KeyStore.Y, y);
         this.bringToFront(doc);
-        e.stopPropagation();
-    }
-
-    private dropDisposer?: DragManager.DragDropDisposer;
-    createDropTarget = (ele: HTMLDivElement) => {
-        if (this.dropDisposer) {
-            this.dropDisposer();
-        }
-        if (ele) {
-            this.dropDisposer = DragManager.MakeDropTarget(ele, {
-                handlers: {
-                    drop: this.drop
-                }
-            });
-        }
     }
 
     @action
@@ -148,36 +126,11 @@ export class CollectionFreeFormView extends React.Component<SubCollectionViewPro
 
     @action
     onDrop = (e: React.DragEvent): void => {
-        e.stopPropagation()
-        e.preventDefault()
-        let fReader = new FileReader()
-        let file = e.dataTransfer.items[0].getAsFile();
-        let that = this;
         const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0);
         const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0);
         let x = e.pageX - panx
         let y = e.pageY - pany
-
-        fReader.addEventListener("load", action("drop", () => {
-            if (fReader.result) {
-                let url = "" + fReader.result;
-                let doc = Documents.ImageDocument(url, {
-                    x: x, y: y
-                })
-                let docs = that.props.Document.GetT(KeyStore.Data, ListField);
-                if (docs != FieldWaiting) {
-                    if (!docs) {
-                        docs = new ListField<Document>();
-                        that.props.Document.Set(KeyStore.Data, docs)
-                    }
-                    docs.Data.push(doc);
-                }
-            }
-        }), false)
-
-        if (file) {
-            fReader.readAsDataURL(file)
-        }
+        super.onDrop(e, { x: x, y: y });
     }
 
     onDragOver = (): void => {
@@ -249,7 +202,7 @@ export class CollectionFreeFormView extends React.Component<SubCollectionViewPro
                 onPointerDown={this.onPointerDown}
                 onWheel={this.onPointerWheel}
                 onContextMenu={(e) => e.preventDefault()}
-                onDrop={this.onDrop}
+                onDrop={this.onDrop.bind(this)}
                 onDragOver={this.onDragOver}
                 style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }}
                 ref={this.createDropTarget}>
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index f25e721c0..df732308a 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -5,7 +5,7 @@ import Measure from "react-measure";
 import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
 import "react-table/react-table.css";
 import { Document } from "../../../fields/Document";
-import { Field } from "../../../fields/Field";
+import { Field, FieldWaiting } from "../../../fields/Field";
 import { KeyStore } from "../../../fields/KeyStore";
 import { CompileScript, ToField } from "../../util/Scripting";
 import { Transform } from "../../util/Transform";
@@ -13,10 +13,11 @@ import { EditableView } from "../EditableView";
 import { DocumentView } from "../nodes/DocumentView";
 import { FieldView, FieldViewProps } from "../nodes/FieldView";
 import "./CollectionSchemaView.scss";
-import { COLLECTION_BORDER_WIDTH, CollectionViewProps, SubCollectionViewProps } from "./CollectionView";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { CollectionViewBase } from "./CollectionViewBase";
 
 @observer
-export class CollectionSchemaView extends React.Component<SubCollectionViewProps> {
+export class CollectionSchemaView extends CollectionViewBase {
     private _mainCont = React.createRef<HTMLDivElement>();
 
     private DIVIDER_WIDTH = 5;
@@ -117,6 +118,7 @@ export class CollectionSchemaView extends React.Component<SubCollectionViewProps
         }
     }
 
+
     getTransform = (): Transform => {
         return this.props.ScreenToLocalTransform().translate(- COLLECTION_BORDER_WIDTH - this.DIVIDER_WIDTH - this._dividerX, - COLLECTION_BORDER_WIDTH).scale(1 / this._parentScaling);
     }
@@ -181,7 +183,10 @@ export class CollectionSchemaView extends React.Component<SubCollectionViewProps
                     }
                 </Measure>
                 <div className="collectionSchemaView-dividerDragger" style={{ position: "relative", background: "black", float: "left", width: `${this.DIVIDER_WIDTH}px`, height: "100%" }} onPointerDown={this.onDividerDown} />
-                <div className="collectionSchemaView-previewRegion" style={{ position: "relative", float: "left", width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)`, height: "100%" }}>
+                <div className="collectionSchemaView-previewRegion"
+                    onDrop={(e: React.DragEvent) => this.onDrop(e, {})}
+                    ref={this.createDropTarget}
+                    style={{ position: "relative", float: "left", width: `calc(${100 - this._splitPercentage}% - ${this.DIVIDER_WIDTH}px)`, height: "100%" }}>
                     {content}
                 </div>
             </div >
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 651d85879..ff1803ec3 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -1,36 +1,18 @@
 import { action, computed } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../fields/Document";
-import { Key } from "../../../fields/Key";
 import { ListField } from "../../../fields/ListField";
 import { SelectionManager } from "../../util/SelectionManager";
 import { ContextMenu } from "../ContextMenu";
 import React = require("react");
-import { Transform } from "../../util/Transform";
 import { KeyStore } from "../../../fields/KeyStore";
 import { NumberField } from "../../../fields/NumberField";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import { CollectionDockingView } from "./CollectionDockingView";
 import { CollectionSchemaView } from "./CollectionSchemaView";
-import { Opt } from "../../../fields/Field";
+import { CollectionViewProps } from "./CollectionViewBase";
 
 
-export interface CollectionViewProps {
-    fieldKey: Key;
-    Document: Document;
-    ScreenToLocalTransform: () => Transform;
-    isSelected: () => boolean;
-    isTopMost: boolean;
-    select: (ctrlPressed: boolean) => void;
-    BackgroundView?: () => JSX.Element;
-}
-
-export interface SubCollectionViewProps extends CollectionViewProps {
-    active: () => boolean;
-    addDocument: (doc: Document) => void;
-    removeDocument: (doc: Document) => boolean;
-    CollectionView: Opt<CollectionView>;
-}
 
 export enum CollectionViewType {
     Invalid,
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
new file mode 100644
index 000000000..06de56383
--- /dev/null
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -0,0 +1,82 @@
+import { action, computed } from "mobx";
+import { Document } from "../../../fields/Document";
+import { ListField } from "../../../fields/ListField";
+import React = require("react");
+import { KeyStore } from "../../../fields/KeyStore";
+import { Opt, FieldWaiting } from "../../../fields/Field";
+import { undoBatch } from "../../util/UndoManager";
+import { DragManager } from "../../util/DragManager";
+import { DocumentView } from "../nodes/DocumentView";
+import { Documents, DocumentOptions } from "../../documents/Documents";
+import { Key } from "../../../fields/Key";
+import { Transform } from "../../util/Transform";
+
+
+export interface CollectionViewProps {
+    fieldKey: Key;
+    Document: Document;
+    ScreenToLocalTransform: () => Transform;
+    isSelected: () => boolean;
+    isTopMost: boolean;
+    select: (ctrlPressed: boolean) => void;
+    BackgroundView?: () => JSX.Element;
+}
+export interface SubCollectionViewProps extends CollectionViewProps {
+    active: () => boolean;
+    addDocument: (doc: Document) => void;
+    removeDocument: (doc: Document) => boolean;
+    CollectionView: any;
+}
+
+export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
+    private dropDisposer?: DragManager.DragDropDisposer;
+    protected createDropTarget = (ele: HTMLDivElement) => {
+        if (this.dropDisposer) {
+            this.dropDisposer();
+        }
+        if (ele) {
+            this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
+        }
+    }
+
+    @undoBatch
+    @action
+    protected drop(e: Event, de: DragManager.DropEvent) {
+        const doc: DocumentView = de.data["document"];
+        if (doc.props.ContainingCollectionView && doc.props.ContainingCollectionView !== this.props.CollectionView) {
+            if (doc.props.RemoveDocument) {
+                doc.props.RemoveDocument(doc.props.Document);
+            }
+            this.props.addDocument(doc.props.Document);
+        }
+        e.stopPropagation();
+    }
+
+    @action
+    protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
+        e.stopPropagation()
+        e.preventDefault()
+        let fReader = new FileReader()
+        let file = e.dataTransfer.items[0].getAsFile();
+        let that = this;
+
+        fReader.addEventListener("load", action("drop", () => {
+            if (fReader.result) {
+                let url = "" + fReader.result;
+                let doc = Documents.ImageDocument(url, options)
+                let docs = that.props.Document.GetT(KeyStore.Data, ListField);
+                if (docs != FieldWaiting) {
+                    if (!docs) {
+                        docs = new ListField<Document>();
+                        that.props.Document.Set(KeyStore.Data, docs)
+                    }
+                    docs.Data.push(doc);
+                }
+            }
+        }), false)
+
+        if (file) {
+            fReader.readAsDataURL(file)
+        }
+    }
+}
-- 
cgit v1.2.3-70-g09d2


From 5d6d6e00e8fbff1f4475235b1912efcee85eb91e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 22 Feb 2019 21:39:40 -0500
Subject: moved backgroundLayout to CollectionFreeForm

---
 src/client/views/DocumentDecorations.tsx           |  2 +-
 .../views/collections/CollectionFreeFormView.tsx   | 58 ++++++++++++++--------
 .../views/collections/CollectionSchemaView.tsx     |  3 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../views/collections/CollectionViewBase.tsx       |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 41 ++++-----------
 src/client/views/nodes/FieldView.tsx               |  3 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  1 -
 8 files changed, 55 insertions(+), 57 deletions(-)

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index efd50e49e..09df8cdaa 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -101,7 +101,7 @@ export class DocumentDecorations extends React.Component {
         }
 
         SelectionManager.SelectedDocuments().forEach(element => {
-            const rect = element.screenRect;
+            const rect = element.screenRect();
             if (rect.width !== 0) {
                 let doc = element.props.Document;
                 let width = doc.GetOrCreate(KeyStore.Width, NumberField);
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 43bc24a12..565402046 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -1,18 +1,25 @@
+import { action, computed } from "mobx";
 import { observer } from "mobx-react";
-import React = require("react");
-import { action, computed, trace } from "mobx";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { DragManager } from "../../util/DragManager";
-import "./CollectionFreeFormView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
-import { KeyStore } from "../../../fields/KeyStore";
 import { Document } from "../../../fields/Document";
-import { ListField } from "../../../fields/ListField";
 import { FieldWaiting } from "../../../fields/Field";
+import { KeyStore } from "../../../fields/KeyStore";
+import { ListField } from "../../../fields/ListField";
+import { TextField } from "../../../fields/TextField";
+import { DragManager } from "../../util/DragManager";
 import { Transform } from "../../util/Transform";
-import { DocumentView } from "../nodes/DocumentView";
 import { undoBatch } from "../../util/UndoManager";
-import { CollectionViewBase, SubCollectionViewProps } from "./CollectionViewBase";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { CollectionSchemaView } from "../collections/CollectionSchemaView";
+import { CollectionView } from "../collections/CollectionView";
+import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
+import { DocumentView } from "../nodes/DocumentView";
+import { FormattedTextBox } from "../nodes/FormattedTextBox";
+import { ImageBox } from "../nodes/ImageBox";
+import "./CollectionFreeFormView.scss";
+import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
+import { CollectionViewBase } from "./CollectionViewBase";
+import React = require("react");
+const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
 
 @observer
 export class CollectionFreeFormView extends CollectionViewBase {
@@ -145,16 +152,13 @@ export class CollectionFreeFormView extends CollectionViewBase {
         });
     }
 
-    getTransform = (): Transform => {
-        return this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).transform(this.getLocalTransform())
-    }
 
-    getLocalTransform = (): Transform => {
-        return Transform.Identity.translate(-this.panX, -this.panY).scale(1 / this.scale);
+    @computed get backgroundLayout(): string | undefined {
+        let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
+        if (field && field !== "<Waiting>") {
+            return field.Data;
+        }
     }
-
-    noScaling = () => 1;
-
     @computed
     get views() {
         const { fieldKey, Document } = this.props;
@@ -175,6 +179,21 @@ export class CollectionFreeFormView extends CollectionViewBase {
         return null;
     }
 
+    @computed
+    get backgroundView() {
+        return !this.backgroundLayout ? (null) :
+            (<JsxParser
+                components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
+                bindings={this.props.bindings}
+                jsx={this.backgroundLayout}
+                showWarnings={true}
+                onError={(test: any) => console.log(test)}
+            />);
+    }
+    getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).transform(this.getLocalTransform())
+    getLocalTransform = (): Transform => Transform.Identity.translate(-this.panX, -this.panY).scale(1 / this.scale);
+    noScaling = () => 1;
+
     render() {
         const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0);
         const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0);
@@ -190,8 +209,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 <div className="collectionfreeformview"
                     style={{ width: "100%", transformOrigin: "left top", transform: ` translate(${panx}px, ${pany}px) scale(${this.zoomScaling}, ${this.zoomScaling})` }}
                     ref={this._canvasRef}>
-
-                    {this.props.BackgroundView ? this.props.BackgroundView() : null}
+                    {this.backgroundView}
                     {this.views}
                 </div>
             </div>
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index ca47f6998..d2db93120 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -34,7 +34,8 @@ export class CollectionSchemaView extends CollectionViewBase {
             fieldKey: rowProps.value[1],
             isSelected: () => false,
             select: () => { },
-            isTopMost: false
+            isTopMost: false,
+            bindings: {}
         }
         let contents = (
             <FieldView {...props} />
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index ff1803ec3..90080ab43 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -28,7 +28,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
 
     public static LayoutString(fieldKey: string = "DataKey") {
         return `<CollectionView Document={Document}
-                    ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select}
+                    ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} isSelected={isSelected} select={select} bindings={bindings}
                     isTopMost={isTopMost} BackgroundView={BackgroundView} />`;
     }
     public active = () => {
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 06de56383..da7f71163 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -19,7 +19,7 @@ export interface CollectionViewProps {
     isSelected: () => boolean;
     isTopMost: boolean;
     select: (ctrlPressed: boolean) => void;
-    BackgroundView?: () => JSX.Element;
+    bindings: any;
 }
 export interface SubCollectionViewProps extends CollectionViewProps {
     active: () => boolean;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d9b1d90d8..a9e211431 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -5,7 +5,6 @@ import { Field, FieldWaiting, Opt } from "../../../fields/Field";
 import { Key } from "../../../fields/Key";
 import { KeyStore } from "../../../fields/KeyStore";
 import { ListField } from "../../../fields/ListField";
-import { TextField } from "../../../fields/TextField";
 import { DragManager } from "../../util/DragManager";
 import { SelectionManager } from "../../util/SelectionManager";
 import { Transform } from "../../util/Transform";
@@ -84,21 +83,13 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     private _downX: number = 0;
     private _downY: number = 0;
 
-    get screenRect(): ClientRect | DOMRect {
-        return (this._mainCont.current) ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
-    }
     @computed get active(): boolean { return SelectionManager.IsSelected(this) || !this.props.ContainingCollectionView || this.props.ContainingCollectionView.active(); }
     @computed get topMost(): boolean { return !this.props.ContainingCollectionView || this.props.ContainingCollectionView.collectionViewType == CollectionViewType.Docking; }
     @computed get layout(): string { return this.props.Document.GetText(KeyStore.Layout, "<p>Error loading layout data</p>"); }
     @computed get layoutKeys(): Key[] { return this.props.Document.GetData(KeyStore.LayoutKeys, ListField, new Array<Key>()); }
     @computed get layoutFields(): Key[] { return this.props.Document.GetData(KeyStore.LayoutFields, ListField, new Array<Key>()); }
-    @computed get backgroundLayout(): string | undefined {
-        let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
-        if (field && field !== "<Waiting>") {
-            return field.Data;
-        }
-    }
 
+    screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
 
     onPointerDown = (e: React.PointerEvent): void => {
         this._downX = e.clientX;
@@ -174,6 +165,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         ContextMenu.Instance.displayMenu(e.pageX - 15, e.pageY - 15)
     }
 
+    @action
     onContextMenu = (e: React.MouseEvent): void => {
         e.preventDefault()
         e.stopPropagation();
@@ -230,32 +222,19 @@ export class DocumentView extends React.Component<DocumentViewProps> {
             let field = this.props.Document.Get(key);
             this._documentBindings[key.Name] = field && field != FieldWaiting ? field.GetValue() : field;
         }
-
-        /*
-        tfs:
-        Should this be moved to CollectionFreeformView or another component that renders
-        Document backgrounds (or contents based on a layout key, which could be used here as well)
-         that CollectionFreeformView uses? It seems like a lot for it to be here considering only one view currently uses it...
-         */
-        let backgroundLayout = this.backgroundLayout;
-        if (backgroundLayout) {
-            let backgroundView = () => (<JsxParser
-                components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
-                bindings={this._documentBindings}
-                jsx={this.backgroundLayout}
-                showWarnings={true}
-                onError={(test: any) => { console.log(test) }}
-            />);
-            this._documentBindings.BackgroundView = backgroundView;
-        }
+        this._documentBindings.bindings = this._documentBindings;
 
         var scaling = this.props.ContentScaling();
         var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
         var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
-        var nodeWidth = nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%";
-        var nodeHeight = nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%";
         return (
-            <div className="documentView-node" ref={this._mainCont} style={{ width: nodeWidth, height: nodeHeight, transformOrigin: "left top", transform: `scale(${scaling},${scaling})` }}
+            <div className="documentView-node" ref={this._mainCont}
+                style={{
+                    width: nativeWidth > 0 ? nativeWidth.toString() + "px" : "100%",
+                    height: nativeHeight > 0 ? nativeHeight.toString() + "px" : "100%",
+                    transformOrigin: "left top",
+                    transform: `scale(${scaling},${scaling})`
+                }}
                 onContextMenu={this.onContextMenu}
                 onPointerDown={this.onPointerDown} >
                 {this.mainContent}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 821172d71..97d3f2a85 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -22,11 +22,12 @@ export interface FieldViewProps {
     isSelected: () => boolean;
     select: () => void;
     isTopMost: boolean;
+    bindings: any;
 }
 
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} fieldKey={DataKey} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; }
+    public static LayoutString(fieldType: { name: string }) { return `<${fieldType.name} doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={DataKey} isSelected={isSelected} select={select} isTopMost={isTopMost} />`; }
     @computed
     get field(): FieldValue<Field> {
         const { doc, fieldKey } = this.props;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index b17650d06..16728d471 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -6,7 +6,6 @@ import { schema } from "prosemirror-schema-basic";
 import { EditorState, Transaction } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { Opt, FieldWaiting, FieldValue } from "../../../fields/Field";
-import { SelectionManager } from "../../util/SelectionManager";
 import "./FormattedTextBox.scss";
 import React = require("react")
 import { RichTextField } from "../../../fields/RichTextField";
-- 
cgit v1.2.3-70-g09d2


From c9d440e487a4c0b648ac68d040f844b762ec1376 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 22 Feb 2019 22:36:22 -0500
Subject: added dropping of image urls

---
 src/client/views/DocumentDecorations.tsx           |  8 ++--
 .../views/collections/CollectionFreeFormView.tsx   |  8 ++--
 .../views/collections/CollectionViewBase.tsx       | 55 +++++++++++++++-------
 3 files changed, 48 insertions(+), 23 deletions(-)

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 09df8cdaa..2f012913d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -21,13 +21,13 @@ export class DocumentDecorations extends React.Component {
 
     @computed
     get Bounds(): { x: number, y: number, b: number, r: number } {
-        return SelectionManager.SelectedDocuments().reduce((bounds, element) => {
-            if (element.props.isTopMost) {
+        return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
+            if (documentView.props.isTopMost) {
                 return bounds;
             }
-            let transform = (element.props.ScreenToLocalTransform().scale(element.props.ContentScaling())).inverse();
+            let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
             var [sptX, sptY] = transform.transformPoint(0, 0);
-            let [bptX, bptY] = transform.transformPoint(element.props.PanelWidth(), element.props.PanelHeight());
+            let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
             return {
                 x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
                 r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 565402046..c12a82617 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -19,6 +19,7 @@ import "./CollectionFreeFormView.scss";
 import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
 import { CollectionViewBase } from "./CollectionViewBase";
 import React = require("react");
+import { Documents } from "../../documents/Documents";
 const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
 
 @observer
@@ -126,9 +127,10 @@ export class CollectionFreeFormView extends CollectionViewBase {
     onDrop = (e: React.DragEvent): void => {
         const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0);
         const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0);
-        let x = e.pageX - panx
-        let y = e.pageY - pany
-        super.onDrop(e, { x: x, y: y });
+        let transform = this.getTransform();
+
+        var pt = transform.transformPoint(e.pageX, e.pageY);
+        super.onDrop(e, { x: pt[0], y: pt[1] });
     }
 
     onDragOver = (): void => {
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index da7f71163..4bf0d074a 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -56,27 +56,50 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
     protected onDrop(e: React.DragEvent, options: DocumentOptions): void {
         e.stopPropagation()
         e.preventDefault()
-        let fReader = new FileReader()
-        let file = e.dataTransfer.items[0].getAsFile();
         let that = this;
 
-        fReader.addEventListener("load", action("drop", () => {
-            if (fReader.result) {
-                let url = "" + fReader.result;
-                let doc = Documents.ImageDocument(url, options)
-                let docs = that.props.Document.GetT(KeyStore.Data, ListField);
-                if (docs != FieldWaiting) {
-                    if (!docs) {
-                        docs = new ListField<Document>();
-                        that.props.Document.Set(KeyStore.Data, docs)
+        for (let i = 0; i < e.dataTransfer.items.length; i++) {
+            let item = e.dataTransfer.items[i];
+            if (item.kind === "string" && item.type.indexOf("uri") != -1) {
+                e.dataTransfer.items[i].getAsString(function (s) {
+                    action(() => {
+                        var img = Documents.ImageDocument(s, { ...options, nativeWidth: 300, nativeHeight: 300, width: 300, height: 300 })
+
+                        let docs = that.props.Document.GetT(KeyStore.Data, ListField);
+                        if (docs != FieldWaiting) {
+                            if (!docs) {
+                                docs = new ListField<Document>();
+                                that.props.Document.Set(KeyStore.Data, docs)
+                            }
+                            docs.Data.push(img);
+                        }
+                    })()
+
+                })
+            }
+            if (item.kind == "file" && item.type.indexOf("image")) {
+                let fReader = new FileReader()
+                let file = item.getAsFile();
+
+                fReader.addEventListener("load", action("drop", () => {
+                    if (fReader.result) {
+                        let url = "" + fReader.result;
+                        let doc = Documents.ImageDocument(url, options)
+                        let docs = that.props.Document.GetT(KeyStore.Data, ListField);
+                        if (docs != FieldWaiting) {
+                            if (!docs) {
+                                docs = new ListField<Document>();
+                                that.props.Document.Set(KeyStore.Data, docs)
+                            }
+                            docs.Data.push(doc);
+                        }
                     }
-                    docs.Data.push(doc);
+                }), false)
+
+                if (file) {
+                    fReader.readAsDataURL(file)
                 }
             }
-        }), false)
-
-        if (file) {
-            fReader.readAsDataURL(file)
         }
     }
 }
-- 
cgit v1.2.3-70-g09d2


From 6c451fa68db11c80f452ca7c85242ad98d867ab0 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Sat, 23 Feb 2019 04:49:48 -0500
Subject: Added web clippings on drag and drop

---
 src/client/documents/Documents.ts                  | 24 +++++++++++++++++++++
 .../views/collections/CollectionFreeFormView.tsx   |  3 ++-
 .../views/collections/CollectionViewBase.tsx       |  9 ++++++++
 src/client/views/nodes/DocumentView.tsx            |  3 ++-
 src/client/views/nodes/FieldView.tsx               |  4 ++++
 src/client/views/nodes/WebView.tsx                 | 22 +++++++++++++++++++
 src/fields/HtmlField.ts                            | 25 ++++++++++++++++++++++
 src/fields/KeyStore.ts                             |  1 +
 src/fields/TextField.ts                            |  2 +-
 src/server/Message.ts                              |  2 +-
 src/server/ServerUtil.ts                           |  6 +++++-
 11 files changed, 96 insertions(+), 5 deletions(-)
 create mode 100644 src/client/views/nodes/WebView.tsx
 create mode 100644 src/fields/HtmlField.ts

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6ec5aa711..d8e1db0b0 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -9,6 +9,8 @@ import { ImageField } from "../../fields/ImageField";
 import { ImageBox } from "../views/nodes/ImageBox";
 import { CollectionView, CollectionViewType } from "../views/collections/CollectionView";
 import { FieldView } from "../views/nodes/FieldView";
+import { HtmlField } from "../../fields/HtmlField";
+import { WebView } from "../views/nodes/WebView";
 
 export interface DocumentOptions {
     x?: number;
@@ -79,6 +81,28 @@ export namespace Documents {
         return doc;
     }
 
+    let htmlProto: Document;
+    const htmlProtoId = "htmlProto";
+    function GetHtmlPrototype(): Document {
+        if (!htmlProto) {
+            htmlProto = new Document(htmlProtoId);
+            htmlProto.Set(KeyStore.X, new NumberField(0));
+            htmlProto.Set(KeyStore.Y, new NumberField(0));
+            htmlProto.Set(KeyStore.Width, new NumberField(300));
+            htmlProto.Set(KeyStore.Height, new NumberField(150));
+            htmlProto.Set(KeyStore.Layout, new TextField(WebView.LayoutString()));
+            htmlProto.Set(KeyStore.LayoutKeys, new ListField([KeyStore.Data]));
+        }
+        return htmlProto;
+    }
+
+    export function HtmlDocument(html: string, options: DocumentOptions = {}): Document {
+        let doc = GetHtmlPrototype().MakeDelegate();
+        setupOptions(doc, options);
+        doc.Set(KeyStore.Data, new HtmlField(html));
+        return doc;
+    }
+
     let imageProto: Document;
     const imageProtoId = "imageProto";
     function GetImagePrototype(): Document {
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 4799eda97..b031c35a7 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -13,6 +13,7 @@ import { CollectionSchemaView } from "../collections/CollectionSchemaView";
 import { CollectionView } from "../collections/CollectionView";
 import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
 import { DocumentView } from "../nodes/DocumentView";
+import { WebView } from "../nodes/WebView";
 import { FormattedTextBox } from "../nodes/FormattedTextBox";
 import { ImageBox } from "../nodes/ImageBox";
 import "./CollectionFreeFormView.scss";
@@ -185,7 +186,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
     get backgroundView() {
         return !this.backgroundLayout ? (null) :
             (<JsxParser
-                components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
+                components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }}
                 bindings={this.props.bindings}
                 jsx={this.backgroundLayout}
                 showWarnings={true}
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 4bf0d074a..7e269caf1 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -58,6 +58,15 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         e.preventDefault()
         let that = this;
 
+        let html = e.dataTransfer.getData("text/html");
+        let text = e.dataTransfer.getData("text/plain");
+        if (html) {
+            let htmlDoc = Documents.HtmlDocument(html, { ...options });
+            htmlDoc.SetText(KeyStore.DocumentText, text);
+            this.props.addDocument(htmlDoc);
+            return;
+        }
+
         for (let i = 0; i < e.dataTransfer.items.length; i++) {
             let item = e.dataTransfer.items[i];
             if (item.kind === "string" && item.type.indexOf("uri") != -1) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a9e211431..ad1328e5d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -12,6 +12,7 @@ import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
 import { CollectionSchemaView } from "../collections/CollectionSchemaView";
 import { CollectionView, CollectionViewType } from "../collections/CollectionView";
+import { WebView } from "./WebView";
 import { ContextMenu } from "../ContextMenu";
 import { FormattedTextBox } from "../nodes/FormattedTextBox";
 import { ImageBox } from "../nodes/ImageBox";
@@ -196,7 +197,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     @computed get mainContent() {
         return <JsxParser
-            components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView }}
+            components={{ FormattedTextBox, ImageBox, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, WebView }}
             bindings={this._documentBindings}
             jsx={this.layout}
             showWarnings={true}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 9dda08d8d..1a9d325db 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -10,6 +10,8 @@ import { ImageField } from "../../../fields/ImageField";
 import { Key } from "../../../fields/Key";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { ImageBox } from "./ImageBox";
+import { HtmlField } from "../../../fields/HtmlField";
+import { WebView } from "./WebView";
 
 //
 // these properties get assigned through the render() method of the DocumentView when it creates this node.
@@ -50,6 +52,8 @@ export class FieldView extends React.Component<FieldViewProps> {
         }
         else if (field instanceof NumberField) {
             return <p>{field.Data}</p>
+        } else if (field instanceof HtmlField) {
+            return <WebView {...this.props} />
         } else if (field != FieldWaiting) {
             return <p>{field.GetValue}</p>
         } else
diff --git a/src/client/views/nodes/WebView.tsx b/src/client/views/nodes/WebView.tsx
new file mode 100644
index 000000000..717aa8bf5
--- /dev/null
+++ b/src/client/views/nodes/WebView.tsx
@@ -0,0 +1,22 @@
+import { FieldViewProps, FieldView } from "./FieldView";
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+import { KeyStore } from "../../../fields/KeyStore";
+import React = require('react')
+import { TextField } from "../../../fields/TextField";
+import { HtmlField } from "../../../fields/HtmlField";
+import { RichTextField } from "../../../fields/RichTextField";
+
+@observer
+export class WebView extends React.Component<FieldViewProps> {
+    public static LayoutString(fieldStr: string = "DataKey") { return FieldView.LayoutString(WebView, fieldStr) }
+
+    @computed
+    get html(): string {
+        return this.props.doc.GetData(KeyStore.Data, HtmlField, "" as string);
+    }
+
+    render() {
+        return <span dangerouslySetInnerHTML={{ __html: this.html }}></span>
+    }
+}
\ No newline at end of file
diff --git a/src/fields/HtmlField.ts b/src/fields/HtmlField.ts
new file mode 100644
index 000000000..a07326095
--- /dev/null
+++ b/src/fields/HtmlField.ts
@@ -0,0 +1,25 @@
+import { BasicField } from "./BasicField";
+import { Types } from "../server/Message";
+import { FieldId } from "./Field";
+
+export class HtmlField extends BasicField<string> {
+    constructor(data: string = "<html></html>", id?: FieldId, save: boolean = true) {
+        super(data, save, id);
+    }
+
+    ToScriptString(): string {
+        return `new HtmlField("${this.Data}")`;
+    }
+
+    Copy() {
+        return new HtmlField(this.Data);
+    }
+
+    ToJson(): { _id: string; type: Types; data: any; } {
+        return {
+            type: Types.Html,
+            data: this.Data,
+            _id: this.Id,
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 42e3f6b58..290fa2be7 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -24,4 +24,5 @@ export namespace KeyStore {
     export const ColumnsKey = new Key("SchemaColumns");
     export const Caption = new Key("Caption");
     export const ActiveFrame = new Key("ActiveFrame");
+    export const DocumentText = new Key("DocumentText");
 }
diff --git a/src/fields/TextField.ts b/src/fields/TextField.ts
index ad96ab6d9..71d8ea310 100644
--- a/src/fields/TextField.ts
+++ b/src/fields/TextField.ts
@@ -22,4 +22,4 @@ export class TextField extends BasicField<string> {
             _id: this.Id
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 7f3190f7f..80fc9a80d 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -45,7 +45,7 @@ export class GetFieldArgs {
 }
 
 export enum Types {
-    Number, List, Key, Image, Document, Text, RichText, DocumentReference
+    Number, List, Key, Image, Document, Text, RichText, DocumentReference, Html
 }
 
 export class DocumentTransfer implements Transferable {
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
index 46c409ec4..08e72fdae 100644
--- a/src/server/ServerUtil.ts
+++ b/src/server/ServerUtil.ts
@@ -9,6 +9,7 @@ import { Document } from './../fields/Document';
 import { Server } from './../client/Server';
 import { Types } from './Message';
 import { Utils } from '../Utils';
+import { HtmlField } from '../fields/HtmlField';
 
 export class ServerUtils {
     public static FromJson(json: any): Field {
@@ -27,6 +28,8 @@ export class ServerUtils {
                 return new NumberField(data, id, false)
             case Types.Text:
                 return new TextField(data, id, false)
+            case Types.Html:
+                return new HtmlField(data, id, false)
             case Types.RichText:
                 return new RichTextField(data, id, false)
             case Types.Key:
@@ -42,7 +45,8 @@ export class ServerUtils {
                     doc._proxies.set(element[0], element[1]);
                 });
                 return doc
+            default:
+                throw Error("Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here");
         }
-        return new TextField(data, id)
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From a2bec39bc152f7493e4171b6446040fa381e6463 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <abdullah_ahmed@brown.edu>
Date: Sun, 17 Mar 2019 20:57:20 -0400
Subject: added tuple field, beginnig remote cursor display

---
 src/client/views/Main.tsx                          | 12 +++-
 .../views/collections/CollectionFreeFormView.tsx   | 15 ++++
 .../views/collections/CollectionViewBase.tsx       | 34 ++++++++-
 src/fields/KeyStore.ts                             |  2 +-
 src/fields/TupleField.ts                           | 59 ++++++++++++++++
 src/server/Message.ts                              |  2 +-
 src/server/RouteStore.ts                           |  3 +
 src/server/ServerUtil.ts                           |  7 +-
 src/server/index.ts                                | 80 +++++++++++-----------
 9 files changed, 169 insertions(+), 45 deletions(-)
 create mode 100644 src/fields/TupleField.ts

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index e76a5e04b..268f70de1 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -38,6 +38,7 @@ import { faPenNib } from '@fortawesome/free-solid-svg-icons';
 import { faFilm } from '@fortawesome/free-solid-svg-icons';
 import { faMusic } from '@fortawesome/free-solid-svg-icons';
 import Measure from 'react-measure';
+import { DashUserModel } from '../../server/authentication/models/user_model';
 
 @observer
 export class Main extends React.Component {
@@ -49,12 +50,13 @@ export class Main extends React.Component {
     @observable public pheight: number = 0;
 
     private mainDocId: string | undefined;
+    private currentUser?: DashUserModel;
 
     constructor(props: Readonly<{}>) {
         super(props);
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
-        if (window.location.pathname !== "/home") {
+        if (window.location.pathname !== RouteStore.home) {
             let pathname = window.location.pathname.split("/");
             this.mainDocId = pathname[pathname.length - 1];
         }
@@ -75,6 +77,14 @@ export class Main extends React.Component {
         Documents.initProtos(() => {
             this.initAuthenticationRouters();
         });
+
+        request.get(this.prepend(RouteStore.getCurrUser), (error, response, body) => {
+            if (body) {
+                this.currentUser = body as DashUserModel;
+            } else {
+                throw new Error("There should be a user! Why does Dash think there isn't one?")
+            }
+        })
     }
 
     initEventListeners = () => {
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index bb28dd20a..a3a596c37 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -322,6 +322,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
         return (
             <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
                 onPointerDown={this.onPointerDown}
+                onPointerMove={(e) => super.setCursorPosition(this.props.ScreenToLocalTransform().transformPoint(e.screenX, e.screenY))}
                 onWheel={this.onPointerWheel}
                 onDrop={this.onDrop.bind(this)}
                 onDragOver={this.onDragOver}
@@ -329,6 +330,20 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }}
                 tabIndex={0}
                 ref={this.createDropTarget}>
+                {super.getCursors().map(entry => {
+                    let point = entry.Data[1]
+                    return (
+                        <div
+                            style={{
+                                position: 'absolute',
+                                left: point[0],
+                                top: point[1],
+                                borderRadius: "50%",
+                                background: "pink"
+                            }}
+                        />
+                    );
+                })}
                 <div className="collectionfreeformview"
                     style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}
                     ref={this._canvasRef}>
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 4a2761139..81d7f4077 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -1,4 +1,4 @@
-import { action, runInAction } from "mobx";
+import { action, runInAction, observable, computed } from "mobx";
 import { Document } from "../../../fields/Document";
 import { ListField } from "../../../fields/ListField";
 import React = require("react");
@@ -12,6 +12,9 @@ import { Key } from "../../../fields/Key";
 import { Transform } from "../../util/Transform";
 import { CollectionView } from "./CollectionView";
 import { RouteStore } from "../../../server/RouteStore";
+import { TupleField } from "../../../fields/TupleField";
+import { Server } from "mongodb";
+import { DashUserModel } from "../../../server/authentication/models/user_model";
 
 export interface CollectionViewProps {
     fieldKey: Key;
@@ -25,13 +28,17 @@ export interface CollectionViewProps {
     panelHeight: () => number;
     focus: (doc: Document) => void;
 }
+
 export interface SubCollectionViewProps extends CollectionViewProps {
     active: () => boolean;
     addDocument: (doc: Document) => void;
     removeDocument: (doc: Document) => boolean;
     CollectionView: CollectionView;
+    currentUser?: DashUserModel;
 }
 
+export type CursorEntry = TupleField<DashUserModel, [number, number]>;
+
 export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
     private dropDisposer?: DragManager.DragDropDisposer;
     protected createDropTarget = (ele: HTMLDivElement) => {
@@ -43,6 +50,31 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         }
     }
 
+    protected setCursorPosition(position: [number, number]) {
+        let user = this.props.currentUser;
+        if (user && user.id) {
+            let ind;
+            let doc = this.props.Document;
+            let cursors = doc.GetOrCreate<ListField<CursorEntry>>(KeyStore.Cursors, ListField, false).Data;
+            let entry = new TupleField<DashUserModel, [number, number]>([user.id, position]);
+            // if ((ind = cursors.findIndex(entry => entry.Data[0] === user.id)) > -1) {
+            //     cursors[ind] = entry;
+            // } else {
+            //     cursors.push(entry);
+            // }
+        }
+    }
+
+    protected getCursors(): CursorEntry[] {
+        let user = this.props.currentUser;
+        if (user && user.id) {
+            let doc = this.props.Document;
+            // return doc.GetList<CursorEntry>(KeyStore.Cursors, []).filter(entry => entry.Data[0] !== user.id);
+        }
+        return [];
+    }
+
+
     @undoBatch
     @action
     protected drop(e: Event, de: DragManager.DropEvent) {
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 08a1c00b9..c6e58ee35 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -37,5 +37,5 @@ export namespace KeyStore {
     export const CurPage = new Key("CurPage");
     export const NumPages = new Key("NumPages");
     export const Ink = new Key("Ink");
-    export const ActiveUsers = new Key("Active Users");
+    export const Cursors = new Key("Cursors");
 }
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
new file mode 100644
index 000000000..778bd5d2f
--- /dev/null
+++ b/src/fields/TupleField.ts
@@ -0,0 +1,59 @@
+import { action, IArrayChange, IArraySplice, IObservableArray, observe, observable, Lambda } from "mobx";
+import { Server } from "../client/Server";
+import { UndoManager } from "../client/util/UndoManager";
+import { Types } from "../server/Message";
+import { BasicField } from "./BasicField";
+import { Field, FieldId } from "./Field";
+
+export class TupleField<T, U> extends BasicField<[T, U]> {
+    constructor(data: [T, U], id?: FieldId, save: boolean = true) {
+        super(data, save, id);
+        if (save) {
+            Server.UpdateField(this);
+        }
+        this.observeTuple();
+    }
+
+    private observeDisposer: Lambda | undefined;
+    private observeTuple(): void {
+        this.observeDisposer = observe(this.Data as (T | U)[] as IObservableArray<T | U>, (change: IArrayChange<T | U> | IArraySplice<T | U>) => {
+            if (change.type === "update") {
+                UndoManager.AddEvent({
+                    undo: () => this.Data[change.index] = change.oldValue,
+                    redo: () => this.Data[change.index] = change.newValue
+                })
+                Server.UpdateField(this);
+            } else {
+                throw new Error("Why are you messing with the length of a tuple, huh?");
+            }
+        });
+    }
+
+    protected setData(value: [T, U]) {
+        if (this.observeDisposer) {
+            this.observeDisposer()
+        }
+        this.data = observable(value) as (T | U)[] as [T, U];
+        this.observeTuple();
+    }
+
+    UpdateFromServer(values: [T, U]) {
+        this.setData(values);
+    }
+
+    ToScriptString(): string {
+        return `new TupleField([${this.Data[0], this.Data[1]}])`;
+    }
+
+    Copy(): Field {
+        return new TupleField<T, U>(this.Data);
+    }
+
+    ToJson(): { type: Types, data: [T, U], _id: string } {
+        return {
+            type: Types.List,
+            data: this.Data,
+            _id: this.Id
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 8a00f6b59..a2d1ab829 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -45,7 +45,7 @@ export class GetFieldArgs {
 }
 
 export enum Types {
-    Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF
+    Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF, Tuple
 }
 
 export class DocumentTransfer implements Transferable {
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index f12aed85e..683fd6d7e 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -13,12 +13,15 @@ export enum RouteStore {
     images = "/images",
 
     // USER AND WORKSPACES
+    getCurrUser = "/getCurrentUser",
     addWorkspace = "/addWorkspaceId",
     getAllWorkspaces = "/getAllWorkspaceIds",
     getActiveWorkspace = "/getActiveWorkspaceId",
     setActiveWorkspace = "/setActiveWorkspaceId",
     updateCursor = "/updateCursor",
 
+    openDocumentWithId = "/doc/:docId",
+
     // AUTHENTICATION
     signup = "/signup",
     login = "/login",
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
index 5331c9e30..dce4bada5 100644
--- a/src/server/ServerUtil.ts
+++ b/src/server/ServerUtil.ts
@@ -14,8 +14,9 @@ import { HtmlField } from '../fields/HtmlField';
 import { WebField } from '../fields/WebField';
 import { AudioField } from '../fields/AudioField';
 import { VideoField } from '../fields/VideoField';
-import {InkField} from '../fields/InkField';
-import {PDFField} from '../fields/PDFField';
+import { InkField } from '../fields/InkField';
+import { PDFField } from '../fields/PDFField';
+import { TupleField } from '../fields/TupleField';
 
 
 
@@ -55,6 +56,8 @@ export class ServerUtils {
                 return new AudioField(new URL(data), id, false)
             case Types.Video:
                 return new VideoField(new URL(data), id, false)
+            case Types.Tuple:
+                return new TupleField(data, id, false);
             case Types.Ink:
                 return InkField.FromJson(id, data);
             case Types.Document:
diff --git a/src/server/index.ts b/src/server/index.ts
index d0df95ca3..7baf896b7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -77,6 +77,10 @@ app.use((req, res, next) => {
     next();
 });
 
+app.get("/hello", (req, res) => {
+    res.send("<p>Hello</p>");
+})
+
 enum Method {
     GET,
     POST
@@ -88,26 +92,26 @@ enum Method {
  * does not execute unless Passport authentication detects a user logged in.
  * @param method whether or not the request is a GET or a POST
  * @param route the forward slash prepended path name (reference and add to RouteStore.ts)
- * @param handler the action to invoke, recieving a DashUserModel and the expected request and response
+ * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response
  * @param onRejection an optional callback invoked on return if no user is found to be logged in
  */
 function addSecureRoute(method: Method,
     route: string,
-    handler: (user: DashUserModel, req: express.Request, res: express.Response) => void,
+    handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
     onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout)) {
     switch (method) {
         case Method.GET:
             app.get(route, (req, res) => {
                 const dashUser: DashUserModel = req.user;
                 if (!dashUser) return onRejection(res);
-                handler(dashUser, req, res);
+                handler(dashUser, res, req);
             });
             break;
         case Method.POST:
             app.post(route, (req, res) => {
                 const dashUser: DashUserModel = req.user;
                 if (!dashUser) return onRejection(res);
-                handler(dashUser, req, res);
+                handler(dashUser, res, req);
             });
             break;
     }
@@ -120,66 +124,64 @@ let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap();
 app.use(express.static(__dirname + RouteStore.public));
 app.use(RouteStore.images, express.static(__dirname + RouteStore.public))
 
-addSecureRoute(Method.POST, RouteStore.upload, (user, req, res) => {
-    let form = new formidable.IncomingForm()
-    form.uploadDir = __dirname + "/public/files/"
-    form.keepExtensions = true
-    // let path = req.body.path;
-    console.log("upload")
-    form.parse(req, (err, fields, files) => {
-        console.log("parsing")
-        let names: any[] = [];
-        for (const name in files) {
-            let file = files[name];
-            names.push(`/files/` + path.basename(file.path));
-        }
-        res.send(names);
-    });
-});
+// GETTERS
 
 // anyone attempting to navigate to localhost at this port will
 // first have to login
-addSecureRoute(Method.GET, RouteStore.root, (user, req, res) => {
+addSecureRoute(Method.GET, RouteStore.root, (user, res) => {
     res.redirect(RouteStore.home);
 });
 
-// YAY! SHOW THEM THEIR WORKSPACES NOW
-addSecureRoute(Method.GET, RouteStore.home, (user, req, res) => {
+addSecureRoute(Method.GET, RouteStore.home, (user, res) => {
     res.sendFile(path.join(__dirname, '../../deploy/index.html'));
 });
 
-addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, req, res) => {
+addSecureRoute(Method.GET, RouteStore.openDocumentWithId, (user, res) => {
+    res.sendFile(path.join(__dirname, '../../deploy/index.html'));
+});
+
+addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, res) => {
     res.send(user.activeWorkspaceId || "");
 });
 
-addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, req, res) => {
+addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, res) => {
     res.send(JSON.stringify(user.allWorkspaceIds));
 });
 
-addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, req, res) => {
+addSecureRoute(Method.GET, RouteStore.getCurrUser, (user, res) => {
+    res.send(JSON.stringify(user));
+});
+
+// SETTERS
+
+addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, res, req) => {
     user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => {
         res.sendStatus(err ? 500 : 200);
     });
 });
 
-addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, req, res) => {
+addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, res, req) => {
     user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => {
         res.sendStatus(err ? 500 : 200);
     });
 });
-// define a route handler for the default home page
-// app.get("/", (req, res) => {
-//     res.redirect("/doc/mainDoc");
-//     // res.sendFile(path.join(__dirname, '../../deploy/index.html'));
-// });
-
-app.get("/doc/:docId", (req, res) => {
-    res.sendFile(path.join(__dirname, '../../deploy/index.html'));
-})
 
-app.get("/hello", (req, res) => {
-    res.send("<p>Hello</p>");
-})
+addSecureRoute(Method.POST, RouteStore.upload, (user, res, req) => {
+    let form = new formidable.IncomingForm()
+    form.uploadDir = __dirname + "/public/files/"
+    form.keepExtensions = true
+    // let path = req.body.path;
+    console.log("upload")
+    form.parse(req, (err, fields, files) => {
+        console.log("parsing")
+        let names: any[] = [];
+        for (const name in files) {
+            let file = files[name];
+            names.push(`/files/` + path.basename(file.path));
+        }
+        res.send(names);
+    });
+});
 
 // AUTHENTICATION
 
-- 
cgit v1.2.3-70-g09d2


From 748412d972bd466a372fcf384448d3a00b42ee9f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <abdullah_ahmed@brown.edu>
Date: Mon, 18 Mar 2019 00:29:20 -0400
Subject: implemented multi-client remote cursor

---
 src/client/views/Main.tsx                          |  24 ++-
 .../views/collections/CollectionFreeFormView.tsx   |  38 +++--
 .../views/collections/CollectionViewBase.tsx       |  44 ++---
 src/fields/TupleField.ts                           |   2 +-
 src/server/ServerUtil.ts                           |   2 +
 src/server/authentication/models/user_utils.ts     |  22 +++
 src/server/index.ts                                | 179 ++++++++++++---------
 7 files changed, 179 insertions(+), 132 deletions(-)
 create mode 100644 src/server/authentication/models/user_utils.ts

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 268f70de1..fd756972b 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -39,6 +39,8 @@ import { faFilm } from '@fortawesome/free-solid-svg-icons';
 import { faMusic } from '@fortawesome/free-solid-svg-icons';
 import Measure from 'react-measure';
 import { DashUserModel } from '../../server/authentication/models/user_model';
+import { ServerUtils } from '../../server/ServerUtil';
+import { UserUtils } from '../../server/authentication/models/user_utils';
 
 @observer
 export class Main extends React.Component {
@@ -61,6 +63,8 @@ export class Main extends React.Component {
             this.mainDocId = pathname[pathname.length - 1];
         }
 
+        UserUtils.loadCurrentUserId();
+
         library.add(faFont);
         library.add(faImage);
         library.add(faFilePdf);
@@ -77,14 +81,6 @@ export class Main extends React.Component {
         Documents.initProtos(() => {
             this.initAuthenticationRouters();
         });
-
-        request.get(this.prepend(RouteStore.getCurrUser), (error, response, body) => {
-            if (body) {
-                this.currentUser = body as DashUserModel;
-            } else {
-                throw new Error("There should be a user! Why does Dash think there isn't one?")
-            }
-        })
     }
 
     initEventListeners = () => {
@@ -101,7 +97,7 @@ export class Main extends React.Component {
 
     initAuthenticationRouters = () => {
         // Load the user's active workspace, or create a new one if initial session after signup
-        request.get(this.prepend(RouteStore.getActiveWorkspace), (error, response, body) => {
+        request.get(ServerUtils.prepend(RouteStore.getActiveWorkspace), (error, response, body) => {
             if (this.mainDocId || body) {
                 Server.GetField(this.mainDocId || body, field => {
                     if (field instanceof Document) {
@@ -122,7 +118,7 @@ export class Main extends React.Component {
     createNewWorkspace = (init: boolean): void => {
         let mainDoc = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: `Main Container ${this.userWorkspaces.length + 1}` });
         let newId = mainDoc.Id;
-        request.post(this.prepend(RouteStore.addWorkspace), {
+        request.post(ServerUtils.prepend(RouteStore.addWorkspace), {
             body: { target: newId },
             json: true
         }, () => { if (init) this.populateWorkspaces(); });
@@ -141,7 +137,7 @@ export class Main extends React.Component {
     @action
     populateWorkspaces = () => {
         // retrieve all workspace documents from the server
-        request.get(this.prepend(RouteStore.getAllWorkspaces), (error, res, body) => {
+        request.get(ServerUtils.prepend(RouteStore.getAllWorkspaces), (error, res, body) => {
             let ids = JSON.parse(body) as string[];
             Server.GetFields(ids, action((fields: { [id: string]: Field }) => this.userWorkspaces = ids.map(id => fields[id] as Document)));
         });
@@ -149,7 +145,7 @@ export class Main extends React.Component {
 
     @action
     openWorkspace = (doc: Document): void => {
-        request.post(this.prepend(RouteStore.setActiveWorkspace), {
+        request.post(ServerUtils.prepend(RouteStore.setActiveWorkspace), {
             body: { target: doc.Id },
             json: true
         });
@@ -163,8 +159,6 @@ export class Main extends React.Component {
         }
     }
 
-    prepend = (extension: string) => window.location.origin + extension;
-
     render() {
         let imgRef = React.createRef<HTMLDivElement>();
         let pdfRef = React.createRef<HTMLDivElement>();
@@ -233,7 +227,7 @@ export class Main extends React.Component {
                 <div className="main-buttonDiv" style={{ top: '34px', left: '2px', position: 'absolute' }} ref={workspacesRef}>
                     <button onClick={this.toggleWorkspaces}>Workspaces</button></div>
                 <div className="main-buttonDiv" style={{ top: '34px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                    <button onClick={() => request.get(this.prepend(RouteStore.logout), () => { })}>Log Out</button></div>
+                    <button onClick={() => request.get(ServerUtils.prepend(RouteStore.logout), () => { })}>Log Out</button></div>
 
                 <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} />
                 {/* for the expandable add nodes menu. Not included with the above because once it expands it expands the whole div with it, making canvas interactions limited. */}
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index a3a596c37..89edd1397 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -322,7 +322,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
         return (
             <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
                 onPointerDown={this.onPointerDown}
-                onPointerMove={(e) => super.setCursorPosition(this.props.ScreenToLocalTransform().transformPoint(e.screenX, e.screenY))}
+                onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
                 onWheel={this.onPointerWheel}
                 onDrop={this.onDrop.bind(this)}
                 onDragOver={this.onDragOver}
@@ -330,20 +330,6 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px`, }}
                 tabIndex={0}
                 ref={this.createDropTarget}>
-                {super.getCursors().map(entry => {
-                    let point = entry.Data[1]
-                    return (
-                        <div
-                            style={{
-                                position: 'absolute',
-                                left: point[0],
-                                top: point[1],
-                                borderRadius: "50%",
-                                background: "pink"
-                            }}
-                        />
-                    );
-                })}
                 <div className="collectionfreeformview"
                     style={{ transformOrigin: "left top", transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}
                     ref={this._canvasRef}>
@@ -351,11 +337,33 @@ export class CollectionFreeFormView extends CollectionViewBase {
                     <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
                     <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} />
                     {this.views}
+                    {super.getCursors().map(entry => {
+                        if (entry.Data.length > 0) {
+                            let point = entry.Data[1]
+                            return (
+                                <div
+                                    key={entry.Data[0]}
+                                    style={{
+                                        position: 'absolute',
+                                        transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
+                                        zIndex: 10000,
+                                        transformOrigin: 'center center',
+                                        width: "20px",
+                                        height: "20px",
+                                        borderRadius: "50%",
+                                        background: "pink",
+                                        border: "2px solid black"
+                                    }}
+                                />
+                            );
+                        }
+                    })}
                 </div>
                 <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
                     addDocument={this.props.addDocument} removeDocument={this.props.removeDocument}
                     getMarqueeTransform={this.getMarqueeTransform} getTransform={this.getTransform} />
                 {this.overlayView}
+
             </div>
         );
     }
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 81d7f4077..02ee49a38 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -13,8 +13,8 @@ import { Transform } from "../../util/Transform";
 import { CollectionView } from "./CollectionView";
 import { RouteStore } from "../../../server/RouteStore";
 import { TupleField } from "../../../fields/TupleField";
-import { Server } from "mongodb";
 import { DashUserModel } from "../../../server/authentication/models/user_model";
+import { UserUtils } from "../../../server/authentication/models/user_utils";
 
 export interface CollectionViewProps {
     fieldKey: Key;
@@ -34,10 +34,9 @@ export interface SubCollectionViewProps extends CollectionViewProps {
     addDocument: (doc: Document) => void;
     removeDocument: (doc: Document) => boolean;
     CollectionView: CollectionView;
-    currentUser?: DashUserModel;
 }
 
-export type CursorEntry = TupleField<DashUserModel, [number, number]>;
+export type CursorEntry = TupleField<string, [number, number]>;
 
 export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
     private dropDisposer?: DragManager.DragDropDisposer;
@@ -50,31 +49,34 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         }
     }
 
+    @action
     protected setCursorPosition(position: [number, number]) {
-        let user = this.props.currentUser;
-        if (user && user.id) {
-            let ind;
-            let doc = this.props.Document;
-            let cursors = doc.GetOrCreate<ListField<CursorEntry>>(KeyStore.Cursors, ListField, false).Data;
-            let entry = new TupleField<DashUserModel, [number, number]>([user.id, position]);
-            // if ((ind = cursors.findIndex(entry => entry.Data[0] === user.id)) > -1) {
-            //     cursors[ind] = entry;
-            // } else {
-            //     cursors.push(entry);
-            // }
+        let ind;
+        let doc = this.props.Document;
+        let id = UserUtils.currentUserId;
+        if (id) {
+            doc.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, field => {
+                let cursors = field.Data;
+                if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0] === id)) > -1) {
+                    cursors[ind].Data[1] = position;
+                } else {
+                    let entry = new TupleField<string, [number, number]>([id, position]);
+                    cursors.push(entry);
+                }
+            })
+
+
         }
     }
 
     protected getCursors(): CursorEntry[] {
-        let user = this.props.currentUser;
-        if (user && user.id) {
-            let doc = this.props.Document;
-            // return doc.GetList<CursorEntry>(KeyStore.Cursors, []).filter(entry => entry.Data[0] !== user.id);
-        }
-        return [];
+        let doc = this.props.Document;
+        let id = UserUtils.currentUserId;
+        let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
+        let notMe = cursors.filter(entry => entry.Data[0] !== id);
+        return id ? notMe : [];
     }
 
-
     @undoBatch
     @action
     protected drop(e: Event, de: DragManager.DropEvent) {
diff --git a/src/fields/TupleField.ts b/src/fields/TupleField.ts
index 778bd5d2f..e2162c751 100644
--- a/src/fields/TupleField.ts
+++ b/src/fields/TupleField.ts
@@ -51,7 +51,7 @@ export class TupleField<T, U> extends BasicField<[T, U]> {
 
     ToJson(): { type: Types, data: [T, U], _id: string } {
         return {
-            type: Types.List,
+            type: Types.Tuple,
             data: this.Data,
             _id: this.Id
         }
diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts
index dce4bada5..f10f82deb 100644
--- a/src/server/ServerUtil.ts
+++ b/src/server/ServerUtil.ts
@@ -22,6 +22,8 @@ import { TupleField } from '../fields/TupleField';
 
 
 export class ServerUtils {
+    public static prepend(extension: string): string { return window.location.origin + extension; }
+
     public static FromJson(json: any): Field {
         let obj = json
         let data: any = obj.data
diff --git a/src/server/authentication/models/user_utils.ts b/src/server/authentication/models/user_utils.ts
new file mode 100644
index 000000000..1497a4ba4
--- /dev/null
+++ b/src/server/authentication/models/user_utils.ts
@@ -0,0 +1,22 @@
+import { DashUserModel } from "./user_model";
+import * as request from 'request'
+import { RouteStore } from "../../RouteStore";
+import { ServerUtils } from "../../ServerUtil";
+
+export class UserUtils {
+    private static current: string;
+
+    public static get currentUserId() {
+        return UserUtils.current;
+    }
+
+    public static loadCurrentUserId() {
+        request.get(ServerUtils.prepend(RouteStore.getCurrUser), (error, response, body) => {
+            if (body) {
+                UserUtils.current = JSON.parse(body) as string;
+            } else {
+                throw new Error("There should be a user! Why does Dash think there isn't one?")
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 17aba99ee..0512ebf72 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -6,19 +6,14 @@ import * as whm from 'webpack-hot-middleware';
 import * as path from 'path'
 import * as formidable from 'formidable'
 import * as passport from 'passport';
-import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message";
+import { MessageStore, Transferable } from "./Message";
 import { Client } from './Client';
 import { Socket } from 'socket.io';
 import { Utils } from '../Utils';
 import { ObservableMap } from 'mobx';
 import { FieldId, Field } from '../fields/Field';
 import { Database } from './database';
-import { ServerUtils } from './ServerUtil';
-import { ObjectID } from 'mongodb';
-import * as bcrypt from "bcrypt-nodejs";
-import { Document } from '../fields/Document';
 import * as io from 'socket.io'
-import * as passportConfig from './authentication/config/passport';
 import { getLogin, postLogin, getSignup, postSignup, getLogout, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller';
 const config = require('../../webpack.config');
 const compiler = webpack(config);
@@ -29,17 +24,14 @@ import expressFlash = require('express-flash');
 import flash = require('connect-flash');
 import * as bodyParser from 'body-parser';
 import * as session from 'express-session';
-// import cookieSession = require('cookie-session');
 import * as cookieParser from 'cookie-parser';
 import c = require("crypto");
 const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
-import { performance } from 'perf_hooks'
-import User, { DashUserModel } from './authentication/models/user_model';
+import { DashUserModel } from './authentication/models/user_model';
 import * as fs from 'fs';
 import * as request from 'request'
 import { RouteStore } from './RouteStore';
-import * as MobileDetect from 'mobile-detect';
 
 const download = (url: string, dest: fs.PathLike) => {
     request.get(url).pipe(fs.createWriteStream(dest));
@@ -56,7 +48,7 @@ mongoose.connection.on('connected', function () {
 
 app.use(cookieParser());
 app.use(session({
-    secret: `our secret`,
+    secret: "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc",
     resave: true,
     cookie: { maxAge: 7 * 24 * 60 * 60 },
     saveUninitialized: true,
@@ -91,30 +83,30 @@ enum Method {
  * It ensures that any requests leading to or containing user-sensitive information
  * does not execute unless Passport authentication detects a user logged in.
  * @param method whether or not the request is a GET or a POST
- * @param route the forward slash prepended path name (reference and add to RouteStore.ts)
  * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response
  * @param onRejection an optional callback invoked on return if no user is found to be logged in
+ * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler 
  */
 function addSecureRoute(method: Method,
-    route: string,
     handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
-    onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout)) {
-    switch (method) {
-        case Method.GET:
-            app.get(route, (req, res) => {
-                const dashUser: DashUserModel = req.user;
-                if (!dashUser) return onRejection(res);
-                handler(dashUser, res, req);
-            });
-            break;
-        case Method.POST:
-            app.post(route, (req, res) => {
-                const dashUser: DashUserModel = req.user;
-                if (!dashUser) return onRejection(res);
-                handler(dashUser, res, req);
-            });
-            break;
+    onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout),
+    ...subscribers: string[]
+) {
+    let abstracted = (req: express.Request, res: express.Response) => {
+        const dashUser: DashUserModel = req.user;
+        if (!dashUser) return onRejection(res);
+        handler(dashUser, res, req);
     }
+    subscribers.forEach(route => {
+        switch (method) {
+            case Method.GET:
+                app.get(route, abstracted);
+                break;
+            case Method.POST:
+                app.post(route, abstracted);
+                break;
+        }
+    });
 }
 
 // STATIC FILE SERVING
@@ -128,60 +120,87 @@ app.use(RouteStore.images, express.static(__dirname + RouteStore.public))
 
 // anyone attempting to navigate to localhost at this port will
 // first have to login
-addSecureRoute(Method.GET, RouteStore.root, (user, res) => {
-    res.redirect(RouteStore.home);
-});
-
-addSecureRoute(Method.GET, RouteStore.home, (user, res) => {
-    res.sendFile(path.join(__dirname, '../../deploy/index.html'));
-});
-
-addSecureRoute(Method.GET, RouteStore.openDocumentWithId, (user, res) => {
-    res.sendFile(path.join(__dirname, '../../deploy/index.html'));
-});
-
-addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, res) => {
-    res.send(user.activeWorkspaceId || "");
-});
-
-addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, res) => {
-    res.send(JSON.stringify(user.allWorkspaceIds));
-});
-
-addSecureRoute(Method.GET, RouteStore.getCurrUser, (user, res) => {
-    res.send(JSON.stringify(user));
-});
+addSecureRoute(
+    Method.GET,
+    (user, res) => res.redirect(RouteStore.home),
+    undefined,
+    RouteStore.root
+);
+
+addSecureRoute(
+    Method.GET,
+    (user, res) => res.sendFile(path.join(__dirname, '../../deploy/index.html')),
+    undefined,
+    RouteStore.home,
+    RouteStore.openDocumentWithId
+);
+
+addSecureRoute(
+    Method.GET,
+    (user, res) => res.send(user.activeWorkspaceId || ""),
+    undefined,
+    RouteStore.getActiveWorkspace,
+);
+
+addSecureRoute(
+    Method.GET,
+    (user, res) => res.send(JSON.stringify(user.allWorkspaceIds)),
+    undefined,
+    RouteStore.getAllWorkspaces
+);
+
+addSecureRoute(
+    Method.GET,
+    (user, res) => res.send(JSON.stringify(user.id)),
+    undefined,
+    RouteStore.getCurrUser
+);
 
 // SETTERS
 
-addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, res, req) => {
-    user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => {
-        res.sendStatus(err ? 500 : 200);
-    });
-});
-
-addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, res, req) => {
-    user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => {
-        res.sendStatus(err ? 500 : 200);
-    });
-});
-
-addSecureRoute(Method.POST, RouteStore.upload, (user, res, req) => {
-    let form = new formidable.IncomingForm()
-    form.uploadDir = __dirname + "/public/files/"
-    form.keepExtensions = true
-    // let path = req.body.path;
-    console.log("upload")
-    form.parse(req, (err, fields, files) => {
-        console.log("parsing")
-        let names: any[] = [];
-        for (const name in files) {
-            let file = files[name];
-            names.push(`/files/` + path.basename(file.path));
-        }
-        res.send(names);
-    });
-});
+addSecureRoute(
+    Method.POST,
+    (user, res, req) => {
+        user.update({ $set: { activeWorkspaceId: req.body.target } }, (err, raw) => {
+            res.sendStatus(err ? 500 : 200);
+        });
+    },
+    undefined,
+    RouteStore.setActiveWorkspace
+);
+
+addSecureRoute(
+    Method.POST,
+    (user, res, req) => {
+        user.update({ $push: { allWorkspaceIds: req.body.target } }, (err, raw) => {
+            res.sendStatus(err ? 500 : 200);
+        });
+    },
+    undefined,
+    RouteStore.addWorkspace
+);
+
+addSecureRoute(
+    Method.POST,
+    (user, res, req) => {
+        let form = new formidable.IncomingForm()
+        form.uploadDir = __dirname + "/public/files/"
+        form.keepExtensions = true
+        // let path = req.body.path;
+        console.log("upload")
+        form.parse(req, (err, fields, files) => {
+            console.log("parsing")
+            let names: any[] = [];
+            for (const name in files) {
+                let file = files[name];
+                names.push(`/files/` + path.basename(file.path));
+            }
+            res.send(names);
+        });
+    },
+    undefined,
+    RouteStore.upload
+);
 
 // AUTHENTICATION
 
-- 
cgit v1.2.3-70-g09d2


From 34f3b837334eb3d4a9416a8397c88cbd1ca421e0 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <abdullah_ahmed@brown.edu>
Date: Mon, 18 Mar 2019 01:43:50 -0400
Subject: flashier remote cursors

---
 src/client/views/Main.tsx                          |  4 +-
 .../views/collections/CollectionFreeFormView.tsx   | 75 +++++++++++++++++++---
 .../views/collections/CollectionViewBase.tsx       |  6 +-
 .../authentication/models/current_user_utils.ts    | 29 +++++++++
 src/server/authentication/models/user_utils.ts     | 22 -------
 src/server/index.ts                                |  7 +-
 6 files changed, 106 insertions(+), 37 deletions(-)
 create mode 100644 src/server/authentication/models/current_user_utils.ts
 delete mode 100644 src/server/authentication/models/user_utils.ts

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index fd756972b..7942367f5 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -40,7 +40,7 @@ import { faMusic } from '@fortawesome/free-solid-svg-icons';
 import Measure from 'react-measure';
 import { DashUserModel } from '../../server/authentication/models/user_model';
 import { ServerUtils } from '../../server/ServerUtil';
-import { UserUtils } from '../../server/authentication/models/user_utils';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 
 @observer
 export class Main extends React.Component {
@@ -63,7 +63,7 @@ export class Main extends React.Component {
             this.mainDocId = pathname[pathname.length - 1];
         }
 
-        UserUtils.loadCurrentUserId();
+        CurrentUserUtils.loadCurrentUser();
 
         library.add(faFont);
         library.add(faImage);
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 89edd1397..4a82bdb77 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -29,7 +29,10 @@ import { CollectionViewBase } from "./CollectionViewBase";
 import { MarqueeView } from "./MarqueeView";
 import { PreviewCursor } from "./PreviewCursor";
 import React = require("react");
+import { Utils } from "../../../Utils";
 const JsxParser = require('react-jsx-parser').default;//TODO Why does this need to be imported like this?
+import v5 = require("uuid/v5");
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 
 @observer
 export class CollectionFreeFormView extends CollectionViewBase {
@@ -310,6 +313,40 @@ export class CollectionFreeFormView extends CollectionViewBase {
         this.PreviewCursorVisible = false;
     }
 
+    private crosshairs?: HTMLCanvasElement;
+    drawCrosshairs = (backgroundColor: string) => {
+        if (this.crosshairs) {
+            let c = this.crosshairs;
+            let ctx = c.getContext('2d');
+            if (ctx) {
+                ctx.fillStyle = backgroundColor;
+                ctx.fillRect(0, 0, 20, 20);
+
+                ctx.fillStyle = "black";
+                ctx.lineWidth = 0.5;
+
+                ctx.beginPath();
+
+                ctx.moveTo(10, 0);
+                ctx.lineTo(10, 8);
+
+                ctx.moveTo(10, 20);
+                ctx.lineTo(10, 12);
+
+                ctx.moveTo(0, 10);
+                ctx.lineTo(8, 10);
+
+                ctx.moveTo(20, 10);
+                ctx.lineTo(12, 10);
+
+                ctx.stroke();
+
+                // ctx.font = "10px Arial";
+                // ctx.fillText(CurrentUserUtils.email[0].toUpperCase(), 10, 10);
+            }
+        }
+    }
+
     render() {
         let [dx, dy] = [this.centeringShiftX, this.centeringShiftY];
 
@@ -339,22 +376,42 @@ export class CollectionFreeFormView extends CollectionViewBase {
                     {this.views}
                     {super.getCursors().map(entry => {
                         if (entry.Data.length > 0) {
-                            let point = entry.Data[1]
+                            let id = entry.Data[0];
+                            let point = entry.Data[1];
+                            this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22")
                             return (
                                 <div
-                                    key={entry.Data[0]}
+                                    key={id}
                                     style={{
-                                        position: 'absolute',
+                                        position: "absolute",
                                         transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
                                         zIndex: 10000,
                                         transformOrigin: 'center center',
-                                        width: "20px",
-                                        height: "20px",
-                                        borderRadius: "50%",
-                                        background: "pink",
-                                        border: "2px solid black"
                                     }}
-                                />
+                                >
+                                    <canvas
+                                        ref={(el) => { if (el) this.crosshairs = el }}
+                                        width={20}
+                                        height={20}
+                                        style={{
+                                            position: 'absolute',
+                                            width: "20px",
+                                            height: "20px",
+                                            opacity: 0.5,
+                                            borderRadius: "50%",
+                                            border: "2px solid black"
+                                        }}
+                                    />
+                                    <p
+                                        style={{
+                                            fontSize: 14,
+                                            color: "black",
+                                            // fontStyle: "italic",
+                                            marginLeft: -12,
+                                            marginTop: 4
+                                        }}
+                                    >{CurrentUserUtils.email[0].toUpperCase()}</p>
+                                </div>
                             );
                         }
                     })}
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 02ee49a38..9b5c88d14 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -14,7 +14,7 @@ import { CollectionView } from "./CollectionView";
 import { RouteStore } from "../../../server/RouteStore";
 import { TupleField } from "../../../fields/TupleField";
 import { DashUserModel } from "../../../server/authentication/models/user_model";
-import { UserUtils } from "../../../server/authentication/models/user_utils";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 
 export interface CollectionViewProps {
     fieldKey: Key;
@@ -53,7 +53,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
     protected setCursorPosition(position: [number, number]) {
         let ind;
         let doc = this.props.Document;
-        let id = UserUtils.currentUserId;
+        let id = CurrentUserUtils.id;
         if (id) {
             doc.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, field => {
                 let cursors = field.Data;
@@ -71,7 +71,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
 
     protected getCursors(): CursorEntry[] {
         let doc = this.props.Document;
-        let id = UserUtils.currentUserId;
+        let id = CurrentUserUtils.id;
         let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
         let notMe = cursors.filter(entry => entry.Data[0] !== id);
         return id ? notMe : [];
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
new file mode 100644
index 000000000..cc433eb73
--- /dev/null
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -0,0 +1,29 @@
+import { DashUserModel } from "./user_model";
+import * as request from 'request'
+import { RouteStore } from "../../RouteStore";
+import { ServerUtils } from "../../ServerUtil";
+
+export class CurrentUserUtils {
+    private static curr_email: string;
+    private static curr_id: string;
+
+    public static get email() {
+        return CurrentUserUtils.curr_email;
+    }
+
+    public static get id() {
+        return CurrentUserUtils.curr_id;
+    }
+
+    public static loadCurrentUser() {
+        request.get(ServerUtils.prepend(RouteStore.getCurrUser), (error, response, body) => {
+            if (body) {
+                let obj = JSON.parse(body);
+                CurrentUserUtils.curr_id = obj.id as string;
+                CurrentUserUtils.curr_email = obj.email as string;
+            } else {
+                throw new Error("There should be a user! Why does Dash think there isn't one?")
+            }
+        });
+    }
+}
\ No newline at end of file
diff --git a/src/server/authentication/models/user_utils.ts b/src/server/authentication/models/user_utils.ts
deleted file mode 100644
index 1497a4ba4..000000000
--- a/src/server/authentication/models/user_utils.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { DashUserModel } from "./user_model";
-import * as request from 'request'
-import { RouteStore } from "../../RouteStore";
-import { ServerUtils } from "../../ServerUtil";
-
-export class UserUtils {
-    private static current: string;
-
-    public static get currentUserId() {
-        return UserUtils.current;
-    }
-
-    public static loadCurrentUserId() {
-        request.get(ServerUtils.prepend(RouteStore.getCurrUser), (error, response, body) => {
-            if (body) {
-                UserUtils.current = JSON.parse(body) as string;
-            } else {
-                throw new Error("There should be a user! Why does Dash think there isn't one?")
-            }
-        });
-    }
-}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 0512ebf72..6d8dd2d6c 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -151,7 +151,12 @@ addSecureRoute(
 
 addSecureRoute(
     Method.GET,
-    (user, res) => res.send(JSON.stringify(user.id)),
+    (user, res) => {
+        res.send(JSON.stringify({
+            id: user.id,
+            email: user.email
+        }));
+    },
     undefined,
     RouteStore.getCurrUser
 );
-- 
cgit v1.2.3-70-g09d2


From 5a2551c83e451823c7d3176a5dbac9adca34dea1 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <abdullah_ahmed@brown.edu>
Date: Mon, 18 Mar 2019 02:13:49 -0400
Subject: fixed email name bug

---
 src/client/views/collections/CollectionFreeFormView.tsx |  5 +++--
 src/client/views/collections/CollectionViewBase.tsx     | 12 +++++++-----
 2 files changed, 10 insertions(+), 7 deletions(-)

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 4a82bdb77..e095f0252 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -376,7 +376,8 @@ export class CollectionFreeFormView extends CollectionViewBase {
                     {this.views}
                     {super.getCursors().map(entry => {
                         if (entry.Data.length > 0) {
-                            let id = entry.Data[0];
+                            let id = entry.Data[0][0];
+                            let email = entry.Data[0][1];
                             let point = entry.Data[1];
                             this.drawCrosshairs("#" + v5(id, v5.URL).substring(0, 6).toUpperCase() + "22")
                             return (
@@ -410,7 +411,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
                                             marginLeft: -12,
                                             marginTop: 4
                                         }}
-                                    >{CurrentUserUtils.email[0].toUpperCase()}</p>
+                                    >{email[0].toUpperCase()}</p>
                                 </div>
                             );
                         }
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 9b5c88d14..c3f7e4951 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -36,7 +36,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
     CollectionView: CollectionView;
 }
 
-export type CursorEntry = TupleField<string, [number, number]>;
+export type CursorEntry = TupleField<[string, string], [number, number]>;
 
 export class CollectionViewBase extends React.Component<SubCollectionViewProps> {
     private dropDisposer?: DragManager.DragDropDisposer;
@@ -54,13 +54,15 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         let ind;
         let doc = this.props.Document;
         let id = CurrentUserUtils.id;
-        if (id) {
+        let email = CurrentUserUtils.email;
+        if (id && email) {
+            let textInfo: [string, string] = [id, email];
             doc.GetOrCreateAsync<ListField<CursorEntry>>(KeyStore.Cursors, ListField, field => {
                 let cursors = field.Data;
-                if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0] === id)) > -1) {
+                if (cursors.length > 0 && (ind = cursors.findIndex(entry => entry.Data[0][0] === id)) > -1) {
                     cursors[ind].Data[1] = position;
                 } else {
-                    let entry = new TupleField<string, [number, number]>([id, position]);
+                    let entry = new TupleField<[string, string], [number, number]>([textInfo, position]);
                     cursors.push(entry);
                 }
             })
@@ -73,7 +75,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         let doc = this.props.Document;
         let id = CurrentUserUtils.id;
         let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
-        let notMe = cursors.filter(entry => entry.Data[0] !== id);
+        let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
         return id ? notMe : [];
     }
 
-- 
cgit v1.2.3-70-g09d2


From b2c3389ce819093d1d6dbc205977f6455057d715 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 18 Mar 2019 10:05:57 -0400
Subject: fixed issue with drag drop of aliases

---
 src/client/views/collections/CollectionViewBase.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/views/collections/CollectionViewBase.tsx')

diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index f3a75dad5..320838904 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -92,6 +92,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
                     }
                 })
             );
+            de.data["document"] = doc;
             this.props.addDocument(doc);
         } else if (docView) {
             if (doc && docView.props.RemoveDocument && docView.props.ContainingCollectionView !== this.props.CollectionView) {
-- 
cgit v1.2.3-70-g09d2

