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

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 416 +++++++++++++++++++++
 1 file changed, 416 insertions(+)
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx

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

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
-- 
cgit v1.2.3-70-g09d2


From eb220da697e2383b1e368dee743613158994746e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 31 Mar 2019 17:58:11 -0400
Subject: restructured marquee and preview cursor to not need reactions. fixed
 some inking bugs.

---
 src/client/views/InkingCanvas.tsx                  |  25 ++-
 .../CollectionFreeFormLinksView.tsx                |   1 +
 .../collectionFreeForm/CollectionFreeFormView.scss |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 199 ++++++++-------------
 .../collectionFreeForm/MarqueeView.scss            |   6 +
 .../collections/collectionFreeForm/MarqueeView.tsx | 169 +++++++++--------
 .../collectionFreeForm/PreviewCursor.scss          |   7 +-
 .../collectionFreeForm/PreviewCursor.tsx           |  71 +++++---
 8 files changed, 245 insertions(+), 234 deletions(-)

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

diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 36a8834a0..8fb74ec48 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -1,16 +1,15 @@
-import { observer } from "mobx-react";
-import { observable } from "mobx";
 import { action, computed } from "mobx";
-import { InkingControl } from "./InkingControl";
-import React = require("react");
-import { Transform } from "../util/Transform";
+import { observer } from "mobx-react";
 import { Document } from "../../fields/Document";
-import { KeyStore } from "../../fields/KeyStore";
+import { FieldWaiting } from "../../fields/Field";
 import { InkField, InkTool, StrokeData, StrokeMap } from "../../fields/InkField";
-import { InkingStroke } from "./InkingStroke";
-import "./InkingCanvas.scss"
+import { KeyStore } from "../../fields/KeyStore";
 import { Utils } from "../../Utils";
-import { FieldWaiting } from "../../fields/Field";
+import { Transform } from "../util/Transform";
+import "./InkingCanvas.scss";
+import { InkingControl } from "./InkingControl";
+import { InkingStroke } from "./InkingStroke";
+import React = require("react");
 
 interface InkCanvasProps {
     getScreenTransform: () => Transform;
@@ -71,8 +70,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
 
         // start the new line, saves a uuid to represent the field of the stroke
         this._idGenerator = Utils.GenerateGuid();
-        let data = this.inkData;
-        data.set(this._idGenerator,
+        this.inkData.set(this._idGenerator,
             {
                 pathData: [point],
                 color: InkingControl.Instance.selectedColor,
@@ -80,7 +78,6 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
                 tool: InkingControl.Instance.selectedTool,
                 page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
             });
-        this.inkData = data;
         this._isDrawing = true;
     }
 
@@ -163,9 +160,7 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
         })
 
         return (
-
-            <div className="inking-canvas" style={canvasStyle}
-                onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} >
+            <div className="inking-canvas" style={canvasStyle} onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} >
                 <svg>
                     {paths}
                 </svg>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index f793b3a49..ea392eaea 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -96,6 +96,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         return (
             <svg className="collectionfreeformlinksview-svgCanvas">
                 {this.uniqueConnections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />)}
+                {this.props.children}
             </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
index 215a75243..c979b27a9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -6,6 +6,7 @@
   left: 0;
   width: 100%;
   height: 100%;
+  transform-origin: left top;
   .inking-canvas {
     transform-origin: 50000px 50000px;
   }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 1f90b0d46..cdc5bdfb0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -3,7 +3,6 @@ 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";
@@ -14,10 +13,9 @@ 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 { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import v5 = require("uuid/v5");
 
@@ -27,12 +25,15 @@ export class CollectionFreeFormView extends CollectionViewBase {
     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
+        // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself and receive text input
         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;
+        this.addDocument(newBox, false);
+    }
+
+    public addDocument = (newBox: Document, allowDuplicates: boolean) => {
+        let added = this.props.addDocument(newBox, false);
+        this.bringToFront(newBox);
+        return added;
     }
 
     public selectDocuments = (docs: Document[]) => {
@@ -42,23 +43,15 @@ export class CollectionFreeFormView extends CollectionViewBase {
 
     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;
+        return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).reduce((active, doc) => {
+            var page = doc.GetNumber(KeyStore.Page, -1);
+            if (page == curPage || page == -1) {
+                active.push(doc);
+            }
+            return active;
+        }, [] as Document[]);
     }
 
-    //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;
@@ -100,12 +93,10 @@ export class CollectionFreeFormView extends CollectionViewBase {
     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);
@@ -120,28 +111,13 @@ export class CollectionFreeFormView extends CollectionViewBase {
     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) {
+            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);
@@ -264,16 +240,12 @@ export class CollectionFreeFormView extends CollectionViewBase {
     @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;
+        return this.props.Document.GetList(this.props.fieldKey, [] as Document[]).filter(doc => doc).reduce((prev, doc) => {
+            var page = doc.GetNumber(KeyStore.Page, -1);
+            if (page == curPage || page == -1)
+                prev.push(<CollectionFreeFormDocumentView key={doc.Id} {...this.getDocumentViewProps(doc)} />);
+            return prev;
+        }, [] as JSX.Element[])
     }
 
     @computed
@@ -290,15 +262,10 @@ export class CollectionFreeFormView extends CollectionViewBase {
     }
 
     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)
