From 460cec4d786e026dabdb8fb873284f6574799367 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 3 Oct 2019 17:52:35 -0400
Subject: start of embedding documents  in text notes.

---
 src/client/views/DocumentButtonBar.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

(limited to 'src/client/views/DocumentButtonBar.tsx')

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 338f6b83e..e57745b86 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -203,7 +203,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
     considerEmbed = () => {
         let thisDoc = this.props.views[0].props.Document;
         let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
-        if (!canEmbed) return (null);
+        // if (!canEmbed) return (null);
         return (
             <div className="linkButtonWrapper">
                 <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
-- 
cgit v1.2.3-70-g09d2


From 54f2067dbadb66e22249c1572bdc5d6d097f41d1 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 4 Oct 2019 17:01:09 -0400
Subject: restored tooltiptextmenu

---
 src/client/util/RichTextRules.ts            |  8 +++--
 src/client/util/RichTextSchema.tsx          |  1 -
 src/client/util/SelectionManager.ts         |  2 --
 src/client/util/TooltipTextMenu.tsx         | 45 +++++++++++++++--------------
 src/client/views/DocumentButtonBar.tsx      |  1 -
 src/client/views/DocumentDecorations.tsx    |  4 +--
 src/client/views/nodes/FormattedTextBox.tsx | 35 +++++-----------------
 7 files changed, 40 insertions(+), 56 deletions(-)

(limited to 'src/client/views/DocumentButtonBar.tsx')

diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index cd37ea0bb..2d5ec1743 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -92,8 +92,10 @@ export const inpRules = {
                 let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "left";
+                    return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
                 }
-                return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+                return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                    state.tr;
             }),
         new InputRule(
             new RegExp(/^\]\]\s$/),
@@ -104,8 +106,10 @@ export const inpRules = {
                 let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "right";
+                    return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
                 }
-                return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+                return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                    state.tr;
             }),
         new InputRule(
             new RegExp(/\^f\s$/),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 53eaf9ce2..948a3c5bd 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -864,7 +864,6 @@ export class FootnoteView {
         if (this.innerView) this.close();
     }
     open() {
-        if (!this.outerView.isOverlay) return;
         // Append a tooltip to the outer node
         let tooltip = this.dom.appendChild(document.createElement("div"));
         tooltip.className = "footnote-tooltip";
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a02a270ee..df1b46b33 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -27,7 +27,6 @@ export namespace SelectionManager {
             } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
                 manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
                 manager.SelectedDocuments = [docView];
-                FormattedTextBox.InputBoxOverlay = undefined;
             }
         }
         @action
