From bc59ea805f32568f0835bd55d39575236c24a066 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 21 Mar 2019 22:22:25 -0400
Subject: added very basic cycle detection when adding to collections

---
 src/client/views/collections/CollectionView.tsx | 22 ++++++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7e1d31018..2e93e6bb8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -37,7 +37,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
     @observable
     public SelectedDocs: FieldId[] = [];
     public active: () => boolean = () => CollectionView.Active(this);
-    addDocument = (doc: Document, allowDuplicates: boolean): void => { CollectionView.AddDocument(this.props, doc, allowDuplicates); }
+    addDocument = (doc: Document, allowDuplicates: boolean): boolean => { return CollectionView.AddDocument(this.props, doc, allowDuplicates); }
     removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
     get subView() { return CollectionView.SubView(this); }
 
@@ -48,17 +48,31 @@ export class CollectionView extends React.Component<CollectionViewProps> {
         return isSelected || childSelected || topMost;
     }
 
+    static createsCycle(doc: Document, props: CollectionViewProps): boolean {
+        let ldata = doc.GetList<Document>(KeyStore.Data, []);
+        for (let i = 0; i < ldata.length; i++) {
+            if (CollectionView.createsCycle(ldata[i], props))
+                return true;
+        }
+        return doc.Id == props.Document.Id;
+    }
+
     @action
-    public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean) {
+    public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean): boolean {
         doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, -1));
         if (props.Document.Get(props.fieldKey) instanceof Field) {
             //TODO This won't create the field if it doesn't already exist
             const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
-            if (!value.some(v => v.Id == doc.Id) || allowDuplicates)
-                value.push(doc);
+            if (!CollectionView.createsCycle(doc, props)) {
+                if (!value.some(v => v.Id == doc.Id) || allowDuplicates)
+                    value.push(doc);
+            }
+            else
+                return false;
         } else {
             props.Document.SetOnPrototype(props.fieldKey, new ListField([doc]));
         }
+        return true;
     }
 
     @action
-- 
cgit v1.2.3-70-g09d2


From eb27dd447502ee1f55296b9e496aee998b0259f2 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 21 Mar 2019 23:10:42 -0400
Subject: avoided another cycle

---
 src/client/views/collections/CollectionView.tsx | 29 +++++++++++++++++++------
 1 file changed, 22 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 2e93e6bb8..be757e22e 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -14,6 +14,7 @@ import { CollectionViewProps } from "./CollectionViewBase";
 import { CollectionTreeView } from "./CollectionTreeView";
 import { Field, FieldId, FieldWaiting } from "../../../fields/Field";
 import { Main } from "../Main";