+    getContainerTransform = (): 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) => {
@@ -345,71 +312,63 @@ export class CollectionFreeFormView extends CollectionViewBase {
 
         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
+                onPointerDown={this.onPointerDown} onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
+                onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onWheel={this.onPointerWheel}
+                style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} tabIndex={0} ref={this.createDropTarget}>
+                <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
+                    addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
+                    getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
+                    <div className="collectionfreeformview" ref={this._canvasRef}
+                        style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}>
+                        {this.backgroundView}
+                        <CollectionFreeFormLinksView {...this.props} />
+                        {this.views}
+                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
+                        {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={{
-                                            fontSize: 14,
-                                            color: "black",
-                                            // fontStyle: "italic",
-                                            marginLeft: -12,
-                                            marginTop: 4
+                                            position: "absolute",
+                                            transform: `translate(${point[0] - 10}px, ${point[1] - 10}px)`,
+                                            zIndex: 10000,
+                                            transformOrigin: 'center center',
                                         }}
-                                    >{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}
-
+                                    >
+                                        <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>
+                    {this.overlayView}
+                </MarqueeView>
             </div>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 6d9a79344..1ee3b244b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,8 +1,14 @@
 
 .marqueeView {
+    position: absolute;
+    width:100%;
+    height:100%;
+}
+.marquee {
     border-style: dashed;
     box-sizing: border-box;
     position: absolute;
     border-width: 1px;
     border-color: black;
+    pointer-events: none;
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 2692690dd..2b0e7d228 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,62 +1,58 @@
-import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../../fields/Document";
-import { FieldWaiting, Opt } from "../../../../fields/Field";
+import { FieldWaiting } from "../../../../fields/Field";
+import { InkField, StrokeData } from "../../../../fields/InkField";
 import { KeyStore } from "../../../../fields/KeyStore";
 import { Documents } from "../../../documents/Documents";
 import { SelectionManager } from "../../../util/SelectionManager";
 import { Transform } from "../../../util/Transform";
+import { InkingCanvas } from "../../InkingCanvas";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
+import { PreviewCursor } from "./PreviewCursor";
 import React = require("react");
-import { InkField, StrokeData } from "../../../../fields/InkField";
-import { Utils } from "../../../../Utils";
-import { InkingCanvas } from "../../InkingCanvas";
 
 interface MarqueeViewProps {
-    getMarqueeTransform: () => Transform;
+    getContainerTransform: () => Transform;
     getTransform: () => Transform;
     container: CollectionFreeFormView;
     addDocument: (doc: Document, allowDuplicates: false) => boolean;
     activeDocuments: () => Document[];
     selectDocuments: (docs: Document[]) => void;
     removeDocument: (doc: Document) => boolean;
+    addLiveTextDocument: (doc: Document) => void;
 }
 
 @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();
-    }
+    @observable _used: boolean = false;
+    @observable _visible: boolean = false;
+    static DRAG_THRESHOLD = 4;
 
     @action
-    cleanupInteractions = () => {
-        document.removeEventListener("pointermove", this.onPointerMove, true)
-        document.removeEventListener("pointerup", this.onPointerUp, true);
+    cleanupInteractions = (all: boolean = false) => {
+        if (all) {
+            document.removeEventListener("pointermove", this.onPointerMove, true)
+            document.removeEventListener("pointerup", this.onPointerUp, true);
+        } else {
+            this._used = true;
+        }
         document.removeEventListener("keydown", this.marqueeCommand, true);
+        this._visible = false;
     }
 
     @action
-    onPointerDown = (visible: boolean, downX: number, downY: number): void => {
-        if (visible) {
-            this._downX = this._lastX = downX;
-            this._downY = this._lastY = downY;
+    onPointerDown = (e: React.PointerEvent): void => {
+        if (e.buttons == 1 && !e.altKey && !e.metaKey && this.props.container.props.active()) {
+            this._downX = this._lastX = e.pageX;
+            this._downY = this._lastY = e.pageY;
+            this._used = false;
             document.addEventListener("pointermove", this.onPointerMove, true)
             document.addEventListener("pointerup", this.onPointerUp, true);
             document.addEventListener("keydown", this.marqueeCommand, true);
@@ -67,11 +63,20 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     onPointerMove = (e: PointerEvent): void => {
         this._lastX = e.pageX;
         this._lastY = e.pageY;
+        if (!e.cancelBubble) {
+            if (!this._used && e.buttons == 1 && !e.altKey && !e.metaKey &&
+                (Math.abs(this._lastX - this._downX) > MarqueeView.DRAG_THRESHOLD || Math.abs(this._lastY - this._downY) > MarqueeView.DRAG_THRESHOLD)) {
+                this._visible = true;
+            }
+            e.stopPropagation();
+            e.preventDefault();
+        }
     }
 
     @action
     onPointerUp = (e: PointerEvent): void => {
-        this.cleanupInteractions();
+        this.cleanupInteractions(true);
+        this._visible = false;
         if (!e.shiftKey) {
             SelectionManager.DeselectAll();
         }
@@ -83,6 +88,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         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);
     }
 
+    @computed
     get Bounds() {
         let left = this._downX < this._lastX ? this._downX : this._lastX;
         let top = this._downY < this._lastY ? this._downY : this._lastY;
@@ -95,7 +101,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     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);
+            let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+            if (ink && ink != FieldWaiting && ink.Data) {
+                this.marqueeInkDelete(ink.Data);
+            }
             this.cleanupInteractions();
         }
         if (e.key == "c") {
@@ -104,53 +113,61 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 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.SetNumber(KeyStore.Page, -1);
                 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);
+            let ink = this.props.container.props.Document.GetT(KeyStore.Ink, InkField);
+            if (ink && ink != FieldWaiting && ink.Data) {
+                //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: this.marqueeInkSelect(ink.Data),
+                    title: "a nested collection"
+                });
+                this.props.addDocument(newCollection, false);
+                this.marqueeInkDelete(ink.Data);
+            }
             // }, 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;
-        }
+    @action
+    marqueeInkSelect(ink: Map<any, any>) {
+        let idata = new Map();
+        let centerShiftX = 0 - (this.Bounds.left + this.Bounds.width / 2); // moves each point by the offset that shifts the selection's center to the origin.
+        let centerShiftY = 0 - (this.Bounds.top + this.Bounds.height / 2);
+        ink.forEach((value: StrokeData, key: string, map: any) => {
+            if (InkingCanvas.IntersectStrokeRect(value, this.Bounds)) {
+                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
+                    });
+            }
+        });
+        return idata;
+    }
+
+    @action
+    marqueeInkDelete(ink: Map<any, any>, ) {
+        // bcz: this appears to work but when you restart all the deleted strokes come back -- InkField isn't observing its changes so they aren't written to the DB.
+        // ink.forEach((value: StrokeData, key: string, map: any) =>
+        //     InkingCanvas.IntersectStrokeRect(value, this.Bounds) && ink.delete(key));
+
+        let idata = new Map();
+        ink.forEach((value: StrokeData, key: string, map: any) =>
+            !InkingCanvas.IntersectStrokeRect(value, this.Bounds) && idata.set(key, value));
+        this.props.container.props.Document.SetDataOnPrototype(KeyStore.Ink, idata, InkField);
     }
 
     marqueeSelect() {
@@ -168,8 +185,16 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     }
 
     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])}` }} />);
+        let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
+        let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
+        return <div className="marqueeView" onPointerDown={this.onPointerDown}>
+            <PreviewCursor container={this.props.container} addLiveTextDocument={this.props.addLiveTextDocument}
+                getContainerTransform={this.props.getContainerTransform} getTransform={this.props.getTransform} >
+                {this.props.children}
+                {!this._visible ? (null) :
+                    <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />}
+
+            </PreviewCursor>
+        </div>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
index a797411f6..7d6d5aaab 100644
--- a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
@@ -5,6 +5,11 @@
     transform-origin: left top;
     pointer-events: none;
 }
+.previewCursorView {
+    position: absolute;
+    width:100%;
+    height:100%;
+}
 
 //this is an animation for the blinking cursor!
 @keyframes blink {
@@ -14,5 +19,5 @@
 }
 
 #previewCursor {
-    animation: blink 1s infinite;
+     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
index 95364f04b..339e056a8 100644
--- a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
@@ -1,7 +1,6 @@
-import { action, IReactionDisposer, observable, reaction } from "mobx";
+import { action, observable } 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";
@@ -11,43 +10,50 @@ import React = require("react");
 
 export interface PreviewCursorProps {
     getTransform: () => Transform;
+    getContainerTransform: () => 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;
+    @observable public _visible: boolean = false;
+    @observable public DownX: number = 0;
+    @observable public DownY: number = 0;
+    _showOnUp: boolean = false;
+    public _previewDivRef = React.createRef<HTMLDivElement>();
 
-    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("pointerup", this.onPointerUp, true);
     }
 
-
     @action
-    cleanupInteractions = () => {
+    onPointerDown = (e: React.PointerEvent) => {
+        this._visible = false;
         document.removeEventListener("keypress", this.onKeyPress, false);
+        this._showOnUp = true;
+        this._lastX = this.DownX = e.pageX;
+        this._lastY = this.DownY = e.pageY;
+        document.addEventListener("pointerup", this.onPointerUp, true);
+        document.addEventListener("pointermove", this.onPointerMove, true);
+    }
+    @action
+    onPointerMove = (e: PointerEvent): void => {
+        if (Math.abs(this.DownX - e.clientX) > 4 || Math.abs(this.DownY - e.clientY) > 4) {
+            this._showOnUp = false;
+        }
     }
 
     @action
-    onCursorPlaced = (visible: boolean, downX: number, downY: number): void => {
-        if (visible) {
+    onPointerUp = (e: PointerEvent): void => {
+        if (this._showOnUp) {
             document.addEventListener("keypress", this.onKeyPress, false);
-            this._lastX = downX;
-            this._lastY = downY;
-        } else
-            this.cleanupInteractions();
+            this._visible = true;
+        }
+        this.cleanupInteractions();
     }
 
     @action
@@ -61,16 +67,29 @@ export class PreviewCursor extends React.Component<PreviewCursorProps>  {
             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);
+            document.removeEventListener("keypress", this.onKeyPress, false);
+            this._visible = false;
             e.stopPropagation();
         }
     }
+    //when focus is lost, this will remove the preview cursor
+    @action
+    onBlur = (): void => {
+        this._visible = false;
+        document.removeEventListener("keypress", this.onKeyPress, false);
+    }
 
     render() {
         //get local position and place cursor there!
-        let [x, y] = this.props.getTransform().transformPoint(this._lastX, this._lastY);
+        let p = this.props.getContainerTransform().transformPoint(this._lastX, this._lastY);
+        if (this._visible && this._previewDivRef.current)
+            this._previewDivRef.current!.focus();
         return (
-            !this.props.container.PreviewCursorVisible ? (null) :
-                <div className="previewCursor" id="previewCursor" style={{ transform: `translate(${x}px, ${y}px)` }}>I</div>)
-
+            <div className="previewCursorView" tabIndex={0} ref={this._previewDivRef} onBlur={this.onBlur} onPointerDown={this.onPointerDown}>
+                {this.props.children}
+                {!this._visible ? (null) :
+                    <div className="previewCursor" id="previewCursor" style={{ transform: `translate(${p[0]}px, ${p[1]}px)` }}>I</div>}
+            </div>
+        )
     }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 2ac86b53d28abb8be2f3abd501b801e543973e28 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 31 Mar 2019 22:45:43 -0400
Subject: restructured inkingCanvas and linksViews

---
 src/client/views/InkingCanvas.scss                 | 164 +++------------------
 src/client/views/InkingCanvas.tsx                  | 121 +++++++--------
 src/client/views/InkingControl.scss                | 135 +++++++++++++++++
 src/client/views/InkingControl.tsx                 |   3 +-
 .../CollectionFreeFormLinksView.scss               |   3 +
 .../CollectionFreeFormLinksView.tsx                |   9 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  10 --
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  10 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   6 +-
 src/client/views/nodes/PDFBox.tsx                  |  10 +-
 10 files changed, 233 insertions(+), 238 deletions(-)
 create mode 100644 src/client/views/InkingControl.scss

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

diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index e79b146b9..6d7821cd2 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -1,149 +1,25 @@
 @import "global_variables";
-.inking-canvas {
+
+.inkingCanvas-paths, .inkingCanvas-paths-none {
     position: absolute;
     top: -50000px;
-    left: -50000px; // z-index: 99; //overlays ink on top of everything
-    svg {
-        position:absolute;
-        width: 100000px;
-        height: 100000px;
-        .highlight {
-            mix-blend-mode: multiply;
-        }
-    }
+    left: -50000px;
+    width: 100000px;
+    height: 100000px;
+    //z-index: 10000; // z-index: 99; //overlays ink on top of everything but that messes up blending and events don't propagate down to nested collections
+    .highlight {
+        mix-blend-mode: multiply;
+    }
+    .inkingCanvas-children {
+      transform: translate(50000px, 50000px);
+      pointer-events: none;
+    }
+    cursor:"crosshair";
+    pointer-events: auto;
+    
+}
+.inkingCanvas-paths-none {
+    pointer-events: none;
+    cursor: "arrow";
 }
 
-.inking-control {
-    position: absolute;
-    left: 70px;
-    bottom: 70px;
-    margin: 0;
-    padding: 0;
-    display: flex;
-    label,
-    input,
-    option {
-        font-size: 12px;
-    }
-    input[type="range"] {
-        -webkit-appearance: none;
-        background-color: transparent;
-        vertical-align: middle;
-        margin-top: 8px;
-        &:focus {
-            outline: none;
-        }
-        &::-webkit-slider-runnable-track {
-            width: 100%;
-            height: 3px;
-            border-radius: 1.5px;
-            cursor: pointer;
-            background: $intermediate-color;
-        }
-        &::-webkit-slider-thumb {
-            height: 12px;
-            width: 12px;
-            border: 1px solid $intermediate-color;
-            border-radius: 6px;
-            background: $light-color;
-            cursor: pointer;
-            -webkit-appearance: none;
-            margin-top: -4px;
-        }
-        &::-moz-range-track {
-            width: 100%;
-            height: 3px;
-            border-radius: 1.5px;
-            cursor: pointer;
-            background: $light-color;
-        }
-        &::-moz-range-thumb {
-            height: 12px;
-            width: 12px;
-            border: 1px solid $intermediate-color;
-            border-radius: 6px;
-            background: $light-color;
-            cursor: pointer;
-            -webkit-appearance: none;
-            margin-top: -4px;
-        }
-    }
-    input[type="text"] {
-        border: none;
-        padding: 0 0px;
-        background: transparent;
-        color: $dark-color;
-        font-size: 12px;
-        margin-top: 4px;
-    }
-    .ink-panel {
-        margin: 6px 12px 6px 0;
-        height: 30px;
-        vertical-align: middle;
-        line-height: 36px;
-        padding: 0 10px;
-        color: $intermediate-color;
-        &:first {
-            margin-top: 0;
-        }
-    }
-    .ink-tools {
-        display: flex;
-        background-color: transparent;
-        border-radius: 0;
-        padding: 0;
-        button {
-            height: 36px;
-            padding: 0px;
-            padding-bottom: 3px;
-            margin-left: 10px;
-            background-color: transparent;
-            color: $intermediate-color;
-        }
-        button:hover {
-            transform: scale(1.15);
-        }
-    }
-    .ink-size {
-        display: flex;
-        justify-content: space-between;
-        input[type="text"] {
-            width: 42px;
-        }
-        >* {
-            margin-right: 6px;
-            &:last-child {
-                margin-right: 0;
-            }
-        }
-    }
-    .ink-color {
-        display: flex;
-        position: relative;
-        padding-right: 0;
-        label {
-            margin-right: 6px;
-        }
-        .ink-color-display {
-            border-radius: 11px;
-            width: 22px;
-            height: 22px;
-            margin-top: 6px;
-            cursor: pointer;
-            text-align: center; //   span {
-            //     color: $light-color;
-            //     font-size: 8px;
-            //     user-select: none;
-            //   }
-        }
-        .ink-color-picker {
-            background-color: $light-color;
-            border-radius: 5px;
-            padding: 12px;
-            position: absolute;
-            bottom: 36px;
-            left: -3px;
-            box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 8fb74ec48..a971264b9 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -57,51 +57,62 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
     }
 
     @action
-    handleMouseDown = (e: React.PointerEvent): void => {
-        if (e.button != 0 ||
+    onPointerDown = (e: React.PointerEvent): void => {
+        this._isDrawing = false;
+        if (e.button != 0 || e.altKey || e.ctrlKey ||
             InkingControl.Instance.selectedTool === InkTool.None) {
             return;
         }
-        e.stopPropagation()
-        if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
-            return
-        }
-        const point = this.relativeCoordinatesForEvent(e);
-
-        // start the new line, saves a uuid to represent the field of the stroke
-        this._idGenerator = Utils.GenerateGuid();
-        this.inkData.set(this._idGenerator,
-            {
-                pathData: [point],
-                color: InkingControl.Instance.selectedColor,
-                width: InkingControl.Instance.selectedWidth,
-                tool: InkingControl.Instance.selectedTool,
-                page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
-            });
+        document.addEventListener("pointermove", this.onPointerMove, true);
+        document.addEventListener("pointerup", this.onPointerUp, true);
+        e.stopPropagation();
+
         this._isDrawing = true;
+        if (InkingControl.Instance.selectedTool != InkTool.Eraser) {
+            const point = this.relativeCoordinatesForEvent(e.clientX, e.clientY);
+
+            // start the new line, saves a uuid to represent the field of the stroke
+            this._idGenerator = Utils.GenerateGuid();
+            this.inkData.set(this._idGenerator,
+                {
+                    pathData: [point],
+                    color: InkingControl.Instance.selectedColor,
+                    width: InkingControl.Instance.selectedWidth,
+                    tool: InkingControl.Instance.selectedTool,
+                    page: this.props.Document.GetNumber(KeyStore.CurPage, -1)
+                });
+        }
     }
 
-    @action
-    handleMouseMove = (e: React.PointerEvent): void => {
-        if (!this._isDrawing ||
-            InkingControl.Instance.selectedTool === InkTool.None) {
-            return;
+    onPointerUp = (e: PointerEvent): void => {
+        if (this._isDrawing) {
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            this._isDrawing = false;
+            e.stopPropagation();
         }
-        e.stopPropagation()
-        if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
-            return
-        }
-        const point = this.relativeCoordinatesForEvent(e);
+    }
 
-        // add points to new line as it is being drawn
-        let data = this.inkData;
-        let strokeData = data.get(this._idGenerator);
-        if (strokeData) {
-            strokeData.pathData.push(point);
-            data.set(this._idGenerator, strokeData);
+    @action
+    onPointerMove = (e: PointerEvent): void => {
+        if (this._isDrawing) {
+            e.stopPropagation()
+            e.preventDefault();
+            if (InkingControl.Instance.selectedTool === InkTool.Eraser) {
+                return
+            }
+            const point = this.relativeCoordinatesForEvent(e.clientX, e.clientY);
+
+            // add points to new line as it is being drawn
+            let data = this.inkData;
+            let strokeData = data.get(this._idGenerator);
+            if (strokeData) {
+                strokeData.pathData.push(point);
+                data.set(this._idGenerator, strokeData);
+            }
+
+            this.inkData = data;
         }
-
-        this.inkData = data;
     }
 
     @action
@@ -109,8 +120,8 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
         this._isDrawing = false;
     }
 
-    relativeCoordinatesForEvent = (e: React.MouseEvent): { x: number, y: number } => {
-        let [x, y] = this.props.getScreenTransform().transformPoint(e.clientX, e.clientY);
+    relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => {
+        let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey);
         x += InkingCanvas.InkOffset;
         y += InkingCanvas.InkOffset;
         return { x, y };
@@ -124,32 +135,9 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
     }
 
     render() {
-        // styling for cursor
-        let canvasStyle = {};
-        if (InkingControl.Instance.selectedTool === InkTool.None) {
-            canvasStyle = { pointerEvents: "none" };
-        } else {
-            canvasStyle = { pointerEvents: "auto", cursor: "crosshair" };
-        }
-
-        // get data from server
-        // let inkField = this.props.Document.GetT(KeyStore.Ink, InkField);
-        // if (!inkField || inkField == FieldWaiting) {
-        //     return (<div className="inking-canvas" style={canvasStyle}
-        //         onMouseDown={this.handleMouseDown} onMouseMove={this.handleMouseMove} >
-        //         <svg>
-        //         </svg>
-        //     </div >)
-        // }
-
-        let lines = this.inkData;
-
         // parse data from server
-        let paths: Array<JSX.Element> = []
         let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1)
-        Array.from(lines).map(item => {
-            let id = item[0];
-            let strokeData = item[1];
+        let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
             if (strokeData.page == -1 || strokeData.page == curPage)
                 paths.push(<InkingStroke key={id} id={id}
                     line={strokeData.pathData}
@@ -157,13 +145,16 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
                     width={strokeData.width}
                     tool={strokeData.tool}
                     deleteCallback={this.removeLine} />)
-        })
+            return paths;
+        }, [] as JSX.Element[]);
+        let svgCanvasStyle = InkingControl.Instance.selectedTool == InkTool.None ? "-none" : "";
 
         return (
-            <div className="inking-canvas" style={canvasStyle} onPointerDown={this.handleMouseDown} onPointerMove={this.handleMouseMove} >
-                <svg>
+            <div className="inkingCanvas" >
+                <svg className={`inkingCanvas-paths${svgCanvasStyle}`} onPointerDown={this.onPointerDown} >
                     {paths}
                 </svg>
+                {this.props.children}
             </div >
         )
     }
diff --git a/src/client/views/InkingControl.scss b/src/client/views/InkingControl.scss
new file mode 100644
index 000000000..0d8fd8784
--- /dev/null
+++ b/src/client/views/InkingControl.scss
@@ -0,0 +1,135 @@
+@import "global_variables";
+.inking-control {
+    position: absolute;
+    left: 70px;
+    bottom: 70px;
+    margin: 0;
+    padding: 0;
+    display: flex;
+    label,
+    input,
+    option {
+        font-size: 12px;
+    }
+    input[type="range"] {
+        -webkit-appearance: none;
+        background-color: transparent;
+        vertical-align: middle;
+        margin-top: 8px;
+        &:focus {
+            outline: none;
+        }
+        &::-webkit-slider-runnable-track {
+            width: 100%;
+            height: 3px;
+            border-radius: 1.5px;
+            cursor: pointer;
+            background: $intermediate-color;
+        }
+        &::-webkit-slider-thumb {
+            height: 12px;
+            width: 12px;
+            border: 1px solid $intermediate-color;
+            border-radius: 6px;
+            background: $light-color;
+            cursor: pointer;
+            -webkit-appearance: none;
+            margin-top: -4px;
+        }
+        &::-moz-range-track {
+            width: 100%;
+            height: 3px;
+            border-radius: 1.5px;
+            cursor: pointer;
+            background: $light-color;
+        }
+        &::-moz-range-thumb {
+            height: 12px;
+            width: 12px;
+            border: 1px solid $intermediate-color;
+            border-radius: 6px;
+            background: $light-color;
+            cursor: pointer;
+            -webkit-appearance: none;
+            margin-top: -4px;
+        }
+    }
+    input[type="text"] {
+        border: none;
+        padding: 0 0px;
+        background: transparent;
+        color: $dark-color;
+        font-size: 12px;
+        margin-top: 4px;
+    }
+    .ink-panel {
+        margin: 6px 12px 6px 0;
+        height: 30px;
+        vertical-align: middle;
+        line-height: 36px;
+        padding: 0 10px;
+        color: $intermediate-color;
+        &:first {
+            margin-top: 0;
+        }
+    }
+    .ink-tools {
+        display: flex;
+        background-color: transparent;
+        border-radius: 0;
+        padding: 0;
+        button {
+            height: 36px;
+            padding: 0px;
+            padding-bottom: 3px;
+            margin-left: 10px;
+            background-color: transparent;
+            color: $intermediate-color;
+        }
+        button:hover {
+            transform: scale(1.15);
+        }
+    }
+    .ink-size {
+        display: flex;
+        justify-content: space-between;
+        input[type="text"] {
+            width: 42px;
+        }
+        >* {
+            margin-right: 6px;
+            &:last-child {
+                margin-right: 0;
+            }
+        }
+    }
+    .ink-color {
+        display: flex;
+        position: relative;
+        padding-right: 0;
+        label {
+            margin-right: 6px;
+        }
+        .ink-color-display {
+            border-radius: 11px;
+            width: 22px;
+            height: 22px;
+            margin-top: 6px;
+            cursor: pointer;
+            text-align: center; //   span {
+            //     color: $light-color;
+            //     font-size: 8px;
+            //     user-select: none;
+            //   }
+        }
+        .ink-color-picker {
+            background-color: $light-color;
+            border-radius: 5px;
+            padding: 12px;
+            position: absolute;
+            bottom: 36px;
+            left: -3px;
+            box-shadow: $intermediate-color 0.2vw 0.2vw 0.8vw;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index eb2172d03..c1519dff8 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -2,10 +2,9 @@ import { observable, action, computed } from "mobx";
 
 import { CirclePicker, ColorResult } from 'react-color'
 import React = require("react");
-import "./InkingCanvas.scss"
 import { InkTool } from "../../fields/InkField";
 import { observer } from "mobx-react";
-import "./InkingCanvas.scss"
+import "./InkingControl.scss"
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { faPen, faHighlighter, faEraser, faBan } from '@fortawesome/free-solid-svg-icons';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index 9af0df7f5..4341c82f7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -4,4 +4,7 @@
     width: 20000px; 
     height: 20000px;
     pointer-events: none;
+  }
+  .collectionfreeformlinksview-container {
+    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
index ea392eaea..ef60aa672 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -94,9 +94,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
     render() {
         return (
-            <svg className="collectionfreeformlinksview-svgCanvas">
-                {this.uniqueConnections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />)}
+            <div className="collectionfreeformlinksview-container">
+                <svg className="collectionfreeformlinksview-svgCanvas">
+                    {this.uniqueConnections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />)}
+                </svg>
                 {this.props.children}
-            </svg>);
+            </div>
+        );
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index c979b27a9..81d21d89a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -7,9 +7,6 @@
   width: 100%;
   height: 100%;
   transform-origin: left top;
-  .inking-canvas {
-    transform-origin: 50000px 50000px;
-  }
 }
 .collectionfreeformview-container {
   .collectionfreeformview > .jsx-parser {
@@ -18,9 +15,6 @@
     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),
@@ -48,10 +42,6 @@
     background: $light-color-secondary;
   }
 
-  .inking-canvas {
-    transform-origin: 50000px 50000px;
-  }
-
   opacity: 0.99;
   border: 0px solid transparent;
   border-radius: $border-radius;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cdc5bdfb0..ae1c775e6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -314,16 +314,18 @@ export class CollectionFreeFormView extends CollectionViewBase {
             <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`}
                 onPointerDown={this.onPointerDown} onPointerMove={(e) => super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY))}
                 onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onWheel={this.onPointerWheel}