@@ -42,7 +41,6 @@ export namespace SelectionManager {
         DeselectAll(): void {
             manager.SelectedDocuments.map(dv => dv.props.whenActiveChanged(false));
             manager.SelectedDocuments = [];
-            FormattedTextBox.InputBoxOverlay = undefined;
         }
     }
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index a83a3949d..9116ef995 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -10,7 +10,6 @@ import { Doc, Field, Opt } from "../../new_fields/Doc";
 import { Id } from "../../new_fields/FieldSymbols";
 import { Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import { FieldViewProps } from "../views/nodes/FieldView";
 import { FormattedTextBoxProps } from "../views/nodes/FormattedTextBox";
 import { DocumentManager } from "./DocumentManager";
@@ -28,10 +27,10 @@ export class TooltipTextMenu {
 
     public tooltip: HTMLElement;
     private view: EditorView;
+    private editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
     private fontStyles: MarkType[];
     private fontSizes: MarkType[];
     private listTypes: (NodeType | any)[];
-    private editorProps: FieldViewProps & FormattedTextBoxProps;
     private fontSizeToNum: Map<MarkType, number>;
     private fontStylesToName: Map<MarkType, string>;
     private listTypeToIcon: Map<NodeType | any, string>;
@@ -59,9 +58,8 @@ export class TooltipTextMenu {
 
     private _collapsed: boolean = false;
 
-    constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
+    constructor(view: EditorView) {
         this.view = view;
-        this.editorProps = editorProps;
 
         this.wrapper = document.createElement("div");
         this.tooltip = document.createElement("div");
@@ -120,10 +118,10 @@ export class TooltipTextMenu {
             //pointer down handler to activate button effects
             dom.addEventListener("pointerdown", e => {
                 e.preventDefault();
-                view.focus();
+                this.view.focus();
                 if (dom.contains(e.target as Node)) {
                     e.stopPropagation();
-                    command(view.state, view.dispatch, view);
+                    command(this.view.state, this.view.dispatch, this.view);
                     // if (this.view.state.selection.empty) {
                     //     if (dom.style.color === "white") { dom.style.color = "greenyellow"; }
                     //     else { dom.style.color = "white"; }
@@ -188,12 +186,10 @@ export class TooltipTextMenu {
 
         this.updateListItemDropdown(":", this.listTypeBtnDom);
 
-        this.update(view, undefined);
-
-        // add tooltip to outerdiv to circumvent scaling problem
-        const outer_div = this.editorProps.outer_div;
-        outer_div && outer_div(this.wrapper);
+        this.update(view, undefined, undefined);
+        TooltipTextMenu.Toolbar = this.wrapper;
     }
+    public static Toolbar: HTMLDivElement | undefined;
 
     //label of dropdown will change to given label
     updateFontSizeDropdown(label: string) {
@@ -275,7 +271,7 @@ export class TooltipTextMenu {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
                                     DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
                                 }
-                                else this.editorProps.addDocTab(f, undefined, "onRight");
+                                else this.editorProps && this.editorProps.addDocTab(f, undefined, "onRight");
                             }
                         }));
                     }
@@ -293,6 +289,7 @@ export class TooltipTextMenu {
             this.linkDrag.style.background = "black";
             this.linkDrag.style.cssFloat = "left";
             this.linkDrag.onpointerdown = (e: PointerEvent) => {
+                if (!this.editorProps) return;
                 let dragData = new DragManager.LinkDragData(this.editorProps.Document);
                 dragData.dontClearTextBox = true;
                 // hack to get source context -sy
@@ -503,19 +500,23 @@ export class TooltipTextMenu {
             if (markType.name[0] === 'p') {
                 let size = this.fontSizeToNum.get(markType);
                 if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
-                let ruleProvider = this.editorProps.ruleProvider;
-                let heading = NumCast(this.editorProps.Document.heading);
-                if (ruleProvider && heading) {
-                    ruleProvider["ruleSize_" + heading] = size;
+                if (this.editorProps) {
+                    let ruleProvider = this.editorProps.ruleProvider;
+                    let heading = NumCast(this.editorProps.Document.heading);
+                    if (ruleProvider && heading) {
+                        ruleProvider["ruleSize_" + heading] = size;
+                    }
                 }
             }
             else {
                 let fontName = this.fontStylesToName.get(markType);
                 if (fontName) { this.updateFontStyleDropdown(fontName); }
-                let ruleProvider = this.editorProps.ruleProvider;
-                let heading = NumCast(this.editorProps.Document.heading);
-                if (ruleProvider && heading) {
-                    ruleProvider["ruleFont_" + heading] = fontName;
+                if (this.editorProps) {
+                    let ruleProvider = this.editorProps.ruleProvider;
+                    let heading = NumCast(this.editorProps.Document.heading);
+                    if (ruleProvider && heading) {
+                        ruleProvider["ruleFont_" + heading] = fontName;
+                    }
                 }
             }
             //actually apply font
@@ -849,8 +850,10 @@ export class TooltipTextMenu {
     }
 
     //updates the tooltip menu when the selection changes
-    update(view: EditorView, lastState: EditorState | undefined) {
+    update(view: EditorView, lastState: EditorState | undefined, props: any) {
+        this.view = view;
         let state = view.state;
+        props && (this.editorProps = props);
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) &&
             lastState.selection.eq(state.selection)) return;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index e57745b86..9e2d41621 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -140,7 +140,6 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
             let selDoc = this.props.views[0];
             let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined;
             let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
-            FormattedTextBox.InputBoxOverlay = undefined;
             this._linkDrag = UndoManager.StartBatch("Drag Link");
             DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
                 handlers: {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 26ffaf3a6..b7ec27957 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -24,6 +24,7 @@ import { FieldView } from "./nodes/FieldView";
 import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { IconBox } from "./nodes/IconBox";
 import React = require("react");
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -466,7 +467,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 break;
         }
 
-        if (!this._resizing) runInAction(() => FormattedTextBox.InputBoxOverlay = undefined);
         SelectionManager.SelectedDocuments().forEach(element => {
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 let doc = PositionDocument(element.props.Document);
@@ -578,7 +578,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 zIndex: SelectionManager.SelectedDocuments().length > 1 ? 900 : 0,
             }} onPointerDown={this.onBackgroundDown} onContextMenu={(e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); }} >
             </div>
-            <div className="documentDecorations-container" style={{
+            <div className="documentDecorations-container" ref={(r) => r && TooltipTextMenu.Toolbar && Array.from(r.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && r.appendChild(TooltipTextMenu.Toolbar)} style={{
                 width: (bounds.r - bounds.x + this._resizeBorderWidth) + "px",
                 height: (bounds.b - bounds.y + this._resizeBorderWidth + this._linkBoxHeight + this._titleHeight + 3) + "px",
                 left: bounds.x - this._resizeBorderWidth / 2,
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 05904e1e7..cbdb0503b 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -116,8 +116,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         return "";
     }
 
-    public static getToolTip() {
-        return this._toolTipTextMenu;
+    public static getToolTip(ev: EditorView) {
+        return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev, undefined);
     }
 
     @undoBatch
@@ -143,29 +143,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
-    // this should be internal to prosemirror, but is needed
-    // here to make sure that footnote view nodes in the overlay editor
-    // get removed when they're not selected.
-
-    syncNodeSelection(view: any, sel: any) {
-        if (sel instanceof NodeSelection) {
-            var desc = view.docView.descAt(sel.from);
-            if (desc !== view.lastSelectedViewDesc) {
-                if (view.lastSelectedViewDesc) {
-                    view.lastSelectedViewDesc.deselectNode();
-                    view.lastSelectedViewDesc = null;
-                }
-                if (desc) { desc.selectNode(); }
-                view.lastSelectedViewDesc = desc;
-            }
-        } else {
-            if (view.lastSelectedViewDesc) {
-                view.lastSelectedViewDesc.deselectNode();
-                view.lastSelectedViewDesc = null;
-            }
-        }
-    }
-
     linkOnDeselect: Map<string, string> = new Map();
 
     doLinkOnDeselect() {
@@ -211,7 +188,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
             const state = this._editorView.state.apply(tx);
             this._editorView.updateState(state);
-            this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected
             if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
@@ -808,8 +784,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    static InputBoxOverlay: FormattedTextBox | undefined;
     @action
     onFocused = (e: React.FocusEvent): void => {
+        FormattedTextBox.InputBoxOverlay = this;
         document.removeEventListener("keypress", this.recordKeyHandler);
         document.addEventListener("keypress", this.recordKeyHandler);
         this.tryUpdateHeight();
@@ -901,7 +879,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let self = FormattedTextBox;
         return new Plugin({
             view(_editorView) {
-                return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
+                return self._toolTipTextMenu = FormattedTextBox.getToolTip(_editorView);
             }
         });
     }
@@ -955,6 +933,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
             ? "none" : "all";
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+        if (this.props.isSelected()) {
+            FormattedTextBox._toolTipTextMenu!.update(this._editorView!, undefined, this.props);
+        }
         return (
             <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
                 style={{
-- 
cgit v1.2.3-70-g09d2


From 57ccb3e236a7dad71848a06dad757c5a5a081ce8 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 16 Oct 2019 12:34:47 -0400
Subject: starting to switch document embedding to alias dragging.

---
 src/client/util/DragManager.ts                      |  1 +
 src/client/views/DocumentButtonBar.tsx              | 21 ++++++++++-----------
 src/client/views/collections/CollectionSubView.tsx  |  3 +++
 .../collectionFreeForm/CollectionFreeFormView.tsx   | 15 +++++++++++++++
 src/client/views/nodes/FormattedTextBox.tsx         | 13 +++++--------
 5 files changed, 34 insertions(+), 19 deletions(-)

(limited to 'src/client/views/DocumentButtonBar.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index dcd19e50b..29a75b9ae 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -311,6 +311,7 @@ export namespace DragManager {
             this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
         }
         embeddableSourceDoc: Doc;
+        embeddedDoc?: Doc;
         urlField?: URLField;
         [id: string]: any;
     }
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 9e2d41621..5561d699c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -164,8 +164,12 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
 
             let dragDocView = this.props.views[0];
             let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
+            const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
+            dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.clientX - left, e.clientY - top);
 
             DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+                offsetX: dragData.offset[0],
+                offsetY: dragData.offset[1],
                 handlers: {
                     dragComplete: action(emptyFunction),
                 },
@@ -199,17 +203,12 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
         e.stopPropagation();
     }
 
-    considerEmbed = () => {
-        let thisDoc = this.props.views[0].props.Document;
-        let canEmbed = thisDoc.data && thisDoc.data instanceof URLField;
-        // if (!canEmbed) return (null);
-        return (
-            <div className="linkButtonWrapper">
-                <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
-                </div>
+    embedDragger = () => {
+        return (<div className="linkButtonWrapper">
+            <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
+                <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
             </div>
-        );
+        </div>);
     }
 
     private get targetDoc() {
@@ -353,7 +352,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
                 <TemplateMenu docs={this.props.views} templates={templates} />
             </div>
             {this.metadataMenu}
-            {this.considerEmbed()}
+            {this.embedDragger()}
             {this.considerGoogleDocsPush()}
             {this.considerGoogleDocsPull()}
             <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9919a9dc3..077142e88 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -143,6 +143,9 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 e.stopPropagation();
                 return added;
             }
+            else if (de.data instanceof DragManager.EmbedDragData) {
+                return this.props.addDocument(de.data.embeddedDoc = Doc.MakeAlias(de.data.embeddableSourceDoc));
+            }
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 e.stopPropagation();
                 return this.props.addDocument(de.data.dropDocument);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index adbad5da5..46aaed509 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -153,6 +153,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]);
                 }
             }
+            else if (de.data instanceof DragManager.EmbedDragData && de.data.embeddedDoc) {
+                const d = de.data.embeddedDoc;
+                d.x = xp;
+                d.y = yp;
+                if (!NumCast(d.width)) {
+                    d.width = 300;
+                }
+                if (!NumCast(d.height)) {
+                    let nw = NumCast(d.nativeWidth);
+                    let nh = NumCast(d.nativeHeight);
+                    d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+                }
+                this.bringToFront(d);
+                this.updateCluster(d);
+            }
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 if (de.data.dropDocument) {
                     let dragDoc = de.data.dropDocument;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 1bdff3ec7..8153cf61c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -249,16 +249,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @undoBatch
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
-        // We're dealing with a link to a document
-        if (de.data instanceof DragManager.EmbedDragData) {
-            let target = de.data.embeddableSourceDoc;
-            // We're dealing with an internal document drop
-            const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
-            let alias = Doc.MakeAlias(target);
-            alias.fitToBox = true;
+        if (de.data instanceof DragManager.EmbedDragData || (de.data instanceof DragManager.DocumentDragData && de.data.userDropAction)) {
+            let target = de.data instanceof DragManager.DocumentDragData ? de.data.droppedDocuments[0] : Doc.MakeAlias(de.data.embeddableSourceDoc);
+            const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
+            target.fitToBox = true;
             let node = schema.nodes.dashDoc.create({
                 width: target[WidthSym](), height: target[HeightSym](),
-                title: "dashDoc", docid: alias[Id],
+                title: "dashDoc", docid: target[Id],
                 float: "right"
             });
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
-- 
cgit v1.2.3-70-g09d2


From fb817995eb94727b11324f298d0a30eebda8dcb7 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 16 Oct 2019 12:47:09 -0400
Subject: removed DragEmbed -- subsumed by dragging aliases.

---
 src/client/util/DragManager.ts                     | 18 ++--------
 src/client/views/DocumentButtonBar.tsx             | 39 +++++++++++-----------
 src/client/views/collections/CollectionSubView.tsx |  3 --
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 15 ---------
 src/client/views/nodes/FormattedTextBox.tsx        |  4 +--
 5 files changed, 23 insertions(+), 56 deletions(-)

(limited to 'src/client/views/DocumentButtonBar.tsx')

diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 29a75b9ae..12e0b11ba 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -211,6 +211,7 @@ export namespace DragManager {
         offset: number[];
         dropAction: dropActionType;
         userDropAction: dropActionType;
+        forceUserDropAction: dropActionType;
         moveDocument?: MoveFunction;
         isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged.  this will suppress onDragStart scripts
         applyAsTemplate?: boolean;
@@ -305,17 +306,6 @@ export namespace DragManager {
         [id: string]: any;
     }
 
-    export class EmbedDragData {
-        constructor(embeddableSourceDoc: Doc) {
-            this.embeddableSourceDoc = embeddableSourceDoc;
-            this.urlField = embeddableSourceDoc.data instanceof URLField ? embeddableSourceDoc.data : undefined;
-        }
-        embeddableSourceDoc: Doc;
-        embeddedDoc?: Doc;
-        urlField?: URLField;
-        [id: string]: any;
-    }
-
     // for column dragging in schema view
     export class ColumnDragData {
         constructor(colKey: SchemaHeaderField) {
@@ -329,10 +319,6 @@ export namespace DragManager {
         StartDrag([ele], dragData, downX, downY, options);
     }
 
-    export function StartEmbedDrag(ele: HTMLElement, dragData: EmbedDragData, downX: number, downY: number, options?: DragOptions) {
-        StartDrag([ele], dragData, downX, downY, options);
-    }
-
     export function StartColumnDrag(ele: HTMLElement, dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) {
         StartDrag([ele], dragData, downX, downY, options);
     }
@@ -428,7 +414,7 @@ export namespace DragManager {
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (dragData instanceof DocumentDragData) {
-                dragData.userDropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
+                dragData.userDropAction = dragData.forceUserDropAction ? dragData.forceUserDropAction : e.ctrlKey || e.altKey ? "alias" : undefined;
             }
             if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
                 AbortDrag();
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 5561d699c..a00a4298f 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -6,7 +6,6 @@ import { observer } from "mobx-react";
 import { Doc } from "../../new_fields/Doc";
 import { RichTextField } from '../../new_fields/RichTextField';
 import { NumCast } from "../../new_fields/Types";
-import { URLField } from '../../new_fields/URLField';
 import { emptyFunction } from "../../Utils";
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
 import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
@@ -45,7 +44,7 @@ const fetch: IconProp = "sync-alt";
 export class DocumentButtonBar extends React.Component<{ views: DocumentView[], stack?: any }, {}> {
     private _linkButton = React.createRef<HTMLDivElement>();
     private _linkerButton = React.createRef<HTMLDivElement>();
-    private _embedButton = React.createRef<HTMLDivElement>();
+    private _aliasButton = React.createRef<HTMLDivElement>();
     private _tooltipoff = React.createRef<HTMLDivElement>();
     private _textDoc?: Doc;
     private _linkDrag?: UndoManager.Batch;
@@ -111,13 +110,13 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
         document.addEventListener("pointerup", this.onLinkerButtonUp);
     }
 
-    onEmbedButtonDown = (e: React.PointerEvent): void => {
+    onAliasButtonDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
         e.preventDefault();
-        document.removeEventListener("pointermove", this.onEmbedButtonMoved);
-        document.addEventListener("pointermove", this.onEmbedButtonMoved);
-        document.removeEventListener("pointerup", this.onEmbedButtonUp);
-        document.addEventListener("pointerup", this.onEmbedButtonUp);
+        document.removeEventListener("pointermove", this.onAliasButtonMoved);
+        document.addEventListener("pointermove", this.onAliasButtonMoved);
+        document.removeEventListener("pointerup", this.onAliasButtonUp);
+        document.addEventListener("pointerup", this.onAliasButtonUp);
     }
 
     onLinkerButtonUp = (e: PointerEvent): void => {
@@ -126,9 +125,9 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
         e.stopPropagation();
     }
 
-    onEmbedButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onEmbedButtonMoved);
-        document.removeEventListener("pointerup", this.onEmbedButtonUp);
+    onAliasButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onAliasButtonMoved);
+        document.removeEventListener("pointerup", this.onAliasButtonUp);
         e.stopPropagation();
     }
 
@@ -157,17 +156,17 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
     }
 
     @action
-    onEmbedButtonMoved = (e: PointerEvent): void => {
-        if (this._embedButton.current !== null) {
-            document.removeEventListener("pointermove", this.onEmbedButtonMoved);
-            document.removeEventListener("pointerup", this.onEmbedButtonUp);
+    onAliasButtonMoved = (e: PointerEvent): void => {
+        if (this._aliasButton.current !== null) {
+            document.removeEventListener("pointermove", this.onAliasButtonMoved);
+            document.removeEventListener("pointerup", this.onAliasButtonUp);
 
             let dragDocView = this.props.views[0];
-            let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
+            let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
             const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
             dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.clientX - left, e.clientY - top);
-
-            DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+            dragData.forceUserDropAction = "alias";
+            DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, e.x, e.y, {
                 offsetX: dragData.offset[0],
                 offsetY: dragData.offset[1],
                 handlers: {
@@ -203,9 +202,9 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
         e.stopPropagation();
     }
 
-    embedDragger = () => {
+    aliasDragger = () => {
         return (<div className="linkButtonWrapper">
-            <div title="Drag Embed" className="linkButton-linker" ref={this._embedButton} onPointerDown={this.onEmbedButtonDown}>
+            <div title="Drag Alias" className="linkButton-linker" ref={this._aliasButton} onPointerDown={this.onAliasButtonDown}>
                 <FontAwesomeIcon className="documentdecorations-icon" icon="image" size="sm" />
             </div>
         </div>);
@@ -352,7 +351,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
                 <TemplateMenu docs={this.props.views} templates={templates} />
             </div>
             {this.metadataMenu}
-            {this.embedDragger()}
+            {this.aliasDragger()}
             {this.considerGoogleDocsPush()}
             {this.considerGoogleDocsPull()}
             <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 077142e88..9919a9dc3 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -143,9 +143,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 e.stopPropagation();
                 return added;
             }
-            else if (de.data instanceof DragManager.EmbedDragData) {
-                return this.props.addDocument(de.data.embeddedDoc = Doc.MakeAlias(de.data.embeddableSourceDoc));
-            }
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 e.stopPropagation();
                 return this.props.addDocument(de.data.dropDocument);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 46aaed509..adbad5da5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -153,21 +153,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]);
                 }
             }
-            else if (de.data instanceof DragManager.EmbedDragData && de.data.embeddedDoc) {
-                const d = de.data.embeddedDoc;
-                d.x = xp;
-                d.y = yp;
-                if (!NumCast(d.width)) {
-                    d.width = 300;
-                }
-                if (!NumCast(d.height)) {
-                    let nw = NumCast(d.nativeWidth);
-                    let nh = NumCast(d.nativeHeight);
-                    d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
-                }
-                this.bringToFront(d);
-                this.updateCluster(d);
-            }
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 if (de.data.dropDocument) {
                     let dragDoc = de.data.dropDocument;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8153cf61c..ae67e94cc 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -249,8 +249,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @undoBatch
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
-        if (de.data instanceof DragManager.EmbedDragData || (de.data instanceof DragManager.DocumentDragData && de.data.userDropAction)) {
-            let target = de.data instanceof DragManager.DocumentDragData ? de.data.droppedDocuments[0] : Doc.MakeAlias(de.data.embeddableSourceDoc);
+        if (de.data instanceof DragManager.DocumentDragData && de.data.userDropAction) {
+            let target = de.data.droppedDocuments[0];
             const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
             target.fitToBox = true;
             let node = schema.nodes.dashDoc.create({
-- 
cgit v1.2.3-70-g09d2


From 8a3cfa8d6e72c9bfea4b38760e0b138b6525574c Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 16 Oct 2019 16:42:37 -0400
Subject: a bunch of fixes to layouts to support templates in paticular.

---
 src/client/util/DocumentManager.ts                 |   2 +
 src/client/util/DragManager.ts                     |   4 +-
 src/client/views/CollectionLinearView.tsx          |   7 +-
 src/client/views/DocumentButtonBar.tsx             |   3 +-
 src/client/views/DocumentDecorations.tsx           |  47 +++++-----
 src/client/views/InkingControl.tsx                 |   9 +-
 .../views/collections/CollectionStackingView.tsx   |  11 ++-
 .../views/collections/CollectionTreeView.scss      |   1 +
 .../views/collections/CollectionTreeView.tsx       |  17 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  63 ++++++-------
 .../collections/collectionFreeForm/MarqueeView.tsx |  31 ++++---
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   4 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/FontIconBox.tsx             |   5 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 101 +++++++++++----------
 src/new_fields/Doc.ts                              |  11 ++-
 16 files changed, 178 insertions(+), 144 deletions(-)

(limited to 'src/client/views/DocumentButtonBar.tsx')

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 00de39671..e23ac55c2 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -72,6 +72,8 @@ export class DocumentManager {
                         toReturn = view;
                     }
                 });
+            } else {
+                break;
             }
         }
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 12e0b11ba..06dab024e 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -211,7 +211,7 @@ export namespace DragManager {
         offset: number[];
         dropAction: dropActionType;
         userDropAction: dropActionType;
-        forceUserDropAction: dropActionType;
+        embedDoc?: boolean;
         moveDocument?: MoveFunction;
         isSelectionMove?: boolean; // indicates that an explicitly selected Document is being dragged.  this will suppress onDragStart scripts
         applyAsTemplate?: boolean;
@@ -414,7 +414,7 @@ export namespace DragManager {
         const moveHandler = (e: PointerEvent) => {
             e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop
             if (dragData instanceof DocumentDragData) {
-                dragData.userDropAction = dragData.forceUserDropAction ? dragData.forceUserDropAction : e.ctrlKey || e.altKey ? "alias" : undefined;
+                dragData.userDropAction = e.ctrlKey ? "alias" : undefined;
             }
             if (((options && !options.withoutShiftDrag) || !options) && e.shiftKey && CollectionDockingView.Instance) {
                 AbortDrag();
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index af3b194ea..44d9b042e 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -15,6 +15,7 @@ import { CollectionViewType } from './collections/CollectionBaseView';
 import { CollectionSubView } from './collections/CollectionSubView';
 import { documentSchema, DocumentView } from './nodes/DocumentView';
 import { translate } from 'googleapis/build/src/apis/translate';
+import { DocumentType } from '../documents/DocumentTypes';
 
 
 type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -50,8 +51,10 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
         (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => {
             let dbox = doc;
             if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) {
-                dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" });
-                dbox.dragFactory = doc;
+                let template = doc.layout instanceof Doc && doc.layout.isTemplate ? doc.layout : doc;
+                template.isTemplate = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction;
+                dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplate ? "font" : "bolt" });
+                dbox.dragFactory = template;
                 dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
                 dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
             } else if (doc.viewType === CollectionViewType.Linear) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index a00a4298f..959b120ed 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -165,7 +165,8 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
             let dragData = new DragManager.DocumentDragData([dragDocView.props.Document]);
             const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
             dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.clientX - left, e.clientY - top);
-            dragData.forceUserDropAction = "alias";
+            dragData.embedDoc = true;
+            dragData.dropAction = "alias";
             DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, e.x, e.y, {
                 offsetX: dragData.offset[0],
                 offsetY: dragData.offset[1],
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 755739a11..2f40ea746 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -471,47 +471,48 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         SelectionManager.SelectedDocuments().forEach(element => {
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 let doc = PositionDocument(element.props.Document);
+                let layoutDoc = PositionDocument(element.props.Document.layout instanceof Doc ? element.props.Document.layout : element.props.Document);
                 let nwidth = doc.nativeWidth || 0;
                 let nheight = doc.nativeHeight || 0;
-                let width = (doc.width || 0);
-                let height = (doc.height || (nheight / nwidth * width));
+                let width = (layoutDoc.width || 0);
+                let height = (layoutDoc.height || (nheight / nwidth * width));
                 let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
                 let actualdW = Math.max(width + (dW * scale), 20);
                 let actualdH = Math.max(height + (dH * scale), 20);
-                doc.x = (doc.x || 0) + dX * (actualdW - width);
-                doc.y = (doc.y || 0) + dY * (actualdH - height);
+                layoutDoc.x = (layoutDoc.x || 0) + dX * (actualdW - width);
+                layoutDoc.y = (layoutDoc.y || 0) + dY * (actualdH - height);
                 let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
-                let fixedAspect = e.ctrlKey || (!doc.ignoreAspect && nwidth && nheight);
-                if (fixedAspect && e.ctrlKey && doc.ignoreAspect) {
-                    doc.ignoreAspect = false;
-                    proto.nativeWidth = nwidth = doc.width || 0;
-                    proto.nativeHeight = nheight = doc.height || 0;
+                let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
+                if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
+                    layoutDoc.ignoreAspect = false;
+                    proto.nativeWidth = nwidth = layoutDoc.width || 0;
+                    proto.nativeHeight = nheight = layoutDoc.height || 0;
                 }
                 if (fixedAspect && (!nwidth || !nheight)) {
-                    proto.nativeWidth = nwidth = doc.width || 0;
-                    proto.nativeHeight = nheight = doc.height || 0;
+                    proto.nativeWidth = nwidth = layoutDoc.width || 0;
+                    proto.nativeHeight = nheight = layoutDoc.height || 0;
                 }
-                if (nwidth > 0 && nheight > 0 && !BoolCast(doc.ignoreAspect)) {
+                if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
                     if (Math.abs(dW) > Math.abs(dH)) {
                         if (!fixedAspect) {
-                            Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
+                            Doc.SetInPlace(doc, "nativeWidth", actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0), true);
                         }
-                        doc.width = actualdW;
-                        if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width;
-                        else doc.height = actualdH;
+                        layoutDoc.width = actualdW;
+                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
+                        else layoutDoc.height = actualdH;
                     }
                     else {
                         if (!fixedAspect) {
-                            Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
+                            Doc.SetInPlace(doc, "nativeHeight", actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0), true);
                         }
-                        doc.height = actualdH;
-                        if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height;
-                        else doc.width = actualdW;
+                        layoutDoc.height = actualdH;
+                        if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
+                        else layoutDoc.width = actualdW;
                     }
                 } else {
-                    dW && (doc.width = actualdW);
-                    dH && (doc.height = actualdH);
-                    dH && element.props.Document.autoHeight && Doc.SetInPlace(element.props.Document, "autoHeight", false, true);
+                    dW && (layoutDoc.width = actualdW);
+                    dH && (layoutDoc.height = actualdH);
+                    dH && layoutDoc.autoHeight && Doc.SetInPlace(layoutDoc, "autoHeight", false, true);
                 }
             }
         });
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 51fc7ca8f..46c6fae1c 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -41,7 +41,9 @@ export class InkingControl {
         if (InkingControl.Instance.selectedTool === InkTool.None) {
             let selected = SelectionManager.SelectedDocuments();
             let oldColors = selected.map(view => {
-                let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+                let targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
+                    view.props.Document.layout instanceof Doc ? view.props.Document.layout :
+                        view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
                 let sel = window.getSelection();
                 if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
                     targetDoc.color = this._selectedColor;
@@ -79,7 +81,10 @@ export class InkingControl {
                     ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
                     ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
                 }
-                !ruleProvider && (targetDoc.backgroundColor = matchedColor);
+                if (!ruleProvider) {
+                    if (targetDoc)
+                        targetDoc.backgroundColor = matchedColor;
+                }
 
                 return {
                     target: targetDoc,
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 90fc9c3e0..cde1a5036 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -110,9 +110,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     componentDidMount() {
-        // is there any reason this needs to exist? -syip.  yes, it handles autoHeight for stacking views (masonry isn't yet supported).
         this._heightDisposer = reaction(() => {
-            if (BoolCast(this.props.Document.autoHeight)) {
+            if (this.props.Document.autoHeight) {
                 let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
                 if (this.isStackingView) {
                     return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght,
@@ -188,15 +187,17 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     getDocHeight(d?: Doc) {
         if (!d) return 0;
+        let layoutDoc = d.layout instanceof Doc ? d.layout : d;
         let nw = NumCast(d.nativeWidth);
         let nh = NumCast(d.nativeHeight);
         let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
-        if (!d.ignoreAspect && !d.fitWidth && nw && nh) {
+        if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
             let aspect = nw && nh ? nh / nw : 1;
-            if (!(d.nativeWidth && !d.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(d[WidthSym](), wid);
+            if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
             return wid * aspect;
         }
-        return d.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin : Math.min(wid * NumCast(d.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : d[HeightSym]();
+        return layoutDoc.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
+            Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
     }
 
     columnDividerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index ca0c321b7..bff8ce5c1 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -16,6 +16,7 @@
     background: $light-color-secondary;
     font-size: 13px;
     overflow: auto;
+    cursor: default;
 
     ul {
         list-style: none;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index abaa9662c..403da0e54 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -251,19 +251,21 @@ class TreeView extends React.Component<TreeViewProps> {
     }
     docWidth = () => {
         let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
-        if (aspect) return Math.min(this.props.document[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
-        return NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
+        let layoutDoc = this.props.document.layout instanceof Doc ? this.props.document.layout : this.props.document;
+        if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
+        return NumCast(this.props.document.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
     }
     docHeight = () => {
         let bounds = this.boundsOfCollectionDocument;
         return Math.min(this.MAX_EMBED_HEIGHT, (() => {
             let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
+            let layoutDoc = this.props.document.layout instanceof Doc ? this.props.document.layout : this.props.document;
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
-            return this.props.document.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
-                Math.min(this.docWidth() * NumCast(this.props.document.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth,
+            return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
+                Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth,
                     NumCast(this.props.containingCollection.height)))) :
-                NumCast(this.props.document.height) ? NumCast(this.props.document.height) : 50;
+                NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50;
         })());
     }
 
@@ -461,10 +463,11 @@ class TreeView extends React.Component<TreeViewProps> {
 
         let rowWidth = () => panelWidth() - 20;
         return docs.map((child, i) => {
-            let pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
+            const pair = Doc.GetLayoutDataDocPair(containingCollection, dataDoc, key, child);
             if (!pair.layout || pair.data instanceof Promise) {
                 return (null);
             }
+            const childLayout = pair.layout.layout instanceof Doc ? pair.layout.layout : pair.layout;
 
             let indent = i === 0 ? undefined : () => {
                 if (StrCast(docs[i - 1].layout).indexOf("fieldKey") !== -1) {
@@ -482,7 +485,7 @@ class TreeView extends React.Component<TreeViewProps> {
             };
             let rowHeight = () => {
                 let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
-                return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
+                return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
             };
             return <TreeView
                 document={pair.layout}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index adbad5da5..229e7fffc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -30,7 +30,6 @@ import { DocumentContentsView } from "../../nodes/DocumentContentsView";
 import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
 import { pageSchema } from "../../nodes/ImageBox";
-import PDFMenu from "../../pdf/PDFMenu";
 import { CollectionSubView } from "../CollectionSubView";
 import { computePivotLayout, ViewDefResult } from "./CollectionFreeFormLayoutEngines";
 import { CollectionFreeFormLinksView } from "./CollectionFreeFormLinksView";
@@ -131,21 +130,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (super.drop(e, de)) {
             if (de.data instanceof DragManager.DocumentDragData) {
                 if (de.data.droppedDocuments.length) {
-                    let z = NumCast(de.data.droppedDocuments[0].z);
+                    let firstLayoutDoc = de.data.droppedDocuments[0].layout instanceof Doc ? de.data.droppedDocuments[0].layout : de.data.droppedDocuments[0];
+                    let z = NumCast(firstLayoutDoc.z);
                     let x = (z ? xpo : xp) - de.data.offset[0];
                     let y = (z ? ypo : yp) - de.data.offset[1];
-                    let dropX = NumCast(de.data.droppedDocuments[0].x);
-                    let dropY = NumCast(de.data.droppedDocuments[0].y);
+                    let dropX = NumCast(firstLayoutDoc.x);
+                    let dropY = NumCast(firstLayoutDoc.y);
                     de.data.droppedDocuments.forEach(action((d: Doc) => {
-                        d.x = x + NumCast(d.x) - dropX;
-                        d.y = y + NumCast(d.y) - dropY;
-                        if (!NumCast(d.width)) {
-                            d.width = 300;
+                        let layoutDoc = d.layout instanceof Doc ? d.layout : d;
+                        layoutDoc.x = x + NumCast(layoutDoc.x) - dropX;
+                        layoutDoc.y = y + NumCast(layoutDoc.y) - dropY;
+                        if (!NumCast(layoutDoc.width)) {
+                            layoutDoc.width = 300;
                         }
                         if (!NumCast(d.height)) {
                             let nw = NumCast(d.nativeWidth);
                             let nh = NumCast(d.nativeHeight);
-                            d.height = nw && nh ? nh / nw * NumCast(d.width) : 300;
+                            layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
                         }
                         this.bringToFront(d);
                     }));
@@ -156,14 +157,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 if (de.data.dropDocument) {
                     let dragDoc = de.data.dropDocument;
+                    let layoutDoc = dragDoc.layout instanceof Doc ? dragDoc.layout : dragDoc;
                     let x = xp - de.data.offset[0];
                     let y = yp - de.data.offset[1];
-                    let dropX = NumCast(de.data.dropDocument.x);
-                    let dropY = NumCast(de.data.dropDocument.y);
-                    dragDoc.x = x + NumCast(dragDoc.x) - dropX;
-                    dragDoc.y = y + NumCast(dragDoc.y) - dropY;
-                    de.data.targetContext = this.props.Document;
-                    dragDoc.targetContext = this.props.Document;
+                    let dropX = NumCast(layoutDoc.x);
+                    let dropY = NumCast(layoutDoc.y);
+                    layoutDoc.x = x + NumCast(layoutDoc.x) - dropX;
+                    layoutDoc.y = y + NumCast(layoutDoc.y) - dropY;
+                    de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
                     this.bringToFront(dragDoc);
                 }
             }
@@ -173,11 +174,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     pickCluster(probe: number[]) {
         return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
-            let cx = NumCast(cd.x) - this._clusterDistance;
-            let cy = NumCast(cd.y) - this._clusterDistance;
-            let cw = NumCast(cd.width) + 2 * this._clusterDistance;
-            let ch = NumCast(cd.height) + 2 * this._clusterDistance;
-            return !cd.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
+            let layoutDoc = cd.layout instanceof Doc ? cd.layout : cd;
+            let cx = NumCast(layoutDoc.x) - this._clusterDistance;
+            let cy = NumCast(layoutDoc.y) - this._clusterDistance;
+            let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
+            let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
+            return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
                 NumCast(cd.cluster) : cluster;
         }, -1);
     }
@@ -185,14 +187,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let cluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
         if (cluster !== -1) {
             let eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => NumCast(cd.cluster) === cluster);
-
-            // hacky way to get a list of DocumentViews in the current view given a list of Documents in the current view
-            let prevSelected = SelectionManager.SelectedDocuments();
-            this.selectDocuments(eles);
-            let clusterDocs = SelectionManager.SelectedDocuments();
-            SelectionManager.DeselectAll();
-            prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
-
+            let clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.CollectionView)!);
             let de = new DragManager.DocumentDragData(eles);
             de.moveDocument = this.props.moveDocument;
             const [left, top] = clusterDocs[0].props.ScreenToLocalTransform().scale(clusterDocs[0].props.ContentScaling()).inverse().transformPoint(0, 0);
@@ -223,8 +218,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
             let preferredInd = NumCast(doc.cluster);
             doc.cluster = -1;
+            let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
             this._clusterSets.map((set, i) => set.map(member => {
-                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
+                let memberLayout = member.layout instanceof Doc ? member.layout : member;
+                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(layoutDoc, memberLayout, this._clusterDistance)) {
                     doc.cluster = i;
                 }
             }));
@@ -296,10 +293,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
             let x = this.Document.panX || 0;
             let y = this.Document.panY || 0;
-            let docs = this.childLayoutPairs.map(pair => pair.layout);
+            let docs = this.childLayoutPairs.map(pair => pair.layout.layout instanceof Doc ? pair.layout.layout : pair.layout);
             let [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
             if (!this.isAnnotationOverlay) {
-                PDFMenu.Instance.fadeOut(true);
                 let minx = docs.length ? NumCast(docs[0].x) : 0;
                 let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
                 let miny = docs.length ? NumCast(docs[0].y) : 0;
@@ -549,7 +545,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
 
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
-            const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
+            let finalLayout = pair.layout.layout instanceof Doc ? pair.layout.layout : pair.layout;
+            const pos = this.getCalculatedPositions({ doc: finalLayout, index: i, collection: this.Document, docs: layoutDocs, state });
             state = pos.state === undefined ? state : pos.state;
             layoutPoolData.set(pair, pos);
         });
@@ -592,7 +589,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             let i = 0;
             const width = Math.max(...docs.map(doc => NumCast(doc.width)));
             const height = Math.max(...docs.map(doc => NumCast(doc.height)));
-            for (const doc of docs) {
+            for (const doc of docs.map(d => d.layout instanceof Doc ? d.layout : d)) {
                 doc.x = x;
                 doc.y = y;
                 x += width + 20;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index ecdd02b0f..1362736cf 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             let bounds = this.Bounds;
             let selected = this.marqueeSelect(false);
             if (e.key === "c") {
-                selected.map(d => {
+                selected.map(d => d.layout instanceof Doc ? d.layout : d).map(d => {
                     this.props.removeDocument(d);
                     d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
                     d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
@@ -325,7 +325,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             this.marqueeInkDelete(inkData);
 
             if (e.key === "s" || e.key === "S") {
-                selected.map(d => {
+                selected.map(d => d.layout instanceof Doc ? d.layout : d).map(d => {
                     this.props.removeDocument(d);
                     d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
                     d.y = NumCast(d.y) - bounds.top - bounds.height / 2;
@@ -394,20 +394,22 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         let selRect = this.Bounds;
         let selection: Doc[] = [];
         this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
-            var x = NumCast(doc.x);
-            var y = NumCast(doc.y);
-            var w = NumCast(doc.width);
-            var h = NumCast(doc.height);
+            let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
+            var x = NumCast(layoutDoc.x);
+            var y = NumCast(layoutDoc.y);
+            var w = NumCast(layoutDoc.width);
+            var h = NumCast(layoutDoc.height);
             if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
                 selection.push(doc);
             }
         });
         if (!selection.length && selectBackgrounds) {
             this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
-                var x = NumCast(doc.x);
-                var y = NumCast(doc.y);
-                var w = NumCast(doc.width);
-                var h = NumCast(doc.height);
+                let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
+                var x = NumCast(layoutDoc.x);
+                var y = NumCast(layoutDoc.y);
+                var w = NumCast(layoutDoc.width);
+                var h = NumCast(layoutDoc.height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
                     selection.push(doc);
                 }
@@ -420,10 +422,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
             let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
             this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
-                var x = NumCast(doc.x);
-                var y = NumCast(doc.y);
-                var w = NumCast(doc.width);
-                var h = NumCast(doc.height);
+                let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
+                var x = NumCast(layoutDoc.x);
+                var y = NumCast(layoutDoc.y);
+                var w = NumCast(layoutDoc.width);
+                var h = NumCast(layoutDoc.height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
                     selection.push(doc);
                 }
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 93f6dc468..5e8ac3ecd 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -36,8 +36,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
     get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : this.Document.x || 0; }
     get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : this.Document.y || 0; }
-    get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.props.Document[WidthSym](); }
-    get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.props.Document[HeightSym](); }
+    get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
+    get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); }
     @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
     @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
     @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index ebd0ca549..a071f782a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -224,7 +224,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             maxLocation = this.Document.maximizeLocation = (!ctrlKey ? !altKey ? maxLocation : (maxLocation !== "inPlace" ? "inPlace" : "onRight") : (maxLocation !== "inPlace" ? "inPlace" : "inTab"));
             if (maxLocation === "inPlace") {
                 expandedDocs.forEach(maxDoc => this.props.addDocument && this.props.addDocument(maxDoc));
-                let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+                let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.layoutDoc.width) / 2, NumCast(this.layoutDoc.height) / 2);
                 DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
             } else {
                 expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
@@ -392,7 +392,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (!anchors.find(anchor2 => anchor2 && anchor2.title === this.Document.title + ".portal" ? true : false)) {
             let portalID = (this.Document.title + ".portal").replace(/^-/, "").replace(/\([0-9]*\)$/, "");
             DocServer.GetRefField(portalID).then(existingPortal => {
-                let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID });
+                let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.layoutDoc.width || 0) + 10, height: this.layoutDoc.height || 0, title: portalID });
                 DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
                 this.Document.isButton = true;
             });
@@ -491,7 +491,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
         }
         layoutItems.push({ description: `${this.Document.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.Document.chromeStatus = (this.Document.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
-        layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.Document.autoHeight = !this.Document.autoHeight, icon: "plus" });
+        layoutItems.push({ description: `${this.Document.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
         layoutItems.push({ description: this.Document.ignoreAspect || !this.Document.nativeWidth || !this.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
         layoutItems.push({ description: this.Document.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.Document.lockedPosition) ? "unlock" : "lock" });
         layoutItems.push({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index 848afccf3..c5bf28d5b 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -8,6 +8,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { StrCast } from '../../../new_fields/Types';
 import { Utils } from "../../../Utils";
 import { runInAction, observable, reaction, IReactionDisposer } from 'mobx';
+import { Doc } from '../../../new_fields/Doc';
 const FontIconSchema = createSchema({
     icon: "string"
 });
@@ -35,8 +36,10 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
         this._backgroundReaction && this._backgroundReaction();
     }
     render() {
+        let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
+        let referenceLayout = referenceDoc.layout instanceof Doc ? referenceDoc.layout : referenceDoc;
         return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
-            style={{ background: StrCast(this.props.Document.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}>
+            style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}>
             <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" />
         </button>;
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ae67e94cc..f157a953e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -127,7 +127,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let view = this._editorView!;
         if (view.state.selection.from === view.state.selection.to) return false;
         if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
-            this.props.Document.color = color;
+            this.layoutDoc.color = color;
         }
         let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
         view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
@@ -143,7 +143,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
 
-    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+
+    // the document containing the view layout information - will be the Document itself unless the Document has
+    // a layout field.  In that case, all layout information comes from there unless overriden by Document
+    @computed get layoutDoc(): Doc {
+        return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+    }
 
     linkOnDeselect: Map<string, string> = new Map();
 
@@ -249,39 +255,41 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @undoBatch
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
-        if (de.data instanceof DragManager.DocumentDragData && de.data.userDropAction) {
-            let target = de.data.droppedDocuments[0];
-            const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
-            target.fitToBox = true;
-            let node = schema.nodes.dashDoc.create({
-                width: target[WidthSym](), height: target[HeightSym](),
-                title: "dashDoc", docid: target[Id],
-                float: "right"
-            });
-            let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
-            link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
-            this.tryUpdateHeight();
-            e.stopPropagation();
-        } else if (de.data instanceof DragManager.DocumentDragData) {
+        if (de.data instanceof DragManager.DocumentDragData) {
             const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
-            if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document)) {
-                if (de.mods === "AltKey") {
-                    if (draggedDoc.data instanceof RichTextField) {
-                        Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
-                        e.stopPropagation();
-                    }
-                } else if (de.mods === "CtrlKey") {
-                    draggedDoc.isTemplate = true;
-                    if (typeof (draggedDoc.layout) === "string") {
-                        let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
-                        layoutDelegateToOverrideFieldKey.layout = StrCast(layoutDelegateToOverrideFieldKey.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
-                        this.props.Document.layout = layoutDelegateToOverrideFieldKey;
-                    } else {
-                        this.props.Document.layout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
-                    }
+            // replace text contents whend dragging with Alt
+            if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "AltKey") {
+                if (draggedDoc.data instanceof RichTextField) {
+                    Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
+                    e.stopPropagation();
+                }
+                // apply as template when dragging with Meta
+            } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") {
+                draggedDoc.isTemplate = true;
+                let newLayout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
+                if (typeof (draggedDoc.layout) === "string") {
+                    newLayout = Doc.MakeDelegate(draggedDoc);
+                    newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
                 }
+                this.props.Document.layout = newLayout;
                 e.stopPropagation();
-            }
+                // embed document when dragging with a userDropAction or an embedDoc flag set
+            } else if (de.data.userDropAction || de.data.embedDoc) {
+                let target = de.data.droppedDocuments[0];
+                const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
+                if (link) {
+                    target.fitToBox = true;
+                    let node = schema.nodes.dashDoc.create({
+                        width: target[WidthSym](), height: target[HeightSym](),
+                        title: "dashDoc", docid: target[Id],
+                        float: "right"
+                    });
+                    let view = this._editorView!;
+                    view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
+                    this.tryUpdateHeight();
+                    e.stopPropagation();
+                }
+            } // otherwise, fall through to outer collection to handle drop
         }
     }
 
@@ -486,7 +494,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         );
 
         this._heightReactionDisposer = reaction(
-            () => [this.props.Document[WidthSym](), this.props.Document.autoHeight],
+            () => [this.layoutDoc[WidthSym](), this.layoutDoc.autoHeight],
             () => this.tryUpdateHeight()
         );
 
@@ -505,7 +513,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
 
         this._searchReactionDisposer = reaction(() => {
-            return StrCast(this.props.Document.search_string);
+            return StrCast(this.layoutDoc.search_string);
         }, searchString => {
             if (searchString) {
                 this.highlightSearchTerms([searchString]);
@@ -518,7 +526,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
         this._rulesReactionDisposer = reaction(() => {
             let ruleProvider = this.props.ruleProvider;
-            let heading = NumCast(this.props.Document.heading);
+            let heading = NumCast(this.layoutDoc.heading);
             if (ruleProvider instanceof Doc) {
                 return {
                     align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
@@ -530,7 +538,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         },
             action((rules: any) => {
                 this._fontFamily = rules ? rules.font : "Arial";
-                this._fontSize = rules ? rules.size : NumCast(this.props.Document.fontSize, 13);
+                this._fontSize = rules ? rules.size : NumCast(this.layoutDoc.fontSize, 13);
                 rules && setTimeout(() => {
                     const view = this._editorView!;
                     if (this._proseRef) {
@@ -547,7 +555,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }), { fireImmediately: true }
         );
         this._scrollToRegionReactionDisposer = reaction(
-            () => StrCast(this.props.Document.scrollToLinkID),
+            () => StrCast(this.layoutDoc.scrollToLinkID),
             async (scrollToLinkID) => {
                 let findLinkFrag = (frag: Fragment, editor: EditorView) => {
                     const nodes: Node[] = [];
@@ -585,7 +593,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
                         setTimeout(() => this.unhighlightSearchTerms(), 2000);
                     }
-                    this.props.Document.scrollToLinkID = undefined;
+                    this.layoutDoc.scrollToLinkID = undefined;
                 }
 
             },
@@ -885,7 +893,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         });
                     }
                 } else {
-                    let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
+                    let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
                     this.props.addDocument && this.props.addDocument(webDoc);
                 }
                 e.stopPropagation();
@@ -983,19 +991,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @action
     tryUpdateHeight() {
         let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
-        if (!this.props.Document.isAnimating && this.props.Document.autoHeight && scrollHeight !== 0 &&
+        if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
             let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
-            let dh = NumCast(this.props.Document.height, 0);
-            this.props.Document.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
+            let dh = NumCast(this.layoutDoc.height, 0);
+            this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
             this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
         }
     }
 
     render() {
         let style = "hidden";
-        let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
-        let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
+        let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
+        let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground
             ? "none" : "all";
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         if (this.props.isSelected()) {
@@ -1004,8 +1012,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         return (
             <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
                 style={{
-                    overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
-                    height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
+                    overflowY: "auto",
+                    maxHeight: "100%",
+                    height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
                     background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
                     opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                     color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 9e0944c69..21d4d16f6 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -563,7 +563,12 @@ export namespace Doc {
 
     let _applyCount: number = 0;
     export function ApplyTemplate(templateDoc: Doc) {
-        return !templateDoc ? undefined : ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+        if (templateDoc) {
+            let applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+            applied && (Doc.GetProto(applied).layout = applied.layout);
+            return applied;
+        }
+        return undefined;
     }
     export function ApplyTemplateTo(templateDoc: Doc, target: Doc, titleTarget: string | undefined = undefined) {
         if (!templateDoc) {
@@ -578,7 +583,7 @@ export namespace Doc {
         let layoutCustom = Doc.MakeTitled("layoutCustom");
         let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
 
-        titleTarget && (target.title = titleTarget);
+        titleTarget && (Doc.GetProto(target).title = titleTarget);
         target.type = DocumentType.TEMPLATE;
         target.width = templateDoc.width;
         target.height = templateDoc.height;
@@ -734,7 +739,7 @@ export namespace Doc {
 Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
 Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
 Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
-Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return Doc.MakeCopy(doc, copyProto); });
+Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplate ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
 Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
 Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
 Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
-- 
cgit v1.2.3-70-g09d2


From dcdefb2a5a80d3c4d5451d6c7fc7213565d5ea5f Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 17 Oct 2019 16:02:37 -0400
Subject: initial working version of draggable link anchors.

---
 src/Utils.ts                             |  23 +++++++
 src/client/util/DragManager.ts           |   4 +-
 src/client/views/DocumentButtonBar.tsx   |  10 +--
 src/client/views/DocumentDecorations.tsx |   3 +-
 src/client/views/InkingControl.tsx       |  12 ++--
 src/client/views/nodes/DocuLinkView.tsx  |  85 +++++++++++++++++++++++
 src/client/views/nodes/DocumentView.tsx  | 114 +++++++------------------------
 7 files changed, 145 insertions(+), 106 deletions(-)
 create mode 100644 src/client/views/nodes/DocuLinkView.tsx

(limited to 'src/client/views/DocumentButtonBar.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 6889936b8..66ed8be5d 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -149,6 +149,29 @@ export namespace Utils {
     }
 
 
+    export function clamp(n: number, lower: number, upper: number) {
+        return Math.max(lower, Math.min(upper, n));
+    }
+
+    export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
+        var r = l + w,
+            b = t + h;
+
+        var x = clamp(x, l, r),
+            y = clamp(y, t, b);
+
+        var dl = Math.abs(x - l),
+            dr = Math.abs(x - r),
+            dt = Math.abs(y - t),
+            db = Math.abs(y - b);
+
+        var m = Math.min(dl, dr, dt, db);
+
+        return (m === dt) ? [x, t] :
+            (m === db) ? [x, b] :
+                (m === dl) ? [l, y] : [r, y];
+    }
+
     export function GetClipboardText(): string {
         var textArea = document.createElement("textarea");
         document.body.appendChild(textArea);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 06dab024e..576b16bc8 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -87,12 +87,12 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
     }
 }
 
-export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
+export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc, singleLink?: Doc) {
     let srcTarg = sourceDoc.proto;
     let draggedDocs: Doc[] = [];
 
     if (srcTarg) {
-        let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
+        let linkDocs = singleLink ? [singleLink] : LinkManager.Instance.getAllRelatedLinks(srcTarg);
         if (linkDocs) {
             draggedDocs = linkDocs.map(link => {
                 let opp = LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 959b120ed..ba87ecfb4 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -47,7 +47,6 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
     private _aliasButton = React.createRef<HTMLDivElement>();
     private _tooltipoff = React.createRef<HTMLDivElement>();
     private _textDoc?: Doc;
-    private _linkDrag?: UndoManager.Batch;
     public static Instance: DocumentButtonBar;
 
     constructor(props: { views: DocumentView[] }) {
@@ -139,15 +138,10 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
             let selDoc = this.props.views[0];
             let container = selDoc.props.ContainingCollectionDoc ? selDoc.props.ContainingCollectionDoc.proto : undefined;
             let dragData = new DragManager.LinkDragData(selDoc.props.Document, container ? [container] : []);
-            this._linkDrag = UndoManager.StartBatch("Drag Link");
+            let _linkDrag = UndoManager.StartBatch("Drag Link");
             DragManager.StartLinkDrag(this._linkerButton.current, dragData, e.pageX, e.pageY, {
                 handlers: {
-                    dragComplete: () => {
-                        if (this._linkDrag) {
-                            this._linkDrag.end();
-                            this._linkDrag = undefined;
-                        }
-                    },
+                    dragComplete: () => _linkDrag && _linkDrag.end()
                 },
                 hideSource: false
             });
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6e8ba2d3d..927729487 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -167,7 +167,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     get Bounds(): { x: number, y: number, b: number, r: number } {
         let x = this._forceUpdate;
         this._lastBox = SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
-            if (documentView.props.renderDepth === 0 ||
+            if (documentView._selectedLink !== -1 ||
+                documentView.props.renderDepth === 0 ||
                 Doc.AreProtosEqual(documentView.props.Document, CurrentUserUtils.UserDocument)) {
                 return bounds;
             }
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index b8f3c2875..1aacdc8ec 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -81,18 +81,18 @@ export class InkingControl {
                     ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
                     ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
                 }
-                (!ruleProvider && targetDoc) && (targetDoc.backgroundColor = matchedColor);
+                (!ruleProvider && targetDoc) && (view.setBackround(matchedColor));
 
                 return {
                     target: targetDoc,
                     previous: oldColor
                 };
             });
-            let captured = this._selectedColor;
-            UndoManager.AddEvent({
-                undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
-                redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
-            });
+            //let captured = this._selectedColor;
+            // UndoManager.AddEvent({
+            //     undo: () => oldColors.forEach(pair => pair.target.backgroundColor = pair.previous),
+            //     redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
+            // });
         } else {
             CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
         }
diff --git a/src/client/views/nodes/DocuLinkView.tsx b/src/client/views/nodes/DocuLinkView.tsx
new file mode 100644
index 000000000..622d41047
--- /dev/null
+++ b/src/client/views/nodes/DocuLinkView.tsx
@@ -0,0 +1,85 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { Doc, Opt } from "../../../new_fields/Doc";
+import { NumCast, StrCast } from "../../../new_fields/Types";
+import { Utils } from '../../../Utils';
+import { DocumentManager } from "../../util/DocumentManager";
+import "./DocumentView.scss";
+import React = require("react");
+import { DragManager, DragLinksAsDocuments } from "../../util/DragManager";
+import { UndoManager } from "../../util/UndoManager";
+
+
+interface DocuLinkViewProps {
+    Document: Doc;
+    isSelected: () => boolean;
+    addDocTab: (doc: Doc, dataDoc: Opt<Doc>, where: string) => void;
+    anchor: string;
+    otherAnchor: string;
+    scale: () => number;
+    contentDiv: HTMLDivElement | null;
+    link: Doc;
+    index: number;
+    selectedLink: () => number;
+    selectLink: (id: number) => void;
+    blacklist: Opt<Doc>
+}
+
+@observer
+export class DocuLinkView extends React.Component<DocuLinkViewProps> {
+    _downx = 0;
+    _downy = 0;
+    @observable _x = 0;
+    @observable _y = 0;
+    @observable _selected = false;
+    _ref = React.createRef<HTMLDivElement>();
+
+    onPointerDown = (e: React.PointerEvent) => {
+        this._downx = e.clientX;
+        this._downy = e.clientY;
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        document.addEventListener("pointermove", this.onPointerMove);
+        document.addEventListener("pointerup", this.onPointerUp);
+        e.stopPropagation();
+    }
+    onPointerMove = action((e: PointerEvent) => {
+        if (this.props.contentDiv && (Math.abs(e.clientX - this._downx) > 5 || Math.abs(e.clientY - this._downy) > 5)) {
+            let bounds = this.props.contentDiv.getBoundingClientRect();
+            let pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
+            let separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY));
+            let dragdist = Math.sqrt((pt[0] - this._downx) * (pt[0] - this._downx) + (pt[1] - this._downy) * (pt[1] - this._downy))
+            if (separation > 100) {
+                DragLinksAsDocuments(this._ref.current!, pt[0], pt[1], this.props.Document, this.props.link);
+                document.removeEventListener("pointermove", this.onPointerMove);
+                document.removeEventListener("pointerup", this.onPointerUp);
+            } else if (dragdist > separation) {
+                this.props.link[this.props.anchor + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
+                this.props.link[this.props.anchor + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
+            }
+        }
+    })
+    onPointerUp = (e: PointerEvent) => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button === 2 || e.ctrlKey)) {
+            this.props.selectLink(this.props.selectedLink() === this.props.index ? -1 : this.props.index);
+        }
+    }
+    onClick = (e: React.MouseEvent) => {
+        if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3 && (e.button !== 2 && !e.ctrlKey)) {
+            DocumentManager.Instance.FollowLink(this.props.link, this.props.link[this.props.anchor] as Doc, document => this.props.addDocTab(document, undefined, "inTab"), false);
+        }
+        e.stopPropagation();
+    }
+    render() {
+        let y = NumCast(this.props.link[this.props.anchor + "_y"], 100);
+        let x = NumCast(this.props.link[this.props.anchor + "_x"], 100);
+        let c = StrCast(this.props.link[this.props.anchor + "_background"], "lightblue");
+        return <div onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.link[this.props.otherAnchor]! as Doc).title)} ref={this._ref} style={{
+            cursor: "default", position: "absolute", background: c, width: "25px", height: "25px", borderRadius: "20px", textAlign: "center", left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+            transform: `scale(${1 / this.props.scale()})`,
+            border: this.props.selectedLink() === this.props.index && this.props.isSelected() ? "solid 3px black" : undefined
+        }} />
+    }
+}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c217c9d86..d8a1841b1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -40,6 +40,7 @@ import SharingManager from '../../util/SharingManager';
 import { Scripting } from '../../util/Scripting';
 import { DictationOverlay } from '../DictationOverlay';
 import { CollectionViewType } from '../collections/CollectionBaseView';
+import { DocuLinkView } from './DocuLinkView';
 
 library.add(fa.faEdit);
 library.add(fa.faTrash);
@@ -241,6 +242,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.nativeEvent.cancelBubble && e.button === 0) return;
+        runInAction(() => this._selectedLink = -1);
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._hitTemplateDrag = false;
@@ -626,6 +628,25 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             layoutKey="layout"
             DataDoc={this.props.DataDoc} />);
     }
+    linkEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "anchor1" : "anchor2";
+    linkOtherEndpoint = (linkDoc: Doc) => Doc.AreProtosEqual(this.props.Document, Cast(linkDoc.anchor1, Doc) as Doc) ? "anchor2" : "anchor1";
+    public setBackround(color: string) {
+        let selLink = this._selectedLink !== -1 && DocListCast(this.Document.links)[this._selectedLink];
+        if (selLink) {
+            let both = selLink["anchor1_background"] === selLink["anchor2_background"];
+            selLink[this.linkEndpoint(selLink) + "_background"] = color;
+            both && (selLink[this.linkOtherEndpoint(selLink) + "_background"] = color);
+        } else {
+            this.Document.backgroundColor = color;
+        }
+    }
+    @observable _selectedLink = -1;
+    selectLink = action((which: number) => {
+        SelectionManager.SelectDoc(this, false);
+        this._selectedLink = which;
+    })
+    selectedLink = () => this._selectedLink;
+
     render() {
         if (!this.props.Document) return (null);
         let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined;
@@ -696,7 +717,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
-                {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) => <DocuLink view={this} link={d} index={i} />)}
+                {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) =>
+                    <DocuLinkView Document={this.props.Document} blacklist={this.props.ContainingCollectionDoc} anchor={this.linkEndpoint(d)} otherAnchor={this.linkOtherEndpoint(d)} addDocTab={this.props.addDocTab} contentDiv={this.ContentDiv}
+                        scale={this.props.ContentScaling}
+                        isSelected={this.isSelected} link={d} index={i} selectLink={this.selectLink} selectedLink={this.selectedLink} />)}
                 {!showTitle && !showCaption ?
                     this.Document.searchFields ?
                         (<div className="documentView-searchWrapper">
@@ -720,94 +744,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 }
 
-
-interface DocuLinkProps {
-    view: DocumentView;
-    link: Doc;
-    index: number;
-}
-
-@observer
-export class DocuLink extends React.Component<DocuLinkProps> {
-    _downx = 0;
-    _downy = 0;
-    @observable _x = 0;
-    @observable _y = 0;
-
-    constructor(props: DocuLinkProps) {
-        super(props);
-    }
-
-    clamp(n: number, lower: number, upper: number) {
-        return Math.max(lower, Math.min(upper, n));
-    }
-
-    getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) {
-        var r = l + w,
-            b = t + h;
-
-        var x = this.clamp(x, l, r),
-            y = this.clamp(y, t, b);
-
-        var dl = Math.abs(x - l),
-            dr = Math.abs(x - r),
-            dt = Math.abs(y - t),
-            db = Math.abs(y - b);
-
-        var m = Math.min(dl, dr, dt, db);
-
-        return (m === dt) ? [x, t] :
-            (m === db) ? [x, b] :
-                (m === dl) ? [l, y] : [r, y];
-    }
-
-    get linkEndpoint() {
-        return Doc.AreProtosEqual(this.props.view.props.Document, Cast(this.props.link.anchor1, Doc) as Doc) ?
-            "anchor1" : "anchor2";
-    }
-    get linkOtherEndpoint() {
-        return !Doc.AreProtosEqual(this.props.view.props.Document, Cast(this.props.link.anchor1, Doc) as Doc) ?
-            "anchor1" : "anchor2";
-    }
-
-    onPointerDown = (e: React.PointerEvent) => {
-        this._downx = e.clientX;
-        this._downy = e.clientY;
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-        document.addEventListener("pointermove", this.onPointerMove);
-        document.addEventListener("pointerup", this.onPointerUp);
-        e.stopPropagation();
-    }
-    onPointerMove = action((e: PointerEvent) => {
-        if (this.props.view.ContentDiv && (Math.abs(e.clientX - this._downx) > 3 || Math.abs(e.clientY - this._downy) > 3)) {
-            let bounds = this.props.view.ContentDiv.getBoundingClientRect();
-            let pt = this.getNearestPointInPerimeter(bounds.left - 25, bounds.top - 25, bounds.width, bounds.height, e.clientX, e.clientY);
-            this.props.link[this.linkEndpoint + "_x"] = (pt[0] - bounds.left) / bounds.width * 100;
-            this.props.link[this.linkEndpoint + "_y"] = (pt[1] - bounds.top) / bounds.height * 100;
-        }
-    })
-    onPointerUp = (e: PointerEvent) => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-    }
-    onClick = (e: React.MouseEvent) => {
-        if (Math.abs(e.clientX - this._downx) < 3 && Math.abs(e.clientY - this._downy) < 3) {
-            DocumentManager.Instance.FollowLink(this.props.link, this.props.view.props.Document, document => this.props.view.props.addDocTab(document, undefined, "inTab"), false);
-        }
-        e.stopPropagation();
-    }
-    render() {
-        return <div onPointerDown={this.onPointerDown} title={StrCast((this.props.link[this.linkOtherEndpoint]! as Doc).title)} style={{
-            position: "absolute", background: "lightblue", width: "25px", height: "25px", borderRadius: "20px",
-            left: `${NumCast(this.props.link[this.linkEndpoint + "_x"], 100)}%`, top: `${NumCast(this.props.link[this.linkEndpoint + "_y"], 100)}%`
-        }} >
-            {this.props.index}
-        </div>
-    }
-}
-
-
 export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) {
     let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc);
     if (oldLayoutExt) {
-- 
cgit v1.2.3-70-g09d2