+import { dropPoint } from "prosemirror-transform";
 
 export enum CollectionViewType {
     Invalid,
@@ -48,13 +49,22 @@ export class CollectionView extends React.Component<CollectionViewProps> {
         return isSelected || childSelected || topMost;
     }
 
-    static createsCycle(doc: Document, props: CollectionViewProps): boolean {
-        let ldata = doc.GetList<Document>(KeyStore.Data, []);
-        for (let i = 0; i < ldata.length; i++) {
-            if (CollectionView.createsCycle(ldata[i], props))
+    static createsCycle(documentToAdd: Document, containerDocument: Document): boolean {
+        let data = documentToAdd.GetList<Document>(KeyStore.Data, []);
+        for (let i = 0; i < data.length; i++) {
+            if (CollectionView.createsCycle(data[i], containerDocument))
                 return true;
         }
-        return doc.Id == props.Document.Id;
+        let annots = documentToAdd.GetList<Document>(KeyStore.Annotations, []);
+        for (let i = 0; i < annots.length; i++) {
+            if (CollectionView.createsCycle(annots[i], containerDocument))
+                return true;
+        }
+        for (let containerProto: any = containerDocument; containerProto && containerProto != FieldWaiting; containerProto = containerProto.GetPrototype()) {
+            if (containerProto.Id == documentToAdd.Id)
+                return true;
+        }
+        return false;
     }
 
     @action
@@ -63,14 +73,19 @@ export class CollectionView extends React.Component<CollectionViewProps> {
         if (props.Document.Get(props.fieldKey) instanceof Field) {
             //TODO This won't create the field if it doesn't already exist
             const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
-            if (!CollectionView.createsCycle(doc, props)) {
+            if (!CollectionView.createsCycle(doc, props.Document)) {
                 if (!value.some(v => v.Id == doc.Id) || allowDuplicates)
                     value.push(doc);
             }
             else
                 return false;
         } else {
-            props.Document.SetOnPrototype(props.fieldKey, new ListField([doc]));
+            let proto = props.Document.GetPrototype();
+            if (!proto || proto == FieldWaiting || !CollectionView.createsCycle(proto, doc)) {
+                props.Document.SetOnPrototype(props.fieldKey, new ListField([doc]));
+            }
+            else
+                return false;
         }
         return true;
     }
-- 
cgit v1.2.3-70-g09d2


From 3eefc8c7e901242ac6b7614bf1163858568d53b0 Mon Sep 17 00:00:00 2001
From: Tyler Schicke <tyler_schicke@brown.edu>
Date: Fri, 22 Mar 2019 05:19:37 -0400
Subject: Moved main doc id

---
 src/client/views/Main.tsx                              | 14 ++++++--------
 src/client/views/collections/CollectionView.tsx        |  4 ++--
 src/server/authentication/models/current_user_utils.ts | 16 +++++++++++++---
 3 files changed, 21 insertions(+), 13 deletions(-)

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

diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index fdbad6ce1..cb49bc4e6 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -73,8 +73,6 @@ export class Main extends React.Component {
         return CurrentUserUtils.UserDocument;
     }
 
-    public mainDocId: string | undefined;
-    private currentUser?: DashUserModel;
     public static Instance: Main;
 
     constructor(props: Readonly<{}>) {
@@ -85,7 +83,7 @@ export class Main extends React.Component {
         if (window.location.pathname !== RouteStore.home) {
             let pathname = window.location.pathname.split("/");
             if (pathname.length > 1 && pathname[pathname.length - 2] == 'doc') {
-                this.mainDocId = pathname[pathname.length - 1];
+                CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
             }
         };
 
@@ -117,8 +115,8 @@ export class Main extends React.Component {
     onHistory = () => {
         if (window.location.pathname !== RouteStore.home) {
             let pathname = window.location.pathname.split("/");
-            this.mainDocId = pathname[pathname.length - 1];
-            Server.GetField(this.mainDocId, action((field: Opt<Field>) => {
+            CurrentUserUtils.MainDocId = pathname[pathname.length - 1];
+            Server.GetField(CurrentUserUtils.MainDocId, action((field: Opt<Field>) => {
                 if (field instanceof Document) {
                     this.openWorkspace(field, true);
                 }
@@ -148,7 +146,7 @@ export class Main extends React.Component {
 
     initAuthenticationRouters = () => {
         // Load the user's active workspace, or create a new one if initial session after signup
-        if (!this.mainDocId) {
+        if (!CurrentUserUtils.MainDocId) {
             this.userDocument.GetTAsync(KeyStore.ActiveWorkspace, Document).then(doc => {
                 if (doc) {
                     this.openWorkspace(doc);
@@ -157,11 +155,11 @@ export class Main extends React.Component {
                 }
             })
         } else {
-            Server.GetField(this.mainDocId).then(field => {
+            Server.GetField(CurrentUserUtils.MainDocId).then(field => {
                 if (field instanceof Document) {
                     this.openWorkspace(field)
                 } else {
-                    this.createNewWorkspace(this.mainDocId);
+                    this.createNewWorkspace(CurrentUserUtils.MainDocId);
                 }
             })
         }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 7e1d31018..c72633175 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -13,7 +13,7 @@ import { CollectionSchemaView } from "./CollectionSchemaView";
 import { CollectionViewProps } from "./CollectionViewBase";
 import { CollectionTreeView } from "./CollectionTreeView";
 import { Field, FieldId, FieldWaiting } from "../../../fields/Field";
-import { Main } from "../Main";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 
 export enum CollectionViewType {
     Invalid,
@@ -96,7 +96,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
     }
 
     specificContextMenu = (e: React.MouseEvent): void => {
-        if (!e.isPropagationStopped() && this.props.Document.Id != Main.Instance.mainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+        if (!e.isPropagationStopped() && this.props.Document.Id != CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
             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: "Treeview", event: () => this.props.Document.SetNumber(KeyStore.ViewType, CollectionViewType.Tree) })
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 17a6d493b..3291c671c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -12,17 +12,27 @@ export class CurrentUserUtils {
     private static curr_email: string;
     private static curr_id: string;
     private static user_document: Document;
+    //TODO tfs: this should be temporary...
+    private static mainDocId: string | undefined;
 
     public static get email(): string {
-        return CurrentUserUtils.curr_email;
+        return this.curr_email;
     }
 
     public static get id(): string {
-        return CurrentUserUtils.curr_id;
+        return this.curr_id;
     }
 
     public static get UserDocument(): Document {
-        return CurrentUserUtils.user_document;
+        return this.user_document;
+    }
+
+    public static get MainDocId(): string | undefined {
+        return this.mainDocId;
+    }
+
+    public static set MainDocId(id: string | undefined) {
+        this.mainDocId = id;
     }
 
     private static createUserDocument(id: string): Document {
-- 
cgit v1.2.3-70-g09d2


From 2264fb874a09ee01540141986325c76650f32c21 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 25 Mar 2019 23:20:22 -0400
Subject: added 1-level navigation to region annotations

---
 .../views/collections/CollectionDockingView.tsx    |   2 +-
 .../views/collections/CollectionVideoView.tsx      | 108 ++++++++++++---------
 src/client/views/collections/CollectionView.tsx    |   6 +-
 src/client/views/nodes/LinkBox.tsx                 |  21 +++-
 src/client/views/nodes/PDFBox.tsx                  |  21 ++--
 src/client/views/nodes/VideoBox.tsx                |  29 ++++--
 src/fields/KeyStore.ts                             |   2 +
 7 files changed, 120 insertions(+), 69 deletions(-)

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

diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 39b284d8e..d4f510f5d 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -191,7 +191,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         var className = (e.target as any).className;
-        if ((className == "lm_title" || className == "lm_tab lm_active") && e.ctrlKey) {
+        if ((className == "lm_title" || className == "lm_tab lm_active") && (e.ctrlKey || e.altKey)) {
             e.stopPropagation();
             e.preventDefault();
             let docid = (e.target as any).DashDocId;
diff --git a/src/client/views/collections/CollectionVideoView.tsx b/src/client/views/collections/CollectionVideoView.tsx
index a3921696f..470a853e3 100644
--- a/src/client/views/collections/CollectionVideoView.tsx
+++ b/src/client/views/collections/CollectionVideoView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../fields/Document";
 import { KeyStore } from "../../../fields/KeyStore";
@@ -12,24 +12,26 @@ import "./CollectionVideoView.scss"
 
 @observer
 export class CollectionVideoView extends React.Component<CollectionViewProps> {
+    private _intervalTimer: any = undefined;
+    private _player: HTMLVideoElement | undefined = undefined;
+
+    @observable _currentTimecode: number = 0;
+    @observable _isPlaying: boolean = false;
 
     public static LayoutString(fieldKey: string = "DataKey") {
         return `<${CollectionVideoView.name} Document={Document}
                     ScreenToLocalTransform={ScreenToLocalTransform} fieldKey={${fieldKey}} panelWidth={PanelWidth} panelHeight={PanelHeight} isSelected={isSelected} select={select} bindings={bindings}
                     isTopMost={isTopMost} SelectOnLoad={selectOnLoad} BackgroundView={BackgroundView} focus={focus}/>`;
     }
-
-    private _mainCont = React.createRef<HTMLDivElement>();
-
     private get uIButtons() {
         let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().transformDirection(1, 1)[0]);
         return ([
             <div className="collectionVideoView-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
-                <span>{"" + Math.round(this.ctime)}</span>
-                <span style={{ fontSize: 8 }}>{" " + Math.round((this.ctime - Math.trunc(this.ctime)) * 100)}</span>
+                <span>{"" + Math.round(this._currentTimecode)}</span>
+                <span style={{ fontSize: 8 }}>{" " + Math.round((this._currentTimecode - Math.trunc(this._currentTimecode)) * 100)}</span>
             </div>,
             <div className="collectionVideoView-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
-                {this.playing ? "\"" : ">"}
+                {this._isPlaying ? "\"" : ">"}
             </div>,
             <div className="collectionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling}, ${scaling})` }}>
                 F
@@ -37,64 +39,54 @@ export class CollectionVideoView extends React.Component<CollectionViewProps> {
         ]);
     }
 
-
-    // "inherited" CollectionView API starts here...
-
-    @observable
-    public SelectedDocs: FieldId[] = []
-    public active: () => boolean = () => CollectionView.Active(this);
-
-    addDocument = (doc: Document, allowDuplicates: boolean): boolean => { return CollectionView.AddDocument(this.props, doc, allowDuplicates); }
-    removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
-
-    specificContextMenu = (e: React.MouseEvent): void => {
-        if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
-            ContextMenu.Instance.addItem({ description: "VideoOptions", event: () => { } });
+    @action
+    mainCont = (ele: HTMLDivElement | null) => {
+        if (ele) {
+            this._player = ele!.getElementsByTagName("video")[0];
+            if (this.props.Document.GetNumber(KeyStore.CurPage, -1) >= 0) {
+                this._currentTimecode = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+            }
         }
     }
 
-    get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
-    get subView(): any { return CollectionView.SubView(this); }
-
     componentDidMount() {
-        this.updateTimecode();
+        this._intervalTimer = setInterval(this.updateTimecode, 1000);
     }
 
-    get player(): HTMLVideoElement | undefined {
-        return this._mainCont.current ? this._mainCont.current.getElementsByTagName("video")[0] : undefined;
+    componentWillUnmount() {
+        clearInterval(this._intervalTimer);
     }
 
     @action
     updateTimecode = () => {
-        if (this.player) {
-            this.ctime = this.player.currentTime;
-            this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this.ctime));
+        if (this._player) {
+            if ((this._player as any).AHackBecauseSomethingResetsTheVideoToZero != -1) {
+                this._player.currentTime = (this._player as any).AHackBecauseSomethingResetsTheVideoToZero;
+                (this._player as any).AHackBecauseSomethingResetsTheVideoToZero = -1;
+            } else {
+                this._currentTimecode = this._player.currentTime;
+                this.props.Document.SetNumber(KeyStore.CurPage, Math.round(this._currentTimecode));
+            }
         }
-        setTimeout(() => this.updateTimecode(), 100)
     }
 
-
-    @observable
-    ctime: number = 0
-    @observable
-    playing: boolean = false;
-
     @action
     onPlayDown = () => {
-        if (this.player) {
-            if (this.player.paused) {
-                this.player.play();
-                this.playing = true;
+        if (this._player) {
+            if (this._player.paused) {
+                this._player.play();
+                this._isPlaying = true;
             } else {
-                this.player.pause();
-                this.playing = false;
+                this._player.pause();
+                this._isPlaying = false;
             }
         }
     }
+
     @action
     onFullDown = (e: React.PointerEvent) => {
-        if (this.player) {
-            this.player.requestFullscreen();
+        if (this._player) {
+            this._player.requestFullscreen();
             e.stopPropagation();
             e.preventDefault();
         }
@@ -102,15 +94,35 @@ export class CollectionVideoView extends React.Component<CollectionViewProps> {
 
     @action
     onResetDown = () => {
-        if (this.player) {
-            this.player.pause();
-            this.player.currentTime = 0;
+        if (this._player) {
+            this._player.pause();
+            this._player.currentTime = 0;
         }
 
     }
 
+    // "inherited" CollectionView API starts here...
+
+    @observable
+    public SelectedDocs: FieldId[] = []
+    public active: () => boolean = () => CollectionView.Active(this);
+
+    addDocument = (doc: Document, allowDuplicates: boolean): boolean => { return CollectionView.AddDocument(this.props, doc, allowDuplicates); }
+    removeDocument = (doc: Document): boolean => { return CollectionView.RemoveDocument(this.props, doc); }
+
+    specificContextMenu = (e: React.MouseEvent): void => {
+        if (!e.isPropagationStopped() && this.props.Document.Id != "mainDoc") { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
+            ContextMenu.Instance.addItem({ description: "VideoOptions", event: () => { } });
+        }
+    }
+
+    get collectionViewType(): CollectionViewType { return CollectionViewType.Freeform; }
+    get subView(): any { return CollectionView.SubView(this); }
+
+
     render() {
-        return (<div className="collectionVideoView-cont" ref={this._mainCont} onContextMenu={this.specificContextMenu}>
+        trace();
+        return (<div className="collectionVideoView-cont" ref={this.mainCont} onContextMenu={this.specificContextMenu}>
             {this.subView}
             {this.props.isSelected() ? this.uIButtons : (null)}
         </div>)
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a740865ad..2fa2c9086 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -68,7 +68,11 @@ export class CollectionView extends React.Component<CollectionViewProps> {
 
     @action
     public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean): boolean {
-        doc.SetNumber(KeyStore.Page, props.Document.GetNumber(KeyStore.CurPage, -1));
+        var curPage = props.Document.GetNumber(KeyStore.CurPage, -1);
+        doc.SetNumber(KeyStore.Page, curPage);
+        if (curPage > 0) {
+            doc.Set(KeyStore.AnnotationOn, props.Document);
+        }
         if (props.Document.Get(props.fieldKey) instanceof Field) {
             //TODO This won't create the field if it doesn't already exist
             const value = props.Document.GetData(props.fieldKey, ListField, new Array<Document>())
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index dd2f71b59..638d3b5a7 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -17,6 +17,8 @@ import { faEye } from '@fortawesome/free-solid-svg-icons';
 import { faEdit } from '@fortawesome/free-solid-svg-icons';
 import { faTimes } from '@fortawesome/free-solid-svg-icons';
 import { undoBatch } from "../../util/UndoManager";
+import { FieldWaiting } from "../../../fields/Field";
+import { NumberField } from "../../../fields/NumberField";
 
 
 library.add(faEye);
@@ -41,7 +43,24 @@ export class LinkBox extends React.Component<Props> {
         if (docView) {
             docView.props.focus(this.props.pairedDoc);
         } else {
-            CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc)
+            this.props.pairedDoc.GetAsync(KeyStore.AnnotationOn, (contextDoc: any) => {
+                if (!contextDoc) {
+                    CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc);
+                } else if (contextDoc instanceof Document) {
+                    this.props.pairedDoc.GetTAsync(KeyStore.Page, NumberField).then((pfield: any) => {
+                        contextDoc.GetTAsync(KeyStore.CurPage, NumberField).then((cfield: any) => {
+                            if (pfield != cfield)
+                                contextDoc.SetNumber(KeyStore.CurPage, pfield.Data);
+                            let contextView = DocumentManager.Instance.getDocumentView(contextDoc);
+                            if (contextView) {
+                                contextView.props.focus(contextDoc);
+                            } else {
+                                CollectionDockingView.Instance.AddRightSplit(contextDoc);
+                            }
+                        })
+                    });
+                }
+            });
         }
     }
 
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 3a0ef2d32..e273b0b4f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,5 +1,5 @@
 import * as htmlToImage from "html-to-image";
-import { action, computed, observable, reaction, IReactionDisposer, trace } from 'mobx';
+import { action, computed, observable, reaction, IReactionDisposer, trace, keys } from 'mobx';
 import { observer } from "mobx-react";
 import 'react-image-lightbox/style.css';
 import Measure from "react-measure";
@@ -18,6 +18,7 @@ import "./PDFBox.scss";
 import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here
 import React = require("react")
 import { RouteStore } from "../../../server/RouteStore";
+import { NumberField } from "../../../fields/NumberField";
 
 /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx
  * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, 
@@ -60,7 +61,6 @@ export class PDFBox extends React.Component<FieldViewProps> {
     //very useful for keeping track of X and y position throughout the PDF Canvas
     private initX: number = 0;
     private initY: number = 0;
-    private initPage: boolean = false;
 
     //checks if tool is on
     private _toolOn: boolean = false; //checks if tool is on
@@ -88,17 +88,15 @@ export class PDFBox extends React.Component<FieldViewProps> {
     @observable private _loaded: boolean = false;
 
     @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); }
+    @computed private get thumbnailPage() { return this.props.doc.GetNumber(KeyStore.ThumbnailPage, -1); }
 
     componentDidMount() {
         this._reactionDisposer = reaction(
-            () => this.curPage,
+            () => [this.curPage, this.thumbnailPage],
             () => {
-                if (this.curPage && this.initPage) {
+                if (this.curPage > 0 && this.thumbnailPage > 0 && this.curPage != this.thumbnailPage) {
                     this.saveThumbnail();
                     this._interactive = true;
-                } else {
-                    if (this.curPage > 0)
-                        this.initPage = true;
                 }
             },
             { fireImmediately: true });
@@ -384,6 +382,7 @@ export class PDFBox extends React.Component<FieldViewProps> {
                 { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 })
                 .then(function (dataUrl: string) {
                     me.props.doc.SetData(KeyStore.Thumbnail, new URL(dataUrl), ImageField);
+                    me.props.doc.SetNumber(KeyStore.ThumbnailPage, me.props.doc.GetNumber(KeyStore.CurPage, -1));
                 })
                 .catch(function (error: any) {
                     console.error('oops, something went wrong!', error);
@@ -473,10 +472,10 @@ export class PDFBox extends React.Component<FieldViewProps> {
 
     @computed
     get imageProxyRenderer() {
-        let field = this.props.doc.Get(KeyStore.Thumbnail);
-        if (field) {
-            let path = field == FieldWaiting ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
-                field instanceof ImageField ? field.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
+        let thumbField = this.props.doc.Get(KeyStore.Thumbnail);
+        if (thumbField) {
+            let path = thumbField == FieldWaiting || this.thumbnailPage != this.curPage ? "https://image.flaticon.com/icons/svg/66/66163.svg" :
+                thumbField instanceof ImageField ? thumbField.Data.href : "http://cs.brown.edu/people/bcz/prairie.jpg";
             return <img src={path} width="100%" />;
         }
         return (null);
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 09ae95183..7c0db83a8 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,23 +1,27 @@
 import React = require("react")
 import { observer } from "mobx-react";
-import { FieldWaiting } from '../../../fields/Field';
+import { FieldWaiting, Opt } from '../../../fields/Field';
 import { VideoField } from '../../../fields/VideoField';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
 import Measure from "react-measure";
-import { action, trace, observable } from "mobx";
+import { action, trace, observable, IReactionDisposer, computed, reaction } from "mobx";
 import { KeyStore } from "../../../fields/KeyStore";
 import { number } from "prop-types";
 
 @observer
 export class VideoBox extends React.Component<FieldViewProps> {
 
+    private _reactionDisposer: Opt<IReactionDisposer>;
+    private _videoRef = React.createRef<HTMLVideoElement>()
     public static LayoutString() { return FieldView.LayoutString(VideoBox) }
 
     constructor(props: FieldViewProps) {
         super(props);
     }
 
+    @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); }
+
 
     _loaded: boolean = false;
 
@@ -39,7 +43,17 @@ export class VideoBox extends React.Component<FieldViewProps> {
         }
     }
 
+    get player(): HTMLVideoElement | undefined {
+        return this._videoRef.current ? this._videoRef.current.getElementsByTagName("video")[0] : undefined;
+    }
 
+    @action
+    setVideoRef = (vref: HTMLVideoElement | null) => {
+        if (this.curPage >= 0 && vref) {
+            vref!.currentTime = this.curPage;
+            (vref! as any).AHackBecauseSomethingResetsTheVideoToZero = this.curPage;
+        }
+    }
 
     render() {
         let field = this.props.doc.GetT(this.props.fieldKey, VideoField);
@@ -47,15 +61,16 @@ export class VideoBox extends React.Component<FieldViewProps> {
             return <div>Loading</div>
         }
         let path = field.Data.href;
-
-        //setTimeout(action(() => this._loaded = true), 500);
+        trace();
         return (
             <Measure onResize={this.setScaling}>
                 {({ measureRef }) =>
-                    <video className="videobox-cont" onClick={() => { }} ref={measureRef}>
-                        <source src={path} type="video/mp4" />
-                        Not supported.
+                    <div style={{ width: "100%", height: "auto" }} ref={measureRef}>
+                        <video className="videobox-cont" onClick={() => { }} ref={this.setVideoRef}>
+                            <source src={path} type="video/mp4" />
+                            Not supported.
                         </video>
+                    </div>
                 }
             </Measure>
         )
diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts
index 611e2951b..f9684b212 100644
--- a/src/fields/KeyStore.ts
+++ b/src/fields/KeyStore.ts
@@ -36,7 +36,9 @@ export namespace KeyStore {
     export const LinkDescription = new Key("LinkDescription");
     export const LinkTags = new Key("LinkTag");
     export const Thumbnail = new Key("Thumbnail");
+    export const ThumbnailPage = new Key("ThumbnailPage");
     export const CurPage = new Key("CurPage");
+    export const AnnotationOn = new Key("AnnotationOn");
     export const NumPages = new Key("NumPages");
     export const Ink = new Key("Ink");
     export const Cursors = new Key("Cursors");
-- 
cgit v1.2.3-70-g09d2


From fddd13da196a26affe61b30d93a3fe1b24f391a6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 28 Mar 2019 12:59:35 -0400
Subject: works better with prototypes and links

---
 src/client/util/DocumentManager.ts                 |  5 ++
 .../views/collections/CollectionFreeFormView.tsx   | 53 +++++++++++++++++-----
 src/client/views/collections/CollectionView.tsx    |  4 +-
 src/client/views/nodes/LinkBox.tsx                 |  2 +-
 4 files changed, 50 insertions(+), 14 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5bc16343e..28624dba8 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -26,6 +26,11 @@ export class DocumentManager {
         // this.DocumentViews = new Array<DocumentView>();
     }
 
+    public getAllDocumentViews(collection: Document) {
+        return this.DocumentViews.filter(dv =>
+            dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.Document == collection);
+    }
+
     public getDocumentView(toFind: Document): DocumentView | null {
 
         let toReturn: DocumentView | null;
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index cba1110a1..0843d3670 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../fields/Document";
-import { FieldWaiting, Field } from "../../../fields/Field";
+import { FieldWaiting, Field, Opt } from "../../../fields/Field";
 import { KeyStore } from "../../../fields/KeyStore";
 import { ListField } from "../../../fields/ListField";
 import { TextField } from "../../../fields/TextField";
@@ -21,6 +21,8 @@ import React = require("react");
 import v5 = require("uuid/v5");
 import { DocumentManager } from "../../util/DocumentManager";
 import { Utils } from "../../../Utils";
+import { Server } from "../../Server";
+import { AverageAggregateParameters } from "../../northstar/model/idea/idea";
 
 @observer
 export class CollectionFreeFormView extends CollectionViewBase {
@@ -424,42 +426,71 @@ export class LinksView extends React.Component<CollectionViewProps> {
         super(props);
     }
 
-    @observable _pairs: { a: Document, b: Document }[] = [];
+    @observable _triples: { a: Document, b: Document, l: Document }[] = [];
 
     findPairs() {
         return DocumentManager.Instance.DocumentViews.filter(dv => dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.Document === this.props.Document).reduce((pairs, dv) => {
+
+        return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
+            let srcViews = [dv];
+            let srcAnnot = dv.props.Document.GetT(KeyStore.AnnotationOn, Document);
+            if (srcAnnot && srcAnnot != FieldWaiting && srcAnnot instanceof Document) {
+                srcViews = DocumentManager.Instance.getDocumentViews(srcAnnot.GetPrototype() as Document)
+            }
+            srcViews = srcViews.filter(sv =>
+                sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == self
+            );
             let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
             if (linksList && linksList != FieldWaiting && linksList.Data.length) {
                 pairs.push(...linksList.Data.reduce((pairs, link) => {
                     if (link instanceof Document) {
                         let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
                         if (linkToDoc && linkToDoc != FieldWaiting) {
-                            DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
-                                pairs.push({ a: dv.props.Document, b: docView1.props.Document })
-                            )
+                            DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+
+                                let targetViews = [docView1];
+                                let docAnnot = docView1.props.Document.GetT(KeyStore.AnnotationOn, Document);
+                                if (docAnnot && docAnnot != FieldWaiting && docAnnot instanceof Document) {
+                                    targetViews = DocumentManager.Instance.getDocumentViews(docAnnot.GetPrototype() as Document)
+                                }
+                                targetViews.filter(tv =>
+                                    tv.props.ContainingCollectionView && tv.props.ContainingCollectionView.props.Document == self
+                                ).map(tv => srcViews.map(sv =>
+                                    pairs.push({ a: sv.props.Document, b: tv.props.Document, l: link })))
+                            })
                         }
                     }
                     return pairs;
-                }, [] as { a: Document, b: Document }[]));
+                }, [] as { a: Document, b: Document, l: Document }[]));
             }
             return pairs;
-        }, [] as { a: Document, b: Document }[]);
+        }, [] as { a: Document, b: Document, l: Document }[]);
     }
     componentDidMount() {
-        reaction(() => this.findPairs(), (pairs) => runInAction(() => this._pairs = pairs));
+        reaction(() => this.findPairs(), (pairs) => runInAction(() => this._triples = pairs));
+    }
+
+    onPointerDown(e: React.PointerEvent) {
+        let line = (e.nativeEvent as any).path[0];
+        line.style.stroke = "red";
+        Server.GetField(line.id, action((f: Opt<Field>) => {
+            if (f instanceof Document) {
+                console.log(f.Title);
+            }
+        }));
     }
 
     render() {
-        if (!this._pairs.length)
+        if (!this._triples.length)
             return (null);
         return <svg className="collectionfreeformview-svgCanvas">
-            {this._pairs.map(pair => {
+            {this._triples.map(pair => {
                 let x1 = pair.a.GetNumber(KeyStore.X, 0) + pair.a.GetNumber(KeyStore.Width, 0) / 2;
                 let y1 = pair.a.GetNumber(KeyStore.Y, 0) + pair.a.GetNumber(KeyStore.Height, 0) / 2;
                 let x2 = pair.b.GetNumber(KeyStore.X, 0) + pair.b.GetNumber(KeyStore.Width, 0) / 2;
                 let y2 = pair.b.GetNumber(KeyStore.Y, 0) + pair.b.GetNumber(KeyStore.Height, 0) / 2;
                 return (
-                    <line key={Utils.GenerateGuid()} className="collectionfreeformview-linkLine"
+                    <line key={Utils.GenerateGuid()} id={pair.l.Id} className="collectionfreeformview-linkLine" onPointerDown={this.onPointerDown}
                         x1={`${x1}`} y1={`${y1}`}
                         x2={`${x2}`} y2={`${y2}`} />
                 )
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 2fa2c9086..789f07e8c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -69,9 +69,9 @@ export class CollectionView extends React.Component<CollectionViewProps> {
     @action
     public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean): boolean {
         var curPage = props.Document.GetNumber(KeyStore.CurPage, -1);
-        doc.SetNumber(KeyStore.Page, curPage);
+        doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage));
         if (curPage > 0) {
-            doc.Set(KeyStore.AnnotationOn, props.Document);
+            doc.SetOnPrototype(KeyStore.AnnotationOn, props.Document);
         }
         if (props.Document.Get(props.fieldKey) instanceof Field) {
             //TODO This won't create the field if it doesn't already exist
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 457fecee3..e81f8fec7 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -45,7 +45,7 @@ export class LinkBox extends React.Component<Props> {
         } else {
             this.props.pairedDoc.GetAsync(KeyStore.AnnotationOn, (contextDoc: any) => {
                 if (!contextDoc) {
-                    CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc);
+                    CollectionDockingView.Instance.AddRightSplit(this.props.pairedDoc.MakeDelegate());
                 } else if (contextDoc instanceof Document) {
                     this.props.pairedDoc.GetTAsync(KeyStore.Page, NumberField).then((pfield: any) => {
                         contextDoc.GetTAsync(KeyStore.CurPage, NumberField).then((cfield: any) => {
-- 
cgit v1.2.3-70-g09d2


From bdea88507ce2ba4da32b7f89c995021d0a058413 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 28 Mar 2019 15:24:57 -0400
Subject: fixed some things with linking.  still some real inefficiencies.

---
 src/client/documents/Documents.ts                  |  1 +
 .../views/collections/CollectionFreeFormView.scss  | 10 +--
 .../views/collections/CollectionFreeFormView.tsx   | 87 ++++++++++++++--------
 src/client/views/collections/CollectionView.tsx    |  2 +-
 4 files changed, 59 insertions(+), 41 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1d23b8c2c..837dfe815 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -119,6 +119,7 @@ export namespace Documents {
             imageProto = setupPrototypeOptions(imageProtoId, "IMAGE_PROTO", CollectionView.LayoutString("AnnotationsKey"),
                 { x: 0, y: 0, nativeWidth: 300, width: 300, layoutKeys: [KeyStore.Data, KeyStore.Annotations, KeyStore.Caption] });
             imageProto.SetText(KeyStore.BackgroundLayout, ImageBox.LayoutString());
+            imageProto.SetNumber(KeyStore.CurPage, 0);
         }
         return imageProto;
     }
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss
index 2dd345b6f..7012ce6b9 100644
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ b/src/client/views/collections/CollectionFreeFormView.scss
@@ -3,19 +3,15 @@
 .collectionfreeformview-linkLine {
     stroke: black;
     stroke-width: 3;
-    transform: translate(10000px,10000px)
-}
-.collectionfreeformview-svgContainer{
-  transform: translate(-10000px,-10000px);
-  position: absolute;
-  width: 20000px; 
-  height: 20000px;
+    transform: translate(10000px,10000px);
+    pointer-events: all;
 }
 .collectionfreeformview-svgCanvas{
   transform: translate(-10000px,-10000px);
   position: absolute;
   width: 20000px; 
   height: 20000px;
+  pointer-events: none;
 }
 .collectionfreeformview-container {
   .collectionfreeformview > .jsx-parser {
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
index 5ce42acc9..91faf0907 100644
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/CollectionFreeFormView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction, runInAction, trace, untracked } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../fields/Document";
 import { FieldWaiting, Field, Opt } from "../../../fields/Field";
@@ -424,6 +424,7 @@ export class LinksView extends React.Component<CollectionViewProps> {
 
     constructor(props: CollectionViewProps) {
         super(props);
+        console.log("Create links View");
     }
 
     @observable _triples: { a: Document, b: Document, l: Document }[] = [];
@@ -432,42 +433,47 @@ export class LinksView extends React.Component<CollectionViewProps> {
         let self = this.props.Document;
 
         return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
-            let srcViews = [dv];
-            let srcAnnot = dv.props.Document.GetT(KeyStore.AnnotationOn, Document);
-            if (srcAnnot && srcAnnot != FieldWaiting && srcAnnot instanceof Document) {
-                srcViews = DocumentManager.Instance.getDocumentViews(srcAnnot.GetPrototype() as Document)
-            }
-            srcViews = srcViews.filter(sv =>
-                sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == self
-            );
-            let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
-            if (linksList && linksList != FieldWaiting && linksList.Data.length) {
-                pairs.push(...linksList.Data.reduce((pairs, link) => {
-                    if (link instanceof Document) {
-                        let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
-                        if (linkToDoc && linkToDoc != FieldWaiting) {
-                            DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
-
-                                let targetViews = [docView1];
-                                let docAnnot = docView1.props.Document.GetT(KeyStore.AnnotationOn, Document);
-                                if (docAnnot && docAnnot != FieldWaiting && docAnnot instanceof Document) {
-                                    targetViews = DocumentManager.Instance.getDocumentViews(docAnnot.GetPrototype() as Document)
-                                }
-                                targetViews.filter(tv =>
-                                    tv.props.ContainingCollectionView && tv.props.ContainingCollectionView.props.Document == self
-                                ).map(tv => srcViews.map(sv =>
-                                    pairs.push({ a: sv.props.Document, b: tv.props.Document, l: link })))
-                            })
+            untracked(() => {
+                let srcViews = [dv];
+                let srcAnnot = dv.props.Document.GetT(KeyStore.AnnotationOn, Document);
+                if (srcAnnot && srcAnnot != FieldWaiting && srcAnnot instanceof Document) {
+                    srcViews = DocumentManager.Instance.getDocumentViews(srcAnnot.GetPrototype() as Document)
+                }
+                srcViews = srcViews.filter(sv =>
+                    sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == self
+                );
+                let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
+                if (linksList && linksList != FieldWaiting && linksList.Data.length) {
+                    pairs.push(...linksList.Data.reduce((pairs, link) => {
+                        if (link instanceof Document) {
+                            let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
+                            if (linkToDoc && linkToDoc != FieldWaiting) {
+                                DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+                                    let targetViews = [docView1];
+                                    let docAnnot = docView1.props.Document.GetT(KeyStore.AnnotationOn, Document);
+                                    if (docAnnot && docAnnot != FieldWaiting && docAnnot instanceof Document) {
+                                        targetViews = DocumentManager.Instance.getDocumentViews(docAnnot.GetPrototype() as Document)
+                                    }
+                                    targetViews.filter(tv =>
+                                        tv.props.ContainingCollectionView && tv.props.ContainingCollectionView.props.Document == self
+                                    ).map(tv => srcViews.map(sv =>
+                                        pairs.push({ a: sv.props.Document, b: tv.props.Document, l: link })))
+                                })
+                            }
                         }
-                    }
-                    return pairs;
-                }, [] as { a: Document, b: Document, l: Document }[]));
-            }
+                        return pairs;
+                    }, [] as { a: Document, b: Document, l: Document }[]));
+                }
+            })
             return pairs;
         }, [] as { a: Document, b: Document, l: Document }[]);
     }
     componentDidMount() {
-        reaction(() => this.findPairs(), (pairs) => runInAction(() => this._triples = pairs));
+        reaction(() => DocumentManager.Instance.DocumentViews.map(dv => [
+            dv.props.Document.Get(KeyStore.AnnotationOn, false),
+            dv.props.Document.Get(KeyStore.LinkedFromDocs, false),
+            dv.props.Document.Get(KeyStore.LinkedToDocs, false)
+        ]), () => runInAction(() => this._triples = this.findPairs()));
     }
 
     onPointerDown(e: React.PointerEvent) {
@@ -483,14 +489,29 @@ export class LinksView extends React.Component<CollectionViewProps> {
     render() {
         if (!this._triples.length)
             return (null);
+        let y: { a: Document, b: Document, l: Document, n: number }[] = []
+        this._triples.map(t => {
+            if (!y.reduce((found, yi) => {
+                if (yi.a == t.a && yi.b == t.b) {
+                    if (yi.l != t.l)
+                        yi.n++;
+                    return true;
+                }
+                return found;
+            }, false)) {
+                y.push({ a: t.a, b: t.b, l: t.l, n: 1 });
+            }
+        })
+        console.log("RENDERING " + y.length)
         return <svg className="collectionfreeformview-svgCanvas">
-            {this._triples.map(pair => {
+            {y.map(pair => {
                 let x1 = pair.a.GetNumber(KeyStore.X, 0) + pair.a.GetNumber(KeyStore.Width, 0) / 2;
                 let y1 = pair.a.GetNumber(KeyStore.Y, 0) + pair.a.GetNumber(KeyStore.Height, 0) / 2;
                 let x2 = pair.b.GetNumber(KeyStore.X, 0) + pair.b.GetNumber(KeyStore.Width, 0) / 2;
                 let y2 = pair.b.GetNumber(KeyStore.Y, 0) + pair.b.GetNumber(KeyStore.Height, 0) / 2;
                 return (
                     <line key={Utils.GenerateGuid()} id={pair.l.Id} className="collectionfreeformview-linkLine" onPointerDown={this.onPointerDown}
+                        style={{ strokeWidth: `${pair.n * 5}` }}
                         x1={`${x1}`} y1={`${y1}`}
                         x2={`${x2}`} y2={`${y2}`} />
                 )
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 789f07e8c..c804085df 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -70,7 +70,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
     public static AddDocument(props: CollectionViewProps, doc: Document, allowDuplicates: boolean): boolean {
         var curPage = props.Document.GetNumber(KeyStore.CurPage, -1);
         doc.SetOnPrototype(KeyStore.Page, new NumberField(curPage));
-        if (curPage > 0) {
+        if (curPage >= 0) {
             doc.SetOnPrototype(KeyStore.AnnotationOn, props.Document);
         }
         if (props.Document.Get(props.fieldKey) instanceof Field) {
-- 
cgit v1.2.3-70-g09d2


From c3eea60fa586e8ff9cf59497c041044fd0143bb1 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 28 Mar 2019 21:03:17 -0400
Subject: organized collectionfreeformview.  fixed issues with showing links

---
 src/client/util/DocumentManager.ts                 |  24 +-
 .../views/collections/CollectionFreeFormView.scss  | 116 -----
 .../views/collections/CollectionFreeFormView.tsx   | 525 ---------------------
 src/client/views/collections/CollectionView.tsx    |   7 +-
 src/client/views/collections/MarqueeView.scss      |   8 -
 src/client/views/collections/MarqueeView.tsx       | 175 -------
 src/client/views/collections/PreviewCursor.scss    |  18 -
 src/client/views/collections/PreviewCursor.tsx     |  76 ---
 .../CollectionFreeFormLinkView.scss                |   6 +
 .../CollectionFreeFormLinkView.tsx                 |  37 ++
 .../CollectionFreeFormLinksView.scss               |   7 +
 .../CollectionFreeFormLinksView.tsx                |  56 +++
 .../collectionFreeForm/CollectionFreeFormView.scss |  95 ++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 416 ++++++++++++++++
 .../collectionFreeForm/MarqueeView.scss            |   8 +
 .../collections/collectionFreeForm/MarqueeView.tsx | 175 +++++++
 .../collectionFreeForm/PreviewCursor.scss          |  18 +
 .../collectionFreeForm/PreviewCursor.tsx           |  76 +++
 src/client/views/nodes/DocumentContentsView.tsx    |   2 +-
 19 files changed, 924 insertions(+), 921 deletions(-)
 delete mode 100644 src/client/views/collections/CollectionFreeFormView.scss
 delete mode 100644 src/client/views/collections/CollectionFreeFormView.tsx
 delete mode 100644 src/client/views/collections/MarqueeView.scss
 delete mode 100644 src/client/views/collections/MarqueeView.tsx
 delete mode 100644 src/client/views/collections/PreviewCursor.scss
 delete mode 100644 src/client/views/collections/PreviewCursor.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/MarqueeView.scss
 create mode 100644 src/client/views/collections/collectionFreeForm/MarqueeView.tsx
 create mode 100644 src/client/views/collections/collectionFreeForm/PreviewCursor.scss
 create mode 100644 src/client/views/collections/collectionFreeForm/PreviewCursor.tsx

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 28624dba8..bf59fbb43 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,10 +1,11 @@
 import React = require('react')
 import { observer } from 'mobx-react';
-import { observable, action } from 'mobx';
+import { observable, action, computed } from 'mobx';
 import { Document } from "../../fields/Document"
 import { DocumentView } from '../views/nodes/DocumentView';
 import { KeyStore } from '../../fields/KeyStore';
 import { FieldWaiting } from '../../fields/Field';
+import { ListField } from '../../fields/ListField';
 
 
 export class DocumentManager {
@@ -74,4 +75,25 @@ export class DocumentManager {
 
         return (toReturn);
     }
+
+    @computed
+    public get LinkedDocumentViews() {
+        return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
+            let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
+            if (linksList && linksList != FieldWaiting && linksList.Data.length) {
+                pairs.push(...linksList.Data.reduce((pairs, link) => {
+                    if (link instanceof Document) {
+                        let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
+                        if (linkToDoc && linkToDoc != FieldWaiting) {
+                            DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
+                                pairs.push({ a: dv, b: docView1, l: link })
+                            })
+                        }
+                    }
+                    return pairs;
+                }, [] as { a: DocumentView, b: DocumentView, l: Document }[]));
+            }
+            return pairs;
+        }, [] as { a: DocumentView, b: DocumentView, l: Document }[]);
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss
deleted file mode 100644
index 7012ce6b9..000000000
--- a/src/client/views/collections/CollectionFreeFormView.scss
+++ /dev/null
@@ -1,116 +0,0 @@
-@import "../global_variables";
-
-.collectionfreeformview-linkLine {
-    stroke: black;
-    stroke-width: 3;
-    transform: translate(10000px,10000px);
-    pointer-events: all;
-}
-.collectionfreeformview-svgCanvas{
-  transform: translate(-10000px,-10000px);
-  position: absolute;
-  width: 20000px; 
-  height: 20000px;
-  pointer-events: none;
-}
-.collectionfreeformview-container {
-  .collectionfreeformview > .jsx-parser {
-    position: absolute;
-    height: 100%;
-    width: 100%;
-  }
-
-  .inking-canvas {
-    transform-origin: 50000px 50000px;
-  }
-  //nested freeform views
-  // .collectionfreeformview-container {
-    // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
-    //   linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
-    // background-size: 30px 30px;
-  // }
-
-  box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
-  border: 0px solid $light-color-secondary;
-  border-radius: $border-radius;
-  box-sizing: border-box;
-  position: relative;
-  overflow: hidden;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  .collectionfreeformview {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    .inking-canvas {
-      transform-origin: 50000px 50000px;
-    }
-  }
-}
-.collectionfreeformview-overlay {
-  .collectionfreeformview > .jsx-parser {
-    position: absolute;
-    height: 100%;
-  }
-  .formattedTextBox-cont {
-    background: $light-color-secondary;
-  }
-
-  .inking-canvas {
-    transform-origin: 50000px 50000px;
-  }
-
-  opacity: 0.99;
-  border: 0px solid transparent;
-  border-radius: $border-radius;
-  box-sizing: border-box;
-  position:relative;
-  overflow: hidden;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 100%;
-  .collectionfreeformview {
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    .inking-canvas {
-      transform-origin: 50000px 50000px;
-    }
-    .formattedTextBox-cont {
-        background:yellow;
-    }
-  }
-}
-
-// selection border...?
-.border {
-  border-style: solid;
-  box-sizing: border-box;
-  width: 98%;
-  height: 98%;
-  border-radius: $border-radius;
-}
-
-//this is an animation for the blinking cursor!
-@keyframes blink {
-  0% {
-    opacity: 0;
-  }
-  49% {
-    opacity: 0;
-  }
-  50% {
-    opacity: 1;
-  }
-}
-
-#prevCursor {
-  animation: blink 1s infinite;
-}
diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx
deleted file mode 100644
index 44f4eadaa..000000000
--- a/src/client/views/collections/CollectionFreeFormView.tsx
+++ /dev/null
@@ -1,525 +0,0 @@
-import { action, computed, observable, reaction, runInAction, trace, untracked, IReactionDisposer } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting, Field, Opt } 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 { undoBatch } from "../../util/UndoManager";
-import { InkingCanvas } from "../InkingCanvas";
-import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView";
-import { DocumentContentsView } from "../nodes/DocumentContentsView";
-import { DocumentViewProps, DocumentView } from "../nodes/DocumentView";
-import "./CollectionFreeFormView.scss";
-import { COLLECTION_BORDER_WIDTH } from "./CollectionView";
-import { CollectionViewBase, CollectionViewProps } from "./CollectionViewBase";
-import { MarqueeView } from "./MarqueeView";
-import { PreviewCursor } from "./PreviewCursor";
-import React = require("react");
-import v5 = require("uuid/v5");
-import { DocumentManager } from "../../util/DocumentManager";
-import { Utils } from "../../../Utils";
-import { Server } from "../../Server";
-import { AverageAggregateParameters } from "../../northstar/model/idea/idea";
-
-@observer
-export class CollectionFreeFormView extends CollectionViewBase {
-    public _canvasRef = React.createRef<HTMLDivElement>();
-    private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
-
-    public addLiveTextBox = (newBox: Document) => {
-        // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself
-        this._selectOnLoaded = newBox.Id;
-        //set text to be the typed key and get focus on text box
-        this.props.addDocument(newBox, false);
-        //remove cursor from screen
-        this.PreviewCursorVisible = false;
-    }
-
-    public selectDocuments = (docs: Document[]) => {
-        this.props.CollectionView.SelectedDocs.length = 0;
-        docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id));
-    }
-
-    public getActiveDocuments = () => {
-        var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
-        const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
-        let active: Document[] = [];
-        if (lvalue && lvalue != FieldWaiting) {
-            lvalue.Data.map(doc => {
-                var page = doc.GetNumber(KeyStore.Page, -1);
-                if (page == curPage || page == -1) {
-                    active.push(doc);
-                }
-            })
-        }
-
-        return active;
-    }
-
-    //determines whether the blinking cursor for indicating whether a text will be made on key down is visible
-    @observable public PreviewCursorVisible: boolean = false;
-    @observable public MarqueeVisible = false;
-    @observable public DownX: number = 0;
-    @observable public DownY: number = 0;
-    @observable private _lastX: number = 0;
-    @observable private _lastY: number = 0;
-
-    @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
-    @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
-    @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
-    @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
-    @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
-    @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
-    @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
-    @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; }  // shift so pan position is at center of window for non-overlay collections
-    @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections
-
-    @undoBatch
-    @action
-    drop = (e: Event, de: DragManager.DropEvent) => {
-        if (super.drop(e, de)) {
-            if (de.data instanceof DragManager.DocumentDragData) {
-                let screenX = de.x - (de.data.xOffset as number || 0);
-                let screenY = de.y - (de.data.yOffset as number || 0);
-                const [x, y] = this.getTransform().transformPoint(screenX, screenY);
-                de.data.droppedDocument.SetNumber(KeyStore.X, x);
-                de.data.droppedDocument.SetNumber(KeyStore.Y, y);
-                if (!de.data.droppedDocument.GetNumber(KeyStore.Width, 0)) {
-                    de.data.droppedDocument.SetNumber(KeyStore.Width, 300);
-                    de.data.droppedDocument.SetNumber(KeyStore.Height, 300);
-                }
-                this.bringToFront(de.data.droppedDocument);
-            }
-            return true;
-        }
-        return false;
-    }
-
-
-    @action
-    cleanupInteractions = () => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-        this.MarqueeVisible = false;
-    }
-
-    @action
-    onPointerDown = (e: React.PointerEvent): void => {
-        this.PreviewCursorVisible = false;
-        if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.addEventListener("pointermove", this.onPointerMove);
-            document.removeEventListener("pointerup", this.onPointerUp);
-            document.addEventListener("pointerup", this.onPointerUp);
-            this._lastX = this.DownX = e.pageX;
-            this._lastY = this.DownY = e.pageY;
-        }
-    }
-
-    @action
-    onPointerUp = (e: PointerEvent): void => {
-        e.stopPropagation();
-
-        if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) {
-            //show preview text cursor on tap
-            this.PreviewCursorVisible = true;
-            //select is not already selected
-            if (!this.props.isSelected()) {
-                this.props.select(false);
-            }
-        }
-        this.cleanupInteractions();
-    }
-
-    @action
-    onPointerMove = (e: PointerEvent): void => {
-        if (!e.cancelBubble && this.props.active()) {
-            if (e.buttons == 1 && !e.altKey && !e.metaKey) {
-                this.MarqueeVisible = true;
-            }
-            if (this.MarqueeVisible) {
-                e.stopPropagation();
-                e.preventDefault();
-            }
-            else if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) {
-                let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
-                let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
-                let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
-                this.SetPan(x - dx, y - dy);
-                this._lastX = e.pageX;
-                this._lastY = e.pageY;
-                e.stopPropagation();
-                e.preventDefault();
-            }
-        }
-    }
-
-    @action
-    onPointerWheel = (e: React.WheelEvent): void => {
-        this.props.select(false);
-        e.stopPropagation();
-        e.preventDefault();
-        let coefficient = 1000;
-
-        if (e.ctrlKey) {
-            var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
-            var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
-            const coefficient = 1000;
-            let deltaScale = (1 - (e.deltaY / coefficient));
-            this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale);
-            this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale);
-            e.stopPropagation();
-            e.preventDefault();
-        } else {
-            // if (modes[e.deltaMode] == 'pixels') coefficient = 50;
-            // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height??
-            let transform = this.getTransform();
-
-            let deltaScale = (1 - (e.deltaY / coefficient));
-            if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay)
-                deltaScale = 1 / this.zoomScaling;
-            let [x, y] = transform.transformPoint(e.clientX, e.clientY);
-
-            let localTransform = this.getLocalTransform()
-            localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y)
-            // console.log(localTransform)
-
-            this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale);
-            this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale);
-        }
-    }
-
-    @action
-    private SetPan(panX: number, panY: number) {
-        var x1 = this.getLocalTransform().inverse().Scale;
-        const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX));
-        const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY));
-        this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
-        this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
-    }
-
-    @action
-    onDrop = (e: React.DragEvent): void => {
-        var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
-        super.onDrop(e, { x: pt[0], y: pt[1] });
-    }
-
-    onDragOver = (): void => {
-    }
-
-    @action
-    bringToFront(doc: Document) {
-        const { fieldKey: fieldKey, Document: Document } = this.props;
-
-        const value: Document[] = Document.GetList<Document>(fieldKey, []).slice();
-        value.sort((doc1, doc2) => {
-            if (doc1 === doc) {
-                return 1;
-            }
-            if (doc2 === doc) {
-                return -1;
-            }
-            return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0);
-        }).map((doc, index) => {
-            doc.SetNumber(KeyStore.ZIndex, index + 1)
-        });
-    }
-
-    @computed get backgroundLayout(): string | undefined {
-        let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
-        if (field && field !== FieldWaiting) {
-            return field.Data;
-        }
-    }
-    @computed get overlayLayout(): string | undefined {
-        let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField);
-        if (field && field !== FieldWaiting) {
-            return field.Data;
-        }
-    }
-
-    focusDocument = (doc: Document) => {
-        let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2;
-        let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2;
-        this.SetPan(x, y);
-        this.props.focus(this.props.Document);
-    }
-
-    getDocumentViewProps(document: Document): DocumentViewProps {
-        return {
-            Document: document,
-            AddDocument: this.props.addDocument,
-            RemoveDocument: this.props.removeDocument,
-            ScreenToLocalTransform: this.getTransform,
-            isTopMost: false,
-            SelectOnLoad: document.Id == this._selectOnLoaded,
-            PanelWidth: document.Width,
-            PanelHeight: document.Height,
-            ContentScaling: this.noScaling,
-            ContainingCollectionView: this.props.CollectionView,
-            focus: this.focusDocument
-        }
-    }
-
-    @computed
-    get views() {
-        var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
-        const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
-        if (lvalue && lvalue != FieldWaiting) {
-            return lvalue.Data.map(doc => {
-                if (!doc) return null;
-                var page = doc.GetNumber(KeyStore.Page, 0);
-                return (page != curPage && page != 0) ? (null) :
-                    (<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
-            })
-        }
-        return null;
-    }
-
-    @computed
-    get backgroundView() {
-        return !this.backgroundLayout ? (null) :
-            (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
-                layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
-    }
-    @computed
-    get overlayView() {
-        return !this.overlayLayout ? (null) :
-            (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
-                layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
-    }
-
-    getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform())
-    getMarqueeTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH)
-    getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY);
-    noScaling = () => 1;
-
-    //when focus is lost, this will remove the preview cursor
-    @action
-    onBlur = (): void => {
-        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];
-
-        const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0);
-        const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0);
-        // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX;
-        // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY;
-        // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY));
-
-        return (
-            <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
-                onPointerDown={this.onPointerDown}
-                onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
-                onWheel={this.onPointerWheel}
-                onDrop={this.onDrop.bind(this)}
-                onDragOver={this.onDragOver}
-                onBlur={this.onBlur}
-                style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}// , zIndex: !this.props.isTopMost ? -1 : undefined }}
-                tabIndex={0}
-                ref={this.createDropTarget}>
-                <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}>
-                    {this.backgroundView}
-                    <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
-                    <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} />
-                    {this.views}
-                    <LinksView {...this.props} />
-                    {super.getCursors().map(entry => {
-                        if (entry.Data.length > 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 (
-                                <div
-                                    key={id}
-                                    style={{
-                                        position: "absolute",
-                                        transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
-                                        zIndex: 10000,
-                                        transformOrigin: 'center center',
-                                    }}
-                                >
-                                    <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
-                                        }}
-                                    >{email[0].toUpperCase()}</p>
-                                </div>
-                            );
-                        }
-                    })}
-                </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>
-        );
-    }
-}
-
-@observer
-export class LinksView extends React.Component<CollectionViewProps> {
-    @observable _connections: { a: Document, b: Document, l: Document }[] = [];
-
-    private _reactionDisposer: Opt<IReactionDisposer>;
-
-    componentDidMount() {
-        this._reactionDisposer = reaction(() => DocumentManager.Instance.DocumentViews.map(dv => [
-            dv.props.Document.Get(KeyStore.AnnotationOn, false),
-            dv.props.Document.Get(KeyStore.LinkedFromDocs, false),
-            dv.props.Document.Get(KeyStore.LinkedToDocs, false)
-        ]), () => runInAction(() => this._connections = this.findPairs()), { fireImmediately: true });
-    }
-    componentWillUnmount() {
-        if (this._reactionDisposer) {
-            this._reactionDisposer();
-        }
-        this._reactionDisposer = undefined;
-    }
-
-    filtered(views: DocumentView[]) {
-        return views.filter(sv =>
-            sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == this.props.Document
-        );
-    }
-
-    findPairs() {
-        return DocumentManager.Instance.DocumentViews.reduce((pairs, dv) => {
-            untracked(() => {
-                let srcViews = [dv];
-                let srcAnnot = dv.props.Document.GetT(KeyStore.AnnotationOn, Document);
-                if (srcAnnot && srcAnnot != FieldWaiting && srcAnnot instanceof Document) {
-                    srcViews = DocumentManager.Instance.getDocumentViews(srcAnnot.GetPrototype() as Document)
-                }
-                let linksList = dv.props.Document.GetT(KeyStore.LinkedToDocs, ListField);
-                if (linksList && linksList != FieldWaiting && linksList.Data.length) {
-                    pairs.push(...linksList.Data.reduce((pairs, link) => {
-                        if (link instanceof Document) {
-                            let linkToDoc = link.GetT(KeyStore.LinkedToDocs, Document);
-                            if (linkToDoc && linkToDoc != FieldWaiting) {
-                                DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
-                                    let targetViews = [docView1];
-                                    let docAnnot = docView1.props.Document.GetT(KeyStore.AnnotationOn, Document);
-                                    if (docAnnot && docAnnot != FieldWaiting && docAnnot instanceof Document) {
-                                        targetViews = DocumentManager.Instance.getDocumentViews(docAnnot.GetPrototype() as Document)
-                                    }
-                                    this.filtered(targetViews).map(tv => this.filtered(srcViews).map(sv =>
-                                        pairs.push({ a: sv.props.Document, b: tv.props.Document, l: link })))
-                                })
-                            }
-                        }
-                        return pairs;
-                    }, [] as { a: Document, b: Document, l: Document }[]));
-                }
-            })
-            return pairs;
-        }, [] as { a: Document, b: Document, l: Document }[]);
-    }
-
-    onPointerDown(e: React.PointerEvent) {
-        let line = (e.nativeEvent as any).path[0];
-        line.style.stroke = "red";
-        Server.GetField(line.id, action((f: Opt<Field>) => {
-            if (f instanceof Document) {
-                console.log(f.Title);
-            }
-        }));
-    }
-
-    @computed
-    get uniqueConnections() {
-        return this._connections.reduce((y, t) => {
-            if (!y.reduce((found, yi) => {
-                if (yi.a == t.a && yi.b == t.b) {
-                    if (yi.l != t.l)
-                        yi.n++;
-                    return true;
-                }
-                return found;
-            }, false)) {
-                y.push({ a: t.a, b: t.b, l: t.l, n: 1 });
-            }
-            return y
-        }, [] as { a: Document, b: Document, l: Document, n: number }[]);
-    }
-
-    render() {
-        if (!this._connections.length)
-            return (null);
-        return <svg className="collectionfreeformview-svgCanvas">
-            {this.uniqueConnections.map(c => {
-                let x1 = c.a.GetNumber(KeyStore.X, 0) + c.a.GetNumber(KeyStore.Width, 0) / 2;
-                let y1 = c.a.GetNumber(KeyStore.Y, 0) + c.a.GetNumber(KeyStore.Height, 0) / 2;
-                let x2 = c.b.GetNumber(KeyStore.X, 0) + c.b.GetNumber(KeyStore.Width, 0) / 2;
-                let y2 = c.b.GetNumber(KeyStore.Y, 0) + c.b.GetNumber(KeyStore.Height, 0) / 2;
-                return (
-                    <line key={Utils.GenerateGuid()} id={c.l.Id} className="collectionfreeformview-linkLine" onPointerDown={this.onPointerDown}
-                        style={{ strokeWidth: `${c.n * 5}` }}
-                        x1={`${x1}`} y1={`${y1}`}
-                        x2={`${x2}`} y2={`${y2}`} />
-                )
-            })}
-        </svg>
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index c804085df..a1498e0ae 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -7,7 +7,7 @@ import { ContextMenu } from "../ContextMenu";
 import React = require("react");
 import { KeyStore } from "../../../fields/KeyStore";
 import { NumberField } from "../../../fields/NumberField";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
 import { CollectionDockingView } from "./CollectionDockingView";
 import { CollectionSchemaView } from "./CollectionSchemaView";
 import { CollectionViewProps } from "./CollectionViewBase";
@@ -104,6 +104,11 @@ export class CollectionView extends React.Component<CollectionViewProps> {
                 break;
             }
         }
+        doc.GetTAsync(KeyStore.AnnotationOn, Document).then((annotationOn) => {
+            if (annotationOn == props.Document) {
+                doc.Set(KeyStore.AnnotationOn, undefined, true);
+            }
+        })
 
         if (index !== -1) {
             value.splice(index, 1)
diff --git a/src/client/views/collections/MarqueeView.scss b/src/client/views/collections/MarqueeView.scss
deleted file mode 100644
index 6d9a79344..000000000
--- a/src/client/views/collections/MarqueeView.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-
-.marqueeView {
-    border-style: dashed;
-    box-sizing: border-box;
-    position: absolute;
-    border-width: 1px;
-    border-color: black;
-}
\ No newline at end of file
diff --git a/src/client/views/collections/MarqueeView.tsx b/src/client/views/collections/MarqueeView.tsx
deleted file mode 100644
index b48ad9c64..000000000
--- a/src/client/views/collections/MarqueeView.tsx
+++ /dev/null
@@ -1,175 +0,0 @@
-import { action, IReactionDisposer, observable, reaction } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { FieldWaiting, Opt } from "../../../fields/Field";
-import { KeyStore } from "../../../fields/KeyStore";
-import { Documents } from "../../documents/Documents";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Transform } from "../../util/Transform";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
-import "./MarqueeView.scss";
-import React = require("react");
-import { InkField, StrokeData } from "../../../fields/InkField";
-import { Utils } from "../../../Utils";
-import { InkingCanvas } from "../InkingCanvas";
-
-interface MarqueeViewProps {
-    getMarqueeTransform: () => Transform;
-    getTransform: () => Transform;
-    container: CollectionFreeFormView;
-    addDocument: (doc: Document, allowDuplicates: false) => boolean;
-    activeDocuments: () => Document[];
-    selectDocuments: (docs: Document[]) => void;
-    removeDocument: (doc: Document) => boolean;
-}
-
-@observer
-export class MarqueeView extends React.Component<MarqueeViewProps>
-{
-    private _reactionDisposer: Opt<IReactionDisposer>;
-
-    @observable _lastX: number = 0;
-    @observable _lastY: number = 0;
-    @observable _downX: number = 0;
-    @observable _downY: number = 0;
-
-    componentDidMount() {
-        this._reactionDisposer = reaction(
-            () => this.props.container.MarqueeVisible,
-            (visible: boolean) => this.onPointerDown(visible, this.props.container.DownX, this.props.container.DownY))
-    }
-    componentWillUnmount() {
-        if (this._reactionDisposer) {
-            this._reactionDisposer();
-        }
-        this.cleanupInteractions();
-    }
-
-    @action
-    cleanupInteractions = () => {
-        document.removeEventListener("pointermove", this.onPointerMove, true)
-        document.removeEventListener("pointerup", this.onPointerUp, true);
-        document.removeEventListener("keydown", this.marqueeCommand, true);
-    }
-
-    @action
-    onPointerDown = (visible: boolean, downX: number, downY: number): void => {
-        if (visible) {
-            this._downX = this._lastX = downX;
-            this._downY = this._lastY = downY;
-            document.addEventListener("pointermove", this.onPointerMove, true)
-            document.addEventListener("pointerup", this.onPointerUp, true);
-            document.addEventListener("keydown", this.marqueeCommand, true);
-        }
-    }
-
-    @action
-    onPointerMove = (e: PointerEvent): void => {
-        this._lastX = e.pageX;
-        this._lastY = e.pageY;
-    }
-
-    @action
-    onPointerUp = (e: PointerEvent): void => {
-        this.cleanupInteractions();
-        if (!e.shiftKey) {
-            SelectionManager.DeselectAll();
-        }
-        this.props.selectDocuments(this.marqueeSelect());
-    }
-
-    intersectRect(r1: { left: number, top: number, width: number, height: number },
-        r2: { left: number, top: number, width: number, height: number }) {
-        return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
-    }
-
-    get Bounds() {
-        let left = this._downX < this._lastX ? this._downX : this._lastX;
-        let top = this._downY < this._lastY ? this._downY : this._lastY;
-        let topLeft = this.props.getTransform().transformPoint(left, top);
-        let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
-        return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }
-    }
-
-    @action
-    marqueeCommand = (e: KeyboardEvent) => {
-        if (e.key == "Backspace" || e.key == "Delete") {
-            this.marqueeSelect().map(d => this.props.removeDocument(d));
-            this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField);
-            this.cleanupInteractions();
-        }
-        if (e.key == "c") {
-            let bounds = this.Bounds;
-            let selected = this.marqueeSelect().map(d => {
-                this.props.removeDocument(d);
-                d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);
-                d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);
-                d.SetNumber(KeyStore.Page, 0);
-                d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));
-                return d;
-            });
-            let liftedInk = this.marqueeInkSelect(true);
-            this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField);
-            //setTimeout(() => {
-            let newCollection = Documents.FreeformDocument(selected, {
-                x: bounds.left,
-                y: bounds.top,
-                panx: 0,
-                pany: 0,
-                width: bounds.width,
-                height: bounds.height,
-                backgroundColor: "Transparent",
-                ink: liftedInk,
-                title: "a nested collection"
-            });
-            this.props.addDocument(newCollection, false);
-            // }, 100);
-            this.cleanupInteractions();
-        }
-    }
-    marqueeInkSelect(select: boolean) {
-        let selRect = this.Bounds;
-        let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
-        let centerShiftY = 0 - (selRect.top + selRect.height / 2);
-        let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
-        if (ink && ink != FieldWaiting && ink.Data) {
-            let idata = new Map();
-            ink.Data.forEach((value: StrokeData, key: string, map: any) => {
-                let inside = InkingCanvas.IntersectStrokeRect(value, selRect);
-                if (inside && select) {
-                    idata.set(key,
-                        {
-                            pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }),
-                            color: value.color,
-                            width: value.width,
-                            tool: value.tool,
-                            page: -1
-                        });
-                } else if (!inside && !select) {
-                    idata.set(key, value);
-                }
-            })
-            return idata;
-        }
-    }
-
-    marqueeSelect() {
-        let selRect = this.Bounds;
-        let selection: Document[] = [];
-        this.props.activeDocuments().map(doc => {
-            var x = doc.GetNumber(KeyStore.X, 0);
-            var y = doc.GetNumber(KeyStore.Y, 0);
-            var w = doc.GetNumber(KeyStore.Width, 0);
-            var h = doc.GetNumber(KeyStore.Height, 0);
-            if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect))
-                selection.push(doc)
-        })
-        return selection;
-    }
-
-    render() {
-        let p = this.props.getMarqueeTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
-        let v = this.props.getMarqueeTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
-        return (!this.props.container.MarqueeVisible ? (null) : <div className="marqueeView" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />);
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/collections/PreviewCursor.scss b/src/client/views/collections/PreviewCursor.scss
deleted file mode 100644
index a797411f6..000000000
--- a/src/client/views/collections/PreviewCursor.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-
-.previewCursor {
-    color: black;
-    position: absolute;
-    transform-origin: left top;
-    pointer-events: none;
-}
-
-//this is an animation for the blinking cursor!
-@keyframes blink {
-	0% {opacity: 0}
-	49%{opacity: 0}
-	50% {opacity: 1}
-}
-
-#previewCursor {
-    animation: blink 1s infinite;
-}
\ No newline at end of file
diff --git a/src/client/views/collections/PreviewCursor.tsx b/src/client/views/collections/PreviewCursor.tsx
deleted file mode 100644
index 8ae59a83a..000000000
--- a/src/client/views/collections/PreviewCursor.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { action, IReactionDisposer, observable, reaction } from "mobx";
-import { observer } from "mobx-react";
-import { Document } from "../../../fields/Document";
-import { Opt } from "../../../fields/Field";
-import { Documents } from "../../documents/Documents";
-import { Transform } from "../../util/Transform";
-import { CollectionFreeFormView } from "./CollectionFreeFormView";
-import "./PreviewCursor.scss";
-import React = require("react");
-
-
-export interface PreviewCursorProps {
-    getTransform: () => Transform;
-    container: CollectionFreeFormView;
-    addLiveTextDocument: (doc: Document) => void;
-}
-
-@observer
-export class PreviewCursor extends React.Component<PreviewCursorProps>  {
-    private _reactionDisposer: Opt<IReactionDisposer>;
-
-    @observable _lastX: number = 0;
-    @observable _lastY: number = 0;
-
-    componentDidMount() {
-        this._reactionDisposer = reaction(
-            () => this.props.container.PreviewCursorVisible,
-            (visible: boolean) => this.onCursorPlaced(visible, this.props.container.DownX, this.props.container.DownY))
-    }
-    componentWillUnmount() {
-        if (this._reactionDisposer) {
-            this._reactionDisposer();
-        }
-        this.cleanupInteractions();
-    }
-
-
-    @action
-    cleanupInteractions = () => {
-        document.removeEventListener("keypress", this.onKeyPress, false);
-    }
-
-    @action
-    onCursorPlaced = (visible: boolean, downX: number, downY: number): void => {
-        if (visible) {
-            document.addEventListener("keypress", this.onKeyPress, false);
-            this._lastX = downX;
-            this._lastY = downY;
-        } else
-            this.cleanupInteractions();
-    }
-
-    @action
-    onKeyPress = (e: KeyboardEvent) => {
-        // Mixing events between React and Native is finicky.  In FormattedTextBox, we set the
-        // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
-        // the keyPress here.
-        //if not these keys, make a textbox if preview cursor is active!
-        if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
-            //make textbox and add it to this collection
-            let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
-            let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" });
-            this.props.addLiveTextDocument(newBox);
-            e.stopPropagation();
-        }
-    }
-
-    render() {
-        //get local position and place cursor there!
-        let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
-        return (
-            !this.props.container.PreviewCursorVisible ? (null) :
-                <div className="previewCursor" id="previewCursor" style={{ transform: `translate(${x}px, ${y}px)` }}>I</div>)
-
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
new file mode 100644
index 000000000..3b2f79be1
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -0,0 +1,6 @@
+.collectionfreeformlinkview-linkLine {
+    stroke: black;
+    stroke-width: 3;
+    transform: translate(10000px,10000px);
+    pointer-events: all;
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
new file mode 100644
index 000000000..e84f0c5ad
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -0,0 +1,37 @@
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Utils } from "../../../../Utils";
+import "./CollectionFreeFormLinkView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+
+export interface CollectionFreeFormLinkViewProps {
+    A: Document;
+    B: Document;
+    LinkDocs: Document[];
+}
+
+@observer
+export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
+
+    onPointerDown = (e: React.PointerEvent) => {
+        this.props.LinkDocs.map(l =>
+            console.log("Link:" + l.Title));
+    }
+    render() {
+        let l = this.props.LinkDocs;
+        let a = this.props.A;
+        let b = this.props.B;
+        let x1 = a.GetNumber(KeyStore.X, 0) + a.GetNumber(KeyStore.Width, 0) / 2;
+        let y1 = a.GetNumber(KeyStore.Y, 0) + a.GetNumber(KeyStore.Height, 0) / 2;
+        let x2 = b.GetNumber(KeyStore.X, 0) + b.GetNumber(KeyStore.Width, 0) / 2;
+        let y2 = b.GetNumber(KeyStore.Y, 0) + b.GetNumber(KeyStore.Height, 0) / 2;
+        return (
+            <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown}
+                style={{ strokeWidth: `${l.length * 5}` }}
+                x1={`${x1}`} y1={`${y1}`}
+                x2={`${x2}`} y2={`${y2}`} />
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
new file mode 100644
index 000000000..9af0df7f5
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -0,0 +1,7 @@
+.collectionfreeformlinksview-svgCanvas{
+    transform: translate(-10000px,-10000px);
+    position: absolute;
+    width: 20000px; 
+    height: 20000px;
+    pointer-events: none;
+  }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
new file mode 100644
index 000000000..4f28f43eb
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -0,0 +1,56 @@
+import { computed } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Utils } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { DocumentView } from "../../nodes/DocumentView";
+import { CollectionViewProps } from "../CollectionViewBase";
+import "./CollectionFreeFormLinksView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
+
+@observer
+export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
+
+    documentAnchors(view: DocumentView) {
+        let equalViews = [view];
+        let containerDoc = view.props.Document.GetT(KeyStore.AnnotationOn, Document);
+        if (containerDoc && containerDoc != FieldWaiting && containerDoc instanceof Document) {
+            equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.GetPrototype() as Document)
+        }
+        return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document == this.props.Document);
+    }
+
+    @computed
+    get uniqueConnections() {
+        return DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+            let srcViews = this.documentAnchors(connection.a);
+            let targetViews = this.documentAnchors(connection.b);
+            let possiblePairs: { a: Document, b: Document, }[] = [];
+            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
+            possiblePairs.map(possiblePair => {
+                if (!drawnPairs.reduce((found, drawnPair) => {
+                    let match = (possiblePair.a == drawnPair.a && possiblePair.b == drawnPair.b);
+                    if (match) {
+                        if (!drawnPair.l.reduce((found, link) => found || link.Id == connection.l.Id, false))
+                            drawnPair.l.push(connection.l);
+                    }
+                    return match || found;
+                }, false)) {
+                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] as Document[] });
+                }
+            })
+            return drawnPairs
+        }, [] as { a: Document, b: Document, l: Document[] }[]);
+    }
+
+    render() {
+        return (
+            <svg className="collectionfreeformlinksview-svgCanvas">
+                {this.uniqueConnections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />)}
+            </svg>);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
new file mode 100644
index 000000000..9c5e98005
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -0,0 +1,95 @@
+@import "../../global_variables";
+
+.collectionfreeformview-container {
+  .collectionfreeformview > .jsx-parser {
+    position: absolute;
+    height: 100%;
+    width: 100%;
+  }
+
+  .inking-canvas {
+    transform-origin: 50000px 50000px;
+  }
+  //nested freeform views
+  // .collectionfreeformview-container {
+    // background-image: linear-gradient(to right, $light-color-secondary 1px, transparent 1px),
+    //   linear-gradient(to bottom, $light-color-secondary 1px, transparent 1px);
+    // background-size: 30px 30px;
+  // }
+
+  box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+  border: 0px solid $light-color-secondary;
+  border-radius: $border-radius;
+  box-sizing: border-box;
+  position: relative;
+  overflow: hidden;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  .collectionfreeformview {
+    position: absolute;
+    top: 0;
+    left: 0;
+    width: 100%;
+    height: 100%;
+    .inking-canvas {
+      transform-origin: 50000px 50000px;
+    }
+  }
+}
+.collectionfreeformview-overlay {
+  .collectionfreeformview > .jsx-parser {
+    position: absolute;
+    height: 100%;
+  }
+  .formattedTextBox-cont {
+    background: $light-color-secondary;
+  }
+
+  .inking-canvas {
+    transform-origin: 50000px 50000px;
+  }
+
+  opacity: 0.99;
+  border: 0px solid transparent;
+  border-radius: $border-radius;
+  box-sizing: border-box;
+  position:relative;
+  overflow: hidden;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  .collectionfreeformview {
+    .formattedTextBox-cont {
+        background:yellow;
+    }
+  }
+}
+
+// selection border...?
+.border {
+  border-style: solid;
+  box-sizing: border-box;
+  width: 98%;
+  height: 98%;
+  border-radius: $border-radius;
+}
+
+//this is an animation for the blinking cursor!
+@keyframes blink {
+  0% {
+    opacity: 0;
+  }
+  49% {
+    opacity: 0;
+  }
+  50% {
+    opacity: 1;
+  }
+}
+
+#prevCursor {
+  animation: blink 1s infinite;
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
new file mode 100644
index 000000000..1f90b0d46
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -0,0 +1,416 @@
+import { action, computed, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+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 { undoBatch } from "../../../util/UndoManager";
+import { InkingCanvas } from "../../InkingCanvas";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentContentsView } from "../../nodes/DocumentContentsView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
+import { COLLECTION_BORDER_WIDTH } from "../CollectionView";
+import { CollectionViewBase } from "../CollectionViewBase";
+import { MarqueeView } from "./MarqueeView";
+import { PreviewCursor } from "./PreviewCursor";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import "./CollectionFreeFormView.scss";
+import React = require("react");
+import v5 = require("uuid/v5");
+
+@observer
+export class CollectionFreeFormView extends CollectionViewBase {
+    public _canvasRef = React.createRef<HTMLDivElement>();
+    private _selectOnLoaded: string = ""; // id of document that should be selected once it's loaded (used for click-to-type)
+
+    public addLiveTextBox = (newBox: Document) => {
+        // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself
+        this._selectOnLoaded = newBox.Id;
+        //set text to be the typed key and get focus on text box
+        this.props.addDocument(newBox, false);
+        //remove cursor from screen
+        this.PreviewCursorVisible = false;
+    }
+
+    public selectDocuments = (docs: Document[]) => {
+        this.props.CollectionView.SelectedDocs.length = 0;
+        docs.map(d => this.props.CollectionView.SelectedDocs.push(d.Id));
+    }
+
+    public getActiveDocuments = () => {
+        var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+        const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
+        let active: Document[] = [];
+        if (lvalue && lvalue != FieldWaiting) {
+            lvalue.Data.map(doc => {
+                var page = doc.GetNumber(KeyStore.Page, -1);
+                if (page == curPage || page == -1) {
+                    active.push(doc);
+                }
+            })
+        }
+
+        return active;
+    }
+
+    //determines whether the blinking cursor for indicating whether a text will be made on key down is visible
+    @observable public PreviewCursorVisible: boolean = false;
+    @observable public MarqueeVisible = false;
+    @observable public DownX: number = 0;
+    @observable public DownY: number = 0;
+    @observable private _lastX: number = 0;
+    @observable private _lastY: number = 0;
+
+    @computed get panX(): number { return this.props.Document.GetNumber(KeyStore.PanX, 0) }
+    @computed get panY(): number { return this.props.Document.GetNumber(KeyStore.PanY, 0) }
+    @computed get scale(): number { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
+    @computed get isAnnotationOverlay() { return this.props.fieldKey.Id === KeyStore.Annotations.Id; } // bcz: ? Why do we need to compare Id's?
+    @computed get nativeWidth() { return this.props.Document.GetNumber(KeyStore.NativeWidth, 0); }
+    @computed get nativeHeight() { return this.props.Document.GetNumber(KeyStore.NativeHeight, 0); }
+    @computed get zoomScaling() { return this.props.Document.GetNumber(KeyStore.Scale, 1); }
+    @computed get centeringShiftX() { return !this.props.Document.GetNumber(KeyStore.NativeWidth, 0) ? this.props.panelWidth() / 2 : 0; }  // shift so pan position is at center of window for non-overlay collections
+    @computed get centeringShiftY() { return !this.props.Document.GetNumber(KeyStore.NativeHeight, 0) ? this.props.panelHeight() / 2 : 0; }// shift so pan position is at center of window for non-overlay collections
+
+    @undoBatch
+    @action
+    drop = (e: Event, de: DragManager.DropEvent) => {
+        if (super.drop(e, de)) {
+            if (de.data instanceof DragManager.DocumentDragData) {
+                let screenX = de.x - (de.data.xOffset as number || 0);
+                let screenY = de.y - (de.data.yOffset as number || 0);
+                const [x, y] = this.getTransform().transformPoint(screenX, screenY);
+                de.data.droppedDocument.SetNumber(KeyStore.X, x);
+                de.data.droppedDocument.SetNumber(KeyStore.Y, y);
+                if (!de.data.droppedDocument.GetNumber(KeyStore.Width, 0)) {
+                    de.data.droppedDocument.SetNumber(KeyStore.Width, 300);
+                    de.data.droppedDocument.SetNumber(KeyStore.Height, 300);
+                }
+                this.bringToFront(de.data.droppedDocument);
+            }
+            return true;
+        }
+        return false;
+    }
+
+
+    @action
+    cleanupInteractions = () => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        this.MarqueeVisible = false;
+    }
+
+    @action
+    onPointerDown = (e: React.PointerEvent): void => {
+        this.PreviewCursorVisible = false;
+        if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) {
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.addEventListener("pointermove", this.onPointerMove);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            document.addEventListener("pointerup", this.onPointerUp);
+            this._lastX = this.DownX = e.pageX;
+            this._lastY = this.DownY = e.pageY;
+        }
+    }
+
+    @action
+    onPointerUp = (e: PointerEvent): void => {
+        e.stopPropagation();
+
+        if (Math.abs(this.DownX - e.clientX) < 4 && Math.abs(this.DownY - e.clientY) < 4) {
+            //show preview text cursor on tap
+            this.PreviewCursorVisible = true;
+            //select is not already selected
+            if (!this.props.isSelected()) {
+                this.props.select(false);
+            }
+        }
+        this.cleanupInteractions();
+    }
+
+    @action
+    onPointerMove = (e: PointerEvent): void => {
+        if (!e.cancelBubble && this.props.active()) {
+            if (e.buttons == 1 && !e.altKey && !e.metaKey) {
+                this.MarqueeVisible = true;
+            }
+            if (this.MarqueeVisible) {
+                e.stopPropagation();
+                e.preventDefault();
+            }
+            else if ((!this.isAnnotationOverlay || this.zoomScaling != 1) && !e.shiftKey) {
+                let x = this.props.Document.GetNumber(KeyStore.PanX, 0);
+                let y = this.props.Document.GetNumber(KeyStore.PanY, 0);
+                let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
+                this.SetPan(x - dx, y - dy);
+                this._lastX = e.pageX;
+                this._lastY = e.pageY;
+                e.stopPropagation();
+                e.preventDefault();
+            }
+        }
+    }
+
+    @action
+    onPointerWheel = (e: React.WheelEvent): void => {
+        this.props.select(false);
+        e.stopPropagation();
+        e.preventDefault();
+        let coefficient = 1000;
+
+        if (e.ctrlKey) {
+            var nativeWidth = this.props.Document.GetNumber(KeyStore.NativeWidth, 0);
+            var nativeHeight = this.props.Document.GetNumber(KeyStore.NativeHeight, 0);
+            const coefficient = 1000;
+            let deltaScale = (1 - (e.deltaY / coefficient));
+            this.props.Document.SetNumber(KeyStore.NativeWidth, nativeWidth * deltaScale);
+            this.props.Document.SetNumber(KeyStore.NativeHeight, nativeHeight * deltaScale);
+            e.stopPropagation();
+            e.preventDefault();
+        } else {
+            // if (modes[e.deltaMode] == 'pixels') coefficient = 50;
+            // else if (modes[e.deltaMode] == 'lines') coefficient = 1000; // This should correspond to line-height??
+            let transform = this.getTransform();
+
+            let deltaScale = (1 - (e.deltaY / coefficient));
+            if (deltaScale * this.zoomScaling < 1 && this.isAnnotationOverlay)
+                deltaScale = 1 / this.zoomScaling;
+            let [x, y] = transform.transformPoint(e.clientX, e.clientY);
+
+            let localTransform = this.getLocalTransform()
+            localTransform = localTransform.inverse().scaleAbout(deltaScale, x, y)
+            // console.log(localTransform)
+
+            this.props.Document.SetNumber(KeyStore.Scale, localTransform.Scale);
+            this.SetPan(-localTransform.TranslateX / localTransform.Scale, -localTransform.TranslateY / localTransform.Scale);
+        }
+    }
+
+    @action
+    private SetPan(panX: number, panY: number) {
+        var x1 = this.getLocalTransform().inverse().Scale;
+        const newPanX = Math.min((1 - 1 / x1) * this.nativeWidth, Math.max(0, panX));
+        const newPanY = Math.min((1 - 1 / x1) * this.nativeHeight, Math.max(0, panY));
+        this.props.Document.SetNumber(KeyStore.PanX, this.isAnnotationOverlay ? newPanX : panX);
+        this.props.Document.SetNumber(KeyStore.PanY, this.isAnnotationOverlay ? newPanY : panY);
+    }
+
+    @action
+    onDrop = (e: React.DragEvent): void => {
+        var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
+        super.onDrop(e, { x: pt[0], y: pt[1] });
+    }
+
+    onDragOver = (): void => {
+    }
+
+    @action
+    bringToFront(doc: Document) {
+        const { fieldKey: fieldKey, Document: Document } = this.props;
+
+        const value: Document[] = Document.GetList<Document>(fieldKey, []).slice();
+        value.sort((doc1, doc2) => {
+            if (doc1 === doc) {
+                return 1;
+            }
+            if (doc2 === doc) {
+                return -1;
+            }
+            return doc1.GetNumber(KeyStore.ZIndex, 0) - doc2.GetNumber(KeyStore.ZIndex, 0);
+        }).map((doc, index) => {
+            doc.SetNumber(KeyStore.ZIndex, index + 1)
+        });
+    }
+
+    @computed get backgroundLayout(): string | undefined {
+        let field = this.props.Document.GetT(KeyStore.BackgroundLayout, TextField);
+        if (field && field !== FieldWaiting) {
+            return field.Data;
+        }
+    }
+    @computed get overlayLayout(): string | undefined {
+        let field = this.props.Document.GetT(KeyStore.OverlayLayout, TextField);
+        if (field && field !== FieldWaiting) {
+            return field.Data;
+        }
+    }
+
+    focusDocument = (doc: Document) => {
+        let x = doc.GetNumber(KeyStore.X, 0) + doc.GetNumber(KeyStore.Width, 0) / 2;
+        let y = doc.GetNumber(KeyStore.Y, 0) + doc.GetNumber(KeyStore.Height, 0) / 2;
+        this.SetPan(x, y);
+        this.props.focus(this.props.Document);
+    }
+
+    getDocumentViewProps(document: Document): DocumentViewProps {
+        return {
+            Document: document,
+            AddDocument: this.props.addDocument,
+            RemoveDocument: this.props.removeDocument,
+            ScreenToLocalTransform: this.getTransform,
+            isTopMost: false,
+            SelectOnLoad: document.Id == this._selectOnLoaded,
+            PanelWidth: document.Width,
+            PanelHeight: document.Height,
+            ContentScaling: this.noScaling,
+            ContainingCollectionView: this.props.CollectionView,
+            focus: this.focusDocument
+        }
+    }
+
+    @computed
+    get views() {
+        var curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1);
+        const lvalue = this.props.Document.GetT<ListField<Document>>(this.props.fieldKey, ListField);
+        if (lvalue && lvalue != FieldWaiting) {
+            return lvalue.Data.map(doc => {
+                if (!doc) return null;
+                var page = doc.GetNumber(KeyStore.Page, 0);
+                return (page != curPage && page != 0) ? (null) :
+                    (<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
+            })
+        }
+        return null;
+    }
+
+    @computed
+    get backgroundView() {
+        return !this.backgroundLayout ? (null) :
+            (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
+                layoutKey={KeyStore.BackgroundLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
+    }
+    @computed
+    get overlayView() {
+        return !this.overlayLayout ? (null) :
+            (<DocumentContentsView {...this.getDocumentViewProps(this.props.Document)}
+                layoutKey={KeyStore.OverlayLayout} isTopMost={this.props.isTopMost} isSelected={() => false} select={() => { }} />);
+    }
+
+    getTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH).translate(-this.centeringShiftX, -this.centeringShiftY).transform(this.getLocalTransform())
+    getMarqueeTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-COLLECTION_BORDER_WIDTH, -COLLECTION_BORDER_WIDTH)
+    getLocalTransform = (): Transform => Transform.Identity.scale(1 / this.scale).translate(this.panX, this.panY);
+    noScaling = () => 1;
+
+    //when focus is lost, this will remove the preview cursor
+    @action
+    onBlur = (): void => {
+        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];
+
+        const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0);
+        const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0);
+        // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX;
+        // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY;
+        // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY));
+
+        return (
+            <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
+                onPointerDown={this.onPointerDown}
+                onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
+                onWheel={this.onPointerWheel}
+                onDrop={this.onDrop.bind(this)}
+                onDragOver={this.onDragOver}
+                onBlur={this.onBlur}
+                style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}// , zIndex: !this.props.isTopMost ? -1 : undefined }}
+                tabIndex={0}
+                ref={this.createDropTarget}>
+                <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}>
+                    {this.backgroundView}
+                    <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
+                    <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox} getTransform={this.getTransform} />
+                    {this.views}
+                    <CollectionFreeFormLinksView {...this.props} />
+                    {super.getCursors().map(entry => {
+                        if (entry.Data.length > 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 (
+                                <div
+                                    key={id}
+                                    style={{
+                                        position: "absolute",
+                                        transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
+                                        zIndex: 10000,
+                                        transformOrigin: 'center center',
+                                    }}
+                                >
+                                    <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
+                                        }}
+                                    >{email[0].toUpperCase()}</p>
+                                </div>
+                            );
+                        }
+                    })}
+                </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>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
new file mode 100644
index 000000000..6d9a79344
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -0,0 +1,8 @@
+
+.marqueeView {
+    border-style: dashed;
+    box-sizing: border-box;
+    position: absolute;
+    border-width: 1px;
+    border-color: black;
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
new file mode 100644
index 000000000..2692690dd
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -0,0 +1,175 @@
+import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { FieldWaiting, Opt } from "../../../../fields/Field";
+import { KeyStore } from "../../../../fields/KeyStore";
+import { Documents } from "../../../documents/Documents";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Transform } from "../../../util/Transform";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./MarqueeView.scss";
+import React = require("react");
+import { InkField, StrokeData } from "../../../../fields/InkField";
+import { Utils } from "../../../../Utils";
+import { InkingCanvas } from "../../InkingCanvas";
+
+interface MarqueeViewProps {
+    getMarqueeTransform: () => Transform;
+    getTransform: () => Transform;
+    container: CollectionFreeFormView;
+    addDocument: (doc: Document, allowDuplicates: false) => boolean;
+    activeDocuments: () => Document[];
+    selectDocuments: (docs: Document[]) => void;
+    removeDocument: (doc: Document) => boolean;
+}
+
+@observer
+export class MarqueeView extends React.Component<MarqueeViewProps>
+{
+    private _reactionDisposer: Opt<IReactionDisposer>;
+
+    @observable _lastX: number = 0;
+    @observable _lastY: number = 0;
+    @observable _downX: number = 0;
+    @observable _downY: number = 0;
+
+    componentDidMount() {
+        this._reactionDisposer = reaction(
+            () => this.props.container.MarqueeVisible,
+            (visible: boolean) => this.onPointerDown(visible, this.props.container.DownX, this.props.container.DownY))
+    }
+    componentWillUnmount() {
+        if (this._reactionDisposer) {
+            this._reactionDisposer();
+        }
+        this.cleanupInteractions();
+    }
+
+    @action
+    cleanupInteractions = () => {
+        document.removeEventListener("pointermove", this.onPointerMove, true)
+        document.removeEventListener("pointerup", this.onPointerUp, true);
+        document.removeEventListener("keydown", this.marqueeCommand, true);
+    }
+
+    @action
+    onPointerDown = (visible: boolean, downX: number, downY: number): void => {
+        if (visible) {
+            this._downX = this._lastX = downX;
+            this._downY = this._lastY = downY;
+            document.addEventListener("pointermove", this.onPointerMove, true)
+            document.addEventListener("pointerup", this.onPointerUp, true);
+            document.addEventListener("keydown", this.marqueeCommand, true);
+        }
+    }
+
+    @action
+    onPointerMove = (e: PointerEvent): void => {
+        this._lastX = e.pageX;
+        this._lastY = e.pageY;
+    }
+
+    @action
+    onPointerUp = (e: PointerEvent): void => {
+        this.cleanupInteractions();
+        if (!e.shiftKey) {
+            SelectionManager.DeselectAll();
+        }
+        this.props.selectDocuments(this.marqueeSelect());
+    }
+
+    intersectRect(r1: { left: number, top: number, width: number, height: number },
+        r2: { left: number, top: number, width: number, height: number }) {
+        return !(r2.left > r1.left + r1.width || r2.left + r2.width < r1.left || r2.top > r1.top + r1.height || r2.top + r2.height < r1.top);
+    }
+
+    get Bounds() {
+        let left = this._downX < this._lastX ? this._downX : this._lastX;
+        let top = this._downY < this._lastY ? this._downY : this._lastY;
+        let topLeft = this.props.getTransform().transformPoint(left, top);
+        let size = this.props.getTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+        return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) }
+    }
+
+    @action
+    marqueeCommand = (e: KeyboardEvent) => {
+        if (e.key == "Backspace" || e.key == "Delete") {
+            this.marqueeSelect().map(d => this.props.removeDocument(d));
+            this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField);
+            this.cleanupInteractions();
+        }
+        if (e.key == "c") {
+            let bounds = this.Bounds;
+            let selected = this.marqueeSelect().map(d => {
+                this.props.removeDocument(d);
+                d.SetNumber(KeyStore.X, d.GetNumber(KeyStore.X, 0) - bounds.left - bounds.width / 2);
+                d.SetNumber(KeyStore.Y, d.GetNumber(KeyStore.Y, 0) - bounds.top - bounds.height / 2);
+                d.SetNumber(KeyStore.Page, 0);
+                d.SetText(KeyStore.Title, "" + d.GetNumber(KeyStore.Width, 0) + " " + d.GetNumber(KeyStore.Height, 0));
+                return d;
+            });
+            let liftedInk = this.marqueeInkSelect(true);
+            this.props.container.props.Document.SetData(KeyStore.Ink, this.marqueeInkSelect(false), InkField);
+            //setTimeout(() => {
+            let newCollection = Documents.FreeformDocument(selected, {
+                x: bounds.left,
+                y: bounds.top,
+                panx: 0,
+                pany: 0,
+                width: bounds.width,
+                height: bounds.height,
+                backgroundColor: "Transparent",
+                ink: liftedInk,
+                title: "a nested collection"
+            });
+            this.props.addDocument(newCollection, false);
+            // }, 100);
+            this.cleanupInteractions();
+        }
+    }
+    marqueeInkSelect(select: boolean) {
+        let selRect = this.Bounds;
+        let centerShiftX = 0 - (selRect.left + selRect.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
+        let centerShiftY = 0 - (selRect.top + selRect.height / 2);
+        let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+        if (ink && ink != FieldWaiting && ink.Data) {
+            let idata = new Map();
+            ink.Data.forEach((value: StrokeData, key: string, map: any) => {
+                let inside = InkingCanvas.IntersectStrokeRect(value, selRect);
+                if (inside && select) {
+                    idata.set(key,
+                        {
+                            pathData: value.pathData.map(val => { return { x: val.x + centerShiftX, y: val.y + centerShiftY } }),
+                            color: value.color,
+                            width: value.width,
+                            tool: value.tool,
+                            page: -1
+                        });
+                } else if (!inside && !select) {
+                    idata.set(key, value);
+                }
+            })
+            return idata;
+        }
+    }
+
+    marqueeSelect() {
+        let selRect = this.Bounds;
+        let selection: Document[] = [];
+        this.props.activeDocuments().map(doc => {
+            var x = doc.GetNumber(KeyStore.X, 0);
+            var y = doc.GetNumber(KeyStore.Y, 0);
+            var w = doc.GetNumber(KeyStore.Width, 0);
+            var h = doc.GetNumber(KeyStore.Height, 0);
+            if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect))
+                selection.push(doc)
+        })
+        return selection;
+    }
+
+    render() {
+        let p = this.props.getMarqueeTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+        let v = this.props.getMarqueeTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+        return (!this.props.container.MarqueeVisible ? (null) : <div className="marqueeView" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
new file mode 100644
index 000000000..a797411f6
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
@@ -0,0 +1,18 @@
+
+.previewCursor {
+    color: black;
+    position: absolute;
+    transform-origin: left top;
+    pointer-events: none;
+}
+
+//this is an animation for the blinking cursor!
+@keyframes blink {
+	0% {opacity: 0}
+	49%{opacity: 0}
+	50% {opacity: 1}
+}
+
+#previewCursor {
+    animation: blink 1s infinite;
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
new file mode 100644
index 000000000..95364f04b
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
@@ -0,0 +1,76 @@
+import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { observer } from "mobx-react";
+import { Document } from "../../../../fields/Document";
+import { Opt } from "../../../../fields/Field";
+import { Documents } from "../../../documents/Documents";
+import { Transform } from "../../../util/Transform";
+import { CollectionFreeFormView } from "./CollectionFreeFormView";
+import "./PreviewCursor.scss";
+import React = require("react");
+
+
+export interface PreviewCursorProps {
+    getTransform: () => Transform;
+    container: CollectionFreeFormView;
+    addLiveTextDocument: (doc: Document) => void;
+}
+
+@observer
+export class PreviewCursor extends React.Component<PreviewCursorProps>  {
+    private _reactionDisposer: Opt<IReactionDisposer>;
+
+    @observable _lastX: number = 0;
+    @observable _lastY: number = 0;
+
+    componentDidMount() {
+        this._reactionDisposer = reaction(
+            () => this.props.container.PreviewCursorVisible,
+            (visible: boolean) => this.onCursorPlaced(visible, this.props.container.DownX, this.props.container.DownY))
+    }
+    componentWillUnmount() {
+        if (this._reactionDisposer) {
+            this._reactionDisposer();
+        }
+        this.cleanupInteractions();
+    }
+
+
+    @action
+    cleanupInteractions = () => {
+        document.removeEventListener("keypress", this.onKeyPress, false);
+    }
+
+    @action
+    onCursorPlaced = (visible: boolean, downX: number, downY: number): void => {
+        if (visible) {
+            document.addEventListener("keypress", this.onKeyPress, false);
+            this._lastX = downX;
+            this._lastY = downY;
+        } else
+            this.cleanupInteractions();
+    }
+
+    @action
+    onKeyPress = (e: KeyboardEvent) => {
+        // Mixing events between React and Native is finicky.  In FormattedTextBox, we set the
+        // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
+        // the keyPress here.
+        //if not these keys, make a textbox if preview cursor is active!
+        if (!e.ctrlKey && !e.altKey && !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+            //make textbox and add it to this collection
+            let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
+            let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "typed text" });
+            this.props.addLiveTextDocument(newBox);
+            e.stopPropagation();
+        }
+    }
+
+    render() {
+        //get local position and place cursor there!
+        let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
+        return (
+            !this.props.container.PreviewCursorVisible ? (null) :
+                <div className="previewCursor" id="previewCursor" style={{ transform: `translate(${x}px, ${y}px)` }}>I</div>)
+
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index f48be2047..b54744337 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -5,7 +5,7 @@ import { Key } from "../../../fields/Key";
 import { KeyStore } from "../../../fields/KeyStore";
 import { ListField } from "../../../fields/ListField";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { CollectionFreeFormView } from "../collections/CollectionFreeFormView";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { CollectionPDFView } from "../collections/CollectionPDFView";
 import { CollectionSchemaView } from "../collections/CollectionSchemaView";
 import { CollectionVideoView } from "../collections/CollectionVideoView";
-- 
cgit v1.2.3-70-g09d2


From 8448679e0a260ec881c54a10578bc464cd38f04e Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 1 Apr 2019 17:05:42 -0400
Subject: added dragging multiple items.

---
 src/client/northstar/dash-nodes/HistogramBox.tsx   |   4 +-
 src/client/util/DragManager.ts                     | 168 +++++++++++----------
 src/client/views/DocumentDecorations.scss          |   6 +
 src/client/views/DocumentDecorations.tsx           | 109 +++++++++----
 src/client/views/Main.tsx                          |   7 +-
 .../views/collections/CollectionDockingView.tsx    |  11 +-
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../views/collections/CollectionViewBase.tsx       |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  21 ++-
 src/client/views/nodes/DocumentView.tsx            |  15 +-
 10 files changed, 213 insertions(+), 134 deletions(-)

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

diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index a968def96..49ebe5ebc 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -54,7 +54,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
     @action
     dropX = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            let h = de.data.draggedDocument.GetT(KeyStore.Data, HistogramField);
+            let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
             if (h && h != FieldWaiting) {
                 this.HistoOp.X = h.Data.X;
             }
@@ -65,7 +65,7 @@ export class HistogramBox extends React.Component<FieldViewProps> {
     @action
     dropY = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            let h = de.data.draggedDocument.GetT(KeyStore.Data, HistogramField);
+            let h = de.data.draggedDocuments[0].GetT(KeyStore.Data, HistogramField);
             if (h && h != FieldWaiting) {
                 this.HistoOp.Y = h.Data.X;
             }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 70b1c9829..9ffe964ef 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -14,9 +14,9 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
 
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
-        var dragData = new DragManager.DocumentDragData(docFunc());
+        var dragData = new DragManager.DocumentDragData([docFunc()]);
         dragData.removeDocument = removeFunc;
-        DragManager.StartDocumentDrag(_reference.current!, dragData);
+        DragManager.StartDocumentDrag([_reference.current!], dragData);
     });
     let onRowUp = action((e: PointerEvent): void => {
         document.removeEventListener("pointermove", onRowMove);
@@ -27,7 +27,7 @@ export function setupDrag(_reference: React.RefObject<HTMLDivElement>, docFunc:
         if (e.button == 0) {
             e.stopPropagation();
             if (e.shiftKey) {
-                CollectionDockingView.Instance.StartOtherDrag(docFunc(), e);
+                CollectionDockingView.Instance.StartOtherDrag([docFunc()], e);
             } else {
                 document.addEventListener("pointermove", onRowMove);
                 document.addEventListener('pointerup', onRowUp);
@@ -101,20 +101,21 @@ export namespace DragManager {
     }
 
     export class DocumentDragData {
-        constructor(dragDoc: Document) {
-            this.draggedDocument = dragDoc;
-            this.droppedDocument = dragDoc;
+        constructor(dragDoc: Document[]) {
+            this.draggedDocuments = dragDoc;
+            this.droppedDocuments = dragDoc;
         }
-        draggedDocument: Document;
-        droppedDocument: Document;
+        draggedDocuments: Document[];
+        droppedDocuments: Document[];
         xOffset?: number;
         yOffset?: number;
         aliasOnDrop?: boolean;
         removeDocument?: (collectionDrop: CollectionView) => void;
         [id: string]: any;
     }
-    export function StartDocumentDrag(ele: HTMLElement, dragData: DocumentDragData, options?: DragOptions) {
-        StartDrag(ele, dragData, options, (dropData: { [id: string]: any }) => dropData.droppedDocument = dragData.aliasOnDrop ? dragData.draggedDocument.CreateAlias() : dragData.draggedDocument);
+
+    export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, options?: DragOptions) {
+        StartDrag(eles, dragData, options, (dropData: { [id: string]: any }) => dropData.droppedDocuments = dragData.aliasOnDrop ? dragData.draggedDocuments.map(d => d.CreateAlias()) : dragData.draggedDocuments);
     }
 
     export class LinkDragData {
@@ -125,49 +126,58 @@ export namespace DragManager {
         [id: string]: any;
     }
     export function StartLinkDrag(ele: HTMLElement, dragData: LinkDragData, options?: DragOptions) {
-        StartDrag(ele, dragData, options);
+        StartDrag([ele], dragData, options);
     }
-    function StartDrag(ele: HTMLElement, dragData: { [id: string]: any }, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
+    function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
         if (!dragDiv) {
             dragDiv = document.createElement("div");
             dragDiv.className = "dragManager-dragDiv"
             DragManager.Root().appendChild(dragDiv);
         }
-        const w = ele.offsetWidth, h = ele.offsetHeight;
-        const rect = ele.getBoundingClientRect();
-        const scaleX = rect.width / w, scaleY = rect.height / h;
-        let x = rect.left, y = rect.top;
-        // const offsetX = e.x - rect.left, offsetY = e.y - rect.top;
-
-        let dragElement = ele.cloneNode(true) as HTMLElement;
-        dragElement.style.opacity = "0.7";
-        dragElement.style.position = "absolute";
-        dragElement.style.bottom = "";
-        dragElement.style.left = "";
-        dragElement.style.transformOrigin = "0 0";
-        dragElement.style.zIndex = "1000";
-        dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
-        dragElement.style.width = `${rect.width / scaleX}px`;
-        dragElement.style.height = `${rect.height / scaleY}px`;
-
-        // bcz: PDFs don't show up if you clone them because they contain a canvas.
-        //      however, PDF's have a thumbnail field that contains an image of their canvas.
-        //      So we replace the pdf's canvas with the image thumbnail
-        const doc: Document = dragData["draggedDocument"];
-        if (doc) {
-            var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
-            let thumbnail = doc.GetT(KeyStore.Thumbnail, ImageField);
-            if (pdfBox && pdfBox.childElementCount && thumbnail) {
-                let img = new Image();
-                img!.src = thumbnail.toString();
-                img!.style.position = "absolute";
-                img!.style.width = `${rect.width / scaleX}px`;
-                img!.style.height = `${rect.height / scaleY}px`;
-                pdfBox.replaceChild(img!, pdfBox.children[0])
+
+        let scaleXs: number[] = [];
+        let scaleYs: number[] = [];
+        let xs: number[] = [];
+        let ys: number[] = [];
+
+        const docs: Document[] = dragData instanceof DocumentDragData ? dragData.draggedDocuments : [];
+        let dragElements = eles.map(ele => {
+            const w = ele.offsetWidth, h = ele.offsetHeight;
+            const rect = ele.getBoundingClientRect();
+            const scaleX = rect.width / w, scaleY = rect.height / h;
+            let x = rect.left, y = rect.top;
+            xs.push(x); ys.push(y);
+            scaleXs.push(scaleX); scaleYs.push(scaleY);
+            let dragElement = ele.cloneNode(true) as HTMLElement;
+            dragElement.style.opacity = "0.7";
+            dragElement.style.position = "absolute";
+            dragElement.style.bottom = "";
+            dragElement.style.left = "";
+            dragElement.style.transformOrigin = "0 0";
+            dragElement.style.zIndex = "1000";
+            dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+            dragElement.style.width = `${rect.width / scaleX}px`;
+            dragElement.style.height = `${rect.height / scaleY}px`;
+
+            // bcz: PDFs don't show up if you clone them because they contain a canvas.
+            //      however, PDF's have a thumbnail field that contains an image of their canvas.
+            //      So we replace the pdf's canvas with the image thumbnail
+            if (docs.length) {
+                var pdfBox = dragElement.getElementsByClassName("pdfBox-cont")[0] as HTMLElement;
+                let thumbnail = docs[0].GetT(KeyStore.Thumbnail, ImageField);
+                if (pdfBox && pdfBox.childElementCount && thumbnail) {
+                    let img = new Image();
+                    img!.src = thumbnail.toString();
+                    img!.style.position = "absolute";
+                    img!.style.width = `${rect.width / scaleX}px`;
+                    img!.style.height = `${rect.height / scaleY}px`;
+                    pdfBox.replaceChild(img!, pdfBox.children[0])
+                }
             }
-        }
 
-        dragDiv.appendChild(dragElement);
+            dragDiv.appendChild(dragElement);
+            return dragElement;
+        });
 
         let hideSource = false;
         if (options) {
@@ -177,62 +187,64 @@ export namespace DragManager {
                 hideSource = options.hideSource();
             }
         }
-        const wasHidden = ele.hidden;
-        if (hideSource) {
-            ele.hidden = true;
-        }
+        eles.map(ele => ele.hidden = hideSource);
+
         const moveHandler = (e: PointerEvent) => {
             e.stopPropagation();
             e.preventDefault();
-            x += e.movementX;
-            y += e.movementY;
             if (dragData instanceof DocumentDragData)
                 dragData.aliasOnDrop = e.ctrlKey || e.altKey;
             if (e.shiftKey) {
                 abortDrag();
-                CollectionDockingView.Instance.StartOtherDrag(doc, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
+                CollectionDockingView.Instance.StartOtherDrag(docs, { pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 });
             }
-            dragElement.style.transform = `translate(${x}px, ${y}px) scale(${scaleX}, ${scaleY})`;
+            dragElements.map((dragElement, i) => dragElement.style.transform = `translate(${xs[i] += e.movementX}px, ${ys[i] += e.movementY}px) scale(${scaleXs[i]}, ${scaleYs[i]})`);
         };
 
         const abortDrag = () => {
             document.removeEventListener("pointermove", moveHandler, true);
             document.removeEventListener("pointerup", upHandler);
-            dragDiv.removeChild(dragElement);
-            ele.hidden = false;
+            dragElements.map(dragElement => dragDiv.removeChild(dragElement));
+            eles.map(ele => ele.hidden = false);
         }
         const upHandler = (e: PointerEvent) => {
             abortDrag();
-            FinishDrag(ele, e, dragData, options, finishDrag);
+            FinishDrag(eles, e, dragData, options, finishDrag);
         };
         document.addEventListener("pointermove", moveHandler, true);
         document.addEventListener("pointerup", upHandler);
     }
 
-    function FinishDrag(dragEle: HTMLElement, e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
-        let parent = dragEle.parentElement;
-        if (parent)
-            parent.removeChild(dragEle);
+    function FinishDrag(dragEles: HTMLElement[], e: PointerEvent, dragData: { [index: string]: any }, options?: DragOptions, finishDrag?: (dragData: { [index: string]: any }) => void) {
+        let removed = dragEles.map(dragEle => {
+            let parent = dragEle.parentElement;
+            if (parent)
+                parent.removeChild(dragEle);
+            return [dragEle, parent];
+        });
         const target = document.elementFromPoint(e.x, e.y);
-        if (parent)
-            parent.appendChild(dragEle);
-        if (!target) {
-            return;
-        }
-        if (finishDrag)
-            finishDrag(dragData);
-
-        target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
-            bubbles: true,
-            detail: {
-                x: e.x,
-                y: e.y,
-                data: dragData
+        removed.map(r => {
+            let dragEle: HTMLElement = r[0]!;
+            let parent: HTMLElement | null = r[1];
+            if (parent)
+                parent.appendChild(dragEle);
+        });
+        if (target) {
+            if (finishDrag)
+                finishDrag(dragData);
+
+            target.dispatchEvent(new CustomEvent<DropEvent>("dashOnDrop", {
+                bubbles: true,
+                detail: {
+                    x: e.x,
+                    y: e.y,
+                    data: dragData
+                }
+            }));
+
+            if (options) {
+                options.handlers.dragComplete({});
             }
-        }));
-
-        if (options) {
-            options.handlers.dragComplete({});
         }
         DocumentDecorations.Instance.Hidden = false;
     }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 11595aa01..7a43f3087 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -32,6 +32,12 @@
     }
 }
 
+.documentDecorations-background {
+    background:lightblue;
+    position: absolute;
+    opacity: 0.1;
+}
+
 //     position: absolute;
 //     display: grid;
 //     z-index: 1000;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 47098c3b5..faba3b6ea 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,14 +1,14 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "../util/SelectionManager";
+import { action, computed, observable, trace } from "mobx";
 import { observer } from "mobx-react";
-import './DocumentDecorations.scss'
-import { KeyStore } from '../../fields/KeyStore'
+import { KeyStore } from '../../fields/KeyStore';
+import { ListField } from "../../fields/ListField";
 import { NumberField } from "../../fields/NumberField";
-import { props } from "bluebird";
 import { DragManager } from "../util/DragManager";
+import { SelectionManager } from "../util/SelectionManager";
+import { CollectionView } from "./collections/CollectionView";
+import './DocumentDecorations.scss';
 import { LinkMenu } from "./nodes/LinkMenu";
-import { ListField } from "../../fields/ListField";
+import React = require("react");
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -22,6 +22,7 @@ export class DocumentDecorations extends React.Component {
     private _resizeBorderWidth = 16;
     private _linkButton = React.createRef<HTMLDivElement>();
     @observable private _hidden = false;
+    @observable private _dragging = false;
 
     constructor(props: Readonly<{}>) {
         super(props)
@@ -50,6 +51,50 @@ export class DocumentDecorations extends React.Component {
     public get Hidden() { return this._hidden; }
     public set Hidden(value: boolean) { this._hidden = value; }
 
+    _lastDrag: number[] = [0, 0];
+    onBackgroundDown = (e: React.PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onBackgroundMove);
+        document.addEventListener("pointermove", this.onBackgroundMove);
+        document.removeEventListener("pointerup", this.onBackgroundUp);
+        document.addEventListener("pointerup", this.onBackgroundUp);
+        this._lastDrag = [e.clientX, e.clientY]
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
+    @action
+    onBackgroundMove = (e: PointerEvent): void => {
+        let dragDocView = SelectionManager.SelectedDocuments()[0];
+        const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+        let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
+        dragData.aliasOnDrop = false;
+        dragData.xOffset = e.x - left;
+        dragData.yOffset = e.y - top;
+        dragData.removeDocument = (dropCollectionView: CollectionView) =>
+            dragData.draggedDocuments.map(d => {
+                if (dragDocView.props.RemoveDocument && dragDocView.props.ContainingCollectionView !== dropCollectionView) {
+                    dragDocView.props.RemoveDocument(d);
+                }
+            });
+        this._dragging = true;
+        document.removeEventListener("pointermove", this.onBackgroundMove);
+        document.removeEventListener("pointerup", this.onBackgroundUp);
+        DragManager.StartDocumentDrag(SelectionManager.SelectedDocuments().map(docView => (docView as any)._mainCont!.current!), dragData, {
+            handlers: {
+                dragComplete: action(() => this._dragging = false),
+            },
+            hideSource: true
+        })
+        e.stopPropagation();
+    }
+
+    onBackgroundUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onBackgroundMove);
+        document.removeEventListener("pointerup", this.onBackgroundUp);
+        e.stopPropagation();
+        e.preventDefault();
+    }
+
     onPointerDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
         if (e.button === 0) {
@@ -191,7 +236,6 @@ export class DocumentDecorations extends React.Component {
     // buttonOnPointerUp = (e: React.PointerEvent): void => {
     //     e.stopPropagation();
     // }
-
     render() {
         var bounds = this.Bounds;
         if (this.Hidden) {
@@ -218,25 +262,36 @@ export class DocumentDecorations extends React.Component {
             </Flyout>);
         }
         return (
-            <div id="documentDecorations-container" style={{
-                width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
-                height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px",
-                left: bounds.x - this._resizeBorderWidth / 2,
-                top: bounds.y - this._resizeBorderWidth / 2,
-            }}>
-                <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-centerCont"></div>
-                <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-                <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
-
-                <div title="View Links" className="linkFlyout" ref={this._linkButton}>{linkButton}</div>
-
-            </div >
+            <div className="documentDecorations">
+                <div className="documentDecorations-background" style={{
+                    width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+                    height: (bounds.b - bounds.y + this._resizeBorderWidth) + "px",
+                    left: bounds.x - this._resizeBorderWidth / 2,
+                    top: bounds.y - this._resizeBorderWidth / 2,
+                    pointerEvents: this._dragging ? "none" : "all",
+                    zIndex: SelectionManager.SelectedDocuments().length > 1 ? 1000 : 0,
+                }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation() }} >
+                </div>
+                <div id="documentDecorations-container" style={{
+                    width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
+                    height: (bounds.b - bounds.y + this._resizeBorderWidth + 30) + "px",
+                    left: bounds.x - this._resizeBorderWidth / 2,
+                    top: bounds.y - this._resizeBorderWidth / 2,
+                }}>
+                    <div id="documentDecorations-topLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-topResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-topRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-leftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-centerCont"></div>
+                    <div id="documentDecorations-rightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomLeftResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+                    <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
+
+                    <div title="View Links" className="linkFlyout" ref={this._linkButton}>{linkButton}</div>
+
+                </div >
+            </div>
         )
     }
 }
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 75855c3d1..6f66f8f38 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -218,7 +218,8 @@ export class Main extends React.Component {
     focusDocument = (doc: Document) => { }
     noScaling = () => 1;
 
-    get content() {
+    @computed
+    get mainContent() {
         return !this.mainContainer ? (null) :
             <DocumentView Document={this.mainContainer}
                 AddDocument={undefined}
@@ -311,17 +312,17 @@ export class Main extends React.Component {
         }
         return (
             <div id="main-div">
+                <DocumentDecorations />
                 <Measure onResize={(r: any) => runInAction(() => {
                     this.pwidth = r.entry.width;
                     this.pheight = r.entry.height;
                 })}>
                     {({ measureRef }) =>
                         <div ref={measureRef} id="mainContent-div">
-                            {this.content}
+                            {this.mainContent}
                         </div>
                     }
                 </Measure>
-                <DocumentDecorations />
                 <ContextMenu />
                 {this.nodesMenu}
                 {this.miscButtons}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index f123149dd..c6cbc05e7 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -47,9 +47,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
     }
-    public StartOtherDrag(dragDoc: Document, e: any) {
-        this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
-            onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 })
+    public StartOtherDrag(dragDocs: Document[], e: any) {
+        dragDocs.map(dragDoc =>
+            this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+                onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: () => { }, button: 0 }));
     }
 
     @action
@@ -199,7 +200,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             let tab = (e.target as any).parentElement as HTMLElement;
             Server.GetField(docid, action((f: Opt<Field>) => {
                 if (f instanceof Document)
-                    DragManager.StartDocumentDrag(tab, new DragManager.DocumentDragData(f as Document),
+                    DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f as Document]),
                         {
                             handlers: {
                                 dragComplete: action(() => { }),
@@ -265,6 +266,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
 
     render() {
+        trace();
         return (
             <div className="collectiondockingview-container" id="menuContainer"
                 onPointerDown={this.onPointerDown} onPointerUp={this.onPointerUp} ref={this._containerRef}
@@ -305,6 +307,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     render() {
+        trace();
         if (!this._document)
             return (null);
         var content =
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a1498e0ae..014aa1d8f 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -113,7 +113,7 @@ export class CollectionView extends React.Component<CollectionViewProps> {
         if (index !== -1) {
             value.splice(index, 1)
 
-            SelectionManager.DeselectAll()
+            //SelectionManager.DeselectAll()
             ContextMenu.Instance.clearItems()
             return true;
         }
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 987f3cb6c..316d20c9d 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -82,9 +82,9 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         if (de.data instanceof DragManager.DocumentDragData) {
             if (de.data.aliasOnDrop) {
                 [KeyStore.Width, KeyStore.Height, KeyStore.CurPage].map(key =>
-                    de.data.draggedDocument.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null));
+                    de.data.draggedDocuments.GetTAsync(key, NumberField, (f: Opt<NumberField>) => f ? de.data.droppedDocument.SetNumber(key, f.Data) : null));
             }
-            let added = this.props.addDocument(de.data.droppedDocument, false);
+            let added = de.data.droppedDocuments.reduce((added, d) => this.props.addDocument(d, false), true);
             if (added && de.data.removeDocument && !de.data.aliasOnDrop) {
                 de.data.removeDocument(this.props.CollectionView);
             }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 60fb95ff5..c5178f69d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -77,13 +77,20 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 let screenX = de.x - (de.data.xOffset as number || 0);
                 let screenY = de.y - (de.data.yOffset as number || 0);
                 const [x, y] = this.getTransform().transformPoint(screenX, screenY);
-                de.data.droppedDocument.SetNumber(KeyStore.X, x);
-                de.data.droppedDocument.SetNumber(KeyStore.Y, y);
-                if (!de.data.droppedDocument.GetNumber(KeyStore.Width, 0)) {
-                    de.data.droppedDocument.SetNumber(KeyStore.Width, 300);
-                    de.data.droppedDocument.SetNumber(KeyStore.Height, 300);
-                }
-                this.bringToFront(de.data.droppedDocument);
+                let dragDoc = de.data.draggedDocuments[0];
+                let dragX = dragDoc.GetNumber(KeyStore.X, 0);
+                let dragY = dragDoc.GetNumber(KeyStore.Y, 0);
+                de.data.draggedDocuments.map(d => {
+                    let docX = d.GetNumber(KeyStore.X, 0);
+                    let docY = d.GetNumber(KeyStore.Y, 0);
+                    d.SetNumber(KeyStore.X, x + (docX - dragX));
+                    d.SetNumber(KeyStore.Y, y + (docY - dragY));
+                    if (!d.GetNumber(KeyStore.Width, 0)) {
+                        d.SetNumber(KeyStore.Width, 300);
+                        d.SetNumber(KeyStore.Height, 300);
+                    }
+                    this.bringToFront(d);
+                })
             }
             return true;
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 71613ca4f..1195128dc 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -106,7 +106,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
             if (this.props.isTopMost) {
                 this.startDragging(e.pageX, e.pageY, e.altKey || e.ctrlKey);
             }
-            else CollectionDockingView.Instance.StartOtherDrag(this.props.Document, e);
+            else CollectionDockingView.Instance.StartOtherDrag([this.props.Document], e);
             e.stopPropagation();
         } else {
             if (this.active && !e.isDefaultPrevented()) {
@@ -125,9 +125,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         if (this._mainCont.current) {
             this.dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
         }
-        runInAction(() => {
-            DocumentManager.Instance.DocumentViews.push(this);
-        })
+        runInAction(() => DocumentManager.Instance.DocumentViews.push(this))
         this._reactionDisposer = reaction(
             () => this.props.ContainingCollectionView && this.props.ContainingCollectionView.SelectedDocs.slice(),
             () => {
@@ -149,10 +147,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         if (this.dropDisposer) {
             this.dropDisposer();
         }
-        runInAction(() => {
-            DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
-
-        })
+        runInAction(() => DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1))
         if (this._reactionDisposer) {
             this._reactionDisposer();
         }
@@ -161,7 +156,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     startDragging(x: number, y: number, dropAliasOfDraggedDoc: boolean) {
         if (this._mainCont.current) {
             const [left, top] = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-            let dragData = new DragManager.DocumentDragData(this.props.Document);
+            let dragData = new DragManager.DocumentDragData([this.props.Document]);
             dragData.aliasOnDrop = dropAliasOfDraggedDoc;
             dragData.xOffset = x - left;
             dragData.yOffset = y - top;
@@ -170,7 +165,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                     this.props.RemoveDocument(this.props.Document);
                 }
             }
-            DragManager.StartDocumentDrag(this._mainCont.current, dragData, {
+            DragManager.StartDocumentDrag([this._mainCont.current], dragData, {
                 handlers: {
                     dragComplete: action(() => { }),
                 },
-- 
cgit v1.2.3-70-g09d2