-                style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} tabIndex={0} ref={this.createDropTarget}>
+                style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} ref={this.createDropTarget}>
                 <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
                     addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
                     <div className="collectionfreeformview" ref={this._canvasRef}
                         style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}>
                         {this.backgroundView}
-                        <CollectionFreeFormLinksView {...this.props} />
-                        {this.views}
-                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} />
+                        <CollectionFreeFormLinksView {...this.props}>
+                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+                                {this.views}
+                            </InkingCanvas>
+                        </CollectionFreeFormLinksView>
                         {super.getCursors().map(entry => {
                             if (entry.Data.length > 0) {
                                 let id = entry.Data[0][0];
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7f951864e..d52b662bd 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { computed, trace, reaction, runInAction, observable } from "mobx";
+import { computed } from "mobx";
 import { observer } from "mobx-react";
 import { KeyStore } from "../../../fields/KeyStore";
 import { NumberField } from "../../../fields/NumberField";
@@ -6,8 +6,7 @@ import { Transform } from "../../util/Transform";
 import { DocumentView, DocumentViewProps } from "./DocumentView";
 import "./DocumentView.scss";
 import React = require("react");
-import { Document } from "../../../fields/Document";
-import { DocumentManager } from "../../util/DocumentManager";
+import { thisExpression } from "babel-types";
 
 
 @observer
@@ -75,6 +74,7 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView
             <div className="collectionFreeFormDocumentView-container" ref={this._mainCont} style={{
                 transformOrigin: "left top",
                 transform: this.transform,
+                pointerEvents: "all",
                 width: this.width,
                 height: this.height,
                 position: "absolute",
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index e273b0b4f..28a1f9757 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -87,7 +87,7 @@ export class PDFBox extends React.Component<FieldViewProps> {
     @observable private _interactive: boolean = false;
     @observable private _loaded: boolean = false;
 
-    @computed private get curPage() { return this.props.doc.GetNumber(KeyStore.CurPage, -1); }
+    @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() {
@@ -435,8 +435,6 @@ export class PDFBox extends React.Component<FieldViewProps> {
     @computed
     get pdfContent() {
         let page = this.curPage;
-        if (page == 0)
-            page = 1;
         const renderHeight = 2400;
         let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField);
         let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight;
@@ -463,10 +461,8 @@ export class PDFBox extends React.Component<FieldViewProps> {
         return [
             this._pageInfo.area.filter(() => this._pageInfo.area).map((element: any) => element),
             this._currAnno.map((element: any) => element),
-            <div key="pdfBox-contentShell">
-                {this.pdfContent}
-                {proxy}
-            </div>
+            this.pdfContent,
+            proxy
         ];
     }
 
-- 
cgit v1.2.3-70-g09d2


From a687a9962c8952074177e52366b69dcce231a18e Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 1 Apr 2019 13:46:32 -0400
Subject: fixed several things related to selections and creating text boxes. 
 moved remote cursor stuff into its own file.

---
 src/client/util/SelectionManager.ts                |  13 ++-
 src/client/views/InkingCanvas.tsx                  |   8 +-
 .../views/collections/CollectionViewBase.tsx       |   8 --
 .../CollectionFreeFormLinksView.tsx                |   7 +-
 .../CollectionFreeFormRemoteCursors.tsx            | 115 ++++++++++++++++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 118 ++++-----------------
 .../collections/collectionFreeForm/MarqueeView.tsx |  24 ++---
 .../collectionFreeForm/PreviewCursor.tsx           |  67 ++++++++----
 8 files changed, 213 insertions(+), 147 deletions(-)
 create mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx

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

diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1a711ae64..6d6ae26fc 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,5 +1,6 @@
 import { observable, action } from "mobx";
 import { DocumentView } from "../views/nodes/DocumentView";
+import { Document } from "../../fields/Document"
 
 export namespace SelectionManager {
     class Manager {
@@ -29,8 +30,16 @@ export namespace SelectionManager {
         return manager.SelectedDocuments.indexOf(doc) !== -1;
     }
 
-    export function DeselectAll(): void {
-        manager.SelectedDocuments = []
+    export function DeselectAll(except?: Document): void {
+        let found: DocumentView | undefined = undefined;
+        if (except) {
+            for (let i = 0; i < manager.SelectedDocuments.length; i++) {
+                let view = manager.SelectedDocuments[i];
+                if (view.props.Document == except)
+                    found = view;
+            }
+        }
+        manager.SelectedDocuments.length = 0;
     }
 
     export function SelectedDocuments(): Array<DocumentView> {
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 15dfb255a..123ff679b 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -101,7 +101,6 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
 
     @computed
     get drawnPaths() {
-        trace();
         // parse data from server
         let curPage = this.props.Document.GetNumber(KeyStore.CurPage, -1)
         let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => {
@@ -111,10 +110,10 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
                     tool={strokeData.tool} deleteCallback={this.removeLine} />)
             return paths;
         }, [] as JSX.Element[]);
-        return [<svg className={`inkingCanvas-paths-markers`}  >
+        return [<svg className={`inkingCanvas-paths-markers`} key="Markers"  >
             {paths.filter(path => path.props.tool == InkTool.Highlighter)}
         </svg>,
-        <svg className={`inkingCanvas-paths-ink`}  >
+        <svg className={`inkingCanvas-paths-ink`} key="Pens" >
             {paths.filter(path => path.props.tool != InkTool.Highlighter)}
         </svg>];
     }
@@ -122,11 +121,10 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
     render() {
         let svgCanvasStyle = InkingControl.Instance.selectedTool != InkTool.None ? "canSelect" : "noSelect";
 
-        trace();
         return (
             <div className="inkingCanvas" >
                 <svg className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
-                {this.props.children}
+                {(this.props.children as any)() /* bcz: is there a better way to know that children is a function? */}
                 {this.drawnPaths}
             </div >
         )
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 48adce4d8..987f3cb6c 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -76,14 +76,6 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         }
     }
 
-    protected getCursors(): CursorEntry[] {
-        let doc = this.props.Document;
-        let id = CurrentUserUtils.id;
-        let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
-        let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
-        return id ? notMe : [];
-    }
-
     @undoBatch
     @action
     protected drop(e: Event, de: DragManager.DropEvent): boolean {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index ef60aa672..eb20b3100 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, reaction, runInAction } from "mobx";
+import { computed, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../../fields/Document";
 import { FieldWaiting } from "../../../../fields/Field";
@@ -71,7 +71,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
     @computed
     get uniqueConnections() {
-        return DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             let srcViews = this.documentAnchors(connection.a);
             let targetViews = this.documentAnchors(connection.b);
             let possiblePairs: { a: Document, b: Document, }[] = [];
@@ -90,13 +90,14 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
             })
             return drawnPairs
         }, [] as { a: Document, b: Document, l: Document[] }[]);
+        return connections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />);
     }
 
     render() {
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
-                    {this.uniqueConnections.map(c => <CollectionFreeFormLinkView key={Utils.GenerateGuid()} A={c.a} B={c.b} LinkDocs={c.l} />)}
+                    {this.uniqueConnections}
                 </svg>
                 {this.props.children}
             </div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
new file mode 100644
index 000000000..19382e66f
--- /dev/null
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -0,0 +1,115 @@
+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 { 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, CollectionViewProps, CursorEntry } from "../CollectionViewBase";
+import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
+import "./CollectionFreeFormView.scss";
+import { MarqueeView } from "./MarqueeView";
+import React = require("react");
+import v5 = require("uuid/v5");
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+
+@observer
+export class CollectionFreeFormRemoteCursors extends React.Component<CollectionViewProps> {
+    protected getCursors(): CursorEntry[] {
+        let doc = this.props.Document;
+        let id = CurrentUserUtils.id;
+        let cursors = doc.GetList<CursorEntry>(KeyStore.Cursors, []);
+        let notMe = cursors.filter(entry => entry.Data[0][0] !== id);
+        return id ? notMe : [];
+    }
+
+    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);
+            }
+        }
+    }
+    @computed
+    get sharedCursors() {
+        return this.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>
+                );
+            }
+        })
+    }
+
+    render() {
+        return this.sharedCursors;
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ae1c775e6..60fb95ff5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.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 { FieldWaiting } from "../../../../fields/Field";
@@ -18,6 +18,8 @@ import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
 import v5 = require("uuid/v5");
+import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
+import { PreviewCursor } from "./PreviewCursor";
 
 @observer
 export class CollectionFreeFormView extends CollectionViewBase {
@@ -60,7 +62,7 @@ export class CollectionFreeFormView extends CollectionViewBase {
     @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 isAnnotationOverlay() { return this.props.fieldKey && 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); }
@@ -97,13 +99,15 @@ export class CollectionFreeFormView extends CollectionViewBase {
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if ((e.button === 2 && this.props.active() && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) {
+        if (((e.button === 2 && (!this.isAnnotationOverlay || this.zoomScaling != 1)) || e.button == 0) && this.props.active()) {
             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;
+            if (this.props.isSelected())
+                e.stopPropagation();
         }
     }
 
@@ -134,7 +138,6 @@ export class CollectionFreeFormView extends CollectionViewBase {
     onPointerWheel = (e: React.WheelEvent): void => {
         this.props.select(false);
         e.stopPropagation();
-        e.preventDefault();
         let coefficient = 1000;
 
         if (e.ctrlKey) {
@@ -265,50 +268,13 @@ export class CollectionFreeFormView extends CollectionViewBase {
     getContainerTransform = (): 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;
-
-
-    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);
-            }
-        }
-    }
+    childViews = () => this.views;
 
     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"}`}
@@ -316,60 +282,22 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onWheel={this.onPointerWheel}
                 style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }} ref={this.createDropTarget}>
                 <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments}
-                    addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
+                    addDocument={this.addDocument} removeDocument={this.props.removeDocument}
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
-                    <div className="collectionfreeformview" ref={this._canvasRef}
-                        style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}>
-                        {this.backgroundView}
-                        <CollectionFreeFormLinksView {...this.props}>
-                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
-                                {this.views}
-                            </InkingCanvas>
-                        </CollectionFreeFormLinksView>
-                        {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>
-                    {this.overlayView}
+                    <PreviewCursor container={this} addLiveTextDocument={this.addLiveTextBox}
+                        getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} >
+                        <div className="collectionfreeformview" ref={this._canvasRef}
+                            style={{ transform: `translate(${dx}px, ${dy}px) scale(${this.zoomScaling}, ${this.zoomScaling}) translate(${panx}px, ${pany}px)` }}>
+                            {this.backgroundView}
+                            <CollectionFreeFormLinksView {...this.props}>
+                                <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} >
+                                    {this.childViews}
+                                </InkingCanvas>
+                            </CollectionFreeFormLinksView>
+                            <CollectionFreeFormRemoteCursors {...this.props} />
+                        </div>
+                        {this.overlayView}
+                    </PreviewCursor>
                 </MarqueeView>
             </div>
         );
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 2b0e7d228..20132a4b1 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.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 { FieldWaiting } from "../../../../fields/Field";
@@ -21,7 +21,6 @@ interface MarqueeViewProps {
     activeDocuments: () => Document[];
     selectDocuments: (docs: Document[]) => void;
     removeDocument: (doc: Document) => boolean;
-    addLiveTextDocument: (doc: Document) => void;
 }
 
 @observer
@@ -77,10 +76,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     onPointerUp = (e: PointerEvent): void => {
         this.cleanupInteractions(true);
         this._visible = false;
+        let mselect = this.marqueeSelect();
         if (!e.shiftKey) {
-            SelectionManager.DeselectAll();
+            SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
         }
-        this.props.selectDocuments(this.marqueeSelect());
+        this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
     }
 
     intersectRect(r1: { left: number, top: number, width: number, height: number },
@@ -184,17 +184,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         return selection;
     }
 
-    render() {
+    @computed
+    get marqueeDiv() {
         let p = this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY);
         let v = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
-        return <div className="marqueeView" onPointerDown={this.onPointerDown}>
-            <PreviewCursor container={this.props.container} addLiveTextDocument={this.props.addLiveTextDocument}
-                getContainerTransform={this.props.getContainerTransform} getTransform={this.props.getTransform} >
-                {this.props.children}
-                {!this._visible ? (null) :
-                    <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />}
+        return <div className="marquee" style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, width: `${Math.abs(v[0])}`, height: `${Math.abs(v[1])}` }} />
+    }
 
-            </PreviewCursor>
+    render() {
+        return <div className="marqueeView" onPointerDown={this.onPointerDown}>
+            {this.props.children}
+            {!this._visible ? (null) : this.marqueeDiv}
         </div>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
index 877de8846..93c98f7b0 100644
--- a/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.tsx
@@ -1,4 +1,4 @@
-import { action, observable, trace } from "mobx";
+import { action, observable, trace, computed, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Document } from "../../../../fields/Document";
 import { Documents } from "../../../documents/Documents";
@@ -6,6 +6,7 @@ import { Transform } from "../../../util/Transform";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./PreviewCursor.scss";
 import React = require("react");
+import { interfaceDeclaration } from "babel-types";
 
 
 export interface PreviewCursorProps {
@@ -23,27 +24,29 @@ export class PreviewCursor extends React.Component<PreviewCursorProps>  {
     @observable public DownX: number = 0;
     @observable public DownY: number = 0;
     _showOnUp: boolean = false;
-    public _previewDivRef = React.createRef<HTMLDivElement>();
 
     @action
     cleanupInteractions = () => {
         document.removeEventListener("pointerup", this.onPointerUp, true);
+        document.removeEventListener("pointermove", this.onPointerMove, true);
     }
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        this._visible = false;
-        document.removeEventListener("keypress", this.onKeyPress, false);
-        this._showOnUp = true;
-        this._lastX = this.DownX = e.pageX;
-        this._lastY = this.DownY = e.pageY;
-        document.addEventListener("pointerup", this.onPointerUp, true);
-        document.addEventListener("pointermove", this.onPointerMove, true);
+        if (e.button == 0 && this.props.container.props.active()) {
+            document.removeEventListener("keypress", this.onKeyPress, false);
+            this._showOnUp = true;
+            this.DownX = e.pageX;
+            this.DownY = e.pageY;
+            document.addEventListener("pointerup", this.onPointerUp, true);
+            document.addEventListener("pointermove", this.onPointerMove, true);
+        }
     }
     @action
     onPointerMove = (e: PointerEvent): void => {
         if (Math.abs(this.DownX - e.clientX) > 4 || Math.abs(this.DownY - e.clientY) > 4) {
             this._showOnUp = false;
+            this._visible = false;
         }
     }
 
@@ -51,6 +54,8 @@ export class PreviewCursor extends React.Component<PreviewCursorProps>  {
     onPointerUp = (e: PointerEvent): void => {
         if (this._showOnUp) {
             document.addEventListener("keypress", this.onKeyPress, false);
+            this._lastX = this.DownX;
+            this._lastY = this.DownY;
             this._visible = true;
         }
         this.cleanupInteractions();
@@ -72,25 +77,43 @@ export class PreviewCursor extends React.Component<PreviewCursorProps>  {
             e.stopPropagation();
         }
     }
-    //when focus is lost, this will remove the preview cursor
-    @action
-    onBlur = (): void => {
-        this._visible = false;
+
+    getPoint = () => this.props.getContainerTransform().transformPoint(this._lastX, this._lastY);
+    getVisible = () => this._visible;
+    setVisible = (v: boolean) => {
+        this._visible = v;
         document.removeEventListener("keypress", this.onKeyPress, false);
     }
-
     render() {
-        //get local position and place cursor there!
-        let p = this.props.getContainerTransform().transformPoint(this._lastX, this._lastY);
-        if (this._visible && this._previewDivRef.current)
-            this._previewDivRef.current!.focus();
-        trace();
         return (
-            <div className="previewCursorView" onBlur={this.onBlur} onPointerDown={this.onPointerDown}>
+            <div className="previewCursorView" onPointerDown={this.onPointerDown}>
                 {this.props.children}
-                {!this._visible ? (null) :
-                    <div className="previewCursor" tabIndex={0} ref={this._previewDivRef} id="previewCursor" style={{ transform: `translate(${p[0]}px, ${p[1]}px)` }}>I</div>}
+                <PreviewCursorPrompt setVisible={this.setVisible} getPoint={this.getPoint} getVisible={this.getVisible} />
             </div>
         )
     }
+}
+
+export interface PromptProps {
+    getPoint: () => number[];
+    getVisible: () => boolean;
+    setVisible: (v: boolean) => void;
+}
+
+@observer
+export class PreviewCursorPrompt extends React.Component<PromptProps> {
+    private _promptRef = React.createRef<HTMLDivElement>();
+
+    //when focus is lost, this will remove the preview cursor
+    @action onBlur = (): void => this.props.setVisible(false);
+
+    render() {
+        let p = this.props.getPoint();
+        if (this.props.getVisible() && this._promptRef.current)
+            this._promptRef.current.focus();
+        return <div className="previewCursor" id="previewCursor" onBlur={this.onBlur} tabIndex={0} ref={this._promptRef}
+            style={{ transform: `translate(${p[0]}px, ${p[1]}px)`, opacity: this.props.getVisible() ? 1 : 0 }}>
+            I
+        </div >;
+    }
 }
\ No newline at end of file
-- 
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/collectionFreeForm/CollectionFreeFormView.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


From b78aff5115862cbfa5e704422cb738aa7fd73c64 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 4 Apr 2019 14:13:29 -0400
Subject: fixed a number of smaller issues related to linking

---
 src/client/northstar/dash-nodes/HistogramBox.scss  |   5 +
 src/client/util/DragManager.ts                     | 533 +++++++-------
 src/client/util/SelectionManager.ts                |  74 +-
 src/client/views/DocumentDecorations.scss          |  61 +-
 src/client/views/DocumentDecorations.tsx           |  81 ++-
 src/client/views/InkingCanvas.scss                 |   2 +
 src/client/views/Main.scss                         |   7 +
 .../views/collections/CollectionDockingView.scss   |   2 +-
 .../views/collections/CollectionPDFView.scss       |   2 +
 .../views/collections/CollectionVideoView.scss     |   2 +
 .../views/collections/CollectionViewBase.tsx       |  20 +
 .../CollectionFreeFormLinkView.tsx                 |   8 +-
 .../CollectionFreeFormLinksView.scss               |   2 +
 .../collectionFreeForm/CollectionFreeFormView.scss |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  13 +-
 .../collectionFreeForm/MarqueeView.scss            |   2 +
 .../collectionFreeForm/PreviewCursor.scss          |   4 +
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   4 +
 src/client/views/nodes/DocumentView.scss           |   2 +
 src/client/views/nodes/PDFBox.scss                 |   4 +
 src/client/views/nodes/WebBox.scss                 |   2 +
 src/fields/Document.ts                             | 773 +++++++++++----------
 22 files changed, 882 insertions(+), 727 deletions(-)

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

diff --git a/src/client/northstar/dash-nodes/HistogramBox.scss b/src/client/northstar/dash-nodes/HistogramBox.scss
index b11840a65..94da36e29 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.scss
+++ b/src/client/northstar/dash-nodes/HistogramBox.scss
@@ -1,6 +1,8 @@
 .histogrambox-container {
     padding: 0vw;
     position: absolute;
+    top: 0;
+    left:0;
     text-align: center;
     width: 100%;
     height: 100%;
@@ -8,6 +10,7 @@
   }
   .histogrambox-xaxislabel {
     position:absolute;
+    left:0;
     width:100%;
     text-align: center;
     bottom:0;
@@ -19,11 +22,13 @@
     position:absolute;
     height:100%;
     width: 25px;
+    left:0;
     bottom:0;
     background: lightgray;
   }
   .histogrambox-yaxislabel-text {
     position:absolute;
+    left:0;
     transform-origin: left;
     transform: rotate(-90deg);
     text-align: center;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c0f482e18..e8f8cce7c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -8,304 +8,307 @@ import { CollectionView } from "../views/collections/CollectionView";
 import { DocumentView } from "../views/nodes/DocumentView";
 
 export function setupDrag(
-  _reference: React.RefObject<HTMLDivElement>,
-  docFunc: () => Document,
-  removeFunc: (containingCollection: CollectionView) => void = () => {}
+    _reference: React.RefObject<HTMLDivElement>,
+    docFunc: () => Document,
+    removeFunc: (containingCollection: CollectionView) => void = () => { }
 ) {
-  let onRowMove = action(
-    (e: PointerEvent): void => {
-      e.stopPropagation();
-      e.preventDefault();
+    let onRowMove = action(
+        (e: PointerEvent): void => {
+            e.stopPropagation();
+            e.preventDefault();
 
-      document.removeEventListener("pointermove", onRowMove);
-      document.removeEventListener("pointerup", onRowUp);
-      var dragData = new DragManager.DocumentDragData([docFunc()]);
-      dragData.removeDocument = removeFunc;
-      DragManager.StartDocumentDrag([_reference.current!], dragData);
-    }
-  );
-  let onRowUp = action(
-    (e: PointerEvent): void => {
-      document.removeEventListener("pointermove", onRowMove);
-      document.removeEventListener("pointerup", onRowUp);
-    }
-  );
-  let onItemDown = (e: React.PointerEvent) => {
-    // if (this.props.isSelected() || this.props.isTopMost) {
-    if (e.button == 0) {
-      e.stopPropagation();
-      if (e.shiftKey) {
-        CollectionDockingView.Instance.StartOtherDrag([docFunc()], e);
-      } else {
-        document.addEventListener("pointermove", onRowMove);
-        document.addEventListener("pointerup", onRowUp);
-      }
-    }
-    //}
-  };
-  return onItemDown;
+            document.removeEventListener("pointermove", onRowMove);
+            document.removeEventListener("pointerup", onRowUp);
+            var dragData = new DragManager.DocumentDragData([docFunc()]);
+            dragData.removeDocument = removeFunc;
+            DragManager.StartDocumentDrag([_reference.current!], dragData);
+        }
+    );
+    let onRowUp = action(
+        (e: PointerEvent): void => {
+            document.removeEventListener("pointermove", onRowMove);
+            document.removeEventListener("pointerup", onRowUp);
+        }
+    );
+    let onItemDown = (e: React.PointerEvent) => {
+        // if (this.props.isSelected() || this.props.isTopMost) {
+        if (e.button == 0) {
+            e.stopPropagation();
+            if (e.shiftKey) {
+                CollectionDockingView.Instance.StartOtherDrag([docFunc()], e);
+            } else {
+                document.addEventListener("pointermove", onRowMove);
+                document.addEventListener("pointerup", onRowUp);
+            }
+        }
+        //}
+    };
+    return onItemDown;
 }
 
 export namespace DragManager {
-  export function Root() {
-    const root = document.getElementById("root");
-    if (!root) {
-      throw new Error("No root element found");
+    export function Root() {
+        const root = document.getElementById("root");
+        if (!root) {
+            throw new Error("No root element found");
+        }
+        return root;
     }
-    return root;
-  }
 
-  let dragDiv: HTMLDivElement;
+    let dragDiv: HTMLDivElement;
 
-  export enum DragButtons {
-    Left = 1,
-    Right = 2,
-    Both = Left | Right
-  }
+    export enum DragButtons {
+        Left = 1,
+        Right = 2,
+        Both = Left | Right
+    }
 
-  interface DragOptions {
-    handlers: DragHandlers;
+    interface DragOptions {
+        handlers: DragHandlers;
 
-    hideSource: boolean | (() => boolean);
-  }
+        hideSource: boolean | (() => boolean);
+    }
 
-  export interface DragDropDisposer {
-    (): void;
-  }
+    export interface DragDropDisposer {
+        (): void;
+    }
 
-  export class DragCompleteEvent {}
+    export class DragCompleteEvent { }
 
-  export interface DragHandlers {
-    dragComplete: (e: DragCompleteEvent) => void;
-  }
+    export interface DragHandlers {
+        dragComplete: (e: DragCompleteEvent) => void;
+    }
 
-  export interface DropOptions {
-    handlers: DropHandlers;
-  }
-  export class DropEvent {
-    constructor(
-      readonly x: number,
-      readonly y: number,
-      readonly data: { [id: string]: any }
-    ) {}
-  }
+    export interface DropOptions {
+        handlers: DropHandlers;
+    }
+    export class DropEvent {
+        constructor(
+            readonly x: number,
+            readonly y: number,
+            readonly data: { [id: string]: any }
+        ) { }
+    }
 
-  export interface DropHandlers {
-    drop: (e: Event, de: DropEvent) => void;
-  }
+    export interface DropHandlers {
+        drop: (e: Event, de: DropEvent) => void;
+    }
 
-  export function MakeDropTarget(
-    element: HTMLElement,
-    options: DropOptions
-  ): DragDropDisposer {
-    if ("canDrop" in element.dataset) {
-      throw new Error(
-        "Element is already droppable, can't make it droppable again"
-      );
+    export function MakeDropTarget(
+        element: HTMLElement,
+        options: DropOptions
+    ): DragDropDisposer {
+        if ("canDrop" in element.dataset) {
+            throw new Error(
+                "Element is already droppable, can't make it droppable again"
+            );
+        }
+        element.dataset["canDrop"] = "true";
+        const handler = (e: Event) => {
+            const ce = e as CustomEvent<DropEvent>;
+            options.handlers.drop(e, ce.detail);
+        };
+        element.addEventListener("dashOnDrop", handler);
+        return () => {
+            element.removeEventListener("dashOnDrop", handler);
+            delete element.dataset["canDrop"];
+        };
     }
-    element.dataset["canDrop"] = "true";
-    const handler = (e: Event) => {
-      const ce = e as CustomEvent<DropEvent>;
-      options.handlers.drop(e, ce.detail);
-    };
-    element.addEventListener("dashOnDrop", handler);
-    return () => {
-      element.removeEventListener("dashOnDrop", handler);
-      delete element.dataset["canDrop"];
-    };
-  }
 
-  export class DocumentDragData {
-    constructor(dragDoc: Document[]) {
-      this.draggedDocuments = dragDoc;
-      this.droppedDocuments = dragDoc;
+    export class DocumentDragData {
+        constructor(dragDoc: Document[]) {
+            this.draggedDocuments = dragDoc;
+            this.droppedDocuments = dragDoc;
+        }
+        draggedDocuments: Document[];
+        droppedDocuments: Document[];
+        xOffset?: number;
+        yOffset?: number;
+        aliasOnDrop?: boolean;
+        removeDocument?: (collectionDrop: CollectionView) => void;
+        [id: string]: any;
     }
-    draggedDocuments: Document[];
-    droppedDocuments: Document[];
-    xOffset?: number;
-    yOffset?: number;
-    aliasOnDrop?: boolean;
-    removeDocument?: (collectionDrop: CollectionView) => void;
-    [id: string]: any;
-  }
 
-  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 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 {
-    constructor(linkSourceDoc: DocumentView) {
-      this.linkSourceDocumentView = linkSourceDoc;
+    export class LinkDragData {
+        constructor(linkSourceDoc: DocumentView) {
+            this.linkSourceDocumentView = linkSourceDoc;
+        }
+        droppedDocuments: Document[] = [];
+        linkSourceDocumentView: DocumentView;
+        [id: string]: any;
     }
-    linkSourceDocumentView: DocumentView;
-    [id: string]: any;
-  }
-  export function StartLinkDrag(
-    ele: HTMLElement,
-    dragData: LinkDragData,
-    options?: DragOptions
-  ) {
-    StartDrag([ele], dragData, options);
-  }
-  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);
+    export function StartLinkDrag(
+        ele: HTMLElement,
+        dragData: LinkDragData,
+        options?: DragOptions
+    ) {
+        StartDrag([ele], dragData, options);
     }
+    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);
+        }
 
-    let scaleXs: number[] = [];
-    let scaleYs: number[] = [];
-    let xs: number[] = [];
-    let ys: number[] = [];
+        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`;
+        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.margin = "0";
+            dragElement.style.top = "0";
+            dragElement.style.bottom = "";
+            dragElement.style.left = "0";
+            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]);
+            // 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);
+            return dragElement;
+        });
+
+        let hideSource = false;
+        if (options) {
+            if (typeof options.hideSource === "boolean") {
+                hideSource = options.hideSource;
+            } else {
+                hideSource = options.hideSource();
+            }
         }
-      }
+        eles.map(ele => (ele.hidden = hideSource));
 
-      dragDiv.appendChild(dragElement);
-      return dragElement;
-    });
+        const moveHandler = (e: PointerEvent) => {
+            e.stopPropagation();
+            e.preventDefault();
+            if (dragData instanceof DocumentDragData)
+                dragData.aliasOnDrop = e.ctrlKey || e.altKey;
+            if (e.shiftKey) {
+                abortDrag();
+                CollectionDockingView.Instance.StartOtherDrag(docs, {
+                    pageX: e.pageX,
+                    pageY: e.pageY,
+                    preventDefault: () => { },
+                    button: 0
+                });
+            }
+            dragElements.map(
+                (dragElement, i) =>
+                    (dragElement.style.transform = `translate(${(xs[i] +=
+                        e.movementX)}px, ${(ys[i] += e.movementY)}px) scale(${
+                        scaleXs[i]
+                        }, ${scaleYs[i]})`)
+            );
+        };
 
-    let hideSource = false;
-    if (options) {
-      if (typeof options.hideSource === "boolean") {
-        hideSource = options.hideSource;
-      } else {
-        hideSource = options.hideSource();
-      }
+        const abortDrag = () => {
+            document.removeEventListener("pointermove", moveHandler, true);
+            document.removeEventListener("pointerup", upHandler);
+            dragElements.map(dragElement => dragDiv.removeChild(dragElement));
+            eles.map(ele => (ele.hidden = false));
+        };
+        const upHandler = (e: PointerEvent) => {
+            abortDrag();
+            FinishDrag(eles, e, dragData, options, finishDrag);
+        };
+        document.addEventListener("pointermove", moveHandler, true);
+        document.addEventListener("pointerup", upHandler);
     }
-    eles.map(ele => (ele.hidden = hideSource));
 
-    const moveHandler = (e: PointerEvent) => {
-      e.stopPropagation();
-      e.preventDefault();
-      if (dragData instanceof DocumentDragData)
-        dragData.aliasOnDrop = e.ctrlKey || e.altKey;
-      if (e.shiftKey) {
-        abortDrag();
-        CollectionDockingView.Instance.StartOtherDrag(docs, {
-          pageX: e.pageX,
-          pageY: e.pageY,
-          preventDefault: () => {},
-          button: 0
+    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];
         });
-      }
-      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);
-      dragElements.map(dragElement => dragDiv.removeChild(dragElement));
-      eles.map(ele => (ele.hidden = false));
-    };
-    const upHandler = (e: PointerEvent) => {
-      abortDrag();
-      FinishDrag(eles, e, dragData, options, finishDrag);
-    };
-    document.addEventListener("pointermove", moveHandler, true);
-    document.addEventListener("pointerup", upHandler);
-  }
-
-  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);
-    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);
+        const target = document.elementFromPoint(e.x, e.y);
+        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
-          }
-        })
-      );
+            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;
     }
-    DocumentDecorations.Instance.Hidden = false;
-  }
 }
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 438659108..958c14491 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -3,48 +3,46 @@ import { DocumentView } from "../views/nodes/DocumentView";
 import { Document } from "../../fields/Document";
 
 export namespace SelectionManager {
-  class Manager {
-    @observable
-    SelectedDocuments: Array<DocumentView> = [];
-
-    @action
-    SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
-      // if doc is not in SelectedDocuments, add it
-      if (!ctrlPressed) {
-        manager.SelectedDocuments = [];
-      }
-
-      if (manager.SelectedDocuments.indexOf(doc) === -1) {
-        manager.SelectedDocuments.push(doc);
-      }
+    class Manager {
+        @observable
+        SelectedDocuments: Array<DocumentView> = [];
+
+        @action
+        SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
+            // if doc is not in SelectedDocuments, add it
+            if (!ctrlPressed) {
+                manager.SelectedDocuments = [];
+            }
+
+            if (manager.SelectedDocuments.indexOf(doc) === -1) {
+                manager.SelectedDocuments.push(doc);
+            }
+        }
     }
-  }
 
-  const manager = new Manager();
+    const manager = new Manager();
 
-  export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
-    if (!doc.isMinimized()) {
-      manager.SelectDoc(doc, ctrlPressed);
+    export function SelectDoc(doc: DocumentView, ctrlPressed: boolean): void {
+        manager.SelectDoc(doc, ctrlPressed);
     }
-  }
-
-  export function IsSelected(doc: DocumentView): boolean {
-    return manager.SelectedDocuments.indexOf(doc) !== -1;
-  }
-
-  export function DeselectAll(except?: Document): void {
-    let found: DocumentView | undefined = undefined;
-    if (except) {
-      for (let i = 0; i < manager.SelectedDocuments.length; i++) {
-        let view = manager.SelectedDocuments[i];
-        if (view.props.Document == except) found = view;
-      }
+
+    export function IsSelected(doc: DocumentView): boolean {
+        return manager.SelectedDocuments.indexOf(doc) !== -1;
+    }
+
+    export function DeselectAll(except?: Document): void {
+        let found: DocumentView | undefined = undefined;
+        if (except) {
+            for (let i = 0; i < manager.SelectedDocuments.length; i++) {
+                let view = manager.SelectedDocuments[i];
+                if (view.props.Document == except) found = view;
+            }
+        }
+        manager.SelectedDocuments.length = 0;
+        if (found) manager.SelectedDocuments.push(found);
     }
-    manager.SelectedDocuments.length = 0;
-    if (found) manager.SelectedDocuments.push(found);
-  }
 
-  export function SelectedDocuments(): Array<DocumentView> {
-    return manager.SelectedDocuments;
-  }
+    export function SelectedDocuments(): Array<DocumentView> {
+        return manager.SelectedDocuments;
+    }
 }
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index befe175b5..080c9fa1e 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -2,13 +2,16 @@
 
 #documentDecorations-container {
     position: absolute;
+    top: 0;
+    left:0;
     display: grid;
     z-index: 1000;
     grid-template-rows: 20px 8px 1fr 8px;
-    grid-template-columns: 8px 1fr 8px;
+    grid-template-columns: 8px 8px 1fr 8px;
     pointer-events: none;
 
     #documentDecorations-centerCont {
+        grid-column:3;
         background: none;
     }
 
@@ -18,6 +21,24 @@
         opacity: 0.8;
     }
 
+    #documentDecorations-topLeftResizer,
+    #documentDecorations-leftResizer,
+    #documentDecorations-bottomLeftResizer {
+        grid-column: 1
+    }
+
+    #documentDecorations-topResizer,
+    #documentDecorations-bottomResizer {
+        grid-column-start: 2;
+        grid-column-end: 4;
+    }
+
+    #documentDecorations-bottomRightResizer,
+    #documentDecorations-topRightResizer,
+    #documentDecorations-rightResizer {
+        grid-column:4
+    }
+
     #documentDecorations-topLeftResizer,
     #documentDecorations-bottomRightResizer {
         cursor: nwse-resize;
@@ -39,15 +60,19 @@
     }
     .title{
         background: lightblue;
-        grid-column-start:2;
-        grid-column-end: 4;
+        grid-column-start:3;
+        grid-column-end: 5;
         pointer-events: auto;
     }
 }
 
 .documentDecorations-minimizeButton {
-    background: rgb(250, 57, 9);
+    background:$alt-accent;
+    opacity: 0.8;
+    grid-column-start: 1;
+    grid-column-end: 3;
     pointer-events: all;
+    text-align: center;
 }
 .documentDecorations-background {
     background: lightblue;
@@ -87,7 +112,8 @@
 //     }
 // }
 .linkFlyout {
-    grid-column: 1/4
+    grid-column: 1/4;
+    margin-left: 25px;
 }
 
 .linkButton-empty:hover {
@@ -102,6 +128,31 @@
     cursor: pointer;
 }
 
+.linkButton-linker {
+    position:absolute;
+    bottom:0px;
+    left: 0px;
+    height: 20px;
+    width: 20px;
+    margin-top: 10px;
+    margin-right: 5px;
+    border-radius: 50%;
+    opacity: 0.9;
+    pointer-events: auto;
+    color: $dark-color;
+    border: $dark-color 1px solid;
+    text-transform: uppercase;
+    letter-spacing: 2px;
+    font-size: 75%;
+    transition: transform 0.2s;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+.linkButton-tray {
+    grid-column: 1/4;
+}
 .linkButton-empty {
     height: 20px;
     width: 20px;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index b74737ec9..a46087c9a 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,10 +1,11 @@
-import { action, computed, observable } from "mobx";
+import { action, computed, observable, trace, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Key } from "../../fields/Key";
 //import ContentEditable from 'react-contenteditable'
 import { KeyStore } from "../../fields/KeyStore";
 import { ListField } from "../../fields/ListField";
 import { NumberField } from "../../fields/NumberField";
+import { Document } from "../../fields/Document";
 import { TextField } from "../../fields/TextField";
 import { DragManager } from "../util/DragManager";
 import { SelectionManager } from "../util/SelectionManager";
@@ -13,6 +14,7 @@ import './DocumentDecorations.scss';
 import { DocumentView } from "./nodes/DocumentView";
 import { LinkMenu } from "./nodes/LinkMenu";
 import React = require("react");
+import { FieldWaiting } from "../../fields/Field";
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -28,6 +30,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     private _linkBoxHeight = 30;
     private _titleHeight = 20;
     private _linkButton = React.createRef<HTMLDivElement>();
+    private _linkerButton = React.createRef<HTMLDivElement>();
     //@observable private _title: string = this._documents[0].props.Document.Title;
     @observable private _title: string = this._documents.length > 0 ? this._documents[0].props.Document.Title : "";
     @observable private _fieldKey: Key = KeyStore.Title;
@@ -174,18 +177,40 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         }
     }
 
-    onLinkButtonDown = (e: React.PointerEvent): void => {
-        // if ()
-        // let linkMenu = new LinkMenu(SelectionManager.SelectedDocuments()[0]);
-        // linkMenu.Hidden = false;
-        console.log("down");
+    onLinkerButtonDown = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        document.removeEventListener("pointermove", this.onLinkerButtonMoved)
+        document.addEventListener("pointermove", this.onLinkerButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkerButtonUp)
+        document.addEventListener("pointerup", this.onLinkerButtonUp);
+    }
+    onLinkerButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onLinkerButtonMoved)
+        document.removeEventListener("pointerup", this.onLinkerButtonUp)
+        e.stopPropagation();
+    }
 
+    onLinkerButtonMoved = (e: PointerEvent): void => {
+        if (this._linkerButton.current != null) {
+            document.removeEventListener("pointermove", this.onLinkerButtonMoved)
+            document.removeEventListener("pointerup", this.onLinkerButtonUp)
+            let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0]);
+            DragManager.StartLinkDrag(this._linkerButton.current, dragData, {
+                handlers: {
+                    dragComplete: action(() => { }),
+                },
+                hideSource: false
+            })
+        }
+        e.stopPropagation();
+    }
+
+    onLinkButtonDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
         document.removeEventListener("pointermove", this.onLinkButtonMoved)
         document.addEventListener("pointermove", this.onLinkButtonMoved);
         document.removeEventListener("pointerup", this.onLinkButtonUp)
         document.addEventListener("pointerup", this.onLinkButtonUp);
-
     }
 
     onLinkButtonUp = (e: PointerEvent): void => {
@@ -194,18 +219,32 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.stopPropagation();
     }
 
-
     onLinkButtonMoved = (e: PointerEvent): void => {
         if (this._linkButton.current != null) {
             document.removeEventListener("pointermove", this.onLinkButtonMoved)
-            document.removeEventListener("pointerup", this.onLinkButtonUp)
-            let dragData = new DragManager.LinkDragData(SelectionManager.SelectedDocuments()[0]);
-            DragManager.StartLinkDrag(this._linkButton.current, dragData, {
-                handlers: {
-                    dragComplete: action(() => { }),
-                },
-                hideSource: false
-            })
+            document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+            let sourceDoc = SelectionManager.SelectedDocuments()[0].props.Document;
+            let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document)
+            let draggedDocs = (srcTarg && srcTarg != FieldWaiting) ?
+                srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]).map(linkDoc =>
+                    (linkDoc.GetT(KeyStore.LinkedToDocs, Document)) as Document) : [];
+            let draggedFromDocs = (srcTarg && srcTarg != FieldWaiting) ?
+                srcTarg.GetList(KeyStore.LinkedFromDocs, [] as Document[]).map(linkDoc =>
+                    (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : [];
+            draggedDocs.push(...draggedFromDocs);
+            if (draggedDocs.length) {
+                let dragData = new DragManager.DocumentDragData(draggedDocs.map(d => {
+                    let annot = d.GetT(KeyStore.AnnotationOn, Document); // bcz: ... needs to change ... the annotationOn document may not have been retrieved yet...
+                    return annot && annot != FieldWaiting ? annot : d;
+                }));
+                DragManager.StartDocumentDrag([this._linkButton.current], dragData, {
+                    handlers: {
+                        dragComplete: action(() => { }),
+                    },
+                    hideSource: false
+                })
+            }
         }
         e.stopPropagation();
     }
@@ -298,7 +337,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
 
     getValue = (): string => {
-        console.log("value watched");
         if (this._title === "changed" && this._documents.length > 0) {
             let field = this._documents[0].props.Document.Get(this._fieldKey);
             if (field instanceof TextField) {
@@ -338,8 +376,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             linkButton = (<Flyout
                 anchorPoint={anchorPoints.RIGHT_TOP}
                 content={
-                    <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent}>
-                    </LinkMenu>
+                    <LinkMenu docView={selFirst} changeFlyout={this.changeFlyoutContent} />
                 }>
                 <div className={"linkButton-" + (selFirst.props.Document.GetData(KeyStore.LinkedToDocs, ListField, []).length ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
             </Flyout>);
@@ -361,7 +398,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
                 opacity: this._opacity
             }}>
-                <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>v</div>
+                <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>...</div>
                 <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onPointerDown} onKeyPress={this.enterPressed} />
                 <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>
@@ -373,8 +410,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 <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 title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton}  </div>
+                <div className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>∞</div>
             </div >
         </div>
         )
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 35c8ee942..c5a2a50cb 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -2,6 +2,8 @@
 
 .inkingCanvas-paths-ink, .inkingCanvas-paths-markers, .inkingCanvas-noSelect, .inkingCanvas-canSelect {
     position: absolute;
+    top: 0;
+    left:0;
     width: 8192px;
     height: 8192px;
     cursor:"crosshair";
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index 698a9c617..594803aab 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -7,6 +7,9 @@ body {
     overflow: hidden;
     font-family: $sans-serif;
     margin: 0;
+    position:absolute;
+    top: 0;
+    left:0;
 }
 
 #dash-title {
@@ -158,11 +161,15 @@ button:hover {
     width:100%;
     height:100%;
     position:absolute;
+    top: 0;
+    left:0;
 }
 #mainContent-div {
     width:100%;
     height:100%;
     position:absolute;
+    top: 0;
+    left:0;
 }
 #add-options-content {
     display: table;
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 2706c3272..583d50c5b 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -3,7 +3,7 @@
 }
 
 .collectiondockingview-container {
-    position: relative;
+    position: absolute;
     top: 0;
     left: 0;
     overflow: hidden;
diff --git a/src/client/views/collections/CollectionPDFView.scss b/src/client/views/collections/CollectionPDFView.scss
index 0144625c1..0eca3f1cd 100644
--- a/src/client/views/collections/CollectionPDFView.scss
+++ b/src/client/views/collections/CollectionPDFView.scss
@@ -9,6 +9,8 @@
     width: 100%;
     height: 100%;
     position: absolute; 
+    top: 0;
+    left:0;
     
 }
 .collectionPdfView-backward {
diff --git a/src/client/views/collections/CollectionVideoView.scss b/src/client/views/collections/CollectionVideoView.scss
index cbb981b13..ed56ad268 100644
--- a/src/client/views/collections/CollectionVideoView.scss
+++ b/src/client/views/collections/CollectionVideoView.scss
@@ -3,6 +3,8 @@
     width: 100%;
     height: 100%;
     position: absolute;
+    top: 0;
+    left:0;
  
 }
 .collectionVideoView-time{
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index 2b0689800..daeca69e2 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -17,6 +17,8 @@ import { NumberField } from "../../../fields/NumberField";
 import request = require("request");
 import { ServerUtils } from "../../../server/ServerUtil";
 import { Server } from "../../Server";
+import { CollectionDockingView } from "./CollectionDockingView";
+import { runReactions } from "mobx/lib/internal";
 
 export interface CollectionViewProps {
     fieldKey: Key;
@@ -92,6 +94,24 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
             e.stopPropagation();
             return added;
         }
+        if (de.data instanceof DragManager.LinkDragData) {
+            let sourceDoc: Document = de.data.linkSourceDocumentView.props.Document;
+            if (sourceDoc) runInAction(() => {
+                let srcTarg = sourceDoc.GetT(KeyStore.Prototype, Document)
+                if (srcTarg && srcTarg != FieldWaiting) {
+                    let linkDocs = srcTarg.GetList(KeyStore.LinkedToDocs, [] as Document[]);
+                    linkDocs.map(linkDoc => {
+                        let targDoc = linkDoc.GetT(KeyStore.LinkedToDocs, Document);
+                        if (targDoc && targDoc != FieldWaiting) {
+                            let dropdoc = targDoc.MakeDelegate();
+                            de.data.droppedDocuments.push(dropdoc);
+                            this.props.addDocument(dropdoc, false);
+                        }
+                    })
+                }
+            })
+            return true;
+        }
         return false;
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index e84f0c5ad..3dfd74ec8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -23,10 +23,10 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         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;
+        let x1 = a.GetNumber(KeyStore.X, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Width, 0) / 2);
+        let y1 = a.GetNumber(KeyStore.Y, 0) + (a.GetBoolean(KeyStore.Minimized, false) ? 5 : a.GetNumber(KeyStore.Height, 0) / 2);
+        let x2 = b.GetNumber(KeyStore.X, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Width, 0) / 2);
+        let y2 = b.GetNumber(KeyStore.Y, 0) + (b.GetBoolean(KeyStore.Minimized, false) ? 5 : b.GetNumber(KeyStore.Height, 0) / 2);
         return (
             <line key={Utils.GenerateGuid()} className="collectionfreeformlinkview-linkLine" onPointerDown={this.onPointerDown}
                 style={{ strokeWidth: `${l.length * 5}` }}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
index 4341c82f7..30e158603 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.scss
@@ -1,6 +1,8 @@
 .collectionfreeformlinksview-svgCanvas{
     transform: translate(-10000px,-10000px);
     position: absolute;
+    top: 0;
+    left: 0;
     width: 20000px; 
     height: 20000px;
     pointer-events: none;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index 81d21d89a..79d520069 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -26,7 +26,7 @@
   border: 0px solid $light-color-secondary;
   border-radius: $border-radius;
   box-sizing: border-box;
-  position: relative;
+  position: absolute;
   overflow: hidden;
   top: 0;
   left: 0;
@@ -41,12 +41,12 @@
   .formattedTextBox-cont {
     background: $light-color-secondary;
   }
-
+ 
   opacity: 0.99;
   border: 0px solid transparent;
   border-radius: $border-radius;
   box-sizing: border-box;
-  position:relative;
+  position:absolute;
   overflow: hidden;
   top: 0;
   left: 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c5178f69d..3484e963e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -73,14 +73,17 @@ export class CollectionFreeFormView extends CollectionViewBase {
     @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);
+            let droppedDocs = de.data.droppedDocuments as Document[];
+            let xoff = de.data.xOffset as number || 0;
+            let yoff = de.data.yOffset as number || 0;
+            if (droppedDocs && droppedDocs.length) {
+                let screenX = de.x - xoff;
+                let screenY = de.y - yoff;
                 const [x, y] = this.getTransform().transformPoint(screenX, screenY);
-                let dragDoc = de.data.draggedDocuments[0];
+                let dragDoc = de.data.droppedDocuments[0];
                 let dragX = dragDoc.GetNumber(KeyStore.X, 0);
                 let dragY = dragDoc.GetNumber(KeyStore.Y, 0);
-                de.data.draggedDocuments.map(d => {
+                droppedDocs.map(d => {
                     let docX = d.GetNumber(KeyStore.X, 0);
                     let docY = d.GetNumber(KeyStore.Y, 0);
                     d.SetNumber(KeyStore.X, x + (docX - dragX));
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 1ee3b244b..0b406e722 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -1,6 +1,8 @@
 
 .marqueeView {
     position: absolute;
+    top:0;
+    left:0;
     width:100%;
     height:100%;
 }
diff --git a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
index 21210be2b..7a67c29bf 100644
--- a/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
+++ b/src/client/views/collections/collectionFreeForm/PreviewCursor.scss
@@ -3,9 +3,13 @@
     color: black;
     position: absolute;
     transform-origin: left top;
+    top: 0;
+    left:0;
     pointer-events: none;
 }
 .previewCursorView {
+    top: 0;
+    left:0;
     position: absolute;
     width:100%;
     height:100%;
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index d52b662bd..1a0f0cbbd 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -66,8 +66,12 @@ export class CollectionFreeFormDocumentView extends React.Component<DocumentView
         return <DocumentView {...this.props}
             ContentScaling={this.contentScaling}
             ScreenToLocalTransform={this.getTransform}
+            PanelWidth={this.panelWidth}
+            PanelHeight={this.panelHeight}
         />
     }
+    panelWidth = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelWidth();
+    panelHeight = () => this.props.Document.GetBoolean(KeyStore.Minimized, false) ? 10 : this.props.PanelHeight();
 
     render() {
         return (
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 127a6b535..5126e69f9 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -2,6 +2,8 @@
 
 .documentView-node {
   position: absolute;
+  top: 0;
+  left:0;
   background: $light-color; //overflow: hidden;
 
   &.minimized {
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index ad947afd5..830dfe6c6 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -1,12 +1,16 @@
 .react-pdf__Page {
     transform-origin: left top;
     position: absolute;
+    top: 0;
+    left:0;
 }
 .react-pdf__Document {
     position: absolute;
 }
 .pdfBox-buttonTray {
     position:absolute;
+    top: 0;
+    left:0;
     z-index: 25;
 }
 .pdfBox-contentContainer {
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a535b2638..c73bc0c47 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -2,6 +2,8 @@
 .webBox-cont {
     padding: 0vw;
     position: absolute;
+    top: 0;
+    left:0;
     width: 100%;
     height: 100%;
     overflow: scroll;
diff --git a/src/fields/Document.ts b/src/fields/Document.ts
index 8e71019a4..92d03d765 100644
--- a/src/fields/Document.ts
+++ b/src/fields/Document.ts
@@ -9,415 +9,420 @@ import { Server } from "../client/Server";
 import { Types } from "../server/Message";
 import { UndoManager } from "../client/util/UndoManager";
 import { HtmlField } from "./HtmlField";
+import { BooleanField } from "./BooleanField";
 
 export class Document extends Field {
-  //TODO tfs: We should probably store FieldWaiting in fields when we request it from the server so that we don't set up multiple server gets for the same document and field
-  public fields: ObservableMap<
-    string,
-    { key: Key; field: Field }
-  > = new ObservableMap();
-  public _proxies: ObservableMap<string, FieldId> = new ObservableMap();
-
-  constructor(id?: string, save: boolean = true) {
-    super(id);
-
-    if (save) {
-      Server.UpdateField(this);
+    //TODO tfs: We should probably store FieldWaiting in fields when we request it from the server so that we don't set up multiple server gets for the same document and field
+    public fields: ObservableMap<
+        string,
+        { key: Key; field: Field }
+    > = new ObservableMap();
+    public _proxies: ObservableMap<string, FieldId> = new ObservableMap();
+
+    constructor(id?: string, save: boolean = true) {
+        super(id);
+
+        if (save) {
+            Server.UpdateField(this);
+        }
     }
-  }
 
-  UpdateFromServer(data: [string, string][]) {
-    for (const key in data) {
-      const element = data[key];
-      this._proxies.set(element[0], element[1]);
-    }
-  }
-
-  public Width = () => {
-    return this.GetNumber(KeyStore.Width, 0);
-  };
-  public Height = () => {
-    return this.GetNumber(
-      KeyStore.Height,
-      this.GetNumber(KeyStore.NativeWidth, 0)
-        ? (this.GetNumber(KeyStore.NativeHeight, 0) /
-            this.GetNumber(KeyStore.NativeWidth, 0)) *
-            this.GetNumber(KeyStore.Width, 0)
-        : 0
-    );
-  };
-  public Scale = () => {
-    return this.GetNumber(KeyStore.Scale, 1);
-  };
-
-  @computed
-  public get Title(): string {
-    let title = this.Get(KeyStore.Title, true);
-    if (title)
-      if (title != FieldWaiting && title instanceof TextField)
-        return title.Data;
-      else return "-waiting-";
-    let parTitle = this.GetT(KeyStore.Title, TextField);
-    if (parTitle)
-      if (parTitle != FieldWaiting) return parTitle.Data + ".alias";
-      else return "-waiting-.alias";
-    return "-untitled-";
-  }
-
-  @computed
-  public get Fields() {
-    return this.fields;
-  }
-
-  /**
-   * Get the field in the document associated with the given key. If the
-   * associated field has not yet been filled in from the server, a request
-   * to the server will automatically be sent, the value will be filled in
-   * when the request is completed, and {@link Field.ts#FieldWaiting} will be returned.
-   * @param key - The key of the value to get
-   * @param ignoreProto - If true, ignore any prototype this document
-   * might have and only search for the value on this immediate document.
-   * If false (default), search up the prototype chain, starting at this document,
-   * for a document that has a field associated with the given key, and return the first
-   * one found.
-   *
-   * @returns If the document does not have a field associated with the given key, returns `undefined`.
-   * If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}.
-   * If the document does have an associated field, and the field has not been fetched from the server, returns the associated field.
-   */
-  Get(key: Key, ignoreProto: boolean = false): FieldValue<Field> {
-    let field: FieldValue<Field>;
-    if (ignoreProto) {
-      if (this.fields.has(key.Id)) {
-        field = this.fields.get(key.Id)!.field;
-      } else if (this._proxies.has(key.Id)) {
-        Server.GetDocumentField(this, key);
-        /*
-                The field might have been instantly filled from the cache
-                Maybe we want to just switch back to returning the value
-                from Server.GetDocumentField if it's in the cache
-                */
-        if (this.fields.has(key.Id)) {
-          field = this.fields.get(key.Id)!.field;
-        } else {
-          field = FieldWaiting;
+    UpdateFromServer(data: [string, string][]) {
+        for (const key in data) {
+            const element = data[key];
+            this._proxies.set(element[0], element[1]);
         }
-      }
-    } else {
-      let doc: FieldValue<Document> = this;
-      while (doc && doc != FieldWaiting && field != FieldWaiting) {
-        let curField = doc.fields.get(key.Id);
-        let curProxy = doc._proxies.get(key.Id);
-        if (!curField || (curProxy && curField.field.Id !== curProxy)) {
-          if (curProxy) {
-            Server.GetDocumentField(doc, key);
-            /*
+    }
+
+    public Width = () => {
+        return this.GetNumber(KeyStore.Width, 0);
+    };
+    public Height = () => {
+        return this.GetNumber(
+            KeyStore.Height,
+            this.GetNumber(KeyStore.NativeWidth, 0)
+                ? (this.GetNumber(KeyStore.NativeHeight, 0) /
+                    this.GetNumber(KeyStore.NativeWidth, 0)) *
+                this.GetNumber(KeyStore.Width, 0)
+                : 0
+        );
+    };
+    public Scale = () => {
+        return this.GetNumber(KeyStore.Scale, 1);
+    };
+
+    @computed
+    public get Title(): string {
+        let title = this.Get(KeyStore.Title, true);
+        if (title)
+            if (title != FieldWaiting && title instanceof TextField)
+                return title.Data;
+            else return "-waiting-";
+        let parTitle = this.GetT(KeyStore.Title, TextField);
+        if (parTitle)
+            if (parTitle != FieldWaiting) return parTitle.Data + ".alias";
+            else return "-waiting-.alias";
+        return "-untitled-";
+    }
+
+    @computed
+    public get Fields() {
+        return this.fields;
+    }
+
+    /**
+     * Get the field in the document associated with the given key. If the
+     * associated field has not yet been filled in from the server, a request
+     * to the server will automatically be sent, the value will be filled in
+     * when the request is completed, and {@link Field.ts#FieldWaiting} will be returned.
+     * @param key - The key of the value to get
+     * @param ignoreProto - If true, ignore any prototype this document
+     * might have and only search for the value on this immediate document.
+     * If false (default), search up the prototype chain, starting at this document,
+     * for a document that has a field associated with the given key, and return the first
+     * one found.
+     *
+     * @returns If the document does not have a field associated with the given key, returns `undefined`.
+     * If the document does have an associated field, but the field has not been fetched from the server, returns {@link Field.ts#FieldWaiting}.
+     * If the document does have an associated field, and the field has not been fetched from the server, returns the associated field.
+     */
+    Get(key: Key, ignoreProto: boolean = false): FieldValue<Field> {
+        let field: FieldValue<Field>;
+        if (ignoreProto) {
+            if (this.fields.has(key.Id)) {
+                field = this.fields.get(key.Id)!.field;
+            } else if (this._proxies.has(key.Id)) {
+                Server.GetDocumentField(this, key);
+                /*
                         The field might have been instantly filled from the cache
                         Maybe we want to just switch back to returning the value
                         from Server.GetDocumentField if it's in the cache
                         */
-            if (this.fields.has(key.Id)) {
-              field = this.fields.get(key.Id)!.field;
-            } else {
-              field = FieldWaiting;
+                if (this.fields.has(key.Id)) {
+                    field = this.fields.get(key.Id)!.field;
+                } else {
+                    field = FieldWaiting;
+                }
             }
-            break;
-          }
-          if (
-            doc.fields.has(KeyStore.Prototype.Id) ||
-            doc._proxies.has(KeyStore.Prototype.Id)
-          ) {
-            doc = doc.GetPrototype();
-          } else {
-            break;
-          }
         } else {
-          field = curField.field;
-          break;
+            let doc: FieldValue<Document> = this;
+            while (doc && doc != FieldWaiting && field != FieldWaiting) {
+                let curField = doc.fields.get(key.Id);
+                let curProxy = doc._proxies.get(key.Id);
+                if (!curField || (curProxy && curField.field.Id !== curProxy)) {
+                    if (curProxy) {
+                        Server.GetDocumentField(doc, key);
+                        /*
+                                    The field might have been instantly filled from the cache
+                                    Maybe we want to just switch back to returning the value
+                                    from Server.GetDocumentField if it's in the cache
+                                    */
+                        if (this.fields.has(key.Id)) {
+                            field = this.fields.get(key.Id)!.field;
+                        } else {
+                            field = FieldWaiting;
+                        }
+                        break;
+                    }
+                    if (
+                        doc.fields.has(KeyStore.Prototype.Id) ||
+                        doc._proxies.has(KeyStore.Prototype.Id)
+                    ) {
+                        doc = doc.GetPrototype();
+                    } else {
+                        break;
+                    }
+                } else {
+                    field = curField.field;
+                    break;
+                }
+            }
+            if (doc == FieldWaiting) field = FieldWaiting;
         }
-      }
-      if (doc == FieldWaiting) field = FieldWaiting;
+
+        return field;
     }
 
-    return field;
-  }
-
-  /**
-   * Tries to get the field associated with the given key, and if there is an
-   * associated field, calls the given callback with that field.
-   * @param key - The key of the value to get
-   * @param callback - A function that will be called with the associated field, if it exists,
-   * once it is fetched from the server (this may be immediately if the field has already been fetched).
-   * Note: The callback will not be called if there is no associated field.
-   * @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise
-   */
-  GetAsync(key: Key, callback: (field: Opt<Field>) => void): void {
-    //TODO: This currently doesn't deal with prototypes
-    let field = this.fields.get(key.Id);
-    if (field && field.field) {
-      callback(field.field);
-    } else if (this._proxies.has(key.Id)) {
-      Server.GetDocumentField(this, key, callback);
-    } else if (this._proxies.has(KeyStore.Prototype.Id)) {
-      this.GetTAsync(KeyStore.Prototype, Document, proto => {
-        if (proto) {
-          proto.GetAsync(key, callback);
+    /**
+     * Tries to get the field associated with the given key, and if there is an
+     * associated field, calls the given callback with that field.
+     * @param key - The key of the value to get
+     * @param callback - A function that will be called with the associated field, if it exists,
+     * once it is fetched from the server (this may be immediately if the field has already been fetched).
+     * Note: The callback will not be called if there is no associated field.
+     * @returns `true` if the field exists on the document and `callback` will be called, and `false` otherwise
+     */
+    GetAsync(key: Key, callback: (field: Opt<Field>) => void): void {
+        //TODO: This currently doesn't deal with prototypes
+        let field = this.fields.get(key.Id);
+        if (field && field.field) {
+            callback(field.field);
+        } else if (this._proxies.has(key.Id)) {
+            Server.GetDocumentField(this, key, callback);
+        } else if (this._proxies.has(KeyStore.Prototype.Id)) {
+            this.GetTAsync(KeyStore.Prototype, Document, proto => {
+                if (proto) {
+                    proto.GetAsync(key, callback);
+                } else {
+                    callback(undefined);
+                }
+            });
         } else {
-          callback(undefined);
+            callback(undefined);
         }
-      });
-    } else {
-      callback(undefined);
     }
-  }
-
-  GetTAsync<T extends Field>(key: Key, ctor: { new (): T }): Promise<Opt<T>>;
-  GetTAsync<T extends Field>(
-    key: Key,
-    ctor: { new (): T },
-    callback: (field: Opt<T>) => void
-  ): void;
-  GetTAsync<T extends Field>(
-    key: Key,
-    ctor: { new (): T },
-    callback?: (field: Opt<T>) => void
-  ): Promise<Opt<T>> | void {
-    let fn = (cb: (field: Opt<T>) => void) => {
-      return this.GetAsync(key, field => {
-        cb(Cast(field, ctor));
-      });
-    };
-    if (callback) {
-      fn(callback);
-    } else {
-      return new Promise(res => fn(res));
+
+    GetTAsync<T extends Field>(key: Key, ctor: { new(): T }): Promise<Opt<T>>;
+    GetTAsync<T extends Field>(
+        key: Key,
+        ctor: { new(): T },
+        callback: (field: Opt<T>) => void
+    ): void;
+    GetTAsync<T extends Field>(
+        key: Key,
+        ctor: { new(): T },
+        callback?: (field: Opt<T>) => void
+    ): Promise<Opt<T>> | void {
+        let fn = (cb: (field: Opt<T>) => void) => {
+            return this.GetAsync(key, field => {
+                cb(Cast(field, ctor));
+            });
+        };
+        if (callback) {
+            fn(callback);
+        } else {
+            return new Promise(res => fn(res));
+        }
     }
-  }
-
-  /**
-   * Same as {@link Document#GetAsync}, except a field of the given type
-   * will be created if there is no field associated with the given key,
-   * or the field associated with the given key is not of the given type.
-   * @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc.
-   */
-  GetOrCreateAsync<T extends Field>(
-    key: Key,
-    ctor: { new (): T },
-    callback: (field: T) => void
-  ): void {
-    //This currently doesn't deal with prototypes
-    if (this._proxies.has(key.Id)) {
-      Server.GetDocumentField(this, key, field => {
-        if (field && field instanceof ctor) {
-          callback(field);
+
+    /**
+     * Same as {@link Document#GetAsync}, except a field of the given type
+     * will be created if there is no field associated with the given key,
+     * or the field associated with the given key is not of the given type.
+     * @param ctor - Constructor of the field type to get. E.g., TextField, ImageField, etc.
+     */
+    GetOrCreateAsync<T extends Field>(
+        key: Key,
+        ctor: { new(): T },
+        callback: (field: T) => void
+    ): void {
+        //This currently doesn't deal with prototypes
+        if (this._proxies.has(key.Id)) {
+            Server.GetDocumentField(this, key, field => {
+                if (field && field instanceof ctor) {
+                    callback(field);
+                } else {
+                    let newField = new ctor();
+                    this.Set(key, newField);
+                    callback(newField);
+                }
+            });
         } else {
-          let newField = new ctor();
-          this.Set(key, newField);
-          callback(newField);
+            let newField = new ctor();
+            this.Set(key, newField);
+            callback(newField);
         }
-      });
-    } else {
-      let newField = new ctor();
-      this.Set(key, newField);
-      callback(newField);
     }
-  }
-
-  /**
-   * Same as {@link Document#Get}, except that it will additionally
-   * check if the field is of the given type.
-   * @param ctor - Constructor of the field type to get. E.g., `TextField`, `ImageField`, etc.
-   * @returns Same as {@link Document#Get}, except will return `undefined`
-   * if there is an associated field but it is of the wrong type.
-   */
-  GetT<T extends Field = Field>(
-    key: Key,
-    ctor: { new (...args: any[]): T },
-    ignoreProto: boolean = false
-  ): FieldValue<T> {
-    var getfield = this.Get(key, ignoreProto);
-    if (getfield != FieldWaiting) {
-      return Cast(getfield, ctor);
+
+    /**
+     * Same as {@link Document#Get}, except that it will additionally
+     * check if the field is of the given type.
+     * @param ctor - Constructor of the field type to get. E.g., `TextField`, `ImageField`, etc.
+     * @returns Same as {@link Document#Get}, except will return `undefined`
+     * if there is an associated field but it is of the wrong type.
+     */
+    GetT<T extends Field = Field>(
+        key: Key,
+        ctor: { new(...args: any[]): T },
+        ignoreProto: boolean = false
+    ): FieldValue<T> {
+        var getfield = this.Get(key, ignoreProto);
+        if (getfield != FieldWaiting) {
+            return Cast(getfield, ctor);
+        }
+        return FieldWaiting;
     }
-    return FieldWaiting;
-  }
-
-  GetOrCreate<T extends Field>(
-    key: Key,
-    ctor: { new (): T },
-    ignoreProto: boolean = false
-  ): T {
-    const field = this.GetT(key, ctor, ignoreProto);
-    if (field && field != FieldWaiting) {
-      return field;
+
+    GetOrCreate<T extends Field>(
+        key: Key,
+        ctor: { new(): T },
+        ignoreProto: boolean = false
+    ): T {
+        const field = this.GetT(key, ctor, ignoreProto);
+        if (field && field != FieldWaiting) {
+            return field;
+        }
+        const newField = new ctor();
+        this.Set(key, newField);
+        return newField;
     }
-    const newField = new ctor();
-    this.Set(key, newField);
-    return newField;
-  }
-
-  GetData<T, U extends Field & { Data: T }>(
-    key: Key,
-    ctor: { new (): U },
-    defaultVal: T
-  ): T {
-    let val = this.Get(key);
-    let vval = val && val instanceof ctor ? val.Data : defaultVal;
-    return vval;
-  }
-
-  GetHtml(key: Key, defaultVal: string): string {
-    return this.GetData(key, HtmlField, defaultVal);
-  }
-
-  GetNumber(key: Key, defaultVal: number): number {
-    return this.GetData(key, NumberField, defaultVal);
-  }
-
-  GetText(key: Key, defaultVal: string): string {
-    return this.GetData(key, TextField, defaultVal);
-  }
-
-  GetList<T extends Field>(key: Key, defaultVal: T[]): T[] {
-    return this.GetData<T[], ListField<T>>(key, ListField, defaultVal);
-  }
-
-  @action
-  Set(key: Key, field: Field | undefined, setOnPrototype = false): void {
-    let old = this.fields.get(key.Id);
-    let oldField = old ? old.field : undefined;
-    if (setOnPrototype) {
-      this.SetOnPrototype(key, field);
-    } else {
-      if (field) {
-        this.fields.set(key.Id, { key, field });
-        this._proxies.set(key.Id, field.Id);
-        // Server.AddDocumentField(this, key, field);
-      } else {
-        this.fields.delete(key.Id);
-        this._proxies.delete(key.Id);
-        // Server.DeleteDocumentField(this, key);
-      }
-      Server.UpdateField(this);
+
+    GetData<T, U extends Field & { Data: T }>(
+        key: Key,
+        ctor: { new(): U },
+        defaultVal: T
+    ): T {
+        let val = this.Get(key);
+        let vval = val && val instanceof ctor ? val.Data : defaultVal;
+        return vval;
     }
-    if (oldField || field) {
-      UndoManager.AddEvent({
-        undo: () => this.Set(key, oldField, setOnPrototype),
-        redo: () => this.Set(key, field, setOnPrototype)
-      });
+
+    GetHtml(key: Key, defaultVal: string): string {
+        return this.GetData(key, HtmlField, defaultVal);
     }
-  }
-
-  @action
-  SetOnPrototype(key: Key, field: Field | undefined): void {
-    this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
-      f && f.Set(key, field);
-    });
-  }
-
-  @action
-  SetDataOnPrototype<T, U extends Field & { Data: T }>(
-    key: Key,
-    value: T,
-    ctor: { new (): U },
-    replaceWrongType = true
-  ) {
-    this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
-      f && f.SetData(key, value, ctor);
-    });
-  }
-
-  @action
-  SetData<T, U extends Field & { Data: T }>(
-    key: Key,
-    value: T,
-    ctor: { new (data: T): U },
-    replaceWrongType = true
-  ) {
-    let field = this.Get(key, true);
-    if (field instanceof ctor) {
-      field.Data = value;
-    } else if (!field || replaceWrongType) {
-      let newField = new ctor(value);
-      // newField.Data = value;
-      this.Set(key, newField);
+
+    GetBoolean(key: Key, defaultVal: boolean): boolean {
+        return this.GetData(key, BooleanField, defaultVal);
     }
-  }
-
-  @action
-  SetText(key: Key, value: string, replaceWrongType = true) {
-    this.SetData(key, value, TextField, replaceWrongType);
-  }
-
-  @action
-  SetNumber(key: Key, value: number, replaceWrongType = true) {
-    this.SetData(key, value, NumberField, replaceWrongType);
-  }
-
-  GetPrototype(): FieldValue<Document> {
-    return this.GetT(KeyStore.Prototype, Document, true);
-  }
-
-  GetAllPrototypes(): Document[] {
-    let protos: Document[] = [];
-    let doc: FieldValue<Document> = this;
-    while (doc && doc != FieldWaiting) {
-      protos.push(doc);
-      doc = doc.GetPrototype();
+
+    GetNumber(key: Key, defaultVal: number): number {
+        return this.GetData(key, NumberField, defaultVal);
+    }
+
+    GetText(key: Key, defaultVal: string): string {
+        return this.GetData(key, TextField, defaultVal);
+    }
+
+    GetList<T extends Field>(key: Key, defaultVal: T[]): T[] {
+        return this.GetData<T[], ListField<T>>(key, ListField, defaultVal);
+    }
+
+    @action
+    Set(key: Key, field: Field | undefined, setOnPrototype = false): void {
+        let old = this.fields.get(key.Id);
+        let oldField = old ? old.field : undefined;
+        if (setOnPrototype) {
+            this.SetOnPrototype(key, field);
+        } else {
+            if (field) {
+                this.fields.set(key.Id, { key, field });
+                this._proxies.set(key.Id, field.Id);
+                // Server.AddDocumentField(this, key, field);
+            } else {
+                this.fields.delete(key.Id);
+                this._proxies.delete(key.Id);
+                // Server.DeleteDocumentField(this, key);
+            }
+            Server.UpdateField(this);
+        }
+        if (oldField || field) {
+            UndoManager.AddEvent({
+                undo: () => this.Set(key, oldField, setOnPrototype),
+                redo: () => this.Set(key, field, setOnPrototype)
+            });
+        }
+    }
+
+    @action
+    SetOnPrototype(key: Key, field: Field | undefined): void {
+        this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
+            f && f.Set(key, field);
+        });
+    }
+
+    @action
+    SetDataOnPrototype<T, U extends Field & { Data: T }>(
+        key: Key,
+        value: T,
+        ctor: { new(): U },
+        replaceWrongType = true
+    ) {
+        this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
+            f && f.SetData(key, value, ctor);
+        });
+    }
+
+    @action
+    SetData<T, U extends Field & { Data: T }>(
+        key: Key,
+        value: T,
+        ctor: { new(data: T): U },
+        replaceWrongType = true
+    ) {
+        let field = this.Get(key, true);
+        if (field instanceof ctor) {
+            field.Data = value;
+        } else if (!field || replaceWrongType) {
+            let newField = new ctor(value);
+            // newField.Data = value;
+            this.Set(key, newField);
+        }
+    }
+
+    @action
+    SetText(key: Key, value: string, replaceWrongType = true) {
+        this.SetData(key, value, TextField, replaceWrongType);
+    }
+
+    @action
+    SetNumber(key: Key, value: number, replaceWrongType = true) {
+        this.SetData(key, value, NumberField, replaceWrongType);
+    }
+
+    GetPrototype(): FieldValue<Document> {
+        return this.GetT(KeyStore.Prototype, Document, true);
+    }
+
+    GetAllPrototypes(): Document[] {
+        let protos: Document[] = [];
+        let doc: FieldValue<Document> = this;
+        while (doc && doc != FieldWaiting) {
+            protos.push(doc);
+            doc = doc.GetPrototype();
+        }
+        return protos;
+    }
+
+    CreateAlias(id?: string): Document {
+        let alias = new Document(id);
+        this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
+            f && alias.Set(KeyStore.Prototype, f);
+        });
+
+        return alias;
+    }
+
+    MakeDelegate(id?: string): Document {
+        let delegate = new Document(id);
+
+        delegate.Set(KeyStore.Prototype, this);
+
+        return delegate;
+    }
+
+    ToScriptString(): string {
+        return "";
+    }
+
+    TrySetValue(value: any): boolean {
+        throw new Error("Method not implemented.");
+    }
+    GetValue() {
+        return this.Title;
+        var title =
+            (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) +
+            "(" +
+            this.Id +
+            ")";
+        return title;
+        //throw new Error("Method not implemented.");
+    }
+    Copy(): Field {
+        throw new Error("Method not implemented.");
+    }
+
+    ToJson(): { type: Types; data: [string, string][]; _id: string } {
+        let fields: [string, string][] = [];
+        this._proxies.forEach((field, key) => {
+            if (field) {
+                fields.push([key, field as string]);
+            }
+        });
+
+        return {
+            type: Types.Document,
+            data: fields,
+            _id: this.Id
+        };
     }
-    return protos;
-  }
-
-  CreateAlias(id?: string): Document {
-    let alias = new Document(id);
-    this.GetTAsync(KeyStore.Prototype, Document, (f: Opt<Document>) => {
-      f && alias.Set(KeyStore.Prototype, f);
-    });
-
-    return alias;
-  }
-
-  MakeDelegate(id?: string): Document {
-    let delegate = new Document(id);
-
-    delegate.Set(KeyStore.Prototype, this);
-
-    return delegate;
-  }
-
-  ToScriptString(): string {
-    return "";
-  }
-
-  TrySetValue(value: any): boolean {
-    throw new Error("Method not implemented.");
-  }
-  GetValue() {
-    return this.Title;
-    var title =
-      (this._proxies.has(KeyStore.Title.Id) ? "???" : this.Title) +
-      "(" +
-      this.Id +
-      ")";
-    return title;
-    //throw new Error("Method not implemented.");
-  }
-  Copy(): Field {
-    throw new Error("Method not implemented.");
-  }
-
-  ToJson(): { type: Types; data: [string, string][]; _id: string } {
-    let fields: [string, string][] = [];
-    this._proxies.forEach((field, key) => {
-      if (field) {
-        fields.push([key, field as string]);
-      }
-    });
-
-    return {
-      type: Types.Document,
-      data: fields,
-      _id: this.Id
-    };
-  }
 }
-- 
cgit v1.2.3-70-g09d2


From e70be930dc7c8ff1e999bf163bcbb511a60bae6f Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 4 Apr 2019 17:46:33 -0400
Subject: added close button and fixed some async stuff.

---
 src/client/views/DocumentDecorations.scss          | 17 +++++++---
 src/client/views/DocumentDecorations.tsx           | 38 +++++++++++++++++++---
 .../views/collections/CollectionViewBase.tsx       |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 +++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  1 +
 src/client/views/nodes/PDFBox.tsx                  | 10 +++---
 6 files changed, 60 insertions(+), 17 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 080c9fa1e..d7137d7a2 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -7,7 +7,7 @@
     display: grid;
     z-index: 1000;
     grid-template-rows: 20px 8px 1fr 8px;
-    grid-template-columns: 8px 8px 1fr 8px;
+    grid-template-columns: 8px 8px 1fr 8px 8px;
     pointer-events: none;
 
     #documentDecorations-centerCont {
@@ -30,13 +30,14 @@
     #documentDecorations-topResizer,
     #documentDecorations-bottomResizer {
         grid-column-start: 2;
-        grid-column-end: 4;
+        grid-column-end: 5;
     }
 
     #documentDecorations-bottomRightResizer,
     #documentDecorations-topRightResizer,
     #documentDecorations-rightResizer {
-        grid-column:4
+        grid-column-start:5;
+        grid-column-end:7;
     }
 
     #documentDecorations-topLeftResizer,
@@ -61,11 +62,19 @@
     .title{
         background: lightblue;
         grid-column-start:3;
-        grid-column-end: 5;
+        grid-column-end: 4;
         pointer-events: auto;
     }
 }
 
+.documentDecorations-closeButton {
+    background:$alt-accent;
+    opacity: 0.8;
+    grid-column-start: 4;
+    grid-column-end: 6;
+    pointer-events: all;
+    text-align: center;
+}
 .documentDecorations-minimizeButton {
     background:$alt-accent;
     opacity: 0.8;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index a46087c9a..8bf1a42d1 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -142,6 +142,30 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.preventDefault();
     }
 
+    onCloseDown = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        if (e.button === 0) {
+            document.removeEventListener("pointermove", this.onCloseMove);
+            document.addEventListener("pointermove", this.onCloseMove);
+            document.removeEventListener("pointerup", this.onCloseUp);
+            document.addEventListener("pointerup", this.onCloseUp);
+        }
+    }
+    onCloseMove = (e: PointerEvent): void => {
+        e.stopPropagation();
+        if (e.button === 0) {
+        }
+    }
+    @action
+    onCloseUp = (e: PointerEvent): void => {
+        e.stopPropagation();
+        if (e.button === 0) {
+            SelectionManager.SelectedDocuments().map(dv => dv.props.RemoveDocument && dv.props.RemoveDocument(dv.props.Document));
+            SelectionManager.DeselectAll();
+            document.removeEventListener("pointermove", this.onCloseMove);
+            document.removeEventListener("pointerup", this.onCloseUp);
+        }
+    }
     onMinimizeDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
         if (e.button === 0) {
@@ -219,7 +243,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.stopPropagation();
     }
 
-    onLinkButtonMoved = (e: PointerEvent): void => {
+    onLinkButtonMoved = async (e: PointerEvent) => {
         if (this._linkButton.current != null) {
             document.removeEventListener("pointermove", this.onLinkButtonMoved)
             document.removeEventListener("pointerup", this.onLinkButtonUp);
@@ -234,10 +258,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     (linkDoc.GetT(KeyStore.LinkedFromDocs, Document)) as Document) : [];
             draggedDocs.push(...draggedFromDocs);
             if (draggedDocs.length) {
-                let dragData = new DragManager.DocumentDragData(draggedDocs.map(d => {
-                    let annot = d.GetT(KeyStore.AnnotationOn, Document); // bcz: ... needs to change ... the annotationOn document may not have been retrieved yet...
-                    return annot && annot != FieldWaiting ? annot : d;
-                }));
+                let moddrag = [] as Document[];
+                for (let i = 0; i < draggedDocs.length; i++) {
+                    let doc = await draggedDocs[i].GetTAsync(KeyStore.AnnotationOn, Document);
+                    if (doc)
+                        moddrag.push(doc);
+                }
+                let dragData = new DragManager.DocumentDragData(moddrag);
                 DragManager.StartDocumentDrag([this._linkButton.current], dragData, {
                     handlers: {
                         dragComplete: action(() => { }),
@@ -400,6 +427,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             }}>
                 <div className="documentDecorations-minimizeButton" onPointerDown={this.onMinimizeDown}>...</div>
                 <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this.getValue()} onChange={this.handleChange} onPointerDown={this.onPointerDown} onKeyPress={this.enterPressed} />
+                <div className="documentDecorations-closeButton" onPointerDown={this.onCloseDown}>X</div>
                 <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>
diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx
index daeca69e2..9eee23a1d 100644
--- a/src/client/views/collections/CollectionViewBase.tsx
+++ b/src/client/views/collections/CollectionViewBase.tsx
@@ -128,6 +128,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
         }
         if (type.indexOf("pdf") !== -1) {
             ctor = Documents.PdfDocument;
+            options.nativeWidth = 600;
         }
         if (type.indexOf("html") !== -1) {
             if (path.includes('localhost')) {
@@ -177,7 +178,6 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps>
             let item = e.dataTransfer.items[i];
             if (item.kind === "string" && item.type.indexOf("uri") != -1) {
                 e.dataTransfer.items[i].getAsString(action((s: string) => {
-                    let document: Document;
                     request.head(ServerUtils.prepend(RouteStore.corsProxy + "/" + s), (err, res, body) => {
                         let type = res.headers["content-type"];
                         if (type) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3484e963e..1ddb84a99 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -20,6 +20,7 @@ import React = require("react");
 import v5 = require("uuid/v5");
 import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCursors";
 import { PreviewCursor } from "./PreviewCursor";
+import { NumberField } from "../../../../fields/NumberField";
 
 @observer
 export class CollectionFreeFormView extends CollectionViewBase {
@@ -83,13 +84,17 @@ export class CollectionFreeFormView extends CollectionViewBase {
                 let dragDoc = de.data.droppedDocuments[0];
                 let dragX = dragDoc.GetNumber(KeyStore.X, 0);
                 let dragY = dragDoc.GetNumber(KeyStore.Y, 0);
-                droppedDocs.map(d => {
+                droppedDocs.map(async 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)) {
+                    let docW = await d.GetTAsync(KeyStore.Width, NumberField);
+                    let docH = await d.GetTAsync(KeyStore.Height, NumberField);
+                    if (!docW) {
                         d.SetNumber(KeyStore.Width, 300);
+                    }
+                    if (!docH) {
                         d.SetNumber(KeyStore.Height, 300);
                     }
                     this.bringToFront(d);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index e2239c8be..df150a045 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -135,6 +135,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             this.marqueeInkDelete(inkData);
             // }, 100);
             this.cleanupInteractions();
+            SelectionManager.DeselectAll();
         }
     }
     @action
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 3cb3cf782..c9fed7f66 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, keys } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from "mobx-react";
 import 'react-image-lightbox/style.css';
 import Measure from "react-measure";
@@ -10,6 +10,7 @@ import { FieldWaiting, Opt } from '../../../fields/Field';
 import { ImageField } from '../../../fields/ImageField';
 import { KeyStore } from '../../../fields/KeyStore';
 import { PDFField } from '../../../fields/PDFField';
+import { RouteStore } from "../../../server/RouteStore";
 import { Utils } from '../../../Utils';
 import { Annotation } from './Annotation';
 import { FieldView, FieldViewProps } from './FieldView';
@@ -17,8 +18,6 @@ import "./ImageBox.scss";
 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, 
@@ -378,8 +377,9 @@ export class PDFBox extends React.Component<FieldViewProps> {
     saveThumbnail = () => {
         setTimeout(() => {
             var me = this;
-            htmlToImage.toPng(this._mainDiv.current!,
-                { width: me.props.doc.GetNumber(KeyStore.NativeWidth, 0), height: me.props.doc.GetNumber(KeyStore.NativeHeight, 0), quality: 0.5 })
+            let nwidth = me.props.doc.GetNumber(KeyStore.NativeWidth, 0);
+            let nheight = me.props.doc.GetNumber(KeyStore.NativeHeight, 0);
+            htmlToImage.toPng(this._mainDiv.current!, { width: nwidth, height: nheight, quality: 1 })
                 .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));
-- 
cgit v1.2.3-70-g09d2

