From cc6040e717632ae9725c134cd8722813cb1733c2 Mon Sep 17 00:00:00 2001
From: kimdahey <claire_kim1@brown.edu>
Date: Fri, 23 Aug 2019 13:49:28 -0400
Subject: progress on guid method

---
 src/client/util/RichTextSchema.tsx | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 6d2abfaa2..44ebc05d4 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -214,7 +214,8 @@ export const marks: { [index: string]: MarkSpec } = {
         attrs: {
             href: {},
             location: { default: null },
-            title: { default: null }
+            title: { default: null },
+            guid: { default: null }
         },
         inclusive: false,
         parseDOM: [{
-- 
cgit v1.2.3-70-g09d2


From 18cc66aaa90c5054268eac9520a963867c0c2f8e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 29 Aug 2019 01:14:18 -0400
Subject: fixed expand/collapse of text.  added user tooltip

---
 src/client/util/RichTextSchema.tsx                 |  5 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 50 +++++---------
 .../views/nodes/FormattedTextBoxComment.scss       | 34 ++++++++++
 src/client/views/nodes/FormattedTextBoxComment.tsx | 78 ++++++++++++++++++++++
 4 files changed, 132 insertions(+), 35 deletions(-)
 create mode 100644 src/client/views/nodes/FormattedTextBoxComment.scss
 create mode 100644 src/client/views/nodes/FormattedTextBoxComment.tsx

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index f567d803e..ee0c0870a 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -343,7 +343,10 @@ export const marks: { [index: string]: MarkSpec } = {
             let hideUsers = node.attrs.hide_users;
             let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail);
             return hidden ?
-                ['span', { class: node.attrs.opened ? "userMarkOpen" : "userMark" }, 0] :
+                (node.attrs.opened ?
+                    ['span', { class: "userMarkOpen" }, 0] :
+                    ['span', { class: "userMark" }, ['span', { style: "font-size:2" }, 0]]
+                ) :
                 ['span', 0];
         }
     },
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 146281f2b..2485760df 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -37,7 +37,7 @@ import { DocumentDecorations } from '../DocumentDecorations';
 import { DictationManager } from '../../util/DictationManager';
 import { ReplaceStep } from 'prosemirror-transform';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { number } from 'prop-types';
+import { selectionSizePlugin, findStartOfMark, findUserMark, findEndOfMark, findOtherUserMark, SelectionSizeTooltip } from './FormattedTextBoxComment';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -325,7 +325,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     props: {
                         attributes: { class: "ProseMirror-example-setup-style" }
                     }
-                })
+                }),
+                selectionSizePlugin
             ] : [
                     history(),
                     keymap(buildKeymap(schema)),
@@ -643,6 +644,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._heightReactionDisposer && this._heightReactionDisposer();
         this._searchReactionDisposer && this._searchReactionDisposer();
         document.removeEventListener("paste", this.paste);
+        this._editorView && this._editorView.destroy();
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
@@ -707,45 +709,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    findUserMark(marks: Mark[]) {
-        return marks.find(m => m.attrs && m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
-    }
-    findStartOfMark(rpos: ResolvedPos) {
-        let before = 0;
-        let nbef = rpos.nodeBefore;
-        while (nbef && this.findUserMark(nbef.marks)) {
-            before += nbef.nodeSize;
-            rpos = this._editorView!.state.doc.resolve(rpos.pos - nbef.nodeSize);
-            rpos && (nbef = rpos.nodeBefore);
-        }
-        return before;
-    }
-    findEndOfMark(rpos: ResolvedPos) {
-        let after = 0;
-        let naft = rpos.nodeAfter;
-        while (naft && this.findUserMark(naft.marks)) {
-            after += naft.nodeSize;
-            rpos = this._editorView!.state.doc.resolve(rpos.pos + naft.nodeSize);
-            rpos && (naft = rpos.nodeAfter);
-        }
-        return after;
-    }
-
     onPointerUp = (e: React.PointerEvent): void => {
         let view = this._editorView!;
         const pos = view.posAtCoords({ left: e.clientX, top: e.clientY });
         const rpos = pos && view.state.doc.resolve(pos.pos);
-        if (pos && rpos && view.state.selection.$from === view.state.selection.$to) {
-            let nbef = this.findStartOfMark(rpos);
-            let naft = this.findEndOfMark(rpos);
+        let noselection = view.state.selection.$from === view.state.selection.$to;
+        if (pos && rpos) {
+            let nbef = findStartOfMark(rpos, view, findOtherUserMark);
+            let naft = findEndOfMark(rpos, view, findOtherUserMark);
             const spos = view.state.doc.resolve(pos.pos - nbef);
             const epos = view.state.doc.resolve(pos.pos + naft);
             let ts = new TextSelection(spos, epos);
-            let child = rpos.nodeBefore;
-            let mark = child && this.findUserMark(child.marks);
-            if (mark && child && nbef && naft) {
-                let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: e.button === 2 ? Doc.CurrentUserEmail : mark.attrs.userid, opened: e.button === 2 ? false : !mark.attrs.opened });
-                view.dispatch(view.state.tr.setSelection(ts).removeMark(ts.from, ts.to, nmark).addMark(ts.from, ts.to, nmark).setSelection(new TextSelection(epos, epos)));
+            let child = rpos.nodeBefore || rpos.nodeAfter;
+            let mark = child && findOtherUserMark(child.marks);
+            if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
+                let opened = e.button === 2 ? false : !mark.attrs.opened;
+                SelectionSizeTooltip.tooltip.style.display = opened ? "" : "none";
+                let mid = opened ? epos : view.state.doc.resolve((spos.pos + epos.pos) / 2);
+                let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: e.button === 2 ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
+                view.dispatch(view.state.tr.addMark(ts.from, ts.to, nmark).setSelection(new TextSelection(mid, mid)));
             }
         }
         if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/FormattedTextBoxComment.scss
new file mode 100644
index 000000000..792cee182
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBoxComment.scss
@@ -0,0 +1,34 @@
+.FormattedTextBox-tooltip {
+    position: absolute;
+    pointer-events: none;
+    z-index: 20;
+    background: white;
+    border: 1px solid silver;
+    border-radius: 2px;
+    padding: 2px 10px;
+    margin-bottom: 7px;
+    -webkit-transform: translateX(-50%);
+    transform: translateX(-50%);
+  }
+  .FormattedTextBox-tooltip:before {
+    content: "";
+    height: 0; width: 0;
+    position: absolute;
+    left: 50%;
+    margin-left: -5px;
+    bottom: -6px;
+    border: 5px solid transparent;
+    border-bottom-width: 0;
+    border-top-color: silver;
+  }
+  .FormattedTextBox-tooltip:after {
+    content: "";
+    height: 0; width: 0;
+    position: absolute;
+    left: 50%;
+    margin-left: -5px;
+    bottom: -4.5px;
+    border: 5px solid transparent;
+    border-bottom-width: 0;
+    border-top-color: white;
+  }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
new file mode 100644
index 000000000..e88c85a86
--- /dev/null
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -0,0 +1,78 @@
+import { Plugin, EditorState, TextSelection } from "prosemirror-state"
+import './FormattedTextBoxComment.scss'
+import { DragManager } from "../../util/DragManager";
+import { ResolvedPos, Mark } from "prosemirror-model";
+import { EditorView } from "prosemirror-view";
+import { Doc } from "../../../new_fields/Doc";
+
+export let selectionSizePlugin = new Plugin({
+    view(editorView) { return new SelectionSizeTooltip(editorView); }
+})
+export function findOtherUserMark(marks: Mark[]): Mark | undefined {
+    return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+}
+export function findUserMark(marks: Mark[]): Mark | undefined {
+    return marks.find(m => m.attrs.userid);
+}
+export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+    let before = 0;
+    let nbef = rpos.nodeBefore;
+    while (nbef && finder(nbef.marks)) {
+        before += nbef.nodeSize;
+        rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
+        rpos && (nbef = rpos.nodeBefore);
+    }
+    return before;
+}
+export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+    let after = 0;
+    let naft = rpos.nodeAfter;
+    while (naft && finder(naft.marks)) {
+        after += naft.nodeSize;
+        rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
+        rpos && (naft = rpos.nodeAfter);
+    }
+    return after;
+}
+
+export class SelectionSizeTooltip {
+    static tooltip: any;
+    constructor(view: any) {
+        if (!SelectionSizeTooltip.tooltip) {
+            SelectionSizeTooltip.tooltip = document.createElement("div");
+            SelectionSizeTooltip.tooltip.className = "FormattedTextBox-tooltip";
+            DragManager.Root().appendChild(SelectionSizeTooltip.tooltip);
+        }
+
+        this.update(view, undefined);
+    }
+
+
+    update(view: EditorView, lastState?: EditorState) {
+        let state = view.state;
+        // Don't do anything if the document/selection didn't change
+        if (lastState && lastState.doc.eq(state.doc) &&
+            lastState.selection.eq(state.selection)) return;
+
+        if (state.selection.$from) {
+            let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
+            let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
+            let child = state.selection.$from.nodeBefore;
+            let mark = child && findOtherUserMark(child.marks);
+            if (mark && child && nbef && naft && mark.attrs.opened && SelectionSizeTooltip.tooltip.offsetParent) {
+                SelectionSizeTooltip.tooltip.textContent = mark.attrs.userid;
+                // These are in screen coordinates
+                let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+                // The box in which the tooltip is positioned, to use as base
+                let box = SelectionSizeTooltip.tooltip.offsetParent.getBoundingClientRect();
+                // Find a center-ish x position from the selection endpoints (when
+                // crossing lines, end may be more to the left)
+                let left = Math.max((start.left + end.left) / 2, start.left + 3);
+                SelectionSizeTooltip.tooltip.style.left = (left - box.left) + "px";
+                SelectionSizeTooltip.tooltip.style.bottom = (box.bottom - start.top) + "px";
+            }
+        }
+    }
+
+    destroy() { SelectionSizeTooltip.tooltip.style.display = "none" }
+}
-- 
cgit v1.2.3-70-g09d2


From 1079d90c5d6752a0a2c06a25d42c1192cb433ed3 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 29 Aug 2019 10:09:03 -0400
Subject: made text author tooltip interactive.

---
 src/client/util/RichTextSchema.tsx                 |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 21 ++++++-----
 src/client/views/nodes/FormattedTextBoxComment.tsx | 41 +++++++++++++++++-----
 3 files changed, 47 insertions(+), 17 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index ee0c0870a..f8da98f17 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -335,7 +335,7 @@ export const marks: { [index: string]: MarkSpec } = {
         attrs: {
             userid: { default: "" },
             hide_users: { default: [] },
-            opened: { default: false }
+            opened: { default: true }
         },
         group: "inline",
         inclusive: false,
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 2485760df..c23b85e83 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -714,27 +714,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         const pos = view.posAtCoords({ left: e.clientX, top: e.clientY });
         const rpos = pos && view.state.doc.resolve(pos.pos);
         let noselection = view.state.selection.$from === view.state.selection.$to;
+        let set = false;
         if (pos && rpos) {
             let nbef = findStartOfMark(rpos, view, findOtherUserMark);
             let naft = findEndOfMark(rpos, view, findOtherUserMark);
-            const spos = view.state.doc.resolve(pos.pos - nbef);
-            const epos = view.state.doc.resolve(pos.pos + naft);
-            let ts = new TextSelection(spos, epos);
+            const spos = pos.pos - nbef;
+            const epos = pos.pos + naft;
             let child = rpos.nodeBefore || rpos.nodeAfter;
             let mark = child && findOtherUserMark(child.marks);
             if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
-                let opened = e.button === 2 ? false : !mark.attrs.opened;
-                SelectionSizeTooltip.tooltip.style.display = opened ? "" : "none";
-                let mid = opened ? epos : view.state.doc.resolve((spos.pos + epos.pos) / 2);
-                let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: e.button === 2 ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
-                view.dispatch(view.state.tr.addMark(ts.from, ts.to, nmark).setSelection(new TextSelection(mid, mid)));
+                SelectionSizeTooltip.SetState(this, mark.attrs.opened, spos, epos, mark);
+                set = true;
             }
         }
+        !set && SelectionSizeTooltip.Hide();
         if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
             e.stopPropagation();
         }
     }
 
+    setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
+        let view = this._editorView!;
+        let mid = view.state.doc.resolve(Math.round((start + end) / 2));
+        let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
+        view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid, mid)));
+    }
+
     @action
     onFocused = (e: React.FocusEvent): void => {
         document.removeEventListener("keypress", this.recordKeyHandler);
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index e88c85a86..31eb06427 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -1,6 +1,5 @@
-import { Plugin, EditorState, TextSelection } from "prosemirror-state"
+import { Plugin, EditorState } from "prosemirror-state"
 import './FormattedTextBoxComment.scss'
-import { DragManager } from "../../util/DragManager";
 import { ResolvedPos, Mark } from "prosemirror-model";
 import { EditorView } from "prosemirror-view";
 import { Doc } from "../../../new_fields/Doc";
@@ -35,18 +34,43 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark
     return after;
 }
 
+
 export class SelectionSizeTooltip {
-    static tooltip: any;
+    static tooltip: HTMLElement;
+    static start: number;
+    static end: number;
+    static mark: Mark;
+    static opened: boolean;
+    static textBox: any;
     constructor(view: any) {
         if (!SelectionSizeTooltip.tooltip) {
+            const root = document.getElementById("root");
             SelectionSizeTooltip.tooltip = document.createElement("div");
             SelectionSizeTooltip.tooltip.className = "FormattedTextBox-tooltip";
-            DragManager.Root().appendChild(SelectionSizeTooltip.tooltip);
+            SelectionSizeTooltip.tooltip.style.pointerEvents = "all";
+            SelectionSizeTooltip.tooltip.onpointerdown = (e: PointerEvent) => {
+                SelectionSizeTooltip.opened = !SelectionSizeTooltip.opened;
+                SelectionSizeTooltip.textBox.setAnnotation(
+                    SelectionSizeTooltip.start, SelectionSizeTooltip.end, SelectionSizeTooltip.mark,
+                    SelectionSizeTooltip.opened, e.button == 2);
+            };
+            root && root.appendChild(SelectionSizeTooltip.tooltip);
         }
-
         this.update(view, undefined);
     }
 
+    public static Hide() {
+        SelectionSizeTooltip.textBox = undefined;
+        SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "none");
+    }
+    public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) {
+        SelectionSizeTooltip.textBox = textBox;
+        SelectionSizeTooltip.start = start;
+        SelectionSizeTooltip.end = end;
+        SelectionSizeTooltip.mark = mark;
+        SelectionSizeTooltip.opened = opened;
+        SelectionSizeTooltip.textBox && SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "");
+    }
 
     update(view: EditorView, lastState?: EditorState) {
         let state = view.state;
@@ -59,12 +83,13 @@ export class SelectionSizeTooltip {
             let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
             let child = state.selection.$from.nodeBefore;
             let mark = child && findOtherUserMark(child.marks);
-            if (mark && child && nbef && naft && mark.attrs.opened && SelectionSizeTooltip.tooltip.offsetParent) {
+            if (mark && child && nbef && naft) {
                 SelectionSizeTooltip.tooltip.textContent = mark.attrs.userid;
                 // These are in screen coordinates
-                let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+                // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+                let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
                 // The box in which the tooltip is positioned, to use as base
-                let box = SelectionSizeTooltip.tooltip.offsetParent.getBoundingClientRect();
+                let box = (document.getElementById("main-div") as any).getBoundingClientRect();
                 // Find a center-ish x position from the selection endpoints (when
                 // crossing lines, end may be more to the left)
                 let left = Math.max((start.left + end.left) / 2, start.left + 3);
-- 
cgit v1.2.3-70-g09d2


From a46ae95ed6570342f03b7590ff70d6249e56f059 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 29 Aug 2019 10:34:08 -0400
Subject: added a timestamp for edit marks.

---
 src/client/util/RichTextSchema.tsx                 |  3 ++-
 src/client/views/nodes/FormattedTextBox.tsx        | 18 +++++++++++++++++-
 src/client/views/nodes/FormattedTextBoxComment.tsx |  2 +-
 3 files changed, 20 insertions(+), 3 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index f8da98f17..a642ee46c 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -335,7 +335,8 @@ export const marks: { [index: string]: MarkSpec } = {
         attrs: {
             userid: { default: "" },
             hide_users: { default: [] },
-            opened: { default: true }
+            opened: { default: true },
+            modified: { default: "when?" }
         },
         group: "inline",
         inclusive: false,
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index c23b85e83..0d4376d8d 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -38,6 +38,7 @@ import { DictationManager } from '../../util/DictationManager';
 import { ReplaceStep } from 'prosemirror-transform';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { selectionSizePlugin, findStartOfMark, findUserMark, findEndOfMark, findOtherUserMark, SelectionSizeTooltip } from './FormattedTextBoxComment';
+import { date } from 'serializr';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -811,8 +812,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false);
             }, 0);
         }
+        function timenow() {
+            var now = new Date();
+            let ampm = 'am';
+            let h = now.getHours();
+            let m: any = now.getMinutes();
+            let s: any = now.getSeconds();
+            if (h >= 12) {
+                if (h > 12) h -= 12;
+                ampm = 'pm';
+            }
+
+            if (m < 10) m = '0' + m;
+            if (s < 10) s = '0' + s;
+            return now.toLocaleDateString() + ' ' + h + ':' + m + ':' + s + ' ' + ampm;
+        }
         var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
-        let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })];
+        let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
         this._editorView!.state.storedMarks = newMarks;
 
         // stop propagation doesn't seem to stop propagation of native keyboard events.
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 31eb06427..4ec0d6064 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -84,7 +84,7 @@ export class SelectionSizeTooltip {
             let child = state.selection.$from.nodeBefore;
             let mark = child && findOtherUserMark(child.marks);
             if (mark && child && nbef && naft) {
-                SelectionSizeTooltip.tooltip.textContent = mark.attrs.userid;
+                SelectionSizeTooltip.tooltip.textContent = mark.attrs.userid + " " + mark.attrs.modified;
                 // These are in screen coordinates
                 // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
                 let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
-- 
cgit v1.2.3-70-g09d2


From 5f39a01998e5ee60eb6b796a5cd6b11fc680ceee Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 30 Aug 2019 13:04:40 -0400
Subject: cleanup of outline mode text

---
 src/client/util/ProsemirrorExampleTransfer.ts | 96 +++++++++++++--------------
 src/client/util/RichTextSchema.tsx            |  6 +-
 src/client/util/TooltipLinkingMenu.tsx        | 20 +-----
 src/client/util/TooltipTextMenu.tsx           |  4 +-
 src/client/util/prosemirrorPatches.js         | 62 +++++++++++++++++
 5 files changed, 117 insertions(+), 71 deletions(-)
 create mode 100644 src/client/util/prosemirrorPatches.js

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 3979d8a49..4af2cf58f 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -2,7 +2,8 @@ import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setB
 import { redo, undo } from "prosemirror-history";
 import { undoInputRule } from "prosemirror-inputrules";
 import { Schema } from "prosemirror-model";
-import { liftListItem, splitListItem, wrapInList, sinkListItem } from "prosemirror-schema-list";
+import { liftListItem, } from "./prosemirrorPatches.js";
+import { splitListItem, wrapInList, sinkListItem } from "prosemirror-schema-list";
 import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
 import { TooltipTextMenu } from "./TooltipTextMenu";
 
@@ -79,75 +80,73 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
 
     bind("Mod-s", TooltipTextMenu.insertStar);
 
-    let nodeTypeMark = (depth: number) => depth === 2 ? "indent2" : depth === 4 ? "indent3" : depth === 6 ? "indent4" : "indent1";
+    let updateBullets = (tx2: Transaction, refStart: number, delta: number) => {
+        for (let i = refStart; i > 0; i--) {
+            let testPos = tx2.doc.nodeAt(i);
+            if (testPos && testPos.type === schema.nodes.list_item) {
+                let start = i;
+                let preve = i > 0 && tx2.doc.nodeAt(start - 1);
+                if (preve && preve.type === schema.nodes.ordered_list) {
+                    start = start - 1;
+                }
+                let rangeStart = tx2.doc.nodeAt(start);
+                if (rangeStart && rangeStart.type === schema.nodes.ordered_list) {
+                    tx2.setNodeMarkup(start, rangeStart.type, { ...rangeStart.attrs, bulletStyle: rangeStart.attrs.bulletStyle + delta }, rangeStart.marks);
+                }
+                rangeStart && rangeStart.descendants((node: any, offset: any, index: any) => {
+                    if (node.type === schema.nodes.ordered_list) {
+                        tx2.setNodeMarkup(start + offset + 1, node.type, { ...node.attrs, bulletStyle: node.attrs.bulletStyle + delta }, node.marks);
+                    }
+                });
+                break;
+            }
+        }
+    }
 
-    let bulletFunc = (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+    bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         var ref = state.selection;
         var range = ref.$from.blockRange(ref.$to);
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
-        let depth = range && range.depth ? range.depth : 0;
         if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
-            const resolvedPos = tx2.doc.resolve(range!.start);
-
-            let path = (resolvedPos as any).path;
-            for (let i = path.length - 1; i > 0; i--) {
-                if (path[i].type === schema.nodes.ordered_list) {
-                    path[i].attrs.bulletStyle = nodeTypeMark(depth);
-                    break;
-                }
-            }
+            updateBullets(tx2, range!.start, 1);
             marks && tx2.ensureMarks([...marks]);
             marks && tx2.setStoredMarks([...marks]);
             dispatch(tx2);
-        })) {
+        })) { // couldn't sink into an existing list, so wrap in a new one
             let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end));
             let newstate = state.applyTransaction(sxf);
             if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
-                const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
-                let path = (resolvedPos as any).path;
-                for (let i = path.length - 1; i > 0; i--) {
-                    if (path[i].type === schema.nodes.ordered_list) {
-                        path[i].attrs.bulletStyle = nodeTypeMark(depth);
+                for (let i = range!.start; i >= 0; i--) {
+                    let rangeStart = tx2.doc.nodeAt(i);
+                    if (rangeStart && rangeStart.type === schema.nodes.ordered_list) {
+                        tx2.setNodeMarkup(i, rangeStart.type, { ...rangeStart.attrs, bulletStyle: 1 }, rangeStart.marks);
                         break;
                     }
                 }
                 // when promoting to a list, assume list will format things so don't copy the stored marks.
-                // marks && tx2.ensureMarks([...marks]);
-                // marks && tx2.setStoredMarks([...marks]);
-
+                marks && tx2.ensureMarks([...marks]);
+                marks && tx2.setStoredMarks([...marks]);
                 dispatch(tx2);
             })) {
-                console.log("bullet fail");
+                console.log("bullet promote fail");
             }
         }
-    };
-
-    bind("Tab", bulletFunc);
+    });
 
     bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        var ref = state.selection;
-        var range = ref.$from.blockRange(ref.$to);
+        var range = state.selection.$from.blockRange(state.selection.$to);
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
-        let depth = range && range.depth > 3 ? range.depth - 4 : 0;
-        liftListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
-            try {
-                const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
-
-                let path = (resolvedPos as any).path;
-                for (let i = path.length - 1; i > 0; i--) {
-                    if (path[i].type === schema.nodes.ordered_list) {
-                        path[i].attrs.bulletStyle = nodeTypeMark(depth);
-                        break;
-                    }
-                }
 
-                marks && tx2.ensureMarks([...marks]);
-                marks && tx2.setStoredMarks([...marks]);
-                dispatch(tx2);
-            } catch (e) {
-                dispatch(tx2);
-            }
-        });
+        let tr = state.tr;
+        updateBullets(tr, range!.start, -1);
+
+        if (!liftListItem(schema.nodes.list_item)(tr, (tx2: Transaction) => {
+            marks && tx2.ensureMarks([...marks]);
+            marks && tx2.setStoredMarks([...marks]);
+            dispatch(tx2);
+        })) {
+            console.log("bullet demote fail");
+        }
     });
 
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -160,8 +159,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
                 marks && tx3.ensureMarks(marks);
                 marks && tx3.setStoredMarks(marks);
-                if (!liftListItem(schema.nodes.list_item)(state, dispatch as ((tx: Transaction<Schema<any, any>>) => void))
-                ) {
+                if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
                     dispatch(tx3);
                 }
             })) {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a642ee46c..0994f5f6b 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -176,13 +176,13 @@ export const nodes: { [index: string]: NodeSpec } = {
         content: 'list_item+',
         group: 'block',
         attrs: {
-            bulletStyle: { default: "" },
+            bulletStyle: { default: 0 },
             mapStyle: { default: "decimal" }
         },
         toDOM(node: Node<any>) {
             const bs = node.attrs.bulletStyle;
-            const decMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "decimal2" : bs === "indent3" ? "decimal3" : bs === "indent4" ? "decimal4" : "";
-            const multiMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "upper-alpha" : bs === "indent3" ? "lower-roman" : bs === "indent4" ? "lower-alpha" : "";
+            const decMap = bs === 1 ? "decimal" : bs === 2 ? "decimal2" : bs === 3 ? "decimal3" : bs === 4 ? "decimal4" : "";
+            const multiMap = bs === 1 ? "decimal" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
             for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map;
             return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0];
diff --git a/src/client/util/TooltipLinkingMenu.tsx b/src/client/util/TooltipLinkingMenu.tsx
index 55e0eb909..e6d6c471f 100644
--- a/src/client/util/TooltipLinkingMenu.tsx
+++ b/src/client/util/TooltipLinkingMenu.tsx
@@ -1,23 +1,9 @@
-import { action, IReactionDisposer, reaction } from "mobx";
-import { Dropdown, DropdownSubmenu, MenuItem, MenuItemSpec, renderGrouped, icons, } from "prosemirror-menu"; //no import css
-import { baseKeymap, lift } from "prosemirror-commands";
-import { history, redo, undo } from "prosemirror-history";
-import { keymap } from "prosemirror-keymap";
-import { EditorState, Transaction, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
-import { schema } from "./RichTextSchema";
-import { Schema, NodeType, MarkType } from "prosemirror-model";
-import React = require("react");
+import { FieldViewProps } from "../views/nodes/FieldView";
 import "./TooltipTextMenu.scss";
+import React = require("react");
 const { toggleMark, setBlockType, wrapIn } = require("prosemirror-commands");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { wrapInList, bulletList, liftListItem, listItem } from 'prosemirror-schema-list';
-import {
-    faListUl,
-} from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { FieldViewProps } from "../views/nodes/FieldView";
-import { throwStatement } from "babel-types";
 
 const SVG = "http://www.w3.org/2000/svg";
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index e979e6cde..3b4b7f05a 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -3,7 +3,7 @@ import { faListUl } from '@fortawesome/free-solid-svg-icons';
 import { action, observable } from "mobx";
 import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
 import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
-import { liftListItem, wrapInList } from 'prosemirror-schema-list';
+import { wrapInList } from 'prosemirror-schema-list';
 import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { Doc, Field, Opt } from "../../new_fields/Doc";
@@ -525,7 +525,7 @@ export class TooltipTextMenu {
                 let path = resolvedPos.path;
                 for (let i = path.length - 1; i > 0; i--) {
                     if (path[i].type === schema.nodes.ordered_list) {
-                        path[i].attrs.bulletStyle = "indent1";
+                        path[i].attrs.bulletStyle = 1;
                         path[i].attrs.mapStyle = (nodeType as any).attrs.mapStyle;
                         break;
                     }
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
new file mode 100644
index 000000000..c273c2323
--- /dev/null
+++ b/src/client/util/prosemirrorPatches.js
@@ -0,0 +1,62 @@
+'use strict';
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+var prosemirrorTransform = require('prosemirror-transform');
+var prosemirrorModel = require('prosemirror-model');
+
+exports.liftListItem = liftListItem;
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to lift the list item around the selection up into
+// a wrapping list.
+function liftListItem(itemType) {
+    return function (tx, dispatch) {
+        var ref = tx.selection;
+        var $from = ref.$from;
+        var $to = ref.$to;
+        var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+        if (!range) { return false }
+        if (!dispatch) { return true }
+        if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
+        { return liftToOuterList(tx, dispatch, itemType, range) }
+        else // Outer list node
+        { return liftOutOfList(tx, dispatch, range) }
+    }
+}
+
+function liftToOuterList(tr, dispatch, itemType, range) {
+    var end = range.end, endOfList = range.$to.end(range.depth);
+    if (end < endOfList) {
+        // There are siblings after the lifted items, which must become
+        // children of the last item
+        tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList,
+            new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
+        range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
+    }
+    dispatch(tr.lift(range, prosemirrorTransform.liftTarget(range)).scrollIntoView());
+    return true
+}
+
+function liftOutOfList(tr, dispatch, range) {
+    var list = range.parent;
+    // Merge the list items into a single big item
+    for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
+        pos -= list.child(i).nodeSize;
+        tr.delete(pos - 1, pos + 1);
+    }
+    var $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
+    var atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
+    var parent = $start.node(-1), indexBefore = $start.index(-1);
+    if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
+        item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) { return false }
+    var start = $start.pos, end = start + item.nodeSize;
+    // Strip off the surrounding list. At the sides where we're not at
+    // the end of the list, the existing list is closed. At sides where
+    // this is the end, it is overwritten to its end.
+    tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
+        new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty)))
+            .append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))),
+            atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
+    dispatch(tr.scrollIntoView());
+    return true
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 6aee8e71c50f363ec8c7eb2a2fc5b136295319d2 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 30 Aug 2019 23:37:01 -0400
Subject: made list levels more regular.  marks now aren't copied on splitting
 list items.

---
 src/client/util/ProsemirrorExampleTransfer.ts |  4 ++--
 src/client/util/RichTextSchema.tsx            |  4 ++--
 src/client/views/nodes/FormattedTextBox.scss  | 24 +++++++++++++++---------
 3 files changed, 19 insertions(+), 13 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 4af2cf58f..bcb8b404b 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -152,8 +152,8 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => {
-            marks && tx3.ensureMarks(marks);
-            marks && tx3.setStoredMarks(marks);
+            // marks && tx3.ensureMarks(marks);
+            // marks && tx3.setStoredMarks(marks);
             dispatch(tx3);
         })) {
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 0994f5f6b..c74881680 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -181,8 +181,8 @@ export const nodes: { [index: string]: NodeSpec } = {
         },
         toDOM(node: Node<any>) {
             const bs = node.attrs.bulletStyle;
-            const decMap = bs === 1 ? "decimal" : bs === 2 ? "decimal2" : bs === 3 ? "decimal3" : bs === 4 ? "decimal4" : "";
-            const multiMap = bs === 1 ? "decimal" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
+            const decMap = bs ? "decimal" + bs : "";
+            const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
             for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map;
             return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0];
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 03e81bfca..dd07f5924 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -84,18 +84,24 @@
     align-content: center;
 }
 
-ol { counter-reset: deci 0;}
-.decimal-ol { counter-reset: deci 0; p { display: inline }; font-size: 24 }
+ol { counter-reset: deci1 0;}
+.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
 .decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
 .decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 }
 .decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 }
+.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 }
+.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 }
+.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 }
 .upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
 .lower-roman-ol {counter-reset: lroman;  p { display: inline }; font-size: 14; }
 .lower-alpha-ol {counter-reset: lalpha;  p { display: inline }; font-size: 10;}
-.decimal:before { content: counter(deci) " "; counter-increment: deci; display:inline-block; width: 30}
-.decimal2:before { content: counter(deci) "." counter(deci2) " "; counter-increment: deci2; display:inline-block; width: 35}
-.decimal3:before { content: counter(deci) "." counter(deci2) "." counter(deci3) " "; counter-increment: deci3; display:inline-block; width: 35}
-.decimal4:before { content: counter(deci) "." counter(deci2) "." counter(deci3) "." counter(deci4) " "; counter-increment: deci4; display:inline-block; width: 40}
-.upper-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) " "; counter-increment: ualph; display:inline-block; width: 35 }
-.lower-roman:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) " "; counter-increment: lroman;display:inline-block; width: 50 }
-.lower-alpha:before { content: counter(deci) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha)" "; counter-increment: lalpha; display:inline-block; width: 35}
+.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; width: 30}
+.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; width: 35}
+.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; width: 35}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; width: 50}
+.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; width: 35 }
+.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; width: 50 }
+.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; width: 35}
-- 
cgit v1.2.3-70-g09d2


From ea25f19b08fba66b1ec281afdadf719ae6a6ed54 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 31 Aug 2019 00:13:33 -0400
Subject: fixed some hacks so that undo/redo works for ordered lists

---
 src/client/util/RichTextSchema.tsx          | 8 ++++++++
 src/client/views/MainOverlayTextBox.tsx     | 5 -----
 src/client/views/nodes/FormattedTextBox.tsx | 8 +++-----
 3 files changed, 11 insertions(+), 10 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index c74881680..3161467b8 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -573,6 +573,14 @@ export class ImageResizeView {
     }
 }
 
+export class OrderedListView {
+    constructor(node: any, view: any, getPos: any) { }
+
+    update(node: any) {
+        return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
+    }
+}
+
 export class SummarizedView {
     // TODO: highlight text that is summarized. to find end of region, walk along mark
     _collapsed: HTMLElement;
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 755e5de14..27e0d181f 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -48,11 +48,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
         MainOverlayTextBox.Instance = this;
         reaction(() => FormattedTextBox.InputBoxOverlay,
             (box?: FormattedTextBox) => {
-                const tb = this._textBox;
-                const container = tb && tb.props.ContainingCollectionView;
-                if (tb && container) {
-                    tb.rebuildEditor();// this forces the edited text box to completely recreate itself to avoid get out of sync -- e.g., bullet labels are only computed when the dom elements are created
-                }
                 this._textBox = box;
                 if (box) {
                     this.ChromeHeight = box.props.ChromeHeight;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 0d4376d8d..0f349780e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -22,7 +22,7 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from "../../util/DragManager";
 import buildKeymap from "../../util/ProsemirrorExampleTransfer";
 import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView, OrderedListView } from "../../util/RichTextSchema";
 import { SelectionManager } from "../../util/SelectionManager";
 import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
 import { TooltipTextMenu } from "../../util/TooltipTextMenu";
@@ -615,6 +615,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 nodeViews: {
                     image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
                     star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
+                    ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }
+
                 },
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
@@ -807,10 +809,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         e.stopPropagation();
         if (e.key === "Tab" || e.key === "Enter") { // bullets typically change "levels" when tab or enter is used.  sometimes backspcae, so maybe that should be added.
             e.preventDefault();
-            setTimeout(() => { // force re-rendering of bullet numbers that may have had their bullet labels change.  (Our prosemirrior code re-"marks" the changed bullets, but nothing causes the Dom to be re-rendered which is where the nubering takes place)
-                SelectionManager.DeselectAll();
-                SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false);
-            }, 0);
         }
         function timenow() {
             var now = new Date();
-- 
cgit v1.2.3-70-g09d2


From 4a1d94325d5d1b5641cd280e89c442c114074e8d Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 3 Sep 2019 08:03:43 -0400
Subject: cleanup names for formattedTextBoxComment stuff.  fixed brushing bug
 with user_mark.

---
 src/client/util/RichTextSchema.tsx                 |  2 +-
 src/client/util/TooltipTextMenu.tsx                |  6 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 12 ++--
 src/client/views/nodes/FormattedTextBoxComment.tsx | 70 +++++++++++-----------
 4 files changed, 41 insertions(+), 49 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 3161467b8..7911cf629 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -346,7 +346,7 @@ export const marks: { [index: string]: MarkSpec } = {
             return hidden ?
                 (node.attrs.opened ?
                     ['span', { class: "userMarkOpen" }, 0] :
-                    ['span', { class: "userMark" }, ['span', { style: "font-size:2" }, 0]]
+                    ['span', { class: "userMark" }, ['span', 0]]
                 ) :
                 ['span', 0];
         }
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 3b4b7f05a..ce7b04e31 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -60,8 +60,6 @@ export class TooltipTextMenu {
     @observable
     private _storedMarks: Mark<any>[] | null | undefined;
 
-    public HackToFixTextSelectionGlitch: boolean = false;
-
 
     constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
         this.view = view;
@@ -629,7 +627,7 @@ export class TooltipTextMenu {
                 if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
                     if (this._brushMarks && to - from > 0) {
                         this.view.dispatch(this.view.state.tr.removeMark(from, to));
-                        this._brushMarks.forEach((mark: Mark) => {
+                        Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
                             const markType = mark.type;
                             this.changeToMarkInGroup(markType, this.view, []);
 
@@ -876,8 +874,6 @@ export class TooltipTextMenu {
                 this.updateFontSizeDropdown("Various");
             }
         }
-        !this.HackToFixTextSelectionGlitch &&
-            this.view.dispatch(this.view.state.tr.setStoredMarks(this._activeMarks)); // bcz: what's the purpose of this line?  It messes up text selection without the Hack.
 
         this.update_mark_doms();
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 93ac2f06f..6232dd3ab 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -37,8 +37,7 @@ import { DocumentDecorations } from '../DocumentDecorations';
 import { DictationManager } from '../../util/DictationManager';
 import { ReplaceStep } from 'prosemirror-transform';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { selectionSizePlugin, findStartOfMark, findUserMark, findEndOfMark, findOtherUserMark, SelectionSizeTooltip } from './FormattedTextBoxComment';
-import { date } from 'serializr';
+import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -172,9 +171,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
             const state = this._editorView.state.apply(tx);
-            FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true);
             this._editorView.updateState(state);
-            FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false);
             if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
@@ -327,7 +324,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         attributes: { class: "ProseMirror-example-setup-style" }
                     }
                 }),
-                selectionSizePlugin
+                formattedTextBoxCommentPlugin
             ] : [
                     history(),
                     keymap(buildKeymap(schema)),
@@ -713,7 +710,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     onPointerUp = (e: React.PointerEvent): void => {
-        SelectionSizeTooltip.textBox = this;
+        FormattedTextBoxComment.textBox = this;
         if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
             e.stopPropagation();
         }
@@ -805,8 +802,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
 
             if (m < 10) m = '0' + m;
-            if (s < 10) s = '0' + s;
-            return now.toLocaleDateString() + ' ' + h + ':' + m + ':' + s + ' ' + ampm;
+            return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
         }
         var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
         let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index c368a3f34..255000936 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -8,8 +8,8 @@ import { DocServer } from "../../DocServer";
 import { Utils } from "../../../Utils";
 import { StrCast } from "../../../new_fields/Types";
 
-export let selectionSizePlugin = new Plugin({
-    view(editorView) { return new SelectionSizeTooltip(editorView); }
+export let formattedTextBoxCommentPlugin = new Plugin({
+    view(editorView) { return new FormattedTextBoxComment(editorView); }
 });
 export function findOtherUserMark(marks: Mark[]): Mark | undefined {
     return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
@@ -42,7 +42,7 @@ export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (mark
 }
 
 
-export class SelectionSizeTooltip {
+export class FormattedTextBoxComment {
     static tooltip: HTMLElement;
     static tooltipText: HTMLElement;
     static start: number;
@@ -51,39 +51,39 @@ export class SelectionSizeTooltip {
     static opened: boolean;
     static textBox: any;
     constructor(view: any) {
-        if (!SelectionSizeTooltip.tooltip) {
+        if (!FormattedTextBoxComment.tooltip) {
             const root = document.getElementById("root");
             let input = document.createElement("input");
             input.type = "checkbox";
-            SelectionSizeTooltip.tooltip = document.createElement("div");
-            SelectionSizeTooltip.tooltipText = document.createElement("div");
-            SelectionSizeTooltip.tooltip.appendChild(SelectionSizeTooltip.tooltipText);
-            SelectionSizeTooltip.tooltip.className = "FormattedTextBox-tooltip";
-            SelectionSizeTooltip.tooltip.style.pointerEvents = "all";
-            SelectionSizeTooltip.tooltip.appendChild(input);
-            SelectionSizeTooltip.tooltip.onpointerdown = (e: PointerEvent) => {
+            FormattedTextBoxComment.tooltip = document.createElement("div");
+            FormattedTextBoxComment.tooltipText = document.createElement("div");
+            FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
+            FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
+            FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+            FormattedTextBoxComment.tooltip.appendChild(input);
+            FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
                 let keep = e.target && (e.target as any).type === "checkbox";
-                SelectionSizeTooltip.opened = keep || !SelectionSizeTooltip.opened;
-                SelectionSizeTooltip.textBox && SelectionSizeTooltip.textBox.setAnnotation(
-                    SelectionSizeTooltip.start, SelectionSizeTooltip.end, SelectionSizeTooltip.mark,
-                    SelectionSizeTooltip.opened, keep);
+                FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
+                FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation(
+                    FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
+                    FormattedTextBoxComment.opened, keep);
             };
-            root && root.appendChild(SelectionSizeTooltip.tooltip);
+            root && root.appendChild(FormattedTextBoxComment.tooltip);
         }
         this.update(view, undefined);
     }
 
     public static Hide() {
-        SelectionSizeTooltip.textBox = undefined;
-        SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "none");
+        FormattedTextBoxComment.textBox = undefined;
+        FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
     }
     public static SetState(textBox: any, opened: boolean, start: number, end: number, mark: Mark) {
-        SelectionSizeTooltip.textBox = textBox;
-        SelectionSizeTooltip.start = start;
-        SelectionSizeTooltip.end = end;
-        SelectionSizeTooltip.mark = mark;
-        SelectionSizeTooltip.opened = opened;
-        SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = "");
+        FormattedTextBoxComment.textBox = textBox;
+        FormattedTextBoxComment.start = start;
+        FormattedTextBoxComment.end = end;
+        FormattedTextBoxComment.mark = mark;
+        FormattedTextBoxComment.opened = opened;
+        FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
     }
 
     update(view: EditorView, lastState?: EditorState) {
@@ -102,10 +102,10 @@ export class SelectionSizeTooltip {
             let mark = child && findOtherUserMark(child.marks);
             let noselection = view.state.selection.$from === view.state.selection.$to;
             if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
-                SelectionSizeTooltip.SetState(this, mark.attrs.opened, spos, epos, mark);
+                FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark);
             }
             if (mark && child && nbef && naft) {
-                SelectionSizeTooltip.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified;
+                FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " " + mark.attrs.modified;
                 // These are in screen coordinates
                 // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
                 let start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
@@ -114,23 +114,23 @@ export class SelectionSizeTooltip {
                 // Find a center-ish x position from the selection endpoints (when
                 // crossing lines, end may be more to the left)
                 let left = Math.max((start.left + end.left) / 2, start.left + 3);
-                SelectionSizeTooltip.tooltip.style.left = (left - box.left) + "px";
-                SelectionSizeTooltip.tooltip.style.bottom = (box.bottom - start.top) + "px";
+                FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+                FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
                 set = "";
             }
         }
         if (set === "none" && state.selection.$from) {
-            SelectionSizeTooltip.textBox = undefined;
+            FormattedTextBoxComment.textBox = undefined;
             let nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
             let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
             let child = state.selection.$from.nodeBefore;
             let mark = child && findLinkMark(child.marks);
             if (mark && child && nbef && naft) {
-                SelectionSizeTooltip.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href);
+                FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href);
                 if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
                     let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     docTarget && DocServer.GetRefField(docTarget).then(linkDoc =>
-                        (linkDoc as Doc) && (SelectionSizeTooltip.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc)!.title)));
+                        (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc)!.title)));
                 }
                 // These are in screen coordinates
                 // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
@@ -140,13 +140,13 @@ export class SelectionSizeTooltip {
                 // Find a center-ish x position from the selection endpoints (when
                 // crossing lines, end may be more to the left)
                 let left = Math.max((start.left + end.left) / 2, start.left + 3);
-                SelectionSizeTooltip.tooltip.style.left = (left - box.left) + "px";
-                SelectionSizeTooltip.tooltip.style.bottom = (box.bottom - start.top) + "px";
+                FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+                FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
                 set = "";
             }
         }
-        SelectionSizeTooltip.tooltip && (SelectionSizeTooltip.tooltip.style.display = set);
+        FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
     }
 
-    destroy() { SelectionSizeTooltip.tooltip.style.display = "none"; }
+    destroy() { FormattedTextBoxComment.tooltip.style.display = "none"; }
 }
-- 
cgit v1.2.3-70-g09d2


From a4d36f835b5c43351d1761034b61513b000445ba Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 3 Sep 2019 16:19:20 -0400
Subject: added inline footnote.

---
 src/client/util/ProsemirrorExampleTransfer.ts |  13 +++
 src/client/util/RichTextSchema.tsx            | 159 +++++++++++++++++++++++++-
 src/client/views/nodes/FormattedTextBox.scss  |  47 ++++++++
 src/client/views/nodes/FormattedTextBox.tsx   |  30 ++++-
 4 files changed, 240 insertions(+), 9 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index bcb8b404b..419311df8 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -51,6 +51,19 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
 
     bind("Ctrl->", wrapIn(schema.nodes.blockquote));
 
+    bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+        let newNode = schema.nodes.footnote.create({});
+        if (dispatch && state.selection.from === state.selection.to) {
+            let tr = state.tr;
+            tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+            dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+                tr.doc.resolve(  // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+                    tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+            return true;
+        }
+        return false;
+    })
+
 
     let cmd = chainCommands(exitCode, (state, dispatch) => {
         if (dispatch) {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 7911cf629..25d972857 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,7 +1,12 @@
 import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
 import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { TextSelection } from "prosemirror-state";
+import { TextSelection, EditorState } from "prosemirror-state";
 import { Doc } from "../../new_fields/Doc";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import { keymap } from "prosemirror-keymap";
+import { undo, redo } from "prosemirror-history";
+import { toggleMark, splitBlock, selectAll, baseKeymap } from "prosemirror-commands";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -14,6 +19,20 @@ export const nodes: { [index: string]: NodeSpec } = {
         content: "block+"
     },
 
+    footnote: {
+        group: "inline",
+        content: "inline*",
+        inline: true,
+        attrs: {
+            visibility: { default: false }
+        },
+        // This makes the view treat the node as a leaf, even though it
+        // technically has content
+        atom: true,
+        toDOM: () => ["footnote", 0],
+        parseDOM: [{ tag: "footnote" }]
+    },
+
     // :: NodeSpec A plain paragraph textblock. Represented in the DOM
     // as a `<p>` element.
     paragraph: {
@@ -177,7 +196,7 @@ export const nodes: { [index: string]: NodeSpec } = {
         group: 'block',
         attrs: {
             bulletStyle: { default: 0 },
-            mapStyle: { default: "decimal" }
+            mapStyle: { default: "decimal" },
         },
         toDOM(node: Node<any>) {
             const bs = node.attrs.bulletStyle;
@@ -186,7 +205,8 @@ export const nodes: { [index: string]: NodeSpec } = {
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
             for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map;
             return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0];
-            //return ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle};`, 0]
+            //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
+            //     ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"];
         }
     },
     //this doesn't currently work for some reason
@@ -313,10 +333,10 @@ export const marks: { [index: string]: MarkSpec } = {
     },
 
     highlight: {
-        parseDOM: [{ style: 'color: blue' }],
+        parseDOM: [{ style: 'text-decoration: underline' }],
         toDOM() {
             return ['span', {
-                style: 'color: blue'
+                style: 'text-decoration: underline; text-decoration-color: rgba(204, 206, 210, 0.92)'
             }];
         }
     },
@@ -581,6 +601,133 @@ export class OrderedListView {
     }
 }
 
+export class FootnoteView {
+    innerView: any;
+    outerView: any;
+    node: any;
+    dom: any;
+    getPos: any;
+
+    constructor(node: any, view: any, getPos: any) {
+        // We'll need these later
+        this.node = node
+        this.outerView = view
+        this.getPos = getPos
+
+        // The node's representation in the editor (empty, for now)
+        this.dom = document.createElement("footnote");
+        this.dom.addEventListener("pointerup", this.toggle, true);
+        // These are used when the footnote is selected
+        this.innerView = null
+    }
+    selectNode() {
+        const attrs = { ...this.node.attrs };
+        attrs.visibility = true;
+        this.dom.classList.add("ProseMirror-selectednode")
+        if (!this.innerView) this.open()
+    }
+
+    deselectNode() {
+        const attrs = { ...this.node.attrs };
+        attrs.visibility = false;
+        this.dom.classList.remove("ProseMirror-selectednode")
+        if (this.innerView) this.close()
+    }
+    open() {
+        if (!(this.outerView as any).isOverlay) return;
+        // Append a tooltip to the outer node
+        let tooltip = this.dom.appendChild(document.createElement("div"))
+        tooltip.className = "footnote-tooltip";
+        // And put a sub-ProseMirror into that
+        this.innerView = new EditorView(tooltip, {
+            // You can use any node as an editor document
+            state: EditorState.create({
+                doc: this.node,
+                plugins: [keymap(baseKeymap),
+                keymap({
+                    "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+                    "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+                    "Mod-b": toggleMark(schema.marks.strong)
+                })]
+            }),
+            // This is the magic part
+            dispatchTransaction: this.dispatchInner.bind(this),
+            handleDOMEvents: {
+                pointerdown: ((view: any, e: PointerEvent) => {
+                    // Kludge to prevent issues due to the fact that the whole
+                    // footnote is node-selected (and thus DOM-selected) when
+                    // the parent editor is focused.
+                    e.stopPropagation();
+                    document.addEventListener("pointerup", this.ignore, true);
+                    if (this.outerView.hasFocus()) this.innerView.focus();
+                }) as any
+            }
+
+        });
+        setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+    }
+
+    ignore = (e: PointerEvent) => {
+        e.stopPropagation();
+        document.removeEventListener("pointerup", this.ignore, true);
+    }
+
+    toggle = () => {
+        if (this.innerView) this.close();
+        else {
+            this.open();
+
+        }
+    }
+    close() {
+        this.innerView && this.innerView.destroy()
+        this.innerView = null
+        this.dom.textContent = ""
+    }
+    dispatchInner(tr: any) {
+        let { state, transactions } = this.innerView.state.applyTransaction(tr)
+        this.innerView.updateState(state)
+
+        if (!tr.getMeta("fromOutside")) {
+            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
+            for (let i = 0; i < transactions.length; i++) {
+                let steps = transactions[i].steps
+                for (let j = 0; j < steps.length; j++)
+                    outerTr.step(steps[j].map(offsetMap))
+            }
+            if (outerTr.docChanged) this.outerView.dispatch(outerTr)
+        }
+    }
+    update(node: any) {
+        if (!node.sameMarkup(this.node)) return false
+        this.node = node
+        if (this.innerView) {
+            let state = this.innerView.state
+            let start = node.content.findDiffStart(state.doc.content)
+            if (start != null) {
+                let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content)
+                let overlap = start - Math.min(endA, endB)
+                if (overlap > 0) { endA += overlap; endB += overlap }
+                this.innerView.dispatch(
+                    state.tr
+                        .replace(start, endB, node.slice(start, endA))
+                        .setMeta("fromOutside", true))
+            }
+        }
+        return true
+    }
+
+    destroy() {
+        if (this.innerView) this.close()
+    }
+
+    stopEvent(event: any) {
+        return this.innerView && this.innerView.dom.contains(event.target)
+    }
+
+    ignoreMutation() { return true }
+}
+
 export class SummarizedView {
     // TODO: highlight text that is summarized. to find end of region, walk along mark
     _collapsed: HTMLElement;
@@ -648,7 +795,7 @@ export class SummarizedView {
             let skip = false;
             this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
                 if (node.isLeaf && !visited.has(node) && !skip) {
-                    if (node.marks.includes(_mark)) {
+                    if (node.marks.find((m: any) => m.type === _mark.type)) {
                         visited.add(node);
                         endPos = i + node.nodeSize - 1;
                     }
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index dd07f5924..8f47402c4 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -84,6 +84,53 @@
     align-content: center;
 }
 
+footnote {
+    display: inline-block;
+    position: relative;
+    cursor: pointer;
+    div {
+        padding : 0 !important;
+    }
+}
+
+footnote::after {
+    content: counter(prosemirror-footnote);
+    vertical-align: super;
+    font-size: 75%;
+    counter-increment: prosemirror-footnote;
+}
+
+.ProseMirror {
+    counter-reset: prosemirror-footnote;
+  }
+
+.footnote-tooltip {
+    cursor: auto;
+    font-size: 75%;
+    position: absolute;
+    left: -30px;
+    top: calc(100% + 10px);
+    background: silver;
+    padding: 3px;
+    border-radius: 2px;
+    max-width: 100px;
+    min-width: 50px;
+    width: max-content;
+}
+
+.footnote-tooltip::before {
+    border: 5px solid silver;
+    border-top-width: 0px;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    position: absolute;
+    top: -5px;
+    left: 27px;
+    content: " ";
+    height: 0;
+    width: 0;
+}
+
 ol { counter-reset: deci1 0;}
 .decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
 .decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6232dd3ab..bb44c5ac6 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -22,7 +22,7 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from "../../util/DragManager";
 import buildKeymap from "../../util/ProsemirrorExampleTransfer";
 import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView, OrderedListView } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
 import { SelectionManager } from "../../util/SelectionManager";
 import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
 import { TooltipTextMenu } from "../../util/TooltipTextMenu";
@@ -168,10 +168,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    // 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;
+            }
+        }
+    }
+
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
             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);
             }
@@ -612,12 +635,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 nodeViews: {
                     image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
                     star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
-                    ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }
-
+                    ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); },
+                    footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) }
                 },
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
             });
+            (this._editorView as any).isOverlay = this.props.isOverlay;
             if (startup) {
                 Doc.GetProto(doc).documentText = undefined;
                 this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
-- 
cgit v1.2.3-70-g09d2


From 155cf5c5a1b45aafe7a5632f2ee6ecf957f04dde Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 3 Sep 2019 23:07:42 -0400
Subject: fixes for bullets

---
 src/client/util/ProsemirrorExampleTransfer.ts | 59 +++++++++++++++++++--------
 src/client/util/RichTextSchema.tsx            | 11 +++--
 2 files changed, 48 insertions(+), 22 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 419311df8..5016e72c6 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -93,35 +93,63 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
 
     bind("Mod-s", TooltipTextMenu.insertStar);
 
+    let updateOrderedList = (start: number, rangeStart: any, delta: number, tx2: Transaction, forward: boolean) => {
+        let bs = rangeStart.attrs.bulletStyle;
+        bs + delta > 0 && tx2.setNodeMarkup(start, rangeStart.type, { mapStyle: rangeStart.attrs.mapStyle, bulletStyle: rangeStart.attrs.bulletStyle + delta }, rangeStart.marks);
+
+        let brk = false;
+        rangeStart.descendants((node: any, offset: any, index: any) => {
+            if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+                if (!brk && (bs !== node.attrs.bulletStyle || delta > 0 || (forward && bs > 1))) {
+                    tx2.setNodeMarkup(start + offset + 1, node.type, { mapStyle: node.attrs.mapStyle, bulletStyle: node.attrs.bulletStyle + delta }, node.marks);
+                } else {
+                    brk = true;
+                }
+            }
+        });
+    }
+
     let updateBullets = (tx2: Transaction, refStart: number, delta: number) => {
-        for (let i = refStart; i > 0; i--) {
+        let i = refStart;
+        for (let i = refStart; i >= 0; i--) {
             let testPos = tx2.doc.nodeAt(i);
-            if (testPos && testPos.type === schema.nodes.list_item) {
+            if (!testPos) {
+                for (let i = refStart + 1; i <= tx2.doc.nodeSize; i++) {
+                    try {
+                        let testPos = tx2.doc.nodeAt(i);
+                        if (testPos && testPos.type === schema.nodes.ordered_list) {
+                            updateOrderedList(i, testPos, delta, tx2, true);
+                            break;
+                        }
+                    } catch (e) {
+                        break;
+                    }
+                }
+                break;
+            }
+            if ((testPos.type === schema.nodes.list_item || testPos.type === schema.nodes.ordered_list)) {
                 let start = i;
                 let preve = i > 0 && tx2.doc.nodeAt(start - 1);
                 if (preve && preve.type === schema.nodes.ordered_list) {
                     start = start - 1;
                 }
                 let rangeStart = tx2.doc.nodeAt(start);
-                if (rangeStart && rangeStart.type === schema.nodes.ordered_list) {
-                    tx2.setNodeMarkup(start, rangeStart.type, { ...rangeStart.attrs, bulletStyle: rangeStart.attrs.bulletStyle + delta }, rangeStart.marks);
+                if (rangeStart) {
+                    updateOrderedList(start, rangeStart, delta, tx2, false);
                 }
-                rangeStart && rangeStart.descendants((node: any, offset: any, index: any) => {
-                    if (node.type === schema.nodes.ordered_list) {
-                        tx2.setNodeMarkup(start + offset + 1, node.type, { ...node.attrs, bulletStyle: node.attrs.bulletStyle + delta }, node.marks);
-                    }
-                });
                 break;
             }
         }
     }
 
+
     bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         var ref = state.selection;
         var range = ref.$from.blockRange(ref.$to);
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
-            updateBullets(tx2, range!.start, 1);
+            var range = state.selection.$from.blockRange(state.selection.$to);
+            updateBullets(tx2, range!.start - 1, 1);
             marks && tx2.ensureMarks([...marks]);
             marks && tx2.setStoredMarks([...marks]);
             dispatch(tx2);
@@ -129,13 +157,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
             let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end));
             let newstate = state.applyTransaction(sxf);
             if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
-                for (let i = range!.start; i >= 0; i--) {
-                    let rangeStart = tx2.doc.nodeAt(i);
-                    if (rangeStart && rangeStart.type === schema.nodes.ordered_list) {
-                        tx2.setNodeMarkup(i, rangeStart.type, { ...rangeStart.attrs, bulletStyle: 1 }, rangeStart.marks);
-                        break;
-                    }
-                }
+                updateBullets(tx2, range!.start, 1);
                 // when promoting to a list, assume list will format things so don't copy the stored marks.
                 marks && tx2.ensureMarks([...marks]);
                 marks && tx2.setStoredMarks([...marks]);
@@ -151,9 +173,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
 
         let tr = state.tr;
-        updateBullets(tr, range!.start, -1);
 
         if (!liftListItem(schema.nodes.list_item)(tr, (tx2: Transaction) => {
+            var range = tx2.selection.$from.blockRange(tx2.selection.$to);
+            updateBullets(tx2, range!.start, -1);
             marks && tx2.ensureMarks([...marks]);
             marks && tx2.setStoredMarks([...marks]);
             dispatch(tx2);
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 25d972857..75e982872 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -203,7 +203,6 @@ export const nodes: { [index: string]: NodeSpec } = {
             const decMap = bs ? "decimal" + bs : "";
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
-            for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map;
             return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0];
             //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
             //     ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"];
@@ -216,7 +215,6 @@ export const nodes: { [index: string]: NodeSpec } = {
         group: 'block',
         // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
         toDOM(node: Node<any>) {
-            for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = "";
             return ['ul', 0];
         }
     },
@@ -231,12 +229,17 @@ export const nodes: { [index: string]: NodeSpec } = {
     // },
     list_item: {
         attrs: {
-            className: { default: "" }
+            bulletStyle: { default: 0 },
+            mapStyle: { default: "decimal" },
         },
         ...listItem,
         content: 'paragraph block*',
         toDOM(node: any) {
-            return ["li", { class: node.attrs.className }, 0];
+            const bs = node.attrs.bulletStyle;
+            const decMap = bs ? "decimal" + bs : "";
+            const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
+            let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
+            return ["li", { class: `${map}` }, 0];
         }
     },
 };
-- 
cgit v1.2.3-70-g09d2


From 2262d7562b57c37c238fc8a7c373304984a5fe3b Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 4 Sep 2019 17:49:19 -0400
Subject: prosemirror restructuring.

---
 src/client/util/ProsemirrorExampleTransfer.ts      | 71 +++++-----------------
 src/client/util/RichTextSchema.tsx                 | 71 +++++++++++-----------
 src/client/util/TooltipTextMenu.tsx                | 37 ++++++-----
 src/client/util/prosemirrorPatches.js              | 30 +++++++++
 src/client/views/OverlayView.tsx                   |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  6 +-
 src/client/views/nodes/FormattedTextBoxComment.tsx |  4 +-
 src/client/views/pdf/Page.tsx                      |  2 +-
 8 files changed, 107 insertions(+), 116 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 5016e72c6..9c4b2d3d1 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -2,8 +2,8 @@ import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setB
 import { redo, undo } from "prosemirror-history";
 import { undoInputRule } from "prosemirror-inputrules";
 import { Schema } from "prosemirror-model";
-import { liftListItem, } from "./prosemirrorPatches.js";
-import { splitListItem, wrapInList, sinkListItem } from "prosemirror-schema-list";
+import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
+import { splitListItem, wrapInList, } from "prosemirror-schema-list";
 import { EditorState, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
 import { TooltipTextMenu } from "./TooltipTextMenu";
 
@@ -62,7 +62,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
             return true;
         }
         return false;
-    })
+    });
 
 
     let cmd = chainCommands(exitCode, (state, dispatch) => {
@@ -93,54 +93,16 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
 
     bind("Mod-s", TooltipTextMenu.insertStar);
 
-    let updateOrderedList = (start: number, rangeStart: any, delta: number, tx2: Transaction, forward: boolean) => {
-        let bs = rangeStart.attrs.bulletStyle;
-        bs + delta > 0 && tx2.setNodeMarkup(start, rangeStart.type, { mapStyle: rangeStart.attrs.mapStyle, bulletStyle: rangeStart.attrs.bulletStyle + delta }, rangeStart.marks);
-
-        let brk = false;
-        rangeStart.descendants((node: any, offset: any, index: any) => {
+    let updateBullets = (tx2: Transaction) => {
+        tx2.doc.descendants((node: any, offset: any, index: any) => {
             if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
-                if (!brk && (bs !== node.attrs.bulletStyle || delta > 0 || (forward && bs > 1))) {
-                    tx2.setNodeMarkup(start + offset + 1, node.type, { mapStyle: node.attrs.mapStyle, bulletStyle: node.attrs.bulletStyle + delta }, node.marks);
-                } else {
-                    brk = true;
-                }
+                let path = (tx2.doc.resolve(offset) as any).path;
+                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+                if (node.type === schema.nodes.ordered_list) depth++;
+                tx2.setNodeMarkup(offset, node.type, { mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
             }
         });
-    }
-
-    let updateBullets = (tx2: Transaction, refStart: number, delta: number) => {
-        let i = refStart;
-        for (let i = refStart; i >= 0; i--) {
-            let testPos = tx2.doc.nodeAt(i);
-            if (!testPos) {
-                for (let i = refStart + 1; i <= tx2.doc.nodeSize; i++) {
-                    try {
-                        let testPos = tx2.doc.nodeAt(i);
-                        if (testPos && testPos.type === schema.nodes.ordered_list) {
-                            updateOrderedList(i, testPos, delta, tx2, true);
-                            break;
-                        }
-                    } catch (e) {
-                        break;
-                    }
-                }
-                break;
-            }
-            if ((testPos.type === schema.nodes.list_item || testPos.type === schema.nodes.ordered_list)) {
-                let start = i;
-                let preve = i > 0 && tx2.doc.nodeAt(start - 1);
-                if (preve && preve.type === schema.nodes.ordered_list) {
-                    start = start - 1;
-                }
-                let rangeStart = tx2.doc.nodeAt(start);
-                if (rangeStart) {
-                    updateOrderedList(start, rangeStart, delta, tx2, false);
-                }
-                break;
-            }
-        }
-    }
+    };
 
 
     bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -148,8 +110,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         var range = ref.$from.blockRange(ref.$to);
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
-            var range = state.selection.$from.blockRange(state.selection.$to);
-            updateBullets(tx2, range!.start - 1, 1);
+            updateBullets(tx2);
             marks && tx2.ensureMarks([...marks]);
             marks && tx2.setStoredMarks([...marks]);
             dispatch(tx2);
@@ -157,7 +118,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
             let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end));
             let newstate = state.applyTransaction(sxf);
             if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
-                updateBullets(tx2, range!.start, 1);
+                updateBullets(tx2);
                 // when promoting to a list, assume list will format things so don't copy the stored marks.
                 marks && tx2.ensureMarks([...marks]);
                 marks && tx2.setStoredMarks([...marks]);
@@ -169,14 +130,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     });
 
     bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        var range = state.selection.$from.blockRange(state.selection.$to);
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
 
-        let tr = state.tr;
-
-        if (!liftListItem(schema.nodes.list_item)(tr, (tx2: Transaction) => {
-            var range = tx2.selection.$from.blockRange(tx2.selection.$to);
-            updateBullets(tx2, range!.start, -1);
+        if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+            updateBullets(tx2);
             marks && tx2.ensureMarks([...marks]);
             marks && tx2.setStoredMarks([...marks]);
             dispatch(tx2);
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 75e982872..3174d668c 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -597,8 +597,6 @@ export class ImageResizeView {
 }
 
 export class OrderedListView {
-    constructor(node: any, view: any, getPos: any) { }
-
     update(node: any) {
         return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
     }
@@ -613,33 +611,33 @@ export class FootnoteView {
 
     constructor(node: any, view: any, getPos: any) {
         // We'll need these later
-        this.node = node
-        this.outerView = view
-        this.getPos = getPos
+        this.node = node;
+        this.outerView = view;
+        this.getPos = getPos;
 
         // The node's representation in the editor (empty, for now)
         this.dom = document.createElement("footnote");
         this.dom.addEventListener("pointerup", this.toggle, true);
         // These are used when the footnote is selected
-        this.innerView = null
+        this.innerView = null;
     }
     selectNode() {
         const attrs = { ...this.node.attrs };
         attrs.visibility = true;
-        this.dom.classList.add("ProseMirror-selectednode")
-        if (!this.innerView) this.open()
+        this.dom.classList.add("ProseMirror-selectednode");
+        if (!this.innerView) this.open();
     }
 
     deselectNode() {
         const attrs = { ...this.node.attrs };
         attrs.visibility = false;
-        this.dom.classList.remove("ProseMirror-selectednode")
-        if (this.innerView) this.close()
+        this.dom.classList.remove("ProseMirror-selectednode");
+        if (this.innerView) this.close();
     }
     open() {
-        if (!(this.outerView as any).isOverlay) return;
+        if (!this.outerView.isOverlay) return;
         // Append a tooltip to the outer node
-        let tooltip = this.dom.appendChild(document.createElement("div"))
+        let tooltip = this.dom.appendChild(document.createElement("div"));
         tooltip.className = "footnote-tooltip";
         // And put a sub-ProseMirror into that
         this.innerView = new EditorView(tooltip, {
@@ -679,56 +677,55 @@ export class FootnoteView {
         if (this.innerView) this.close();
         else {
             this.open();
-
         }
     }
     close() {
-        this.innerView && this.innerView.destroy()
-        this.innerView = null
-        this.dom.textContent = ""
+        this.innerView && this.innerView.destroy();
+        this.innerView = null;
+        this.dom.textContent = "";
     }
     dispatchInner(tr: any) {
-        let { state, transactions } = this.innerView.state.applyTransaction(tr)
-        this.innerView.updateState(state)
+        let { state, transactions } = this.innerView.state.applyTransaction(tr);
+        this.innerView.updateState(state);
 
         if (!tr.getMeta("fromOutside")) {
-            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
-            for (let i = 0; i < transactions.length; i++) {
-                let steps = transactions[i].steps
-                for (let j = 0; j < steps.length; j++)
-                    outerTr.step(steps[j].map(offsetMap))
+            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+            for (let steps of transactions) {
+                for (let step of steps) {
+                    outerTr.step(step.map(offsetMap));
+                }
             }
-            if (outerTr.docChanged) this.outerView.dispatch(outerTr)
+            if (outerTr.docChanged) this.outerView.dispatch(outerTr);
         }
     }
     update(node: any) {
-        if (!node.sameMarkup(this.node)) return false
-        this.node = node
+        if (!node.sameMarkup(this.node)) return false;
+        this.node = node;
         if (this.innerView) {
-            let state = this.innerView.state
-            let start = node.content.findDiffStart(state.doc.content)
-            if (start != null) {
-                let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content)
-                let overlap = start - Math.min(endA, endB)
-                if (overlap > 0) { endA += overlap; endB += overlap }
+            let state = this.innerView.state;
+            let start = node.content.findDiffStart(state.doc.content);
+            if (start !== null) {
+                let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+                let overlap = start - Math.min(endA, endB);
+                if (overlap > 0) { endA += overlap; endB += overlap; }
                 this.innerView.dispatch(
                     state.tr
                         .replace(start, endB, node.slice(start, endA))
-                        .setMeta("fromOutside", true))
+                        .setMeta("fromOutside", true));
             }
         }
-        return true
+        return true;
     }
 
     destroy() {
-        if (this.innerView) this.close()
+        if (this.innerView) this.close();
     }
 
     stopEvent(event: any) {
-        return this.innerView && this.innerView.dom.contains(event.target)
+        return this.innerView && this.innerView.dom.contains(event.target);
     }
 
-    ignoreMutation() { return true }
+    ignoreMutation() { return true; }
 }
 
 export class SummarizedView {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index ce7b04e31..6d375fc1d 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -4,7 +4,7 @@ import { action, observable } from "mobx";
 import { Dropdown, icons, MenuItem } from "prosemirror-menu"; //no import css
 import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
 import { wrapInList } from 'prosemirror-schema-list';
-import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection, Transaction } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { Doc, Field, Opt } from "../../new_fields/Doc";
 import { Id } from "../../new_fields/FieldSymbols";
@@ -509,30 +509,37 @@ export class TooltipTextMenu {
         }
     }
 
+    updateBullets = (tx2: Transaction, style: string) => {
+        tx2.doc.descendants((node: any, offset: any, index: any) => {
+            if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+                let path = (tx2.doc.resolve(offset) as any).path;
+                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+                if (node.type === schema.nodes.ordered_list) depth++;
+                tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks);
+            }
+        });
+    };
     //remove all node typeand apply the passed-in one to the selected text
-    changeToNodeType(nodeType: NodeType | undefined, view: EditorView) {
+    changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
         //remove oldif (nodeType) { //add new
         if (nodeType === schema.nodes.bullet_list) {
             wrapInList(nodeType)(view.state, view.dispatch);
         } else {
-            var ref = view.state.selection;
-            var range = ref.$from.blockRange(ref.$to);
             var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
-            wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
-                const resolvedPos = tx2.doc.resolve(Math.round((range!.start + range!.end) / 2));
-                let path = resolvedPos.path;
-                for (let i = path.length - 1; i > 0; i--) {
-                    if (path[i].type === schema.nodes.ordered_list) {
-                        path[i].attrs.bulletStyle = 1;
-                        path[i].attrs.mapStyle = (nodeType as any).attrs.mapStyle;
-                        break;
-                    }
-                }
+            if (!wrapInList(schema.nodes.ordered_list)(view.state, (tx2: any) => {
+                this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
                 marks && tx2.ensureMarks([...marks]);
                 marks && tx2.setStoredMarks([...marks]);
 
                 view.dispatch(tx2);
-            });
+            })) {
+                let tx2 = view.state.tr;
+                this.updateBullets(tx2, (nodeType as any).attrs.mapStyle);
+                marks && tx2.ensureMarks([...marks]);
+                marks && tx2.setStoredMarks([...marks]);
+
+                view.dispatch(tx2);
+            }
         }
     }
 
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
index c273c2323..6bf4395ad 100644
--- a/src/client/util/prosemirrorPatches.js
+++ b/src/client/util/prosemirrorPatches.js
@@ -6,6 +6,7 @@ var prosemirrorTransform = require('prosemirror-transform');
 var prosemirrorModel = require('prosemirror-model');
 
 exports.liftListItem = liftListItem;
+exports.sinkListItem = sinkListItem;
 // :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
 // Create a command to lift the list item around the selection up into
 // a wrapping list.
@@ -59,4 +60,33 @@ function liftOutOfList(tr, dispatch, range) {
             atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
     dispatch(tr.scrollIntoView());
     return true
+}
+
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to sink the list item around the selection down
+// into an inner list.
+function sinkListItem(itemType) {
+    return function (state, dispatch) {
+        var ref = state.selection;
+        var $from = ref.$from;
+        var $to = ref.$to;
+        var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+        if (!range) { return false }
+        var startIndex = range.startIndex;
+        if (startIndex == 0) { return false }
+        var parent = range.parent, nodeBefore = parent.child(startIndex - 1);
+        if (nodeBefore.type != itemType) { return false; }
+
+        if (dispatch) {
+            var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
+            var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
+            let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create(parent.attrs, inner)))),
+                nestedBefore ? 3 : 1, 0);
+            var before = range.start, after = range.end;
+            dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
+                before, after, slice, 1, true))
+                .scrollIntoView());
+        }
+        return true
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 54ab8696e..fe06e4440 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -191,7 +191,7 @@ export class OverlayView extends React.Component {
                     zoomToScale={emptyFunction}
                     getScale={returnOne} />
             </div>;
-        })
+        });
     }
 
     render() {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 4cb52ec78..f9ad040a2 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -174,7 +174,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     syncNodeSelection(view: any, sel: any) {
         if (sel instanceof NodeSelection) {
             var desc = view.docView.descAt(sel.from);
-            if (desc != view.lastSelectedViewDesc) {
+            if (desc !== view.lastSelectedViewDesc) {
                 if (view.lastSelectedViewDesc) {
                     view.lastSelectedViewDesc.deselectNode();
                     view.lastSelectedViewDesc = null;
@@ -635,8 +635,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 nodeViews: {
                     image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
                     star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
-                    ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); },
-                    footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) }
+                    ordered_list(node, view, getPos) { return new OrderedListView(); },
+                    footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
                 },
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 255000936..2d2fff06d 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -92,7 +92,7 @@ export class FormattedTextBoxComment {
         if (lastState && lastState.doc.eq(state.doc) &&
             lastState.selection.eq(state.selection)) return;
 
-        let set = "none"
+        let set = "none";
         if (state.selection.$from) {
             let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
             let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
@@ -130,7 +130,7 @@ export class FormattedTextBoxComment {
                 if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
                     let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     docTarget && DocServer.GetRefField(docTarget).then(linkDoc =>
-                        (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc)!.title)));
+                        (linkDoc as Doc) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc).title)));
                 }
                 // These are in screen coordinates
                 // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index 8df2dce29..856e883e7 100644
--- a/src/client/views/pdf/Page.tsx
+++ b/src/client/views/pdf/Page.tsx
@@ -64,7 +64,7 @@ export default class Page extends React.Component<IPageProps> {
         // lower scale = easier to read at small sizes, higher scale = easier to read at large sizes
         if (this._state !== "rendering" && !this._page && this._canvas.current && this._textLayer.current) {
             this._state = "rendering";
-            let viewport = page.getViewport({ scale: scale });
+            let viewport = page.getViewport(scale);
             this._canvas.current.width = this._width = viewport.width;
             this._canvas.current.height = this._height = viewport.height;
             this.props.pageLoaded(viewport);
-- 
cgit v1.2.3-70-g09d2


From a017d20fb40ffb5b99b06b716ac5a6f8635c45e6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 4 Sep 2019 20:43:28 -0400
Subject: added collapsing bullets

---
 src/client/util/ProsemirrorExampleTransfer.ts |  2 +-
 src/client/util/RichTextSchema.tsx            |  8 ++++++--
 src/client/views/nodes/FormattedTextBox.tsx   | 10 ++++++++++
 3 files changed, 17 insertions(+), 3 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 9c4b2d3d1..9f6da7ade 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -99,7 +99,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
                 let path = (tx2.doc.resolve(offset) as any).path;
                 let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
                 if (node.type === schema.nodes.ordered_list) depth++;
-                tx2.setNodeMarkup(offset, node.type, { mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
+                tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
             }
         });
     };
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 3174d668c..2b689b7ef 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -197,13 +197,15 @@ export const nodes: { [index: string]: NodeSpec } = {
         attrs: {
             bulletStyle: { default: 0 },
             mapStyle: { default: "decimal" },
+            visibility: { default: true }
         },
         toDOM(node: Node<any>) {
             const bs = node.attrs.bulletStyle;
             const decMap = bs ? "decimal" + bs : "";
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
-            return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0];
+            return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
+                 ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
             //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
             //     ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"];
         }
@@ -231,6 +233,7 @@ export const nodes: { [index: string]: NodeSpec } = {
         attrs: {
             bulletStyle: { default: 0 },
             mapStyle: { default: "decimal" },
+            visibility: { default: true }
         },
         ...listItem,
         content: 'paragraph block*',
@@ -239,7 +242,8 @@ export const nodes: { [index: string]: NodeSpec } = {
             const decMap = bs ? "decimal" + bs : "";
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
-            return ["li", { class: `${map}` }, 0];
+            return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
+            //return ["li", { class: `${map}` }, 0];
         }
     },
 };
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f9ad040a2..2dbfeeba7 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -672,6 +672,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
+        let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+        if (pos) {
+            if (e.nativeEvent.offsetX < 40) {
+                let node = this._editorView!.state.doc.nodeAt(pos.pos);
+                let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
+                if (node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
+                    this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
+                }
+            }
+        }
         if (this.props.onClick && e.button === 0) {
             e.preventDefault();
         }
-- 
cgit v1.2.3-70-g09d2


From 179b2c7c68e3d240f3f39083bdb686ad31761c04 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 5 Sep 2019 12:00:18 -0400
Subject: fixed some template issues.  added attribution for pasted text

---
 src/client/util/RichTextSchema.tsx                 | 11 ++++--
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 .../views/collections/CollectionStackingView.tsx   |  4 ++
 src/client/views/nodes/FormattedTextBox.scss       |  3 ++
 src/client/views/nodes/FormattedTextBox.tsx        | 44 ++++------------------
 src/client/views/nodes/PDFBox.tsx                  | 15 +++++---
 src/new_fields/Doc.ts                              |  4 +-
 7 files changed, 35 insertions(+), 48 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 2b689b7ef..9c38ee458 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -205,7 +205,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
             return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
-                 ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+                ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
             //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
             //     ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"];
         }
@@ -262,7 +262,8 @@ export const marks: { [index: string]: MarkSpec } = {
         attrs: {
             href: {},
             location: { default: null },
-            title: { default: null }
+            title: { default: null },
+            docref: { default: false }
         },
         inclusive: false,
         parseDOM: [{
@@ -270,7 +271,11 @@ export const marks: { [index: string]: MarkSpec } = {
                 return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") };
             }
         }],
-        toDOM(node: any) { return ["a", node.attrs, 0]; }
+        toDOM(node: any) {
+            return node.attrs.docref && node.attrs.title ?
+                ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] :
+                ["a", { ...node.attrs }, 0];
+        }
     },
 
     // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 95f94875c..fb8b0c41b 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -607,7 +607,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
         return Transform.Identity();
     }
-    get previewPanelCenteringOffset() { return this.nativeWidth && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; }
+    get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; }
 
     addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
         if (doc.dockingConfig) {
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 97b31bf2a..654ff2279 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -310,6 +310,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     sectionMasonry(heading: SchemaHeaderField | undefined, docList: Doc[]) {
         let cols = Math.max(1, Math.min(docList.length,
             Math.floor((this.props.PanelWidth() - 2 * this.xMargin) / (this.columnWidth + this.gridGap))));
+        if (isNaN(cols)) {
+            console.log("naN");
+            cols = 1;
+        }
         return <div key={heading ? heading.heading : "empty"} className="collectionStackingView-masonrySection">
             {!heading ? (null) :
                 <div key={`${heading.heading}`} className="collectionStackingView-sectionHeader" style={{ background: heading.color }}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 8f47402c4..b27ff0fb9 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -118,6 +118,9 @@ footnote::after {
     width: max-content;
 }
 
+.prosemirror-attribution {
+    font-size: 8px;
+}
 .footnote-tooltip::before {
     border: 5px solid silver;
     border-top-width: 0px;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d811273e8..7c3895336 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -141,37 +141,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
 
-    paste = (e: ClipboardEvent) => {
-        if (e.clipboardData && this._editorView) {
-            // let pdfPasteText = `${Utils.GenerateDeterministicGuid("pdf paste")}`;
-            // for (let i = 0; i < e.clipboardData.items.length; i++) {
-            //     let item = e.clipboardData.items.item(i);
-            //     console.log(item)
-            //     if (item.type === "text/plain") {
-            //         console.log("plain")
-            //         item.getAsString((text) => {
-            //             let pdfPasteIndex = text.indexOf(pdfPasteText);
-            //             if (pdfPasteIndex > -1) {
-            //                 let insertText = text.substr(0, pdfPasteIndex);
-            //                 const tx = this._editorView!.state.tr.insertText(insertText);
-            //                 // tx.setSelection(new Selection(tx.))
-            //                 const state = this._editorView!.state;
-            //                 this._editorView!.dispatch(tx);
-            //                 if (FormattedTextBox._toolTipTextMenu) {
-            //                     // this._toolTipTextMenu.makeLinkWithState(state)
-            //                 }
-            //                 e.stopPropagation();
-            //                 e.preventDefault();
-            //             }
-            //         });
-            //     }
-            // }
-        }
-    }
-
     // 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);
@@ -363,8 +336,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     componentDidMount() {
-        document.addEventListener("paste", this.paste);
-
         if (!this.props.isOverlay) {
             this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
                 () => {
@@ -584,7 +555,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 if (link) {
                     cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
                     linkId = link[Id];
-                    let frag = addMarkToFrag(slice.content);
+                    let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title)));
                     slice = new Slice(frag, slice.openStart, slice.openEnd);
                     var tr = view.state.tr.replaceSelection(slice);
                     view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
@@ -594,19 +565,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
         return true;
 
-        function addMarkToFrag(frag: Fragment) {
+        function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
             const nodes: Node[] = [];
-            frag.forEach(node => nodes.push(addLinkMark(node)));
+            frag.forEach(node => nodes.push(marker(node)));
             return Fragment.fromArray(nodes);
         }
-        function addLinkMark(node: Node) {
+        function addLinkMark(node: Node, title: string) {
             if (!node.isText) {
-                const content = addMarkToFrag(node.content);
+                const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title));
                 return node.copy(content);
             }
             const marks = [...node.marks];
             const linkIndex = marks.findIndex(mark => mark.type.name === "link");
-            const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight" });
+            const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
             if (linkIndex !== -1) {
                 marks.splice(linkIndex, 1, link);
             } else {
@@ -668,7 +639,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._pullReactionDisposer && this._pullReactionDisposer();
         this._heightReactionDisposer && this._heightReactionDisposer();
         this._searchReactionDisposer && this._searchReactionDisposer();
-        document.removeEventListener("paste", this.paste);
         this._editorView && this._editorView.destroy();
     }
 
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 18f82ff47..df35b603c 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -33,7 +33,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
 
     @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; }
-    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+    @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
+
     @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); }
 
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -48,7 +50,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     componentDidMount() {
         this.props.setPdfBox && this.props.setPdfBox(this);
 
-        const pdfUrl = Cast(this.props.Document.data, PdfField);
+        const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         if (pdfUrl instanceof PdfField) {
             Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
         }
@@ -78,7 +80,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
 
     @action
     public GotoPage(p: number) {
-        if (p > 0 && p <= NumCast(this.props.Document.numPages)) {
+        if (p > 0 && p <= NumCast(this.dataDoc.numPages)) {
             this.props.Document.curPage = p;
             this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight);
         }
@@ -87,7 +89,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     @action
     public ForwardPage() {
         let cp = this.GetPage() + 1;
-        if (cp <= NumCast(this.props.Document.numPages)) {
+        if (cp <= NumCast(this.dataDoc.numPages)) {
             this.props.Document.curPage = cp;
             this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
         }
@@ -185,11 +187,12 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
         }
     }
 
+
     render() {
-        const pdfUrl = Cast(this.props.Document.data, PdfField);
+        const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
         return (!(pdfUrl instanceof PdfField) || !this._pdf ?
-            <div>{`pdf, ${this.props.Document.data}, not found`}</div> :
+            <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
             <div className={classname}
                 onScroll={this.onScroll}
                 style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index a703f1cef..e7ef1dcd3 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -522,6 +522,7 @@ export namespace Doc {
         otherdoc.type = DocumentType.TEMPLATE;
         !templateDoc.nativeWidth && (otherdoc.nativeWidth = 0);
         !templateDoc.nativeHeight && (otherdoc.nativeHeight = 0);
+        !templateDoc.nativeWidth && (otherdoc.ignoreAspect = true);
         return otherdoc;
     }
     export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc) {
@@ -538,6 +539,7 @@ export namespace Doc {
         target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined;
         !templateDoc.nativeWidth && (target.nativeWidth = 0);
         !templateDoc.nativeHeight && (target.nativeHeight = 0);
+        !templateDoc.nativeHeight && (target.ignoreAspect = true);
         target.width = templateDoc.width;
         target.height = templateDoc.height;
         target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
@@ -576,7 +578,7 @@ export namespace Doc {
         /* move certain layout properties from the original data doc to the template layout to avoid
            inheriting them from the template's data doc which may also define these fields for its own use.
         */
-        fieldTemplate.ignoreAspect = BoolCast(fieldTemplate.ignoreAspect);
+        fieldTemplate.ignoreAspect = fieldTemplate.ignoreAspect === undefined ? undefined : BoolCast(fieldTemplate.ignoreAspect);
         fieldTemplate.singleColumn = BoolCast(fieldTemplate.singleColumn);
         fieldTemplate.nativeWidth = Cast(fieldTemplate.nativeWidth, "number");
         fieldTemplate.nativeHeight = Cast(fieldTemplate.nativeHeight, "number");
-- 
cgit v1.2.3-70-g09d2


From e5b52b1083c86b946a2758983e397826579d2282 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 6 Sep 2019 01:41:06 -0400
Subject: fixed summarizing in text notes.

---
 src/Utils.ts                                 |  14 +++
 src/client/util/RichTextSchema.tsx           | 157 ++++++++++++---------------
 src/client/util/TooltipTextMenu.tsx          |  18 ++-
 src/client/views/nodes/FormattedTextBox.scss |  20 ++++
 src/client/views/nodes/FormattedTextBox.tsx  |  41 ++-----
 src/new_fields/Doc.ts                        |   2 +-
 6 files changed, 120 insertions(+), 132 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 959b89fe5..f805ae872 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -133,6 +133,20 @@ export function WithKeys(obj: any, keys: string[], addKeyFunc?: (dup: any) => vo
     return dup;
 }
 
+export function timenow() {
+    var now = new Date();
+    let ampm = 'am';
+    let h = now.getHours();
+    let m: any = now.getMinutes();
+    let s: any = now.getSeconds();
+    if (h >= 12) {
+        if (h > 12) h -= 12;
+        ampm = 'pm';
+    }
+    if (m < 10) m = '0' + m;
+    return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
+}
+
 export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
 
 export function returnTrue() { return true; }
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 2b689b7ef..a8ba0a4be 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,4 +1,4 @@
-import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice, Fragment } from "prosemirror-model";
 import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
 import { TextSelection, EditorState } from "prosemirror-state";
 import { Doc } from "../../new_fields/Doc";
@@ -7,6 +7,8 @@ import { EditorView } from "prosemirror-view";
 import { keymap } from "prosemirror-keymap";
 import { undo, redo } from "prosemirror-history";
 import { toggleMark, splitBlock, selectAll, baseKeymap } from "prosemirror-commands";
+import { Domain } from "domain";
+import { DOM } from "@fortawesome/fontawesome-svg-core";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -42,14 +44,6 @@ export const nodes: { [index: string]: NodeSpec } = {
         toDOM() { return pDOM; }
     },
 
-    // starmine: {
-    //     inline: true,
-    //     attrs: { oldtext: { default: "" } },
-    //     group: "inline",
-    //     toDOM() { return ["star", "㊉"]; },
-    //     parseDOM: [{ tag: "star" }]
-    // },
-
     // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
     blockquote: {
         content: "block+",
@@ -107,10 +101,9 @@ export const nodes: { [index: string]: NodeSpec } = {
             visibility: { default: false },
             text: { default: undefined },
             textslice: { default: undefined },
-            textlen: { default: 0 }
-
         },
         group: "inline",
+        inclusive: false,
         toDOM(node) {
             const attrs = { style: `width: 40px` };
             return ["span", { ...node.attrs, ...attrs }];
@@ -205,12 +198,10 @@ export const nodes: { [index: string]: NodeSpec } = {
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
             return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
-                 ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
-            //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
-            //     ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"];
+                ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
         }
     },
-    //this doesn't currently work for some reason
+
     bullet_list: {
         ...bulletList,
         content: 'list_item+',
@@ -221,14 +212,6 @@ export const nodes: { [index: string]: NodeSpec } = {
         }
     },
 
-    //bullet_list: {
-    //  content: 'list_item+',
-    // group: 'block',
-    //active: blockActive(schema.nodes.bullet_list),
-    //enable: wrapInList(schema.nodes.bullet_list),
-    //run: wrapInList(schema.nodes.bullet_list),
-    //select: state => true,
-    // },
     list_item: {
         attrs: {
             bulletStyle: { default: 0 },
@@ -251,7 +234,6 @@ export const nodes: { [index: string]: NodeSpec } = {
 const emDOM: DOMOutputSpecArray = ["em", 0];
 const strongDOM: DOMOutputSpecArray = ["strong", 0];
 const codeDOM: DOMOutputSpecArray = ["code", 0];
-const underlineDOM: DOMOutputSpecArray = ["underline", 0];
 
 // :: Object [Specs](#model.MarkSpec) for the marks in the schema.
 export const marks: { [index: string]: MarkSpec } = {
@@ -289,16 +271,6 @@ export const marks: { [index: string]: MarkSpec } = {
         toDOM() { return strongDOM; }
     },
 
-    underline: {
-        parseDOM: [
-            { tag: 'u' },
-            { style: 'text-decoration=underline' }
-        ],
-        toDOM: () => ['span', {
-            style: 'text-decoration:underline'
-        }]
-    },
-
     strikethrough: {
         parseDOM: [
             { tag: 'strike' },
@@ -340,14 +312,52 @@ export const marks: { [index: string]: MarkSpec } = {
     },
 
     highlight: {
-        parseDOM: [{ style: 'text-decoration: underline' }],
+        parseDOM: [
+            {
+                tag: "span",
+                getAttrs: (p: any) => {
+                    if (typeof (p) !== "string") {
+                        let style = getComputedStyle(p);
+                        if (style.textDecoration === "underline") return null;
+                        if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+                            p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1)
+                            return null;
+                    }
+                    return false;
+                }
+            },
+        ],
+        inclusive: false,
+        priority: 100,
         toDOM() {
             return ['span', {
-                style: 'text-decoration: underline; text-decoration-color: rgba(204, 206, 210, 0.92)'
+                style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
             }];
         }
     },
 
+    underline: {
+        parseDOM: [
+            {
+                tag: "span",
+                getAttrs: (p: any) => {
+                    if (typeof (p) !== "string") {
+                        let style = getComputedStyle(p);
+                        if (style.textDecoration === "underline")
+                            return null;
+                        if (p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1)
+                            return null;
+                    }
+                    return false;
+                }
+            }
+            // { style: "text-decoration=underline" }
+        ],
+        toDOM: () => ['span', {
+            style: 'text-decoration:underline;text-decoration-style:line'
+        }]
+    },
+
     search_highlight: {
         parseDOM: [{ style: 'background: yellow' }],
         toDOM() {
@@ -530,9 +540,6 @@ export const marks: { [index: string]: MarkSpec } = {
         }]
     },
 };
-function getFontSize(element: any) {
-    return parseFloat((getComputedStyle(element) as any).fontSize);
-}
 
 export class ImageResizeView {
     _handle: HTMLElement;
@@ -733,73 +740,52 @@ export class FootnoteView {
 }
 
 export class SummarizedView {
-    // TODO: highlight text that is summarized. to find end of region, walk along mark
     _collapsed: HTMLElement;
     _view: any;
     constructor(node: any, view: any, getPos: any) {
         this._collapsed = document.createElement("span");
-        this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉";
-        this._collapsed.style.opacity = "0.5";
-        this._collapsed.style.position = "relative";
-        this._collapsed.style.width = "40px";
-        this._collapsed.style.height = "20px";
-        let self = this;
+        this._collapsed.className = this.className(node.attrs.visibility);
         this._view = view;
         const js = node.toJSON;
         node.toJSON = function () {
-
             return js.apply(this, arguments);
         };
-        this._collapsed.onpointerdown = function (e: any) {
-            if (node.attrs.visibility) {
-                // node.attrs.visibility = !node.attrs.visibility;
-                let y = getPos();
-                const attrs = { ...node.attrs };
-                attrs.visibility = !attrs.visibility;
-                let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight);
-                let length = to - from;
-                let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length);
-                // update attrs of node
-                attrs.text = newSelection.content();
-                attrs.textslice = newSelection.content().toJSON();
-                view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
-                view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { }));
-                let marks = view.state.storedMarks.filter((m: any) => m.type !== view.state.schema.marks.highlight);
-                view.state.storedMarks = marks;
-                self._collapsed.textContent = "㊉";
-            } else {
-                // node.attrs.visibility = !node.attrs.visibility;
-                let y = getPos();
-                const attrs = { ...node.attrs };
-                attrs.visibility = !attrs.visibility;
-                view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs));
-                let mark = view.state.schema.mark(view.state.schema.marks.highlight);
-                view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1)));
-                const from = view.state.selection.from;
-                let size = node.attrs.text.size;
-                view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark));
-                self._collapsed.textContent = "㊀";
+
+        this._collapsed.onpointerdown = (e: any) => {
+            const visible = !node.attrs.visibility;
+            const attrs = { ...node.attrs, visibility: visible };
+            let textSelection = TextSelection.create(view.state.doc, getPos() + 1, getPos() + 1);
+            if (!visible) { // update summarized text and save in attrs
+                textSelection = this.updateSummarizedText(getPos() + 1);
+                attrs.text = textSelection.content();
+                attrs.textslice = attrs.text.toJSON();
             }
+            view.dispatch(view.state.tr.
+                setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+                replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
+                setNodeMarkup(getPos(), undefined, attrs)); // update the attrs 
             e.preventDefault();
             e.stopPropagation();
+            this._collapsed.className = this.className(visible);
         };
         (this as any).dom = this._collapsed;
-
-    }
-    selectNode() {
     }
+    selectNode() { }
+
+    deselectNode() { }
 
-    updateSummarizedText(start?: any, mark?: any) {
-        let $start = this._view.state.doc.resolve(start);
+    className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+    updateSummarizedText(start?: any) {
+        let mark = this._view.state.schema.marks.highlight.create();
         let endPos = start;
 
-        let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight);
         let visited = new Set();
         for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
             let skip = false;
             this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
                 if (node.isLeaf && !visited.has(node) && !skip) {
-                    if (node.marks.find((m: any) => m.type === _mark.type)) {
+                    if (node.marks.find((m: any) => m.type === mark.type)) {
                         visited.add(node);
                         endPos = i + node.nodeSize - 1;
                     }
@@ -807,10 +793,7 @@ export class SummarizedView {
                 }
             });
         }
-        return { from: start, to: endPos };
-    }
-
-    deselectNode() {
+        return TextSelection.create(this._view.state.doc, start, endPos);
     }
 }
 // :: Schema
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 6d375fc1d..020c51c36 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -307,10 +307,9 @@ export class TooltipTextMenu {
                     {
                         handlers: {
                             dragComplete: action(() => {
-                                // let m = dragData.droppedDocuments;
                                 let linkDoc = dragData.linkDocument;
                                 let proto = Doc.GetProto(linkDoc);
-                                if (docView && docView.props.ContainingCollectionView) {
+                                if (proto && docView && docView.props.ContainingCollectionView) {
                                     proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
                                 }
                                 linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
@@ -322,8 +321,6 @@ export class TooltipTextMenu {
                 e.preventDefault();
             };
             this.linkEditor.appendChild(this.linkDrag);
-            // this.linkEditor.appendChild(this.linkText);
-            // this.linkEditor.appendChild(linkBtn);
             this.tooltip.appendChild(this.linkEditor);
         }
 
@@ -432,11 +429,13 @@ export class TooltipTextMenu {
     }
 
     public static insertStar(state: EditorState<any>, dispatch: any) {
-        let newNode = schema.nodes.star.create({ visibility: false, text: state.selection.content(), textslice: state.selection.content().toJSON(), textlen: state.selection.to - state.selection.from });
-        if (dispatch) {
-            //console.log(newNode.attrs.text.toString());
-            dispatch(state.tr.replaceSelectionWith(newNode));
-        }
+        if (state.selection.empty) return false;
+        let mark = state.schema.marks.highlight.create();
+        let tr = state.tr;
+        tr.addMark(state.selection.from, state.selection.to, mark);
+        let content = tr.selection.content();
+        let newNode = schema.nodes.star.create({ visibility: false, text: content, textslice: content.toJSON() });
+        dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
         return true;
     }
 
@@ -637,7 +636,6 @@ export class TooltipTextMenu {
                         Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
                             const markType = mark.type;
                             this.changeToMarkInGroup(markType, this.view, []);
-
                         });
                     }
                 }
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 8f47402c4..0de050e79 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -131,6 +131,26 @@ footnote::after {
     width: 0;
 }
 
+.formattedTextBox-summarizer {
+    opacity :0.5;
+    position: relative;
+    width:40px;
+    height:20px;
+}
+.formattedTextBox-summarizer::after{
+    content: "←" ;
+}
+
+.formattedTextBox-summarizer-collapsed {
+    opacity :0.5;
+    position: relative;
+    width:40px;
+    height:20px;
+}
+.formattedTextBox-summarizer-collapsed::after {
+    content: "...";
+}
+
 ol { counter-reset: deci1 0;}
 .decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
 .decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d811273e8..006a33011 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -15,7 +15,7 @@ import { List } from '../../../new_fields/List';
 import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
 import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Utils, numberRange } from '../../../Utils';
+import { Utils, numberRange, timenow } from '../../../Utils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -117,6 +117,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @undoBatch
     public setFontColor(color: string) {
+        this._editorView!.state.storedMarks
         if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
         if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
             this.props.Document.color = color;
@@ -654,10 +655,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this.props.select(false);
         }
         else if (this.props.isOverlay) this._editorView!.focus();
-        var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
-        let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })];
-        this._editorView!.state.storedMarks = newMarks;
-
+        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).
+            addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
     }
 
     componentWillUnmount() {
@@ -681,10 +680,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
         if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
             e.stopPropagation();
-            if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
-                //this._toolTipTextMenu.tooltip.style.opacity = "0";
-            }
         }
+        this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).
+            addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
         let ctrlKey = e.ctrlKey;
         if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
             let href = (e.target as any).href;
@@ -744,13 +742,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
-        let view = this._editorView!;
-        let mid = view.state.doc.resolve(Math.round((start + end) / 2));
-        let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
-        view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid, mid)));
-    }
-
     @action
     onFocused = (e: React.FocusEvent): void => {
         document.removeEventListener("keypress", this.recordKeyHandler);
@@ -774,7 +765,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     onClick = (e: React.MouseEvent): void => {
         if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
             let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
-            if (pos) {
+            if (pos && pos.pos > 0) {
                 let node = this._editorView!.state.doc.nodeAt(pos.pos);
                 let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
                 if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
@@ -833,24 +824,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             //     SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false);
             // }, 0);
         }
-        function timenow() {
-            var now = new Date();
-            let ampm = 'am';
-            let h = now.getHours();
-            let m: any = now.getMinutes();
-            let s: any = now.getSeconds();
-            if (h >= 12) {
-                if (h > 12) h -= 12;
-                ampm = 'pm';
-            }
-
-            if (m < 10) m = '0' + m;
-            return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
-        }
-        var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
-        let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
-        this._editorView!.state.storedMarks = newMarks;
-
         // stop propagation doesn't seem to stop propagation of native keyboard events.
         // so we set a flag on the native event that marks that the event's been handled.
         (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index a703f1cef..1462cdfad 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -308,7 +308,7 @@ export namespace Doc {
 
     // gets the document's prototype or returns the document if it is a prototype
     export function GetProto(doc: Doc) {
-        return Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc);
+        return doc && (Doc.GetT(doc, "isPrototype", "boolean", true) ? doc : (doc.proto || doc));
     }
     export function GetDataDoc(doc: Doc): Doc {
         let proto = Doc.GetProto(doc);
-- 
cgit v1.2.3-70-g09d2


From 95f773b56b8ed4fa95b1fd308c19baee1744275a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 6 Sep 2019 09:18:11 -0400
Subject: added note types. fixed user_mark

---
 src/client/util/ProsemirrorExampleTransfer.ts      |  5 +++++
 src/client/util/RichTextSchema.tsx                 |  3 +--
 src/client/views/ContextMenu.tsx                   |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  8 ++++++++
 src/client/views/nodes/FormattedTextBox.tsx        | 22 ++++++++++------------
 5 files changed, 25 insertions(+), 15 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 9f6da7ade..55e07cfb9 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -14,6 +14,7 @@ export type KeyMap = { [key: string]: any };
 export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
     let keys: { [key: string]: any } = {}, type;
 
+    keys["ACTIVE"] = false;
     function bind(key: string, cmd: any) {
         if (mapKeys) {
             let mapped = mapKeys[key];
@@ -143,6 +144,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     });
 
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+        if (!keys["ACTIVE"]) {
+            dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from - 1, state.selection.from)).deleteSelection());
+            return true;
+        }
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => {
             // marks && tx3.ensureMarks(marks);
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 31a65dd3a..675c1d387 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -381,7 +381,6 @@ export const marks: { [index: string]: MarkSpec } = {
             modified: { default: "when?" }
         },
         group: "inline",
-        inclusive: false,
         toDOM(node: any) {
             let hideUsers = node.attrs.hide_users;
             let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail);
@@ -759,7 +758,7 @@ export class SummarizedView {
         this._collapsed.onpointerdown = (e: any) => {
             const visible = !node.attrs.visibility;
             const attrs = { ...node.attrs, visibility: visible };
-            let textSelection = TextSelection.create(view.state.doc, getPos() + 1, getPos() + 1);
+            let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
             if (!visible) { // update summarized text and save in attrs
                 textSelection = this.updateSummarizedText(getPos() + 1);
                 attrs.text = textSelection.content();
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 760736501..e27318429 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -246,7 +246,7 @@ export class ContextMenu extends React.Component {
                 this.selectedIndex--;
             }
             e.preventDefault();
-        } else if (e.key === "Enter") {
+        } else if (e.key === "Enter" || e.key === "Tab") {
             const item = this.flatItems[this.selectedIndex];
             item.event();
             this.closeMenu();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a7acd9e91..aadb7f6e9 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -879,9 +879,17 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         });
         layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
         layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
+        layoutItems.push({ description: "1: Note", event: () => this.createText("yellow"), icon: "eye" });
+        layoutItems.push({ description: "2: Idea", event: () => this.createText("pink"), icon: "eye" });
+        layoutItems.push({ description: "3: Topic", event: () => this.createText("lightBlue"), icon: "eye" });
+        layoutItems.push({ description: "4: Person", event: () => this.createText("lightGreen"), icon: "eye" });
         ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
     }
 
+    createText = (color: string) => {
+        let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY);
+        this.addLiveTextBox(Docs.Create.TextDocument({ x: pt[0], y: pt[1], backgroundColor: color }))
+    }
 
     private childViews = () => [
         <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 0d530c9a1..944cc3211 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -173,6 +173,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
+            this._keymap["ACTIVE"] = true;
 
             this._applyingChange = true;
             this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
@@ -307,14 +308,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
     }
 
+    _keymap: any = undefined;
     @computed get config() {
+        this._keymap = buildKeymap(schema);
+        this._keymap["ACTIVE"] = this.extensionDoc.text;
         return {
             schema,
             inpRules, //these currently don't do anything, but could eventually be helpful
             plugins: this.props.isOverlay ? [
                 this.tooltipTextMenuPlugin(),
                 history(),
-                keymap(buildKeymap(schema)),
+                keymap(this._keymap),
                 keymap(baseKeymap),
                 // this.tooltipLinkingMenuPlugin(),
                 new Plugin({
@@ -325,7 +329,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 formattedTextBoxCommentPlugin
             ] : [
                     history(),
-                    keymap(buildKeymap(schema)),
+                    keymap(this._keymap),
                     keymap(baseKeymap),
                 ]
         };
@@ -626,8 +630,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this.props.select(false);
         }
         else if (this.props.isOverlay) this._editorView!.focus();
-        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).
-            addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
+        this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
     }
 
     componentWillUnmount() {
@@ -651,8 +654,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
             e.stopPropagation();
         }
-        this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).
-            addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
         let ctrlKey = e.ctrlKey;
         if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
             let href = (e.target as any).href;
@@ -786,14 +787,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             SelectionManager.DeselectAll();
         }
         e.stopPropagation();
-        if (e.key === "Tab" || e.key === "Enter") { // bullets typically change "levels" when tab or enter is used.  sometimes backspcae, so maybe that should be added. e.preventDefault();
+        if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
-            // bcz: if we use this, it fixes some problesm with bullet numbers but kills undo/redo
-            // setTimeout(() => { // force re-rendering of bullet numbers that may have had their bullet labels change.  (Our prosemirrior code re-"marks" the changed bullets, but nothing causes the Dom to be re-rendered which is where the nubering takes place)
-            //     SelectionManager.DeselectAll();
-            //     SelectionManager.SelectDoc(DocumentManager.Instance.getDocumentView(this.props.Document, this.props.ContainingCollectionView)!, false);
-            // }, 0);
         }
+        //this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
+        this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
         // stop propagation doesn't seem to stop propagation of native keyboard events.
         // so we set a flag on the native event that marks that the event's been handled.
         (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
-- 
cgit v1.2.3-70-g09d2


From eb05b987d7a1b2ca2e50268a0c15f2de7d44c5bd Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 6 Sep 2019 10:11:23 -0400
Subject: cleanup of prosemirror stuff.

---
 src/client/util/ProsemirrorExampleTransfer.ts | 11 ++-----
 src/client/util/RichTextSchema.tsx            |  2 --
 src/client/views/PreviewCursor.tsx            |  6 ++--
 src/client/views/nodes/FormattedTextBox.tsx   | 47 +++++++++++----------------
 4 files changed, 24 insertions(+), 42 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 55e07cfb9..e7566e3a4 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -116,8 +116,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
             marks && tx2.setStoredMarks([...marks]);
             dispatch(tx2);
         })) { // couldn't sink into an existing list, so wrap in a new one
-            let sxf = state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end));
-            let newstate = state.applyTransaction(sxf);
+            let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
             if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
                 updateBullets(tx2);
                 // when promoting to a list, assume list will format things so don't copy the stored marks.
@@ -144,16 +143,12 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     });
 
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        if (!keys["ACTIVE"]) {
+        if (!keys["ACTIVE"]) {// hack to ignore an initial carriage return when creating a textbox from the action menu
             dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from - 1, state.selection.from)).deleteSelection());
             return true;
         }
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
-        if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => {
-            // marks && tx3.ensureMarks(marks);
-            // marks && tx3.setStoredMarks(marks);
-            dispatch(tx3);
-        })) {
+        if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
                 marks && tx3.ensureMarks(marks);
                 marks && tx3.setStoredMarks(marks);
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 675c1d387..baa95acb0 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -103,7 +103,6 @@ export const nodes: { [index: string]: NodeSpec } = {
             textslice: { default: undefined },
         },
         group: "inline",
-        inclusive: false,
         toDOM(node) {
             const attrs = { style: `width: 40px` };
             return ["span", { ...node.attrs, ...attrs }];
@@ -333,7 +332,6 @@ export const marks: { [index: string]: MarkSpec } = {
             },
         ],
         inclusive: false,
-        priority: 100,
         toDOM() {
             return ['span', {
                 style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx
index 1329dc02c..45a8556bf 100644
--- a/src/client/views/PreviewCursor.tsx
+++ b/src/client/views/PreviewCursor.tsx
@@ -93,9 +93,7 @@ export class PreviewCursor extends React.Component<{}> {
 
     @action
     onKeyPress = (e: KeyboardEvent) => {
-        // Mixing events between React and Native is finicky.  In FormattedTextBox, we set the
-        // DASHFormattedTextBoxHandled flag when a text box consumes a key press so that we can ignore
-        // the keyPress here. 112-
+        // Mixing events between React and Native is finicky. 
         //if not these keys, make a textbox if preview cursor is active!
         if (e.key !== "Escape" && e.key !== "Backspace" && e.key !== "Delete" && e.key !== "CapsLock" &&
             e.key !== "Alt" && e.key !== "Shift" && e.key !== "Meta" && e.key !== "Control" &&
@@ -103,7 +101,7 @@ export class PreviewCursor extends React.Component<{}> {
             e.key !== "NumLock" &&
             (e.keyCode < 112 || e.keyCode > 123) && // F1 thru F12 keys
             !e.key.startsWith("Arrow") &&
-            !e.defaultPrevented && !(e as any).DASHFormattedTextBoxHandled) {
+            !e.defaultPrevented) {
             if (!e.ctrlKey && !e.metaKey) {//  /^[a-zA-Z0-9$*^%#@+-=_|}{[]"':;?/><.,}]$/.test(e.key)) {
                 PreviewCursor.Visible && PreviewCursor._onKeyPress && PreviewCursor._onKeyPress(e);
                 PreviewCursor.Visible = false;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 944cc3211..8d3286d71 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -173,19 +173,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
-            this._keymap["ACTIVE"] = true;
+            this._keymap["ACTIVE"] = true; // hack to ignore an initial carriage return when creating a textbox from the action menu
 
             this._applyingChange = true;
             this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
             this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
             this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
             this._applyingChange = false;
+            this.updateTitle();
             let title = StrCast(this.dataDoc.title);
-            if (title && title.startsWith("-") && this._editorView && !this.Document.customTitle) {
-                let str = this._editorView.state.doc.textContent;
-                let titlestr = str.substr(0, Math.min(40, str.length));
-                this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
-            }
+        }
+    }
+
+    updateTitle = () => {
+        if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
+            let str = this._editorView.state.doc.textContent;
+            let titlestr = str.substr(0, Math.min(40, str.length));
+            this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
         }
     }
 
@@ -311,7 +315,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     _keymap: any = undefined;
     @computed get config() {
         this._keymap = buildKeymap(schema);
-        this._keymap["ACTIVE"] = this.extensionDoc.text;
+        this._keymap["ACTIVE"] = this.extensionDoc.text;  // hack to ignore an initial carriage return only when creating a textbox from the action menu
         return {
             schema,
             inpRules, //these currently don't do anything, but could eventually be helpful
@@ -335,11 +339,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         };
     }
 
-    @action
-    rebuildEditor() {
-        this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
-    }
-
     componentDidMount() {
         if (!this.props.isOverlay) {
             this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
@@ -583,11 +582,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             const marks = [...node.marks];
             const linkIndex = marks.findIndex(mark => mark.type.name === "link");
             const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
-            if (linkIndex !== -1) {
-                marks.splice(linkIndex, 1, link);
-            } else {
-                marks.push(link);
-            }
+            marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
             return node.mark(marks);
         }
     }
@@ -630,6 +625,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this.props.select(false);
         }
         else if (this.props.isOverlay) this._editorView!.focus();
+        // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
         this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
     }
 
@@ -734,6 +730,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     onClick = (e: React.MouseEvent): void => {
+        // this hackiness handles clicking on the list item bullets to do expand/collapse.  the bullets are ::before pseudo elements so there's no real way to hit test against them.
         if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
             let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
             if (pos && pos.pos > 0) {
@@ -790,16 +787,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        //this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
-        this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
-        // stop propagation doesn't seem to stop propagation of native keyboard events.
-        // so we set a flag on the native event that marks that the event's been handled.
-        (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
-        if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) {
-            let str = this._editorView.state.doc.textContent;
-            let titlestr = str.substr(0, Math.min(40, str.length));
-            this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
-        }
+
+        this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
+
+        this.updateTitle();
+
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
         }
@@ -820,7 +812,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
 
     render() {
-        let self = this;
         let style = this.props.isOverlay ? "scroll" : "hidden";
         let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
         let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ||
-- 
cgit v1.2.3-70-g09d2


From 2707e0898d535cc143272b7bf3b80f829368c097 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 6 Sep 2019 11:37:59 -0400
Subject: added metadata ui for text

---
 src/client/util/ProsemirrorExampleTransfer.ts      | 27 ++++++++++++++++++++--
 src/client/util/RichTextSchema.tsx                 |  6 +++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 12 +++++-----
 src/client/views/nodes/FormattedTextBox.tsx        | 11 ++++++++-
 4 files changed, 47 insertions(+), 9 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index e7566e3a4..da26da4f9 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -150,8 +150,8 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
-                marks && tx3.ensureMarks(marks);
-                marks && tx3.setStoredMarks(marks);
+                marks && tx3.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
+                marks && tx3.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
                 if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
                     dispatch(tx3);
                 }
@@ -161,6 +161,29 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         }
         return true;
     });
+    bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+        var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+        let tx = state.tr;
+        marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
+        marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
+        dispatch(tx);
+        return false;
+    });
+    bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+        let range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+            return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
+        });
+        let path = (state.doc.resolve(state.selection.from - 1) as any).path;
+        let spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+        let textsel = TextSelection.create(state.doc, range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator, range!.end);
+        let text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+        let whitespace = text.length - 1;
+        for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
+        if (text.endsWith(":")) {
+            dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any));
+        }
+        return false;
+    });
 
 
     return keys;
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index baa95acb0..5ee445590 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -315,6 +315,12 @@ export const marks: { [index: string]: MarkSpec } = {
         }
     },
 
+    metadata: {
+        toDOM() {
+            return ['span', { style: 'border-radius:5px; background:rgba(100, 100, 100, 0.1); box-shadow: black 1px 1px 1px' }];
+        }
+    },
+
     highlight: {
         parseDOM: [
             {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aadb7f6e9..074bc1822 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -879,16 +879,16 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         });
         layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
         layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
-        layoutItems.push({ description: "1: Note", event: () => this.createText("yellow"), icon: "eye" });
-        layoutItems.push({ description: "2: Idea", event: () => this.createText("pink"), icon: "eye" });
-        layoutItems.push({ description: "3: Topic", event: () => this.createText("lightBlue"), icon: "eye" });
-        layoutItems.push({ description: "4: Person", event: () => this.createText("lightGreen"), icon: "eye" });
+        layoutItems.push({ description: "1: Note", event: () => this.createText("Note", "yellow"), icon: "eye" });
+        layoutItems.push({ description: "2: Idea", event: () => this.createText("Idea", "pink"), icon: "eye" });
+        layoutItems.push({ description: "3: Topic", event: () => this.createText("Topic", "lightBlue"), icon: "eye" });
+        layoutItems.push({ description: "4: Person", event: () => this.createText("Person", "lightGreen"), icon: "eye" });
         ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
     }
 
-    createText = (color: string) => {
+    createText = (noteStyle: string, color: string) => {
         let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY);
-        this.addLiveTextBox(Docs.Create.TextDocument({ x: pt[0], y: pt[1], backgroundColor: color }))
+        this.addLiveTextBox(Docs.Create.TextDocument({ title: noteStyle, x: pt[0], y: pt[1], backgroundColor: color }))
     }
 
     private childViews = () => [
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8d3286d71..c09e88592 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -173,6 +173,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
+
+            let metadata = this._editorView!.state.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
+            if (metadata) {
+                let range = this._editorView!.state.selection.$from.blockRange(this._editorView!.state.selection.$to);
+                let text = range ? this._editorView!.state.doc.textBetween(range.start, range.end) : "";
+                let key = text.split("::")[0];
+                let value = text.split("::")[text.split("::").length - 1];
+                this.dataDoc[key] = value;
+            }
+
             this._keymap["ACTIVE"] = true; // hack to ignore an initial carriage return when creating a textbox from the action menu
 
             this._applyingChange = true;
@@ -787,7 +797,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-
         this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
 
         this.updateTitle();
-- 
cgit v1.2.3-70-g09d2


From 173863d85ee590c276bf22b1cfe91e0d00986720 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 6 Sep 2019 13:45:04 -0400
Subject: added named target docs from rich text.

---
 src/client/documents/Documents.ts                  |  4 +--
 src/client/util/ProsemirrorExampleTransfer.ts      | 16 +++++----
 src/client/util/RichTextSchema.tsx                 | 12 ++++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |  5 ++-
 src/client/views/nodes/FormattedTextBox.tsx        | 40 ++++++++++++++++------
 6 files changed, 54 insertions(+), 25 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ef8b68c2f..fbdfa8966 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -419,8 +419,8 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + ".kvp", ...options });
         }
 
-        export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions) {
-            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform });
+        export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List([new SchemaHeaderField("title", "#f1efeb")]), ...options, viewType: CollectionViewType.Freeform }, id);
         }
 
         export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index da26da4f9..cc2ae7d38 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -142,6 +142,11 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         }
     });
 
+    let splitMetadata = (marks: any, tx: Transaction) => {
+        marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+        marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+        return tx;
+    }
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         if (!keys["ACTIVE"]) {// hack to ignore an initial carriage return when creating a textbox from the action menu
             dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from - 1, state.selection.from)).deleteSelection());
@@ -150,8 +155,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
-                marks && tx3.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
-                marks && tx3.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
+                splitMetadata(marks, tx3);
                 if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
                     dispatch(tx3);
                 }
@@ -163,10 +167,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
     });
     bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
-        let tx = state.tr;
-        marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
-        marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata));
-        dispatch(tx);
+        dispatch(splitMetadata(marks, state.tr));
         return false;
     });
     bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -180,7 +181,8 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         let whitespace = text.length - 1;
         for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
         if (text.endsWith(":")) {
-            dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any));
+            dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
+                addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
         }
         return false;
     });
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 5ee445590..6bae63174 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -317,7 +317,17 @@ export const marks: { [index: string]: MarkSpec } = {
 
     metadata: {
         toDOM() {
-            return ['span', { style: 'border-radius:5px; background:rgba(100, 100, 100, 0.1); box-shadow: black 1px 1px 1px' }];
+            return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
+        }
+    },
+    metadataKey: {
+        toDOM() {
+            return ['span', { style: 'font-style:italic; ' }];
+        }
+    },
+    metadataVal: {
+        toDOM() {
+            return ['span', { style: 'background:rgba(100, 100, 100, 0.1);' }];
         }
     },
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 074bc1822..fac4d4970 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -888,7 +888,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     createText = (noteStyle: string, color: string) => {
         let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY);
-        this.addLiveTextBox(Docs.Create.TextDocument({ title: noteStyle, x: pt[0], y: pt[1], backgroundColor: color }))
+        this.addLiveTextBox(Docs.Create.TextDocument({ title: noteStyle, x: pt[0], y: pt[1], autoHeight: true, backgroundColor: color }))
     }
 
     private childViews = () => [
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 27eafd769..5015ee39a 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -93,9 +93,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 }
             });
         } else if (!e.ctrlKey) {
-            let newBox = Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, title: "-typed text-" });
-            newBox.proto!.autoHeight = true;
-            this.props.addLiveTextDocument(newBox);
+            this.props.addLiveTextDocument(
+                Docs.Create.TextDocument({ width: 200, height: 100, x: x, y: y, autoHeight: true, title: "-typed text-" }));
         }
         e.stopPropagation();
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index c09e88592..1dd84a3db 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -167,6 +167,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
+            let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
+            if (metadata) {
+                let range = tx.selection.$from.blockRange(tx.selection.$to);
+                let text = range ? tx.doc.textBetween(range.start, range.end) : "";
+                let textEndSelection = tx.selection.to;
+                for (; textEndSelection < range!.end && text[textEndSelection - range!.start] != " "; textEndSelection++) { }
+                text = text.substr(0, textEndSelection - range!.start);
+                text = text.split(" ")[text.split(" ").length - 1];
+                let split = text.split("::");
+                if (split.length > 1 && split[1]) {
+                    let key = split[0];
+                    let value = split[split.length - 1];
+
+                    DocServer.GetRefField(value).then(doc => this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value));
+                    const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${value}`, location: "onRight", title: value });
+                    const mval = this._editorView!.state.schema.marks.metadataVal.create();
+                    let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
+                    tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
+                    this.dataDoc[key] = value;
+                }
+            }
             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
@@ -174,15 +195,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
 
-            let metadata = this._editorView!.state.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
-            if (metadata) {
-                let range = this._editorView!.state.selection.$from.blockRange(this._editorView!.state.selection.$to);
-                let text = range ? this._editorView!.state.doc.textBetween(range.start, range.end) : "";
-                let key = text.split("::")[0];
-                let value = text.split("::")[text.split("::").length - 1];
-                this.dataDoc[key] = value;
-            }
-
             this._keymap["ACTIVE"] = true; // hack to ignore an initial carriage return when creating a textbox from the action menu
 
             this._applyingChange = true;
@@ -191,7 +203,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
             this._applyingChange = false;
             this.updateTitle();
-            let title = StrCast(this.dataDoc.title);
         }
     }
 
@@ -670,6 +681,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
                 href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
             }
+            let node = this._editorView!.state.doc.nodeAt(this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY })!.pos);
+            if (node) {
+                let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
+                href = link && link.attrs.href;
+                location = link && link.attrs.location;
+            }
             if (href) {
                 if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                     this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
@@ -690,7 +707,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                                     DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
                                 } else if (jumpToDoc) {
                                     DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
-
+                                } else {
+                                    DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
                                 }
                             }
                         });
-- 
cgit v1.2.3-70-g09d2


From 0d9133bbd8417e68dfd62706369067c1e2e30c97 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 6 Sep 2019 15:59:10 -0400
Subject: turned link lines back on...  switched metadata links into real links

---
 src/client/documents/Documents.ts                         |  4 ++--
 src/client/util/RichTextSchema.tsx                        |  4 ++--
 .../collectionFreeForm/CollectionFreeFormLinksView.tsx    |  4 ++--
 src/client/views/nodes/FormattedTextBox.tsx               | 15 ++++++++++-----
 4 files changed, 16 insertions(+), 11 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fbdfa8966..ae65fde1e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -606,13 +606,13 @@ export namespace Docs {
 
 export namespace DocUtils {
 
-    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc) {
+    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string) {
         if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
         let sv = DocumentManager.Instance.getDocumentView(source);
         if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
         if (target === CurrentUserUtils.UserDocument) return undefined;
 
-        let linkDocProto = new Doc();
+        let linkDocProto = new Doc(id, true);
         UndoManager.RunInBatch(() => {
             linkDocProto.type = DocumentType.LINK;
 
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 6bae63174..8851839a2 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -327,7 +327,7 @@ export const marks: { [index: string]: MarkSpec } = {
     },
     metadataVal: {
         toDOM() {
-            return ['span', { style: 'background:rgba(100, 100, 100, 0.1);' }];
+            return ['span'];
         }
     },
 
@@ -347,7 +347,7 @@ export const marks: { [index: string]: MarkSpec } = {
                 }
             },
         ],
-        inclusive: false,
+        inclusive: true,
         toDOM() {
             return ['span', {
                 style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 2d94f1b8e..a593128be 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -120,9 +120,9 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
     render() {
         return (
             <div className="collectionfreeformlinksview-container">
-                {/* <svg className="collectionfreeformlinksview-svgCanvas">
+                <svg className="collectionfreeformlinksview-svgCanvas">
                     {this.uniqueConnections}
-                </svg> */}
+                </svg>
                 {this.props.children}
             </div>
         );
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 1dd84a3db..5f185d8ae 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -180,8 +180,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     let key = split[0];
                     let value = split[split.length - 1];
 
-                    DocServer.GetRefField(value).then(doc => this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value));
-                    const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${value}`, location: "onRight", title: value });
+                    let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+                    DocServer.GetRefField(value).then(doc => {
+                        DocServer.GetRefField(id).then(linkDoc => {
+                            this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
+                            if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
+                            else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id);
+                        })
+                    });
+                    const link = this._editorView!.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
                     const mval = this._editorView!.state.schema.marks.metadataVal.create();
                     let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
                     tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
@@ -203,6 +210,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
             this._applyingChange = false;
             this.updateTitle();
+            this.tryUpdateHeight();
         }
     }
 
@@ -817,12 +825,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
         this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
 
-        this.updateTitle();
-
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
         }
-        this.tryUpdateHeight();
     }
 
     @action
-- 
cgit v1.2.3-70-g09d2


From ee8f7e6ff149defe919a5ac219ed700a4688b46c Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 6 Sep 2019 17:46:02 -0400
Subject: fixed image resizing within text

---
 src/client/util/RichTextSchema.tsx | 4 ++--
 src/client/views/nodes/WebBox.scss | 1 +
 2 files changed, 3 insertions(+), 2 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 8851839a2..153aaa791 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -598,9 +598,9 @@ export class ImageResizeView {
                 document.removeEventListener("pointermove", onpointermove);
                 document.removeEventListener("pointerup", onpointerup);
                 view.dispatch(
-                    view.state.tr.setNodeMarkup(getPos(), null,
+                    view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
                         { src: node.attrs.src, width: self._outer.style.width })
-                        .setSelection(view.state.selection));
+                        );
             };
 
             document.addEventListener("pointermove", onpointermove);
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index 43220df71..fbe9bf063 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -30,6 +30,7 @@
     width: 100%;
     height: 100%;
     position: absolute;
+    pointer-events: all;
 }
 
 .webBox-button {
-- 
cgit v1.2.3-70-g09d2


From 8b7bfb4f9ecc9f85e9a413f57decd74e88179a6e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 7 Sep 2019 01:37:13 -0400
Subject: fixed image dragging within textboxes

---
 src/client/util/RichTextSchema.tsx          | 17 +++++++++--------
 src/client/views/nodes/FormattedTextBox.tsx | 11 +++++++++--
 2 files changed, 18 insertions(+), 10 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 153aaa791..6ded78b4d 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -9,6 +9,7 @@ import { undo, redo } from "prosemirror-history";
 import { toggleMark, splitBlock, selectAll, baseKeymap } from "prosemirror-commands";
 import { Domain } from "domain";
 import { DOM } from "@fortawesome/fontawesome-svg-core";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -124,9 +125,10 @@ export const nodes: { [index: string]: NodeSpec } = {
         inline: true,
         attrs: {
             src: {},
-            width: { default: "100px" },
+            width: { default: 100 },
             alt: { default: null },
-            title: { default: null }
+            title: { default: null },
+            float: { default: "left" }
         },
         group: "inline",
         draggable: true,
@@ -140,11 +142,6 @@ export const nodes: { [index: string]: NodeSpec } = {
                 };
             }
         }],
-        // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why?
-        toDOM(node) {
-            const attrs = { style: `width: ${node.attrs.width}` };
-            return ["img", { ...node.attrs, ...attrs }];
-        }
     },
 
     video: {
@@ -571,6 +568,7 @@ export class ImageResizeView {
         this._outer.style.width = node.attrs.width;
         this._outer.style.display = "inline-block";
         this._outer.style.overflow = "hidden";
+        (this._outer.style as any).float = node.attrs.float;
 
         this._img.setAttribute("src", node.attrs.src);
         this._img.style.width = "100%";
@@ -592,6 +590,8 @@ export class ImageResizeView {
                 const currentX = e.pageX;
                 const diffInPx = currentX - startX;
                 self._outer.style.width = `${startWidth + diffInPx}`;
+                //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
+                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
             };
 
             const onpointerup = () => {
@@ -600,7 +600,8 @@ export class ImageResizeView {
                 view.dispatch(
                     view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
                         { src: node.attrs.src, width: self._outer.style.width })
-                        );
+                );
+                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
             };
 
             document.addEventListener("pointermove", onpointermove);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5f185d8ae..794d3a573 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -261,7 +261,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             // }
         }
     }
-
+    setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
+        let view = this._editorView!;
+        let mid = view.state.doc.resolve(Math.round((start + end) / 2));
+        let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
+        view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid)));
+    }
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._proseRef = ele;
         this.dropDisposer && this.dropDisposer();
@@ -276,7 +281,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             // We're dealing with an internal document drop
             let url = de.data.urlField.url.href;
             let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
-            this._editorView!.dispatch(this._editorView!.state.tr.insert(0, model.create({ src: url })));
+            let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
+            this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url })));
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
             const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
@@ -629,6 +635,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
         }
         if (this._proseRef) {
+            let self = this;
             this._editorView && this._editorView.destroy();
             this._editorView = new EditorView(this._proseRef, {
                 state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
-- 
cgit v1.2.3-70-g09d2


From af34c087bf3226c09a657959cc14fc4ae291feb0 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 7 Sep 2019 13:51:11 -0400
Subject: added linking to images in text boxes

---
 src/client/util/RichTextSchema.tsx          | 51 ++++++++++++++++++++++-------
 src/client/views/MainOverlayTextBox.tsx     |  1 -
 src/client/views/nodes/FormattedTextBox.tsx |  9 +++--
 3 files changed, 46 insertions(+), 15 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 6ded78b4d..05a37759f 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,15 +1,16 @@
-import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice, Fragment } from "prosemirror-model";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
 import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { TextSelection, EditorState } from "prosemirror-state";
-import { Doc } from "../../new_fields/Doc";
+import { EditorState, TextSelection } from "prosemirror-state";
 import { StepMap } from "prosemirror-transform";
 import { EditorView } from "prosemirror-view";
-import { keymap } from "prosemirror-keymap";
-import { undo, redo } from "prosemirror-history";
-import { toggleMark, splitBlock, selectAll, baseKeymap } from "prosemirror-commands";
-import { Domain } from "domain";
-import { DOM } from "@fortawesome/fontawesome-svg-core";
+import { Doc } from "../../new_fields/Doc";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { DocServer } from "../DocServer";
+import { Cast, NumCast } from "../../new_fields/Types";
+import { DocumentManager } from "./DocumentManager";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -128,7 +129,8 @@ export const nodes: { [index: string]: NodeSpec } = {
             width: { default: 100 },
             alt: { default: null },
             title: { default: null },
-            float: { default: "left" }
+            float: { default: "left" },
+            docid: { default: "" }
         },
         group: "inline",
         draggable: true,
@@ -560,7 +562,7 @@ export class ImageResizeView {
     _handle: HTMLElement;
     _img: HTMLElement;
     _outer: HTMLElement;
-    constructor(node: any, view: any, getPos: any) {
+    constructor(node: any, view: any, getPos: any, addDocTab: any) {
         this._handle = document.createElement("span");
         this._img = document.createElement("img");
         this._outer = document.createElement("span");
@@ -581,6 +583,33 @@ export class ImageResizeView {
         this._handle.style.bottom = "-10px";
         this._handle.style.right = "-10px";
         let self = this;
+        this._img.onpointerdown = function (e: any) {
+            if (!view.isOverlay || e.ctrlKey) {
+                e.preventDefault();
+                e.stopPropagation();
+                DocServer.GetRefField(node.attrs.docid).then(async linkDoc => {
+                    if (linkDoc instanceof Doc) {
+                        let proto = Doc.GetProto(linkDoc);
+                        let targetContext = await Cast(proto.targetContext, Doc);
+                        let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
+                        if (jumpToDoc) {
+                            if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+
+                                DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+                                return;
+                            }
+                        }
+                        if (targetContext) {
+                            DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+                        } else if (jumpToDoc) {
+                            DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+                        } else {
+                            DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+                        } e.ctrlKey
+                    }
+                });
+            }
+        }
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
@@ -599,7 +628,7 @@ export class ImageResizeView {
                 document.removeEventListener("pointerup", onpointerup);
                 view.dispatch(
                     view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
-                        { src: node.attrs.src, width: self._outer.style.width })
+                        { ...node.attrs, width: self._outer.style.width })
                 );
                 FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
             };
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 27e0d181f..c3a2cb214 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -25,7 +25,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
     private _textTargetDiv: HTMLDivElement | undefined;
     private _textProxyDiv: React.RefObject<HTMLDivElement>;
     private _textBottom: boolean | undefined;
-    private _textAutoHeight: boolean | undefined;
     private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); };
     private _outerdiv: HTMLElement | null = null;
     private _textBox: FormattedTextBox | undefined;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 794d3a573..ae1393fc4 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -278,11 +278,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     drop = async (e: Event, de: DragManager.DropEvent) => {
         // We're dealing with a link to a document
         if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
+            let target = de.data.embeddableSourceDoc;
             // We're dealing with an internal document drop
             let url = de.data.urlField.url.href;
             let model: NodeType = (url.includes(".mov") || url.includes(".mp4")) ? schema.nodes.video : schema.nodes.image;
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
-            this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url })));
+            this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
+            DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]);
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
             const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
@@ -641,7 +643,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
                 dispatchTransaction: this.dispatchTransaction,
                 nodeViews: {
-                    image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
+                    image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
                     star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
                     ordered_list(node, view, getPos) { return new OrderedListView(); },
                     footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
@@ -696,7 +698,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
                 href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
             }
-            let node = this._editorView!.state.doc.nodeAt(this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY })!.pos);
+            let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+            let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
             if (node) {
                 let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
                 href = link && link.attrs.href;
-- 
cgit v1.2.3-70-g09d2


From 82b0b08979f63b88b93bf1419cde659cb262e2a3 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 9 Sep 2019 18:16:12 -0400
Subject: need toDOM for images or you cant paste them

---
 src/client/util/RichTextSchema.tsx | 5 +++++
 1 file changed, 5 insertions(+)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 05a37759f..e71031a87 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -144,6 +144,11 @@ export const nodes: { [index: string]: NodeSpec } = {
                 };
             }
         }],
+        // TODO if we don't define toDom, dragging the image crashes. Why?
+        toDOM(node) {
+            const attrs = { style: `width: ${node.attrs.width}` };
+            return ["img", { ...node.attrs, ...attrs }];
+        }
     },
 
     video: {
-- 
cgit v1.2.3-70-g09d2


From a1e08c0be1a1067232ea71ed3344ce44d9522bf7 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 9 Sep 2019 23:17:50 -0400
Subject: added center alignment for text using ^^

---
 src/client/util/ParagraphNodeSpec.ts          | 133 ++++++++++++++++++++++++++
 src/client/util/ProsemirrorExampleTransfer.ts |  24 ++---
 src/client/util/RichTextRules.ts              |  23 +++++
 src/client/util/RichTextSchema.tsx            |  20 ++--
 src/client/util/clamp.js                      |  15 +++
 src/client/util/convertToCSSPTValue.js        |  43 +++++++++
 src/client/util/toCSSLineSpacing.js           |  64 +++++++++++++
 7 files changed, 302 insertions(+), 20 deletions(-)
 create mode 100644 src/client/util/ParagraphNodeSpec.ts
 create mode 100644 src/client/util/clamp.js
 create mode 100644 src/client/util/convertToCSSPTValue.js
 create mode 100644 src/client/util/toCSSLineSpacing.js

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ParagraphNodeSpec.ts b/src/client/util/ParagraphNodeSpec.ts
new file mode 100644
index 000000000..3a993e1ff
--- /dev/null
+++ b/src/client/util/ParagraphNodeSpec.ts
@@ -0,0 +1,133 @@
+import clamp from './clamp';
+import convertToCSSPTValue from './convertToCSSPTValue';
+import toCSSLineSpacing from './toCSSLineSpacing';
+import { Node, DOMOutputSpec } from 'prosemirror-model';
+
+//import type { NodeSpec } from './Types';
+type NodeSpec = {
+    attrs?: { [key: string]: any },
+    content?: string,
+    draggable?: boolean,
+    group?: string,
+    inline?: boolean,
+    name?: string,
+    parseDOM?: Array<any>,
+    toDOM?: (node: any) => DOMOutputSpec,
+};
+
+// This assumes that every 36pt maps to one indent level.
+export const INDENT_MARGIN_PT_SIZE = 36;
+export const MIN_INDENT_LEVEL = 0;
+export const MAX_INDENT_LEVEL = 7;
+export const ATTRIBUTE_INDENT = 'data-indent';
+
+export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']);
+
+const ALIGN_PATTERN = /(left|right|center|justify)/;
+
+// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js
+// :: NodeSpec A plain paragraph textblock. Represented in the DOM
+// as a `<p>` element.
+const ParagraphNodeSpec: NodeSpec = {
+    attrs: {
+        align: { default: null },
+        color: { default: null },
+        id: { default: null },
+        indent: { default: null },
+        lineSpacing: { default: null },
+        // TODO: Add UI to let user edit / clear padding.
+        paddingBottom: { default: null },
+        // TODO: Add UI to let user edit / clear padding.
+        paddingTop: { default: null },
+    },
+    content: 'inline*',
+    group: 'block',
+    parseDOM: [{ tag: 'p', getAttrs }],
+    toDOM,
+};
+
+function getAttrs(dom: HTMLElement): Object {
+    const {
+        lineHeight,
+        textAlign,
+        marginLeft,
+        paddingTop,
+        paddingBottom,
+    } = dom.style;
+
+    let align = dom.getAttribute('align') || textAlign || '';
+    align = ALIGN_PATTERN.test(align) ? align : "";
+
+    let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10);
+
+    if (!indent && marginLeft) {
+        indent = convertMarginLeftToIndentValue(marginLeft);
+    }
+
+    indent = indent || MIN_INDENT_LEVEL;
+
+    const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : null;
+
+    const id = dom.getAttribute('id') || '';
+    return { align, indent, lineSpacing, paddingTop, paddingBottom, id };
+}
+
+function toDOM(node: Node): DOMOutputSpec {
+    const {
+        align,
+        indent,
+        lineSpacing,
+        paddingTop,
+        paddingBottom,
+        id,
+    } = node.attrs;
+    const attrs: { [key: string]: any } | null = {};
+
+    let style = '';
+    if (align && align !== 'left') {
+        style += `text-align: ${align};`;
+    }
+
+    if (lineSpacing) {
+        const cssLineSpacing = toCSSLineSpacing(lineSpacing);
+        style +=
+            `line-height: ${cssLineSpacing};` +
+            // This creates the local css variable `--czi-content-line-height`
+            // that its children may apply.
+            `--czi-content-line-height: ${cssLineSpacing}`;
+    }
+
+    if (paddingTop && !EMPTY_CSS_VALUE.has(paddingTop)) {
+        style += `padding-top: ${paddingTop};`;
+    }
+
+    if (paddingBottom && !EMPTY_CSS_VALUE.has(paddingBottom)) {
+        style += `padding-bottom: ${paddingBottom};`;
+    }
+
+    style && (attrs.style = style);
+
+    if (indent) {
+        attrs[ATTRIBUTE_INDENT] = String(indent);
+    }
+
+    if (id) {
+        attrs.id = id;
+    }
+
+    return ['p', attrs, 0];
+}
+
+export const toParagraphDOM = toDOM;
+export const getParagraphNodeAttrs = getAttrs;
+
+export function convertMarginLeftToIndentValue(marginLeft: string): number {
+    const ptValue = convertToCSSPTValue(marginLeft);
+    return clamp(
+        MIN_INDENT_LEVEL,
+        Math.floor(ptValue / INDENT_MARGIN_PT_SIZE),
+        MAX_INDENT_LEVEL
+    );
+}
+
+export default ParagraphNodeSpec;
\ No newline at end of file
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index cc2ae7d38..bac0177ad 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -52,18 +52,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
 
     bind("Ctrl->", wrapIn(schema.nodes.blockquote));
 
-    bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        let newNode = schema.nodes.footnote.create({});
-        if (dispatch && state.selection.from === state.selection.to) {
-            let tr = state.tr;
-            tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
-            dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
-                tr.doc.resolve(  // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
-                    tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
-            return true;
-        }
-        return false;
-    });
+    // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+    //     let newNode = schema.nodes.footnote.create({});
+    //     if (dispatch && state.selection.from === state.selection.to) {
+    //         let tr = state.tr;
+    //         tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+    //         dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+    //             tr.doc.resolve(  // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+    //                 tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+    //         return true;
+    //     }
+    //     return false;
+    // });
 
 
     let cmd = chainCommands(exitCode, (state, dispatch) => {
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 979b76988..5d1131410 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,6 +1,7 @@
 import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
 import { schema } from "./RichTextSchema";
 import { wrappingInputRule } from "./prosemirrorPatches";
+import { NodeSelection } from "prosemirror-state";
 
 export const inpRules = {
     rules: [
@@ -58,5 +59,27 @@ export const inpRules = {
             (state, match, start, end) => {
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }))
             }),
+        new InputRule(
+            new RegExp(/^\^\^\s$/),
+            (state, match, start, end) => {
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                let sm = state.storedMarks || undefined;
+                return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                    state.tr;
+            }),
+        new InputRule(
+            new RegExp(/\^f\s$/),
+            (state, match, start, end) => {
+                let newNode = schema.nodes.footnote.create({});
+                let tr = state.tr;
+                tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
+                return tr.setSelection(new NodeSelection( // select the footnote node to open its display
+                    tr.doc.resolve(  // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+                        tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
+            }),
+        // let newNode = schema.nodes.footnote.create({});
+        // if (dispatch && state.selection.from === state.selection.to) {
+        //     return true;
+        // }
     ]
 };
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index e71031a87..5081c81d8 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -11,6 +11,7 @@ import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 import { DocServer } from "../DocServer";
 import { Cast, NumCast } from "../../new_fields/Types";
 import { DocumentManager } from "./DocumentManager";
+import ParagraphNodeSpec from "./ParagraphNodeSpec";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -23,6 +24,7 @@ export const nodes: { [index: string]: NodeSpec } = {
         content: "block+"
     },
 
+
     footnote: {
         group: "inline",
         content: "inline*",
@@ -37,14 +39,16 @@ export const nodes: { [index: string]: NodeSpec } = {
         parseDOM: [{ tag: "footnote" }]
     },
 
-    // :: NodeSpec A plain paragraph textblock. Represented in the DOM
-    // as a `<p>` element.
-    paragraph: {
-        content: "inline*",
-        group: "block",
-        parseDOM: [{ tag: "p" }],
-        toDOM() { return pDOM; }
-    },
+    // // :: NodeSpec A plain paragraph textblock. Represented in the DOM
+    // // as a `<p>` element.
+    // paragraph: {
+    //     content: "inline*",
+    //     group: "block",
+    //     parseDOM: [{ tag: "p" }],
+    //     toDOM() { return pDOM; }
+    // },
+
+    paragraph: ParagraphNodeSpec,
 
     // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
     blockquote: {
diff --git a/src/client/util/clamp.js b/src/client/util/clamp.js
new file mode 100644
index 000000000..9c7fd78a4
--- /dev/null
+++ b/src/client/util/clamp.js
@@ -0,0 +1,15 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+  value: true
+});
+exports.default = clamp;
+function clamp(min, val, max) {
+  if (val < min) {
+    return min;
+  }
+  if (val > max) {
+    return max;
+  }
+  return val;
+}
\ No newline at end of file
diff --git a/src/client/util/convertToCSSPTValue.js b/src/client/util/convertToCSSPTValue.js
new file mode 100644
index 000000000..179557953
--- /dev/null
+++ b/src/client/util/convertToCSSPTValue.js
@@ -0,0 +1,43 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+exports.PT_TO_PX_RATIO = exports.PX_TO_PT_RATIO = undefined;
+exports.default = convertToCSSPTValue;
+exports.toClosestFontPtSize = toClosestFontPtSize;
+
+// var _FontSizeCommandMenuButton = require('./ui/FontSizeCommandMenuButton');
+
+var SIZE_PATTERN = /([\d\.]+)(px|pt)/i;
+
+var PX_TO_PT_RATIO = exports.PX_TO_PT_RATIO = 0.7518796992481203; // 1 / 1.33.
+var PT_TO_PX_RATIO = exports.PT_TO_PX_RATIO = 1.33;
+
+function convertToCSSPTValue(styleValue) {
+    var matches = styleValue.match(SIZE_PATTERN);
+    if (!matches) {
+        return 0;
+    }
+    var value = parseFloat(matches[1]);
+    var unit = matches[2];
+    if (!value || !unit) {
+        return 0;
+    }
+    if (unit === 'px') {
+        value = PX_TO_PT_RATIO * value;
+    }
+    return value;
+}
+
+function toClosestFontPtSize(styleValue) {
+    var originalPTValue = convertToCSSPTValue(styleValue);
+
+    // if (_FontSizeCommandMenuButton.FONT_PT_SIZES.includes(originalPTValue)) {
+    //     return originalPTValue;
+    // }
+
+    return _FontSizeCommandMenuButton.FONT_PT_SIZES.reduce(function (prev, curr) {
+        return Math.abs(curr - originalPTValue) < Math.abs(prev - originalPTValue) ? curr : prev;
+    }, Number.NEGATIVE_INFINITY);
+}
\ No newline at end of file
diff --git a/src/client/util/toCSSLineSpacing.js b/src/client/util/toCSSLineSpacing.js
new file mode 100644
index 000000000..939d11a0e
--- /dev/null
+++ b/src/client/util/toCSSLineSpacing.js
@@ -0,0 +1,64 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+exports.default = toCSSLineSpacing;
+
+
+// Line spacing names and their values.
+var LINE_SPACING_100 = exports.LINE_SPACING_100 = '125%';
+var LINE_SPACING_115 = exports.LINE_SPACING_115 = '138%';
+var LINE_SPACING_150 = exports.LINE_SPACING_150 = '165%';
+var LINE_SPACING_200 = exports.LINE_SPACING_200 = '232%';
+
+var SINGLE_LINE_SPACING = exports.SINGLE_LINE_SPACING = LINE_SPACING_100;
+var DOUBLE_LINE_SPACING = exports.DOUBLE_LINE_SPACING = LINE_SPACING_200;
+
+var NUMBER_VALUE_PATTERN = /^\d+(.\d+)?$/;
+
+// Normalize the css line-height vlaue to percentage-based value if applicable.
+// Also, it calibrates the incorrect line spacing value exported from Google
+// Doc.
+function toCSSLineSpacing(source) {
+    if (!source) {
+        return '';
+    }
+
+    var strValue = String(source);
+
+    // e.g. line-height: 1.5;
+    if (NUMBER_VALUE_PATTERN.test(strValue)) {
+        var numValue = parseFloat(strValue);
+        strValue = String(Math.round(numValue * 100)) + '%';
+    }
+
+    // Google Doc exports line spacing with wrong values. For instance:
+    // - Single => 100%
+    // - 1.15 => 115%
+    // - Double => 200%
+    // But the actual CSS value measured in Google Doc is like this:
+    // - Single => 125%
+    // - 1.15 => 138%
+    // - Double => 232%
+    // The following `if` block will calibrate the value if applicable.
+
+    if (strValue === '100%') {
+        return LINE_SPACING_100;
+    }
+
+    if (strValue === '115%') {
+        return LINE_SPACING_115;
+    }
+
+    if (strValue === '150%') {
+        return LINE_SPACING_150;
+    }
+
+    if (strValue === '200%') {
+        return LINE_SPACING_200;
+    }
+
+    // e.g. line-height: 15px;
+    return strValue;
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 149fa3116cdbb58f2eed144cc0bb90c1b1cd2b2a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 9 Sep 2019 23:49:13 -0400
Subject: fixed footnote saving

---
 src/client/util/RichTextSchema.tsx | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 5081c81d8..3def4a579 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -757,10 +757,11 @@ export class FootnoteView {
         this.innerView.updateState(state);
 
         if (!tr.getMeta("fromOutside")) {
-            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
-            for (let steps of transactions) {
-                for (let step of steps) {
-                    outerTr.step(step.map(offsetMap));
+            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
+            for (let i = 0; i < transactions.length; i++) {
+                let steps = transactions[i].steps;
+                for (let j = 0; j < steps.length; j++) {
+                    outerTr.step(steps[j].map(offsetMap));
                 }
             }
             if (outerTr.docChanged) this.outerView.dispatch(outerTr);
-- 
cgit v1.2.3-70-g09d2


From 5ea7e3318620865146318e0f3826b6f13aec0675 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 10 Sep 2019 13:38:16 -0400
Subject: added more support for creating stylized layouts

---
 src/Utils.ts                                       | 92 ++++++++++++++++++++++
 src/client/util/ProsemirrorExampleTransfer.ts      |  5 --
 src/client/util/RichTextRules.ts                   | 40 ++++++++++
 src/client/util/RichTextSchema.tsx                 | 19 +++++
 src/client/views/ContextMenu.tsx                   |  1 +
 src/client/views/DocumentDecorations.tsx           |  5 ++
 src/client/views/InkingControl.tsx                 | 32 +++++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 27 ++++---
 .../collections/collectionFreeForm/MarqueeView.tsx | 26 +++++-
 src/client/views/nodes/FormattedTextBox.tsx        | 48 ++++++++---
 .../authentication/models/current_user_utils.ts    | 20 ++---
 11 files changed, 270 insertions(+), 45 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index f805ae872..3921a49c3 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -53,6 +53,98 @@ export class Utils {
         document.body.removeChild(textArea);
     }
 
+    public static fromRGBAstr(rgba: string) {
+        let rm = rgba.match(/rgb[a]?\(([0-9]+)/);
+        let r = rm ? Number(rm[1]) : 0;
+        let gm = rgba.match(/rgb[a]?\([0-9]+,([0-9]+)/);
+        let g = gm ? Number(gm[1]) : 0;
+        let bm = rgba.match(/rgb[a]?\([0-9]+,[0-9]+,([0-9]+)/);
+        let b = bm ? Number(bm[1]) : 0;
+        let am = rgba.match(/rgba?\([0-9]+,[0-9]+,[0-9]+,([0-9]+)/);
+        let a = am ? Number(am[1]) : 0;
+        return { r: r, g: g, b: b, a: a };
+    }
+    public static toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
+        return "rgba(" + col.r + "," + col.g + "," + col.b + (col.a !== undefined ? "," + col.a : "") + ")";
+    }
+
+    public static HSLtoRGB(h: number, s: number, l: number) {
+        // Must be fractions of 1
+        // s /= 100;
+        // l /= 100;
+
+        let c = (1 - Math.abs(2 * l - 1)) * s,
+            x = c * (1 - Math.abs((h / 60) % 2 - 1)),
+            m = l - c / 2,
+            r = 0,
+            g = 0,
+            b = 0;
+        if (0 <= h && h < 60) {
+            r = c; g = x; b = 0;
+        } else if (60 <= h && h < 120) {
+            r = x; g = c; b = 0;
+        } else if (120 <= h && h < 180) {
+            r = 0; g = c; b = x;
+        } else if (180 <= h && h < 240) {
+            r = 0; g = x; b = c;
+        } else if (240 <= h && h < 300) {
+            r = x; g = 0; b = c;
+        } else if (300 <= h && h < 360) {
+            r = c; g = 0; b = x;
+        }
+        r = Math.round((r + m) * 255);
+        g = Math.round((g + m) * 255);
+        b = Math.round((b + m) * 255);
+        return { r: r, g: g, b: b };
+    }
+
+    public static RGBToHSL(r: number, g: number, b: number) {
+        // Make r, g, and b fractions of 1
+        r /= 255;
+        g /= 255;
+        b /= 255;
+
+        // Find greatest and smallest channel values
+        let cmin = Math.min(r, g, b),
+            cmax = Math.max(r, g, b),
+            delta = cmax - cmin,
+            h = 0,
+            s = 0,
+            l = 0;
+        // Calculate hue
+
+        // No difference
+        if (delta == 0)
+            h = 0;
+        // Red is max
+        else if (cmax == r)
+            h = ((g - b) / delta) % 6;
+        // Green is max
+        else if (cmax == g)
+            h = (b - r) / delta + 2;
+        // Blue is max
+        else
+            h = (r - g) / delta + 4;
+
+        h = Math.round(h * 60);
+
+        // Make negative hues positive behind 360°
+        if (h < 0)
+            h += 360; // Calculate lightness
+
+        l = (cmax + cmin) / 2;
+
+        // Calculate saturation
+        s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+
+        // Multiply l and s by 100
+        // s = +(s * 100).toFixed(1);
+        // l = +(l * 100).toFixed(1);
+
+        return { h: h, s: s, l: l };
+    }
+
+
     public static GetClipboardText(): string {
         var textArea = document.createElement("textarea");
         document.body.appendChild(textArea);
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index bac0177ad..1d2d33800 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -14,7 +14,6 @@ export type KeyMap = { [key: string]: any };
 export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
     let keys: { [key: string]: any } = {}, type;
 
-    keys["ACTIVE"] = false;
     function bind(key: string, cmd: any) {
         if (mapKeys) {
             let mapped = mapKeys[key];
@@ -148,10 +147,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         return tx;
     }
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
-        if (!keys["ACTIVE"]) {// hack to ignore an initial carriage return when creating a textbox from the action menu
-            dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from - 1, state.selection.from)).deleteSelection());
-            return true;
-        }
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
             if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 5d1131410..00e671db9 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -2,6 +2,9 @@ import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from
 import { schema } from "./RichTextSchema";
 import { wrappingInputRule } from "./prosemirrorPatches";
 import { NodeSelection } from "prosemirror-state";
+import { NumCast, Cast } from "../../new_fields/Types";
+import { Doc } from "../../new_fields/Doc";
+import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 
 export const inpRules = {
     rules: [
@@ -57,6 +60,12 @@ export const inpRules = {
         new InputRule(
             new RegExp(/^#([0-9]+)\s$/),
             (state, match, start, end) => {
+                let size = Number(match[1]);
+                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                if (ruleProvider && heading) {
+                    ruleProvider["ruleSize_" + heading] = size;
+                }
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }))
             }),
         new InputRule(
@@ -64,9 +73,40 @@ export const inpRules = {
             (state, match, start, end) => {
                 let node = (state.doc.resolve(start) as any).nodeAfter;
                 let sm = state.storedMarks || undefined;
+                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                if (ruleProvider && heading) {
+                    ruleProvider["ruleAlign_" + heading] = "center";
+                }
                 return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
             }),
+        new InputRule(
+            new RegExp(/^\[\[\s$/),
+            (state, match, start, end) => {
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                let sm = state.storedMarks || undefined;
+                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                if (ruleProvider && heading) {
+                    ruleProvider["ruleAlign_" + heading] = "left";
+                }
+                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$/),
+            (state, match, start, end) => {
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                let sm = state.storedMarks || undefined;
+                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
+                if (ruleProvider && heading) {
+                    ruleProvider["ruleAlign_" + heading] = "right";
+                }
+                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$/),
             (state, match, start, end) => {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 3def4a579..f027a4bf7 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -12,6 +12,7 @@ import { DocServer } from "../DocServer";
 import { Cast, NumCast } from "../../new_fields/Types";
 import { DocumentManager } from "./DocumentManager";
 import ParagraphNodeSpec from "./ParagraphNodeSpec";
+import { times } from "async";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -422,6 +423,24 @@ export const marks: { [index: string]: MarkSpec } = {
         toDOM() { return codeDOM; }
     },
 
+    // pFontFamily: {
+    //     attrs: {
+    //         style: { default: 'font-family: "Times New Roman", Times, serif;' },
+    //     },
+    //     parseDOM: [{
+    //         tag: "span", getAttrs(dom: any) {
+    //             if (getComputedStyle(dom).font === "Times New Roman") return { style: `font-family: "Times New Roman", Times, serif;` };
+    //             if (getComputedStyle(dom).font === "Arial, Helvetica") return { style: `font-family: Arial, Helvetica, sans-serif;` };
+    //             if (getComputedStyle(dom).font === "Georgia") return { style: `font-family: Georgia, serif;` };
+    //             if (getComputedStyle(dom).font === "Comic Sans") return { style: `font-family: "Comic Sans MS", cursive, sans-serif;` };
+    //             if (getComputedStyle(dom).font === "Tahoma, Geneva") return { style: `font-family: Tahoma, Geneva, sans-serif;` };
+    //         }
+    //     }],
+    //     toDOM: (node: any) => ['span', {
+    //         style: node.attrs.style
+    //     }]
+    // },
+
 
     /* FONTS */
     timesNewRoman: {
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 890bfdfb7..68b97f2b6 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -250,6 +250,7 @@ export class ContextMenu extends React.Component {
             const item = this.flatItems[this.selectedIndex];
             item && item.event();
             this.closeMenu();
+            e.preventDefault();
         }
     }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 7cdb16f52..94aab8b2f 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -621,6 +621,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 doc.y = (doc.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 || (!BoolCast(doc.ignoreAspect) && nwidth && nheight);
+                if (fixedAspect && e.ctrlKey && BoolCast(doc.ignoreAspect)) {
+                    doc.ignoreAspect = false;
+                    proto.nativeWidth = nwidth = doc.width || 0;
+                    proto.nativeHeight = nheight = doc.height || 0;
+                }
                 if (fixedAspect && (!nwidth || !nheight)) {
                     proto.nativeWidth = nwidth = doc.width || 0;
                     proto.nativeHeight = nheight = doc.height || 0;
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index eb6312e78..519792308 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -10,8 +10,10 @@ import { InkTool } from "../../new_fields/InkField";
 import { Doc } from "../../new_fields/Doc";
 import { undoBatch, UndoManager } from "../util/UndoManager";
 import { StrCast, NumCast, Cast } from "../../new_fields/Types";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { MainOverlayTextBox } from "./MainOverlayTextBox";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { Utils } from "../../Utils";
 
 library.add(faPen, faHighlighter, faEraser, faBan);
 
@@ -49,11 +51,34 @@ export class InkingControl extends React.Component {
             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 oldColor = StrCast(targetDoc.backgroundColor);
-                targetDoc.backgroundColor = this._selectedColor;
+                if (view.props.ContainingCollectionView && view.props.ContainingCollectionView.props.Document.colorPalette) {
+                    let cp = Cast(view.props.ContainingCollectionView.props.Document.colorPalette, listSpec("string")) as string[];
+                    let closest = 0;
+                    let dist = 10000000;
+                    let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
+                    for (let i = 0; i < cp.length; i++) {
+                        let cpcol = Utils.fromRGBAstr(cp[i]);
+                        let d = Math.sqrt((ccol.r - cpcol.r) * (ccol.r - cpcol.r) + (ccol.b - cpcol.b) * (ccol.b - cpcol.b) + (ccol.g - cpcol.g) * (ccol.g - cpcol.g));
+                        if (d < dist) {
+                            dist = d;
+                            closest = i;
+                        }
+                    }
+                    cp[closest] = "rgb(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + ")";
+                    view.props.ContainingCollectionView.props.Document.colorPalette = new List(cp);
+                    targetDoc.backgroundColor = cp[closest];
+                } else
+                    targetDoc.backgroundColor = this._selectedColor;
                 if (view.props.Document.heading) {
                     let cv = view.props.ContainingCollectionView;
                     let ruleProvider = cv && (Cast(cv.props.Document.ruleProvider, Doc) as Doc);
-                    cv && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = this._selectedColor);
+                    let parback = cv && StrCast(cv.props.Document.backgroundColor);
+                    cv && parback && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb));
+                    // if (parback && cv && parback.indexOf("rgb") !== -1) {
+                    //     let parcol = Utils.fromRGBAstr(parback);
+                    //     let hsl = Utils.RGBToHSL(parcol.r, parcol.g, parcol.b);
+                    //     cv && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = color.hsl.s - hsl.s);
+                    // }
                 }
                 return {
                     target: targetDoc,
@@ -67,7 +92,6 @@ export class InkingControl extends React.Component {
             });
         }
     });
-
     @action
     switchWidth = (width: string): void => {
         this._selectedWidth = width;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f7c1bedbb..c9b2c30b1 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,7 +8,7 @@ import { Id } from "../../../../new_fields/FieldSymbols";
 import { InkField, StrokeData } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../../new_fields/Types";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../../new_fields/Types";
 import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
 import { Docs } from "../../../documents/Documents";
@@ -262,14 +262,23 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 newBox.heading = 2;
             }
         }
-        let ruleProvider = Cast(this.props.Document.ruleProvider, Doc);
-        if (!(ruleProvider instanceof Doc)) ruleProvider = this.props.Document;
-        let col = StrCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]);
-        let round = StrCast(ruleProvider["ruleRounding_" + NumCast(newBox.heading)]);
-        round && (newBox.borderRounding = round);
-        col && (newBox.backgroundColor = col);
-        newBox.ruleProvider = ruleProvider;
-        this.addDocument(newBox, false);
+        PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => {
+            if (!ruleProvider) ruleProvider = this.props.Document;
+            // saturation shift
+            // let col = NumCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]);
+            // let back = Utils.fromRGBAstr(StrCast(this.props.Document.backgroundColor));
+            // let hsl = Utils.RGBToHSL(back.r, back.g, back.b);
+            // let newcol = { h: hsl.h, s: hsl.s + col, l: hsl.l };
+            // col && (Doc.GetProto(newBox).backgroundColor = Utils.toRGBAstr(Utils.HSLtoRGB(newcol.h, newcol.s, newcol.l)));
+            // OR transparency set
+            let col = StrCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]);
+            col && (Doc.GetProto(newBox).backgroundColor = col);
+
+            let round = StrCast(ruleProvider["ruleRounding_" + NumCast(newBox.heading)]);
+            round && (newBox.borderRounding = round);
+            newBox.ruleProvider = ruleProvider;
+            this.addDocument(newBox, false);
+        });
     }
     private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
         this.props.addDocument(newBox, false);
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 100e6d817..56d8127e2 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,11 +1,11 @@
 import * as htmlToImage from "html-to-image";
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, FieldResult } from "../../../../new_fields/Doc";
+import { Doc, FieldResult, DocListCast } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
 import { InkField, StrokeData } from "../../../../new_fields/InkField";
 import { List } from "../../../../new_fields/List";
-import { Cast, NumCast } from "../../../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../../../new_fields/Types";
 import { Utils } from "../../../../Utils";
 import { DocServer } from "../../../DocServer";
 import { Docs } from "../../../documents/Documents";
@@ -20,6 +20,8 @@ import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
 import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField";
+import { string } from "prop-types";
+import { listSpec } from "../../../../new_fields/Schema";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -272,14 +274,30 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                     return d;
                 });
             }
+            let defaultPalette = ["rgb(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+                "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
+            let colorPalette = Cast(this.props.container.props.Document.colorPalette, listSpec("string"));
+            if (!colorPalette) this.props.container.props.Document.colorPalette = new List<string>(defaultPalette);
+            let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
+            let usedPaletted = new Map<string, number>();
+            [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
+                let bg = StrCast(child.backgroundColor);
+                if (palette.indexOf(bg) !== -1) {
+                    palette.splice(palette.indexOf(bg), 1);
+                    if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
+                    else usedPaletted.set(bg, 1);
+                }
+            });
+            let usedSequnce = Array.from(usedPaletted.keys()).sort((a, b) => usedPaletted.get(a)! < usedPaletted.get(b)! ? -1 : usedPaletted.get(a)! > usedPaletted.get(b)! ? 1 : 0);
+            let chosenColor = usedPaletted.get("white") || usedPaletted.get("rgb(255,255,255)") && usedPaletted.size === 1 ? "white" : palette.length ? palette[0] : usedSequnce[0];
             let inkData = this.ink ? this.ink.inkData : undefined;
             let newCollection = Docs.Create.FreeformDocument(selected, {
                 x: bounds.left,
                 y: bounds.top,
                 panX: 0,
                 panY: 0,
-                backgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
-                defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : "white",
+                backgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
+                defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
                 width: bounds.width,
                 height: bounds.height,
                 title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6a6000dc5..0ea36cdc2 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -13,7 +13,7 @@ import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
 import { Copy, Id } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
 import { RichTextField, ToPlainText, FromPlainText } from "../../../new_fields/RichTextField";
-import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types";
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
 import { Utils, numberRange, timenow } from '../../../Utils';
 import { DocServer } from "../../DocServer";
@@ -39,6 +39,7 @@ import { ReplaceStep } from 'prosemirror-transform';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
 import { inputRules } from 'prosemirror-inputrules';
+import { select } from 'async';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -203,8 +204,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
 
-            this._keymap["ACTIVE"] = true; // hack to ignore an initial carriage return when creating a textbox from the action menu
-
             this._applyingChange = true;
             this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
             this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
@@ -353,7 +352,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     _keymap: any = undefined;
     @computed get config() {
         this._keymap = buildKeymap(schema);
-        this._keymap["ACTIVE"] = this.extensionDoc.text;  // hack to ignore an initial carriage return only when creating a textbox from the action menu
         return {
             schema,
             plugins: this.props.isOverlay ? [
@@ -659,7 +657,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
         }
 
-        if (this.props.Document[Id] === FormattedTextBox.SelectOnLoad) {
+        let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad;
+        if (selectOnLoad) {
             FormattedTextBox.SelectOnLoad = "";
             this.props.select(false);
         }
@@ -667,15 +666,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
         this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
         let heading = this.props.Document.heading;
-        if (heading) {
-            let ruleProvider = Cast(this.props.Document.ruleProvider, Doc);
-            if (ruleProvider instanceof Doc) {
-                let font = StrCast(ruleProvider["ruleFont_" + heading]);
-                let size = NumCast(ruleProvider["ruleSize_" + heading]);
-                size && (this._editorView!.state.storedMarks = [...this._editorView!.state.storedMarks, schema.marks.pFontSize.create({ fontSize: size })]);
-                font && (this._editorView!.state.storedMarks = [...this._editorView!.state.storedMarks, font === "Arial" ? schema.marks.arial.create() : schema.marks.comicSans.create()]);
-            }
+        if (heading && selectOnLoad) {
+            PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => {
+                if (ruleProvider) {
+                    let align = StrCast(ruleProvider["ruleAlign_" + heading]);
+                    let font = StrCast(ruleProvider["ruleFont_" + heading]);
+                    let size = NumCast(ruleProvider["ruleSize_" + heading]);
+                    if (align) {
+                        let tr = this._editorView!.state.tr;
+                        tr = tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(2))).
+                            replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: align }), true).
+                            setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(0)));
+                        this._editorView!.dispatch(tr);
+                    }
+                    let sm = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
+                    size && (sm = [...sm, schema.marks.pFontSize.create({ fontSize: size })]);
+                    font && (sm = [...sm, this.getFont(font)]);
+                    this._editorView!.dispatch(this._editorView!.state.tr.setStoredMarks(sm));
+                }
+            });
+        }
+    }
+    getFont(font: string) {
+        switch (font) {
+            case "Arial": return schema.marks.arial.create();
+            case "Times New Roman": return schema.marks.timesNewRoman.create();
+            case "Georgia": return schema.marks.georgia.create();
+            case "Comic Sans MS": return schema.marks.comicSans.create();
+            case "Tahoma": return schema.marks.tahoma.create();
+            case "Impact": return schema.marks.impact.create();
+            case "ACrimson Textrial": return schema.marks.crimson.create();
         }
+        return schema.marks.arial.create();
     }
 
     componentWillUnmount() {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 9866e22eb..9d35d36d3 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -115,17 +115,17 @@ export class CurrentUserUtils {
                 throw new Error("There should be a user id! Why does Dash think there isn't one?");
             }
         });
-        try {
-            const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" });
-            NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
-            await Gateway.Instance.ClearCatalog();
-            const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
-            let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc)));
-            let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras);
-            // if (catprom) await Promise.all(catprom);
-        } catch (e) {
+        // try {
+        //     const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" });
+        //     NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json());
+        //     await Gateway.Instance.ClearCatalog();
+        //     const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []);
+        //     let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc)));
+        //     let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras);
+        //     // if (catprom) await Promise.all(catprom);
+        // } catch (e) {
 
-        }
+        // }
     }
 
     /* Northstar catalog ... really just for testing so this should eventually go away */
-- 
cgit v1.2.3-70-g09d2


From 36a4caa28a7a83823027e6f0af47ffb1878ae86b Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 16 Sep 2019 12:19:17 -0400
Subject: fixes  for converting between google and our doc formats

---
 src/client/util/RichTextSchema.tsx            |  2 ++
 src/client/views/nodes/DocumentView.tsx       |  1 +
 src/new_fields/RichTextUtils.ts               | 32 +++++++++++++++------------
 src/server/apis/google/existing_uploads.json  |  2 +-
 src/server/credentials/google_docs_token.json |  2 +-
 5 files changed, 23 insertions(+), 16 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index f027a4bf7..2750203f2 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -135,6 +135,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             alt: { default: null },
             title: { default: null },
             float: { default: "left" },
+            location: { default: "onRight" },
             docid: { default: "" }
         },
         group: "inline",
@@ -616,6 +617,7 @@ export class ImageResizeView {
                 e.preventDefault();
                 e.stopPropagation();
                 DocServer.GetRefField(node.attrs.docid).then(async linkDoc => {
+                    const location = node.attrs.location;
                     if (linkDoc instanceof Doc) {
                         let proto = Doc.GetProto(linkDoc);
                         let targetContext = await Cast(proto.targetContext, Doc);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 81805af64..bf7e7df97 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -606,6 +606,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 Doc.MakeTemplate(fieldTemplate, metaKey, proto);
 
                 Doc.ApplyTemplateTo(docTemplate, document, undefined, false);
+                document.customLayout = document.layout;
             }
         });
     });
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 44fdae638..ff28e6861 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -195,16 +195,19 @@ export namespace RichTextUtils {
                     nodes.push(list(state.schema, items));
                 } else {
                     if (element.contents.some(child => "inlineObjectId" in child)) {
-                        let node: Node<any>;
-                        for (const child of element.contents) {
+                        const group = element.contents;
+                        group.forEach((child, i) => {
+                            let node: Opt<Node<any>>;
                             if ("inlineObjectId" in child) {
                                 node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote);
-                            } else {
+                            } else if ("content" in child && (i !== group.length - 1 || child.content!.removeTrailingNewlines().length)) {
                                 node = paragraphNode(state.schema, [child]);
                             }
-                            nodes.push(node);
-                            position += node.nodeSize;
-                        }
+                            if (node) {
+                                position += node.nodeSize;
+                                nodes.push(node);
+                            }
+                        });
                     } else {
                         let paragraph = paragraphNode(state.schema, element.contents);
                         nodes.push(paragraph);
@@ -278,7 +281,7 @@ export namespace RichTextUtils {
             } else {
                 docid = backingDocId;
             }
-            return schema.node("image", { src, width, docid, float: null });
+            return schema.node("image", { src, width, docid, float: null, location: "onRight" });
         };
 
         const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
@@ -385,12 +388,13 @@ export namespace RichTextUtils {
                             if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) {
                                 const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]);
                                 if (linkDoc instanceof Doc) {
-                                    const target = (await Cast(linkDoc.anchor2, Doc))!;
-                                    const exported = Doc.MakeAlias(target);
-                                    DocumentView.makeCustomViewClicked(exported);
-                                    const documentId = exported[Id];
-                                    target && (url = Utils.shareUrl(documentId));
-                                    linkDoc.anchor2 = exported;
+                                    let exported = (await Cast(linkDoc.anchor2, Doc))!;
+                                    if (!exported.customLayout) {
+                                        exported = Doc.MakeAlias(exported);
+                                        DocumentView.makeCustomViewClicked(exported);
+                                        linkDoc.anchor2 = exported;
+                                    }
+                                    url = Utils.shareUrl(exported[Id]);
                                 }
                             }
                             value = { url };
@@ -418,7 +422,7 @@ export namespace RichTextUtils {
                 }
                 if (node.type.name === "image") {
                     requests.push(await EncodeImage({
-                        startIndex: position + nodeSize,
+                        startIndex: position + nodeSize - 1,
                         uri: attrs.src,
                         width: attrs.width
                     }));
diff --git a/src/server/apis/google/existing_uploads.json b/src/server/apis/google/existing_uploads.json
index 4d723868e..399c27672 100644
--- a/src/server/apis/google/existing_uploads.json
+++ b/src/server/apis/google/existing_uploads.json
@@ -1 +1 @@
-{"23394":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_l.png"],"fileNames":{"clean":"upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2.png","_o":"upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_o.png","_s":"upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_s.png","_m":"upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_m.png","_l":"upload_e6e6239c-a436-4f08-bea5-de29ad4e72f2_l.png"},"contentSize":23394,"contentType":"image/jpeg"},"23406":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2b310488-a12b-4ecf-8adf-38943282381a_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2b310488-a12b-4ecf-8adf-38943282381a_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2b310488-a12b-4ecf-8adf-38943282381a_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2b310488-a12b-4ecf-8adf-38943282381a_l.png"],"fileNames":{"clean":"upload_2b310488-a12b-4ecf-8adf-38943282381a.png","_o":"upload_2b310488-a12b-4ecf-8adf-38943282381a_o.png","_s":"upload_2b310488-a12b-4ecf-8adf-38943282381a_s.png","_m":"upload_2b310488-a12b-4ecf-8adf-38943282381a_m.png","_l":"upload_2b310488-a12b-4ecf-8adf-38943282381a_l.png"},"contentSize":23406,"contentType":"image/jpeg"},"23408":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_l.png"],"fileNames":{"clean":"upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78.png","_o":"upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_o.png","_s":"upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_s.png","_m":"upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_m.png","_l":"upload_a35aef02-3dac-434f-a2d5-932ee3fc6b78_l.png"},"contentSize":23408,"contentType":"image/jpeg"},"23413":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_l.png"],"fileNames":{"clean":"upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c.png","_o":"upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_o.png","_s":"upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_s.png","_m":"upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_m.png","_l":"upload_8babdd04-c4af-4ebf-9e48-bcf5cb51ab6c_l.png"},"contentSize":23413,"contentType":"image/jpeg"},"23415":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2e51d02c-a113-4eec-8030-badb54d0f719_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2e51d02c-a113-4eec-8030-badb54d0f719_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2e51d02c-a113-4eec-8030-badb54d0f719_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_2e51d02c-a113-4eec-8030-badb54d0f719_l.png"],"fileNames":{"clean":"upload_2e51d02c-a113-4eec-8030-badb54d0f719.png","_o":"upload_2e51d02c-a113-4eec-8030-badb54d0f719_o.png","_s":"upload_2e51d02c-a113-4eec-8030-badb54d0f719_s.png","_m":"upload_2e51d02c-a113-4eec-8030-badb54d0f719_m.png","_l":"upload_2e51d02c-a113-4eec-8030-badb54d0f719_l.png"},"contentSize":23415,"contentType":"image/jpeg"},"23466":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_l.png"],"fileNames":{"clean":"upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f.png","_o":"upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_o.png","_s":"upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_s.png","_m":"upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_m.png","_l":"upload_ec4c15ea-9858-4886-bc15-03a5073b8f4f_l.png"},"contentSize":23466,"contentType":"image/jpeg"},"23625":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_l.png"],"fileNames":{"clean":"upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1.png","_o":"upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_o.png","_s":"upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_s.png","_m":"upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_m.png","_l":"upload_e1488792-46cb-4ff5-bb26-f62ea2ef91d1_l.png"},"contentSize":23625,"contentType":"image/jpeg"},"45184":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_l.png"],"fileNames":{"clean":"upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402.png","_o":"upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_o.png","_s":"upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_s.png","_m":"upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_m.png","_l":"upload_c0de990f-e3b5-4830-ab5d-17cfb4ddc402_l.png"},"contentSize":45184,"contentType":"image/jpeg"},"45211":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_l.png"],"fileNames":{"clean":"upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8.png","_o":"upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_o.png","_s":"upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_s.png","_m":"upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_m.png","_l":"upload_43ae4f14-b987-4a1a-9430-cc90bb24a9c8_l.png"},"contentSize":45211,"contentType":"image/jpeg"},"45228":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_608688a6-53bc-4d1f-adfc-3aac153066fb_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_608688a6-53bc-4d1f-adfc-3aac153066fb_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_608688a6-53bc-4d1f-adfc-3aac153066fb_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_608688a6-53bc-4d1f-adfc-3aac153066fb_l.png"],"fileNames":{"clean":"upload_608688a6-53bc-4d1f-adfc-3aac153066fb.png","_o":"upload_608688a6-53bc-4d1f-adfc-3aac153066fb_o.png","_s":"upload_608688a6-53bc-4d1f-adfc-3aac153066fb_s.png","_m":"upload_608688a6-53bc-4d1f-adfc-3aac153066fb_m.png","_l":"upload_608688a6-53bc-4d1f-adfc-3aac153066fb_l.png"},"contentSize":45228,"contentType":"image/jpeg"},"45247":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_1de378be-f389-41d7-a6bf-d680b2eee551_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_1de378be-f389-41d7-a6bf-d680b2eee551_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_1de378be-f389-41d7-a6bf-d680b2eee551_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_1de378be-f389-41d7-a6bf-d680b2eee551_l.png"],"fileNames":{"clean":"upload_1de378be-f389-41d7-a6bf-d680b2eee551.png","_o":"upload_1de378be-f389-41d7-a6bf-d680b2eee551_o.png","_s":"upload_1de378be-f389-41d7-a6bf-d680b2eee551_s.png","_m":"upload_1de378be-f389-41d7-a6bf-d680b2eee551_m.png","_l":"upload_1de378be-f389-41d7-a6bf-d680b2eee551_l.png"},"contentSize":45247,"contentType":"image/jpeg"},"45263":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_984f220d-1409-418d-998b-44667879fde2_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_984f220d-1409-418d-998b-44667879fde2_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_984f220d-1409-418d-998b-44667879fde2_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_984f220d-1409-418d-998b-44667879fde2_l.png"],"fileNames":{"clean":"upload_984f220d-1409-418d-998b-44667879fde2.png","_o":"upload_984f220d-1409-418d-998b-44667879fde2_o.png","_s":"upload_984f220d-1409-418d-998b-44667879fde2_s.png","_m":"upload_984f220d-1409-418d-998b-44667879fde2_m.png","_l":"upload_984f220d-1409-418d-998b-44667879fde2_l.png"},"contentSize":45263,"contentType":"image/jpeg"},"45273":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_l.png"],"fileNames":{"clean":"upload_86280e72-0c1e-40cf-a6c3-79291a7ec384.png","_o":"upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_o.png","_s":"upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_s.png","_m":"upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_m.png","_l":"upload_86280e72-0c1e-40cf-a6c3-79291a7ec384_l.png"},"contentSize":45273,"contentType":"image/jpeg"},"45492":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a3070781-0b4d-43f7-80b8-5be8138d0930_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a3070781-0b4d-43f7-80b8-5be8138d0930_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a3070781-0b4d-43f7-80b8-5be8138d0930_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_a3070781-0b4d-43f7-80b8-5be8138d0930_l.png"],"fileNames":{"clean":"upload_a3070781-0b4d-43f7-80b8-5be8138d0930.png","_o":"upload_a3070781-0b4d-43f7-80b8-5be8138d0930_o.png","_s":"upload_a3070781-0b4d-43f7-80b8-5be8138d0930_s.png","_m":"upload_a3070781-0b4d-43f7-80b8-5be8138d0930_m.png","_l":"upload_a3070781-0b4d-43f7-80b8-5be8138d0930_l.png"},"contentSize":45492,"contentType":"image/jpeg"},"45510":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_892f666b-ac98-4feb-9017-39448434b732_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_892f666b-ac98-4feb-9017-39448434b732_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_892f666b-ac98-4feb-9017-39448434b732_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_892f666b-ac98-4feb-9017-39448434b732_l.png"],"fileNames":{"clean":"upload_892f666b-ac98-4feb-9017-39448434b732.png","_o":"upload_892f666b-ac98-4feb-9017-39448434b732_o.png","_s":"upload_892f666b-ac98-4feb-9017-39448434b732_s.png","_m":"upload_892f666b-ac98-4feb-9017-39448434b732_m.png","_l":"upload_892f666b-ac98-4feb-9017-39448434b732_l.png"},"contentSize":45510,"contentType":"image/jpeg"},"45585":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_l.png"],"fileNames":{"clean":"upload_f684c94c-7d37-47ed-91cb-bb458f9cff15.png","_o":"upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_o.png","_s":"upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_s.png","_m":"upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_m.png","_l":"upload_f684c94c-7d37-47ed-91cb-bb458f9cff15_l.png"},"contentSize":45585,"contentType":"image/jpeg"},"74829":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_l.png"],"fileNames":{"clean":"upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5.png","_o":"upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_o.png","_s":"upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_s.png","_m":"upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_m.png","_l":"upload_cc43cdb6-b877-4b30-a702-b7f591a8bca5_l.png"},"contentSize":74829,"contentType":"image/jpeg"}}
\ No newline at end of file
+{"23394":{"mediaPaths":["C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_33298c77-531a-4559-822c-46a4c43ec062_o.png","C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_33298c77-531a-4559-822c-46a4c43ec062_s.png","C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_33298c77-531a-4559-822c-46a4c43ec062_m.png","C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_33298c77-531a-4559-822c-46a4c43ec062_l.png"],"fileNames":{"clean":"upload_33298c77-531a-4559-822c-46a4c43ec062.png","_o":"upload_33298c77-531a-4559-822c-46a4c43ec062_o.png","_s":"upload_33298c77-531a-4559-822c-46a4c43ec062_s.png","_m":"upload_33298c77-531a-4559-822c-46a4c43ec062_m.png","_l":"upload_33298c77-531a-4559-822c-46a4c43ec062_l.png"},"contentSize":23394,"contentType":"image/jpeg"},"23406":{"mediaPaths":["C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_o.png","C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_s.png","C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_m.png","C:\\gitstuff\\GitCode\\Dash-Web\\src\\server\\public\\files\\upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_l.png"],"fileNames":{"clean":"upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb.png","_o":"upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_o.png","_s":"upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_s.png","_m":"upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_m.png","_l":"upload_bd462cce-cf8d-4aa8-bd39-c6ad69bf1fbb_l.png"},"contentSize":23406,"contentType":"image/jpeg"}}
\ No newline at end of file
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 5998ccfd8..8900f3301 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GlyFByFfWe7VNNjHImJwA58yoh2cAKDJUPhBKn5IDVUY9oPlXbpyhdJMjfGSRhDZpgEWM0QoSqONu9gBVWDV9aXsf7p8r9TDXK7jBfWs1Qnox4zPf54kSfCHsmV6iw","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568646012183}
\ No newline at end of file
+{"access_token":"ya29.GlyFB38lolkvmzKJYKWGKXTSSAkRH3HA1SrCMEp3YH8Gy_I06P7w5MN1C9zckoYXQPb-7OuO4vSCchAmiwfds19hhyXDFZhegUZ30y5WhBChJ0vS5X-086QWLgKbsg","refresh_token":"1/HTv_xFHszu2Nf3iiFrUTaeKzC_Vp2-6bpIB06xW_WHI","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/photoslibrary.sharing","token_type":"Bearer","expiry_date":1568653300717}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 3e6f24e4ac4a4b64620d8c9f614f214f8f1c7b94 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 17 Sep 2019 19:47:05 -0400
Subject: cleanup for animating document icon collapse/expand - still need to
 fix for docs without native dimensions.   lots of lint fixes.

---
 src/Utils.ts                                       | 17 ++---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/ProsemirrorExampleTransfer.ts      |  4 +-
 src/client/util/RichTextRules.ts                   |  6 +-
 src/client/util/RichTextSchema.tsx                 | 14 ++--
 src/client/util/TooltipTextMenu.tsx                |  4 +-
 src/client/views/DocumentDecorations.tsx           |  2 +-
 src/client/views/GlobalKeyHandler.ts               |  2 +-
 src/client/views/OverlayView.tsx                   |  1 +
 src/client/views/ScriptBox.tsx                     |  2 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 14 ++--
 src/client/views/nodes/ButtonBox.tsx               |  2 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 35 ++++------
 src/client/views/nodes/DocumentView.tsx            | 75 +++++++++-------------
 src/client/views/nodes/FormattedTextBox.tsx        |  6 +-
 src/client/views/nodes/ImageBox.tsx                |  8 +--
 src/client/views/nodes/PDFBox.tsx                  |  6 +-
 src/client/views/nodes/VideoBox.tsx                |  6 +-
 src/client/views/pdf/Annotation.tsx                |  2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 src/new_fields/Doc.ts                              | 12 ++--
 .../authentication/models/current_user_utils.ts    |  2 +-
 23 files changed, 99 insertions(+), 129 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 415023ac4..65eb3cffd 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -115,28 +115,23 @@ export class Utils {
         // Calculate hue
 
         // No difference
-        if (delta == 0)
-            h = 0;
+        if (delta === 0) h = 0;
         // Red is max
-        else if (cmax == r)
-            h = ((g - b) / delta) % 6;
+        else if (cmax === r) h = ((g - b) / delta) % 6;
         // Green is max
-        else if (cmax == g)
-            h = (b - r) / delta + 2;
+        else if (cmax === g) h = (b - r) / delta + 2;
         // Blue is max
-        else
-            h = (r - g) / delta + 4;
+        else h = (r - g) / delta + 4;
 
         h = Math.round(h * 60);
 
         // Make negative hues positive behind 360°
-        if (h < 0)
-            h += 360; // Calculate lightness
+        if (h < 0) h += 360; // Calculate lightness
 
         l = (cmax + cmin) / 2;
 
         // Calculate saturation
-        s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+        s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
 
         // Multiply l and s by 100
         // s = +(s * 100).toFixed(1);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 673acfbaf..206e2c4f1 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -631,8 +631,8 @@ export namespace DocUtils {
                                 LinkManager.Instance.deleteLink(link);
                                 LinkManager.Instance.addLink(link);
                             }
-                        })
-                    })
+                        });
+                    });
                 }
             }
         });
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 1d2d33800..3e3d3155c 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -97,7 +97,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         tx2.doc.descendants((node: any, offset: any, index: any) => {
             if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
                 let path = (tx2.doc.resolve(offset) as any).path;
-                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
                 if (node.type === schema.nodes.ordered_list) depth++;
                 tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
             }
@@ -145,7 +145,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
         marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
         return tx;
-    }
+    };
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index c727eec73..cd37ea0bb 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -20,7 +20,7 @@ export const inpRules = {
             /^1\.\s$/,
             schema.nodes.ordered_list,
             () => {
-                return ({ mapStyle: "decimal", bulletStyle: 1 })
+                return ({ mapStyle: "decimal", bulletStyle: 1 });
             },
             (match: any, node: any) => {
                 return node.childCount + node.attrs.order === +match[1];
@@ -33,7 +33,7 @@ export const inpRules = {
             schema.nodes.ordered_list,
             // match => {
             () => {
-                return ({ mapStyle: "alpha", bulletStyle: 1 })
+                return ({ mapStyle: "alpha", bulletStyle: 1 });
                 // return ({ order: +match[1] })
             },
             (match: any, node: any) => {
@@ -67,7 +67,7 @@ export const inpRules = {
                     (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
                     return state.tr.deleteRange(start, end);
                 }
-                return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }))
+                return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
             }),
         new InputRule(
             new RegExp(/^\^\^\s$/),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index f027a4bf7..ba4b92a25 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -349,8 +349,9 @@ export const marks: { [index: string]: MarkSpec } = {
                         let style = getComputedStyle(p);
                         if (style.textDecoration === "underline") return null;
                         if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
-                            p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1)
+                            p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
                             return null;
+                        }
                     }
                     return false;
                 }
@@ -371,10 +372,9 @@ export const marks: { [index: string]: MarkSpec } = {
                 getAttrs: (p: any) => {
                     if (typeof (p) !== "string") {
                         let style = getComputedStyle(p);
-                        if (style.textDecoration === "underline")
-                            return null;
-                        if (p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1)
+                        if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
                             return null;
+                        }
                     }
                     return false;
                 }
@@ -633,11 +633,11 @@ export class ImageResizeView {
                             DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
                         } else {
                             DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
-                        } e.ctrlKey
+                        }
                     }
                 });
             }
-        }
+        };
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
@@ -776,7 +776,7 @@ export class FootnoteView {
         this.innerView.updateState(state);
 
         if (!tr.getMeta("fromOutside")) {
-            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
+            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
             for (let i = 0; i < transactions.length; i++) {
                 let steps = transactions[i].steps;
                 for (let j = 0; j < steps.length; j++) {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 84d045e6f..5764af282 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -523,12 +523,12 @@ export class TooltipTextMenu {
         tx2.doc.descendants((node: any, offset: any, index: any) => {
             if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
                 let path = (tx2.doc.resolve(offset) as any).path;
-                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
                 if (node.type === schema.nodes.ordered_list) depth++;
                 tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks);
             }
         });
-    };
+    }
     //remove all node typeand apply the passed-in one to the selected text
     changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
         //remove oldif (nodeType) { //add new
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 7829bd7f1..9a2105467 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -439,7 +439,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             let heading = NumCast(dv.props.Document.heading);
             ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
             usingRule = usingRule || (ruleProvider && heading ? true : false);
-        })
+        });
         !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
             map(d => d.borderRounding = `${Math.min(100, dist)}%`);
         e.stopPropagation();
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index ba125d6e5..59229418d 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -166,7 +166,7 @@ export default class KeyManager {
                 break;
             case "o":
                 let target = SelectionManager.SelectedDocuments()[0];
-                target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target)
+                target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target);
                 break;
             case "r":
                 preventDefault = false;
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index da4b71e5c..15faea3cd 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -172,6 +172,7 @@ export class OverlayView extends React.Component {
                     ChromeHeight={returnZero}
                     isSelected={returnFalse}
                     select={emptyFunction}
+                    ruleProvider={undefined}
                     layoutKey={"layout"}
                     bringToFront={emptyFunction}
                     addDocument={undefined}
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 8f06cf770..8ef9f3be6 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -98,7 +98,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
         // tslint:disable-next-line: no-unnecessary-callback-wrapper
         let params: string[] = [];
         let setParams = (p: string[]) => params.splice(0, params.length, ...p);
-        let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+        let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
             if (prewrapper) {
                 text = prewrapper + text + (postwrapper ? postwrapper : "");
             }
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 548f663ec..5f4742834 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -99,7 +99,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
             subItems.push({
                 description: "Stacking (AutoHeight)", event: () => {
                     this.props.Document.viewType = CollectionViewType.Stacking;
-                    this.props.Document.autoHeight = true
+                    this.props.Document.autoHeight = true;
                 }, icon: "ellipsis-v"
             });
             subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 03ac012b4..8d392d764 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -24,9 +24,9 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { DocumentViewProps, documentSchema } from "../../nodes/DocumentView";
 import { pageSchema } from "../../nodes/ImageBox";
 import { OverlayElementOptions, OverlayView } from "../../OverlayView";
 import PDFMenu from "../../pdf/PDFMenu";
@@ -176,8 +176,8 @@ export namespace PivotView {
 
 }
 
-type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
-const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
 
 @observer
 export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@@ -341,7 +341,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         this.bringToFront(d);
                     });
 
-                    de.data.droppedDocuments.length == 1 && this.updateCluster(de.data.droppedDocuments[0]);
+                    de.data.droppedDocuments.length === 1 && this.updateCluster(de.data.droppedDocuments[0]);
                 }
             }
             else if (de.data instanceof DragManager.AnnotationDragData) {
@@ -473,7 +473,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 // choose a cluster color from a palette
                 let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
                 clusterColor = colors[cluster % colors.length];
-                let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor != s.defaultBackgroundColor)) : undefined;
+                let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)) : undefined;
                 // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
                 set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
                 set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
@@ -848,7 +848,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor;
                 }
             })
-        )
+        );
     }
 
     analyzeStrokes = async () => {
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 68d3b8ae1..eebf6c167 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -49,7 +49,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
         funcs.push({
             description: "Clear Script Params", event: () => {
                 let params = Cast(this.props.Document.buttonParams, listSpec("string"));
-                params && params.map(p => this.props.Document[p] = undefined)
+                params && params.map(p => this.props.Document[p] = undefined);
             }, icon: "trash"
         });
 
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 19d4a6784..bade3f8c1 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,10 +1,10 @@
-import { computed, action, observable, reaction, IReactionDisposer } from "mobx";
+import { computed, action, observable, reaction, IReactionDisposer, trace } from "mobx";
 import { observer } from "mobx-react";
 import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
 import { FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types";
 import { Transform } from "../../util/Transform";
 import { DocComponent } from "../DocComponent";
-import { percent2frac } from "../../../Utils"
+import { percent2frac } from "../../../Utils";
 import { DocumentView, DocumentViewProps, documentSchema } from "./DocumentView";
 import "./CollectionFreeFormDocumentView.scss";
 import React = require("react");
@@ -18,7 +18,7 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     height?: number;
     jitterRotation: number;
 }
-const positionSchema = createSchema({
+export const positionSchema = createSchema({
     zIndex: "number",
     x: "number",
     y: "number",
@@ -32,8 +32,8 @@ export const PositionDocument = makeInterface(documentSchema, positionSchema);
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
     _disposer: IReactionDisposer | undefined = undefined;
     @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
-    @computed get X() { return this._animx !== undefined ? this._animx : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
-    @computed get Y() { return this._animy !== undefined ? this._animy : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
+    @computed get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
+    @computed get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
     @computed get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.Document[WidthSym](); }
     @computed get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.Document[HeightSym](); }
     @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
@@ -59,22 +59,10 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         this._disposer && this._disposer();
     }
     componentDidMount() {
-        this._disposer = reaction(() => this.props.Document.iconTarget,
+        this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating],
             () => {
-                const icon = this.props.Document.iconTarget ? Array.from(Cast(this.props.Document.iconTarget, listSpec("number"))!) : undefined;
-                if (icon) {
-                    let target = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]);
-                    if (icon[2] === 1) {
-                        this._animx = target[0];
-                        this._animy = target[1];
-                    }
-                    setTimeout(action(() => {
-                        this._animx = icon[2] === 1 ? this.Document.x : target[0];
-                        this._animy = icon[2] === 1 ? this.Document.y : target[1];
-                    }), 25);
-                } else {
-                    this._animx = this._animy = undefined;
-                }
+                const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined;
+                this._animPos = !target ? undefined : target[2] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
             }, { fireImmediately: true });
     }
 
@@ -83,7 +71,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     panelHeight = () => this.props.PanelHeight();
     getTransform = (): Transform => this.props.ScreenToLocalTransform()
         .translate(-this.X, -this.Y)
-        .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth);
+        .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth)
 
     borderRounding = () => {
         let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
@@ -107,8 +95,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
     }
 
-    @observable _animx: number | undefined = undefined;
-    @observable _animy: number | undefined = undefined;
+    @observable _animPos: number[] | undefined = undefined;
 
     render() {
         const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
@@ -123,7 +110,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                                         StrCast(this.layoutDoc.boxShadow, ""),
                     borderRadius: this.borderRounding(),
                     transform: this.transform,
-                    transition: this.props.Document.isIconAnimating ? "transform .5s" : hasPosition ? "transform 1s" : StrCast(this.layoutDoc.transition),
+                    transition: this.Document.isAnimating !== undefined ? "transform .5s ease-in" : hasPosition ? "transform 1s" : StrCast(this.layoutDoc.transition),
                     width: this.width,
                     height: this.height,
                     zIndex: this.Document.zIndex || 0,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 37c38cc04..669c58ac7 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,9 +1,9 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, runInAction } from "mobx";
+import { action, computed, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
-import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
 import { Copy, Id } from '../../../new_fields/FieldSymbols';
 import { List } from "../../../new_fields/List";
 import { ObjectField } from "../../../new_fields/ObjectField";
@@ -209,43 +209,31 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @action
     public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
         SelectionManager.DeselectAll();
-        if (expandedDocs) {
-            let isMinimized: boolean | undefined;
-            expandedDocs.map(maximizedDoc => {
-                if (isMinimized === undefined) {
-                    isMinimized = BoolCast(maximizedDoc.isMinimized);
+        expandedDocs && expandedDocs.map(expDoc => {
+            if (expDoc.isMinimized || expDoc.isAnimating === "min") { // MAXIMIZE DOC
+                if (expDoc.isMinimized) {  // docs are never actaully at the minimized location.  so when we unminimize one, we have to set our overrides to make it look like it was at the minimize location
+                    expDoc.isMinimized = false;
+                    expDoc.animateToPos = new List<number>([...scrpt, 0]);
+                    expDoc.animateToDimensions = new List<number>([0, 0]);
                 }
-                let w = NumCast(maximizedDoc.width);
-                let h = NumCast(maximizedDoc.height);
-                let iconAnimating = maximizedDoc.isIconAnimating ? Array.from(Cast(maximizedDoc.isIconAnimating, listSpec("number"))!) : undefined;
-                if (isMinimized || (iconAnimating && iconAnimating.length && iconAnimating[0] === 0)) {
-                    // MAXIMIZE DOC
-                    if (maximizedDoc.isMinimized) {
-                        maximizedDoc.isIconAnimating = new List<number>([0, 0]);
-                        maximizedDoc.isMinimized = false;
+                setTimeout(() => {
+                    expDoc.isAnimating = "max";
+                    expDoc.animateToPos = new List<number>([0, 0, 1]);
+                    expDoc.animateToDimensions = new List<number>([NumCast(expDoc.width), NumCast(expDoc.height)]);
+                    setTimeout(() => expDoc.isAnimating === "max" && (expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined), 600);
+                }, 0);
+            } else {  // MINIMIZE DOC
+                expDoc.isAnimating = "min";
+                expDoc.animateToPos = new List<number>([...scrpt, 0]);
+                expDoc.animateToDimensions = new List<number>([0, 0]);
+                setTimeout(() => {
+                    if (expDoc.isAnimating === "min") {
+                        expDoc.isMinimized = true;
+                        expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined;
                     }
-                    maximizedDoc.iconTarget = new List<number>([...scrpt, 1]);
-                    setTimeout(() => {
-                        maximizedDoc.isIconAnimating = new List<number>([w, h]);
-                        setTimeout(() => {
-                            if (maximizedDoc.isIconAnimating && Array.from(Cast(maximizedDoc.isIconAnimating, listSpec("number"))!)[0] !== 0) {
-                                maximizedDoc.isIconAnimating = undefined;
-                            }
-                        }, 750);
-                    }, 0);
-                } else {
-                    maximizedDoc.iconTarget = new List<number>([...scrpt, 0]);
-                    // MINIMIZE DOC
-                    maximizedDoc.isIconAnimating = new List<number>([0, 0]);
-                    setTimeout(() => {
-                        if (maximizedDoc.isIconAnimating && Array.from(Cast(maximizedDoc.isIconAnimating, listSpec("number"))!)[0] === 0) {
-                            maximizedDoc.isMinimized = true;
-                            maximizedDoc.isIconAnimating = undefined;
-                        }
-                    }, 750);
-                }
-            });
-        }
+                }, 600);
+            }
+        });
     }
 
     onClick = async (e: React.MouseEvent) => {
@@ -530,14 +518,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 let portal = existingPortal instanceof Doc ? existingPortal : Docs.Create.FreeformDocument([], { width: (this.Document.width || 0) + 10, height: this.Document.height || 0, title: portalID });
                 DocUtils.MakeLink(this.props.Document, portal, undefined, portalID);
                 Doc.GetProto(this.props.Document).isButton = true;
-            })
+            });
         }
     }
     @undoBatch
     @action
     toggleCustomView = (): void => {
         if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) {
-            Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc)
+            Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc);
         } else {
             if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
                 this.makeCustomViewClicked();
@@ -623,7 +611,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
         layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
         if (this.props.DataDoc) {
-            layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" })
+            layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
         }
         layoutItems.push({ description: `${this.layoutDoc.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.layoutDoc.chromeStatus = (this.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
         layoutItems.push({ description: `${this.layoutDoc.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
@@ -786,7 +774,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
         const borderRounding = this.Document.borderRounding || ruleRounding;
         const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
-        const iconAnimating = this.Document.isIconAnimating ? Array.from(Cast(this.Document.isIconAnimating, listSpec("number"))!) : undefined;
         const searchHighlight = (!this.Document.searchFields ? (null) :
             <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
                 {this.Document.searchFields}
@@ -817,7 +804,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
                 style={{
-                    transition: iconAnimating ? "transform .5s" : StrCast(this.layoutDoc.transition),
+                    transition: this.props.Document.isAnimating !== undefined ? "transform .5s linear" : StrCast(this.layoutDoc.transition),
                     pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all",
                     color: StrCast(this.layoutDoc.color),
                     outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
@@ -881,7 +868,7 @@ let makeNativeView = (doc: any): void => {
     doc.nonCustomNativeWidth = undefined;
     doc.nonCustomNativeHeight = undefined;
     doc.nonCustomIgnoreAspect = undefined;
-}
+};
 let makeCustomView = (doc: any): void => {
     doc.nativeLayout = doc.layout;
     doc.nativeType = doc.type;
@@ -911,7 +898,7 @@ let makeCustomView = (doc: any): void => {
         doc.customNativeHeight = undefined;
         doc.customIgnoreAspect = undefined;
     }
-}
+};
 Scripting.addGlobal(function toggleDetail(doc: any) {
     if (doc.type !== DocumentType.COL && doc.type !== DocumentType.TEMPLATE) {
         makeCustomView(doc);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 77e29632e..1c946aa78 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -182,9 +182,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
                     if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
                     else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true);
-                })
+                });
             });
-        })
+        });
         this.linkOnDeselect.clear();
     }
 
@@ -195,7 +195,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 let range = tx.selection.$from.blockRange(tx.selection.$to);
                 let text = range ? tx.doc.textBetween(range.start, range.end) : "";
                 let textEndSelection = tx.selection.to;
-                for (; textEndSelection < range!.end && text[textEndSelection - range!.start] != " "; textEndSelection++) { }
+                for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { }
                 text = text.substr(0, textEndSelection - range!.start);
                 text = text.split(" ")[text.split(" ").length - 1];
                 let split = text.split("::");
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index beccce9dd..42307ee02 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -23,7 +23,7 @@ import { ContextMenu } from "../../views/ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from '../DocComponent';
 import { InkingControl } from '../InkingControl';
-import { positionSchema } from './DocumentView';
+import { documentSchema } from './DocumentView';
 import FaceRectangles from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ImageBox.scss";
@@ -50,8 +50,8 @@ declare class MediaRecorder {
     constructor(e: any);
 }
 
-type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
-const ImageDocument = makeInterface(pageSchema, positionSchema);
+type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
+const ImageDocument = makeInterface(pageSchema, documentSchema);
 
 @observer
 export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
@@ -220,7 +220,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
             let modes: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
             modes.push({ description: "Generate Tags", event: this.generateMetadata, icon: "tag" });
             modes.push({ description: "Find Faces", event: this.extractFaces, icon: "camera" });
-            !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" })
+            !existingAnalyze && ContextMenu.Instance.addItem({ description: "Analyzers...", subitems: modes, icon: "hand-point-right" });
 
             ContextMenu.Instance.addItem({ description: "Image Funcs...", subitems: funcs, icon: "asterisk" });
         }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index df35b603c..31e8f122b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -14,14 +14,14 @@ import { CompileScript } from '../../util/Scripting';
 import { DocComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
 import { PDFViewer } from "../pdf/PDFViewer";
-import { positionSchema } from "./DocumentView";
+import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
 
-type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
-const PdfDocument = makeInterface(positionSchema, pageSchema);
+type PdfDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(documentSchema, pageSchema);
 export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === KeyCodes.BACKSPACE) e.stopPropagation(); };
 
 @observer
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 96f011eff..a696f255d 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -14,7 +14,7 @@ import { ContextMenuProps } from "../ContextMenuItem";
 import { DocComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
-import { positionSchema } from "./DocumentView";
+import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./VideoBox.scss";
@@ -25,8 +25,8 @@ import { Doc } from "../../../new_fields/Doc";
 import { ScriptField } from "../../../new_fields/ScriptField";
 var path = require('path');
 
-type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
-const VideoDocument = makeInterface(positionSchema, pageSchema);
+type VideoDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
+const VideoDocument = makeInterface(documentSchema, pageSchema);
 
 library.add(faVideo);
 
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index eeb2531a2..34e3b0931 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -58,7 +58,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
                     runInAction(() => this._brushed = brushed);
                 }
             }
-        )
+        );
     }
 
     componentWillUnmount() {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 7bc1d3507..c508935f2 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -81,7 +81,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return annotations.filter(anno => {
             let run = this._script.run({ this: anno });
             return run.success ? run.result : true;
-        })
+        });
     }
 
     @computed get nonDocAnnotations() {
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 614babd3c..0cf1208d7 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -144,12 +144,12 @@ export class Doc extends RefField {
     private [Self] = this;
     private [SelfProxy]: any;
     public [WidthSym] = () => {
-        let iconAnimating = this[SelfProxy].isIconAnimating ? Array.from(Cast(this[SelfProxy].isIconAnimating, listSpec("number"))!) : undefined;
-        return iconAnimating ? iconAnimating[0] : NumCast(this[SelfProxy].width);
+        let animDims = this[SelfProxy].animateToDimensions ? Array.from(Cast(this[SelfProxy].animateToDimensions, listSpec("number"))!) : undefined;
+        return animDims ? animDims[0] : NumCast(this[SelfProxy].width);
     }
     public [HeightSym] = () => {
-        let iconAnimating = this[SelfProxy].isIconAnimating ? Array.from(Cast(this[SelfProxy].isIconAnimating, listSpec("number"))!) : undefined;
-        return iconAnimating ? iconAnimating[1] : NumCast(this[SelfProxy].height);
+        let animDims = this[SelfProxy].animateToDimensions ? Array.from(Cast(this[SelfProxy].animateToDimensions, listSpec("number"))!) : undefined;
+        return animDims ? animDims[1] : NumCast(this[SelfProxy].height);
     }
 
     [ToScriptString]() {
@@ -334,7 +334,7 @@ export namespace Doc {
     }
 
     export function IndexOf(toFind: Doc, list: Doc[]) {
-        return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind))
+        return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
     }
     export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
         if (target[key] === undefined) {
@@ -419,7 +419,7 @@ export namespace Doc {
     export function MakeAlias(doc: Doc) {
         let alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
         if (alias.layout instanceof Doc) {
-            alias.layout = Doc.MakeAlias(alias.layout as Doc);
+            alias.layout = Doc.MakeAlias(alias.layout);
         }
         let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
         let script = `return renameAlias(self, ${aliasNumber})`;
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index af5774ebe..8cac8550c 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -59,7 +59,7 @@ export class CurrentUserUtils {
             noteTypes.excludeFromLibrary = true;
             doc.noteTypes = noteTypes;
         }
-        PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(vals => DocListCast(vals)));
+        PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
         if (doc.recentlyClosed === undefined) {
             const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
             recentlyClosed.excludeFromLibrary = true;
-- 
cgit v1.2.3-70-g09d2


From f82458be8bc8beaab387cc2813b7b18c9b3caac2 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 22 Sep 2019 17:16:18 -0400
Subject: initial commit post master merge

---
 src/Utils.ts                                       |  21 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   7 +-
 src/client/documents/Documents.ts                  |  51 +-
 src/client/util/DictationManager.ts                |   2 +-
 src/client/util/DocumentManager.ts                 |  44 +-
 src/client/util/DragManager.ts                     |  44 +-
 src/client/util/History.ts                         |   4 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   1 +
 src/client/util/ProsemirrorExampleTransfer.ts      |   4 +-
 src/client/util/RichTextRules.ts                   |  24 +-
 src/client/util/RichTextSchema.tsx                 |  22 +-
 src/client/util/Scripting.ts                       |   8 +-
 src/client/util/SelectionManager.ts                |   4 +
 src/client/util/SharingManager.tsx                 |   2 +-
 src/client/util/TooltipTextMenu.tsx                |  20 +-
 src/client/util/UndoManager.ts                     |   2 +-
 src/client/views/ContextMenu.tsx                   |   2 +-
 src/client/views/DocumentButtonBar.scss            | 129 ++++
 src/client/views/DocumentButtonBar.tsx             | 368 ++++++++++
 src/client/views/DocumentDecorations.scss          |   2 +-
 src/client/views/DocumentDecorations.tsx           | 425 ++---------
 src/client/views/GlobalKeyHandler.ts               |  11 +-
 src/client/views/InkingCanvas.scss                 |   1 +
 src/client/views/InkingControl.tsx                 |  34 +-
 src/client/views/MainOverlayTextBox.tsx            |  23 +-
 src/client/views/MainView.tsx                      |  80 +-
 src/client/views/OverlayView.tsx                   |   4 +-
 src/client/views/ScriptBox.tsx                     |   2 +-
 src/client/views/ScriptingRepl.tsx                 |  20 +-
 src/client/views/TemplateMenu.tsx                  |  57 +-
 .../views/collections/CollectionBaseView.tsx       |  24 +-
 .../views/collections/CollectionDockingView.tsx    | 130 ++--
 .../views/collections/CollectionSchemaCells.tsx    |  17 +-
 .../CollectionSchemaMovableTableHOC.tsx            |   8 +-
 .../views/collections/CollectionSchemaView.tsx     |  22 +-
 .../views/collections/CollectionStackingView.tsx   |   9 +-
 .../CollectionStackingViewFieldColumn.tsx          |  13 +-
 src/client/views/collections/CollectionSubView.tsx |  67 +-
 .../views/collections/CollectionTreeView.tsx       |  36 +-
 src/client/views/collections/CollectionView.tsx    |   6 +
 .../views/collections/CollectionViewChromes.tsx    |  17 +-
 .../views/collections/ParentDocumentSelector.scss  |   9 +
 .../views/collections/ParentDocumentSelector.tsx   |  46 +-
 .../CollectionFreeFormLinkView.scss                |   3 +-
 .../CollectionFreeFormLinkView.tsx                 |   1 -
 .../CollectionFreeFormLinksView.tsx                |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 474 +++++-------
 .../collections/collectionFreeForm/MarqueeView.tsx |  78 +-
 .../document_templates/image_card/ImageCard.tsx    |   3 -
 src/client/views/linking/LinkFollowBox.tsx         |  48 +-
 src/client/views/linking/LinkMenu.tsx              |   2 +-
 src/client/views/linking/LinkMenuGroup.tsx         |  26 +-
 src/client/views/linking/LinkMenuItem.tsx          |   2 +-
 src/client/views/nodes/ButtonBox.tsx               |   6 +-
 .../nodes/CollectionFreeFormDocumentView.scss      |   5 +
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  95 ++-
 src/client/views/nodes/DocumentContentsView.tsx    |   9 +-
 src/client/views/nodes/DocumentView.scss           |  42 ++
 src/client/views/nodes/DocumentView.tsx            | 810 ++++++++-------------
 src/client/views/nodes/DragBox.tsx                 |  10 +-
 src/client/views/nodes/FieldView.tsx               |   4 +-
 src/client/views/nodes/FormattedTextBox.scss       |   1 -
 src/client/views/nodes/FormattedTextBox.tsx        | 165 +++--
 src/client/views/nodes/IconBox.tsx                 |  52 +-
 src/client/views/nodes/ImageBox.tsx                |  55 +-
 src/client/views/nodes/KeyValueBox.tsx             |   6 +-
 src/client/views/nodes/KeyValuePair.tsx            |   9 +-
 src/client/views/nodes/PDFBox.tsx                  | 106 ++-
 src/client/views/nodes/VideoBox.tsx                |  31 +-
 src/client/views/pdf/Annotation.tsx                |  10 +-
 src/client/views/pdf/PDFAnnotationLayer.scss       |   6 -
 src/client/views/pdf/PDFAnnotationLayer.tsx        |  21 -
 src/client/views/pdf/PDFViewer.tsx                 |  89 +--
 src/client/views/pdf/Page.scss                     |   5 +
 src/client/views/pdf/Page.tsx                      |   2 +-
 .../views/presentationview/PresentationElement.tsx |   2 +
 .../views/presentationview/PresentationList.tsx    |   4 +-
 src/client/views/search/FilterBox.tsx              |   6 +-
 src/client/views/search/SearchItem.tsx             |   8 +-
 src/debug/Repl.tsx                                 |   8 +-
 src/new_fields/Doc.ts                              | 176 +++--
 src/new_fields/RichTextUtils.ts                    |   4 +-
 src/new_fields/ScriptField.ts                      |  36 +-
 src/server/authentication/config/passport.ts       |   2 +-
 .../authentication/models/current_user_utils.ts    |  40 +-
 85 files changed, 2212 insertions(+), 2078 deletions(-)
 create mode 100644 src/client/views/DocumentButtonBar.scss
 create mode 100644 src/client/views/DocumentButtonBar.tsx
 create mode 100644 src/client/views/nodes/CollectionFreeFormDocumentView.scss
 delete mode 100644 src/client/views/pdf/PDFAnnotationLayer.scss
 delete mode 100644 src/client/views/pdf/PDFAnnotationLayer.tsx

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index a842f5a20..6489eff77 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -123,28 +123,23 @@ export class Utils {
         // Calculate hue
 
         // No difference
-        if (delta == 0)
-            h = 0;
+        if (delta === 0) h = 0;
         // Red is max
-        else if (cmax == r)
-            h = ((g - b) / delta) % 6;
+        else if (cmax === r) h = ((g - b) / delta) % 6;
         // Green is max
-        else if (cmax == g)
-            h = (b - r) / delta + 2;
+        else if (cmax === g) h = (b - r) / delta + 2;
         // Blue is max
-        else
-            h = (r - g) / delta + 4;
+        else h = (r - g) / delta + 4;
 
         h = Math.round(h * 60);
 
         // Make negative hues positive behind 360°
-        if (h < 0)
-            h += 360; // Calculate lightness
+        if (h < 0) h += 360; // Calculate lightness
 
         l = (cmax + cmin) / 2;
 
         // Calculate saturation
-        s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
+        s = delta === 0 ? 0 : delta / (1 - Math.abs(2 * l - 1));
 
         // Multiply l and s by 100
         // s = +(s * 100).toFixed(1);
@@ -248,6 +243,10 @@ export function timenow() {
     return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
 }
 
+export function percent2frac(percent: string) {
+    return Number(percent.substr(0, percent.length - 1)) / 100;
+}
+
 export function numberRange(num: number) { return Array.from(Array(num)).map((v, i) => i); }
 
 export function returnTrue() { return true; }
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 671d05421..559b8fd6a 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -15,6 +15,7 @@ import { AssertionError } from "assert";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
 import { DocumentView } from "../../views/nodes/DocumentView";
+import { DocumentManager } from "../../util/DocumentManager";
 
 export namespace GooglePhotos {
 
@@ -140,6 +141,8 @@ export namespace GooglePhotos {
     export namespace Query {
 
         const delimiter = ", ";
+        const comparator = (a: string, b: string) => (a < b) ? -1 : (a > b ? 1 : 0);
+
         export const TagChildImages = async (collection: Doc) => {
             const idMapping = await Cast(collection.googlePhotosIdMapping, Doc);
             if (!idMapping) {
@@ -172,7 +175,7 @@ export namespace GooglePhotos {
                 const tags = concatenated.split(delimiter);
                 if (tags.length > 1) {
                     const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, "");
-                    image.googlePhotosTags = cleaned.split(delimiter).sort((a, b) => (a < b) ? -1 : (a > b ? 1 : 0)).join(delimiter);
+                    image.googlePhotosTags = cleaned.split(delimiter).sort(comparator).join(delimiter);
                 } else {
                     image.googlePhotosTags = ContentCategories.NONE;
                 }
@@ -326,7 +329,7 @@ export namespace GooglePhotos {
                 const url = data.url.href;
                 const target = Doc.MakeAlias(source);
                 const description = parseDescription(target, descriptionKey);
-                DocumentView.makeCustomViewClicked(target);
+                DocumentView.makeCustomViewClicked(target, undefined);
                 media.push({ url, description });
             });
             if (media.length) {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cfed2bf14..a7a006f47 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -20,8 +20,8 @@ import { AttributeTransformationModel } from "../northstar/core/attribute/Attrib
 import { AggregateFunction } from "../northstar/model/idea/idea";
 import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
 import { IconBox } from "../views/nodes/IconBox";
-import { Field, Doc, Opt } from "../../new_fields/Doc";
-import { OmitKeys, JSONUtils, Utils } from "../../Utils";
+import { OmitKeys, JSONUtils } from "../../Utils";
+import { Field, Doc, Opt, DocListCastAsync } from "../../new_fields/Doc";
 import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
 import { HtmlField } from "../../new_fields/HtmlField";
 import { List } from "../../new_fields/List";
@@ -65,7 +65,7 @@ export interface DocumentOptions {
     panY?: number;
     page?: number;
     scale?: number;
-    layout?: string;
+    layout?: string | Doc;
     isTemplate?: boolean;
     templates?: List<string>;
     viewType?: number;
@@ -121,7 +121,7 @@ export namespace Docs {
             }],
             [DocumentType.IMG, {
                 layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
-                options: { nativeWidth: 600, curPage: 0 }
+                options: { curPage: 0 }
             }],
             [DocumentType.WEB, {
                 layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
@@ -137,7 +137,7 @@ export namespace Docs {
             }],
             [DocumentType.VID, {
                 layout: { view: VideoBox, collectionView: [CollectionVideoView, data, anno] as CollectionViewType },
-                options: { nativeWidth: 600, curPage: 0 },
+                options: { curPage: 0 },
             }],
             [DocumentType.AUDIO, {
                 layout: { view: AudioBox },
@@ -614,10 +614,40 @@ export namespace Docs {
 
 export namespace DocUtils {
 
-    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string) {
+    export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
+        targetID = targetID.replace(/^-/, "").replace(/\([0-9]*\)$/, "");
+        DocServer.GetRefField(targetID).then(doc => {
+            if (promoteDoc !== doc) {
+                let copy = doc as Doc;
+                if (copy) {
+                    Doc.Overwrite(promoteDoc, copy, true);
+                } else {
+                    copy = Doc.MakeCopy(promoteDoc, true, targetID);
+                }
+                !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID);
+                addDoc && addDoc(copy);
+                remDoc && remDoc(promoteDoc);
+                if (!doc) {
+                    DocListCastAsync(promoteDoc.links).then(links => {
+                        links && links.map(async link => {
+                            if (link) {
+                                let a1 = await Cast(link.anchor1, Doc);
+                                if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy;
+                                let a2 = await Cast(link.anchor2, Doc);
+                                if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy;
+                                LinkManager.Instance.deleteLink(link);
+                                LinkManager.Instance.addLink(link);
+                            }
+                        });
+                    });
+                }
+            }
+        });
+    }
+    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
         if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
         let sv = DocumentManager.Instance.getDocumentView(source);
-        if (sv && sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === target) return;
+        if (sv && sv.props.ContainingCollectionDoc === target) return;
         if (target === CurrentUserUtils.UserDocument) return undefined;
 
         let linkDocProto = new Doc(id, true);
@@ -633,16 +663,15 @@ export namespace DocUtils {
             linkDocProto.anchor1 = source;
             linkDocProto.anchor1Page = source.curPage;
             linkDocProto.anchor1Groups = new List<Doc>([]);
+            linkDocProto.anchor1anchored = anchored1;
             linkDocProto.anchor2 = target;
             linkDocProto.anchor2Page = target.curPage;
             linkDocProto.anchor2Groups = new List<Doc>([]);
 
             LinkManager.Instance.addLink(linkDocProto);
 
-            let script = `return links(this);`;
-            let computed = CompileScript(script, { params: { this: "Doc" }, typecheck: false });
-            computed.compiled && (Doc.GetProto(source).links = new ComputedField(computed));
-            computed.compiled && (Doc.GetProto(target).links = new ComputedField(computed));
+            Doc.GetProto(source).links = ComputedField.MakeFunction("links(this)");
+            Doc.GetProto(target).links = ComputedField.MakeFunction("links(this)");
         }, "make link");
         return linkDocProto;
     }
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 0711effe6..cebb56bbe 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -327,7 +327,7 @@ export namespace DictationManager {
             ["open fields", {
                 action: (target: DocumentView) => {
                     let kvp = Docs.Create.KVPDocument(target.props.Document, { width: 300, height: 300 });
-                    target.props.addDocTab(kvp, target.dataDoc, "onRight");
+                    target.props.addDocTab(kvp, target.props.DataDoc, "onRight");
                 }
             }],
 
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ec731da84..a3c7429b9 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -10,6 +10,7 @@ import { DocumentView } from '../views/nodes/DocumentView';
 import { LinkManager } from './LinkManager';
 import { undoBatch, UndoManager } from './UndoManager';
 import { Scripting } from './Scripting';
+import { List } from '../../new_fields/List';
 
 
 export class DocumentManager {
@@ -146,6 +147,7 @@ export class DocumentManager {
             if (!contextDoc) {
                 let docs = docContext ? await DocListCastAsync(docContext.data) : undefined;
                 let found = false;
+                // bcz: this just searches within the context for the target -- perhaps it should recursively search through all children?
                 docs && docs.map(d => found = found || Doc.AreProtosEqual(d, docDelegate));
                 if (docContext && found) {
                     let targetContextView: DocumentView | null;
@@ -154,16 +156,19 @@ export class DocumentManager {
                         docContext.panTransformType = "Ease";
                         targetContextView.props.focus(docDelegate, willZoom);
                     } else {
-                        (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, undefined);
+                        (dockFunc || CollectionDockingView.AddRightSplit)(docContext, undefined);
                         setTimeout(() => {
-                            this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
-                        }, 10);
+                            let dv = DocumentManager.Instance.getDocumentView(docContext);
+                            dv && this.jumpToDocument(docDelegate, willZoom, forceDockFunc,
+                                doc => dv!.props.focus(dv!.props.Document, true, 1, () => dv!.props.addDocTab(doc, undefined, "inPlace")),
+                                linkPage);
+                        }, 1050);
                     }
                 } else {
                     const actualDoc = Doc.MakeAlias(docDelegate);
                     Doc.BrushDoc(actualDoc);
                     if (linkPage !== undefined) actualDoc.curPage = linkPage;
-                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, undefined);
+                    (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc, undefined);
                 }
             } else {
                 let contextView: DocumentView | null;
@@ -172,7 +177,7 @@ export class DocumentManager {
                     contextDoc.panTransformType = "Ease";
                     contextView.props.focus(docDelegate, willZoom);
                 } else {
-                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, undefined);
+                    (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined);
                     setTimeout(() => {
                         this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
                     }, 10);
@@ -203,5 +208,34 @@ export class DocumentManager {
             return 1;
         }
     }
+
+    @action
+    animateBetweenPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
+        expandedDocs && expandedDocs.map(expDoc => {
+            if (expDoc.isMinimized || expDoc.isAnimating === "min") { // MAXIMIZE DOC
+                if (expDoc.isMinimized) {  // docs are never actaully at the minimized location.  so when we unminimize one, we have to set our overrides to make it look like it was at the minimize location
+                    expDoc.isMinimized = false;
+                    expDoc.animateToPos = new List<number>([...scrpt, 0]);
+                    expDoc.animateToDimensions = new List<number>([0, 0]);
+                }
+                setTimeout(() => {
+                    expDoc.isAnimating = "max";
+                    expDoc.animateToPos = new List<number>([0, 0, 1]);
+                    expDoc.animateToDimensions = new List<number>([NumCast(expDoc.width), NumCast(expDoc.height)]);
+                    setTimeout(() => expDoc.isAnimating === "max" && (expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined), 600);
+                }, 0);
+            } else {  // MINIMIZE DOC
+                expDoc.isAnimating = "min";
+                expDoc.animateToPos = new List<number>([...scrpt, 0]);
+                expDoc.animateToDimensions = new List<number>([0, 0]);
+                setTimeout(() => {
+                    if (expDoc.isAnimating === "min") {
+                        expDoc.isMinimized = true;
+                        expDoc.isAnimating = expDoc.animateToPos = expDoc.animateToDimensions = undefined;
+                    }
+                }, 600);
+            }
+        });
+    }
 }
 Scripting.addGlobal(function focus(doc: any) { DocumentManager.Instance.getDocumentViews(Doc.GetProto(doc)).map(view => view.props.focus(doc, true)); });
\ No newline at end of file
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4c9c9c17c..56496c99b 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -32,7 +32,7 @@ export function SetupDrag(
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
         let doc = await docFunc();
-        var dragData = new DragManager.DocumentDragData([doc], [undefined]);
+        var dragData = new DragManager.DocumentDragData([doc]);
         dragData.dropAction = dropAction;
         dragData.moveDocument = moveFunc;
         dragData.options = options;
@@ -66,7 +66,7 @@ export function SetupDrag(
 
 function moveLinkedDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
     const document = SelectionManager.SelectedDocuments()[0];
-    document.props.removeDocument && document.props.removeDocument(doc);
+    document && document.props.removeDocument && document.props.removeDocument(doc);
     addDocument(doc);
     return true;
 }
@@ -76,7 +76,7 @@ export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: num
     if (draggeddoc) {
         let moddrag = await Cast(draggeddoc.annotationOn, Doc);
         let dragdocs = moddrag ? [moddrag] : [draggeddoc];
-        let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+        let dragData = new DragManager.DocumentDragData(dragdocs);
         dragData.moveDocument = moveLinkedDocument;
         DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
@@ -107,7 +107,7 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
             if (doc) moddrag.push(doc);
         }
         let dragdocs = moddrag.length ? moddrag : draggedDocs;
-        let dragData = new DragManager.DocumentDragData(dragdocs, dragdocs);
+        let dragData = new DragManager.DocumentDragData(dragdocs);
         dragData.moveDocument = moveLinkedDocument;
         DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
@@ -201,18 +201,14 @@ export namespace DragManager {
 
     export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     export class DocumentDragData {
-        constructor(dragDoc: Doc[], dragDataDocs: (Doc | undefined)[]) {
+        constructor(dragDoc: Doc[]) {
             this.draggedDocuments = dragDoc;
-            this.draggedDataDocs = dragDataDocs;
             this.droppedDocuments = dragDoc;
-            this.xOffset = 0;
-            this.yOffset = 0;
+            this.offset = [0, 0];
         }
         draggedDocuments: Doc[];
-        draggedDataDocs: (Doc | undefined)[];
         droppedDocuments: Doc[];
-        xOffset: number;
-        yOffset: number;
+        offset: number[];
         dropAction: dropActionType;
         userDropAction: dropActionType;
         moveDocument?: MoveFunction;
@@ -225,14 +221,13 @@ export namespace DragManager {
             this.dragDocument = dragDoc;
             this.dropDocument = dropDoc;
             this.annotationDocument = annotationDoc;
-            this.xOffset = this.yOffset = 0;
+            this.offset = [0, 0];
         }
         targetContext: Doc | undefined;
         dragDocument: Doc;
         annotationDocument: Doc;
         dropDocument: Doc;
-        xOffset: number;
-        yOffset: number;
+        offset: number[];
         dropAction: dropActionType;
         userDropAction: dropActionType;
     }
@@ -252,21 +247,13 @@ export namespace DragManager {
             });
     }
 
-    export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize?: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
-        let dragData = new DragManager.DocumentDragData([], [undefined]);
+    export function StartButtonDrag(eles: HTMLElement[], script: string, title: string, vars: { [name: string]: Field }, params: string[], initialize: (button: Doc) => void, downX: number, downY: number, options?: DragOptions) {
+        let dragData = new DragManager.DocumentDragData([]);
         runInAction(() => StartDragFunctions.map(func => func()));
         StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
             (dropData: { [id: string]: any }) => {
                 let bd = Docs.Create.ButtonDocument({ width: 150, height: 50, title: title });
-                let compiled = CompileScript(script, {
-                    params: { doc: Doc.name },
-                    typecheck: false,
-                    editable: true
-                });
-                if (compiled.compiled) {
-                    let scriptField = new ScriptField(compiled);
-                    bd.onClick = scriptField;
-                }
+                bd.onClick = ScriptField.MakeScript(script);
                 params.map(p => Object.keys(vars).indexOf(p) !== -1 && (Doc.GetProto(bd)[p] = new PrefetchProxy(vars[p] as Doc)));
                 initialize && initialize(bd);
                 bd.buttonParams = new List<string>(params);
@@ -283,7 +270,8 @@ export namespace DragManager {
                 let droppedDocuments: Doc[] = dragData.draggedDocuments.reduce((droppedDocs: Doc[], d) => {
                     let dvs = DocumentManager.Instance.getDocumentViews(d);
                     if (dvs.length) {
-                        let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView);
+                        let containingView = SelectionManager.SelectedDocuments()[0] ? SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView : undefined;
+                        let inContext = dvs.filter(dv => dv.props.ContainingCollectionView === containingView);
                         if (inContext.length) {
                             inContext.forEach(dv => droppedDocs.push(dv.props.Document));
                         } else {
@@ -363,8 +351,6 @@ export namespace DragManager {
 
         const docs: Doc[] =
             dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
-        const datadocs: (Doc | undefined)[] =
-            dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
         let dragElements = eles.map(ele => {
             const w = ele.offsetWidth,
                 h = ele.offsetHeight;
@@ -449,7 +435,7 @@ export namespace DragManager {
                     pageY: e.pageY,
                     preventDefault: emptyFunction,
                     button: 0
-                }, docs, datadocs);
+                }, docs);
             }
             //TODO: Why can't we use e.movementX and e.movementY?
             let moveX = e.pageX - lastX;
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index c72ae05de..899abbe40 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -54,7 +54,9 @@ export namespace HistoryUtil {
     }
 
     export function getState(): ParsedUrl {
-        return copyState(history.state);
+        let state = copyState(history.state);
+        state.initializers = state.initializers || {};
+        return state;
     }
 
     // export function addHandler(handler: (state: ParsedUrl | null) => void) {
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 8948b73f7..6670f685e 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -279,6 +279,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                             }} />
                         <label
                             htmlFor={"selector"}
+                            onClick={console.log}
                             style={{
                                 opacity: isEditing ? 0 : 1,
                                 pointerEvents: isEditing ? "none" : "all",
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 1d2d33800..3e3d3155c 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -97,7 +97,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         tx2.doc.descendants((node: any, offset: any, index: any) => {
             if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
                 let path = (tx2.doc.resolve(offset) as any).path;
-                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
                 if (node.type === schema.nodes.ordered_list) depth++;
                 tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
             }
@@ -145,7 +145,7 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
         marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
         return tx;
-    }
+    };
     bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!splitListItem(schema.nodes.list_item)(state, (tx3: Transaction) => dispatch(tx3))) {
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 00e671db9..cd37ea0bb 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -20,7 +20,7 @@ export const inpRules = {
             /^1\.\s$/,
             schema.nodes.ordered_list,
             () => {
-                return ({ mapStyle: "decimal", bulletStyle: 1 })
+                return ({ mapStyle: "decimal", bulletStyle: 1 });
             },
             (match: any, node: any) => {
                 return node.childCount + node.attrs.order === +match[1];
@@ -33,7 +33,7 @@ export const inpRules = {
             schema.nodes.ordered_list,
             // match => {
             () => {
-                return ({ mapStyle: "alpha", bulletStyle: 1 })
+                return ({ mapStyle: "alpha", bulletStyle: 1 });
                 // return ({ order: +match[1] })
             },
             (match: any, node: any) => {
@@ -61,22 +61,24 @@ export const inpRules = {
             new RegExp(/^#([0-9]+)\s$/),
             (state, match, start, end) => {
                 let size = Number(match[1]);
-                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
                 let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
-                    ruleProvider["ruleSize_" + heading] = size;
+                    (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
+                    return state.tr.deleteRange(start, end);
                 }
-                return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }))
+                return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
             }),
         new InputRule(
             new RegExp(/^\^\^\s$/),
             (state, match, start, end) => {
                 let node = (state.doc.resolve(start) as any).nodeAfter;
                 let sm = state.storedMarks || undefined;
-                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
                 let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "center";
+                    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: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
@@ -86,26 +88,24 @@ export const inpRules = {
             (state, match, start, end) => {
                 let node = (state.doc.resolve(start) as any).nodeAfter;
                 let sm = state.storedMarks || undefined;
-                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
                 let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "left";
                 }
-                return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
-                    state.tr;
+                return node ? state.tr.deleteRange(start, end).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
             }),
         new InputRule(
             new RegExp(/^\]\]\s$/),
             (state, match, start, end) => {
                 let node = (state.doc.resolve(start) as any).nodeAfter;
                 let sm = state.storedMarks || undefined;
-                let ruleProvider = Cast(FormattedTextBox.InputBoxOverlay!.props.Document.ruleProvider, Doc) as Doc;
+                let ruleProvider = FormattedTextBox.InputBoxOverlay!.props.ruleProvider;
                 let heading = NumCast(FormattedTextBox.InputBoxOverlay!.props.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleAlign_" + heading] = "right";
                 }
-                return node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
-                    state.tr;
+                return node ? state.tr.deleteRange(start, end).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 2750203f2..0a89f8c17 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -350,8 +350,9 @@ export const marks: { [index: string]: MarkSpec } = {
                         let style = getComputedStyle(p);
                         if (style.textDecoration === "underline") return null;
                         if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
-                            p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1)
+                            p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
                             return null;
+                        }
                     }
                     return false;
                 }
@@ -372,10 +373,9 @@ export const marks: { [index: string]: MarkSpec } = {
                 getAttrs: (p: any) => {
                     if (typeof (p) !== "string") {
                         let style = getComputedStyle(p);
-                        if (style.textDecoration === "underline")
-                            return null;
-                        if (p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1)
+                        if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
                             return null;
+                        }
                     }
                     return false;
                 }
@@ -635,11 +635,11 @@ export class ImageResizeView {
                             DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
                         } else {
                             DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
-                        } e.ctrlKey
+                        }
                     }
                 });
             }
-        }
+        };
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
@@ -778,11 +778,11 @@ export class FootnoteView {
         this.innerView.updateState(state);
 
         if (!tr.getMeta("fromOutside")) {
-            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1)
-            for (let i = 0; i < transactions.length; i++) {
-                let steps = transactions[i].steps;
-                for (let j = 0; j < steps.length; j++) {
-                    outerTr.step(steps[j].map(offsetMap));
+            let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+            for (let transaction of transactions) {
+                let steps = transaction.steps;
+                for (let step of steps) {
+                    outerTr.step(step.map(offsetMap));
                 }
             }
             if (outerTr.docChanged) this.outerView.dispatch(outerTr);
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 1d0916ac0..ff4451824 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -19,6 +19,7 @@ export interface ScriptSucccess {
 export interface ScriptError {
     success: false;
     error: any;
+    result: any;
 }
 
 export type ScriptResult = ScriptSucccess | ScriptError;
@@ -27,7 +28,7 @@ export interface CompiledScript {
     readonly compiled: true;
     readonly originalScript: string;
     readonly options: Readonly<ScriptOptions>;
-    run(args?: { [name: string]: any }): ScriptResult;
+    run(args?: { [name: string]: any }, onError?: (res: any) => void, errorVal?: any): ScriptResult;
 }
 
 export interface CompileError {
@@ -100,7 +101,7 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
     // let params: any[] = [Docs, ...fieldTypes];
     let compiledFunction = new Function(...paramNames, `return ${script}`);
     let { capturedVariables = {} } = options;
-    let run = (args: { [name: string]: any } = {}): ScriptResult => {
+    let run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => {
         let argsArray: any[] = [];
         for (let name of customParams) {
             if (name === "this") {
@@ -127,7 +128,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an
             if (batch) {
                 batch.end();
             }
-            return { success: false, error };
+            onError && onError(error);
+            return { success: false, error, result: errorVal };
         }
     };
     return { compiled: true, run, originalScript, options };
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 9efef888d..4c97a1056 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -24,6 +24,10 @@ export namespace SelectionManager {
                 manager.SelectedDocuments.push(docView);
                 // console.log(manager.SelectedDocuments);
                 docView.props.whenActiveChanged(true);
+            } else if (!ctrlPressed && manager.SelectedDocuments.length > 1) {
+                manager.SelectedDocuments.map(dv => dv !== docView && dv.props.whenActiveChanged(false));
+                manager.SelectedDocuments = [docView];
+                FormattedTextBox.InputBoxOverlay = undefined;
             }
         }
         @action
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 72a4b4141..f427e40b1 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -102,7 +102,7 @@ export default class SharingManager extends React.Component<{}> {
     populateUsers = async () => {
         let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
         runInAction(() => {
-            this.users = (JSON.parse(userList) as User[]).filter(({ email }) => email !== CurrentUserUtils.email);
+            this.users = (JSON.parse(userList) as User[]).filter(({ email }) => email !== Doc.CurrentUserEmail);
         });
     }
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index c376b6f86..b6de048e4 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -58,10 +58,6 @@ export class TooltipTextMenu {
 
     private _collapsed: boolean = false;
 
-    @observable
-    private _storedMarks: Mark<any>[] | null | undefined;
-
-
     constructor(view: EditorView, editorProps: FieldViewProps & FormattedTextBoxProps) {
         this.view = view;
         this.editorProps = editorProps;
@@ -84,8 +80,6 @@ export class TooltipTextMenu {
 
         this.dragElement(dragger);
 
-        this._storedMarks = this.view.state.storedMarks;
-
         // this.createCollapse();
         // if (this._collapseBtn) {
         //     this.tooltip.appendChild(this._collapseBtn.render(this.view).dom);
@@ -280,7 +274,7 @@ export class TooltipTextMenu {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
                                     DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
                                 }
-                                else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f, undefined);
+                                else this.editorProps.addDocTab(f, undefined, "onRight");
                             }
                         }));
                     }
@@ -310,8 +304,8 @@ export class TooltipTextMenu {
                             dragComplete: action(() => {
                                 let linkDoc = dragData.linkDocument;
                                 let proto = Doc.GetProto(linkDoc);
-                                if (proto && docView && docView.props.ContainingCollectionView) {
-                                    proto.sourceContext = docView.props.ContainingCollectionView.props.Document;
+                                if (proto && docView) {
+                                    proto.sourceContext = docView.props.ContainingCollectionDoc;
                                 }
                                 linkDoc instanceof Doc && this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab");
                             }),
@@ -496,7 +490,7 @@ export class TooltipTextMenu {
             if (markType.name[0] === 'p') {
                 let size = this.fontSizeToNum.get(markType);
                 if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
-                let ruleProvider = Cast(this.editorProps.Document.ruleProvider, Doc) as Doc;
+                let ruleProvider = this.editorProps.ruleProvider;
                 let heading = NumCast(this.editorProps.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleSize_" + heading] = size;
@@ -505,7 +499,7 @@ export class TooltipTextMenu {
             else {
                 let fontName = this.fontStylesToName.get(markType);
                 if (fontName) { this.updateFontStyleDropdown(fontName); }
-                let ruleProvider = Cast(this.editorProps.Document.ruleProvider, Doc) as Doc;
+                let ruleProvider = this.editorProps.ruleProvider;
                 let heading = NumCast(this.editorProps.Document.heading);
                 if (ruleProvider && heading) {
                     ruleProvider["ruleFont_" + heading] = fontName;
@@ -523,12 +517,12 @@ export class TooltipTextMenu {
         tx2.doc.descendants((node: any, offset: any, index: any) => {
             if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
                 let path = (tx2.doc.resolve(offset) as any).path;
-                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && (c as any).type === schema.nodes.ordered_list ? 1 : 0), 0);
+                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
                 if (node.type === schema.nodes.ordered_list) depth++;
                 tx2.setNodeMarkup(offset, node.type, { mapStyle: style, bulletStyle: depth }, node.marks);
             }
         });
-    };
+    }
     //remove all node typeand apply the passed-in one to the selected text
     changeToNodeType = (nodeType: NodeType | undefined, view: EditorView) => {
         //remove oldif (nodeType) { //add new
diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts
index 156390fd3..7abb9d1ee 100644
--- a/src/client/util/UndoManager.ts
+++ b/src/client/util/UndoManager.ts
@@ -127,7 +127,7 @@ export namespace UndoManager {
 
     export function StartBatch(batchName: string): Batch {
         batchCounter++;
-        if (batchCounter > 0) {
+        if (batchCounter > 0 && currentBatch === undefined) {
             currentBatch = [];
         }
         return new Batch(batchName);
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 68b97f2b6..5d452e72e 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -248,7 +248,7 @@ export class ContextMenu extends React.Component {
             e.preventDefault();
         } else if (e.key === "Enter" || e.key === "Tab") {
             const item = this.flatItems[this.selectedIndex];
-            item && item.event();
+            item && item.event({ x: this.pageX, y: this.pageY });
             this.closeMenu();
             e.preventDefault();
         }
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
new file mode 100644
index 000000000..8cd419bbe
--- /dev/null
+++ b/src/client/views/DocumentButtonBar.scss
@@ -0,0 +1,129 @@
+@import "globalCssVariables";
+
+$linkGap : 3px;
+
+.linkFlyout {
+    grid-column: 2/4;
+}
+
+.linkButton-empty:hover {
+    background: $main-accent;
+    transform: scale(1.05);
+    cursor: pointer;
+}
+
+.linkButton-nonempty:hover {
+    background: $main-accent;
+    transform: scale(1.05);
+    cursor: pointer;
+}
+
+.documentButtonBar {
+    margin-top: $linkGap;
+    grid-column: 1/4;
+    width: max-content;
+    height: auto;
+    display: flex;
+    flex-direction: row;
+}
+
+.linkButtonWrapper {
+    pointer-events: auto;
+    padding-right: 5px;
+    width: 25px;
+}
+
+.linkButton-linker {
+    height: 20px;
+    width: 20px;
+    text-align: center;
+    border-radius: 50%;
+    pointer-events: auto;
+    color: $dark-color;
+    border: $dark-color 1px solid;
+}
+
+.linkButton-linker:hover {
+    cursor: pointer;
+    transform: scale(1.05);
+}
+
+.linkButton-empty,
+.linkButton-nonempty {
+    height: 20px;
+    width: 20px;
+    border-radius: 50%;
+    opacity: 0.9;
+    pointer-events: auto;
+    background-color: $dark-color;
+    color: $light-color;
+    text-transform: uppercase;
+    letter-spacing: 2px;
+    font-size: 75%;
+    transition: transform 0.2s;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+
+    &:hover {
+        background: $main-accent;
+        transform: scale(1.05);
+        cursor: pointer;
+    }
+}
+
+.templating-menu {
+    position: absolute;
+    pointer-events: auto;
+    text-transform: uppercase;
+    letter-spacing: 2px;
+    font-size: 75%;
+    transition: transform 0.2s;
+    text-align: center;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+}
+
+.templating-button,
+.docDecs-tagButton {
+    width: 20px;
+    height: 20px;
+    border-radius: 50%;
+    opacity: 0.9;
+    font-size: 14;
+    background-color: $dark-color;
+    color: $light-color;
+    text-align: center;
+    cursor: pointer;
+
+    &:hover {
+        background: $main-accent;
+        transform: scale(1.05);
+    }
+}
+
+#template-list {
+    position: absolute;
+    top: 25px;
+    left: 0px;
+    width: max-content;
+    font-family: $sans-serif;
+    font-size: 12px;
+    background-color: $light-color-secondary;
+    padding: 2px 12px;
+    list-style: none;
+
+    .templateToggle, .chromeToggle {
+        text-align: left;
+    }
+
+    input {
+        margin-right: 10px;
+    }
+}
+
+@-moz-keyframes spin { 100% { -moz-transform: rotate(360deg); } }
+@-webkit-keyframes spin { 100% { -webkit-transform: rotate(360deg); } }
+@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
\ No newline at end of file
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
new file mode 100644
index 000000000..338f6b83e
--- /dev/null
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -0,0 +1,368 @@
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, observable, runInAction } from "mobx";
+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";
+import { LinkManager } from '../util/LinkManager';
+import { UndoManager } from "../util/UndoManager";
+import './DocumentButtonBar.scss';
+import './collections/ParentDocumentSelector.scss';
+import { LinkMenu } from "./linking/LinkMenu";
+import { MetadataEntryMenu } from './MetadataEntryMenu';
+import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
+import { TemplateMenu } from "./TemplateMenu";
+import { Template, Templates } from "./Templates";
+import React = require("react");
+import { DocumentView } from './nodes/DocumentView';
+import { ParentDocSelector } from './collections/ParentDocumentSelector';
+import { CollectionDockingView } from './collections/CollectionDockingView';
+const higflyout = require("@hig/flyout");
+export const { anchorPoints } = higflyout;
+export const Flyout = higflyout.default;
+
+library.add(faLink);
+library.add(faTag);
+library.add(faTimes);
+library.add(faArrowAltCircleDown);
+library.add(faArrowAltCircleUp);
+library.add(faStopCircle);
+library.add(faCheckCircle);
+library.add(faCloudUploadAlt);
+library.add(faSyncAlt);
+library.add(faShare);
+
+const cloud: IconProp = "cloud-upload-alt";
+const fetch: IconProp = "sync-alt";
+
+@observer
+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 _tooltipoff = React.createRef<HTMLDivElement>();
+    private _textDoc?: Doc;
+    private _linkDrag?: UndoManager.Batch;
+    public static Instance: DocumentButtonBar;
+
+    constructor(props: { views: DocumentView[] }) {
+        super(props);
+        DocumentButtonBar.Instance = this;
+    }
+
+    @observable public pushIcon: IconProp = "arrow-alt-circle-up";
+    @observable public pullIcon: IconProp = "arrow-alt-circle-down";
+    @observable public pullColor: string = "white";
+    @observable public isAnimatingFetch = false;
+    @observable public openHover = false;
+    public pullColorAnimating = false;
+
+    private pullAnimating = false;
+    private pushAnimating = false;
+
+    public startPullOutcome = action((success: boolean) => {
+        if (!this.pullAnimating) {
+            this.pullAnimating = true;
+            this.pullIcon = success ? "check-circle" : "stop-circle";
+            setTimeout(() => runInAction(() => {
+                this.pullIcon = "arrow-alt-circle-down";
+                this.pullAnimating = false;
+            }), 1000);
+        }
+    });
+
+    public startPushOutcome = action((success: boolean) => {
+        if (!this.pushAnimating) {
+            this.pushAnimating = true;
+            this.pushIcon = success ? "check-circle" : "stop-circle";
+            setTimeout(() => runInAction(() => {
+                this.pushIcon = "arrow-alt-circle-up";
+                this.pushAnimating = false;
+            }), 1000);
+        }
+    });
+
+    public setPullState = action((unchanged: boolean) => {
+        this.isAnimatingFetch = false;
+        if (!this.pullColorAnimating) {
+            this.pullColorAnimating = true;
+            this.pullColor = unchanged ? "lawngreen" : "red";
+            setTimeout(this.clearPullColor, 1000);
+        }
+    });
+
+    private clearPullColor = action(() => {
+        this.pullColor = "white";
+        this.pullColorAnimating = false;
+    });
+
+    onLinkerButtonDown = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        e.preventDefault();
+        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+        document.addEventListener("pointermove", this.onLinkerButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkerButtonUp);
+        document.addEventListener("pointerup", this.onLinkerButtonUp);
+    }
+
+    onEmbedButtonDown = (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);
+    }
+
+    onLinkerButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkerButtonUp);
+        e.stopPropagation();
+    }
+
+    onEmbedButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+        document.removeEventListener("pointerup", this.onEmbedButtonUp);
+        e.stopPropagation();
+    }
+
+    @action
+    onLinkerButtonMoved = (e: PointerEvent): void => {
+        if (this._linkerButton.current !== null) {
+            document.removeEventListener("pointermove", this.onLinkerButtonMoved);
+            document.removeEventListener("pointerup", this.onLinkerButtonUp);
+            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: {
+                    dragComplete: () => {
+                        if (this._linkDrag) {
+                            this._linkDrag.end();
+                            this._linkDrag = undefined;
+                        }
+                    },
+                },
+                hideSource: false
+            });
+        }
+        e.stopPropagation();
+    }
+
+    @action
+    onEmbedButtonMoved = (e: PointerEvent): void => {
+        if (this._embedButton.current !== null) {
+            document.removeEventListener("pointermove", this.onEmbedButtonMoved);
+            document.removeEventListener("pointerup", this.onEmbedButtonUp);
+
+            let dragDocView = this.props.views[0];
+            let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
+
+            DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
+                handlers: {
+                    dragComplete: action(emptyFunction),
+                },
+                hideSource: false
+            });
+        }
+        e.stopPropagation();
+    }
+
+    onLinkButtonDown = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        e.preventDefault();
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.addEventListener("pointermove", this.onLinkButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkButtonUp);
+        document.addEventListener("pointerup", this.onLinkButtonUp);
+    }
+
+    onLinkButtonUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onLinkButtonMoved);
+        document.removeEventListener("pointerup", this.onLinkButtonUp);
+        e.stopPropagation();
+    }
+
+    onLinkButtonMoved = async (e: PointerEvent) => {
+        if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+            document.removeEventListener("pointermove", this.onLinkButtonMoved);
+            document.removeEventListener("pointerup", this.onLinkButtonUp);
+            DragLinksAsDocuments(this._linkButton.current, e.x, e.y, this.props.views[0].props.Document);
+        }
+        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>
+            </div>
+        );
+    }
+
+    private get targetDoc() {
+        return this.props.views[0].props.Document;
+    }
+
+    considerGoogleDocsPush = () => {
+        let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+        if (!canPush) return (null);
+        let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
+        let icon: IconProp = published ? (this.pushIcon as any) : cloud;
+        return (
+            <div className={"linkButtonWrapper"}>
+                <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
+                    DocumentButtonBar.hasPushedHack = false;
+                    this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
+                }}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
+                </div>
+            </div>
+        );
+    }
+
+    considerGoogleDocsPull = () => {
+        let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
+        let dataDoc = Doc.GetProto(this.targetDoc);
+        if (!canPull || !dataDoc[GoogleRef]) return (null);
+        let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
+        icon = this.openHover ? "share" : icon;
+        let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
+        let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
+        return (
+            <div className={"linkButtonWrapper"}>
+                <div
+                    title={title}
+                    className="linkButton-linker"
+                    style={{
+                        backgroundColor: this.pullColor,
+                        transition: "0.2s ease all"
+                    }}
+                    onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
+                    onPointerLeave={() => runInAction(() => this.openHover = false)}
+                    onClick={e => {
+                        if (e.altKey) {
+                            e.preventDefault();
+                            window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
+                        } else {
+                            this.clearPullColor();
+                            DocumentButtonBar.hasPulledHack = false;
+                            this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
+                            dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
+                        }
+                    }}>
+                    <FontAwesomeIcon
+                        style={{
+                            WebkitAnimation: animation,
+                            MozAnimation: animation
+                        }}
+                        className="documentdecorations-icon"
+                        icon={icon}
+                        size="sm"
+                    />
+                </div>
+            </div>
+        );
+    }
+
+    public static hasPushedHack = false;
+    public static hasPulledHack = false;
+
+    considerTooltip = () => {
+        let thisDoc = this.props.views[0].props.Document;
+        let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
+        if (!isTextDoc) return null;
+        this._textDoc = thisDoc;
+        return (
+            <div className="tooltipwrapper">
+                <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
+                    {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
+                </div>
+            </div>
+
+        );
+    }
+
+    onTooltipOff = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        if (this._textDoc) {
+            if (this._tooltipoff.current) {
+                if (this._tooltipoff.current.title === "Hide Tooltip") {
+                    this._tooltipoff.current.title = "Show Tooltip";
+                    this._textDoc.tooltip = "hi";
+                }
+                else {
+                    this._tooltipoff.current.title = "Hide Tooltip";
+                }
+            }
+        }
+    }
+
+    get metadataMenu() {
+        return (
+            <div className="linkButtonWrapper">
+                <Flyout anchorPoint={anchorPoints.TOP_LEFT}
+                    content={<MetadataEntryMenu docs={() => this.props.views.map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
+                    <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
+                </Flyout>
+            </div>
+        );
+    }
+
+    render() {
+        let linkButton = null;
+        if (this.props.views.length > 0) {
+            let selFirst = this.props.views[0];
+
+            let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
+            linkButton = (<Flyout
+                anchorPoint={anchorPoints.RIGHT_TOP}
+                content={<LinkMenu docView={selFirst}
+                    addDocTab={selFirst.props.addDocTab}
+                    changeFlyout={emptyFunction} />}>
+                <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
+            </Flyout >);
+        }
+
+        let templates: Map<Template, boolean> = new Map();
+        Array.from(Object.values(Templates.TemplateList)).map(template =>
+            templates.set(template, this.props.views.reduce((checked, doc) => checked || doc.getLayoutPropStr("show" + template.Name) ? true : false, false as boolean)));
+
+        return (<div className="documentButtonBar">
+            <div className="linkButtonWrapper">
+                <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton}  </div>
+            </div>
+            <div className="linkButtonWrapper">
+                <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
+                    <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
+                </div>
+            </div>
+            <div className="linkButtonWrapper">
+                <TemplateMenu docs={this.props.views} templates={templates} />
+            </div>
+            {this.metadataMenu}
+            {this.considerEmbed()}
+            {this.considerGoogleDocsPush()}
+            {this.considerGoogleDocsPull()}
+            <ParentDocSelector Document={this.props.views[0].props.Document} addDocTab={(doc, data, where) => {
+                where === "onRight" ? CollectionDockingView.AddRightSplit(doc, data) : this.props.stack ? CollectionDockingView.Instance.AddTab(this.props.stack, doc, data) : this.props.views[0].props.addDocTab(doc, data, "onRight");
+                return true;
+            }} />
+            {/* {this.considerTooltip()} */}
+        </div>
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 117e63a37..32346165d 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -154,7 +154,7 @@ $linkGap : 3px;
 .link-button-container {
     margin-top: $linkGap;
     grid-column: 1/4;
-    width: auto;
+    width: max-content;
     height: auto;
     display: flex;
     flex-direction: row;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 772b4917e..1a50dc404 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -1,36 +1,29 @@
-import { library, IconProp } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faTag, faTimes, faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faStopCircle, faCloudUploadAlt, faSyncAlt, faShare } from '@fortawesome/free-solid-svg-icons';
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faArrowAltCircleDown, faArrowAltCircleUp, faCheckCircle, faCloudUploadAlt, faLink, faShare, faStopCircle, faSyncAlt, faTag, faTimes } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, reaction, runInAction, trace } from "mobx";
+import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, DocListCastAsync } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
+import { ObjectField } from '../../new_fields/ObjectField';
 import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
-import { URLField } from '../../new_fields/URLField';
+import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { emptyFunction, Utils } from "../../Utils";
-import { Docs } from "../documents/Documents";
+import { Docs, DocUtils } from "../documents/Documents";
 import { DocumentManager } from "../util/DocumentManager";
-import { DragLinksAsDocuments, DragManager } from "../util/DragManager";
+import { DragManager } from "../util/DragManager";
 import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch, UndoManager } from "../util/UndoManager";
 import { MINIMIZED_ICON_SIZE } from "../views/globalCssVariables.scss";
 import { CollectionView } from "./collections/CollectionView";
+import { DocumentButtonBar } from './DocumentButtonBar';
 import './DocumentDecorations.scss';
-import { DocumentView, PositionDocument } from "./nodes/DocumentView";
+import { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
+import { DocumentView, swapViews } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "./nodes/FormattedTextBox";
+import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { IconBox } from "./nodes/IconBox";
-import { LinkMenu } from "./linking/LinkMenu";
-import { TemplateMenu } from "./TemplateMenu";
-import { Template, Templates } from "./Templates";
 import React = require("react");
-import { RichTextField } from '../../new_fields/RichTextField';
-import { LinkManager } from '../util/LinkManager';
-import { MetadataEntryMenu } from './MetadataEntryMenu';
-import { ImageBox } from './nodes/ImageBox';
-import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
-import { ObjectField } from '../../new_fields/ObjectField';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -46,28 +39,21 @@ library.add(faCloudUploadAlt);
 library.add(faSyncAlt);
 library.add(faShare);
 
-const cloud: IconProp = "cloud-upload-alt";
-const fetch: IconProp = "sync-alt";
-
 @observer
 export class DocumentDecorations extends React.Component<{}, { value: string }> {
     static Instance: DocumentDecorations;
     private _isPointerDown = false;
     private _resizing = "";
-    private keyinput: React.RefObject<HTMLInputElement>;
+    private _keyinput: React.RefObject<HTMLInputElement>;
     private _resizeBorderWidth = 16;
     private _linkBoxHeight = 20 + 3; // link button height + margin
     private _titleHeight = 20;
-    private _linkButton = React.createRef<HTMLDivElement>();
-    private _linkerButton = React.createRef<HTMLDivElement>();
     private _embedButton = React.createRef<HTMLDivElement>();
-    private _tooltipoff = React.createRef<HTMLDivElement>();
-    private _textDoc?: Doc;
     private _downX = 0;
     private _downY = 0;
     private _iconDoc?: Doc = undefined;
     private _resizeUndo?: UndoManager.Batch;
-    private _linkDrag?: UndoManager.Batch;
+    private _radiusDown = [0, 0];
     @observable private _minimizedX = 0;
     @observable private _minimizedY = 0;
     @observable private _title: string = "";
@@ -77,7 +63,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable private _opacity = 1;
     @observable private _removeIcon = false;
     @observable public Interacting = false;
-    @observable private _isMoving = false;
 
     @observable public pushIcon: IconProp = "arrow-alt-circle-up";
     @observable public pullIcon: IconProp = "arrow-alt-circle-down";
@@ -85,52 +70,11 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable public isAnimatingFetch = false;
     @observable public isAnimatingPulse = false;
     @observable public openHover = false;
-    public pullColorAnimating = false;
-
-    private pullAnimating = false;
-    private pushAnimating = false;
-
-    public startPullOutcome = action((success: boolean) => {
-        if (!this.pullAnimating) {
-            this.pullAnimating = true;
-            this.pullIcon = success ? "check-circle" : "stop-circle";
-            setTimeout(() => runInAction(() => {
-                this.pullIcon = "arrow-alt-circle-down";
-                this.pullAnimating = false;
-            }), 1000);
-        }
-    });
-
-    public startPushOutcome = action((success: boolean) => {
-        if (!this.pushAnimating) {
-            this.pushAnimating = true;
-            this.isAnimatingPulse = false;
-            this.pushIcon = success ? "check-circle" : "stop-circle";
-            setTimeout(() => runInAction(() => {
-                this.pushIcon = "arrow-alt-circle-up";
-                this.pushAnimating = false;
-            }), 1000);
-        }
-    });
-
-    public setPullState = action((unchanged: boolean) => {
-        this.isAnimatingFetch = false;
-        if (!this.pullColorAnimating) {
-            this.pullColorAnimating = true;
-            this.pullColor = unchanged ? "lawngreen" : "red";
-            setTimeout(this.clearPullColor, 1000);
-        }
-    });
-
-    private clearPullColor = action(() => {
-        this.pullColor = "white";
-        this.pullColorAnimating = false;
-    });
 
     constructor(props: Readonly<{}>) {
         super(props);
         DocumentDecorations.Instance = this;
-        this.keyinput = React.createRef();
+        this._keyinput = React.createRef();
         reaction(() => SelectionManager.SelectedDocuments().slice(), docs => this._edtingTitle = false);
     }
 
@@ -144,23 +88,30 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             if (text[0] === '#') {
                 this._fieldKey = text.slice(1, text.length);
                 this._title = this.selectionTitle;
+            } else if (text.startsWith("::")) {
+                let targetID = text.slice(2, text.length);
+                let promoteDoc = SelectionManager.SelectedDocuments()[0];
+                DocUtils.Publish(promoteDoc.props.Document, targetID, promoteDoc.props.addDocument, promoteDoc.props.removeDocument);
             } else if (text.startsWith(">")) {
                 let fieldTemplateView = SelectionManager.SelectedDocuments()[0];
                 SelectionManager.DeselectAll();
                 let fieldTemplate = fieldTemplateView.props.Document;
                 let containerView = fieldTemplateView.props.ContainingCollectionView;
-                if (containerView) {
-                    let docTemplate = containerView.props.Document;
+                let docTemplate = fieldTemplateView.props.ContainingCollectionDoc;
+                if (containerView && docTemplate) {
                     let metaKey = text.startsWith(">>") ? text.slice(2, text.length) : text.slice(1, text.length);
-                    let proto = Doc.GetProto(docTemplate);
                     if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) {
                         const fd = fieldTemplate.data;
                         fd instanceof ObjectField && (Doc.GetProto(containerView.props.DataDoc)[metaKey] = ObjectField.MakeCopy(fd));
                     }
-                    Doc.MakeTemplate(fieldTemplate, metaKey, proto);
+                    fieldTemplate.title = metaKey;
+                    Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
                     if (text.startsWith(">>")) {
-                        proto.detailedLayout = proto.layout;
-                        proto.miniLayout = ImageBox.LayoutString(metaKey);
+                        let layoutNative = Doc.MakeTitled("layoutNative");
+                        Doc.GetProto(docTemplate).layoutNative = layoutNative;
+                        swapViews(fieldTemplate, "", "layoutNative", layoutNative);
+                        layoutNative.layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+                        layoutNative.backgroundLayout = StrCast(fieldTemplateView.props.Document.backgroundLayout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
                     }
                 }
             }
@@ -222,7 +173,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             }
             let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
             if (transform.TranslateX === 0 && transform.TranslateY === 0) {
-                setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...
+                setTimeout(action(() => this._forceUpdate++), 0); // bcz: fix CollectionStackingView's getTransform() somehow...without this, resizing things in the library view, for instance, show the wrong bounds
                 return this._lastBox;
             }
 
@@ -247,11 +198,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @action
     onBackgroundMove = (e: PointerEvent): void => {
         let dragDocView = SelectionManager.SelectedDocuments()[0];
+        let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document));
         const [left, top] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).inverse().transformPoint(0, 0);
-        const [xoff, yoff] = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
-        let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc ? dv.props.DataDoc : dv.props.Document));
-        dragData.xOffset = xoff;
-        dragData.yOffset = yoff;
+        dragData.offset = dragDocView.props.ScreenToLocalTransform().scale(dragDocView.props.ContentScaling()).transformDirection(e.x - left, e.y - top);
         dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
         this.Interacting = true;
         this._hidden = true;
@@ -354,22 +303,16 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             if (this._iconDoc && selectedDocs.length === 1 && this._removeIcon) {
                 selectedDocs[0].props.removeDocument && selectedDocs[0].props.removeDocument(this._iconDoc);
             }
-            if (!this._removeIcon) {
-                if (selectedDocs.length === 1) {
-                    this.getIconDoc(selectedDocs[0]).then(icon => selectedDocs[0].toggleMinimized());
-                } else if (Math.abs(e.pageX - this._downX) < Utils.DRAG_THRESHOLD &&
-                    Math.abs(e.pageY - this._downY) < Utils.DRAG_THRESHOLD) {
-                    let docViews = SelectionManager.ViewsSortedVertically();
-                    let topDocView = docViews[0];
-                    let ind = topDocView.templates.indexOf(Templates.Bullet.Layout);
-                    if (ind !== -1) {
-                        topDocView.templates.splice(ind, 1);
-                        topDocView.props.Document.subBulletDocs = undefined;
-                    } else {
-                        topDocView.addTemplate(Templates.Bullet);
-                        topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
+            if (!this._removeIcon && selectedDocs.length === 1) { // if you click on the top-left button when just 1 doc is selected, then collapse it.  not sure why we don't do it for multiple selections
+                this.getIconDoc(selectedDocs[0]).then(async icon => {
+                    let minimizedDoc = await Cast(selectedDocs[0].props.Document.minimizedDoc, Doc);
+                    if (minimizedDoc) {
+                        let scrpt = selectedDocs[0].props.ScreenToLocalTransform().scale(selectedDocs[0].props.ContentScaling()).inverse().transformPoint(
+                            NumCast(minimizedDoc.x) - NumCast(selectedDocs[0].Document.x), NumCast(minimizedDoc.y) - NumCast(selectedDocs[0].Document.y));
+                        SelectionManager.DeselectAll();
+                        DocumentManager.Instance.animateBetweenPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
                     }
-                }
+                });
             }
             this._removeIcon = false;
         }
@@ -381,10 +324,9 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let doc = selected[0].props.Document;
         let iconDoc = Docs.Create.IconDocument(layoutString);
         iconDoc.isButton = true;
-        iconDoc.proto!.title = selected.length > 1 ? "-multiple-.icon" : StrCast(doc.title) + ".icon";
-        iconDoc.labelField = selected.length > 1 ? undefined : this._fieldKey;
+
+        IconBox.AutomaticTitle(iconDoc);
         //iconDoc.proto![this._fieldKey] = selected.length > 1 ? "collection" : undefined;
-        iconDoc.proto!.isMinimized = false;
         iconDoc.width = Number(MINIMIZED_ICON_SIZE);
         iconDoc.height = Number(MINIMIZED_ICON_SIZE);
         iconDoc.x = NumCast(doc.x);
@@ -413,7 +355,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         iconDoc.y = where[1] + NumCast(selView.props.Document.y);
     }
 
-    _radiusDown = [0, 0];
     @action
     onRadiusDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
@@ -426,23 +367,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             document.addEventListener("pointermove", this.onRadiusMove);
             document.addEventListener("pointerup", this.onRadiusUp);
         }
-        if (!this._isMoving) {
-            SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
-                map(d => d.borderRounding = "0%");
-        }
     }
 
     onRadiusMove = (e: PointerEvent): void => {
-        this._isMoving = true;
         let dist = Math.sqrt((e.clientX - this._radiusDown[0]) * (e.clientX - this._radiusDown[0]) + (e.clientY - this._radiusDown[1]) * (e.clientY - this._radiusDown[1]));
-        SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
-            map(d => d.borderRounding = `${Math.min(100, dist)}%`);
+        dist = dist < 3 ? 0 : dist;
+        let usingRule = false;
         SelectionManager.SelectedDocuments().map(dv => {
-            let cv = dv.props.ContainingCollectionView;
-            let ruleProvider = cv && (Cast(cv.props.Document.ruleProvider, Doc) as Doc);
+            let ruleProvider = dv.props.ruleProvider;
             let heading = NumCast(dv.props.Document.heading);
-            cv && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleRounding_" + heading] = StrCast(dv.props.Document.borderRounding));
+            ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
+            usingRule = usingRule || (ruleProvider && heading ? true : false);
         });
+        !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplate ? dv.props.Document : Doc.GetProto(dv.props.Document)).
+            map(d => d.borderRounding = `${Math.min(100, dist)}%`);
         e.stopPropagation();
         e.preventDefault();
     }
@@ -452,15 +390,18 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.preventDefault();
         this._isPointerDown = false;
         this._resizeUndo && this._resizeUndo.end();
-        this._isMoving = false;
         document.removeEventListener("pointermove", this.onRadiusMove);
         document.removeEventListener("pointerup", this.onRadiusUp);
     }
 
+    _lastX = 0;
+    _lastY = 0;
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
         if (e.button === 0) {
+            this._lastX = e.clientX;
+            this._lastY = e.clientY;
             this._isPointerDown = true;
             this._resizing = e.currentTarget.id;
             this.Interacting = true;
@@ -472,13 +413,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         }
     }
 
-    onLinkerButtonDown = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
-        document.addEventListener("pointermove", this.onLinkerButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkerButtonUp);
-        document.addEventListener("pointerup", this.onLinkerButtonUp);
-    }
 
     onEmbedButtonDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
@@ -488,11 +422,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         document.addEventListener("pointerup", this.onEmbedButtonUp);
     }
 
-    onLinkerButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onLinkerButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkerButtonUp);
-        e.stopPropagation();
-    }
+
 
     onEmbedButtonUp = (e: PointerEvent): void => {
         document.removeEventListener("pointermove", this.onEmbedButtonMoved);
@@ -500,31 +430,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.stopPropagation();
     }
 
-    @action
-    onLinkerButtonMoved = (e: PointerEvent): void => {
-        if (this._linkerButton.current !== null) {
-            document.removeEventListener("pointermove", this.onLinkerButtonMoved);
-            document.removeEventListener("pointerup", this.onLinkerButtonUp);
-            let selDoc = SelectionManager.SelectedDocuments()[0];
-            let container = selDoc.props.ContainingCollectionView ? selDoc.props.ContainingCollectionView.props.Document.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: {
-                    dragComplete: () => {
-                        if (this._linkDrag) {
-                            this._linkDrag.end();
-                            this._linkDrag = undefined;
-                        }
-                    },
-                },
-                hideSource: false
-            });
-        }
-        e.stopPropagation();
-    }
-
     @action
     onEmbedButtonMoved = (e: PointerEvent): void => {
         if (this._embedButton.current !== null) {
@@ -544,29 +449,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         e.stopPropagation();
     }
 
-    onLinkButtonDown = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        document.removeEventListener("pointermove", this.onLinkButtonMoved);
-        document.addEventListener("pointermove", this.onLinkButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkButtonUp);
-        document.addEventListener("pointerup", this.onLinkButtonUp);
-    }
-
-    onLinkButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onLinkButtonMoved);
-        document.removeEventListener("pointerup", this.onLinkButtonUp);
-        e.stopPropagation();
-    }
-
-    onLinkButtonMoved = async (e: PointerEvent) => {
-        if (this._linkButton.current !== null && (e.movementX > 1 || e.movementY > 1)) {
-            document.removeEventListener("pointermove", this.onLinkButtonMoved);
-            document.removeEventListener("pointerup", this.onLinkButtonUp);
-            DragLinksAsDocuments(this._linkButton.current, e.x, e.y, SelectionManager.SelectedDocuments()[0].props.Document);
-        }
-        e.stopPropagation();
-    }
-
     onPointerMove = (e: PointerEvent): void => {
         e.stopPropagation();
         e.preventDefault();
@@ -576,42 +458,47 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
         let dX = 0, dY = 0, dW = 0, dH = 0;
 
+        let moveX = e.clientX - this._lastX; // e.movementX;
+        let moveY = e.clientY - this._lastY; // e.movementY;
+        this._lastX = e.clientX;
+        this._lastY = e.clientY;
+
         switch (this._resizing) {
             case "":
                 break;
             case "documentDecorations-topLeftResizer":
                 dX = -1;
                 dY = -1;
-                dW = -(e.movementX);
-                dH = -(e.movementY);
+                dW = -moveX;
+                dH = -moveY;
                 break;
             case "documentDecorations-topRightResizer":
-                dW = e.movementX;
+                dW = moveX;
                 dY = -1;
-                dH = -(e.movementY);
+                dH = -moveY;
                 break;
             case "documentDecorations-topResizer":
                 dY = -1;
-                dH = -(e.movementY);
+                dH = -moveY;
                 break;
             case "documentDecorations-bottomLeftResizer":
                 dX = -1;
-                dW = -(e.movementX);
-                dH = e.movementY;
+                dW = -moveX;
+                dH = moveY;
                 break;
             case "documentDecorations-bottomRightResizer":
-                dW = e.movementX;
-                dH = e.movementY;
+                dW = moveX;
+                dH = moveY;
                 break;
             case "documentDecorations-bottomResizer":
-                dH = e.movementY;
+                dH = moveY;
                 break;
             case "documentDecorations-leftResizer":
                 dX = -1;
-                dW = -(e.movementX);
+                dW = -moveX;
                 break;
             case "documentDecorations-rightResizer":
-                dW = e.movementX;
+                dW = moveX;
                 break;
         }
 
@@ -695,136 +582,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return "-unset-";
     }
 
-    changeFlyoutContent = (): void => {
-
-    }
-    // buttonOnPointerUp = (e: React.PointerEvent): void => {
-    //     e.stopPropagation();
-    // }
-
-    considerEmbed = () => {
-        let thisDoc = SelectionManager.SelectedDocuments()[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>
-            </div>
-        );
-    }
-
-    private get targetDoc() {
-        return SelectionManager.SelectedDocuments()[0].props.Document;
-    }
-
-    considerGoogleDocsPush = () => {
-        let canPush = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
-        if (!canPush) return (null);
-        let published = Doc.GetProto(this.targetDoc)[GoogleRef] !== undefined;
-        let icon: IconProp = published ? (this.pushIcon as any) : cloud;
-        return (
-            <div className={"linkButtonWrapper"}>
-                <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
-                    runInAction(() => this.isAnimatingPulse = true);
-                    DocumentDecorations.hasPushedHack = false;
-                    this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
-                }} style={{ animation: this.isAnimatingPulse ? "shadow-pulse 1s infinite" : "none" }}>
-                    <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
-                </div>
-            </div>
-        );
-    }
-
-    considerGoogleDocsPull = () => {
-        let canPull = this.targetDoc.data && this.targetDoc.data instanceof RichTextField;
-        let dataDoc = Doc.GetProto(this.targetDoc);
-        if (!canPull || !dataDoc[GoogleRef]) return (null);
-        let icon = dataDoc.unchanged === false ? (this.pullIcon as any) : fetch;
-        icon = this.openHover ? "share" : icon;
-        let animation = this.isAnimatingFetch ? "spin 0.5s linear infinite" : "none";
-        let title = `${!dataDoc.unchanged ? "Pull from" : "Fetch"} Google Docs`;
-        return (
-            <div className={"linkButtonWrapper"}>
-                <div
-                    title={title}
-                    className="linkButton-linker"
-                    style={{
-                        backgroundColor: this.pullColor,
-                        transition: "0.2s ease all"
-                    }}
-                    onPointerEnter={e => e.altKey && runInAction(() => this.openHover = true)}
-                    onPointerLeave={() => runInAction(() => this.openHover = false)}
-                    onClick={e => {
-                        if (e.altKey) {
-                            e.preventDefault();
-                            window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
-                        } else {
-                            this.clearPullColor();
-                            DocumentDecorations.hasPulledHack = false;
-                            this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
-                            dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
-                        }
-                    }}>
-                    <FontAwesomeIcon
-                        style={{
-                            WebkitAnimation: animation,
-                            MozAnimation: animation
-                        }}
-                        className="documentdecorations-icon"
-                        icon={icon}
-                        size="sm"
-                    />
-                </div>
-            </div>
-        );
-    }
-
-    public static hasPushedHack = false;
-    public static hasPulledHack = false;
-
-    considerTooltip = () => {
-        let thisDoc = SelectionManager.SelectedDocuments()[0].props.Document;
-        let isTextDoc = thisDoc.data && thisDoc.data instanceof RichTextField;
-        if (!isTextDoc) return null;
-        this._textDoc = thisDoc;
-        return (
-            <div className="tooltipwrapper">
-                <div title="Hide Tooltip" className="linkButton-linker" ref={this._tooltipoff} onPointerDown={this.onTooltipOff}>
-                    {/* <FontAwesomeIcon className="fa-image" icon="image" size="sm" /> */}
-                </div>
-            </div>
-
-        );
-    }
-
-    onTooltipOff = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        if (this._textDoc) {
-            if (this._tooltipoff.current) {
-                if (this._tooltipoff.current.title === "Hide Tooltip") {
-                    this._tooltipoff.current.title = "Show Tooltip";
-                    this._textDoc.tooltip = "hi";
-                }
-                else {
-                    this._tooltipoff.current.title = "Hide Tooltip";
-                }
-            }
-        }
-    }
-
-    get metadataMenu() {
-        return (
-            <div className="linkButtonWrapper">
-                <Flyout anchorPoint={anchorPoints.TOP_LEFT}
-                    content={<MetadataEntryMenu docs={() => SelectionManager.SelectedDocuments().map(dv => dv.props.Document)} suggestWithFunction />}>{/* tfs: @bcz This might need to be the data document? */}
-                    <div className="docDecs-tagButton" title="Add fields"><FontAwesomeIcon className="documentdecorations-icon" icon="tag" size="sm" /></div>
-                </Flyout>
-            </div>
-        );
-    }
-
     render() {
         var bounds = this.Bounds;
         let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -836,27 +593,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 {SelectionManager.SelectedDocuments().length === 1 ? IconBox.DocumentIcon(StrCast(SelectionManager.SelectedDocuments()[0].props.Document.layout, "...")) : "..."}
             </div>);
 
-        let linkButton = null;
-        if (SelectionManager.SelectedDocuments().length > 0) {
-            let selFirst = SelectionManager.SelectedDocuments()[0];
-
-            let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
-            linkButton = (<Flyout
-                anchorPoint={anchorPoints.RIGHT_TOP}
-                content={<LinkMenu docView={selFirst}
-                    addDocTab={selFirst.props.addDocTab}
-                    changeFlyout={this.changeFlyoutContent} />}>
-                <div className={"linkButton-" + (linkCount ? "nonempty" : "empty")} onPointerDown={this.onLinkButtonDown} >{linkCount}</div>
-            </Flyout >);
-        }
-
-        let templates: Map<Template, boolean> = new Map();
-        Array.from(Object.values(Templates.TemplateList)).map(template => {
-            let checked = false;
-            SelectionManager.SelectedDocuments().map(doc => checked = checked || (doc.props.Document["show" + template.Name] !== undefined));
-            templates.set(template, checked);
-        });
-
         bounds.x = Math.max(0, bounds.x - this._resizeBorderWidth / 2) + this._resizeBorderWidth / 2;
         bounds.y = Math.max(0, bounds.y - this._resizeBorderWidth / 2 - this._titleHeight) + this._resizeBorderWidth / 2 + this._titleHeight;
         const borderRadiusDraggerWidth = 15;
@@ -888,7 +624,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 {minimizeIcon}
 
                 {this._edtingTitle ?
-                    <input ref={this.keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
+                    <input ref={this._keyinput} className="title" type="text" name="dynbox" value={this._title} onBlur={this.titleBlur} onChange={this.titleChanged} onKeyPress={this.titleEntered} /> :
                     <div className="title" onPointerDown={this.onTitleDown} ><span>{`${this.selectionTitle}`}</span></div>}
                 <div className="documentDecorations-closeButton" title="Close Document" onPointerDown={this.onCloseDown}>
                     <FontAwesomeIcon className="documentdecorations-times" icon={faTimes} size="lg" />
@@ -904,22 +640,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 <div id="documentDecorations-bottomRightResizer" className="documentDecorations-resizer" onPointerDown={this.onPointerDown} onContextMenu={(e) => e.preventDefault()}></div>
                 <div id="documentDecorations-borderRadius" className="documentDecorations-radius" onPointerDown={this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}><span className="borderRadiusTooltip" title="Drag Corner Radius"></span></div>
                 <div className="link-button-container">
-                    <div className="linkButtonWrapper">
-                        <div title="View Links" className="linkFlyout" ref={this._linkButton}> {linkButton}  </div>
-                    </div>
-                    <div className="linkButtonWrapper">
-                        <div title="Drag Link" className="linkButton-linker" ref={this._linkerButton} onPointerDown={this.onLinkerButtonDown}>
-                            <FontAwesomeIcon className="documentdecorations-icon" icon="link" size="sm" />
-                        </div>
-                    </div>
-                    <div className="linkButtonWrapper">
-                        <TemplateMenu docs={SelectionManager.ViewsSortedVertically()} templates={templates} />
-                    </div>
-                    {this.metadataMenu}
-                    {this.considerEmbed()}
-                    {this.considerGoogleDocsPush()}
-                    {this.considerGoogleDocsPull()}
-                    {/* {this.considerTooltip()} */}
+                    <DocumentButtonBar views={SelectionManager.SelectedDocuments()} />
                 </div>
             </div >
         </div>
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 0255ab78a..c519991a5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -31,7 +31,7 @@ export default class KeyManager {
     }
 
     public handle = async (e: KeyboardEvent) => {
-        let keyname = e.key.toLowerCase();
+        let keyname = e.key && e.key.toLowerCase();
         this.handleGreedy(keyname);
 
         if (modifiers.includes(keyname)) {
@@ -90,9 +90,6 @@ export default class KeyManager {
                     });
                 }, "delete");
                 break;
-            case "enter":
-                SelectionManager.SelectedDocuments().map(selected => Doc.ToggleDetailLayout(selected.props.Document));
-                break;
         }
 
         return {
@@ -146,7 +143,7 @@ export default class KeyManager {
                         return { stopPropagation: false, preventDefault: false };
                     }
                 }
-                MainView.Instance.mainFreeform && CollectionDockingView.Instance.AddRightSplit(MainView.Instance.mainFreeform, undefined);
+                MainView.Instance.mainFreeform && CollectionDockingView.AddRightSplit(MainView.Instance.mainFreeform, undefined);
                 break;
             case "arrowleft":
                 if (document.activeElement) {
@@ -154,7 +151,7 @@ export default class KeyManager {
                         return { stopPropagation: false, preventDefault: false };
                     }
                 }
-                MainView.Instance.mainFreeform && CollectionDockingView.Instance.CloseRightSplit(MainView.Instance.mainFreeform);
+                MainView.Instance.mainFreeform && CollectionDockingView.CloseRightSplit(MainView.Instance.mainFreeform);
                 break;
             case "backspace":
                 if (document.activeElement) {
@@ -168,7 +165,7 @@ export default class KeyManager {
                 break;
             case "o":
                 let target = SelectionManager.SelectedDocuments()[0];
-                target && target.fullScreenClicked();
+                target && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(target);
                 break;
             case "r":
                 preventDefault = false;
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 1365974dd..f16eeb402 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -2,6 +2,7 @@
 
 .inkingCanvas {
     opacity: 0.99;
+    touch-action: none;
 
     .jsx-parser {
         position: absolute;
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 147418400..a10df0e75 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -51,8 +51,17 @@ export class InkingControl extends React.Component {
             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 oldColor = StrCast(targetDoc.backgroundColor);
-                if (view.props.ContainingCollectionView && view.props.ContainingCollectionView.props.Document.colorPalette) {
-                    let cp = Cast(view.props.ContainingCollectionView.props.Document.colorPalette, listSpec("string")) as string[];
+                let matchedColor = this._selectedColor;
+                const cvd = view.props.ContainingCollectionDoc;
+                let ruleProvider = view.props.ruleProvider;
+                if (cvd) {
+                    if (!cvd.colorPalette) {
+                        let defaultPalette = ["rg(114,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+                            "rgb(209,150,226)", "rgb(127,235,144)", "rgb(252,188,189)", "rgb(247,175,81)",];
+                        let colorPalette = Cast(cvd.colorPalette, listSpec("string"));
+                        if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette);
+                    }
+                    let cp = Cast(cvd.colorPalette, listSpec("string")) as string[];
                     let closest = 0;
                     let dist = 10000000;
                     let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
@@ -65,22 +74,13 @@ export class InkingControl extends React.Component {
                         }
                     }
                     cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")";
-                    view.props.ContainingCollectionView.props.Document.colorPalette = new List(cp);
-                    targetDoc.backgroundColor = cp[closest];
-                } else {
-                    targetDoc.backgroundColor = this._selectedColor;
-                }
-                if (view.props.Document.heading) {
-                    let cv = view.props.ContainingCollectionView;
-                    let ruleProvider = cv && (Cast(cv.props.Document.ruleProvider, Doc) as Doc);
-                    let parback = cv && StrCast(cv.props.Document.backgroundColor);
-                    cv && parback && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb));
-                    // if (parback && cv && parback.indexOf("rgb") !== -1) {
-                    //     let parcol = Utils.fromRGBAstr(parback);
-                    //     let hsl = Utils.RGBToHSL(parcol.r, parcol.g, parcol.b);
-                    //     cv && ((ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = color.hsl.s - hsl.s);
-                    // }
+                    cvd.colorPalette = new List(cp);
+                    matchedColor = cp[closest];
+                    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);
+
                 return {
                     target: targetDoc,
                     previous: oldColor
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index c3a2cb214..335cc609f 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -2,7 +2,7 @@ import { action, observable, reaction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import "normalize.css";
 import * as React from 'react';
-import { Doc, DocListCast } from '../../new_fields/Doc';
+import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
 import { BoolCast } from '../../new_fields/Types';
 import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils';
 import { DragManager } from '../util/DragManager';
@@ -72,14 +72,13 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
         if (this._textTargetDiv) {
             this._textTargetDiv.style.color = this._textColor;
         }
-        this._textAutoHeight = autoHeight;
         this.TextFieldKey = textFieldKey!;
         let txf = tx ? tx : () => Transform.Identity();
         this._textXf = txf;
         this._textTargetDiv = div;
         this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
         if (div) {
-            this._textBottom = div.parentElement && div.parentElement.style.bottom ? true : false;
+            this._textBottom = div.parentElement && getComputedStyle(div.parentElement).top !== "0px" ? true : false;
             this._textColor = (getComputedStyle(div) as any).color;
             div.style.color = "transparent";
         }
@@ -103,10 +102,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
         if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) {
             document.removeEventListener("pointermove", this.textBoxMove);
             document.removeEventListener('pointerup', this.textBoxUp);
-            let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document], [FormattedTextBox.InputBoxOverlay.props.DataDoc]);
+            let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document]);
             const [left, top] = this._textXf().inverse().transformPoint(0, 0);
-            dragData.xOffset = e.clientX - left;
-            dragData.yOffset = e.clientY - top;
+            dragData.offset = [e.clientX - left, e.clientY - top];
             DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, {
                 handlers: {
                     dragComplete: action(emptyFunction),
@@ -120,10 +118,8 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
         document.removeEventListener('pointerup', this.textBoxUp);
     }
 
-    addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
-        if (true) { // location === "onRight") { need to figure out stack to add "inTab"
-            CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
-        }
+    addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
+        return this._textBox && this._textBox.props.addDocTab(doc, dataDoc, location) ? true : false;
     }
     render() {
         this.TextDoc; this.TextDataDoc;
@@ -143,9 +139,10 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                                 Document={FormattedTextBox.InputBoxOverlay.props.Document}
                                 DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
                                 onClick={undefined}
-                                ChromeHeight={this.ChromeHeight}
-                                isSelected={returnTrue} select={emptyFunction} renderDepth={0}
-                                ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
+                                ruleProvider={this._textBox ? this._textBox.props.ruleProvider : undefined}
+                                ChromeHeight={this.ChromeHeight} isSelected={returnTrue} select={emptyFunction} renderDepth={0}
+                                ContainingCollectionDoc={undefined} ContainingCollectionView={undefined}
+                                whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
                                 ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction}
                                 pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
                         </div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2111dba0a..b1f753635 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -256,37 +256,46 @@ export class MainView extends React.Component {
     initAuthenticationRouters = async () => {
         // Load the user's active workspace, or create a new one if initial session after signup
         let received = CurrentUserUtils.MainDocId;
-        if (received && !this.userDoc) {
-            reaction(
-                () => CurrentUserUtils.GuestTarget,
-                target => target && this.createNewWorkspace(),
-                { fireImmediately: true }
-            );
-        } else {
-            if (received && this.urlState.sharing) {
+        if (received) {
+            if (!this.userDoc) {
                 reaction(
-                    () => {
-                        let docking = CollectionDockingView.Instance;
-                        return docking && docking.initialized;
-                    },
-                    initialized => {
-                        if (initialized && received) {
-                            DocServer.GetRefField(received).then(field => {
-                                if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
-                                    CollectionDockingView.Instance.AddRightSplit(field, undefined);
-                                    DocumentManager.Instance.jumpToDocument(field, true, undefined, undefined, undefined, undefined);
-                                }
-                            });
-                        }
-                    },
+                    () => CurrentUserUtils.GuestTarget,
+                    target => target && this.createNewWorkspace(),
+                    { fireImmediately: true }
                 );
-            }
-            let doc: Opt<Doc>;
-            if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
-                this.openWorkspace(doc);
+            } else if (this.urlState.sharing) {
+                if (received && this.urlState.sharing) {
+                    reaction(
+                        () => {
+                            let docking = CollectionDockingView.Instance;
+                            return docking && docking.initialized;
+                        },
+                        initialized => {
+                            if (initialized && received) {
+                                DocServer.GetRefField(received).then(field => {
+                                    if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
+                                        CollectionDockingView.AddRightSplit(field, undefined);
+                                        DocumentManager.Instance.jumpToDocument(field, true, undefined, undefined, undefined, undefined);
+                                    }
+                                });
+                            }
+                        },
+                    );
+                }
+                let doc: Opt<Doc>;
+                if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
+                    this.openWorkspace(doc);
+                } else {
+                    this.createNewWorkspace();
+                }
             } else {
-                this.createNewWorkspace();
+                DocServer.GetRefField(received).then(field => {
+                    field instanceof Doc ? this.openWorkspace(field) :
+                        this.createNewWorkspace(received);
+                });
             }
+        } else {
+            this.createNewWorkspace();
         }
     }
 
@@ -362,6 +371,7 @@ export class MainView extends React.Component {
                 }
             }
         }, 100);
+        return true;
     }
 
     onDrop = (e: React.DragEvent<HTMLDivElement>) => {
@@ -393,9 +403,10 @@ export class MainView extends React.Component {
                         <DocumentView Document={mainCont}
                             DataDoc={undefined}
                             addDocument={undefined}
-                            addDocTab={emptyFunction}
+                            addDocTab={this.addDocTabFunc}
                             pinToPres={emptyFunction}
                             onClick={undefined}
+                            ruleProvider={undefined}
                             removeDocument={undefined}
                             ScreenToLocalTransform={Transform.Identity}
                             ContentScaling={returnOne}
@@ -408,6 +419,7 @@ export class MainView extends React.Component {
                             whenActiveChanged={emptyFunction}
                             bringToFront={emptyFunction}
                             ContainingCollectionView={undefined}
+                            ContainingCollectionDoc={undefined}
                             zoomToScale={emptyFunction}
                             getScale={returnOne}
                         />}
@@ -440,11 +452,15 @@ export class MainView extends React.Component {
         document.removeEventListener("pointerup", this.onPointerUp);
     }
     flyoutWidthFunc = () => this.flyoutWidth;
-    addDocTabFunc = (doc: Doc) => {
+    addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string) => {
+        if (where === "close") {
+            return CollectionDockingView.CloseRightSplit(doc);
+        }
         if (doc.dockingConfig) {
             this.openWorkspace(doc);
+            return true;
         } else {
-            CollectionDockingView.Instance.AddRightSplit(doc, undefined);
+            return CollectionDockingView.AddRightSplit(doc, undefined);
         }
     }
     @computed
@@ -460,6 +476,7 @@ export class MainView extends React.Component {
             addDocTab={this.addDocTabFunc}
             pinToPres={emptyFunction}
             removeDocument={undefined}
+            ruleProvider={undefined}
             onClick={undefined}
             ScreenToLocalTransform={Transform.Identity}
             ContentScaling={returnOne}
@@ -472,6 +489,7 @@ export class MainView extends React.Component {
             whenActiveChanged={emptyFunction}
             bringToFront={emptyFunction}
             ContainingCollectionView={undefined}
+            ContainingCollectionDoc={undefined}
             zoomToScale={emptyFunction}
             getScale={returnOne}>
         </DocumentView>;
@@ -643,7 +661,7 @@ export class MainView extends React.Component {
         let next = () => PresBox.CurrentPresentation.next();
         let back = () => PresBox.CurrentPresentation.back();
         let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres();
-        let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document); });
+        let closePresMode = action(() => { PresBox.CurrentPresentation.presMode = false; this.addDocTabFunc(PresBox.CurrentPresentation.props.Document, undefined, "close"); });
         return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
     }
 
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 7a1fac4c8..bb6dd5076 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -175,6 +175,7 @@ export class OverlayView extends React.Component {
                     ChromeHeight={returnZero}
                     isSelected={returnFalse}
                     select={emptyFunction}
+                    ruleProvider={undefined}
                     layoutKey={"layout"}
                     bringToFront={emptyFunction}
                     addDocument={undefined}
@@ -188,9 +189,10 @@ export class OverlayView extends React.Component {
                     whenActiveChanged={emptyFunction}
                     focus={emptyFunction}
                     backgroundColor={returnEmptyString}
-                    addDocTab={emptyFunction}
+                    addDocTab={returnFalse}
                     pinToPres={emptyFunction}
                     ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
                     zoomToScale={emptyFunction}
                     getScale={returnOne} />
             </div>;
diff --git a/src/client/views/ScriptBox.tsx b/src/client/views/ScriptBox.tsx
index 8f06cf770..8ef9f3be6 100644
--- a/src/client/views/ScriptBox.tsx
+++ b/src/client/views/ScriptBox.tsx
@@ -98,7 +98,7 @@ export class ScriptBox extends React.Component<ScriptBoxProps> {
         // tslint:disable-next-line: no-unnecessary-callback-wrapper
         let params: string[] = [];
         let setParams = (p: string[]) => params.splice(0, params.length, ...p);
-        let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={() => overlayDisposer()} onSave={(text, onError) => {
+        let scriptingBox = <ScriptBox initialText={originalText} setParams={setParams} onCancel={overlayDisposer} onSave={(text, onError) => {
             if (prewrapper) {
                 text = prewrapper + text + (postwrapper ? postwrapper : "");
             }
diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx
index e05195ca0..1eb380e0b 100644
--- a/src/client/views/ScriptingRepl.tsx
+++ b/src/client/views/ScriptingRepl.tsx
@@ -135,19 +135,17 @@ export class ScriptingRepl extends React.Component {
                     this.commands.push({ command: this.commandString, result: script.errors });
                     return;
                 }
-                const result = script.run({ args: this.args });
-                if (!result.success) {
-                    this.commands.push({ command: this.commandString, result: result.error.toString() });
-                    return;
-                }
-                this.commands.push({ command: this.commandString, result: result.result });
-                this.commandsHistory.push(this.commandString);
+                const result = script.run({ args: this.args }, e => this.commands.push({ command: this.commandString, result: e.toString() }));
+                if (result.success) {
+                    this.commands.push({ command: this.commandString, result: result.result });
+                    this.commandsHistory.push(this.commandString);
 
-                this.maybeScrollToBottom();
+                    this.maybeScrollToBottom();
 
-                this.commandString = "";
-                this.commandBuffer = "";
-                this.historyIndex = -1;
+                    this.commandString = "";
+                    this.commandBuffer = "";
+                    this.historyIndex = -1;
+                }
                 break;
             }
             case "ArrowUp": {
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 4e371ffd1..9e5e62e03 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -1,6 +1,5 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
-import { DocumentType } from "../documents/DocumentTypes";
 import { DocumentManager } from "../util/DocumentManager";
 import { DragManager } from "../util/DragManager";
 import { SelectionManager } from "../util/SelectionManager";
@@ -9,6 +8,7 @@ import './DocumentDecorations.scss';
 import { DocumentView } from "./nodes/DocumentView";
 import { Template, Templates } from "./Templates";
 import React = require("react");
+import { Doc } from "../../new_fields/Doc";
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -29,12 +29,12 @@ class TemplateToggle extends React.Component<{ template: Template, checked: bool
     }
 }
 @observer
-class ChromeToggle extends React.Component<{ checked: boolean, toggle: (event: React.ChangeEvent<HTMLInputElement>) => void }> {
+class OtherToggle extends React.Component<{ checked: boolean, name: string, toggle: (event: React.ChangeEvent<HTMLInputElement>) => void }> {
     render() {
         return (
             <li className="chromeToggle">
                 <input type="checkbox" checked={this.props.checked} onChange={(event) => this.props.toggle(event)} />
-                Chrome
+                {this.props.name}
             </li>
         );
     }
@@ -50,33 +50,23 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     @observable private _hidden: boolean = true;
     dragRef = React.createRef<HTMLUListElement>();
 
-    constructor(props: TemplateMenuProps) {
-        super(props);
+    toggleCustom = (e: React.ChangeEvent<HTMLInputElement>): void => {
+        this.props.docs.map(dv => dv.setCustomView(e.target.checked));
     }
 
-    toggleCustom = (e: React.MouseEvent): void => {
-        this.props.docs.map(dv => {
-            if (dv.Document.type !== DocumentType.COL && dv.Document.type !== DocumentType.TEMPLATE) {
-                DocumentView.makeCustomViewClicked(dv.props.Document);
-            } else if (dv.Document.nativeLayout) {
-                DocumentView.makeNativeViewClicked(dv.props.Document);
-            }
-        });
-    }
-
-    toggleFloat = (e: React.MouseEvent): void => {
+    toggleFloat = (e: React.ChangeEvent<HTMLInputElement>): void => {
         SelectionManager.DeselectAll();
         let topDocView = this.props.docs[0];
         let topDoc = topDocView.props.Document;
         let xf = topDocView.props.ScreenToLocalTransform();
-        let ex = e.clientX;
-        let ey = e.clientY;
+        let ex = e.target.clientLeft;
+        let ey = e.target.clientTop;
         undoBatch(action(() => topDoc.z = topDoc.z ? 0 : 1))();
-        if (!topDoc.z) {
+        if (e.target.checked) {
             setTimeout(() => {
                 let newDocView = DocumentManager.Instance.getDocumentView(topDoc);
                 if (newDocView) {
-                    let de = new DragManager.DocumentDragData([topDoc], [undefined]);
+                    let de = new DragManager.DocumentDragData([topDoc]);
                     de.moveDocument = topDocView.props.moveDocument;
                     let xf = newDocView.ContentDiv!.getBoundingClientRect();
                     DragManager.StartDocumentDrag([newDocView.ContentDiv!], de, ex, ey, {
@@ -99,16 +89,23 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     @action
     toggleTemplate = (event: React.ChangeEvent<HTMLInputElement>, template: Template): void => {
         if (event.target.checked) {
-            this.props.docs.map(d => d.props.Document["show" + template.Name] = template.Name.toLowerCase());
+            this.props.docs.map(d => d.Document["show" + template.Name] = template.Name.toLowerCase());
         } else {
-            this.props.docs.map(d => d.props.Document["show" + template.Name] = undefined);
+            this.props.docs.map(d => d.Document["show" + template.Name] = "");
         }
     }
 
     @undoBatch
     @action
     clearTemplates = (event: React.MouseEvent) => {
-        Templates.TemplateList.map(template => this.props.docs.map(d => d.props.Document["show" + template.Name] = false));
+        Templates.TemplateList.forEach(template => this.props.docs.forEach(d => d.Document["show" + template.Name] = undefined));
+        ["backgroundColor", "borderRounding", "width", "height"].forEach(field => this.props.docs.forEach(d => {
+            if (d.Document.isTemplate && d.props.DataDoc) {
+                d.Document[field] = undefined;
+            } else if (d.Document["default" + field[0].toUpperCase() + field.slice(1)] !== undefined) {
+                d.Document[field] = Doc.GetProto(d.Document)[field] = undefined;
+            }
+        }));
     }
 
     @action
@@ -119,22 +116,26 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     @undoBatch
     @action
     toggleChrome = (): void => {
-        this.props.docs.map(dv => dv.layoutDoc.chromeStatus = (dv.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"));
+        this.props.docs.map(dv => {
+            let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document;
+            layout.chromeStatus = (layout.chromeStatus !== "disabled" ? "disabled" : "enabled");
+        });
     }
 
     render() {
+        let layout = this.props.docs[0].Document.layout instanceof Doc ? this.props.docs[0].Document.layout : this.props.docs[0].Document;
         let templateMenu: Array<JSX.Element> = [];
         this.props.templates.forEach((checked, template) =>
             templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
-        templateMenu.push(<ChromeToggle key={"chrome"} checked={this.props.docs[0].Document.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
+        templateMenu.push(<OtherToggle key={"float"} name={"Float"} checked={this.props.docs[0].Document.z ? true : false} toggle={this.toggleFloat} />);
+        templateMenu.push(<OtherToggle key={"custom"} name={"Custom"} checked={typeof this.props.docs[0].Document.layout === "string" ? false : true} toggle={this.toggleCustom} />);
+        templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
         return (
             <div className="templating-menu" >
                 <div title="Template Options" className="templating-button" onClick={() => this.toggleTemplateActivity()}>+</div>
                 <ul id="template-list" ref={this.dragRef} style={{ display: this._hidden ? "none" : "block" }}>
                     {templateMenu}
-                    <button onClick={this.toggleCustom}>{this.props.docs[0].Document.nativeLayout ? "Native" : "Custom"}</button>
-                    <button onClick={this.toggleFloat}>Float</button>
-                    {/* <button onClick={this.clearTemplates}>Clear</button> */}
+                    {<button onClick={this.clearTemplates}>Restore Defaults</button>}
                 </ul>
             </div>
         );
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 93eaab453..818a41009 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -12,7 +12,6 @@ import { ContextMenu } from '../ContextMenu';
 import { FieldViewProps } from '../nodes/FieldView';
 import './CollectionBaseView.scss';
 import { DateField } from '../../../new_fields/DateField';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { ImageField } from '../../../new_fields/URLField';
 
 export enum CollectionViewType {
@@ -81,12 +80,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         }
     }
 
-    @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc ? this.props.DataDoc : this.props.Document : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+    @computed get dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
     @computed get dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; }
 
     active = (): boolean => {
         var isSelected = this.props.isSelected();
-        return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0 || BoolCast(this.props.Document.excludeFromLibrary);
+        return isSelected || BoolCast(this.props.Document.forceActive) || this._isChildActive || this.props.renderDepth === 0;
     }
 
     //TODO should this be observable?
@@ -96,7 +95,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         this.props.whenActiveChanged(isActive);
     }
 
-    @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+    @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
 
     @action.bound
     addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
@@ -105,7 +104,6 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         if (this.props.fieldExt) { // bcz: fieldExt !== undefined means this is an overlay layer
             Doc.GetProto(doc).annotationOn = this.props.Document;
         }
-        allowDuplicates = true;
         let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
         let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
         const value = Cast(targetDataDoc[targetField], listSpec(Doc));
@@ -128,7 +126,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         let targetDataDoc = this.props.fieldExt || this.props.Document.isTemplate ? this.extensionDoc : this.props.Document;
         let targetField = (this.props.fieldExt || this.props.Document.isTemplate) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
         let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
-        let index = value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
+        let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
+        index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
         PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
             annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined));
 
@@ -142,17 +141,16 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         return false;
     }
 
+    // this is called with the document that was dragged and the collection to move it into.
+    // if the target collection is the same as this collection, then the move will be allowed.
+    // otherwise, the document being moved must be able to be removed from its container before
+    // moving it into the target.  
     @action.bound
     moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
-        let self = this;
-        let targetDataDoc = this.props.Document;
-        if (Doc.AreProtosEqual(targetDataDoc, targetCollection)) {
+        if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
             return true;
         }
-        if (this.removeDocument(doc)) {
-            return addDocument(doc);
-        }
-        return false;
+        return this.removeDocument(doc) ? addDocument(doc) : false;
     }
 
     showIsTagged = () => {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index f184a3944..ef2681410 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,36 +1,35 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faFile } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import 'golden-layout/src/css/goldenlayout-base.css';
 import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, Lambda, observable, reaction, trace, computed, runInAction } from "mobx";
+import { action, Lambda, observable, reaction, computed, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as ReactDOM from 'react-dom';
 import Measure from "react-measure";
 import * as GoldenLayout from "../../../client/goldenLayout";
+import { DateField } from '../../../new_fields/DateField';
 import { Doc, DocListCast, Field, Opt } from "../../../new_fields/Doc";
 import { Id } from '../../../new_fields/FieldSymbols';
+import { List } from '../../../new_fields/List';
 import { FieldId } from "../../../new_fields/RefField";
 import { listSpec } from "../../../new_fields/Schema";
-import { Cast, NumCast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils, returnOne, returnEmptyString } from "../../../Utils";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from "../../../Utils";
 import { DocServer } from "../../DocServer";
+import { Docs } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
 import { SelectionManager } from '../../util/SelectionManager';
 import { Transform } from '../../util/Transform';
-import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { undoBatch } from "../../util/UndoManager";
+import { MainView } from '../MainView';
 import { DocumentView } from "../nodes/DocumentView";
 import "./CollectionDockingView.scss";
 import { SubCollectionViewProps } from "./CollectionSubView";
-import { ParentDocSelector } from './ParentDocumentSelector';
 import React = require("react");
-import { MainView } from '../MainView';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faFile, faUnlockAlt } from '@fortawesome/free-solid-svg-icons';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
-import { Docs } from '../../documents/Documents';
-import { DateField } from '../../../new_fields/DateField';
-import { List } from '../../../new_fields/List';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { ButtonSelector } from './ParentDocumentSelector';
 library.add(faFile);
 
 @observer
@@ -44,7 +43,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             width: width,
             props: {
                 documentId: document[Id],
-                dataDocumentId: dataDoc ? dataDoc[Id] : ""
+                dataDocumentId: dataDoc && dataDoc[Id] !== document[Id] ? dataDoc[Id] : ""
                 //collectionDockingView: CollectionDockingView.Instance
             }
         };
@@ -63,24 +62,22 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     constructor(props: SubCollectionViewProps) {
         super(props);
-        if (props.addDocTab === emptyFunction) {
-            runInAction(() => CollectionDockingView.Instance = this);
-        }
+        !CollectionDockingView.Instance && (CollectionDockingView.Instance = this);
         //Why is this here?
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
     }
     hack: boolean = false;
     undohack: any = null;
-    public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs: (Doc | undefined)[] = []) {
+    public StartOtherDrag(e: any, dragDocs: Doc[]) {
         let config: any;
         if (dragDocs.length === 1) {
-            config = CollectionDockingView.makeDocumentConfig(dragDocs[0], dragDataDocs[0]);
+            config = CollectionDockingView.makeDocumentConfig(dragDocs[0], undefined);
         } else {
             config = {
                 type: 'row',
                 content: dragDocs.map((doc, i) => {
-                    CollectionDockingView.makeDocumentConfig(doc, dragDataDocs[i]);
+                    CollectionDockingView.makeDocumentConfig(doc, undefined);
                 })
             };
         }
@@ -90,18 +87,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             dragSource.destroy();
         });
         dragSource._dragListener.onMouseDown(e);
-        // dragSource.destroy();
-        // this.hack = true;
-        // this.undohack = UndoManager.StartBatch("goldenDrag");
-        // dragDocs.map((dragDoc, i) =>
-        //     this.AddRightSplit(dragDoc, dragDataDocs[i], true).contentItems[0].tab._dragListener.
-        //         onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
     }
 
+    @undoBatch
     @action
     public OpenFullScreen(docView: DocumentView) {
         let document = Doc.MakeAlias(docView.props.Document);
-        let dataDoc = docView.dataDoc;
+        let dataDoc = docView.props.DataDoc;
         let newItemStackConfig = {
             type: 'stack',
             content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
@@ -113,6 +105,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         this._maximizedSrc = docView;
         this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig());
         this.stateChanged();
+        SelectionManager.DeselectAll();
     }
 
     public CloseFullScreen = () => {
@@ -131,21 +124,25 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     @undoBatch
     @action
-    public CloseRightSplit = (document: Doc): boolean => {
+    public static CloseRightSplit(document: Doc): boolean {
+        if (!CollectionDockingView.Instance) return false;
+        let instance = CollectionDockingView.Instance;
         let retVal = false;
-        if (this._goldenLayout.root.contentItems[0].isRow) {
-            retVal = Array.from(this._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
+        if (instance._goldenLayout.root.contentItems[0].isRow) {
+            retVal = Array.from(instance._goldenLayout.root.contentItems[0].contentItems).some((child: any) => {
                 if (child.contentItems.length === 1 && child.contentItems[0].config.component === "DocumentFrameRenderer" &&
+                    DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId) &&
                     Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(child.contentItems[0].config.props.documentId)!.Document, document)) {
                     child.contentItems[0].remove();
-                    this.layoutChanged(document);
+                    instance.layoutChanged(document);
                     return true;
                 } else {
                     Array.from(child.contentItems).filter((tab: any) => tab.config.component === "DocumentFrameRenderer").some((tab: any, j: number) => {
-                        if (Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
+                        if (DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId) &&
+                            Doc.AreProtosEqual(DocumentManager.Instance.getDocumentViewById(tab.config.props.documentId)!.Document, document)) {
                             child.contentItems[j].remove();
                             child.config.activeItemIndex = Math.max(child.contentItems.length - 1, 0);
-                            let docs = Cast(this.props.Document.data, listSpec(Doc));
+                            let docs = Cast(instance.props.Document.data, listSpec(Doc));
                             docs && docs.indexOf(document) !== -1 && docs.splice(docs.indexOf(document), 1);
                             return true;
                         }
@@ -156,7 +153,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             });
         }
         if (retVal) {
-            this.stateChanged();
+            instance.stateChanged();
         }
         return retVal;
     }
@@ -181,9 +178,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     //
     //  Creates a vertical split on the right side of the docking view, and then adds the Document to that split
     //
+    @undoBatch
     @action
-    public AddRightSplit = (document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) => {
-        let docs = Cast(this.props.Document.data, listSpec(Doc));
+    public static AddRightSplit(document: Doc, dataDoc: Doc | undefined, minimize: boolean = false) {
+        if (!CollectionDockingView.Instance) return false;
+        let instance = CollectionDockingView.Instance;
+        let docs = Cast(instance.props.Document.data, listSpec(Doc));
         if (docs) {
             docs.push(document);
         }
@@ -192,15 +192,15 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
         };
 
-        var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
+        var newContentItem = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout);
 
-        if (this._goldenLayout.root.contentItems.length === 0) {
-            this._goldenLayout.root.addChild(newContentItem);
-        } else if (this._goldenLayout.root.contentItems[0].isRow) {
-            this._goldenLayout.root.contentItems[0].addChild(newContentItem);
+        if (instance._goldenLayout.root.contentItems.length === 0) {
+            instance._goldenLayout.root.addChild(newContentItem);
+        } else if (instance._goldenLayout.root.contentItems[0].isRow) {
+            instance._goldenLayout.root.contentItems[0].addChild(newContentItem);
         } else {
-            var collayout = this._goldenLayout.root.contentItems[0];
-            var newRow = collayout.layoutManager.createContentItem({ type: "row" }, this._goldenLayout);
+            var collayout = instance._goldenLayout.root.contentItems[0];
+            var newRow = collayout.layoutManager.createContentItem({ type: "row" }, instance._goldenLayout);
             collayout.parent.replaceChild(collayout, newRow);
 
             newRow.addChild(newContentItem, undefined, true);
@@ -215,10 +215,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             // newContentItem.config.height = 10;
         }
         newContentItem.callDownwards('_$init');
-        this.layoutChanged();
-
-        return newContentItem;
+        instance.layoutChanged();
+        return true;
     }
+
+    @undoBatch
     @action
     public AddTab = (stack: any, document: Doc, dataDocument: Doc | undefined) => {
         Doc.GetProto(document).lastOpened = new DateField;
@@ -245,6 +246,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             stack.addChild(docContentConfig, undefined);
         }
         this.layoutChanged();
+        return true;
     }
 
     setupGoldenLayout() {
@@ -404,6 +406,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 dragSpan.style.bottom = "6px";
                 dragSpan.style.paddingLeft = "4px";
                 dragSpan.style.paddingRight = "2px";
+                let gearSpan = document.createElement("span");
+                gearSpan.style.position = "relative";
+                gearSpan.style.paddingLeft = "0px";
+                gearSpan.style.paddingRight = "12px";
                 let upDiv = document.createElement("span");
                 const stack = tab.contentItem.parent;
                 // shifts the focus to this tab when another tab is dragged over it
@@ -419,14 +425,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     e => {
                         e.preventDefault();
                         e.stopPropagation();
-                        DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc], [dataDoc]), e.clientX, e.clientY, {
+                        DragManager.StartDocumentDrag([dragSpan], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, {
                             handlers: { dragComplete: emptyFunction },
                             hideSource: false
                         });
                     }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
-                ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv);
-                tab.reactComponents = [dragSpan, upDiv];
+                ReactDOM.render(<ButtonSelector Document={doc} Stack={stack} />, gearSpan);
+                // ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, data, where) => {
+                //     where === "onRight" ? CollectionDockingView.AddRightSplit(doc, dataDoc) : CollectionDockingView.Instance.AddTab(stack, doc, dataDoc);
+                //     return true;
+                // }} />, upDiv);
+                tab.reactComponents = [dragSpan, gearSpan, upDiv];
                 tab.element.append(dragSpan);
+                tab.element.append(gearSpan);
                 tab.element.append(upDiv);
                 tab.reactionDisposer = reaction(() => [doc.title, Doc.IsBrushedDegree(doc)], () => {
                     tab.titleElement[0].textContent = doc.title, { fireImmediately: true };
@@ -538,11 +549,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     @observable private _isActive: boolean = false;
 
     get _stack(): any {
-        let parent = (this.props as any).glContainer.parent.parent;
-        if (this._document && this._document.excludeFromLibrary && parent.parent && parent.parent.contentItems.length > 1) {
-            return parent.parent.contentItems[1];
-        }
-        return parent;
+        return (this.props as any).glContainer.parent.parent;
     }
     constructor(props: any) {
         super(props);
@@ -616,17 +623,18 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
         return Transform.Identity();
     }
-    get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; }
+    get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; }
 
-    addDocTab = (doc: Doc, dataDoc: Doc | undefined, location: string) => {
+    addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
         if (doc.dockingConfig) {
             MainView.Instance.openWorkspace(doc);
+            return true;
         } else if (location === "onRight") {
-            CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
+            return CollectionDockingView.AddRightSplit(doc, dataDoc);
         } else if (location === "close") {
-            CollectionDockingView.Instance.CloseRightSplit(doc);
+            return CollectionDockingView.CloseRightSplit(doc);
         } else {
-            CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
+            return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
         }
     }
     @computed get docView() {
@@ -640,6 +648,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             bringToFront={emptyFunction}
             addDocument={undefined}
             removeDocument={undefined}
+            ruleProvider={undefined}
             ContentScaling={this.contentScaling}
             PanelWidth={this.panelWidth}
             PanelHeight={this.panelHeight}
@@ -652,6 +661,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             addDocTab={this.addDocTab}
             pinToPres={this.PinDoc}
             ContainingCollectionView={undefined}
+            ContainingCollectionDoc={undefined}
             zoomToScale={emptyFunction}
             getScale={returnOne} />;
     }
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index c59107b53..4dac27e60 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -39,7 +39,7 @@ export interface CellProps {
     Document: Doc;
     fieldKey: string;
     renderDepth: number;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     isFocused: boolean;
@@ -149,7 +149,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             DataDoc: this.props.rowProps.original,
             fieldKey: this.props.rowProps.column.id as string,
             fieldExt: "",
+            ruleProvider: undefined,
             ContainingCollectionView: this.props.CollectionView,
+            ContainingCollectionDoc: this.props.CollectionView.props.Document,
             isSelected: returnFalse,
             select: emptyFunction,
             renderDepth: this.props.renderDepth + 1,
@@ -171,7 +173,8 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         let onItemDown = (e: React.PointerEvent) => {
             if (fieldIsDoc) {
                 SetupDrag(this._focusRef, () => this._document[props.fieldKey] instanceof Doc ? this._document[props.fieldKey] : this._document,
-                    this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument, this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
+                    this._document[props.fieldKey] instanceof Doc ? (doc: Doc, target: Doc, addDoc: (newDoc: Doc) => any) => addDoc(doc) : this.props.moveDocument,
+                    this._document[props.fieldKey] instanceof Doc ? "alias" : this.props.Document.schemaDoc ? "copy" : undefined)(e);
             }
         };
         let onPointerEnter = (e: React.PointerEvent): void => {
@@ -235,13 +238,11 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                                 return this.applyToDoc(props.Document, this.props.row, this.props.col, script.run);
                             }}
                             OnFillDown={async (value: string) => {
-                                let script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
-                                if (!script.compiled) {
-                                    return;
+                                const script = CompileScript(value, { requiredType: type, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                if (script.compiled) {
+                                    DocListCast(this.props.Document[this.props.fieldKey]).
+                                        forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, script.run));
                                 }
-                                const run = script.run;
-                                const val = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
-                                val && val.forEach((doc, i) => this.applyToDoc(doc, i, this.props.col, run));
                             }}
                         />
                     </div >
diff --git a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
index ec40043cc..39abc41ec 100644
--- a/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
+++ b/src/client/views/collections/CollectionSchemaMovableTableHOC.tsx
@@ -201,12 +201,8 @@ export class MovableRow extends React.Component<MovableRowProps> {
     @action
     move: DragManager.MoveFunction = (doc: Doc, target: Doc, addDoc) => {
         let targetView = DocumentManager.Instance.getDocumentView(target);
-        if (targetView) {
-            let targetContainingColl = targetView.props.ContainingCollectionView; //.props.ContainingCollectionView.props.Document;
-            if (targetContainingColl) {
-                let targetContCollDoc = targetContainingColl.props.Document;
-                return doc !== target && doc !== targetContCollDoc && this.props.removeDoc(doc) && addDoc(doc);
-            }
+        if (targetView && targetView.props.ContainingCollectionDoc) {
+            return doc !== target && doc !== targetView.props.ContainingCollectionDoc && this.props.removeDoc(doc) && addDoc(doc);
         }
         return doc !== target && this.props.removeDoc(doc) && addDoc(doc);
     }
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 9d83aa6c1..7bd2a1971 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -14,7 +14,7 @@ import { listSpec } from "../../../new_fields/Schema";
 import { Docs, DocumentOptions } from "../../documents/Documents";
 import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
 import { Gateway } from "../../northstar/manager/Gateway";
-import { SetupDrag, DragManager } from "../../util/DragManager";
+import { DragManager } from "../../util/DragManager";
 import { CompileScript, ts, Transformer } from "../../util/Scripting";
 import { Transform } from "../../util/Transform";
 import { COLLECTION_BORDER_WIDTH } from '../../views/globalCssVariables.scss';
@@ -32,6 +32,7 @@ import { CellProps, CollectionSchemaCell, CollectionSchemaNumberCell, Collection
 import { MovableColumn, MovableRow } from "./CollectionSchemaMovableTableHOC";
 import { ComputedField, ScriptField } from "../../../new_fields/ScriptField";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import { DocumentType } from "../../documents/DocumentTypes";
 
 
 library.add(faCog, faPlus, faSortUp, faSortDown);
@@ -161,6 +162,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
                 DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
                 childDocs={this.childDocs}
                 renderDepth={this.props.renderDepth}
+                ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
                 width={this.previewWidth}
                 height={this.previewHeight}
                 getTransform={this.getPreviewTransform}
@@ -194,6 +196,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
                 childDocs={this.childDocs}
                 CollectionView={this.props.CollectionView}
                 ContainingCollectionView={this.props.ContainingCollectionView}
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
                 fieldKey={this.props.fieldKey}
                 renderDepth={this.props.renderDepth}
                 moveDocument={this.props.moveDocument}
@@ -245,6 +248,7 @@ export interface SchemaTableProps {
     childDocs?: Doc[];
     CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+    ContainingCollectionDoc: Opt<Doc>;
     fieldKey: string;
     renderDepth: number;
     deleteDocument: (document: Doc) => boolean;
@@ -252,7 +256,7 @@ export interface SchemaTableProps {
     ScreenToLocalTransform: () => Transform;
     active: () => boolean;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     isSelected: () => boolean;
     isFocused: (document: Doc) => boolean;
@@ -901,6 +905,7 @@ interface CollectionSchemaPreviewProps {
     fitToBox?: boolean;
     width: () => number;
     height: () => number;
+    ruleProvider: Doc | undefined;
     showOverlays?: (doc: Doc) => { title?: string, caption?: string };
     CollectionView?: CollectionView | CollectionPDFView | CollectionVideoView;
     onClick?: ScriptField;
@@ -910,7 +915,7 @@ interface CollectionSchemaPreviewProps {
     removeDocument: (document: Doc) => boolean;
     active: () => boolean;
     whenActiveChanged: (isActive: boolean) => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     setPreviewScript: (script: string) => void;
     previewScript?: string;
@@ -943,13 +948,12 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            let docDrag = de.data;
-            let computed = CompileScript("return this.image_data[0]", { params: { this: "Doc" } });
             this.props.childDocs && this.props.childDocs.map(otherdoc => {
-                let doc = docDrag.draggedDocuments[0];
                 let target = Doc.GetProto(otherdoc);
-                target.layout = target.detailedLayout = Doc.MakeDelegate(doc);
-                computed.compiled && (target.miniLayout = new ComputedField(computed));
+                let layoutNative = Doc.MakeTitled("layoutNative");
+                layoutNative.layout = ComputedField.MakeFunction("this.image_data[0]");
+                target.layoutNative = layoutNative;
+                target.layoutCUstom = target.layout = Doc.MakeDelegate(de.data.draggedDocuments[0]);
             });
             e.stopPropagation();
         }
@@ -995,12 +999,14 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                         Document={this.props.Document}
                         fitToBox={this.props.fitToBox}
                         onClick={this.props.onClick}
+                        ruleProvider={this.props.ruleProvider}
                         showOverlays={this.props.showOverlays}
                         addDocument={this.props.addDocument}
                         removeDocument={this.props.removeDocument}
                         moveDocument={this.props.moveDocument}
                         whenActiveChanged={this.props.whenActiveChanged}
                         ContainingCollectionView={this.props.CollectionView}
+                        ContainingCollectionDoc={this.props.CollectionView && this.props.CollectionView.props.Document}
                         addDocTab={this.props.addDocTab}
                         pinToPres={this.props.pinToPres}
                         parentActive={this.props.active}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 91e10b0ac..ccf131797 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -29,7 +29,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     _masonryGridRef: HTMLDivElement | null = null;
     _draggerRef = React.createRef<HTMLDivElement>();
     _heightDisposer?: IReactionDisposer;
-    _childLayoutDisposer?: IReactionDisposer;
     _sectionFilterDisposer?: IReactionDisposer;
     _docXfs: any[] = [];
     _columnStart: number = 0;
@@ -87,10 +86,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     componentDidMount() {
-        this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
-            async (args) => args[1] instanceof Doc &&
-                this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), undefined)));
-
         // 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 (this.isStackingView && BoolCast(this.props.Document.autoHeight)) {
@@ -115,7 +110,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         );
     }
     componentWillUnmount() {
-        this._childLayoutDisposer && this._childLayoutDisposer();
         this._heightDisposer && this._heightDisposer();
         this._sectionFilterDisposer && this._sectionFilterDisposer();
     }
@@ -134,7 +128,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     @computed get onChildClickHandler() { return ScriptCast(this.Document.onChildClick); }
-    @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : ScriptCast(this.Document.onChildClick); }
+    @computed get onClickHandler() { return ScriptCast(this.Document.onChildClick); }
 
     getDisplayDoc(layoutDoc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
         let height = () => this.getDocHeight(layoutDoc);
@@ -144,6 +138,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             DataDocument={dataDoc}
             showOverlays={this.overlays}
             renderDepth={this.props.renderDepth}
+            ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
             fitToBox={this.props.fitToBox}
             onClick={layoutDoc.isTemplate ? this.onClickHandler : this.onChildClickHandler}
             width={width}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index bc4fe7dd7..b3b7b40dd 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -155,6 +155,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
         let key = StrCast(this.props.parent.props.Document.sectionFilter);
         let newDoc = Docs.Create.TextDocument({ height: 18, width: 200, documentText: "@@@" + value, title: value, autoHeight: true });
         newDoc[key] = this.getValue(this.props.heading);
+        let maxHeading = this.props.docList.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+        let heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3;
+        newDoc.heading = heading;
         return this.props.parent.props.addDocument(newDoc);
     }
 
@@ -175,13 +178,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             let key = StrCast(this.props.parent.props.Document.sectionFilter);
             let value = this.getValue(this._heading);
             value = typeof value === "string" ? `"${value}"` : value;
-            let script = `return doc.${key} === ${value}`;
-            let compiled = CompileScript(script, { params: { doc: Doc.name } });
-            if (compiled.compiled) {
-                let scriptField = new ScriptField(compiled);
-                alias.viewSpecScript = scriptField;
-                let dragData = new DragManager.DocumentDragData([alias], [alias.proto]);
-                DragManager.StartDocumentDrag([this._headerRef.current!], dragData, e.clientX, e.clientY);
+            alias.viewSpecScript = ScriptField.MakeFunction(`doc.${key} === ${value}`, { doc: Doc.name });
+            if (alias.viewSpecScript) {
+                DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY);
             }
 
             e.stopPropagation();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 59fc11359..0680f6644 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,4 +1,4 @@
-import { action, computed } from "mobx";
+import { action, computed, IReactionDisposer, reaction } from "mobx";
 import * as rp from 'request-promise';
 import CursorField from "../../../new_fields/CursorField";
 import { Doc, DocListCast, HeightSym } from "../../../new_fields/Doc";
@@ -36,11 +36,14 @@ export interface CollectionViewProps extends FieldViewProps {
 
 export interface SubCollectionViewProps extends CollectionViewProps {
     CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+    ruleProvider: Doc | undefined;
 }
 
 export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
     class CollectionSubView extends DocComponent<SubCollectionViewProps, T>(schemaCtor) {
         private dropDisposer?: DragManager.DragDropDisposer;
+        private _childLayoutDisposer?: IReactionDisposer;
+
         protected createDropTarget = (ele: HTMLDivElement) => {
             this.dropDisposer && this.dropDisposer();
             if (ele) {
@@ -51,33 +54,35 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             this.createDropTarget(ele);
         }
 
-        @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+        componentDidMount() {
+            this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
+                async (args) => args[1] instanceof Doc &&
+                    this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc))));
 
+        }
+        componentWillUnmount() {
+            this._childLayoutDisposer && this._childLayoutDisposer();
+        }
 
-        get childDocs() {
-            let self = this;
-            //TODO tfs: This might not be what we want?
-            //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
-            let docs = DocListCast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
-            let viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
-            if (viewSpecScript) {
-                let script = viewSpecScript.script;
-                docs = docs.filter(d => {
-                    let res = script.run({ doc: d });
-                    if (res.success) {
-                        return res.result;
-                    }
-                    else {
-                        console.log(res.error);
-                    }
-                });
-            }
-            return docs;
+        // The data field for rendeing this collection will be on the this.props.Document unless we're rendering a template in which case we try to use props.DataDoc.
+        // When a document has a DataDoc but it's not a template, then it contains its own rendering data, but needs to pass the DataDoc through
+        // to its children which may be templates.
+        // The name of the data field comes from fieldExt if it's an extension, or fieldKey otherwise.
+        @computed get dataField() {
+            return Doc.fieldExtensionDoc(this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey];
+        }
+
+
+        get childLayoutPairs() {
+            return this.childDocs.map(cd => Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, cd)).filter(pair => pair.layout).map(pair => ({ layout: pair.layout!, data: pair.data! }));
         }
         get childDocList() {
-            //TODO tfs: This might not be what we want?
-            //This linter error can't be fixed because of how js arguments work, so don't switch this to filter(FieldValue)
-            return Cast(this.extensionDoc[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey], listSpec(Doc));
+            return Cast(this.dataField, listSpec(Doc));
+        }
+        get childDocs() {
+            let docs = DocListCast(this.dataField);
+            const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
+            return viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
         }
 
         @action
@@ -118,7 +123,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             if (de.data instanceof DragManager.DocumentDragData && !de.data.applyAsTemplate) {
                 if (de.mods === "AltKey" && de.data.draggedDocuments.length) {
                     this.childDocs.map(doc =>
-                        Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, undefined)
+                        Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc)
                     );
                     e.stopPropagation();
                     return true;
@@ -127,9 +132,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 if (de.data.dropAction || de.data.userDropAction) {
                     added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
                 } else if (de.data.moveDocument) {
-                    let movedDocs = de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
-                    added = movedDocs.reduce((added: boolean, d) =>
-                        de.data.moveDocument(d, this.props.Document, this.props.addDocument) || added, false);
+                    let movedDocs = de.data.draggedDocuments;// de.data.options === this.props.Document[Id] ? de.data.draggedDocuments : de.data.droppedDocuments;
+                    // note that it's possible the drag function might create a drop document that's not the same as the
+                    // original dragged document.  So we explicitly call addDocument() with a droppedDocument and 
+                    added = movedDocs.reduce((added: boolean, d, i) =>
+                        de.data.moveDocument(d, this.props.Document, (doc: Doc) => this.props.addDocument(de.data.droppedDocuments[i])) || added, false);
                 } else {
                     added = de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d) || added, false);
                 }
@@ -271,6 +278,10 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     promises.push(prom);
                 }
             }
+            if (text) {
+                this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
+                return;
+            }
 
             if (promises.length) {
                 Promise.all(promises).finally(() => { completed && completed(); batch.end(); });
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index f5bb76966..e5313f68c 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -37,9 +37,10 @@ export interface TreeViewProps {
     containingCollection: Doc;
     renderDepth: number;
     deleteDoc: (doc: Doc) => boolean;
+    ruleProvider: Doc | undefined;
     moveDocument: DragManager.MoveFunction;
     dropAction: "alias" | "copy" | undefined;
-    addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     panelWidth: () => number;
     panelHeight: () => number;
@@ -177,7 +178,7 @@ class TreeView extends React.Component<TreeViewProps> {
         SetValue={undoBatch((value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true)}
         OnFillDown={undoBatch((value: string) => {
             Doc.GetProto(this.dataDoc)[key] = value;
-            let doc = this.props.document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.detailedLayout)) : undefined;
+            let doc = this.props.document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.document.layoutCustom)) : undefined;
             if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
             TreeView.loadId = doc[Id];
             return this.props.addDocument(doc);
@@ -200,6 +201,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.props.deleteDoc(this.props.document), icon: "trash-alt" });
             }
             ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, this.props.dataDoc ? this.props.dataDoc : kvp, "onRight"); }, icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.document, StrCast(this.props.document.title), () => { }, () => { }), icon: "file" });
             ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
             e.stopPropagation();
             e.preventDefault();
@@ -324,6 +326,7 @@ class TreeView extends React.Component<TreeViewProps> {
                     DataDocument={this.resolvedDataDoc}
                     renderDepth={this.props.renderDepth}
                     showOverlays={this.noOverlays}
+                    ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider}
                     fitToBox={this.boundsOfCollectionDocument !== undefined}
                     width={this.docWidth}
                     height={this.docHeight}
@@ -344,7 +347,7 @@ class TreeView extends React.Component<TreeViewProps> {
 
     @computed
     get renderBullet() {
-        return <div className="bullet" title="view inline" onClick={action(() => this.treeViewOpen = !this.treeViewOpen)} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
+        return <div className="bullet" title="view inline" onClick={action((e: React.MouseEvent) => { this.treeViewOpen = !this.treeViewOpen; e.stopPropagation(); })} style={{ color: StrCast(this.props.document.color, "black"), opacity: 0.4 }}>
             {<FontAwesomeIcon icon={!this.treeViewOpen ? (this.childDocs ? "caret-square-right" : "caret-right") : (this.childDocs ? "caret-square-down" : "caret-down")} />}
         </div>;
     }
@@ -402,7 +405,7 @@ class TreeView extends React.Component<TreeViewProps> {
         </div>;
     }
     public static GetChildElements(
-        docList: Doc[],
+        docs: Doc[],
         treeViewId: string,
         containingCollection: Doc,
         dataDoc: Doc | undefined,
@@ -411,7 +414,7 @@ class TreeView extends React.Component<TreeViewProps> {
         remove: ((doc: Doc) => boolean),
         move: DragManager.MoveFunction,
         dropAction: dropActionType,
-        addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void,
+        addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean,
         pinToPres: (document: Doc) => void,
         screenToLocalXf: () => Transform,
         outerXf: () => { translateX: number, translateY: number },
@@ -422,19 +425,9 @@ class TreeView extends React.Component<TreeViewProps> {
         preventTreeViewOpen: boolean,
         renderedIds: string[]
     ) {
-        let docs = docList.filter(child => !child.excludeFromLibrary && child.opacity !== 0);
-        let viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
+        const viewSpecScript = Cast(containingCollection.viewSpecScript, ScriptField);
         if (viewSpecScript) {
-            let script = viewSpecScript.script;
-            docs = docs.filter(d => {
-                let res = script.run({ doc: d });
-                if (res.success) {
-                    return res.result;
-                }
-                else {
-                    console.log(res.error);
-                }
-            });
+            docs = docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result);
         }
 
         let ascending = Cast(containingCollection.sortAscending, "boolean", null);
@@ -491,6 +484,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 dataDoc={pair.data}
                 containingCollection={containingCollection}
                 treeViewId={treeViewId}
+                ruleProvider={containingCollection.isRuleProvider && pair.layout.type !== DocumentType.TEXT ? containingCollection : containingCollection.ruleProvider as Doc}
                 key={child[Id]}
                 indentDocument={indent}
                 renderDepth={renderDepth}
@@ -544,7 +538,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
-        if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) { // excludeFromLibrary means this is the user document
+        if (!e.isPropagationStopped() && this.props.Document.workspaceLibrary) {
             ContextMenu.Instance.addItem({ description: "Create Workspace", event: () => MainView.Instance.createNewWorkspace(), icon: "plus" });
             ContextMenu.Instance.addItem({ description: "Delete Workspace", event: () => this.remove(this.props.Document), icon: "minus" });
             e.stopPropagation();
@@ -559,8 +553,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     outerXf = () => Utils.GetScreenTransform(this._mainEle!);
     onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
     openNotifsCol = () => {
-        if (CollectionTreeView.NotifsCol && CollectionDockingView.Instance) {
-            CollectionDockingView.Instance.AddRightSplit(CollectionTreeView.NotifsCol, undefined);
+        if (CollectionTreeView.NotifsCol) {
+            this.props.addDocTab(CollectionTreeView.NotifsCol, undefined, "onRight");
         }
     }
 
@@ -610,7 +604,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                     SetValue={undoBatch((value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true)}
                     OnFillDown={undoBatch((value: string) => {
                         Doc.GetProto(this.props.Document).title = value;
-                        let doc = this.props.Document.detailedLayout instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.detailedLayout)) : undefined;
+                        let doc = this.props.Document.layoutCustom instanceof Doc ? Doc.ApplyTemplate(Doc.GetProto(this.props.Document.layoutCustom)) : undefined;
                         if (!doc) doc = Docs.Create.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
                         TreeView.loadId = doc[Id];
                         Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, this.childDocs.length ? this.childDocs[0] : undefined, true, false, false, false);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 94b49fb98..1f2dc9b5c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -105,6 +105,12 @@ export class CollectionView extends React.Component<FieldViewProps> {
             subItems.push({ description: "Schema", event: () => this.props.Document.viewType = CollectionViewType.Schema, icon: "th-list" });
             subItems.push({ description: "Treeview", event: () => this.props.Document.viewType = CollectionViewType.Tree, icon: "tree" });
             subItems.push({ description: "Stacking", event: () => this.props.Document.viewType = CollectionViewType.Stacking, icon: "ellipsis-v" });
+            subItems.push({
+                description: "Stacking (AutoHeight)", event: () => {
+                    this.props.Document.viewType = CollectionViewType.Stacking;
+                    this.props.Document.autoHeight = true;
+                }, icon: "ellipsis-v"
+            });
             subItems.push({ description: "Masonry", event: () => this.props.Document.viewType = CollectionViewType.Masonry, icon: "columns" });
             switch (this.props.Document.viewType) {
                 case CollectionViewType.Freeform: {
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index c897af17e..7510b86a0 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -215,14 +215,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
             }
         }
         let fullScript = dateRestrictionScript.length || keyRestrictionScript.length ? dateRestrictionScript.length ?
-            `return ${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
-            `return (${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
-            "return true";
+            `${dateRestrictionScript} ${keyRestrictionScript.length ? "&&" : ""} (${keyRestrictionScript})` :
+            `(${keyRestrictionScript}) ${dateRestrictionScript.length ? "&&" : ""} ${dateRestrictionScript}` :
+            "true";
 
-        let compiled = CompileScript(fullScript, { params: { doc: Doc.name }, typecheck: false });
-        if (compiled.compiled) {
-            this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
-        }
+        this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction(fullScript, { doc: Doc.name });
     }
 
     @action
@@ -283,11 +280,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
 
     @action.bound
     clearFilter = () => {
-        let compiled = CompileScript("return true", { params: { doc: Doc.name }, typecheck: false });
-        if (compiled.compiled) {
-            this.props.CollectionView.props.Document.viewSpecScript = new ScriptField(compiled);
-        }
-
+        this.props.CollectionView.props.Document.viewSpecScript = ScriptField.MakeFunction("true", { doc: Doc.name });
         this._keyRestrictions = [];
         this.addKeyRestrictions([]);
     }
diff --git a/src/client/views/collections/ParentDocumentSelector.scss b/src/client/views/collections/ParentDocumentSelector.scss
index 2dd3e49f2..c186d15f8 100644
--- a/src/client/views/collections/ParentDocumentSelector.scss
+++ b/src/client/views/collections/ParentDocumentSelector.scss
@@ -19,4 +19,13 @@
         border-right: 0px;
         border-left: 0px;
     }
+}
+.parentDocumentSelector-button {
+    pointer-events: all;
+}
+.buttonSelector {
+    position: absolute; 
+    display: inline-block; 
+    padding-left: 5px; 
+    padding-right: 5px;
 }
\ No newline at end of file
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index d8475a467..7f2913214 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -8,8 +8,15 @@ import { SearchUtil } from "../../util/SearchUtil";
 import { CollectionDockingView } from "./CollectionDockingView";
 import { NumCast } from "../../../new_fields/Types";
 import { CollectionViewType } from "./CollectionBaseView";
+import { DocumentButtonBar } from "../DocumentButtonBar";
+import { DocumentManager } from "../../util/DocumentManager";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faEdit } from "@fortawesome/free-solid-svg-icons";
+import { library } from "@fortawesome/fontawesome-svg-core";
 
-type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
+library.add(faEdit);
+
+type SelectorProps = { Document: Doc, Stack?: any, addDocTab(doc: Doc, dataDoc: Doc | undefined, location: string): void };
 @observer
 export class SelectorContextMenu extends React.Component<SelectorProps> {
     @observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -83,7 +90,7 @@ export class ParentDocSelector extends React.Component<SelectorProps> {
             );
         }
         return (
-            <span style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
+            <span className="parentDocumentSelector-button" style={{ position: "relative", display: "inline-block", paddingLeft: "5px", paddingRight: "5px" }}
                 onMouseEnter={this.onMouseEnter}
                 onMouseLeave={this.onMouseLeave}>
                 <p>^</p>
@@ -92,3 +99,38 @@ export class ParentDocSelector extends React.Component<SelectorProps> {
         );
     }
 }
+
+@observer
+export class ButtonSelector extends React.Component<{ Document: Doc, Stack: any }> {
+    @observable hover = false;
+
+    @action
+    onMouseLeave = () => {
+        this.hover = false;
+    }
+
+    @action
+    onMouseEnter = () => {
+        this.hover = true;
+    }
+
+    render() {
+        let flyout;
+        if (this.hover) {
+            let view = DocumentManager.Instance.getDocumentView(this.props.Document);
+            flyout = !view ? (null) : (
+                <div className="PDS-flyout" title=" " onMouseLeave={this.onMouseLeave}>
+                    <DocumentButtonBar views={[view]} stack={this.props.Stack} />
+                </div>
+            );
+        }
+        return (
+            <span className="buttonSelector"
+                onMouseEnter={this.onMouseEnter}
+                onMouseLeave={this.onMouseLeave}>
+                {this.hover ? (null) : <FontAwesomeIcon icon={faEdit} size={"sm"} />}
+                {flyout}
+            </span>
+        );
+    }
+}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 2a64a7afb..cfd18ad35 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,8 +1,9 @@
 .collectionfreeformlinkview-linkLine {
     stroke: black;
     transform: translate(10000px,10000px);
-    // opacity: 0.5;
+    opacity: 0.8;
     pointer-events: all;
+    stroke-width: 3px;
 }
 .collectionfreeformlinkview-linkCircle {
     stroke: rgb(0,0,0);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 12771d11e..df089eb00 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -50,7 +50,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         return (
             <>
                 <line key="linkLine" className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${3}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index a25627dd1..a81f5315a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -79,15 +79,15 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         if (containerDoc) {
             equalViews = DocumentManager.Instance.getDocumentViews(containerDoc.proto!);
         }
-        if (view.props.ContainingCollectionView) {
-            let collid = view.props.ContainingCollectionView.props.Document[Id];
+        if (view.props.ContainingCollectionDoc) {
+            let collid = view.props.ContainingCollectionDoc[Id];
             DocListCast(this.props.Document[this.props.fieldKey]).
                 filter(child =>
                     child[Id] === collid).map(view =>
                         DocumentManager.Instance.getDocumentViews(view).map(view =>
                             equalViews.push(view)));
         }
-        return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document);
+        return equalViews.filter(sv => sv.props.ContainingCollectionDoc === this.props.Document);
     }
 
     @computed
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f7bda0a26..eb738d783 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -8,10 +8,10 @@ import { Id } from "../../../../new_fields/FieldSymbols";
 import { InkField, StrokeData } from "../../../../new_fields/InkField";
 import { createSchema, makeInterface } from "../../../../new_fields/Schema";
 import { ScriptField } from "../../../../new_fields/ScriptField";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../../new_fields/Types";
+import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue, DateCast } from "../../../../new_fields/Types";
 import { emptyFunction, returnEmptyString, returnOne, Utils } from "../../../../Utils";
 import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
-import { Docs } from "../../../documents/Documents";
+import { Docs, DocumentOptions } from "../../../documents/Documents";
 import { DocumentType } from "../../../documents/DocumentTypes";
 import { DocumentManager } from "../../../util/DocumentManager";
 import { DragManager } from "../../../util/DragManager";
@@ -24,9 +24,9 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
 import { DocumentContentsView } from "../../nodes/DocumentContentsView";
-import { DocumentViewProps, positionSchema } from "../../nodes/DocumentView";
+import { DocumentViewProps, documentSchema } from "../../nodes/DocumentView";
 import { pageSchema } from "../../nodes/ImageBox";
 import { OverlayElementOptions, OverlayView } from "../../OverlayView";
 import PDFMenu from "../../pdf/PDFMenu";
@@ -51,6 +51,9 @@ export const panZoomSchema = createSchema({
     scale: "number",
     arrangeScript: ScriptField,
     arrangeInit: ScriptField,
+    useClusters: "boolean",
+    isRuleProvider: "boolean",
+    fitToBox: "boolean"
 });
 
 export interface ViewDefBounds {
@@ -161,6 +164,7 @@ export namespace PivotView {
                         y={pos.y}
                         width={pos.width}
                         height={pos.height}
+                        transition={"transform 1s"}
                         jitterRotation={NumCast(target.props.Document.jitterRotation)}
                         {...target.getChildDocumentViewProps(doc)}
                     />,
@@ -181,8 +185,8 @@ export namespace PivotView {
 
 }
 
-type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof positionSchema, typeof pageSchema]>;
-const PanZoomDocument = makeInterface(panZoomSchema, positionSchema, pageSchema);
+type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
+const PanZoomDocument = makeInterface(panZoomSchema, documentSchema, positionSchema, pageSchema);
 
 @observer
 export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
@@ -190,35 +194,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private _lastY: number = 0;
     private get _pwidth() { return this.props.PanelWidth(); }
     private get _pheight() { return this.props.PanelHeight(); }
-    private inkKey = "ink";
-    private _childLayoutDisposer?: IReactionDisposer;
-    private _childDisposer?: IReactionDisposer;
-
-    componentDidMount() {
-        this._childDisposer = reaction(() => this.childDocs,
-            async (childDocs) => {
-                let childLayout = Cast(this.props.Document.childLayout, Doc) as Doc;
-                childLayout && childDocs.map(async doc => {
-                    if (!Doc.AreProtosEqual(childLayout, (await doc).layout as Doc)) {
-                        Doc.ApplyTemplateTo(childLayout, doc, undefined);
-                    }
-                });
-            });
-        this._childLayoutDisposer = reaction(() => Cast(this.props.Document.childLayout, Doc),
-            async (childLayout) => {
-                this.childDocs.map(async doc => {
-                    if (!Doc.AreProtosEqual(childLayout as Doc, (await doc).layout as Doc)) {
-                        Doc.ApplyTemplateTo(childLayout as Doc, doc, undefined);
-                    }
-                });
-            });
-    }
-    componentWillUnmount() {
-        this._childDisposer && this._childDisposer();
-        this._childLayoutDisposer && this._childLayoutDisposer();
-    }
-
-    get parentScaling() {
+    private get parentScaling() {
         return (this.props as any).ContentScaling && this.fitToBox && !this.isAnnotationOverlay ? (this.props as any).ContentScaling() : 1;
     }
 
@@ -249,7 +225,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return res;
     }
 
-    @computed get fitToBox() { return this.props.fitToBox || this.props.Document.fitToBox; }
+    @computed get fitToBox() { return this.props.fitToBox || this.Document.fitToBox; }
     @computed get nativeWidth() { return this.fitToBox ? 0 : this.Document.nativeWidth || 0; }
     @computed get nativeHeight() { return this.fitToBox ? 0 : this.Document.nativeHeight || 0; }
     public get isAnnotationOverlay() { return this.props.fieldExt ? true : false; } // fieldExt will be "" or "annotation". should maybe generalize this, or make it more specific (ie, 'annotation' instead of 'fieldExt')
@@ -265,29 +241,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private getLocalTransform = (): Transform => Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY());
     private addLiveTextBox = (newBox: Doc) => {
         FormattedTextBox.SelectOnLoad = newBox[Id];// track the new text box so we can give it a prop that tells it to focus itself when it's displayed
-        newBox.heading = 1;
-        for (let child of this.childDocs) {
-            if (child.heading === 1) {
-                newBox.heading = 2;
-            }
+        let maxHeading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
+        let heading = maxHeading === 0 || this.childDocs.length === 0 ? 1 : maxHeading === 1 ? 2 : 0;
+        if (heading === 0) {
+            let sorted = this.childDocs.filter(d => d.type === DocumentType.TEXT && d.data_ext instanceof Doc && d.data_ext.lastModified).sort((a, b) => DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date > DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? 1 :
+                DateCast((Cast(a.data_ext, Doc) as Doc).lastModified).date < DateCast((Cast(b.data_ext, Doc) as Doc).lastModified).date ? -1 : 0);
+            heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
         }
-        PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => {
-            if (!ruleProvider) ruleProvider = this.props.Document;
-            // saturation shift
-            // let col = NumCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]);
-            // let back = Utils.fromRGBAstr(StrCast(this.props.Document.backgroundColor));
-            // let hsl = Utils.RGBToHSL(back.r, back.g, back.b);
-            // let newcol = { h: hsl.h, s: hsl.s + col, l: hsl.l };
-            // col && (Doc.GetProto(newBox).backgroundColor = Utils.toRGBAstr(Utils.HSLtoRGB(newcol.h, newcol.s, newcol.l)));
-            // OR transparency set
-            let col = StrCast(ruleProvider["ruleColor_" + NumCast(newBox.heading)]);
-            (newBox.backgroundColor === newBox.defaultBackgroundColor) && col && (Doc.GetProto(newBox).backgroundColor = col);
-
-            let round = StrCast(ruleProvider["ruleRounding_" + NumCast(newBox.heading)]);
-            round && (Doc.GetProto(newBox).borderRounding = round);
-            newBox.ruleProvider = ruleProvider;
-            this.addDocument(newBox, false);
-        });
+        !this.Document.isRuleProvider && (newBox.heading = heading);
+        this.addDocument(newBox, false);
     }
     private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
         this.props.addDocument(newBox, false);
@@ -302,14 +264,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
     public getActiveDocuments = () => {
         const curPage = FieldValue(this.Document.curPage, -1);
-        return this.childDocs.filter(doc => {
-            var page = NumCast(doc.page, -1);
+        return this.childLayoutPairs.filter(pair => {
+            var page = NumCast(pair.layout.page, -1);
             return page === curPage || page === -1;
-        });
+        }).map(pair => pair.layout);
     }
 
     @computed get fieldExtensionDoc() {
-        return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
+        return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
     }
 
     intersectRect(r1: { left: number, top: number, width: number, height: number },
@@ -342,8 +304,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             if (de.data instanceof DragManager.DocumentDragData) {
                 if (de.data.droppedDocuments.length) {
                     let z = NumCast(de.data.droppedDocuments[0].z);
-                    let x = (z ? xpo : xp) - de.data.xOffset;
-                    let y = (z ? ypo : yp) - de.data.yOffset;
+                    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);
                     de.data.droppedDocuments.forEach(d => {
@@ -366,8 +328,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 if (de.data.dropDocument) {
                     let dragDoc = de.data.dropDocument;
-                    let x = xp - de.data.xOffset;
-                    let y = yp - de.data.yOffset;
+                    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;
@@ -383,7 +345,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     tryDragCluster(e: PointerEvent) {
         let probe = this.getTransform().transformPoint(e.clientX, e.clientY);
-        let cluster = this.childDocs.reduce((cluster, cd) => {
+        let cluster = 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;
@@ -394,7 +356,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             return cluster;
         }, -1);
         if (cluster !== -1) {
-            let eles = this.childDocs.filter(cd => NumCast(cd.cluster) === cluster);
+            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();
@@ -403,13 +365,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             SelectionManager.DeselectAll();
             prevSelected.map(dv => SelectionManager.SelectDoc(dv, true));
 
-            let de = new DragManager.DocumentDragData(eles, eles.map(d => undefined));
+            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);
-            const [xoff, yoff] = this.getTransform().transformDirection(e.x - left, e.y - top);
+            de.offset = this.getTransform().transformDirection(e.x - left, e.y - top);
             de.dropAction = e.ctrlKey || e.altKey ? "alias" : undefined;
-            de.xOffset = xoff;
-            de.yOffset = yoff;
             DragManager.StartDocumentDrag(clusterDocs.map(v => v.ContentDiv!), de, e.clientX, e.clientY, {
                 handlers: { dragComplete: action(emptyFunction) },
                 hideSource: !de.dropAction
@@ -423,9 +383,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @undoBatch
     @action
-    updateClusters() {
+    updateClusters(useClusters: boolean) {
+        this.Document.useClusters = useClusters;
         this.sets.length = 0;
-        this.childDocs.map(c => {
+        this.childLayoutPairs.map(pair => pair.layout).map(c => {
             let included = [];
             for (let i = 0; i < this.sets.length; i++) {
                 for (let member of this.sets[i]) {
@@ -453,20 +414,21 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     @undoBatch
     @action
     updateCluster(doc: Doc) {
+        let childLayouts = this.childLayoutPairs.map(pair => pair.layout);
         if (this.props.Document.useClusters) {
             this.sets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
             let preferredInd = NumCast(doc.cluster);
             doc.cluster = -1;
             this.sets.map((set, i) => set.map(member => {
-                if (doc.cluster === -1 && Doc.IndexOf(member, this.childDocs) !== -1 && this.boundsOverlap(doc, member)) {
+                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && this.boundsOverlap(doc, member)) {
                     doc.cluster = i;
                 }
             }));
-            if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, this.childDocs) !== -1).length)) {
+            if (doc.cluster === -1 && preferredInd !== -1 && (!this.sets[preferredInd] || !this.sets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
                 doc.cluster = preferredInd;
             }
             this.sets.map((set, i) => {
-                if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, this.childDocs) !== -1).length) {
+                if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
                     doc.cluster = i;
                 }
             });
@@ -481,22 +443,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     getClusterColor = (doc: Doc) => {
-        if (this.props.Document.useClusters) {
-            let cluster = NumCast(doc.cluster);
+        let clusterColor = "";
+        let cluster = NumCast(doc.cluster);
+        if (this.Document.useClusters) {
             if (this.sets.length <= cluster) {
-                setTimeout(() => this.updateCluster(doc), 0);//  this.updateClusters(), 0);
-                return "";
+                setTimeout(() => this.updateCluster(doc), 0);
+            } else {
+                // choose a cluster color from a palette
+                let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
+                clusterColor = colors[cluster % colors.length];
+                let set = this.sets.length > cluster ? this.sets[cluster].filter(s => s.backgroundColor && (s.backgroundColor !== s.defaultBackgroundColor)) : undefined;
+                // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
+                set && set.filter(s => !s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
+                set && set.filter(s => s.isBackground).map(s => clusterColor = StrCast(s.backgroundColor));
             }
-            let set = this.sets.length > cluster ? this.sets[cluster] : undefined;
-            let colors = ["#da42429e", "#31ea318c", "#8c4000", "#4a7ae2c4", "#d809ff", "#ff7601", "#1dffff", "yellow", "#1b8231f2", "#000000ad"];
-            let clusterColor = colors[cluster % colors.length];
-            set && set.filter(s => !s.isBackground).map(s =>
-                s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
-            set && set.filter(s => s.isBackground).map(s =>
-                s.backgroundColor && s.backgroundColor !== s.defaultBackgroundColor && (clusterColor = StrCast(s.backgroundColor)));
-            return clusterColor;
         }
-        return "";
+        return clusterColor;
     }
 
     @action
@@ -528,40 +490,39 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
             let x = this.Document.panX || 0;
             let y = this.Document.panY || 0;
-            let docs = this.childDocs || [];
+            let docs = this.childLayoutPairs.map(pair => 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;
-            //     let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
-            //     let ranges = docs.filter(doc => doc).reduce((range, doc) => {
-            //         let x = NumCast(doc.x);
-            //         let xe = x + NumCast(doc.width);
-            //         let y = NumCast(doc.y);
-            //         let ye = y + NumCast(doc.height);
-            //         return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
-            //         [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
-            //     }, [[minx, maxx], [miny, maxy]]);
-            //     let ink = Cast(this.fieldExtensionDoc.ink, InkField);
-            //     if (ink && ink.inkData) {
-            //         ink.inkData.forEach((value: StrokeData, key: string) => {
-            //             let bounds = InkingCanvas.StrokeRect(value);
-            //             ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
-            //             ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
-            //         });
-            //     }
-
-            //     let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling(),
-            //         this._pheight / this.zoomScaling());
-            //     let panelwidth = panelDim[0];
-            //     let panelheight = panelDim[1];
-            //     if (ranges[0][0] - dx > (this.panX() + panelwidth / 2)) x = ranges[0][1] + panelwidth / 2;
-            //     if (ranges[0][1] - dx < (this.panX() - panelwidth / 2)) x = ranges[0][0] - panelwidth / 2;
-            //     if (ranges[1][0] - dy > (this.panY() + panelheight / 2)) y = ranges[1][1] + panelheight / 2;
-            //     if (ranges[1][1] - dy < (this.panY() - panelheight / 2)) y = ranges[1][0] - panelheight / 2;
-            // }
+            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;
+                let maxy = docs.length ? NumCast(docs[0].height) + miny : miny;
+                let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+                    let x = NumCast(doc.x);
+                    let xe = x + NumCast(doc.width);
+                    let y = NumCast(doc.y);
+                    let ye = y + NumCast(doc.height);
+                    return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
+                    [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
+                }, [[minx, maxx], [miny, maxy]]);
+                let ink = Cast(this.fieldExtensionDoc.ink, InkField);
+                if (ink && ink.inkData) {
+                    ink.inkData.forEach((value: StrokeData, key: string) => {
+                        let bounds = InkingCanvas.StrokeRect(value);
+                        ranges[0] = [Math.min(ranges[0][0], bounds.left), Math.max(ranges[0][1], bounds.right)];
+                        ranges[1] = [Math.min(ranges[1][0], bounds.top), Math.max(ranges[1][1], bounds.bottom)];
+                    });
+                }
+
+                let cscale = this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.scale) : 1;
+                let panelDim = this.props.ScreenToLocalTransform().transformDirection(this._pwidth / this.zoomScaling() * cscale,
+                    this._pheight / this.zoomScaling() * cscale);
+                if (ranges[0][0] - dx > (this.panX() + panelDim[0] / 2)) x = ranges[0][1] + panelDim[0] / 2;
+                if (ranges[0][1] - dx < (this.panX() - panelDim[0] / 2)) x = ranges[0][0] - panelDim[0] / 2;
+                if (ranges[1][0] - dy > (this.panY() + panelDim[1] / 2)) y = ranges[1][1] + panelDim[1] / 2;
+                if (ranges[1][1] - dy < (this.panY() - panelDim[1] / 2)) y = ranges[1][0] - panelDim[1] / 2;
+            }
             this.setPan(x - dx, y - dy);
             this._lastX = e.pageX;
             this._lastY = e.pageY;
@@ -575,31 +536,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (BoolCast(this.props.Document.lockedPosition)) return;
         if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
             e.stopPropagation();
-            return;
         }
-
-        let childSelected = this.childDocs.some(doc => {
-            var dv = DocumentManager.Instance.getDocumentView(doc);
-            return dv && SelectionManager.IsSelected(dv) ? true : false;
-        });
-        if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) {
-            return;
-        }
-        e.stopPropagation();
-
-        // bcz: this changes the nativewidth/height, but ImageBox will just revert it back to its defaults.  need more logic to fix.
-        // if (e.ctrlKey && this.props.Document.scrollHeight === undefined) {
-        //     let deltaScale = (1 - (e.deltaY / coefficient));
-        //     let nw = this.nativeWidth * deltaScale;
-        //     let nh = this.nativeHeight * deltaScale;
-        //     if (nw && nh) {
-        //         this.props.Document.nativeWidth = nw;
-        //         this.props.Document.nativeHeight = nh;
-        //     }
-        //     e.preventDefault();
-        // } 
-        // else 
-        {
+        else if (this.props.active()) {
+            e.stopPropagation();
             let deltaScale = e.deltaY > 0 ? (1 / 1.1) : 1.1;
             if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) {
                 deltaScale = 1 / this.zoomScaling();
@@ -628,46 +567,38 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     @action
-    onDrop = (e: React.DragEvent): void => {
+    onDrop = async (e: React.DragEvent): Promise<void> => {
         var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
-        super.onDrop(e, { x: pt[0], y: pt[1] });
-    }
-
-    onDragOver = (): void => {
+        await super.onDrop(e, { x: pt[0], y: pt[1] });
     }
 
     bringToFront = (doc: Doc, sendToBack?: boolean) => {
         if (sendToBack || doc.isBackground) {
             doc.zIndex = 0;
-            return;
         }
-        const docs = this.childDocs;
-        docs.slice().sort((doc1, doc2) => {
-            if (doc1 === doc) return 1;
-            if (doc2 === doc) return -1;
-            return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
-        }).forEach((doc, index) => doc.zIndex = index + 1);
-        doc.zIndex = docs.length + 1;
+        else {
+            const docs = this.childLayoutPairs.map(pair => pair.layout);
+            docs.slice().sort((doc1, doc2) => {
+                if (doc1 === doc) return 1;
+                if (doc2 === doc) return -1;
+                return NumCast(doc1.zIndex) - NumCast(doc2.zIndex);
+            }).forEach((doc, index) => doc.zIndex = index + 1);
+            doc.zIndex = docs.length + 1;
+        }
     }
 
-    focusDocument = (doc: Doc, willZoom: boolean, scale?: number) => {
-        const panX = this.Document.panX;
-        const panY = this.Document.panY;
-        const id = this.Document[Id];
+    focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => {
         const state = HistoryUtil.getState();
-        state.initializers = state.initializers || {};
         // TODO This technically isn't correct if type !== "doc", as 
         // currently nothing is done, but we should probably push a new state
-        if (state.type === "doc" && panX !== undefined && panY !== undefined) {
-            const init = state.initializers[id];
+        if (state.type === "doc" && this.Document.panX !== undefined && this.Document.panY !== undefined) {
+            const init = state.initializers![this.Document[Id]];
             if (!init) {
-                state.initializers[id] = {
-                    panX, panY
-                };
+                state.initializers![this.Document[Id]] = { panX: this.Document.panX, panY: this.Document.panY };
                 HistoryUtil.pushState(state);
-            } else if (init.panX !== panX || init.panY !== panY) {
-                init.panX = panX;
-                init.panY = panY;
+            } else if (init.panX !== this.Document.panX || init.panY !== this.Document.panY) {
+                init.panX = this.Document.panX;
+                init.panY = this.Document.panY;
                 HistoryUtil.pushState(state);
             }
         }
@@ -675,8 +606,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
         const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
         const newState = HistoryUtil.getState();
-        (newState.initializers || (newState.initializers = {}))[id] = { panX: newPanX, panY: newPanY };
+        newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
         HistoryUtil.pushState(newState);
+
+        let px = this.Document.panX;
+        let py = this.Document.panY;
+        let s = this.Document.scale;
         this.setPan(newPanX, newPanY);
 
         this.props.Document.panTransformType = "Ease";
@@ -684,7 +619,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (willZoom) {
             this.setScaleToZoom(doc, scale);
         }
-
+        console.log("Focused " + this.Document.title + " " + s);
+        afterFocus && setTimeout(() => {
+            if (afterFocus && afterFocus()) {
+                console.log("UnFocused " + this.Document.title + " " + s);
+                this.Document.panX = px;
+                this.Document.panY = py;
+                this.Document.scale = s;
+            }
+        }, 1000);
     }
 
     setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
@@ -716,13 +659,15 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             addDocument: this.props.addDocument,
             removeDocument: this.props.removeDocument,
             moveDocument: this.props.moveDocument,
-            onClick: this.props.onClick,
+            ruleProvider: this.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider, //bcz: hack! - currently ruleProviders apply to documents in nested colleciton, not direct children of themselves
+            onClick: undefined, // this.props.onClick,  // bcz: check this out -- I don't think we want to inherit click handlers, or we at least need a way to ignore them
             ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
             renderDepth: this.props.renderDepth + 1,
             PanelWidth: childLayout[WidthSym],
             PanelHeight: childLayout[HeightSym],
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
+            ContainingCollectionDoc: this.props.CollectionView.props.Document,
             focus: this.focusDocument,
             backgroundColor: this.getClusterColor,
             parentActive: this.props.active,
@@ -741,6 +686,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             addDocument: this.props.addDocument,
             removeDocument: this.props.removeDocument,
             moveDocument: this.props.moveDocument,
+            ruleProvider: this.props.ruleProvider,
             onClick: this.props.onClick,
             ScreenToLocalTransform: this.getTransform,
             renderDepth: this.props.renderDepth,
@@ -748,6 +694,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             PanelHeight: layoutDoc[HeightSym],
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
+            ContainingCollectionDoc: this.props.CollectionView.props.Document,
             focus: this.focusDocument,
             backgroundColor: returnEmptyString,
             parentActive: this.props.active,
@@ -760,13 +707,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         };
     }
 
-    getCalculatedPositions(script: ScriptField, params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, state?: any } {
-        const result = script.script.run(params);
-        if (!result.success) {
-            return {};
+    getCalculatedPositions(params: { doc: Doc, index: number, collection: Doc, docs: Doc[], state: any }): { x?: number, y?: number, z?: number, width?: number, height?: number, transition?: string, state?: any } {
+        const script = this.Document.arrangeScript;
+        const result = script && script.script.run(params, console.log);
+        if (result && result.success) {
+            return { ...result, transition: "transform 1s" };
         }
-        let doc = params.doc;
-        return result.result === undefined ? { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") } : result.result;
+        return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(params.doc.width, "number"), height: Cast(params.doc.height, "number") };
     }
 
     viewDefsToJSX = (views: PivotView.PivotData[]) => {
@@ -829,14 +776,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (this.Document.usePivotLayout) return PivotView.elements(this);
         let curPage = FieldValue(this.Document.curPage, -1);
         const initScript = this.Document.arrangeInit;
-        const script = this.Document.arrangeScript;
         let state: any = undefined;
-        let docs = this.childDocs;
-        let overlayDocs = DocListCast(this.props.Document.localOverlays);
-        overlayDocs && docs.push(...overlayDocs);
+        let pairs = this.childLayoutPairs;
         let elements: ViewDefResult[] = [];
         if (initScript) {
-            const initResult = initScript.script.run({ docs, collection: this.Document });
+            const initResult = initScript.script.run({ docs: pairs.map(pair => pair.layout), collection: this.Document }, console.log);
             if (initResult.success) {
                 const result = initResult.result;
                 const { state: scriptState, views } = result;
@@ -844,25 +788,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 elements = this.viewDefsToJSX(views);
             }
         }
-        let docviews = docs.filter(doc => doc instanceof Doc).reduce((prev, doc) => {
-            var page = NumCast(doc.page, -1);
-            if ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1) {
-                let minim = BoolCast(doc.isMinimized);
-                if (minim === undefined || !minim) {
-                    const pos = script ? this.getCalculatedPositions(script, { doc, index: prev.length, collection: this.Document, docs, state }) :
-                        { x: Cast(doc.x, "number"), y: Cast(doc.y, "number"), z: Cast(doc.z, "number"), width: Cast(doc.width, "number"), height: Cast(doc.height, "number") };
-                    state = pos.state === undefined ? state : pos.state;
-                    let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, doc);
-                    if (pair.layout && !(pair.data instanceof Promise)) {
-                        prev.push({
-                            ele: <CollectionFreeFormDocumentView key={doc[Id]}
-                                jitterRotation={NumCast(this.props.Document.jitterRotation)}
-                                x={script ? pos.x : undefined} y={script ? pos.y : undefined}
-                                width={script ? pos.width : undefined} height={script ? pos.height : undefined} {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
-                            bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: NumCast(pos.width), height: NumCast(pos.height) }
-                        });
-                    }
-                }
+        let docviews = pairs.reduce((prev, pair) => {
+            var page = NumCast(pair.layout.page, -1);
+            if (!pair.layout.isMinimized && ((Math.abs(Math.round(page) - Math.round(curPage)) < 3) || page === -1)) {
+                const pos = this.getCalculatedPositions({ doc: pair.layout, index: prev.length, collection: this.Document, docs: pairs.map(pair => pair.layout), state });
+                state = pos.state === undefined ? state : pos.state;
+                prev.push({
+                    ele: <CollectionFreeFormDocumentView key={pair.layout[Id]}
+                        ruleProvider={this.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
+                        jitterRotation={NumCast(this.props.Document.jitterRotation)}
+                        transition={pos.transition} x={pos.x} y={pos.y} width={pos.width} height={pos.height}
+                        {...this.getChildDocumentViewProps(pair.layout, pair.data)} />,
+                    bounds: { x: pos.x || 0, y: pos.y || 0, z: pos.z, width: pos.width || 0, height: pos.height || 0 }
+                });
             }
             return prev;
         }, elements);
@@ -872,22 +810,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @computed.struct
     get views() {
-        let source = this.elements;
-        return source.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele);
+        return this.elements.filter(ele => ele.bounds && !ele.bounds.z).map(ele => ele.ele);
     }
     @computed.struct
     get overlayViews() {
         return this.elements.filter(ele => ele.bounds && ele.bounds.z).map(ele => ele.ele);
     }
 
-
     @action
     onCursorMove = (e: React.PointerEvent) => {
         super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     }
 
-    fitToContainer = async () => this.props.Document.fitToBox = !this.fitToBox;
-
     arrangeContents = async () => {
         const docs = await DocListCastAsync(this.Document[this.props.fieldKey]);
         UndoManager.RunInBatch(() => {
@@ -912,98 +846,86 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }, "arrange contents");
     }
 
+    autoFormat = () => {
+        this.Document.isRuleProvider = !this.Document.isRuleProvider;
+        // find rule colorations when rule providing is turned on by looking at each document to see if it has a coloring -- if so, use it's color as the rule for its associated heading.
+        this.Document.isRuleProvider && this.childLayoutPairs.map(pair =>
+            // iterate over the children of a displayed document (or if the displayed document is a template, iterate over the children of that template)
+            DocListCast(pair.layout.layout instanceof Doc ? pair.layout.layout.data : pair.layout.data).map(heading => {
+                let headingPair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, heading);
+                let headingLayout = headingPair.layout && (pair.layout.data_ext instanceof Doc) && (pair.layout.data_ext[`Layout[${headingPair.layout[Id]}]`] as Doc) || headingPair.layout;
+                if (headingLayout && NumCast(headingLayout.heading) > 0 && headingLayout.backgroundColor !== headingLayout.defaultBackgroundColor) {
+                    Doc.GetProto(this.props.Document)["ruleColor_" + NumCast(headingLayout.heading)] = headingLayout.backgroundColor;
+                }
+            })
+        );
+    }
+
     analyzeStrokes = async () => {
-        let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField);
-        if (!data) {
-            return;
+        let data = Cast(this.fieldExtensionDoc.ink, InkField);
+        if (data) {
+            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
         }
-        let relevantKeys = ["inkAnalysis", "handwriting"];
-        CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, relevantKeys, data.inkData);
     }
 
     onContextMenu = (e: React.MouseEvent) => {
         let layoutItems: ContextMenuProps[] = [];
 
-        if (this.childDocs.some(d => d.isTemplate)) {
-            layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab && this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
+        if (this.childDocs.some(d => BoolCast(d.isTemplate))) {
+            layoutItems.push({ description: "Template Layout Instance", event: () => this.props.addDocTab(Doc.ApplyTemplate(this.props.Document)!, undefined, "onRight"), icon: "project-diagram" });
         }
         layoutItems.push({ description: "reset view", event: () => { this.props.Document.panX = this.props.Document.panY = 0; this.props.Document.scale = 1; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: this.fitToContainer, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
-        layoutItems.push({
-            description: `${this.props.Document.useClusters ? "Uncluster" : "Use Clusters"}`,
-            event: async () => {
-                Docs.Prototypes.get(DocumentType.TEXT).defaultBackgroundColor = "#f1efeb"; // backward compatibility with databases that didn't have a default background color on prototypes
-                Docs.Prototypes.get(DocumentType.COL).defaultBackgroundColor = "white";
-                this.props.Document.useClusters = !this.props.Document.useClusters;
-                this.updateClusters();
-            },
-            icon: !this.props.Document.useClusters ? "braille" : "braille"
-        });
-        this.props.Document.useClusters && layoutItems.push({
-            description: `${this.props.Document.clusterOverridesDefaultBackground ? "Use Default Backgrounds" : "Clusters Override Defaults"}`,
-            event: async () => this.props.Document.clusterOverridesDefaultBackground = !this.props.Document.clusterOverridesDefaultBackground,
-            icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
-        });
+        layoutItems.push({ description: `${this.fitToBox ? "Unset" : "Set"} Fit To Container`, event: async () => this.Document.fitToBox = !this.fitToBox, icon: !this.fitToBox ? "expand-arrows-alt" : "compress-arrows-alt" });
+        layoutItems.push({ description: `${this.Document.useClusters ? "Uncluster" : "Use Clusters"}`, event: () => this.updateClusters(!this.Document.useClusters), icon: "braille" });
+        layoutItems.push({ description: `${this.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: "chalkboard" });
         layoutItems.push({ description: "Arrange contents in grid", event: this.arrangeContents, icon: "table" });
         layoutItems.push({ description: "Analyze Strokes", event: this.analyzeStrokes, icon: "paint-brush" });
         layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
         layoutItems.push({
-            description: "Import document", icon: "upload", event: () => {
+            description: "Import document", icon: "upload", event: ({ x, y }) => {
                 const input = document.createElement("input");
                 input.type = "file";
                 input.accept = ".zip";
                 input.onchange = async _e => {
-                    const files = input.files;
-                    if (!files) return;
-                    const file = files[0];
-                    let formData = new FormData();
-                    formData.append('file', file);
-                    formData.append('remap', "true");
                     const upload = Utils.prepend("/uploadDoc");
-                    const response = await fetch(upload, { method: "POST", body: formData });
-                    const json = await response.json();
-                    if (json === "error") {
-                        return;
-                    }
-                    const doc = await DocServer.GetRefField(json);
-                    if (!doc || !(doc instanceof Doc)) {
-                        return;
+                    let formData = new FormData();
+                    const file = input.files && input.files[0];
+                    if (file) {
+                        formData.append('file', file);
+                        formData.append('remap', "true");
+                        const response = await fetch(upload, { method: "POST", body: formData });
+                        const json = await response.json();
+                        if (json !== "error") {
+                            const doc = await DocServer.GetRefField(json);
+                            if (doc instanceof Doc) {
+                                const [xx, yy] = this.props.ScreenToLocalTransform().transformPoint(x, y);
+                                doc.x = xx, doc.y = yy;
+                                this.props.addDocument && this.props.addDocument(doc, false);
+                            }
+                        }
                     }
-                    const [x, y] = this.props.ScreenToLocalTransform().transformPoint(e.pageX, e.pageY);
-                    doc.x = x, doc.y = y;
-                    this.props.addDocument &&
-                        this.props.addDocument(doc, false);
                 };
                 input.click();
             }
         });
 
-        let noteItems: ContextMenuProps[] = [];
-        if (CurrentUserUtils.UserDocument) {
-            let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
-            notes.map((node, i) => noteItems.push({ description: (i + 1) + ": " + StrCast(node.title), event: () => this.createText(i), icon: "eye" }));
-        }
-        layoutItems.push({ description: "Add Note ...", subitems: noteItems, icon: "eye" });
+        layoutItems.push({
+            description: "Add Note ...",
+            subitems: DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data).map((note, i) => ({
+                description: (i + 1) + ": " + StrCast(note.title),
+                event: (args: { x: number, y: number }) => this.addLiveTextBox(Docs.Create.TextDocument({ width: 200, height: 100, x: this.getTransform().transformPoint(args.x, args.y)[0], y: this.getTransform().transformPoint(args.x, args.y)[1], autoHeight: true, layout: note, title: StrCast(note.title) })),
+                icon: "eye"
+            })) as ContextMenuProps[],
+            icon: "eye"
+        });
         ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
     }
 
-    createText = (noteStyle: number) => {
-        let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY);
-        if (CurrentUserUtils.UserDocument) {
-            let notes = DocListCast((CurrentUserUtils.UserDocument.noteTypes as Doc).data);
-            let text = Docs.Create.TextDocument({ width: 200, height: 100, x: pt[0], y: pt[1], autoHeight: true, title: StrCast(notes[noteStyle % notes.length].title) });
-            text.layout = notes[noteStyle % notes.length];
-            this.addLiveTextBox(text);
-        }
-    }
 
     private childViews = () => [
         <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
         ...this.views
     ]
-    private overlayChildViews = () => {
-        return [...this.overlayViews];
-    }
 
     public static AddCustomLayout(doc: Doc, dataKey: string): () => void {
         return () => {
@@ -1034,15 +956,18 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     render() {
+        // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
         this.props.Document.fitX = this.actualContentBounds && this.actualContentBounds.x;
         this.props.Document.fitY = this.actualContentBounds && this.actualContentBounds.y;
         this.props.Document.fitW = this.actualContentBounds && (this.actualContentBounds.r - this.actualContentBounds.x);
         this.props.Document.fitH = this.actualContentBounds && (this.actualContentBounds.b - this.actualContentBounds.y);
+        // if fieldExt is set, then children will be stored in the extension document for the fieldKey. 
+        // otherwise, they are stored in fieldKey.  All annotations to this document are stored in the extension document
+        Doc.UpdateDocumentExtensionForField(this.props.DataDoc || this.props.Document, this.props.fieldKey);
         const easing = () => this.props.Document.panTransformType === "Ease";
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         return (
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
-                onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onDragOver={this.onDragOver} onContextMenu={this.onContextMenu}>
+                onPointerDown={this.onPointerDown} onPointerMove={this.onCursorMove} onDrop={this.onDrop.bind(this)} onContextMenu={this.onContextMenu}>
                 <MarqueeView container={this} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} isSelected={this.props.isSelected}
                     addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox}
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform}>
@@ -1056,7 +981,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
-                {this.overlayChildViews()}
+                {this.overlayViews}
                 <CollectionFreeFormOverlayView  {...this.props} {...this.getDocumentViewProps(this.props.Document)} />
             </div>
         );
@@ -1077,7 +1002,6 @@ class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps &
 @observer
 class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
     @computed get backgroundView() {
-        let props = this.props;
         return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
             renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
     }
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index fe48a3485..bbea4a555 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -1,28 +1,24 @@
-import * as htmlToImage from "html-to-image";
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, FieldResult, DocListCast } from "../../../../new_fields/Doc";
-import { Id } from "../../../../new_fields/FieldSymbols";
+import { Doc, DocListCast } from "../../../../new_fields/Doc";
 import { InkField, StrokeData } from "../../../../new_fields/InkField";
 import { List } from "../../../../new_fields/List";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
 import { Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
 import { Utils } from "../../../../Utils";
-import { DocServer } from "../../../DocServer";
 import { Docs } from "../../../documents/Documents";
 import { SelectionManager } from "../../../util/SelectionManager";
 import { Transform } from "../../../util/Transform";
 import { undoBatch } from "../../../util/UndoManager";
 import { InkingCanvas } from "../../InkingCanvas";
 import { PreviewCursor } from "../../PreviewCursor";
-import { Templates } from "../../Templates";
 import { CollectionViewType } from "../CollectionBaseView";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
-import { SchemaHeaderField, RandomPastel } from "../../../../new_fields/SchemaHeaderField";
-import { string } from "prop-types";
-import { listSpec } from "../../../../new_fields/Schema";
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
@@ -232,18 +228,14 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         return { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
     }
 
-    get ink() {
-        let container = this.props.container.props.Document;
-        let containerKey = this.props.container.props.fieldKey;
-        let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
-        return Cast(extensionDoc.ink, InkField);
+    get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
+        let cprops = this.props.container.props;
+        return Cast(Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
-        let container = Doc.GetProto(this.props.container.props.Document);
-        let containerKey = this.props.container.props.fieldKey;
-        let extensionDoc = Doc.resolvedFieldDataDoc(container, containerKey, "true");
-        extensionDoc.ink = value;
+        let cprops = this.props.container.props;
+        Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
     }
 
     @undoBatch
@@ -287,7 +279,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             let palette = Array.from(Cast(this.props.container.props.Document.colorPalette, listSpec("string")) as string[]);
             let usedPaletted = new Map<string, number>();
             [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
-                let bg = StrCast(child.backgroundColor);
+                let bg = StrCast(child.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor);
                 if (palette.indexOf(bg) !== -1) {
                     palette.splice(palette.indexOf(bg), 1);
                     if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
@@ -309,13 +301,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 defaultBackgroundColor: this.props.container.isAnnotationOverlay ? undefined : chosenColor,
                 width: bounds.width,
                 height: bounds.height,
-                title: e.key === "s" || e.key === "S" ? "-summary-" : "a nested collection",
+                title: "a nested collection",
             });
             let dataExtensionField = Doc.CreateDocumentExtensionForField(newCollection, "data");
             dataExtensionField.ink = inkData ? new InkField(this.marqueeInkSelect(inkData)) : undefined;
             this.marqueeInkDelete(inkData);
 
-            if (e.key === "s") {
+            if (e.key === "s" || e.key === "S") {
                 selected.map(d => {
                     this.props.removeDocument(d);
                     d.x = NumCast(d.x) - bounds.left - bounds.width / 2;
@@ -324,39 +316,23 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                     return d;
                 });
                 newCollection.chromeStatus = "disabled";
-                let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
-                newCollection.proto!.summaryDoc = summary;
-                selected = [newCollection];
+                let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, autoHeight: true, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
+                Doc.GetProto(summary).summarizedDocs = new List<Doc>([newCollection]);
                 newCollection.x = bounds.left + bounds.width;
-                summary.proto!.subBulletDocs = new List<Doc>(selected);
-                summary.templates = new List<string>([Templates.Bullet.Layout]);
-                let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
-                container.viewType = CollectionViewType.Stacking;
-                container.autoHeight = true;
-                this.props.addLiveTextDocument(container);
-                // });
-            } else if (e.key === "S") {
-                selected.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;
-                    d.page = -1;
-                    return d;
-                });
-                newCollection.chromeStatus = "disabled";
-                let summary = Docs.Create.TextDocument({ x: bounds.left, y: bounds.top, width: 300, height: 100, backgroundColor: "#e2ad32" /* yellow */, title: "-summary-" });
-                newCollection.proto!.summaryDoc = summary;
-                selected = [newCollection];
-                newCollection.x = bounds.left + bounds.width;
-                //this.props.addDocument(newCollection, false);
-                summary.proto!.summarizedDocs = new List<Doc>(selected);
-                summary.proto!.maximizeLocation = "inTab";  // or "inPlace", or "onRight"
-                summary.autoHeight = true;
-
-                this.props.addLiveTextDocument(summary);
+                Doc.GetProto(newCollection).summaryDoc = summary;
+                Doc.GetProto(newCollection).title = ComputedField.MakeFunction(`summaryTitle(this);`);
+                if (e.key === "s") { // summary is wrapped in an expand/collapse container that also contains the summarized documents in a free form view.
+                    let container = Docs.Create.FreeformDocument([summary, newCollection], { x: bounds.left, y: bounds.top, width: 300, height: 200, chromeStatus: "disabled", title: "-summary-" });
+                    container.viewType = CollectionViewType.Stacking;
+                    container.autoHeight = true;
+                    Doc.GetProto(summary).maximizeLocation = "inPlace";  // or "onRight"
+                    this.props.addLiveTextDocument(container);
+                } else if (e.key === "S") { // the summary stands alone, but is linked to a collection of the summarized documents - set the OnCLick behavior to link follow to access them
+                    Doc.GetProto(summary).maximizeLocation = "inTab";  // or "inPlace", or "onRight"
+                    this.props.addLiveTextDocument(summary);
+                }
             }
             else {
-                newCollection.ruleProvider = this.props.container.props.Document;
                 this.props.addDocument(newCollection, false);
                 this.props.selectDocuments([newCollection]);
             }
diff --git a/src/client/views/document_templates/image_card/ImageCard.tsx b/src/client/views/document_templates/image_card/ImageCard.tsx
index 9931515f3..868afc423 100644
--- a/src/client/views/document_templates/image_card/ImageCard.tsx
+++ b/src/client/views/document_templates/image_card/ImageCard.tsx
@@ -1,8 +1,5 @@
 import * as React from 'react';
-import { DocComponent } from '../../DocComponent';
 import { FieldViewProps } from '../../nodes/FieldView';
-import { createSchema, makeInterface } from '../../../../new_fields/Schema';
-import { createInterface } from 'readline';
 import { ImageBox } from '../../nodes/ImageBox';
 
 export default class ImageCard extends React.Component<FieldViewProps> {
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index f8807641b..81b0249dd 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -2,7 +2,7 @@ import { observable, computed, action, runInAction, reaction, IReactionDisposer
 import React = require("react");
 import { observer } from "mobx-react";
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
-import { Doc, DocListCastAsync } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt } from "../../../new_fields/Doc";
 import { undoBatch } from "../../util/UndoManager";
 import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
 import { CollectionViewType } from "../collections/CollectionBaseView";
@@ -85,11 +85,10 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     }
 
     async resetPan() {
-        if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionView) {
-            let colDoc = this.sourceView.props.ContainingCollectionView.props.Document;
-            runInAction(() => { this.canPan = false; });
-            if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) {
-                let docs = Cast(colDoc.data, listSpec(Doc), []);
+        if (LinkFollowBox.destinationDoc && this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+            runInAction(() => this.canPan = false);
+            if (this.sourceView.props.ContainingCollectionDoc.viewType === CollectionViewType.Freeform) {
+                let docs = Cast(this.sourceView.props.ContainingCollectionDoc.data, listSpec(Doc), []);
                 let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
 
                 aliases.forEach(alias => {
@@ -172,7 +171,6 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         if (LinkFollowBox.destinationDoc) {
             let view: DocumentView | null = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
             view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
-            SelectionManager.DeselectAll();
         }
     }
 
@@ -188,7 +186,6 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             let view = DocumentManager.Instance.getDocumentView(options.context);
             view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
             this.highlightDoc();
-            SelectionManager.DeselectAll();
         }
     }
 
@@ -198,9 +195,9 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     }
 
-    _addDocTab: (undefined | ((doc: Doc, dataDoc: Doc | undefined, where: string) => void));
+    _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
 
-    setAddDocTab = (addFunc: (doc: Doc, dataDoc: Doc | undefined, where: string) => void) => {
+    setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
         this._addDocTab = addFunc;
     }
 
@@ -214,7 +211,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 options.context.panX = newPanX;
                 options.context.panY = newPanY;
             }
-            CollectionDockingView.Instance.AddRightSplit(options.context, undefined);
+            (this._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
 
             if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
 
@@ -227,7 +224,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     openLinkRight = () => {
         if (LinkFollowBox.destinationDoc) {
             let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
-            CollectionDockingView.Instance.AddRightSplit(alias, undefined);
+            (this._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
             this.highlightDoc();
             SelectionManager.DeselectAll();
         }
@@ -247,7 +244,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             let sourceContext = await Cast(proto.sourceContext, Doc);
             const shouldZoom = options ? options.shouldZoom : false;
 
-            let dockingFunc = (document: Doc) => { this._addDocTab && this._addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
+            let dockingFunc = (document: Doc) => { (this._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
 
             if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) {
                 DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
@@ -274,7 +271,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         if (LinkFollowBox.destinationDoc) {
             let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
             // this.prosp.addDocTab is empty -- use the link source's addDocTab 
-            this._addDocTab && this._addDocTab(fullScreenAlias, undefined, "inTab");
+            (this._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
 
             this.highlightDoc();
             SelectionManager.DeselectAll();
@@ -291,7 +288,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 options.context.panX = newPanX;
                 options.context.panY = newPanY;
             }
-            this._addDocTab && this._addDocTab(options.context, undefined, "inTab");
+            (this._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
             if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
 
             this.highlightDoc();
@@ -373,9 +370,9 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN);
 
         if (this.shouldUseOnlyParentContext) {
-            if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
-                this.selectedContext = this.sourceView.props.ContainingCollectionView.props.Document;
-                this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionView.props.Document.title));
+            if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+                this.selectedContext = this.sourceView.props.ContainingCollectionDoc;
+                this.selectedContextString = (StrCast(this.sourceView.props.ContainingCollectionDoc.title));
             }
         }
     }
@@ -396,9 +393,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     @computed
     get canOpenInPlace() {
-        if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
-            let colView = this.sourceView.props.ContainingCollectionView;
-            let colDoc = colView.props.Document;
+        if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+            let colDoc = this.sourceView.props.ContainingCollectionDoc;
             if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
         }
         return false;
@@ -459,17 +455,15 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     @computed
     get parentName() {
-        if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
-            let colView = this.sourceView.props.ContainingCollectionView;
-            return colView.props.Document.title;
+        if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+            return this.sourceView.props.ContainingCollectionDoc.title;
         }
     }
 
     @computed
     get parentID(): string {
-        if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
-            let colView = this.sourceView.props.ContainingCollectionView;
-            return StrCast(colView.props.Document[Id]);
+        if (this.sourceView && this.sourceView.props.ContainingCollectionDoc) {
+            return StrCast(this.sourceView.props.ContainingCollectionDoc[Id]);
         }
         return "col";
     }
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 842ce45b1..27af873b5 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -16,7 +16,7 @@ library.add(faTrash);
 interface Props {
     docView: DocumentView;
     changeFlyout: () => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
 }
 
 @observer
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index b6a24b0c8..1891919ce 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -1,27 +1,25 @@
-import { action, observable } from "mobx";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action } from "mobx";
 import { observer } from "mobx-react";
-import { DocumentView } from "../nodes/DocumentView";
-import { LinkMenuItem } from "./LinkMenuItem";
-import { LinkEditor } from "./LinkEditor";
-import './LinkMenu.scss';
-import React = require("react");
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
-import { LinkManager } from "../../util/LinkManager";
-import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
+import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import { emptyFunction } from "../../../Utils";
 import { Docs } from "../../documents/Documents";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { DragManager, SetupDrag } from "../../util/DragManager";
+import { LinkManager } from "../../util/LinkManager";
 import { UndoManager } from "../../util/UndoManager";
-import { StrCast } from "../../../new_fields/Types";
-import { SchemaHeaderField, RandomPastel } from "../../../new_fields/SchemaHeaderField";
+import { DocumentView } from "../nodes/DocumentView";
+import './LinkMenu.scss';
+import { LinkMenuItem } from "./LinkMenuItem";
+import React = require("react");
 
 interface LinkMenuGroupProps {
     sourceDoc: Doc;
     group: Doc[];
     groupType: string;
     showEditor: (linkDoc: Doc) => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     docView: DocumentView;
 
 }
@@ -57,7 +55,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                     let opp = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
                     if (opp) return opp;
                 }) as Doc[];
-                let dragData = new DragManager.DocumentDragData(draggedDocs, draggedDocs.map(d => undefined));
+                let dragData = new DragManager.DocumentDragData(draggedDocs);
 
                 DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
                     handlers: {
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 19a0023e9..82fe3df23 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -21,7 +21,7 @@ interface LinkMenuItemProps {
     sourceDoc: Doc;
     destinationDoc: Doc;
     showEditor: (linkDoc: Doc) => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
 }
 
 @observer
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 68d3b8ae1..f08ea4891 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -3,7 +3,7 @@ import { faEdit } from '@fortawesome/free-regular-svg-icons';
 import { action, computed } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCastAsync } from '../../../new_fields/Doc';
+import { Doc, DocListCastAsync, DocListCast } from '../../../new_fields/Doc';
 import { List } from '../../../new_fields/List';
 import { createSchema, makeInterface, listSpec } from '../../../new_fields/Schema';
 import { ScriptField } from '../../../new_fields/ScriptField';
@@ -49,7 +49,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
         funcs.push({
             description: "Clear Script Params", event: () => {
                 let params = Cast(this.props.Document.buttonParams, listSpec("string"));
-                params && params.map(p => this.props.Document[p] = undefined)
+                params && params.map(p => this.props.Document[p] = undefined);
             }, icon: "trash"
         });
 
@@ -68,7 +68,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
     render() {
         let params = Cast(this.props.Document.buttonParams, listSpec("string"));
         let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
-        params && params.map(async p => await DocListCastAsync(this.props.Document[p])); // bcz: really hacky form of prefetching ... 
+        params && params.map(p => DocListCast(this.props.Document[p])); // bcz: really hacky form of prefetching ... 
         return (
             <div className="buttonBox-outerDiv" ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
                 <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.scss b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
new file mode 100644
index 000000000..c0d9e1267
--- /dev/null
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.scss
@@ -0,0 +1,5 @@
+.collectionFreeFormDocumentView-container {
+    transform-origin: left top;
+    position: absolute;
+    background-color: transparent;
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 07dd1cae7..9685f9bca 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,13 +1,14 @@
-import { computed } from "mobx";
+import { computed, action, observable, reaction, IReactionDisposer, trace } from "mobx";
 import { observer } from "mobx-react";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { BoolCast, FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types";
+import { createSchema, makeInterface, listSpec } from "../../../new_fields/Schema";
+import { FieldValue, NumCast, StrCast, Cast } from "../../../new_fields/Types";
 import { Transform } from "../../util/Transform";
 import { DocComponent } from "../DocComponent";
-import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView";
-import "./DocumentView.scss";
+import { percent2frac } from "../../../Utils";
+import { DocumentView, DocumentViewProps, documentSchema } from "./DocumentView";
+import "./CollectionFreeFormDocumentView.scss";
 import React = require("react");
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
 import { random } from "animejs";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
@@ -16,31 +17,34 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     width?: number;
     height?: number;
     jitterRotation: number;
+    transition?: string;
 }
-
-const schema = createSchema({
+export const positionSchema = createSchema({
     zIndex: "number",
+    x: "number",
+    y: "number",
+    z: "number",
 });
 
-//TODO Types: The import order is wrong, so positionSchema is undefined
-type FreeformDocument = makeInterface<[typeof schema, typeof positionSchema]>;
-const FreeformDocument = makeInterface(schema, positionSchema);
+export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+export const PositionDocument = makeInterface(documentSchema, positionSchema);
 
 @observer
-export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, FreeformDocument>(FreeformDocument) {
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
+    _disposer: IReactionDisposer | undefined = undefined;
     @computed get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
-    @computed get X() { return this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
-    @computed get Y() { return this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
-    @computed get width(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.Document.width || 0; }
-    @computed get height(): number { return BoolCast(this.props.Document.willMaximize) ? 0 : this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.Document.height || 0; }
-    @computed get nativeWidth(): number { return FieldValue(this.Document.nativeWidth, 0); }
-    @computed get nativeHeight(): number { return FieldValue(this.Document.nativeHeight, 0); }
-    @computed get scaleToOverridingWidth() { return this.width / NumCast(this.props.Document.width, this.width); }
+    @computed get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.Document.x || 0; }
+    @computed get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.Document.y || 0; }
+    @computed get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.Document[WidthSym](); }
+    @computed get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.Document[HeightSym](); }
+    @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
+    @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
+    @computed get scaleToOverridingWidth() { return this.width / FieldValue(this.Document.width, this.width); }
 
     @computed get renderScriptDim() {
         if (this.Document.renderScript) {
-            let someView = Cast(this.Document.someView, Doc);
-            let minimap = Cast(this.Document.minimap, Doc);
+            let someView = Cast(this.props.Document.someView, Doc);
+            let minimap = Cast(this.props.Document.minimap, Doc);
             if (someView instanceof Doc && minimap instanceof Doc) {
                 let x = (NumCast(someView.panX) - NumCast(someView.width) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitX) - NumCast(minimap.fitW) / 2)) / NumCast(minimap.fitW) * NumCast(minimap.width) - NumCast(minimap.width) / 2;
                 let y = (NumCast(someView.panY) - NumCast(someView.height) / 2 / NumCast(someView.scale) - (NumCast(minimap.fitY) - NumCast(minimap.fitH) / 2)) / NumCast(minimap.fitH) * NumCast(minimap.height) - NumCast(minimap.height) / 2;
@@ -52,34 +56,31 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return undefined;
     }
 
-    contentScaling = () => this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? this.width / this.nativeWidth : 1;
+    componentWillUnmount() {
+        this._disposer && this._disposer();
+    }
+    componentDidMount() {
+        this._disposer = reaction(() => [this.props.Document.animateToPos, this.props.Document.isAnimating],
+            () => {
+                const target = this.props.Document.animateToPos ? Array.from(Cast(this.props.Document.animateToPos, listSpec("number"))!) : undefined;
+                this._animPos = !target ? undefined : target[2] ? [this.Document.x || 0, this.Document.y || 0] : this.props.ScreenToLocalTransform().transformPoint(target[0], target[1]);
+            }, { fireImmediately: true });
+    }
+
+    contentScaling = () => this.nativeWidth > 0 && !this.props.Document.ignoreAspect ? this.width / this.nativeWidth : 1;
     panelWidth = () => this.props.PanelWidth();
     panelHeight = () => this.props.PanelHeight();
     getTransform = (): Transform => this.props.ScreenToLocalTransform()
         .translate(-this.X, -this.Y)
         .scale(1 / this.contentScaling()).scale(1 / this.scaleToOverridingWidth)
 
-    animateBetweenIcon = (icon: number[], stime: number, maximizing: boolean) => {
-        this.props.bringToFront(this.props.Document);
-        let targetPos = [this.Document.x || 0, this.Document.y || 0];
-        let iconPos = this.props.ScreenToLocalTransform().transformPoint(icon[0], icon[1]);
-        DocumentView.animateBetweenIconFunc(this.props.Document,
-            this.Document.width || 0, this.Document.height || 0, stime, maximizing, (progress: number) => {
-                let pval = maximizing ?
-                    [iconPos[0] + (targetPos[0] - iconPos[0]) * progress, iconPos[1] + (targetPos[1] - iconPos[1]) * progress] :
-                    [targetPos[0] + (iconPos[0] - targetPos[0]) * progress, targetPos[1] + (iconPos[1] - targetPos[1]) * progress];
-                this.Document.x = progress === 1 ? targetPos[0] : pval[0];
-                this.Document.y = progress === 1 ? targetPos[1] : pval[1];
-            });
-    }
-
     borderRounding = () => {
-        let br = StrCast(this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout.borderRounding : this.props.Document.borderRounding);
+        let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
+        let br = StrCast(((this.layoutDoc.layout as Doc) || this.Document).borderRounding);
+        br = !br && ruleRounding ? ruleRounding : br;
         if (br.endsWith("%")) {
-            let percent = Number(br.substr(0, br.length - 1)) / 100;
             let nativeDim = Math.min(NumCast(this.layoutDoc.nativeWidth), NumCast(this.layoutDoc.nativeHeight));
-            let minDim = percent * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
-            return minDim;
+            return percent2frac(br) * (nativeDim ? nativeDim : Math.min(this.props.PanelWidth(), this.props.PanelHeight()));
         }
         return undefined;
     }
@@ -95,24 +96,21 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
     }
 
+    @observable _animPos: number[] | undefined = undefined;
+
     render() {
-        const hasPosition = this.props.x !== undefined || this.props.y !== undefined;
         return (
             <div className="collectionFreeFormDocumentView-container"
                 style={{
-                    transformOrigin: "left top",
-                    position: "absolute",
-                    backgroundColor: "transparent",
                     boxShadow:
                         this.layoutDoc.opacity === 0 ? undefined :  // if it's not visible, then no shadow
                             this.layoutDoc.z ? `#9c9396  ${StrCast(this.layoutDoc.boxShadow, "10px 10px 0.9vw")}` :  // if it's a floating doc, give it a big shadow
-                                this.layoutDoc.isBackground ? `0px 0px 50px 50px ${this.clusterColor}` :  // if it's a background & has a cluster color, make the shadow spread really big
-                                    this.clusterColor ? (
-                                        `${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${50 / this.props.ContentScaling()}px`)}`) :  // if it's just in a cluster, make the shadown roughly match the cluster border extent
+                                this.clusterColor ? (`${this.clusterColor} ${StrCast(this.layoutDoc.boxShadow, `0vw 0vw ${(this.layoutDoc.isBackground ? 100 : 50) / this.props.ContentScaling()}px`)}`) :  // if it's just in a cluster, make the shadown roughly match the cluster border extent
+                                    this.layoutDoc.isBackground ? `1px 1px 1px ${this.clusterColor}` :  // if it's a background & has a cluster color, make the shadow spread really big
                                         StrCast(this.layoutDoc.boxShadow, ""),
                     borderRadius: this.borderRounding(),
                     transform: this.transform,
-                    transition: hasPosition ? "transform 1s" : StrCast(this.layoutDoc.transition),
+                    transition: this.Document.isAnimating !== undefined ? ".5s ease-in" : this.props.transition ? this.props.transition : StrCast(this.layoutDoc.transition),
                     width: this.width,
                     height: this.height,
                     zIndex: this.Document.zIndex || 0,
@@ -123,7 +121,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     backgroundColor={this.clusterColorFunc}
                     PanelWidth={this.panelWidth}
                     PanelHeight={this.panelHeight}
-                    animateBetweenIcon={this.animateBetweenIcon}
                 />
             </div>
         );
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d0e117fe4..3c3cc0d91 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -93,13 +93,6 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         return { props: list };
     }
 
-    @computed get templates(): List<string> {
-        let field = this.props.Document.templates;
-        if (field && field instanceof List) {
-            return field;
-        }
-        return new List<string>();
-    }
     @computed get finalLayout() {
         return this.props.layoutKey === "overlayLayout" ? "<div/>" : this.layout;
     }
@@ -107,7 +100,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     render() {
         let self = this;
         if (this.props.renderDepth > 7) return (null);
-        if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
+        if (!this.layout && this.props.layoutKey !== "overlayLayout") return (null);
         return <ObserverJsxParser
             blacklistedAttrs={[]}
             components={{ FormattedTextBox, ImageBox, IconBox, DirectoryImportBox, DragBox, ButtonBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, PresBox, YoutubeBox, LinkFollowBox }}
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index a91683c94..4ea200e6d 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -4,6 +4,8 @@
   position: inherit;
   top: 0;
   left:0;
+  border-radius: inherit;
+                    
  // background: $light-color; //overflow: hidden;
   transform-origin: left top;
 
@@ -45,4 +47,44 @@
 }
 .documentView-node-topmost {
     background: white;
+}
+
+.documentView-styleWrapper {
+    position: absolute; 
+    display: inline-block;
+    width:100%;
+    height:100%;
+    pointer-events: none;
+
+    .documentView-styleContentWrapper {
+        width:100%;
+        display: inline-block;
+        position: absolute;
+    }
+    .documentView-titleWrapper {
+        overflow:hidden; 
+        color: white; 
+        transform-origin: top left; 
+        top: 0; 
+        height: 25;
+        background: rgba(0, 0, 0, .4);
+        padding: 4px; 
+        text-align: center; 
+        text-overflow: ellipsis; 
+        white-space: pre;
+    }
+
+    .documentView-searchHighlight {
+        position: absolute; 
+        background: yellow; 
+        bottom: -20px;
+        border-radius: 5px;
+        transform-origin: bottom left; 
+    }
+
+    .documentView-captionWrapper {
+        position: absolute; 
+        bottom: 0; 
+        transform-origin: bottom left;
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9b97705a0..e98fc73e3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,15 +1,15 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, IReactionDisposer, reaction, runInAction, trace, observable } from "mobx";
+import { action, computed, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from "request-promise";
-import { Doc, DocListCast, DocListCastAsync, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
 import { Copy, Id } from '../../../new_fields/FieldSymbols';
 import { List } from "../../../new_fields/List";
 import { ObjectField } from "../../../new_fields/ObjectField";
 import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, FieldValue, NumCast, StrCast, PromiseValue } from "../../../new_fields/Types";
+import { BoolCast, Cast, FieldValue, NumCast, PromiseValue, StrCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { RouteStore } from '../../../server/RouteStore';
 import { emptyFunction, returnTrue, Utils } from "../../../Utils";
@@ -35,7 +35,6 @@ import { MainView } from '../MainView';
 import { OverlayView } from '../OverlayView';
 import { ScriptBox } from '../ScriptBox';
 import { ScriptingRepl } from '../ScriptingRepl';
-import { Template } from "./../Templates";
 import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
@@ -45,6 +44,7 @@ import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../../new_fields/URLField';
 import { IDisposable } from '../../northstar/utils/IDisposable';
 import SharingManager from '../../util/SharingManager';
+import { Scripting } from '../../util/Scripting';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(fa.faTrash);
@@ -69,19 +69,9 @@ library.add(fa.faUnlock);
 library.add(fa.faLock);
 library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCompass, fa.faSnowflake, fa.faMicrophone);
 
-// const linkSchema = createSchema({
-//     title: "string",
-//     linkDescription: "string",
-//     linkTags: "string",
-//     linkedTo: Doc,
-//     linkedFrom: Doc
-// });
-
-// type LinkDoc = makeInterface<[typeof linkSchema]>;
-// const LinkDoc = makeInterface(linkSchema);
-
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+    ContainingCollectionDoc: Opt<Doc>;
     Document: Doc;
     DataDoc?: Doc;
     fitToBox?: boolean;
@@ -93,47 +83,51 @@ export interface DocumentViewProps {
     renderDepth: number;
     showOverlays?: (doc: Doc) => { title?: string, caption?: string };
     ContentScaling: () => number;
+    ruleProvider: Doc | undefined;
     PanelWidth: () => number;
     PanelHeight: () => number;
-    focus: (doc: Doc, willZoom: boolean, scale?: number) => void;
+    focus: (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => void;
     parentActive: () => boolean;
     whenActiveChanged: (isActive: boolean) => void;
     bringToFront: (doc: Doc, sendToBack?: boolean) => void;
-    addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (doc: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
-    collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
     zoomToScale: (scale: number) => void;
     backgroundColor: (doc: Doc) => string | undefined;
     getScale: () => number;
-    animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
+    animateBetweenIcon?: (maximize: boolean, target: number[]) => void;
     ChromeHeight?: () => number;
 }
 
-const schema = createSchema({
-    layout: "string",
-    nativeWidth: "number",
-    nativeHeight: "number",
-    backgroundColor: "string",
-    opacity: "number",
-    hidden: "boolean",
-    onClick: ScriptField,
-});
-
-export const positionSchema = createSchema({
-    nativeWidth: "number",
-    nativeHeight: "number",
-    width: "number",
-    height: "number",
-    x: "number",
-    y: "number",
-    z: "number",
+export const documentSchema = createSchema({
+    // layout: "string", // this should be a "string" or Doc, but can't do that in schemas, so best to leave it out
+    title: "string",            // document title (can be on either data document or layout)
+    nativeWidth: "number",      // native width of document which determines how much document contents are scaled when the document's width is set
+    nativeHeight: "number",     // "
+    width: "number",            // width of document in its container's coordinate system
+    height: "number",           // "
+    backgroundColor: "string",  // background color of document
+    opacity: "number",          // opacity of document
+    onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    ignoreAspect: "boolean",    // whether aspect ratio should be ignored when laying out or manipulating the document
+    autoHeight: "boolean",      // whether the height of the document should be computed automatically based on its contents
+    isTemplate: "boolean",      // whether this document acts as a template layout for describing how other documents should be displayed
+    isBackground: "boolean",    // whether document is a background element and ignores input events (can only selet with marquee)
+    type: "string",             // enumerated type of document
+    maximizeLocation: "string", // flag for where to place content when following a click interaction (e.g., onRight, inPlace, inTab) 
+    lockedPosition: "boolean",  // whether the document can be spatially manipulated
+    borderRounding: "string",   // border radius rounding of document
+    searchFields: "string",     // the search fields to display when this document matches a search in its metadata
+    heading: "number",          // the logical layout 'heading' of this document (used by rule provider to stylize h1 header elements, from h2, etc)
+    showCaption: "string",      // whether editable caption text is overlayed at the bottom of the document 
+    showTitle: "string",        // whether an editable title banner is displayed at tht top of the document
+    isButton: "boolean",        // whether document functions as a button (overiding native interactions of its content)      
+    ignoreClick: "boolean",     // whether documents ignores input clicks (but does not ignore manipulation and other events) 
 });
 
-export type PositionDocument = makeInterface<[typeof positionSchema]>;
-export const PositionDocument = makeInterface(positionSchema);
 
-type Document = makeInterface<[typeof schema]>;
-const Document = makeInterface(schema);
+type Document = makeInterface<[typeof documentSchema]>;
+const Document = makeInterface(documentSchema);
 
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
@@ -141,107 +135,41 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _downY: number = 0;
     private _lastTap: number = 0;
     private _doubleTap = false;
-    private _hitExpander = false;
     private _hitTemplateDrag = false;
     private _mainCont = React.createRef<HTMLDivElement>();
     private _dropDisposer?: DragManager.DragDropDisposer;
-    _animateToIconDisposer?: IReactionDisposer;
-    _reactionDisposer?: IReactionDisposer;
 
     public get ContentDiv() { return this._mainCont.current; }
-    @computed get active(): boolean { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
-    @computed get topMost(): boolean { return this.props.renderDepth === 0; }
-    screenRect = (): ClientRect | DOMRect => this._mainCont.current ? this._mainCont.current.getBoundingClientRect() : new DOMRect();
+    @computed get active() { return SelectionManager.IsSelected(this) || this.props.parentActive(); }
+    @computed get topMost() { return this.props.renderDepth === 0; }
+    @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
+    @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
+    @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
 
     @action
     componentDidMount() {
-        if (this._mainCont.current) {
-            this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
-                handlers: { drop: this.drop.bind(this) }
-            });
-        }
-        // bcz: kind of ugly .. setup a reaction to update the title of a summary document's target (maximizedDocs) whenver the summary doc's title changes
-        this._reactionDisposer = reaction(() => [DocListCast(this.props.Document.maximizedDocs).map(md => md.title),
-        this.props.Document.summaryDoc, this.props.Document.summaryDoc instanceof Doc ? this.props.Document.summaryDoc.title : ""],
-            () => {
-                let maxDoc = DocListCast(this.props.Document.maximizedDocs);
-                if (maxDoc.length === 1 && StrCast(this.props.Document.title).startsWith("-") && StrCast(this.props.Document.layout).indexOf("IconBox") !== -1) {
-                    this.props.Document.proto!.title = "-" + maxDoc[0].title + ".icon";
-                }
-                let sumDoc = Cast(this.props.Document.summaryDoc, Doc);
-                if (sumDoc instanceof Doc && StrCast(this.props.Document.title).startsWith("-")) {
-                    this.props.Document.proto!.title = "-" + sumDoc.title + ".expanded";
-                }
-            }, { fireImmediately: true });
-        this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
-            (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
-            , { fireImmediately: true });
+        this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }));
         DocumentManager.Instance.DocumentViews.push(this);
     }
 
-    animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
-        this.props.animateBetweenIcon ? this.props.animateBetweenIcon(iconPos, startTime, maximizing) :
-            DocumentView.animateBetweenIconFunc(this.props.Document, this.Document[WidthSym](), this.Document[HeightSym](), startTime, maximizing);
-    }
-
-    public static animateBetweenIconFunc = (doc: Doc, width: number, height: number, stime: number, maximizing: boolean, cb?: (progress: number) => void) => {
-        setTimeout(() => {
-            let now = Date.now();
-            let progress = now < stime + 200 ? Math.min(1, (now - stime) / 200) : 1;
-            doc.width = progress === 1 ? width : maximizing ? 25 + (width - 25) * progress : width + (25 - width) * progress;
-            doc.height = progress === 1 ? height : maximizing ? 25 + (height - 25) * progress : height + (25 - height) * progress;
-            cb && cb(progress);
-            if (now < stime + 200) {
-                DocumentView.animateBetweenIconFunc(doc, width, height, stime, maximizing, cb);
-            }
-            else {
-                doc.isMinimized = !maximizing;
-                doc.isIconAnimating = undefined;
-            }
-            doc.willMaximize = false;
-        },
-            2);
-    }
     @action
     componentDidUpdate() {
         this._dropDisposer && this._dropDisposer();
-        if (this._mainCont.current) {
-            this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, {
-                handlers: { drop: this.drop.bind(this) }
-            });
-        }
+        this._mainCont.current && (this._dropDisposer = DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } }));
     }
+
     @action
     componentWillUnmount() {
-        this._reactionDisposer && this._reactionDisposer();
-        this._animateToIconDisposer && this._animateToIconDisposer();
         this._dropDisposer && this._dropDisposer();
         DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
     }
 
-    stopPropagation = (e: React.SyntheticEvent) => {
-        e.stopPropagation();
-    }
-
-    get dataDoc() {
-        if (this.props.DataDoc === undefined && (this.props.Document.layout instanceof Doc || this.props.Document instanceof Promise)) {
-            // if there is no dataDoc (ie, we're not rendering a temlplate layout), but this document
-            // has a template layout document, then we will render the template layout but use 
-            // this document as the data document for the layout.
-            return this.props.Document;
-        }
-        return this.props.DataDoc !== this.props.Document ? this.props.DataDoc : undefined;
-    }
-    startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean, applyAsTemplate?: boolean) {
+    startDragging(x: number, y: number, dropAction: dropActionType, applyAsTemplate?: boolean) {
         if (this._mainCont.current) {
-            let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
-            let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+            let dragData = new DragManager.DocumentDragData([this.props.Document]);
             const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
-            let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected);
-            const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
+            dragData.offset = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
             dragData.dropAction = dropAction;
-            dragData.xOffset = xoff;
-            dragData.yOffset = yoff;
             dragData.moveDocument = this.props.moveDocument;
             dragData.applyAsTemplate = applyAsTemplate;
             DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, {
@@ -252,151 +180,79 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             });
         }
     }
-    toggleMinimized = async () => {
-        let minimizedDoc = await Cast(this.props.Document.minimizedDoc, Doc);
-        if (minimizedDoc) {
-            let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(
-                NumCast(minimizedDoc.x) - NumCast(this.Document.x), NumCast(minimizedDoc.y) - NumCast(this.Document.y));
-            this.collapseTargetsToPoint(scrpt, await DocListCastAsync(minimizedDoc.maximizedDocs));
-        }
-    }
-
-    static _undoBatch?: UndoManager.Batch = undefined;
-    @action
-    public collapseTargetsToPoint = (scrpt: number[], expandedDocs: Doc[] | undefined): void => {
-        SelectionManager.DeselectAll();
-        if (expandedDocs) {
-            if (!DocumentView._undoBatch) {
-                DocumentView._undoBatch = UndoManager.StartBatch("iconAnimating");
-            }
-            let isMinimized: boolean | undefined;
-            expandedDocs.map(maximizedDoc => {
-                let iconAnimating = Cast(maximizedDoc.isIconAnimating, List);
-                if (!iconAnimating || (Date.now() - iconAnimating[2] > 1000)) {
-                    if (isMinimized === undefined) {
-                        isMinimized = BoolCast(maximizedDoc.isMinimized);
-                    }
-                    maximizedDoc.willMaximize = isMinimized;
-                    maximizedDoc.isMinimized = false;
-                    maximizedDoc.isIconAnimating = new List<number>([scrpt[0], scrpt[1], Date.now(), isMinimized ? 1 : 0]);
-                }
-            });
-            setTimeout(() => {
-                DocumentView._undoBatch && DocumentView._undoBatch.end();
-                DocumentView._undoBatch = undefined;
-            }, 500);
-        }
-    }
 
     onClick = async (e: React.MouseEvent) => {
-        if (e.nativeEvent.cancelBubble) return; // needed because EditableView may stopPropagation which won't apparently stop this event from firing.
-        if (this.onClickHandler && this.onClickHandler.script) {
+        if (!e.nativeEvent.cancelBubble && !this.Document.ignoreClick && CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
+            (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
             e.stopPropagation();
-            this.onClickHandler.script.run({ this: this.props.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document });
             e.preventDefault();
-            return;
+            if (this._doubleTap && this.props.renderDepth) {
+                let fullScreenAlias = Doc.MakeAlias(this.props.Document);
+                let layoutNative = await PromiseValue(Cast(this.props.Document.layoutNative, Doc));
+                if (layoutNative && fullScreenAlias.layout === layoutNative.layout) {
+                    await swapViews(fullScreenAlias, "layoutCustom", "layoutNative");
+                }
+                this.props.addDocTab(fullScreenAlias, undefined, "inTab");
+                SelectionManager.DeselectAll();
+                Doc.UnBrushDoc(this.props.Document);
+            } else if (this.onClickHandler && this.onClickHandler.script) {
+                this.onClickHandler.script.run({ this: this.Document.isTemplate && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
+            } else if (this.Document.isButton) {
+                SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
+                this.buttonClick(e.altKey, e.ctrlKey);
+            } else SelectionManager.SelectDoc(this, e.ctrlKey);
         }
-        let altKey = e.altKey;
-        let ctrlKey = e.ctrlKey;
-        if (this._doubleTap && this.props.renderDepth) {
-            e.stopPropagation();
-            let fullScreenAlias = Doc.MakeAlias(this.props.Document);
-            fullScreenAlias.templates = new List<string>();
-            Doc.UseDetailLayout(fullScreenAlias);
-            fullScreenAlias.showCaption = true;
-            this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");
+    }
+
+    buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
+        let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
+        let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
+        let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
+        let expandedDocs: Doc[] = [];
+        expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
+        expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
+        // let expandedDocs = [ ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
+        if (expandedDocs.length) {
             SelectionManager.DeselectAll();
-            Doc.UnBrushDoc(this.props.Document);
-        }
-        else if (CurrentUserUtils.MainDocId !== this.props.Document[Id] &&
-            (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
-                Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD)) {
-            if (BoolCast(this.props.Document.ignoreClick)) {
-                return;
+            let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace");
+            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, false));
+                let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
+                DocumentManager.Instance.animateBetweenPoint(scrpt, expandedDocs);
+            } else {
+                expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
             }
-            e.stopPropagation();
-            SelectionManager.SelectDoc(this, e.ctrlKey);
-            let isExpander = (e.target as any).id === "isExpander";
-            if (BoolCast(this.props.Document.isButton) || this.props.Document.type === DocumentType.BUTTON || isExpander) {
-                let subBulletDocs = await DocListCastAsync(this.props.Document.subBulletDocs);
-                let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
-                let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
-                let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
-                let expandedDocs: Doc[] = [];
-                expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
-                expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
-                expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
-                // let expandedDocs = [...(subBulletDocs ? subBulletDocs : []), ...(maximizedDocs ? maximizedDocs : []), ...(summarizedDocs ? summarizedDocs : []),];
-                if (expandedDocs.length) {   // bcz: need a better way to associate behaviors with click events on widget-documents
-                    SelectionManager.DeselectAll();
-                    let maxLocation = StrCast(this.props.Document.maximizeLocation, "inPlace");
-                    let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
-                    if (altKey || ctrlKey) {
-                        maxLocation = this.props.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" || !maxLocation ? "inTab" : "inPlace"));
-                        if (!maxLocation || maxLocation === "inPlace") {
-                            let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
-                            let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !BoolCast(d.IsMinimized), false);
-                            expandedDocs.forEach(maxDoc => Doc.GetProto(maxDoc).isMinimized = false);
-                            let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
-                            if (!hasView) {
-                                this.props.addDocument && expandedDocs.forEach(async maxDoc => this.props.addDocument!(getDispDoc(maxDoc), false));
-                            }
-                            expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
-                        }
-                    }
-                    if (maxLocation && maxLocation !== "inPlace" && CollectionDockingView.Instance) {
-                        let dataDocs = DocListCast(CollectionDockingView.Instance.props.Document.data);
-                        if (dataDocs) {
-                            expandedDocs.forEach(maxDoc =>
-                                (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
-                                    this.props.addDocTab(getDispDoc(maxDoc), undefined, maxLocation)));
-                        }
-                    } else {
-                        let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
-                        this.collapseTargetsToPoint(scrpt, expandedDocs);
-                    }
-                }
-                else if (linkedDocs.length) {
-                    SelectionManager.DeselectAll();
-                    let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document));
-                    let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
-
-                    // @TODO: shouldn't always follow target context
-                    let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
-
-                    let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
-
-                    if (!linkedFwdDocs.some(l => l instanceof Promise)) {
-                        let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
-                        let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
-                        DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
-                            document => {  // open up target if it's not already in view ...
-                                let cv = this.props.ContainingCollectionView;  // bcz: ugh --- maybe need to have a props.unfocus() method so that we leave things in the state we found them??
-                                let px = cv && cv.props.Document.panX;
-                                let py = cv && cv.props.Document.panY;
-                                let s = cv && cv.props.Document.scale;
-                                this.props.focus(this.props.Document, true, 1);  // by zooming into the button document first
-                                setTimeout(() => {
-                                    this.props.addDocTab(document, undefined, maxLocation);
-                                    cv && (cv.props.Document.panX = px);
-                                    cv && (cv.props.Document.panY = py);
-                                    cv && (cv.props.Document.scale = s);
-                                }, 1000); // then after the 1sec animation, open up the target in a new tab
-                            },
-                            linkedFwdPage[altKey ? 1 : 0], targetContext);
-                    }
-                }
+        }
+        else if (linkedDocs.length) {
+            SelectionManager.DeselectAll();
+            let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored);
+            let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+            if (firstUnshown.length) first = [firstUnshown[0]];
+            let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[0]];
+
+            // @TODO: shouldn't always follow target context
+            let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
+            let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
+
+            if (!linkedFwdDocs.some(l => l instanceof Promise)) {
+                let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
+                let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
+                DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
+                    // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
+                    doc => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
+                    linkedFwdPage[altKey ? 1 : 0], targetContext);
             }
         }
     }
 
-
     onPointerDown = (e: React.PointerEvent): void => {
         if (e.nativeEvent.cancelBubble) return;
         this._downX = e.clientX;
         this._downY = e.clientY;
-        this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
         this._hitTemplateDrag = false;
+        // this whole section needs to move somewhere else.  We're trying to initiate a special "template" drag where
+        // this document is the template and we apply it to whatever we drop it on.
         for (let element = (e.target as any); element && !this._hitTemplateDrag; element = element.parentElement) {
             if (element.className && element.className.toString() === "collectionViewBaseChrome-collapse") {
                 this._hitTemplateDrag = true;
@@ -413,11 +269,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             document.removeEventListener("pointermove", this.onPointerMove);
         }
         else if (!e.cancelBubble && this.active) {
-            if (!this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3)) {
-                if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.props.Document.lockedPosition)) {
+            if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+                if (!e.altKey && !this.topMost && e.buttons === 1 && !BoolCast(this.Document.lockedPosition)) {
                     document.removeEventListener("pointermove", this.onPointerMove);
                     document.removeEventListener("pointerup", this.onPointerUp);
-                    this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitExpander, this._hitTemplateDrag);
+                    this.startDragging(this._downX, this._downY, e.ctrlKey || e.altKey ? "alias" : undefined, this._hitTemplateDrag);
                 }
             }
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -435,77 +291,72 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
 
     @undoBatch
-    fieldsClicked = (): void => {
-        let kvp = Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 });
-        this.props.addDocTab(kvp, this.dataDoc, "onRight");
-    }
+    static makeNativeViewClicked = (doc: Doc): void => { swapViews(doc, "layoutNative", "layoutCustom"); }
 
     @undoBatch
-    makeBtnClicked = (): void => {
-        let doc = Doc.GetProto(this.props.Document);
-        if (doc.isButton || doc.onClick) {
-            doc.isButton = false;
-            doc.onClick = undefined;
+    static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => {
+        if (doc.layoutCustom === undefined) {
+            Doc.GetProto(dataDoc || doc).layoutNative = Doc.MakeTitled("layoutNative");
+            await swapViews(doc, "", "layoutNative");
+
+            const width = NumCast(doc.width);
+            const height = NumCast(doc.height);
+            const options = { title: "data", width, x: -width / 2, y: - height / 2, };
+            let fieldTemplate = doc.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) :
+                doc.type === DocumentType.VID ? Docs.Create.VideoDocument("http://www.cs.brown.edu", options) :
+                    Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
+
+            fieldTemplate.backgroundColor = doc.backgroundColor;
+            fieldTemplate.heading = 1;
+            fieldTemplate.autoHeight = true;
+
+            let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: doc.title + "_layout", width: width + 20, height: Math.max(100, height + 45) });
+
+            Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate), true);
+            Doc.ApplyTemplateTo(docTemplate, doc, undefined);
+            Doc.GetProto(dataDoc || doc).layoutCustom = Doc.MakeTitled("layoutCustom");
         } else {
-            doc.isButton = true;
+            swapViews(doc, "layoutCustom", "layoutNative");
         }
-
-        // if (doc.isButton) {
-        //     if (!doc.nativeWidth) {
-        //         doc.nativeWidth = this.props.Document[WidthSym]();
-        //         doc.nativeHeight = this.props.Document[HeightSym]();
-        //     }
-        // } else {
-        //     doc.nativeWidth = doc.nativeHeight = undefined;
-        // }
     }
 
     @undoBatch
-    public fullScreenClicked = (): void => {
-        CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this);
-        SelectionManager.DeselectAll();
+    makeBtnClicked = (): void => {
+        if (this.Document.isButton || this.Document.onClick || this.Document.ignoreClick) {
+            this.Document.isButton = false;
+            this.Document.ignoreClick = false;
+            this.Document.onClick = undefined;
+        } else {
+            this.Document.isButton = true;
+        }
     }
 
     @undoBatch
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.AnnotationDragData) {
+            /// this whole section for handling PDF annotations looks weird.  Need to rethink this to make it cleaner
             e.stopPropagation();
-            let annotationDoc = de.data.annotationDocument;
-            annotationDoc.linkedToDoc = true;
-            de.data.targetContext = this.props.ContainingCollectionView!.props.Document;
+            let sourceDoc = de.data.annotationDocument;
             let targetDoc = this.props.Document;
+            let annotations = await DocListCastAsync(sourceDoc.annotations);
+            sourceDoc.linkedToDoc = true;
+            de.data.targetContext = this.props.ContainingCollectionDoc;
             targetDoc.targetContext = de.data.targetContext;
-            let annotations = await DocListCastAsync(annotationDoc.annotations);
             annotations && annotations.forEach(anno => anno.target = targetDoc);
 
-            DocUtils.MakeLink(annotationDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(annotationDoc.title)}`);
+            DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionDoc, `Link from ${StrCast(sourceDoc.title)}`);
         }
         if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) {
-            Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, this.props.DataDoc);
+            Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document);
             e.stopPropagation();
         }
         if (de.data instanceof DragManager.LinkDragData) {
-            let sourceDoc = de.data.linkSourceDocument;
-            let destDoc = this.props.Document;
-
             e.stopPropagation();
-            if (de.mods === "AltKey") {
-                const protoDest = destDoc.proto;
-                const protoSrc = sourceDoc.proto;
-                let src = protoSrc ? protoSrc : sourceDoc;
-                let dst = protoDest ? protoDest : destDoc;
-                dst.data = (src.data! as ObjectField)[Copy]();
-                dst.nativeWidth = src.nativeWidth;
-                dst.nativeHeight = src.nativeHeight;
-            }
-            else {
-                // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
-                // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
-                let linkDoc = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView ? this.props.ContainingCollectionView.props.Document : undefined);
-                de.data.droppedDocuments.push(destDoc);
-                de.data.linkDocument = linkDoc;
-            }
+            // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
+            // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
+            de.data.linkSourceDocument !== this.props.Document &&
+                (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc));
         }
     }
 
@@ -513,9 +364,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     onDrop = (e: React.DragEvent) => {
         let text = e.dataTransfer.getData("text/plain");
         if (!e.isDefaultPrevented() && text && text.startsWith("<div")) {
-            let oldLayout = FieldValue(this.Document.layout) || "";
+            let oldLayout = StrCast(this.props.Document.layout);
             let layout = text.replace("{layout}", oldLayout);
-            this.Document.layout = layout;
+            this.props.Document.layout = layout;
             e.stopPropagation();
             e.preventDefault();
         }
@@ -524,38 +375,50 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     freezeNativeDimensions = (): void => {
-        let proto = this.props.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
-        this.props.Document.autoHeight = proto.autoHeight = false;
-        proto.ignoreAspect = !BoolCast(proto.ignoreAspect);
-        if (!BoolCast(proto.ignoreAspect) && !proto.nativeWidth) {
+        let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
+        proto.autoHeight = this.Document.autoHeight = false;
+        proto.ignoreAspect = !proto.ignoreAspect;
+        if (!proto.ignoreAspect && !proto.nativeWidth) {
             proto.nativeWidth = this.props.PanelWidth();
             proto.nativeHeight = this.props.PanelHeight();
         }
     }
+
+    @undoBatch
+    @action
+    makeIntoPortal = async () => {
+        let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
+        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 });
+                DocUtils.MakeLink(this.props.Document, portal, undefined, portalID);
+                this.Document.isButton = true;
+            });
+        }
+    }
+
     @undoBatch
     @action
-    makeIntoPortal = (): void => {
-        if (!DocListCast(this.props.Document.links).find(doc => {
-            if (Cast(doc.anchor2, Doc) instanceof Doc && (Cast(doc.anchor2, Doc) as Doc).title === this.props.Document.title + ".portal") return true;
-            return false;
-        })) {
-            let portal = Docs.Create.FreeformDocument([], { width: this.props.Document[WidthSym]() + 10, height: this.props.Document[HeightSym](), title: this.props.Document.title + ".portal" });
-            DocUtils.MakeLink(this.props.Document, portal, undefined, this.props.Document.title + ".portal");
-            Doc.GetProto(this.props.Document).isButton = true;
+    setCustomView = (custom: boolean): void => {
+        if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.DataDoc) {
+            Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.ContainingCollectionView.props.DataDoc);
+        } else { // bcz: not robust -- for now documents with string layout are native documents, and those with Doc layouts are customized
+            custom ? DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc) : DocumentView.makeNativeViewClicked(this.props.Document);
         }
     }
 
     @undoBatch
     @action
     makeBackground = (): void => {
-        this.layoutDoc.isBackground = !this.layoutDoc.isBackground;
-        this.layoutDoc.isBackground && this.props.bringToFront(this.layoutDoc, true);
+        this.Document.isBackground = !this.Document.isBackground;
+        this.Document.isBackground && this.props.bringToFront(this.Document, true);
     }
 
     @undoBatch
     @action
     toggleLockPosition = (): void => {
-        this.layoutDoc.lockedPosition = BoolCast(this.layoutDoc.lockedPosition) ? undefined : true;
+        this.Document.lockedPosition = this.Document.lockedPosition ? undefined : true;
     }
 
     listen = async () => {
@@ -570,48 +433,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
     }
 
-    public static makeNativeViewClicked = undoBatch((document: Doc): void => {
-        document.customLayout = document.layout;
-        document.layout = document.nativeLayout;
-        document.type = document.nativeType;
-        document.nativeWidth = document.nativeNativeWidth;
-        document.nativeHeight = document.nativeNativeHeight;
-        document.ignoreAspect = document.nativeIgnoreAspect;
-        document.nativeLayout = undefined;
-        document.nativeNativeWidth = undefined;
-        document.nativeNativeHeight = undefined;
-        document.nativeIgnoreAspect = undefined;
-    });
-
-    public static makeCustomViewClicked = undoBatch((document: Doc, showTitle = undefined): void => {
-        document.nativeLayout = document.layout;
-        document.nativeType = document.type;
-        document.nativeNativeWidth = document.nativeWidth;
-        document.nativeNativeHeight = document.nativeHeight;
-        document.nativeIgnoreAspect = document.ignoreAspect;
-        PromiseValue(Cast(document.customLayout, Doc)).then(custom => {
-            if (custom) {
-                document.type = DocumentType.TEMPLATE;
-                document.layout = custom;
-                !custom.nativeWidth && (document.nativeWidth = 0);
-                !custom.nativeHeight && (document.nativeHeight = 0);
-                !custom.nativeWidth && (document.ignoreAspect = true);
-            } else {
-                let options = { title: "data", width: NumCast(document.width), height: NumCast(document.height) + 25, x: -NumCast(document.width) / 2, y: -NumCast(document.height) / 2, };
-                let fieldTemplate = document.type === DocumentType.TEXT ? Docs.Create.TextDocument(options) : Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
-
-                let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(document.title) + "layout", width: NumCast(document.width) + 20, height: Math.max(100, NumCast(document.height) + 45) });
-                let metaKey = "data";
-                let proto = Doc.GetProto(docTemplate);
-                Doc.MakeTemplate(fieldTemplate, metaKey, proto);
-                fieldTemplate.showTitle = showTitle;
-
-                Doc.ApplyTemplateTo(docTemplate, document, undefined, false);
-                document.customLayout = document.layout;
-            }
-        });
-    });
-
     @action
     onContextMenu = async (e: React.MouseEvent): Promise<void> => {
         e.persist();
@@ -625,13 +446,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         const cm = ContextMenu.Instance;
         let subitems: ContextMenuProps[] = [];
-        subitems.push({ description: "Open Full Screen", event: this.fullScreenClicked, icon: "desktop" });
-        subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.dataDoc, "onRight"), icon: "caret-square-right" });
-        subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.dataDoc, "onRight"), icon: "caret-square-right" });
-        subitems.push({ description: "Open Fields", event: this.fieldsClicked, icon: "layer-group" });
+        subitems.push({ description: "Open Full Screen", event: () => CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(this), icon: "desktop" });
+        subitems.push({ description: "Open Tab        ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" });
+        subitems.push({ description: "Open Right      ", event: () => this.props.addDocTab(this.props.Document, this.props.DataDoc, "onRight"), icon: "caret-square-right" });
+        subitems.push({ description: "Open Alias Tab  ", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
+        subitems.push({ description: "Open Alias Right", event: () => this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "onRight"), icon: "caret-square-right" });
+        subitems.push({ description: "Open Fields     ", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
         cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
+
         if (Cast(this.props.Document.data, ImageField)) {
             cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
         }
@@ -640,25 +462,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.TagChildImages(this.props.Document), icon: "caret-square-right" });
             cm.addItem({ description: "Write Back Link to Album", event: () => GooglePhotos.Transactions.AddTextEnrichment(this.props.Document), icon: "caret-square-right" });
         }
-        let existingMake = ContextMenu.Instance.findByDescription("Make...");
-        let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
-        makes.push({ description: this.props.Document.isBackground ? "Remove Background" : "Into Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" });
-        makes.push({ description: "Custom Document View", event: () => DocumentView.makeCustomViewClicked(this.props.Document), icon: "concierge-bell" });
-        makes.push({ description: "Metadata Field View", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" });
-        makes.push({ description: "Into Portal", event: this.makeIntoPortal, icon: "window-restore" });
-        makes.push({ description: this.layoutDoc.ignoreClick ? "Selectable" : "Unselectable", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" });
-        !existingMake && cm.addItem({ description: "Make...", subitems: makes, icon: "hand-point-right" });
-
 
         let existingOnClick = ContextMenu.Instance.findByDescription("OnClick...");
         let onClicks: ContextMenuProps[] = existingOnClick && "subitems" in existingOnClick ? existingOnClick.subitems : [];
         onClicks.push({ description: "Enter Portal", event: this.makeIntoPortal, icon: "window-restore" });
-        onClicks.push({ description: this.layoutDoc.ignoreClick ? "Select" : "Do Nothing", event: () => this.layoutDoc.ignoreClick = !this.layoutDoc.ignoreClick, icon: this.layoutDoc.ignoreClick ? "unlock" : "lock" });
-        onClicks.push({ description: this.props.Document.isButton || this.props.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
+        onClicks.push({ description: "Toggle Detail", event: () => this.Document.onClick = ScriptField.MakeScript("toggleDetail(this)"), icon: "window-restore" });
+        onClicks.push({ description: this.Document.ignoreClick ? "Select" : "Do Nothing", event: () => this.Document.ignoreClick = !this.Document.ignoreClick, icon: this.Document.ignoreClick ? "unlock" : "lock" });
+        onClicks.push({ description: this.Document.isButton || this.Document.onClick ? "Remove Click Behavior" : "Follow Link", event: this.makeBtnClicked, icon: "concierge-bell" });
         onClicks.push({ description: "Edit onClick Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", obj.x, obj.y) });
         onClicks.push({
             description: "Edit onClick Foreach Doc Script", icon: "edit", event: (obj: any) => {
-                this.props.Document.collectionContext = this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document;
+                this.props.Document.collectionContext = this.props.ContainingCollectionDoc;
                 ScriptBox.EditButtonScript("Foreach Collection Doc (d) => ", this.props.Document, "onClick", obj.x, obj.y, "docList(this.collectionContext.data).map(d => {", "});\n");
             }
         });
@@ -666,22 +480,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         let existing = ContextMenu.Instance.findByDescription("Layout...");
         let layoutItems: ContextMenuProps[] = existing && "subitems" in existing ? existing.subitems : [];
-        layoutItems.push({ description: this.props.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.props.Document.lockedPosition ? "unlock" : "lock" });
-        if (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.layout instanceof Doc) {
-            layoutItems.push({ description: "Make View of Metadata Field", event: () => this.props.ContainingCollectionView && Doc.MakeTemplate(this.props.Document, StrCast(this.props.Document.title), this.props.ContainingCollectionView.props.Document), icon: "concierge-bell" });
+        layoutItems.push({ description: this.Document.isBackground ? "As Foreground" : "As Background", event: this.makeBackground, icon: this.Document.lockedPosition ? "unlock" : "lock" });
+        if (this.props.DataDoc) {
+            layoutItems.push({ description: "Make View of Metadata Field", event: () => Doc.MakeMetadataFieldTemplate(this.props.Document, this.props.DataDoc!), icon: "concierge-bell" });
         }
-        layoutItems.push({ description: `${this.layoutDoc.chromeStatus !== "disabled" ? "Hide" : "Show"} Chrome`, event: () => this.layoutDoc.chromeStatus = (this.layoutDoc.chromeStatus !== "disabled" ? "disabled" : "enabled"), icon: "project-diagram" });
-        layoutItems.push({ description: `${this.layoutDoc.autoHeight ? "Variable Height" : "Auto Height"}`, event: () => this.layoutDoc.autoHeight = !this.layoutDoc.autoHeight, icon: "plus" });
-        layoutItems.push({ description: this.props.Document.ignoreAspect || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "snowflake" });
-        layoutItems.push({ description: this.layoutDoc.lockedPosition ? "Unlock Position" : "Lock Position", event: this.toggleLockPosition, icon: BoolCast(this.layoutDoc.lockedPosition) ? "unlock" : "lock" });
+        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.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" });
         layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
-        if (this.props.Document.detailedLayout && !this.props.Document.isTemplate) {
-            layoutItems.push({ description: "Toggle detail", event: () => Doc.ToggleDetailLayout(this.props.Document), icon: "image" });
-        }
-        if (this.props.Document.type !== DocumentType.COL && this.props.Document.type !== DocumentType.TEMPLATE) {
-            layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document), icon: "concierge-bell" });
-        } else if (this.props.Document.nativeLayout) {
+        if (this.Document.type !== DocumentType.COL && this.Document.type !== DocumentType.TEMPLATE) {
+            layoutItems.push({ description: "Use Custom Layout", event: () => DocumentView.makeCustomViewClicked(this.props.Document, this.props.DataDoc), icon: "concierge-bell" });
+        } else if (this.props.Document.layoutNative) {
             layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" });
         }
         !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
@@ -697,19 +508,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         cm.addItem({ description: "Pin to Presentation", event: () => this.props.pinToPres(this.props.Document), icon: "map-pin" }); //I think this should work... and it does! A miracle!
         cm.addItem({ description: "Add Repl", icon: "laptop-code", event: () => OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" }) });
         cm.addItem({
-            description: "Download document", icon: "download", event: async () => {
-                let y = JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
+            description: "Download document", icon: "download", event: async () =>
+                console.log(JSON.parse(await rp.get(Utils.CorsProxy("http://localhost:8983/solr/dash/select"), {
                     qs: { q: 'world', fq: 'NOT baseProto_b:true AND NOT deleted:true', start: '0', rows: '100', hl: true, 'hl.fl': '*' }
-                }));
-                console.log(y);
-                // const a = document.createElement("a");
-                // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
-                // a.href = url;
-                // a.download = `DocExport-${this.props.Document[Id]}.zip`;
-                // a.click();
-            }
+                })))
+            // const a = document.createElement("a");
+            // const url = Utils.prepend(`/downloadId/${this.props.Document[Id]}`);
+            // a.href = url;
+            // a.download = `DocExport-${this.props.Document[Id]}.zip`;
+            // a.click();
         });
 
+        cm.addItem({ description: "Publish", event: () => DocUtils.Publish(this.props.Document, this.Document.title || "", this.props.addDocument, this.props.removeDocument), icon: "file" });
         cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
         runInAction(() => {
             if (!ClientUtils.RELEASE) {
@@ -754,80 +564,90 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
     }
 
-    onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
-    onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
 
-    isSelected = () => SelectionManager.IsSelected(this);
-    @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
-    @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
-    @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
-    @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
-    @computed get contents() {
-        return (<DocumentContentsView {...this.props}
-            ChromeHeight={this.chromeHeight}
-            isSelected={this.isSelected}
-            select={this.select}
-            onClick={this.onClickHandler}
-            layoutKey={"layout"}
-            fitToBox={BoolCast(this.props.Document.fitToBox) ? true : this.props.fitToBox}
-            DataDoc={this.dataDoc} />);
+    // 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
+    get layoutDoc(): Document {
+        return Document(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document);
     }
 
-    chromeHeight = () => {
-        let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
-        let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
-        let templates = Cast(this.layoutDoc.templates, listSpec("string"));
-        if (!showOverlays && templates instanceof List) {
-            templates.map(str => {
-                if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
-            });
-        }
-        return (showTitle ? 25 : 0) + 1;// bcz: why 8??
-    }
+    // does Document set a layout prop 
+    setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
+    // get the a layout prop by first choosing the prop from Document, then falling back to the layout doc otherwise.
+    getLayoutPropStr = (prop: string) => StrCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
+    getLayoutPropNum = (prop: string) => NumCast(this.setsLayoutProp(prop) ? this.props.Document[prop] : this.layoutDoc[prop]);
 
-    get layoutDoc() {
-        // if this document's layout field contains a document (ie, a rendering template), then we will use that
-        // to determine the render JSX string, otherwise the layout field should directly contain a JSX layout string.
-        return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
-    }
+    isSelected = () => SelectionManager.IsSelected(this);
+    select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
 
+    chromeHeight = () => {
+        let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
+        let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.Document.showTitle);
+        return (showTitle ? 25 : 0) + 1;
+    }
 
     render() {
-        let backgroundColor = this.layoutDoc.isBackground || (this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground && this.layoutDoc.backgroundColor === this.layoutDoc.defaultBackgroundColor) ?
-            this.props.backgroundColor(this.layoutDoc) || StrCast(this.layoutDoc.backgroundColor) :
-            StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.layoutDoc);
-        let foregroundColor = StrCast(this.layoutDoc.color);
-        var nativeWidth = this.nativeWidth > 0 && !BoolCast(this.props.Document.ignoreAspect) ? `${this.nativeWidth}px` : "100%";
-        var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
-        let showOverlays = this.props.showOverlays ? this.props.showOverlays(this.layoutDoc) : undefined;
-        let showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : StrCast(this.layoutDoc.showTitle);
-        let showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : StrCast(this.layoutDoc.showCaption);
-        let templates = Cast(this.layoutDoc.templates, listSpec("string"));
-        if (!showOverlays && templates instanceof List) {
-            templates.map(str => {
-                if (!showTitle && str.indexOf("{props.Document.title}") !== -1) showTitle = "title";
-                if (!showCaption && str.indexOf("fieldKey={\"caption\"}") !== -1) showCaption = "caption";
-            });
-        }
-        let showTextTitle = showTitle && StrCast(this.layoutDoc.layout).startsWith("<FormattedTextBox") ? showTitle : undefined;
-        let fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
-        let borderRounding = StrCast(Doc.GetProto(this.props.Document).borderRounding);
-        let localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
-        let searchHighlight = (!this.props.Document.search_fields ? (null) :
-            <div key="search" style={{ position: "absolute", background: "yellow", bottom: "-20px", borderRadius: "5px", transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
-                {StrCast(this.props.Document.search_fields)}
+        const ruleColor = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleColor_" + this.Document.heading]) : undefined;
+        const ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
+        const colorSet = this.setsLayoutProp("backgroundColor");
+        const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground;
+        const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ?
+            this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
+            ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
+
+        const nativeWidth = this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
+        const nativeHeight = this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+        const showOverlays = this.props.showOverlays ? this.props.showOverlays(this.Document) : undefined;
+        const showTitle = showOverlays && "title" in showOverlays ? showOverlays.title : this.getLayoutPropStr("showTitle");
+        const showCaption = showOverlays && "caption" in showOverlays ? showOverlays.caption : this.getLayoutPropStr("showCaption");
+        const showTextTitle = showTitle && StrCast(this.Document.layout).indexOf("FormattedTextBox") !== -1 ? showTitle : undefined;
+        const fullDegree = Doc.isBrushedHighlightedDegree(this.props.Document);
+        const borderRounding = this.getLayoutPropStr("borderRounding") || ruleRounding;
+        const localScale = this.props.ScreenToLocalTransform().Scale * fullDegree;
+        const searchHighlight = (!this.Document.searchFields ? (null) :
+            <div className="documentView-searchHighlight" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+                {this.Document.searchFields}
+            </div>);
+        const captionView = (!showCaption ? (null) :
+            <div className="documentView-captionWrapper" style={{ width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
+                <FormattedTextBox {...this.props}
+                    onClick={this.onClickHandler} DataDoc={this.props.DataDoc} active={returnTrue}
+                    isSelected={this.isSelected} focus={emptyFunction} select={this.select}
+                    fieldExt={""} hideOnLeave={true} fieldKey={showCaption}
+                />
+            </div>);
+        const titleView = (!showTitle ? (null) :
+            <div className="documentView-titleWrapper" style={{
+                position: showTextTitle ? "relative" : "absolute",
+                pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
+                width: `${100 * this.props.ContentScaling()}%`,
+                transform: `scale(${1 / this.props.ContentScaling()})`
+            }}>
+                <EditableView
+                    contents={this.Document[showTitle]}
+                    display={"block"} height={72} fontSize={12}
+                    GetValue={() => StrCast(this.Document[showTitle])}
+                    SetValue={(value: string) => (Doc.GetProto(this.Document)[showTitle] = value) ? true : true}
+                />
             </div>);
+        const contents = (<DocumentContentsView {...this.props}
+            ChromeHeight={this.chromeHeight}
+            isSelected={this.isSelected}
+            select={this.select}
+            onClick={this.onClickHandler}
+            layoutKey={"layout"}
+            DataDoc={this.props.DataDoc} />);
         return (
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
                 style={{
-                    pointerEvents: this.layoutDoc.isBackground && !this.isSelected() ? "none" : "all",
-                    color: foregroundColor,
+                    transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
+                    pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
+                    color: StrCast(this.Document.color),
                     outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
                     outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree],
                     outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px",
                     border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined,
-                    borderRadius: "inherit",
                     background: backgroundColor,
                     width: nativeWidth,
                     height: nativeHeight,
@@ -835,44 +655,60 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     opacity: this.Document.opacity
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-                onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
+                onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
                 {!showTitle && !showCaption ?
-                    this.props.Document.search_fields ? <div>
-                        {this.contents}
-                        {searchHighlight}
-                    </div> :
-                        this.contents :
-                    <div style={{ position: "absolute", display: "inline-block", width: "100%", height: "100%", pointerEvents: "none" }}>
-                        <div style={{ width: "100%", height: showTextTitle ? "calc(100% - 29px)" : "100%", display: "inline-block", position: "absolute", top: showTextTitle ? "29px" : undefined }}>
-                            {this.contents}
+                    this.Document.searchFields ?
+                        (<div className="documentView-searchWrapper">
+                            {contents}
+                            {searchHighlight}
+                        </div>)
+                        :
+                        contents
+                    :
+                    <div className="documentView-styleWrapper" >
+                        <div className="documentView-styleContentWrapper" style={{ height: showTextTitle ? "calc(100% - 29px)" : "100%", top: showTextTitle ? "29px" : undefined }}>
+                            {contents}
                         </div>
-                        {!showTitle ? (null) :
-                            <div style={{
-                                position: showTextTitle ? "relative" : "absolute", top: 0, padding: "4px", textAlign: "center", textOverflow: "ellipsis", whiteSpace: "pre",
-                                pointerEvents: SelectionManager.GetIsDragging() ? "none" : "all",
-                                overflow: "hidden", width: `${100 * this.props.ContentScaling()}%`, height: 25, background: "rgba(0, 0, 0, .4)", color: "white",
-                                transformOrigin: "top left", transform: `scale(${1 / this.props.ContentScaling()})`
-                            }}>
-                                <EditableView
-                                    contents={(this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle]}
-                                    display={"block"}
-                                    height={72}
-                                    fontSize={12}
-                                    GetValue={() => StrCast((this.layoutDoc.isTemplate || !this.dataDoc ? this.layoutDoc : this.dataDoc)[showTitle!])}
-                                    SetValue={(value: string) => ((this.layoutDoc.isTemplate ? this.layoutDoc : Doc.GetProto(this.layoutDoc))[showTitle!] = value) ? true : true}
-                                />
-                            </div>
-                        }
-                        {!showCaption ? (null) :
-                            <div style={{ position: "absolute", bottom: 0, transformOrigin: "bottom left", width: `${100 * this.props.ContentScaling()}%`, transform: `scale(${1 / this.props.ContentScaling()})` }}>
-                                <FormattedTextBox {...this.props} onClick={this.onClickHandler} DataDoc={this.dataDoc} active={returnTrue} isSelected={this.isSelected} focus={emptyFunction} select={this.select} fieldExt={""} hideOnLeave={true} fieldKey={showCaption} />
-                            </div>
-                        }
+                        {titleView}
+                        {captionView}
                         {searchHighlight}
                     </div>
                 }
             </div>
         );
     }
-}
\ No newline at end of file
+}
+
+export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField: string, oldLayout?: Doc) {
+    let oldLayoutExt = oldLayout || await Cast(doc[oldLayoutField], Doc);
+    if (oldLayoutExt) {
+        oldLayoutExt.autoHeight = doc.autoHeight;
+        oldLayoutExt.width = doc.width;
+        oldLayoutExt.height = doc.height;
+        oldLayoutExt.nativeWidth = doc.nativeWidth;
+        oldLayoutExt.nativeHeight = doc.nativeHeight;
+        oldLayoutExt.ignoreAspect = doc.ignoreAspect;
+        oldLayoutExt.backgroundLayout = doc.backgroundLayout;
+        oldLayoutExt.type = doc.type;
+        oldLayoutExt.layout = doc.layout;
+    }
+
+    let newLayoutExt = newLayoutField && await Cast(doc[newLayoutField], Doc);
+    if (newLayoutExt) {
+        doc.autoHeight = newLayoutExt.autoHeight;
+        doc.width = newLayoutExt.width;
+        doc.height = newLayoutExt.height;
+        doc.nativeWidth = newLayoutExt.nativeWidth;
+        doc.nativeHeight = newLayoutExt.nativeHeight;
+        doc.ignoreAspect = newLayoutExt.ignoreAspect;
+        doc.backgroundLayout = newLayoutExt.backgroundLayout;
+        doc.type = newLayoutExt.type;
+        doc.layout = await newLayoutExt.layout;
+    }
+}
+
+Scripting.addGlobal(function toggleDetail(doc: any) {
+    let native = typeof doc.layout === "string";
+    swapViews(doc, native ? "layoutCustom" : "layoutNative", native ? "layoutNative" : "layoutCustom");
+});
\ No newline at end of file
diff --git a/src/client/views/nodes/DragBox.tsx b/src/client/views/nodes/DragBox.tsx
index 1f2c88086..6c3db18c4 100644
--- a/src/client/views/nodes/DragBox.tsx
+++ b/src/client/views/nodes/DragBox.tsx
@@ -45,17 +45,15 @@ export class DragBox extends DocComponent<FieldViewProps, DragDocument>(DragDocu
     }
 
     onDragMove = (e: MouseEvent) => {
-        if (!e.cancelBubble && !this.props.Document.excludeFromLibrary && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) {
+        if (!e.cancelBubble && (Math.abs(this._downX - e.clientX) > 5 || Math.abs(this._downY - e.clientY) > 5)) {
             document.removeEventListener("pointermove", this.onDragMove);
             document.removeEventListener("pointerup", this.onDragUp);
             const onDragStart = this.Document.onDragStart;
             e.stopPropagation();
             e.preventDefault();
-            let res = onDragStart ? onDragStart.script.run({ this: this.props.Document }) : undefined;
-            let doc = res !== undefined && res.success ?
-                res.result as Doc :
-                Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" });
-            doc && DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc], [undefined]), e.clientX, e.clientY);
+            let res = onDragStart && onDragStart.script.run({ this: this.props.Document }).result;
+            let doc = (res as Doc) ||  Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" });
+            DragManager.StartDocumentDrag([this._mainCont.current!], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY);
         }
         e.stopPropagation();
         e.preventDefault();
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index d9774303b..49fc2263d 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,6 +30,8 @@ export interface FieldViewProps {
     leaveNativeSize?: boolean;
     fitToBox?: boolean;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+    ContainingCollectionDoc: Opt<Doc>;
+    ruleProvider: Doc | undefined;
     Document: Doc;
     DataDoc?: Doc;
     onClick?: ScriptField;
@@ -37,7 +39,7 @@ export interface FieldViewProps {
     select: (isCtrlPressed: boolean) => void;
     renderDepth: number;
     addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     removeDocument?: (document: Doc) => boolean;
     moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index d7ac7a9c5..0d7277cbe 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -4,7 +4,6 @@
     width: 100%;
     height: 100%;
     min-height: 100%;
-    font-family: $serif;
 }
 
 .ProseMirror:focus {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8f0f142c4..cb9fecfc5 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -41,7 +41,7 @@ import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import * as _ from "lodash";
 import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
 import { inputRules } from 'prosemirror-inputrules';
-import { select } from 'async';
+import { DocumentButtonBar } from '../DocumentButtonBar';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -81,15 +81,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _linkClicked = "";
     private _nodeClicked: any;
     private _undoTyping?: UndoManager.Batch;
-    private _reactionDisposer: Opt<IReactionDisposer>;
     private _searchReactionDisposer?: Lambda;
+    private _reactionDisposer: Opt<IReactionDisposer>;
     private _textReactionDisposer: Opt<IReactionDisposer>;
     private _heightReactionDisposer: Opt<IReactionDisposer>;
+    private _rulesReactionDisposer: Opt<IReactionDisposer>;
     private _proxyReactionDisposer: Opt<IReactionDisposer>;
     private _pullReactionDisposer: Opt<IReactionDisposer>;
     private _pushReactionDisposer: Opt<IReactionDisposer>;
     private dropDisposer?: DragManager.DragDropDisposer;
 
+    @observable private _fontSize = 13;
+    @observable private _fontFamily = "Arial";
+    @observable private _fontAlign = "";
     @observable private _entered = false;
     @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
     public static SelectOnLoad = "";
@@ -121,14 +125,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @undoBatch
     public setFontColor(color: string) {
-        this._editorView!.state.storedMarks;
-        if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
-        if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
+        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;
         }
-        let colorMark = this._editorView!.state.schema.mark(this._editorView!.state.schema.marks.pFontColor, { color: color });
-        this._editorView!.dispatch(this._editorView!.state.tr.addMark(this._editorView!.state.selection.from,
-            this._editorView!.state.selection.to, colorMark));
+        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));
         return true;
     }
 
@@ -142,10 +145,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
 
-    @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); }
-
-    @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+    @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); }
 
     // this should be internal to prosemirror, but is needed
     // here to make sure that footnote view nodes in the overlay editor
@@ -170,6 +172,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    linkOnDeselect: Map<string, string> = new Map();
+
+    doLinkOnDeselect() {
+        Array.from(this.linkOnDeselect.entries()).map(entry => {
+            let key = entry[0];
+            let value = entry[1];
+            let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+            DocServer.GetRefField(value).then(doc => {
+                DocServer.GetRefField(id).then(linkDoc => {
+                    this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
+                    DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
+                    if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
+                    else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true);
+                });
+            });
+        });
+        this.linkOnDeselect.clear();
+    }
+
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
             let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
@@ -184,15 +205,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 if (split.length > 1 && split[1]) {
                     let key = split[0];
                     let value = split[split.length - 1];
+                    this.linkOnDeselect.set(key, value);
 
                     let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
-                    DocServer.GetRefField(value).then(doc => {
-                        DocServer.GetRefField(id).then(linkDoc => {
-                            this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
-                            if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
-                            else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id);
-                        });
-                    });
                     const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value });
                     const mval = this._editorView.state.schema.marks.metadataVal.create();
                     let offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
@@ -256,12 +271,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     }
                 }
             });
-            //     const fieldkey = 'search_string';
-            //     if (Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
-            //         this.props.Document[fieldkey] = undefined;
-            //     }
-            //     else this.props.Document.proto![fieldkey] = undefined;
-            // }
         }
     }
     setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
@@ -292,16 +301,29 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
             const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
-            if (draggedDoc && draggedDoc.type === DocumentType.TEXT && StrCast(draggedDoc.layout) !== "") {
-                this.props.Document.layout = draggedDoc;
-                draggedDoc.isTemplate = true;
+            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 {
+                    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;
+                    }
+                }
                 e.stopPropagation();
             }
         }
     }
 
     recordKeyHandler = (e: KeyboardEvent) => {
-        if (this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
+        if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
             if (e.key === "R" && e.altKey) {
                 e.stopPropagation();
                 e.preventDefault();
@@ -380,6 +402,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     componentDidMount() {
+
         if (!this.props.isOverlay) {
             this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
                 () => {
@@ -410,8 +433,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._pullReactionDisposer = reaction(
             () => this.props.Document[Pulls],
             () => {
-                if (!DocumentDecorations.hasPulledHack) {
-                    DocumentDecorations.hasPulledHack = true;
+                if (!DocumentButtonBar.hasPulledHack) {
+                    DocumentButtonBar.hasPulledHack = true;
                     let unchanged = this.dataDoc.unchanged;
                     this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
                 }
@@ -421,8 +444,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._pushReactionDisposer = reaction(
             () => this.props.Document[Pushes],
             () => {
-                if (!DocumentDecorations.hasPushedHack) {
-                    DocumentDecorations.hasPushedHack = true;
+                if (!DocumentButtonBar.hasPushedHack) {
+                    DocumentButtonBar.hasPushedHack = true;
                     this.pushToGoogleDoc();
                 }
             }
@@ -462,6 +485,39 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 this.unhighlightSearchTerms();
             }
         }, { fireImmediately: true });
+
+
+        this._rulesReactionDisposer = reaction(() => {
+            let ruleProvider = this.props.ruleProvider;
+            let heading = NumCast(this.props.Document.heading);
+            if (ruleProvider instanceof Doc) {
+                return {
+                    align: StrCast(ruleProvider["ruleAlign_" + heading], ""),
+                    font: StrCast(ruleProvider["ruleFont_" + heading], "Arial"),
+                    size: NumCast(ruleProvider["ruleSize_" + heading], 13)
+                };
+            }
+            return undefined;
+        },
+            action((rules: any) => {
+                this._fontFamily = rules ? rules.font : "Arial";
+                this._fontSize = rules ? rules.size : 13;
+                rules && setTimeout(() => {
+                    const view = this._editorView!;
+                    if (this._proseRef) {
+                        let n = new NodeSelection(view.state.doc.resolve(0));
+                        if (this._editorView!.state.doc.textContent === "") {
+                            view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))).
+                                replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true));
+                        } else if (n.node && n.node.type === view.state.schema.nodes.paragraph) {
+                            view.dispatch(view.state.tr.setNodeMarkup(0, n.node.type, { ...n.node.attrs, align: rules.align }));
+                        }
+                        this.tryUpdateHeight();
+                    }
+                }, 0);
+            }), { fireImmediately: true }
+        );
+
         setTimeout(() => this.tryUpdateHeight(), 0);
     }
 
@@ -481,7 +537,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     response && (this.dataDoc[GoogleRef] = response.documentId);
                     let pushSuccess = response !== undefined && !("errors" in response);
                     dataDoc.unchanged = pushSuccess;
-                    DocumentDecorations.Instance.startPushOutcome(pushSuccess);
+                    DocumentButtonBar.Instance.startPushOutcome(pushSuccess);
                 }
             };
             let undo = () => {
@@ -529,7 +585,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         } else {
             delete dataDoc[GoogleRef];
         }
-        DocumentDecorations.Instance.startPullOutcome(pullSuccess);
+        DocumentButtonBar.Instance.startPullOutcome(pullSuccess);
     }
 
     checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
@@ -538,7 +594,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let equalTitles = dataDoc.title === exportState.title;
             let unchanged = equalContent && equalTitles;
             dataDoc.unchanged = unchanged;
-            DocumentDecorations.Instance.setPullState(unchanged);
+            DocumentButtonBar.Instance.setPullState(unchanged);
         }
     }
 
@@ -585,7 +641,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
                 let annotations = DocListCast(region.annotations);
                 annotations.forEach(anno => anno.target = this.props.Document);
-                let fieldExtDoc = Doc.resolvedFieldDataDoc(doc, "data", "true");
+                let fieldExtDoc = Doc.fieldExtensionDoc(doc, "data");
                 let targetAnnotations = DocListCast(fieldExtDoc.annotations);
                 if (targetAnnotations) {
                     targetAnnotations.push(region);
@@ -666,27 +722,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         else if (this.props.isOverlay) this._editorView!.focus();
         // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
         this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
-        let heading = this.props.Document.heading;
-        if (heading && selectOnLoad) {
-            PromiseValue(Cast(this.props.Document.ruleProvider, Doc)).then(ruleProvider => {
-                if (ruleProvider) {
-                    let align = StrCast(ruleProvider["ruleAlign_" + heading]);
-                    let font = StrCast(ruleProvider["ruleFont_" + heading]);
-                    let size = NumCast(ruleProvider["ruleSize_" + heading]);
-                    if (align) {
-                        let tr = this._editorView!.state.tr;
-                        tr = tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(2))).
-                            replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: align }), true).
-                            setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(0)));
-                        this._editorView!.dispatch(tr);
-                    }
-                    let sm = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
-                    size && (sm = [...sm, schema.marks.pFontSize.create({ fontSize: size })]);
-                    font && (sm = [...sm, this.getFont(font)]);
-                    this._editorView!.dispatch(this._editorView!.state.tr.setStoredMarks(sm));
-                }
-            });
-        }
     }
     getFont(font: string) {
         switch (font) {
@@ -702,6 +737,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     componentWillUnmount() {
+        this._rulesReactionDisposer && this._rulesReactionDisposer();
         this._reactionDisposer && this._reactionDisposer();
         this._proxyReactionDisposer && this._proxyReactionDisposer();
         this._textReactionDisposer && this._textReactionDisposer();
@@ -857,6 +893,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this._undoTyping.end();
             this._undoTyping = undefined;
         }
+        this.doLinkOnDeselect();
     }
     onKeyPress = (e: React.KeyboardEvent) => {
         if (e.key === "Escape") {
@@ -866,7 +903,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        this._editorView!.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
+        this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
 
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
@@ -877,7 +914,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     tryUpdateHeight() {
         const ChromeHeight = this.props.ChromeHeight;
         let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
-        if (!this.props.isOverlay && this.props.Document.autoHeight && sh !== 0) {
+        if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
             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 * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
@@ -885,12 +922,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-
     render() {
         let style = this.props.isOverlay ? "scroll" : "hidden";
         let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
-        let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground ||
-            (this.props.Document.isButton && !this.props.isSelected()) ? "none" : "all";
+        let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
+            ? "none" : "all";
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         return (
             <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
@@ -901,7 +937,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
                     color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
                     pointerEvents: interactive,
-                    fontSize: "13px"
+                    fontSize: this._fontSize,
+                    fontFamily: this._fontFamily,
                 }}
                 onKeyDown={this.onKeyPress}
                 onFocus={this.onFocused}
@@ -914,7 +951,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 onPointerEnter={action(() => this._entered = true)}
                 onPointerLeave={action(() => this._entered = false)}
             >
-                <div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} />
+                <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.props.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
             </div>
         );
     }
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 7e78ec684..63a504d1a 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -12,6 +12,8 @@ import { IconField } from "../../../new_fields/IconField";
 import { ContextMenu } from "../ContextMenu";
 import Measure from "react-measure";
 import { MINIMIZED_ICON_SIZE } from "../../views/globalCssVariables.scss";
+import { Scripting } from "../../util/Scripting";
+import { ComputedField } from "../../../new_fields/ScriptField";
 
 
 library.add(faCaretUp);
@@ -27,6 +29,25 @@ export class IconBox extends React.Component<FieldViewProps> {
     @computed get layout(): string { const field = Cast(this.props.Document[this.props.fieldKey], IconField); return field ? field.icon : "<p>Error loading icon data</p>"; }
     @computed get minimizedIcon() { return IconBox.DocumentIcon(this.layout); }
 
+    public static summaryTitleScript(inputDoc: Doc) {
+        const sumDoc = Cast(inputDoc.summaryDoc, Doc) as Doc;
+        if (sumDoc && StrCast(sumDoc.title).startsWith("-")) {
+            return sumDoc.title + ".expanded";
+        }
+        return "???";
+    }
+    public static titleScript(inputDoc: Doc) {
+        const maxDoc = DocListCast(inputDoc.maximizedDocs);
+        if (maxDoc.length === 1) {
+            return maxDoc[0].title + ".icon";
+        }
+        return maxDoc.length > 1 ? "-multiple-.icon" : "???";
+    }
+
+    public static AutomaticTitle(doc: Doc) {
+        Doc.GetProto(doc).title = ComputedField.MakeFunction('iconTitle(this);');
+    }
+
     public static DocumentIcon(layout: string) {
         let button = layout.indexOf("PDFBox") !== -1 ? faFilePdf :
             layout.indexOf("ImageBox") !== -1 ? faImage :
@@ -38,35 +59,20 @@ export class IconBox extends React.Component<FieldViewProps> {
     }
 
     setLabelField = (): void => {
-        this.props.Document.hideLabel = !BoolCast(this.props.Document.hideLabel);
-    }
-    setUseOwnTitleField = (): void => {
-        this.props.Document.useOwnTitle = !BoolCast(this.props.Document.useTargetTitle);
+        this.props.Document.hideLabel = !this.props.Document.hideLabel;
     }
 
     specificContextMenu = (): void => {
-        ContextMenu.Instance.addItem({
-            description: BoolCast(this.props.Document.hideLabel) ? "Show label with icon" : "Remove label from icon",
-            event: this.setLabelField,
-            icon: "tag"
-        });
-        let maxDocs = DocListCast(this.props.Document.maximizedDocs);
-        if (maxDocs.length === 1 && !BoolCast(this.props.Document.hideLabel)) {
-            ContextMenu.Instance.addItem({
-                description: BoolCast(this.props.Document.useOwnTitle) ? "Use target title for label" : "Use own title label",
-                event: this.setUseOwnTitleField,
-                icon: "text-height"
-            });
+        let cm = ContextMenu.Instance;
+        cm.addItem({ description: this.props.Document.hideLabel ? "Show label with icon" : "Remove label from icon", event: this.setLabelField, icon: "tag" });
+        if (!this.props.Document.hideLabel) {
+            cm.addItem({ description: "Use Target Title", event: () => IconBox.AutomaticTitle(this.props.Document), icon: "text-height" });
         }
     }
     @observable _panelWidth: number = 0;
     @observable _panelHeight: number = 0;
     render() {
-        let labelField = StrCast(this.props.Document.labelField);
-        let hideLabel = BoolCast(this.props.Document.hideLabel);
-        let maxDocs = DocListCast(this.props.Document.maximizedDocs);
-        let firstDoc = maxDocs.length ? maxDocs[0] : undefined;
-        let label = hideLabel ? "" : (firstDoc && labelField && !BoolCast(this.props.Document.useOwnTitle) ? firstDoc[labelField] : this.props.Document.title);
+        let label = this.props.Document.hideLabel ? "" : this.props.Document.title;
         return (
             <div className="iconBox-container" onContextMenu={this.specificContextMenu}>
                 {this.minimizedIcon}
@@ -82,4 +88,6 @@ export class IconBox extends React.Component<FieldViewProps> {
                 </Measure>
             </div>);
     }
-}
\ No newline at end of file
+}
+Scripting.addGlobal(function iconTitle(doc: any) { return IconBox.titleScript(doc); });
+Scripting.addGlobal(function summaryTitle(doc: any) { return IconBox.summaryTitleScript(doc); });
\ No newline at end of file
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 649d2d056..1645c4ffd 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -17,13 +17,12 @@ import { Utils } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
-import { CompileScript } from '../../util/Scripting';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from "../../views/ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent } from '../DocComponent';
 import { InkingControl } from '../InkingControl';
-import { positionSchema } from './DocumentView';
+import { documentSchema } from './DocumentView';
 import FaceRectangles from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ImageBox.scss";
@@ -50,8 +49,8 @@ declare class MediaRecorder {
     constructor(e: any);
 }
 
-type ImageDocument = makeInterface<[typeof pageSchema, typeof positionSchema]>;
-const ImageDocument = makeInterface(pageSchema, positionSchema);
+type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
+const ImageDocument = makeInterface(pageSchema, documentSchema);
 
 @observer
 export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
@@ -65,7 +64,9 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     private dropDisposer?: DragManager.DragDropDisposer;
     @observable private hoverActive = false;
 
-    @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+    @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); }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         if (this.dropDisposer) {
@@ -81,32 +82,18 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         console.log("IMPLEMENT ME PLEASE");
     }
 
-    @computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "Alternates"); }
-
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
-            de.data.droppedDocuments.forEach(action((drop: Doc) => {
-                if (de.mods === "AltKey" && /*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
-                    Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(drop.data.url);
-                    e.stopPropagation();
-                } else if (de.mods === "MetaKey") {
-                    if (this.extensionDoc !== this.dataDoc) {
-                        let layout = StrCast(drop.backgroundLayout);
-                        if (layout.indexOf(ImageBox.name) !== -1) {
-                            let imgData = this.extensionDoc.Alternates;
-                            if (!imgData) {
-                                Doc.GetProto(this.extensionDoc).Alternates = new List([]);
-                            }
-                            let imgList = Cast(this.extensionDoc.Alternates, listSpec(Doc), [] as any[]);
-                            imgList && imgList.push(drop);
-                            e.stopPropagation();
-                        }
-                    }
-                }
+            if (de.mods === "AltKey" && de.data.draggedDocuments.length && de.data.draggedDocuments[0].data instanceof ImageField) {
+                Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new ImageField(de.data.draggedDocuments[0].data.url);
+                e.stopPropagation();
+            }
+            de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => {
+                Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
+                e.stopPropagation();
             }));
-            // de.data.removeDocument()  bcz: need to implement
         }
     }
 
@@ -243,9 +230,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
             results.tags.map((tag: Tag) => {
                 tagsList.push(tag.name);
                 let sanitized = tag.name.replace(" ", "_");
-                let script = `return (${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`;
-                let computed = CompileScript(script, { params: { this: "Doc" } });
-                computed.compiled && (tagDoc[sanitized] = new ComputedField(computed));
+                tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
             });
             this.extensionDoc.generatedTags = tagsList;
             tagDoc.title = "Generated Tags Doc";
@@ -261,13 +246,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     onDotDown(index: number) {
         this.Document.curPage = index;
     }
-
-    @computed get fieldExtensionDoc() {
-        return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true");
-    }
-
     @computed private get url() {
-        let data = Cast(Doc.GetProto(this.props.Document).data, ImageField);
+        let data = Cast(Doc.GetProto(this.props.Document)[this.props.fieldKey], ImageField);
         return data ? data.url.href : undefined;
     }
 
@@ -407,7 +387,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
         // let w = bptX - sptX;
 
-        let id = (this.props as any).id; // bcz: used to set id = "isExpander" in templates.tsx
         let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
         let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
         let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
@@ -433,13 +412,13 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
 
         return (
-            <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
+            <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
                 onPointerDown={this.onPointerDown}
                 onPointerEnter={action(() => this.hoverActive = true)}
                 onPointerLeave={action(() => this.hoverActive = false)}
                 onDrop={this.onDrop} ref={this.createDropTarget} onContextMenu={this.specificContextMenu}>
                 <div id="cf">
-                    <img id={id}
+                    <img
                         key={this._smallRetryCount + (this._mediumRetryCount << 4) + (this._largeRetryCount << 8)} // force cache to update on retrys
                         src={srcpath}
                         style={{ transform: `translate(0px, ${shift}px) rotate(${rotation}deg) scale(${aspect})` }}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index f80f414b1..3a9318469 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -76,7 +76,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         } else if (type === "script") {
             field = new ScriptField(script);
         } else {
-            let res = script.run({ this: target });
+            let res = script.run({ this: target }, console.log);
             if (!res.success) return false;
             field = res.result;
         }
@@ -124,7 +124,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         let i = 0;
         const self = this;
         for (let key of Object.keys(ids).slice().sort()) {
-            rows.push(<KeyValuePair doc={realDoc} ref={(function () {
+            rows.push(<KeyValuePair doc={realDoc} addDocTab={this.props.addDocTab} ref={(function () {
                 let oldEl: KeyValuePair | undefined;
                 return (el: KeyValuePair) => {
                     if (oldEl) self.rows.splice(self.rows.indexOf(oldEl), 1);
@@ -198,7 +198,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
             return;
         }
         let previousViewType = fieldTemplate.viewType;
-        Doc.MakeTemplate(fieldTemplate, metaKey, Doc.GetProto(parentStackingDoc));
+        Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(parentStackingDoc));
         previousViewType && (fieldTemplate.viewType = previousViewType);
 
         Cast(parentStackingDoc.data, listSpec(Doc))!.push(fieldTemplate);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index a27dbd83d..1fed4c8bb 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,7 +1,7 @@
 import { action, observable } from 'mobx';
 import { observer } from "mobx-react";
 import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
-import { Doc, Field } from '../../../new_fields/Doc';
+import { Doc, Field, Opt } from '../../../new_fields/Doc';
 import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { Transform } from '../../util/Transform';
@@ -22,6 +22,7 @@ export interface KeyValuePairProps {
     keyName: string;
     doc: Doc;
     keyWidth: number;
+    addDocTab: (doc: Doc, data: Opt<Doc>, where: string) => boolean;
 }
 @observer
 export class KeyValuePair extends React.Component<KeyValuePairProps> {
@@ -45,7 +46,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
         if (value instanceof Doc) {
             e.stopPropagation();
             e.preventDefault();
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.Create.KVPDocument(value, { width: 300, height: 300 }); CollectionDockingView.Instance.AddRightSplit(kvp, undefined); }, icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { width: 300, height: 300 }), undefined, "onRight"), icon: "layer-group" });
             ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
         }
     }
@@ -55,6 +56,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             Document: this.props.doc,
             DataDoc: this.props.doc,
             ContainingCollectionView: undefined,
+            ContainingCollectionDoc: undefined,
+            ruleProvider: undefined,
             fieldKey: this.props.keyName,
             fieldExt: "",
             isSelected: returnFalse,
@@ -66,7 +69,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             focus: emptyFunction,
             PanelWidth: returnZero,
             PanelHeight: returnZero,
-            addDocTab: returnZero,
+            addDocTab: returnFalse,
             pinToPres: returnZero,
             ContentScaling: returnOne
         };
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index df35b603c..764051d62 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -4,40 +4,28 @@ import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
 import 'react-image-lightbox/style.css';
-import { Doc, WidthSym, Opt } from "../../../new_fields/Doc";
+import { Doc, Opt, WidthSym } from "../../../new_fields/Doc";
 import { makeInterface } from "../../../new_fields/Schema";
-import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast } from "../../../new_fields/Types";
+import { ComputedField, ScriptField } from '../../../new_fields/ScriptField';
+import { Cast, NumCast } from "../../../new_fields/Types";
 import { PdfField } from "../../../new_fields/URLField";
 import { KeyCodes } from '../../northstar/utils/KeyCodes';
-import { CompileScript } from '../../util/Scripting';
+import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { DocComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
 import { PDFViewer } from "../pdf/PDFViewer";
-import { positionSchema } from "./DocumentView";
+import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
 
-type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
-const PdfDocument = makeInterface(positionSchema, pageSchema);
-export const handleBackspace = (e: React.KeyboardEvent) => { if (e.keyCode === KeyCodes.BACKSPACE) e.stopPropagation(); };
+type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
+const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
 
 @observer
 export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
     public static LayoutString() { return FieldView.LayoutString(PDFBox); }
-
-    @observable private _flyout: boolean = false;
-    @observable private _alt = false;
-    @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
-
-    @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; }
-    @computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
-
-
-    @computed get fieldExtensionDoc() { return Doc.resolvedFieldDataDoc(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, "true"); }
-
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _reactionDisposer?: IReactionDisposer;
     private _keyValue: string = "";
@@ -47,16 +35,26 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     private _valueRef: React.RefObject<HTMLInputElement> = React.createRef();
     private _scriptRef: React.RefObject<HTMLInputElement> = React.createRef();
 
+    @observable private _flyout: boolean = false;
+    @observable private _alt = false;
+    @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
+
+    @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); }
+
     componentDidMount() {
         this.props.setPdfBox && this.props.setPdfBox(this);
 
+        this.props.Document.curPage = ComputedField.MakeFunction("Math.floor(Number(this.panY) / Number(this.nativeHeight) + 1)");
+
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         if (pdfUrl instanceof PdfField) {
             Pdfjs.getDocument(pdfUrl.url.pathname).promise.then(pdf => runInAction(() => this._pdf = pdf));
         }
         this._reactionDisposer = reaction(
-            () => this.props.Document.panY,
-            () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.Document.panY), behavior: "auto" })
+            () => this.Document.panY,
+            () => this._mainCont.current && this._mainCont.current.scrollTo({ top: this.Document.panY || 0, behavior: "auto" })
         );
     }
 
@@ -65,24 +63,22 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     }
 
     public GetPage() {
-        return Math.floor(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;
+        return Math.floor((this.Document.panY || 0) / (this.Document.nativeHeight || 0)) + 1;
     }
 
     @action
     public BackPage() {
-        let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;
+        let cp = Math.ceil((this.Document.panY || 0) / (this.Document.nativeHeight || 0)) + 1;
         cp = cp - 1;
         if (cp > 0) {
-            this.props.Document.curPage = cp;
-            this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
+            this.Document.panY = (cp - 1) * (this.Document.nativeHeight || 0);
         }
     }
 
     @action
-    public GotoPage(p: number) {
+    public GotoPage = (p: number) => {
         if (p > 0 && p <= NumCast(this.dataDoc.numPages)) {
-            this.props.Document.curPage = p;
-            this.props.Document.panY = (p - 1) * NumCast(this.dataDoc.nativeHeight);
+            this.Document.panY = (p - 1) * (this.Document.nativeHeight || 0);
         }
     }
 
@@ -90,23 +86,20 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     public ForwardPage() {
         let cp = this.GetPage() + 1;
         if (cp <= NumCast(this.dataDoc.numPages)) {
-            this.props.Document.curPage = cp;
-            this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
+            this.Document.panY = (cp - 1) * (this.Document.nativeHeight || 0);
         }
     }
 
     @action
     setPanY = (y: number) => {
-        this.containingCollectionDocument && (this.containingCollectionDocument.panY = y);
+        this.Document.panY = y;
     }
 
     @action
     private applyFilter = () => {
-        let scriptText = this._scriptValue.length > 0 ? this._scriptValue :
-            this._keyValue.length > 0 && this._valueValue.length > 0 ?
-                `return this.${this._keyValue} === ${this._valueValue}` : "return true";
-        let script = CompileScript(scriptText, { params: { this: Doc.name } });
-        script.compiled && (this.props.Document.filterScript = new ScriptField(script));
+        let scriptText = this._scriptValue ? this._scriptValue :
+            this._keyValue && this._valueValue ? `this.${this._keyValue} === ${this._valueValue}` : "true";
+        this.props.Document.filterScript = ScriptField.MakeFunction(scriptText);
     }
 
     scrollTo = (y: number) => {
@@ -114,8 +107,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     }
 
     private resetFilters = () => {
-        this._keyValue = this._valueValue = "";
-        this._scriptValue = "return true";
+        this._keyValue = this._valueValue = this._scriptValue = "";
         this._keyRef.current && (this._keyRef.current.value = "");
         this._valueRef.current && (this._valueRef.current.value = "");
         this._scriptRef.current && (this._scriptRef.current.value = "");
@@ -129,7 +121,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
         return !this.props.active() ? (null) :
             (<div className="pdfBox-settingsCont" onPointerDown={(e) => e.stopPropagation()}>
                 <button className="pdfBox-settingsButton" onClick={action(() => this._flyout = !this._flyout)} title="Open Annotation Settings"
-                    style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}>
+                    style={{ marginTop: `${this.Document.panY || 0}px` }}>
                     <div className="pdfBox-settingsButton-arrow"
                         style={{
                             borderTop: `25px solid ${this._flyout ? "#121721" : "transparent"}`,
@@ -146,13 +138,13 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                         Annotation View Settings
                     </div>
                     <div className="pdfBox-settingsFlyout-kvpInput">
-                        <input placeholder="Key" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newKeyChange}
+                        <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange}
                             style={{ gridColumn: 1 }} ref={this._keyRef} />
-                        <input placeholder="Value" className="pdfBox-settingsFlyout-input" onKeyDown={handleBackspace} onChange={this.newValueChange}
+                        <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange}
                             style={{ gridColumn: 3 }} ref={this._valueRef} />
                     </div>
                     <div className="pdfBox-settingsFlyout-kvpInput">
-                        <input placeholder="Custom Script" onChange={this.newScriptChange} onKeyDown={handleBackspace} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
+                        <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} ref={this._scriptRef} />
                     </div>
                     <div className="pdfBox-settingsFlyout-kvpInput">
                         <button style={{ gridColumn: 1 }} onClick={this.resetFilters}>
@@ -170,20 +162,20 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
 
     loaded = (nw: number, nh: number, np: number) => {
         this.dataDoc.numPages = np;
-        if (!this.dataDoc.nativeWidth || !this.dataDoc.nativeHeight || !this.dataDoc.scrollHeight) {
-            let oldaspect = NumCast(this.dataDoc.nativeHeight) / NumCast(this.dataDoc.nativeWidth, 1);
-            this.dataDoc.nativeWidth = nw;
-            this.dataDoc.nativeHeight = this.dataDoc.nativeHeight ? nw * oldaspect : nh;
-            this.dataDoc.height = this.dataDoc[WidthSym]() * (nh / nw);
-            this.dataDoc.scrollHeight = np * this.dataDoc.nativeHeight;
+        if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) {
+            let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
+            this.Document.nativeWidth = nw;
+            this.Document.nativeHeight = this.Document.nativeHeight ? nw * oldaspect : nh;
+            this.Document.height = this.Document[WidthSym]() * (nh / nw);
+            this.Document.scrollHeight = np * this.Document.nativeHeight;
         }
     }
 
     @action
     onScroll = (e: React.UIEvent<HTMLDivElement>) => {
-        if (e.currentTarget && this.containingCollectionDocument) {
-            this.containingCollectionDocument.panTransformType = "None";
-            this.containingCollectionDocument.panY = e.currentTarget.scrollTop;
+        if (e.currentTarget && this.props.ContainingCollectionDoc) {
+            this.props.Document.panTransformType = "None";
+            this.Document.panY = e.currentTarget.scrollTop;
         }
     }
 
@@ -195,14 +187,14 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
             <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
             <div className={classname}
                 onScroll={this.onScroll}
-                style={{ marginTop: `${this.containingCollectionDocument ? NumCast(this.containingCollectionDocument.panY) : 0}px` }}
+                style={{ marginTop: `${(this.Document.panY || 0)}px` }}
                 ref={this._mainCont}>
-                <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + (NumCast(this.props.Document.nativeHeight) - NumCast(this.props.Document.nativeHeight) / NumCast(this.props.Document.scale, 1)), width: "100%" }} />
-                <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={NumCast(this.props.Document.panY)}
-                    Document={this.props.Document} DataDoc={this.props.DataDoc}
-                    addDocTab={this.props.addDocTab} setPanY={this.setPanY}
+                <div className="pdfBox-scrollHack" style={{ height: NumCast(this.props.Document.scrollHeight) + ((this.Document.nativeHeight || 0) - (this.Document.nativeHeight || 0) / (this.Document.scale || 1)) }} />
+                <PDFViewer pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded} panY={this.Document.panY || 0}
+                    Document={this.props.Document} DataDoc={this.dataDoc}
+                    addDocTab={this.props.addDocTab} setPanY={this.setPanY} GoToPage={this.GotoPage}
                     pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
-                    fieldKey={this.props.fieldKey} fieldExtensionDoc={this.fieldExtensionDoc} />
+                    fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} />
                 {this.settingsPanel()}
             </div>);
     }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 3f4ee8960..b7d9a1eab 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
 import * as rp from 'request-promise';
 import { InkTool } from "../../../new_fields/InkField";
 import { makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { Cast, FieldValue, NumCast, BoolCast } from "../../../new_fields/Types";
 import { VideoField } from "../../../new_fields/URLField";
 import { RouteStore } from "../../../server/RouteStore";
 import { Utils } from "../../../Utils";
@@ -14,19 +14,18 @@ import { ContextMenuProps } from "../ContextMenuItem";
 import { DocComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
-import { positionSchema } from "./DocumentView";
+import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./VideoBox.scss";
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faVideo } from "@fortawesome/free-solid-svg-icons";
-import { CompileScript } from "../../util/Scripting";
 import { Doc } from "../../../new_fields/Doc";
 import { ScriptField } from "../../../new_fields/ScriptField";
 var path = require('path');
 
-type VideoDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
-const VideoDocument = makeInterface(positionSchema, pageSchema);
+type VideoDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
+const VideoDocument = makeInterface(documentSchema, pageSchema);
 
 library.add(faVideo);
 
@@ -116,18 +115,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                 x: NumCast(this.props.Document.x) + width, y: NumCast(this.props.Document.y),
                 width: 150, height: 50, title: NumCast(this.props.Document.curPage).toString()
             });
-            const script = CompileScript(`(self as any).curPage = ${NumCast(this.props.Document.curPage)}`, {
-                params: { this: Doc.name },
-                capturedVariables: { self: this.props.Document },
-                typecheck: false,
-                editable: true,
-            });
-            if (script.compiled) {
-                b.onClick = new ScriptField(script);
-                this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(b, false);
-            } else {
-                console.log(script.errors.map(error => error.messageText).join("\n"));
-            }
+            b.onClick = ScriptField.MakeScript(`this.curPage = ${NumCast(this.props.Document.curPage)}`);
         } else {
             //convert to desired file format
             var dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png'
@@ -204,7 +192,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
         }
     }
     specificContextMenu = (e: React.MouseEvent): void => {
-        let field = Cast(this.Document[this.props.fieldKey], VideoField);
+        let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         if (field) {
             let url = field.url.href;
             let subitems: ContextMenuProps[] = [];
@@ -216,7 +204,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     }
 
     @computed get content() {
-        let field = Cast(this.Document[this.props.fieldKey], VideoField);
+        let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
@@ -228,7 +216,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     }
 
     @computed get youtubeVideoId() {
-        let field = Cast(this.Document[this.props.fieldKey], VideoField);
+        let field = Cast(this.dataDoc[this.props.fieldKey], VideoField);
         return field && field.url.href.indexOf("youtube") !== -1 ? ((arr: string[]) => arr[arr.length - 1])(field.url.href.split("/")) : "";
     }
 
@@ -269,6 +257,8 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
 
     }
 
+    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
         this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
@@ -281,6 +271,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     }
 
     render() {
+        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         return <div style={{ pointerEvents: "all", width: "100%", height: "100%" }} onContextMenu={this.specificContextMenu}>
             {this.youtubeVideoId ? this.youtubeContent : this.content}
         </div>;
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index eeb2531a2..a9fa883c8 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,20 +1,18 @@
 import React = require("react");
 import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
 import { DocumentManager } from "../../util/DocumentManager";
 import PDFMenu from "./PDFMenu";
 import "./Annotation.scss";
-import { scale } from "./PDFViewer";
-import { PresBox } from "../nodes/PresBox";
 
 interface IAnnotationProps {
     anno: Doc;
     fieldExtensionDoc: Doc;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
     pinToPres: (document: Doc) => void;
 }
 
@@ -31,7 +29,7 @@ interface IRegionAnnotationProps {
     width: number;
     height: number;
     fieldExtensionDoc: Doc;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     document: Doc;
 }
@@ -58,7 +56,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
                     runInAction(() => this._brushed = brushed);
                 }
             }
-        )
+        );
     }
 
     componentWillUnmount() {
diff --git a/src/client/views/pdf/PDFAnnotationLayer.scss b/src/client/views/pdf/PDFAnnotationLayer.scss
deleted file mode 100644
index 733533007..000000000
--- a/src/client/views/pdf/PDFAnnotationLayer.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.pdfAnnotationLayer-cont {
-    width:100%;
-    height:100%;
-    position:relative;
-    top:-200%;
-}
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFAnnotationLayer.tsx b/src/client/views/pdf/PDFAnnotationLayer.tsx
deleted file mode 100644
index 4f267a5c0..000000000
--- a/src/client/views/pdf/PDFAnnotationLayer.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React = require("react");
-import { observer } from "mobx-react";
-import "./PDFAnnotationLayer.scss";
-
-interface IAnnotationProps {
-
-}
-
-@observer
-export class PDFAnnotationLayer extends React.Component {
-    onPointerDown = (e: React.PointerEvent) => {
-        if (e.ctrlKey) {
-            console.log("annotating");
-            e.stopPropagation();
-        }
-    }
-
-    render() {
-        return <div className="pdfAnnotationLayer-cont" onPointerDown={this.onPointerDown} />;
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 7bc1d3507..b82c3bb32 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
@@ -19,6 +19,7 @@ import Annotation from "./Annotation";
 import Page from "./Page";
 import "./PDFViewer.scss";
 import React = require("react");
+import requestPromise = require("request-promise");
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 
 export const scale = 2;
@@ -35,7 +36,8 @@ interface IViewerProps {
     scrollTo: (y: number) => void;
     active: () => boolean;
     setPanY?: (n: number) => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    GoToPage?: (n: number) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
 }
@@ -61,7 +63,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _filterReactionDisposer?: IReactionDisposer;
     private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
-    private _pdfViewer: any;
+    public _pdfViewer: any;
     private _pdfFindController: any;
     private _searchString: string = "";
     private _selectionText: string = "";
@@ -77,18 +79,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
     }
 
     @computed get allAnnotations() {
-        let annotations = DocListCast(this.props.fieldExtensionDoc.annotations);
-        return annotations.filter(anno => {
-            let run = this._script.run({ this: anno });
-            return run.success ? run.result : true;
-        })
+        return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
+            anno => this._script.run({ this: anno }, console.log, true).result);
     }
 
     @computed get nonDocAnnotations() {
-        return this._annotations.filter(anno => {
-            let run = this._script.run({ this: anno });
-            return run.success ? run.result : true;
-        });
+        return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result);
     }
 
     componentDidUpdate = (prevProps: IViewerProps) => this.panY !== prevProps.panY && this.renderPages();
@@ -114,10 +110,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 if (this._script.originalScript !== oldScript) {
                     this.Index = -1;
                 }
-                annos.forEach(d => {
-                    let run = this._script.run(d);
-                    d.opacity = !run.success || run.result ? 1 : 0;
-                });
+                annos.forEach(d => d.opacity = this._script.run({ this: d }, console.log, 1).result ? 1 : 0);
             }),
             { fireImmediately: true }
         );
@@ -372,7 +365,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @action
     search = (searchString: string) => {
         if (this._pdfViewer._pageViewsReady) {
-            this._pdfFindController.executeCommand('find', {
+            this._pdfFindController.executeCommand('findagain', {
                 caseSensitive: false,
                 findPrevious: undefined,
                 highlightAll: true,
@@ -381,13 +374,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
             });
         }
         else if (this._mainCont.current) {
-            let executeFind = () => this._pdfFindController.executeCommand('find', {
-                caseSensitive: false,
-                findPrevious: undefined,
-                highlightAll: true,
-                phraseSearch: true,
-                query: searchString
-            });
+            let executeFind = () => {
+                this._pdfFindController.executeCommand('find', {
+                    caseSensitive: false,
+                    findPrevious: undefined,
+                    highlightAll: true,
+                    phraseSearch: true,
+                    query: searchString
+                });
+            };
             this._mainCont.current.addEventListener("pagesloaded", executeFind);
             this._mainCont.current.addEventListener("pagerendered", executeFind);
         }
@@ -400,14 +395,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
         this._searching = !this._searching;
 
         if (this._searching) {
-            if (!this._pdfFindController && this._mainCont.current && this._viewer.current) {
-                let simpleLinkService = new SimpleLinkService();
+            if (!this._pdfFindController && this._mainCont.current && this._viewer.current && !this._pdfViewer) {
+                let simpleLinkService = new SimpleLinkService(this);
                 this._pdfViewer = new PDFJSViewer.PDFViewer({
                     container: this._mainCont.current,
                     viewer: this._viewer.current,
                     linkService: simpleLinkService
                 });
-                simpleLinkService.setPdf(this.props.pdf);
+                simpleLinkService.setPDFJSview(this._pdfViewer);
                 this._mainCont.current.addEventListener("pagesinit", () => this._pdfViewer.currentScaleValue = 1);
                 this._mainCont.current.addEventListener("pagerendered", () => console.log("rendered"));
                 this._pdfViewer.setDocument(this.props.pdf);
@@ -415,21 +410,16 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 this._pdfViewer.findController = this._pdfFindController;
             }
         }
-        else {
-            this._pdfFindController = null;
-            if (this._viewer.current) {
-                let cns = this._viewer.current.childNodes;
-                for (let i = cns.length - 1; i >= 0; i--) {
-                    cns.item(i).remove();
-                }
-            }
-        }
+    }
+    @computed get visibleElementWrapper() {
+        trace();
+        return this._visibleElements;
     }
 
     render() {
         return (<div className="pdfViewer-viewer" ref={this._mainCont} >
             <div className="pdfViewer-visibleElements" style={this._searching ? { position: "absolute", top: 0 } : {}}>
-                {this._visibleElements}
+                {this.visibleElementWrapper}
             </div>
             <div className="pdfViewer-text" ref={this._viewer} />
             <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
@@ -467,9 +457,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
 export enum AnnotationTypes { Region }
 
 class SimpleLinkService {
-    externalLinkTarget: any = null;
-    externalLinkRel: any = null;
-    pdf: any = null;
+    _viewer: PDFViewer;
+    _pdfjsView: any;
+
+    constructor(viewer: PDFViewer) {
+        this._viewer = viewer;
+    }
+    setPDFJSview(v: any) { this._pdfjsView = v; }
 
     navigateTo() { }
 
@@ -479,15 +473,24 @@ class SimpleLinkService {
 
     setHash() { }
 
+    isPageVisible(page: number) { return true; }
+
     executeNamedAction() { }
 
     cachePageRef() { }
 
-    get pagesCount() { return this.pdf ? this.pdf.numPages : 0; }
+    get pagesCount() { return this._viewer._pdfViewer.pagesCount; }
 
-    get page() { return 0; }
+    get page() { return NumCast(this._viewer.props.Document.curPage); }
+    set page(p: number) {
+        this._pdfjsView._ensurePdfPageLoaded(this._pdfjsView._pages[p - 1]).then(() => {
+            this._pdfjsView.renderingQueue.renderView(this._pdfjsView._pages[p - 1]);
+            if (this._viewer.props.GoToPage) {
+                this._viewer.props.GoToPage(p);
+            }
+        });
+    }
 
-    setPdf(pdf: any) { this.pdf = pdf; }
 
     get rotation() { return 0; }
     set rotation(value: any) { }
diff --git a/src/client/views/pdf/Page.scss b/src/client/views/pdf/Page.scss
index af1628a6f..d8034b4b4 100644
--- a/src/client/views/pdf/Page.scss
+++ b/src/client/views/pdf/Page.scss
@@ -1,4 +1,9 @@
 
+.pdfViewer-text {
+    .page {
+        position: relative;
+    }
+}
 .pdfPage-cont {
     position: relative;
 
diff --git a/src/client/views/pdf/Page.tsx b/src/client/views/pdf/Page.tsx
index 856e883e7..533247170 100644
--- a/src/client/views/pdf/Page.tsx
+++ b/src/client/views/pdf/Page.tsx
@@ -137,7 +137,7 @@ export default class Page extends React.Component<IPageProps> {
         view.nativeWidth = this.props.Document.nativeWidth;
         view.startY = marquee.top + this.props.getScrollFromPage(this.props.page);
         view.width = this.props.Document[WidthSym]();
-        DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view], [undefined]), 0, 0);
+        DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
     }
 
     @action
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 80aa25f48..126d62c52 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -351,6 +351,7 @@ export default class PresentationElement extends React.Component<PresentationEle
                     Document={this.props.document}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
+                    ruleProvider={undefined}
                     ScreenToLocalTransform={Transform.Identity}
                     addDocTab={returnFalse}
                     pinToPres={returnFalse}
@@ -365,6 +366,7 @@ export default class PresentationElement extends React.Component<PresentationEle
                     zoomToScale={emptyFunction}
                     getScale={returnOne}
                     ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
                     ContentScaling={scale}
                 />
                 <div style={{
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
index 930ce202e..f730b4cf2 100644
--- a/src/client/views/presentationview/PresentationList.tsx
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -17,7 +17,9 @@ interface PresListProps {
     presStatus: boolean;
     removeDocByRef(doc: Doc): boolean;
     clearElemMap(): void;
-
+    groupMappings: Map<String, Doc[]>;
+    presGroupBackUp: Doc;
+    presButtonBackUp: Doc;
 }
 
 
diff --git a/src/client/views/search/FilterBox.tsx b/src/client/views/search/FilterBox.tsx
index c13d1d276..da733d64b 100644
--- a/src/client/views/search/FilterBox.tsx
+++ b/src/client/views/search/FilterBox.tsx
@@ -195,11 +195,9 @@ export class FilterBox extends React.Component {
                     collections.push(element.props.Document);
                 }
             }
-            //gets the selected doc's containing view
-            let containingView = element.props.ContainingCollectionView;
             //makes sure collections aren't added more than once
-            if (containingView && !collections.includes(containingView.props.Document)) {
-                collections.push(containingView.props.Document);
+            if (element.props.ContainingCollectionDoc && !collections.includes(element.props.ContainingCollectionDoc)) {
+                collections.push(element.props.ContainingCollectionDoc);
             }
         });
 
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index c56d093fa..96eefacc2 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -76,7 +76,7 @@ export class SelectorContextMenu extends React.Component<SearchItemProps> {
                 col.panX = newPanX;
                 col.panY = newPanY;
             }
-            CollectionDockingView.Instance.AddRightSplit(col, undefined);
+            CollectionDockingView.AddRightSplit(col, undefined);
         };
     }
     render() {
@@ -110,7 +110,7 @@ export class LinkContextMenu extends React.Component<LinkMenuProps> {
 
     unHighlightDoc = (doc: Doc) => () => Doc.UnBrushDoc(doc);
 
-    getOnClick = (col: Doc) => () => CollectionDockingView.Instance.AddRightSplit(col, undefined);
+    getOnClick = (col: Doc) => () => CollectionDockingView.AddRightSplit(col, undefined);
 
     render() {
         return (
@@ -165,6 +165,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
                     Document={this.props.doc}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
+                    ruleProvider={undefined}
                     ScreenToLocalTransform={Transform.Identity}
                     addDocTab={returnFalse}
                     pinToPres={returnFalse}
@@ -179,6 +180,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
                     zoomToScale={emptyFunction}
                     getScale={returnOne}
                     ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
                     ContentScaling={scale}
                 />
             </div>;
@@ -269,7 +271,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
     onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
         e.stopPropagation();
         const doc = Doc.IsPrototype(this.props.doc) ? Doc.MakeDelegate(this.props.doc) : this.props.doc;
-        DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc], []), e.clientX, e.clientY, {
+        DragManager.StartDocumentDrag([e.currentTarget], new DragManager.DocumentDragData([doc]), e.clientX, e.clientY, {
             handlers: { dragComplete: emptyFunction },
             hideSource: false,
         });
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 4f4db13d2..fd6b47ff0 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -28,12 +28,8 @@ class Repl extends React.Component {
             if (!script.compiled) {
                 this.executedCommands.push({ command: this.text, result: "Compile Error" });
             } else {
-                const result = script.run({ makeInterface });
-                if (result.success) {
-                    this.executedCommands.push({ command: this.text, result: result.result });
-                } else {
-                    this.executedCommands.push({ command: this.text, result: result.error.message || result.error });
-                }
+                const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e }));
+                result.success && this.executedCommands.push({ command: this.text, result: result.result });
             }
             this.text = "";
         }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 74d7b3ae7..b875e7896 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -1,8 +1,8 @@
-import { observable, ObservableMap, runInAction } from "mobx";
+import { observable, ObservableMap, runInAction, action } from "mobx";
 import { alias, map, serializable } from "serializr";
 import { DocServer } from "../client/DocServer";
 import { DocumentType } from "../client/documents/DocumentTypes";
-import { CompileScript, Scripting, scriptingGlobal } from "../client/util/Scripting";
+import { Scripting, scriptingGlobal } from "../client/util/Scripting";
 import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from "../client/util/SerializationHelper";
 import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, Update } from "./FieldSymbols";
 import { List } from "./List";
@@ -143,8 +143,14 @@ export class Doc extends RefField {
 
     private [Self] = this;
     private [SelfProxy]: any;
-    public [WidthSym] = () => NumCast(this[SelfProxy].width);  // bcz: is this the right way to access width/height?   it didn't work with : this.width
-    public [HeightSym] = () => NumCast(this[SelfProxy].height);
+    public [WidthSym] = () => {
+        let animDims = this[SelfProxy].animateToDimensions ? Array.from(Cast(this[SelfProxy].animateToDimensions, listSpec("number"))!) : undefined;
+        return animDims ? animDims[0] : NumCast(this[SelfProxy].width);
+    }
+    public [HeightSym] = () => {
+        let animDims = this[SelfProxy].animateToDimensions ? Array.from(Cast(this[SelfProxy].animateToDimensions, listSpec("number"))!) : undefined;
+        return animDims ? animDims[1] : NumCast(this[SelfProxy].height);
+    }
 
     [ToScriptString]() {
         return "invalid";
@@ -376,13 +382,13 @@ export namespace Doc {
     }
 
     //
-    // Resolves a reference to a field by returning 'doc' if field extension is specified,
+    // Resolves a reference to a field by returning 'doc' if no field extension is specified,
     // otherwise, it returns the extension document stored in doc.<fieldKey>_ext.
     // This mechanism allows any fields to be extended with an extension document that can
     // be used to capture field-specific metadata.  For example, an image field can be extended
     // to store annotations, ink, and other data.
     //
-    export function resolvedFieldDataDoc(doc: Doc, fieldKey: string, fieldExt: string) {
+    export function fieldExtensionDoc(doc: Doc, fieldKey: string, fieldExt: string = "yes") {
         return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
     }
 
@@ -399,26 +405,28 @@ export namespace Doc {
         return docExtensionForField;
     }
 
-    export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+    export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
         let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
         if (docExtensionForField === undefined) {
-            setTimeout(() => {
-                CreateDocumentExtensionForField(doc, fieldKey);
-            }, 0);
+            if (immediate) CreateDocumentExtensionForField(doc, fieldKey);
+            else setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
         } else if (doc instanceof Doc) { // backward compatibility -- add fields for docs that don't have them already
             docExtensionForField.extendsDoc === undefined && setTimeout(() => docExtensionForField.extendsDoc = doc, 0);
             docExtensionForField.type === undefined && setTimeout(() => docExtensionForField.type = DocumentType.EXTENSION, 0);
         }
     }
+    export function MakeTitled(title: string) {
+        let doc = new Doc();
+        doc.title = title;
+        return doc;
+    }
     export function MakeAlias(doc: Doc) {
         let alias = !GetT(doc, "isPrototype", "boolean", true) ? Doc.MakeCopy(doc) : Doc.MakeDelegate(doc);
-        let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
-        let script = `return renameAlias(self, ${aliasNumber})`;
-        //let script = "StrCast(self.title).replace(/\\([0-9]*\\)/, \"\") + `(${n})`";
-        let compiled = CompileScript(script, { params: { this: "Doc" }, capturedVariables: { self: doc }, typecheck: false });
-        if (compiled.compiled) {
-            alias.title = new ComputedField(compiled);
+        if (alias.layout instanceof Doc) {
+            alias.layout = Doc.MakeAlias(alias.layout);
         }
+        let aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1;
+        alias.title = ComputedField.MakeFunction(`renameAlias(this, ${aliasNumber})`);
         return alias;
     }
 
@@ -469,19 +477,42 @@ export namespace Doc {
         let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
         if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
             Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
-            let fieldExtensionDoc = Doc.resolvedFieldDataDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
+            let fieldExtensionDoc = Doc.fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
             layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined);
         } else layoutDoc = Doc.expandTemplateLayout(childDocLayout, resolvedDataDoc);
         return { layout: layoutDoc, data: resolvedDataDoc };
     }
 
-    export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
-        const copy = new Doc;
+    export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
+        Object.keys(doc).forEach(key => {
+            const field = ProxyField.WithoutProxy(() => doc[key]);
+            if (key === "proto" && copyProto) {
+                if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) {
+                    overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto);
+                }
+            } else {
+                if (field instanceof RefField) {
+                    overwrite[key] = field;
+                } else if (field instanceof ObjectField) {
+                    overwrite[key] = ObjectField.MakeCopy(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happend...
+                } else {
+                    overwrite[key] = field;
+                }
+            }
+        });
+
+        return overwrite;
+    }
+
+    export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
+        const copy = new Doc(copyProtoId, true);
         Object.keys(doc).forEach(key => {
             const field = ProxyField.WithoutProxy(() => doc[key]);
             if (key === "proto" && copyProto) {
-                if (field instanceof Doc) {
-                    copy[key] = Doc.MakeCopy(field);
+                if (doc[key] instanceof Doc) {
+                    copy[key] = Doc.MakeCopy(doc[key]!, false);
                 }
             } else {
                 if (field instanceof RefField) {
@@ -513,22 +544,9 @@ export namespace Doc {
 
     let _applyCount: number = 0;
     export function ApplyTemplate(templateDoc: Doc) {
-        if (!templateDoc) return undefined;
-        let datadoc = new Doc();
-        let otherdoc = Doc.MakeDelegate(datadoc);
-        otherdoc.width = templateDoc[WidthSym]();
-        otherdoc.height = templateDoc[HeightSym]();
-        otherdoc.title = templateDoc.title + "(..." + _applyCount++ + ")";
-        otherdoc.layout = Doc.MakeDelegate(templateDoc);
-        otherdoc.miniLayout = StrCast(templateDoc.miniLayout);
-        otherdoc.detailedLayout = otherdoc.layout;
-        otherdoc.type = DocumentType.TEMPLATE;
-        !templateDoc.nativeWidth && (otherdoc.nativeWidth = 0);
-        !templateDoc.nativeHeight && (otherdoc.nativeHeight = 0);
-        !templateDoc.nativeWidth && (otherdoc.ignoreAspect = true);
-        return otherdoc;
-    }
-    export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetData?: Doc, useTemplateDoc?: boolean) {
+        return !templateDoc ? undefined : ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+    }
+    export function ApplyTemplateTo(templateDoc: Doc, target: Doc, titleTarget: string | undefined = undefined) {
         if (!templateDoc) {
             target.layout = undefined;
             target.nativeWidth = undefined;
@@ -537,46 +555,41 @@ export namespace Doc {
             target.type = undefined;
             return;
         }
-        let temp = useTemplateDoc ? templateDoc : Doc.MakeDelegate(templateDoc);
-        target.nativeWidth = Doc.GetProto(target).nativeWidth = undefined;
-        target.nativeHeight = Doc.GetProto(target).nativeHeight = undefined;
-        !templateDoc.nativeWidth && (target.nativeWidth = 0);
-        !templateDoc.nativeHeight && (target.nativeHeight = 0);
-        !templateDoc.nativeHeight && (target.ignoreAspect = true);
+
+        let layoutCustom = Doc.MakeTitled("layoutCustom");
+        let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
+
+        titleTarget && (target.title = titleTarget);
+        target.type = DocumentType.TEMPLATE;
         target.width = templateDoc.width;
         target.height = templateDoc.height;
+        target.nativeWidth = templateDoc.nativeWidth ? templateDoc.nativeWidth : 0;
+        target.nativeHeight = templateDoc.nativeHeight ? templateDoc.nativeHeight : 0;
+        target.ignoreAspect = templateDoc.nativeWidth ? true : false;
         target.onClick = templateDoc.onClick instanceof ObjectField && templateDoc.onClick[Copy]();
-        target.type = DocumentType.TEMPLATE;
-        if (targetData && targetData.layout === target) {
-            targetData.layout = temp;
-            targetData.miniLayout = StrCast(templateDoc.miniLayout);
-            targetData.detailedLayout = targetData.layout;
-        } else {
-            target.layout = temp;
-            target.miniLayout = StrCast(templateDoc.miniLayout);
-            target.detailedLayout = target.layout;
-        }
+        target.layout = layoutCustomLayout;
+        target.backgroundLayout = layoutCustomLayout.backgroundLayout;
+
+        target.layoutNative = Cast(templateDoc.layoutNative, Doc) as Doc;
+        target.layoutCustom = layoutCustom;
+        return target;
     }
 
-    export function MakeTemplate(fieldTemplate: Doc, metaKey: string, templateDataDoc: Doc) {
+    export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false) {
         // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
+        let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
         let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
         let fieldLayoutDoc = fieldTemplate;
         if (fieldTemplate.layout instanceof Doc) {
             fieldLayoutDoc = Doc.MakeDelegate(fieldTemplate.layout);
         }
-        let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
         if (backgroundLayout) {
-            backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
+            backgroundLayout = backgroundLayout.replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
         }
 
-        let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
-        layoutDelegate.layout = layout;
-
-        fieldTemplate.templateField = metaKey;
-        fieldTemplate.title = metaKey;
+        fieldTemplate.templateField = metadataFieldName;
+        fieldTemplate.title = metadataFieldName;
         fieldTemplate.isTemplate = true;
-        fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
         fieldTemplate.backgroundLayout = backgroundLayout;
         /* move certain layout properties from the original data doc to the template layout to avoid
            inheriting them from the template's data doc which may also define these fields for its own use.
@@ -588,32 +601,17 @@ export namespace Doc {
         fieldTemplate.panX = 0;
         fieldTemplate.panY = 0;
         fieldTemplate.scale = 1;
-        fieldTemplate.showTitle = "title";
-        setTimeout(() => fieldTemplate.proto = templateDataDoc);
-    }
-
-    export function ToggleDetailLayout(d: Doc) {
-        runInAction(async () => {
-            let miniLayout = await PromiseValue(d.miniLayout);
-            let detailLayout = await PromiseValue(d.detailedLayout);
-            d.layout !== miniLayout ? miniLayout && (d.layout = d.miniLayout) : detailLayout && (d.layout = detailLayout);
-            if (d.layout === detailLayout) d.nativeWidth = d.nativeHeight = 0;
-            if (StrCast(d.layout) !== "") d.nativeWidth = d.nativeHeight = undefined;
-        });
-    }
-    export function UseDetailLayout(d: Doc) {
-        runInAction(async () => {
-            let detailLayout = await d.detailedLayout;
-            if (detailLayout) {
-                d.layout = detailLayout;
-                d.nativeWidth = d.nativeHeight = undefined;
-                if (detailLayout instanceof Doc) {
-                    let delegDetailLayout = Doc.MakeDelegate(detailLayout);
-                    d.layout = delegDetailLayout;
-                    delegDetailLayout.layout = await delegDetailLayout.detailedLayout;
-                }
-            }
-        });
+        fieldTemplate.showTitle = suppressTitle ? undefined : "title";
+        let data = fieldTemplate.data;
+        setTimeout(action(() => {
+            !templateDataDoc[metadataFieldName] && data instanceof ObjectField && (Doc.GetProto(templateDataDoc)[metadataFieldName] = ObjectField.MakeCopy(data));
+            let layout = StrCast(fieldLayoutDoc.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metadataFieldName}"}`);
+            let layoutDelegate = fieldTemplate.layout instanceof Doc ? fieldLayoutDoc : fieldTemplate;
+            layoutDelegate.layout = layout;
+            fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
+            if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
+            fieldTemplate.proto = templateDataDoc;
+        }), 0);
     }
 
     export function isBrushedHighlightedDegree(doc: Doc) {
@@ -685,7 +683,7 @@ export namespace Doc {
         manager.BrushedDoc.clear();
     }
 }
-Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(doc.title).replace(/\([0-9]*\)/, "") + `(${n})`; });
+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 copyField(field: any) { return ObjectField.MakeCopy(field); });
 Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 8f4ab58eb..02079e92c 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -275,7 +275,7 @@ export namespace RichTextUtils {
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
                 const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 });
-                DocumentView.makeCustomViewClicked(backingDoc);
+                DocumentView.makeCustomViewClicked(backingDoc, undefined);
                 docid = backingDoc[Id];
                 textNote[guid] = docid;
             } else {
@@ -404,7 +404,7 @@ export namespace RichTextUtils {
                                     let exported = (await Cast(linkDoc.anchor2, Doc))!;
                                     if (!exported.customLayout) {
                                         exported = Doc.MakeAlias(exported);
-                                        DocumentView.makeCustomViewClicked(exported);
+                                        DocumentView.makeCustomViewClicked(exported, undefined);
                                         linkDoc.anchor2 = exported;
                                     }
                                     url = Utils.shareUrl(exported[Id]);
diff --git a/src/new_fields/ScriptField.ts b/src/new_fields/ScriptField.ts
index 83fb52d07..cdc9871a8 100644
--- a/src/new_fields/ScriptField.ts
+++ b/src/new_fields/ScriptField.ts
@@ -1,5 +1,5 @@
 import { ObjectField } from "./ObjectField";
-import { CompiledScript, CompileScript, scriptingGlobal } from "../client/util/Scripting";
+import { CompiledScript, CompileScript, scriptingGlobal, ScriptOptions } from "../client/util/Scripting";
 import { Copy, ToScriptString, Parent, SelfProxy } from "./FieldSymbols";
 import { serializable, createSimpleSchema, map, primitive, object, deserialize, PropSchema, custom, SKIP } from "serializr";
 import { Deserializable, autoObject } from "../client/util/SerializationHelper";
@@ -101,19 +101,39 @@ export class ScriptField extends ObjectField {
     [ToScriptString]() {
         return "script field";
     }
+    public static CompileScript(script: string, params: object = {}, addReturn = false) {
+        let compiled = CompileScript(script, {
+            params: { this: Doc.name, ...params },
+            typecheck: false,
+            editable: true,
+            addReturn: addReturn
+        });
+        return compiled;
+    }
+    public static MakeFunction(script: string, params: object = {}) {
+        let compiled = ScriptField.CompileScript(script, params, true);
+        return compiled.compiled ? new ScriptField(compiled) : undefined;
+    }
+
+    public static MakeScript(script: string, params: object = {}) {
+        let compiled = ScriptField.CompileScript(script, params, false);
+        return compiled.compiled ? new ScriptField(compiled) : undefined;
+    }
 }
 
 @scriptingGlobal
 @Deserializable("computed", deserializeScript)
 export class ComputedField extends ScriptField {
     //TODO maybe add an observable cache based on what is passed in for doc, considering there shouldn't really be that many possible values for doc
-    value = computedFn((doc: Doc) => {
-        const val = this.script.run({ this: doc });
-        if (val.success) {
-            return val.result;
-        }
-        return undefined;
-    });
+    value = computedFn((doc: Doc) => this.script.run({ this: doc }, console.log).result);
+    public static MakeScript(script: string, params: object = {}, ) {
+        let compiled = ScriptField.CompileScript(script, params, false);
+        return compiled.compiled ? new ComputedField(compiled) : undefined;
+    }
+    public static MakeFunction(script: string, params: object = {}) {
+        let compiled = ScriptField.CompileScript(script, params, true);
+        return compiled.compiled ? new ComputedField(compiled) : undefined;
+    }
 }
 
 export namespace ComputedField {
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 8b20d29b1..f69b274d3 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -42,7 +42,7 @@ export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
     const provider = req.path.split("/").slice(-1)[0];
 
     if (req.user) {
-        if (_.find((req.user as any).tokens, { kind: provider })) {
+        if (_.find(req.user.tokens, { kind: provider })) {
             next();
         } else {
             res.redirect(`/auth/${provider}`);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 050a71eb4..24bad694d 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -38,46 +38,62 @@ export class CurrentUserUtils {
         doc.xMargin = 5;
         doc.yMargin = 5;
         doc.boxShadow = "0 0";
-        doc.excludeFromLibrary = true;
         doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
         return doc;
     }
 
     static updateUserDocument(doc: Doc) {
+
+        // setup workspaces library item
         if (doc.workspaces === undefined) {
             const workspaces = Docs.Create.TreeDocument([], { title: "Workspaces", height: 100 });
-            workspaces.excludeFromLibrary = true;
-            workspaces.workspaceLibrary = true;
             workspaces.boxShadow = "0 0";
             doc.workspaces = workspaces;
         }
-        PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => workspaces && (workspaces.preventTreeViewOpen = true));
+        PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => {
+            if (workspaces) {
+                workspaces.preventTreeViewOpen = true;
+                workspaces.forceActive = true;
+                workspaces.lockedPosition = true;
+            }
+        });
+
+        // setup notes list
         if (doc.noteTypes === undefined) {
             let notes = [Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplate: true }),
             Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplate: true }),
             Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplate: true }),
             Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplate: true })];
             const noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
-            noteTypes.excludeFromLibrary = true;
             doc.noteTypes = noteTypes;
         }
         PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+
+        // setup Recently Closed library item
         if (doc.recentlyClosed === undefined) {
             const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
-            recentlyClosed.excludeFromLibrary = true;
             recentlyClosed.boxShadow = "0 0";
             doc.recentlyClosed = recentlyClosed;
         }
-        PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => recent && (recent.preventTreeViewOpen = true));
+        PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => {
+            if (recent) {
+                recent.preventTreeViewOpen = true;
+                recent.forceActive = true;
+                recent.lockedPosition = true;
+            }
+        });
+
+
         if (doc.curPresentation === undefined) {
             const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" });
-            curPresentation.excludeFromLibrary = true;
             curPresentation.boxShadow = "0 0";
             doc.curPresentation = curPresentation;
         }
+
         if (doc.sidebar === undefined) {
             const sidebar = Docs.Create.StackingDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Sidebar" });
-            sidebar.excludeFromLibrary = true;
+            sidebar.forceActive = true;
+            sidebar.lockedPosition = true;
             sidebar.gridGap = 5;
             sidebar.xMargin = 5;
             sidebar.yMargin = 5;
@@ -85,18 +101,22 @@ export class CurrentUserUtils {
             sidebar.boxShadow = "1 1 3";
             doc.sidebar = sidebar;
         }
+
         if (doc.overlays === undefined) {
             const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
-            overlays.excludeFromLibrary = true;
             Doc.GetProto(overlays).backgroundColor = "#aca3a6";
             doc.overlays = overlays;
         }
+
         if (doc.linkFollowBox === undefined) {
             PromiseValue(Cast(doc.overlays, Doc)).then(overlays => overlays && Doc.AddDocToList(overlays, "data", doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" })));
         }
+
         StrCast(doc.title).indexOf("@") !== -1 && (doc.title = StrCast(doc.title).split("@")[0] + "'s Library");
         doc.width = 100;
         doc.preventTreeViewOpen = true;
+        doc.forceActive = true;
+        doc.lockedPosition = true;
     }
 
     public static loadCurrentUser() {
-- 
cgit v1.2.3-70-g09d2


From 9fbdb0088bb42235bb530602c5275e015f2609bd Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 23 Sep 2019 13:27:38 -0400
Subject: restructured link following to text regions

---
 src/client/documents/Documents.ts           |  3 --
 src/client/util/LinkManager.ts              | 15 ------
 src/client/util/RichTextSchema.tsx          |  3 +-
 src/client/util/TooltipTextMenu.tsx         | 25 ++++-----
 src/client/views/linking/LinkFollowBox.tsx  |  8 ++-
 src/client/views/nodes/FormattedTextBox.tsx | 80 +++++++++++++----------------
 6 files changed, 53 insertions(+), 81 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 079ff00db..4ae770e25 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -638,9 +638,6 @@ export namespace DocUtils {
         });
     }
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
-        // if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
-        if (LinkManager.Instance.doesNormalLinkExist(source, target) && description !== "in-text link being created") return undefined; // normal describes the type of link attempting to be created
-        // if normal link already exists and !normal (in text link is not being created) then return
         let sv = DocumentManager.Instance.getDocumentView(source);
         if (sv && sv.props.ContainingCollectionDoc === target) return;
         if (target === CurrentUserUtils.UserDocument) return undefined;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index b285b967b..8a668e8d8 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -238,21 +238,6 @@ export class LinkManager {
         return index !== -1;
     }
 
-    // checks if a normal link (i.e. no in-text link) exists with given anchors
-    public doesNormalLinkExist(anchor1: Doc, anchor2: Doc): boolean {
-        let allLinks = LinkManager.Instance.getAllLinks();
-        let index = allLinks.findIndex(linkDoc => {
-            if ((Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2)) ||
-                (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1))) {
-                console.log("guid: " + linkDoc.guid);
-            }
-            return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor2) && linkDoc.guid === undefined) ||
-                (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, null), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, null), anchor1) && linkDoc.guid === undefined);
-        });
-        return index !== -1;
-    }
-
-
     // finds the opposite anchor of a given anchor in a link
     //TODO This should probably return undefined if there isn't an opposite anchor
     //TODO This should also await the return value of the anchor so we don't filter out promises
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index ea31671ac..9d5ccffe9 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -253,8 +253,7 @@ export const marks: { [index: string]: MarkSpec } = {
             href: {},
             location: { default: null },
             title: { default: null },
-            guid: { default: null },
-            docref: { default: false }
+            docref: { default: false } // flags whether the linked text comes from a document within Dash.  If so, an attribution label is appended after the text
         },
         inclusive: false,
         parseDOM: [{
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index c84d98df9..987bc4f58 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -302,16 +302,16 @@ export class TooltipTextMenu {
                     {
                         handlers: {
                             dragComplete: action(() => {
-                                let linkDoc = dragData.linkDocument;
-                                let guid = Utils.GenerateGuid();
-                                let proto = Doc.GetProto(linkDoc);
-                                if (proto && docView) {
-                                    proto.sourceContext = docView.props.ContainingCollectionDoc;
-                                }
-                                linkDoc.guid = guid;
-                                let text = this.makeLink(Utils.prepend("/doc/" + linkDoc[Id]), ctrlKey ? "onRight" : "inTab", guid);
-                                if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
-                                    proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
+                                if (dragData.linkDocument) {
+                                    let linkDoc = dragData.linkDocument;
+                                    let proto = Doc.GetProto(linkDoc);
+                                    if (proto && docView) {
+                                        proto.sourceContext = docView.props.ContainingCollectionDoc;
+                                    }
+                                    let text = this.makeLink(linkDoc, ctrlKey ? "onRight" : "inTab");
+                                    if (linkDoc instanceof Doc && linkDoc.anchor2 instanceof Doc) {
+                                        proto.title = text === "" ? proto.title : text + " to " + linkDoc.anchor2.title; // TODODO open to more descriptive descriptions of following in text link
+                                    }
                                 }
                             }),
                         },
@@ -398,9 +398,10 @@ export class TooltipTextMenu {
     //     let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
     // }
 
-    makeLink = (target: string, location: string, guid?: string): string => {
+    makeLink = (targetDoc: Doc, location: string): string => {
+        let target = Utils.prepend("/doc/" + targetDoc[Id]);
         let node = this.view.state.selection.$from.nodeAfter;
-        let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location, guid: guid });
+        let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location, guid: targetDoc[Id] });
         this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
         this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
         node = this.view.state.selection.$from.nodeAfter;
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 1280ae28b..cad404d1f 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -243,7 +243,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             let proto = Doc.GetProto(LinkFollowBox.linkDoc);
             let targetContext = await Cast(proto.targetContext, Doc);
             let sourceContext = await Cast(proto.sourceContext, Doc);
-            let guid = StrCast(LinkFollowBox.linkDoc.guid);
+            let guid = StrCast(LinkFollowBox.linkDoc[Id]);
             const shouldZoom = options ? options.shouldZoom : false;
 
             let dockingFunc = (document: Doc) => { (this._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
@@ -255,12 +255,10 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
                 if (LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc) {
                     if (guid) {
-                        LinkFollowBox.destinationDoc.guid = guid;
+                        let views = DocumentManager.Instance.getDocumentViews(jumpToDoc);
+                        views.length && (views[0].props.Document.scrollToLinkID = guid);
                     } else {
                         jumpToDoc.linkHref = Utils.prepend("/doc/" + StrCast(LinkFollowBox.linkDoc[Id]));
-                        let newguid = Utils.GenerateGuid();
-                        LinkFollowBox.linkDoc.guid = newguid;
-                        LinkFollowBox.destinationDoc.guid = newguid;
                     }
                 }
             }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 53f28ac00..0a8b841a9 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -80,6 +80,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _nodeClicked: any;
     private _undoTyping?: UndoManager.Batch;
     private _searchReactionDisposer?: Lambda;
+    private _guidReactionDisposer: Opt<IReactionDisposer>;
     private _reactionDisposer: Opt<IReactionDisposer>;
     private _textReactionDisposer: Opt<IReactionDisposer>;
     private _heightReactionDisposer: Opt<IReactionDisposer>;
@@ -139,66 +140,50 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
         }
 
-        this.props.Document.guid = undefined;
-        this.props.Document.linkHref = undefined;
-
-        reaction(
-            () => StrCast(this.props.Document.guid),
-            async (guid) => {
-                let start = -1;
-                let href = this.props.Document.linkHref;
-
-                if (this._editorView && guid) {
-                    let editor = this._editorView;
-                    let ret = findLinkFrag(editor.state.doc.content, editor);
-
-                    if (ret.frag.size > 2) {
-                        let tr;
-                        if (ret.frag.firstChild) {
-                            let between = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize));
-                            tr = editor.state.tr.setSelection(between);
-                        } else {
-                            let near = TextSelection.near(editor.state.doc.resolve(ret.start));
-                            tr = editor.state.tr.setSelection(near);
-                        }
-
-                        editor.focus();
-                        editor.dispatch(tr.scrollIntoView());
-                        editor.dispatch(tr.scrollIntoView()); // bcz: sometimes selection doesn't fully scroll into view on smaller text boxes <5 lines visibility -- hopefully avoidable by ppl just not using small boxes...?
-
-                        this.props.Document.guid = undefined;
-                        this.props.Document.linkHref = undefined;
-                    }
-                }
-
-                function findLinkFrag(frag: Fragment, editor: EditorView) {
+        this._guidReactionDisposer = reaction(
+            () => StrCast(this.props.Document.scrollToLinkID),
+            async (scrollToLinkID) => {
+                let findLinkFrag = (frag: Fragment, editor: EditorView) => {
                     const nodes: Node[] = [];
                     frag.forEach((node, index) => {
                         let examinedNode = findLinkNode(node, editor);
-                        if (examinedNode && examinedNode.textContent !== "") {
+                        if (examinedNode && examinedNode.textContent) {
                             nodes.push(examinedNode);
                             start += index;
                         }
                     });
                     return { frag: Fragment.fromArray(nodes), start: start };
                 }
-                function findLinkNode(node: Node, editor: EditorView) {
+                let findLinkNode = (node: Node, editor: EditorView) => {
                     if (!node.isText) {
                         const content = findLinkFrag(node.content, editor);
                         return node.copy(content.frag);
                     }
                     const marks = [...node.marks];
-                    const linkIndex = marks.findIndex(mark => mark.type.name === "link");
-                    if (linkIndex !== -1) {
-                        if (guid === marks[linkIndex].attrs.guid) {
-                            return node;
-                        } else if (href && href === marks[linkIndex].attrs.href) { // retroactively fixing old in-text links by adding guid
-                            marks[linkIndex].attrs.guid = guid;
-                            return node;
+                    const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+                    return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+                }
+
+                let start = -1;
+
+                if (this._editorView && scrollToLinkID) {
+                    let editor = this._editorView;
+                    let ret = findLinkFrag(editor.state.doc.content, editor);
+
+                    if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
+                        let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
+                        if (ret.frag.firstChild) {
+                            selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
                         }
+                        editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+                        const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+                        setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+                        setTimeout(() => this.unhighlightSearchTerms(), 2000);
+
+                        this.props.Document.scrollToLinkID = undefined;
                     }
-                    return undefined;
                 }
+
             }
         );
     }
@@ -759,6 +744,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             this._editorView && this._editorView.destroy();
             this._editorView = new EditorView(this._proseRef, {
                 state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
+                handleScrollToSelection: (editorView) => {
+                    let ref = editorView.domAtPos(editorView.state.selection.from);
+                    let r1 = (ref.node as any).getBoundingClientRect();
+                    let r3 = self._ref.current!.getBoundingClientRect();
+                    self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale;
+                    return true;
+                },
                 dispatchTransaction: this.dispatchTransaction,
                 nodeViews: {
                     image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
@@ -799,6 +791,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     componentWillUnmount() {
+        this._guidReactionDisposer && this._guidReactionDisposer();
         this._rulesReactionDisposer && this._rulesReactionDisposer();
         this._reactionDisposer && this._reactionDisposer();
         this._proxyReactionDisposer && this._proxyReactionDisposer();
@@ -823,7 +816,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let ctrlKey = e.ctrlKey;
         if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) {
             let href = (e.target as any).href;
-            let guid = (e.target as any).guid;
             let location: string;
             if ((e.target as any).attributes.location) {
                 location = (e.target as any).attributes.location.value;
-- 
cgit v1.2.3-70-g09d2


From 0e08e20021a4fdb3235cb13b2a288ad8a3705529 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 23 Sep 2019 21:51:02 -0400
Subject: allowed resizing of bullets in a more reasonable way. restore
 use_marks.

---
 src/client/util/ProsemirrorExampleTransfer.ts | 48 +++++++++++++++------------
 src/client/util/RichTextSchema.tsx            |  7 ++--
 src/client/util/TooltipTextMenu.tsx           | 10 +++---
 src/client/util/prosemirrorPatches.js         |  2 +-
 src/client/views/nodes/FormattedTextBox.scss  | 20 +++++------
 src/client/views/nodes/FormattedTextBox.tsx   | 15 +++++++--
 6 files changed, 61 insertions(+), 41 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index 3e3d3155c..aab437176 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -11,6 +11,20 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
 
 export type KeyMap = { [key: string]: any };
 
+export let updateBullets = (tx2: Transaction, schema: Schema) => {
+    let fontSize: number | undefined = undefined;
+    tx2.doc.descendants((node: any, offset: any, index: any) => {
+        if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+            let path = (tx2.doc.resolve(offset) as any).path;
+            let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
+            if (node.type === schema.nodes.ordered_list) depth++;
+            fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
+            let fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
+            tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+        }
+    });
+    return tx2;
+};
 export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?: KeyMap): KeyMap {
     let keys: { [key: string]: any } = {}, type;
 
@@ -93,16 +107,6 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
 
     bind("Mod-s", TooltipTextMenu.insertStar);
 
-    let updateBullets = (tx2: Transaction) => {
-        tx2.doc.descendants((node: any, offset: any, index: any) => {
-            if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
-                let path = (tx2.doc.resolve(offset) as any).path;
-                let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
-                if (node.type === schema.nodes.ordered_list) depth++;
-                tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: node.attrs.mapStyle, bulletStyle: depth }, node.marks);
-            }
-        });
-    };
 
 
     bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
@@ -110,18 +114,18 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         var range = ref.$from.blockRange(ref.$to);
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
         if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
-            updateBullets(tx2);
-            marks && tx2.ensureMarks([...marks]);
-            marks && tx2.setStoredMarks([...marks]);
-            dispatch(tx2);
+            let tx3 = updateBullets(tx2, schema);
+            marks && tx3.ensureMarks([...marks]);
+            marks && tx3.setStoredMarks([...marks]);
+            dispatch(tx3);
         })) { // couldn't sink into an existing list, so wrap in a new one
             let newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
             if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
-                updateBullets(tx2);
+                let tx3 = updateBullets(tx2, schema);
                 // when promoting to a list, assume list will format things so don't copy the stored marks.
-                marks && tx2.ensureMarks([...marks]);
-                marks && tx2.setStoredMarks([...marks]);
-                dispatch(tx2);
+                marks && tx3.ensureMarks([...marks]);
+                marks && tx3.setStoredMarks([...marks]);
+                dispatch(tx3);
             })) {
                 console.log("bullet promote fail");
             }
@@ -132,10 +136,10 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
         var marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
 
         if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
-            updateBullets(tx2);
-            marks && tx2.ensureMarks([...marks]);
-            marks && tx2.setStoredMarks([...marks]);
-            dispatch(tx2);
+            let tx3 = updateBullets(tx2, schema);
+            marks && tx3.ensureMarks([...marks]);
+            marks && tx3.setStoredMarks([...marks]);
+            dispatch(tx3);
         })) {
             console.log("bullet demote fail");
         }
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 9d5ccffe9..710d55605 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -198,6 +198,8 @@ export const nodes: { [index: string]: NodeSpec } = {
         attrs: {
             bulletStyle: { default: 0 },
             mapStyle: { default: "decimal" },
+            setFontSize: { default: undefined },
+            inheritedFontSize: { default: undefined },
             visibility: { default: true }
         },
         toDOM(node: Node<any>) {
@@ -205,8 +207,9 @@ export const nodes: { [index: string]: NodeSpec } = {
             const decMap = bs ? "decimal" + bs : "";
             const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : "";
             let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap;
-            return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
-                ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+            let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+            return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;font-size: ${fsize}` }, 0] :
+                ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}` }];
         }
     },
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 987bc4f58..a83a3949d 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -19,6 +19,7 @@ import { LinkManager } from "./LinkManager";
 import { schema } from "./RichTextSchema";
 import "./TooltipTextMenu.scss";
 import { Cast, NumCast } from '../../new_fields/Types';
+import { updateBullets } from './ProsemirrorExampleTransfer';
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
 
@@ -518,10 +519,11 @@ export class TooltipTextMenu {
                 }
             }
             //actually apply font
-            return toggleMark(markType)(view.state, view.dispatch, view);
-        }
-        else {
-            return;
+            if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
+                view.dispatch(updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+                    { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema));
+            }
+            else toggleMark(markType)(view.state, view.dispatch, view);
         }
     }
 
diff --git a/src/client/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
index 188e3e1c5..269423482 100644
--- a/src/client/util/prosemirrorPatches.js
+++ b/src/client/util/prosemirrorPatches.js
@@ -82,7 +82,7 @@ function sinkListItem(itemType) {
         if (dispatch) {
             var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
             var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
-            let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create(parent.attrs, inner)))),
+            let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create({ ...parent.attrs, fontSize: parent.attrs.fontSize ? parent.attrs.fontSize - 4 : undefined }, inner)))),
                 nestedBefore ? 3 : 1, 0);
             var before = range.start, after = range.end;
             dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 0d7277cbe..435f5c055 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -164,13 +164,13 @@ ol { counter-reset: deci1 0;}
 .upper-alpha-ol {counter-reset: ualph; p { display: inline }; font-size: 18 }
 .lower-roman-ol {counter-reset: lroman;  p { display: inline }; font-size: 14; }
 .lower-alpha-ol {counter-reset: lalpha;  p { display: inline }; font-size: 10;}
-.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; width: 30}
-.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; width: 35}
-.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; width: 35}
-.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; width: 40}
-.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; width: 40}
-.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; width: 45}
-.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; width: 50}
-.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; width: 35 }
-.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; width: 50 }
-.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; width: 35}
+.decimal1:before { content: counter(deci1) ")"; counter-increment: deci1; display:inline-block; min-width: 30;}
+.decimal2:before { content: counter(deci1) "." counter(deci2) ")"; counter-increment: deci2; display:inline-block; min-width: 35}
+.decimal3:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) ")"; counter-increment: deci3; display:inline-block; min-width: 35}
+.decimal4:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) ")"; counter-increment: deci4; display:inline-block; min-width: 40}
+.decimal5:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) ")"; counter-increment: deci5; display:inline-block; min-width: 40}
+.decimal6:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) ")"; counter-increment: deci6; display:inline-block; min-width: 45}
+.decimal7:before { content: counter(deci1) "." counter(deci2) "." counter(deci3) "." counter(deci4) "." counter(deci5) "." counter(deci6) "." counter(deci7) ")"; counter-increment: deci7; display:inline-block; min-width: 50}
+.upper-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) ")"; counter-increment: ualph; display:inline-block; min-width: 35 }
+.lower-roman:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) ")"; counter-increment: lroman;display:inline-block; min-width: 50 }
+.lower-alpha:before { content: counter(deci1) "." counter(ualph, upper-alpha) "." counter(lroman, lower-roman) "." counter(lalpha, lower-alpha) ")"; counter-increment: lalpha; display:inline-block; min-width: 35}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ddabe7e10..47b64e260 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -910,7 +910,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 let node = this._editorView!.state.doc.nodeAt(pos.pos);
                 let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined;
                 if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) {
-                    this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
+                    let hit = this._editorView!.domAtPos(pos.pos).node as any;
+                    let beforeEle = document.querySelector("." + hit.className) as Element;
+                    let before = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined;
+                    let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
+                    if (beforeWidth && e.nativeEvent.offsetX < beforeWidth) {
+                        let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined;
+                        if (ol && ol.type === schema.nodes.ordered_list && !e.shiftKey) {
+                            this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
+                        } else {
+                            this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
+                        }
+                    }
                 }
             }
         }
@@ -961,7 +972,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() }));
+        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
 
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
-- 
cgit v1.2.3-70-g09d2


From 9427474b473d70974784a1517a1be902fb8d18ee Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 2 Oct 2019 18:26:55 -0400
Subject: many fixes to pdfs, linking, annotations, presentations.

---
 src/client/documents/Documents.ts                  |  24 ++--
 src/client/util/DocumentManager.ts                 | 125 +++++++++++----------
 src/client/util/RichTextSchema.tsx                 |   9 +-
 src/client/util/SharingManager.tsx                 |   2 +-
 .../views/collections/CollectionBaseView.tsx       |   9 +-
 .../views/collections/CollectionDockingView.tsx    |  10 +-
 src/client/views/collections/CollectionSubView.tsx |   1 +
 .../views/collections/CollectionTreeView.tsx       |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  10 +-
 src/client/views/linking/LinkFollowBox.tsx         |  11 +-
 src/client/views/nodes/DocumentView.tsx            |  44 ++------
 src/client/views/nodes/FormattedTextBox.tsx        |  79 ++++++-------
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |  17 ++-
 src/client/views/nodes/PresBox.tsx                 |  24 ++--
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/pdf/Annotation.tsx                |  17 ++-
 src/client/views/pdf/PDFMenu.tsx                   |   6 +-
 src/client/views/pdf/PDFViewer.tsx                 |  72 +++++-------
 .../views/presentationview/PresElementBox.tsx      |  32 +++---
 src/new_fields/Doc.ts                              |   2 +
 21 files changed, 224 insertions(+), 276 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4d9698532..2d323ea4b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -653,32 +653,30 @@ export namespace DocUtils {
             }
         });
     }
-    export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
-        let sv = DocumentManager.Instance.getDocumentView(source);
-        if (sv && sv.props.ContainingCollectionDoc === target) return;
-        if (target === CurrentUserUtils.UserDocument) return undefined;
+    export function MakeLink(source: {doc:Doc,ctx?:Doc}, target: {doc:Doc,ctx?:Doc}, title: string = "", description: string = "", id?: string, anchored1?: boolean) {
+        let sv = DocumentManager.Instance.getDocumentView(source.doc);
+        if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
+        if (target.doc === CurrentUserUtils.UserDocument) return undefined;
 
         let linkDocProto = new Doc(id, true);
         UndoManager.RunInBatch(() => {
             linkDocProto.type = DocumentType.LINK;
 
-            linkDocProto.targetContext = targetContext;
-            linkDocProto.sourceContext = sourceContext;
-            linkDocProto.title = title === "" ? source.title + " to " + target.title : title;
+            linkDocProto.targetContext = target.ctx;
+            linkDocProto.sourceContext = source.ctx;
+            linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title;
             linkDocProto.linkDescription = description;
 
-            linkDocProto.anchor1 = source;
-            linkDocProto.anchor1Page = source.curPage;
+            linkDocProto.anchor1 = source.doc;
             linkDocProto.anchor1Groups = new List<Doc>([]);
             linkDocProto.anchor1anchored = anchored1;
-            linkDocProto.anchor2 = target;
-            linkDocProto.anchor2Page = target.curPage;
+            linkDocProto.anchor2 = target.doc;
             linkDocProto.anchor2Groups = new List<Doc>([]);
 
             LinkManager.Instance.addLink(linkDocProto);
 
-            Doc.GetProto(source).links = ComputedField.MakeFunction("links(this)");
-            Doc.GetProto(target).links = ComputedField.MakeFunction("links(this)");
+            Doc.GetProto(source.doc).links = ComputedField.MakeFunction("links(this)");
+            Doc.GetProto(target.doc).links = ComputedField.MakeFunction("links(this)");
         }, "make link");
         return linkDocProto;
     }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4e6968f08..4ebcdf83c 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
 import { action, computed, observable } from 'mobx';
 import { Doc, DocListCastAsync } from '../../new_fields/Doc';
 import { Id } from '../../new_fields/FieldSymbols';
-import { Cast, NumCast } from '../../new_fields/Types';
+import { Cast, NumCast, StrCast } from '../../new_fields/Types';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionPDFView } from '../views/collections/CollectionPDFView';
 import { CollectionVideoView } from '../views/collections/CollectionVideoView';
@@ -56,9 +56,9 @@ export class DocumentManager {
         return this.getDocumentViewsById(doc[Id]);
     }
 
-    public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+    public getDocumentViewById(id: string, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | undefined {
 
-        let toReturn: DocumentView | null = null;
+        let toReturn: DocumentView | undefined;
         let passes = preferredCollection ? [preferredCollection, undefined] : [undefined];
 
         for (let pass of passes) {
@@ -81,10 +81,14 @@ export class DocumentManager {
         return toReturn;
     }
 
-    public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | null {
+    public getDocumentView(toFind: Doc, preferredCollection?: CollectionView | CollectionPDFView | CollectionVideoView): DocumentView | undefined {
         return this.getDocumentViewById(toFind[Id], preferredCollection);
     }
 
+    public getFirstDocumentView(toFind: Doc): DocumentView | undefined {
+        const views = this.getDocumentViews(toFind);
+        return views.length ? views[0] : undefined;
+    }
     public getDocumentViews(toFind: Doc): DocumentView[] {
 
         let toReturn: DocumentView[] = [];
@@ -127,72 +131,70 @@ export class DocumentManager {
         return pairs;
     }
 
-
-    @undoBatch
-    public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
-        let doc = Doc.GetProto(docDelegate);
-        const contextDoc = await Cast(doc.annotationOn, Doc);
-        if (contextDoc) {
-            contextDoc.scrollY = NumCast(doc.y) - NumCast(contextDoc.height) / 2 * 72 / 96;
-        }
-
-        let docView: DocumentView | null;
-        // using forceDockFunc as a flag for splitting linked to doc to the right...can change later if needed
-        if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
-            Doc.BrushDoc(docView.props.Document);
-            if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
-            UndoManager.RunInBatch(() => docView!.props.focus(docView!.props.Document, willZoom), "focus");
+    public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, closeContextIfNotFound: boolean = false): Promise<void> => {
+        const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+        const annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
+        if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
+            annotatedDoc && docView.props.focus(annotatedDoc, false);
+            docView.props.focus(targetDoc, willZoom);
         } else {
-            if (!contextDoc) {
-                let docs = docContext ? await DocListCastAsync(docContext.data) : undefined;
-                let found = false;
-                // bcz: this just searches within the context for the target -- perhaps it should recursively search through all children?
-                docs && docs.map(d => found = found || Doc.AreProtosEqual(d, docDelegate));
-                if (docContext && found) {
-                    let targetContextView: DocumentView | null;
-
-                    if (!forceDockFunc && docContext && (targetContextView = DocumentManager.Instance.getDocumentView(docContext))) {
-                        docContext.panTransformType = "Ease";
-                        targetContextView.props.focus(docDelegate, willZoom);
-                    } else {
-                        (dockFunc || CollectionDockingView.AddRightSplit)(docContext, undefined);
-                        setTimeout(() => {
-                            let dv = DocumentManager.Instance.getDocumentView(docContext);
-                            dv && this.jumpToDocument(docDelegate, willZoom, forceDockFunc,
-                                doc => dv!.props.focus(dv!.props.Document, true, 1, () => dv!.props.addDocTab(doc, undefined, "inPlace")),
-                                linkPage);
-                        }, 1050);
-                    }
-                } else {
-                    const actualDoc = Doc.MakeAlias(docDelegate);
-                    Doc.BrushDoc(actualDoc);
-                    if (linkPage !== undefined) actualDoc.curPage = linkPage;
-                    (dockFunc || CollectionDockingView.AddRightSplit)(actualDoc, undefined);
-                }
+            const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
+            const contextDoc = contextDocs && contextDocs.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
+            const targetDocContext = (annotatedDoc ? annotatedDoc : contextDoc);
+
+            if (!targetDocContext) { // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
+                (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined);
             } else {
-                let contextView: DocumentView | null;
-                Doc.BrushDoc(docDelegate);
-                let contextViews = DocumentManager.Instance.getDocumentViews(contextDoc);
-                if (!forceDockFunc && contextViews.length) {
-                    contextView = contextViews[0];
-                    SelectionManager.SelectDoc(contextView, false); // force unrendered annotations to be created
-                    contextDoc.panTransformType = "Ease";
+                const targetDocContextView = DocumentManager.Instance.getFirstDocumentView(targetDocContext);
+                if (targetDocContextView) { // we have a context view and aren't forced to create a new one ... focus on the context
+                    targetDocContext.panTransformType = "Ease";
+                    targetDocContext.scrollY = 0;
+                    targetDocContextView.props.focus(targetDocContextView.props.Document, willZoom);
+
+                    // now find the target document within the context
                     setTimeout(() => {
-                        SelectionManager.DeselectDoc(contextView!);
-                        docView = DocumentManager.Instance.getDocumentView(docDelegate);
-                        docView && contextView!.props.focus(contextView!.props.Document, willZoom);
-                        docView && UndoManager.RunInBatch(() => docView!.props.focus(docView!.props.Document, willZoom), "focus");
+                        const retryDocView = DocumentManager.Instance.getDocumentView(targetDoc);
+                        if (retryDocView) {
+                            retryDocView.props.focus(targetDoc, willZoom); // focus on the target if it now exists in the context
+                        } else {
+                            if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
+                            (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
+                        }
                     }, 0);
-                } else {
-                    (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined);
+                } else {  // there's no context view so we need to create one first and try again
+                    targetDocContext.scrollY = 0;
+                    (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined);
                     setTimeout(() => {
-                        this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
-                    }, 1000);
+                        const foundTargetDocContextView = DocumentManager.Instance.getDocumentView(targetDocContext);
+                        if (foundTargetDocContextView) { // we should always find a target context here....
+                            this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, true); // so call jump to doc again and if the doc isn't found, it will be created.
+                        }
+                    }, 2000); // the long timeout gives the context view a chance to create its children.  think pdf's which need to be activated to render their annotations.
                 }
             }
         }
     }
 
+    public async FollowLink(doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
+        let linkDocs = LinkManager.Instance.getAllRelatedLinks(doc);
+        SelectionManager.DeselectAll();
+        let firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) && !linkDoc.anchor1anchored);
+        let secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) && !linkDoc.anchor2anchored);
+        const firstDocWithoutView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
+        const secondDocWithoutView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
+        let first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
+        let second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
+        let linkFollowDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor2 as Doc] : undefined;
+        let linkFollowDocContexts = first.length ? [await (first[0].targetContext) as Doc, await (first[0].sourceContext) as Doc] : second.length ? [await (second[0].sourceContext) as Doc, await (second[0].targetContext) as Doc] : [undefined, undefined];
+        if (linkFollowDocs && !linkFollowDocs.some(l => l instanceof Promise)) {
+            let maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
+            let targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
+            DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom,
+                // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
+                (doc: Doc) => focus(doc, maxLocation), targetContext);
+        }
+    }
+
     @action
     zoomIntoScale = (docDelegate: Doc, scale: number) => {
         let docView = DocumentManager.Instance.getDocumentView(Doc.GetProto(docDelegate));
@@ -202,8 +204,7 @@ export class DocumentManager {
     getScaleOfDocView = (docDelegate: Doc) => {
         let doc = Doc.GetProto(docDelegate);
 
-        let docView: DocumentView | null;
-        docView = DocumentManager.Instance.getDocumentView(doc);
+        const docView = DocumentManager.Instance.getDocumentView(doc);
         if (docView) {
             return docView.props.getScale();
         } else {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 64821d8db..49bd93942 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -627,17 +627,16 @@ export class ImageResizeView {
                         let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
                         if (jumpToDoc) {
                             if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-
-                                DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+                                DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
                                 return;
                             }
                         }
                         if (targetContext) {
-                            DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+                            DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
                         } else if (jumpToDoc) {
-                            DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+                            DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
                         } else {
-                            DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, false, document => addDocTab(document, undefined, location ? location : "inTab"));
+                            DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
                         }
                     }
                 });
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index f427e40b1..1cde2aa8e 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -192,7 +192,7 @@ export default class SharingManager extends React.Component<{}> {
                 onClick={() => {
                     let context: Opt<CollectionVideoView | CollectionPDFView | CollectionView>;
                     if (this.targetDoc && this.targetDocView && (context = this.targetDocView.props.ContainingCollectionView)) {
-                        DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, undefined, undefined, context.props.Document);
+                        DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, context.props.Document);
                     }
                 }}
                 onPointerEnter={action(() => {
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index e928887e2..38d050e5c 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -5,7 +5,7 @@ import { Doc, DocListCast } from '../../../new_fields/Doc';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
 import { listSpec } from '../../../new_fields/Schema';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types';
+import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from '../../../new_fields/Types';
 import { DocumentManager } from '../../util/DocumentManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { ContextMenu } from '../ContextMenu';
@@ -130,8 +130,11 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         let value = Cast(targetDataDoc[targetField], listSpec(Doc), []);
         let index = value.reduce((p, v, i) => (v instanceof Doc && v === doc) ? i : p, -1);
         index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
-        PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
-            annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined));
+        PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => {
+            if (Doc.AreProtosEqual(annotationOn, FieldValue(Cast(this.dataDoc.extendsDoc, Doc)))) {
+                Doc.GetProto(doc).annotationOn = undefined;
+            }
+        });
 
         if (index !== -1) {
             value.splice(index, 1);
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 98aff41d3..14e513157 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -574,8 +574,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         let curPres = Cast(CurrentUserUtils.UserDocument.curPresentation, Doc) as Doc;
         if (curPres) {
             let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
-            Doc.GetProto(pinDoc).target = doc;
-            Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()');
+            Doc.GetProto(pinDoc).presentationTargetDoc = doc;
+            Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
             const data = Cast(curPres.data, listSpec(Doc));
             if (data) {
                 data.push(pinDoc);
@@ -614,8 +614,8 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
     }
 
-    panelWidth = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
-    panelHeight = () => this._document!.ignoreAspect || this._document!.fitWidth ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), this.nativeHeight()));
+    panelWidth = () => this._document && this._document.maxWidth ? Math.min(Math.max(NumCast(this._document.width), NumCast(this._document.nativeWidth)), this._panelWidth) : this._panelWidth;
+    panelHeight = () => this._panelHeight;
 
     nativeWidth = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
     nativeHeight = () => !this._document!.ignoreAspect && !this._document!.fitWidth ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
@@ -644,7 +644,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
         return Transform.Identity();
     }
-    get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; }
+    get previewPanelCenteringOffset() { return this.nativeWidth() && !this._document!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
 
     addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
         SelectionManager.DeselectAll();
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 155f2718b..28d1eb384 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -31,6 +31,7 @@ export interface CollectionViewProps extends FieldViewProps {
     moveDocument: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     PanelWidth: () => number;
     PanelHeight: () => number;
+    VisibleHeight?: () => number;
     chromeCollapsed: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
 }
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index ffd1f9170..882a0f144 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -217,7 +217,7 @@ class TreeView extends React.Component<TreeViewProps> {
         if (de.data instanceof DragManager.LinkDragData) {
             let sourceDoc = de.data.linkSourceDocument;
             let destDoc = this.props.document;
-            DocUtils.MakeLink(sourceDoc, destDoc);
+            DocUtils.MakeLink({doc:sourceDoc}, {doc:destDoc});
             e.stopPropagation();
         }
         if (de.data instanceof DragManager.DocumentDragData) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eb40e0bcb..4bdede48a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -403,8 +403,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         SelectionManager.DeselectAll();
         if (this.props.Document.scrollHeight) {
             let annotOn = Cast(doc.annotationOn, Doc) as Doc;
-            let offset = annotOn && (NumCast(annotOn.height) / 2 * 72 / 96);
-            this.props.Document.scrollY = NumCast(doc.y) - offset;
+            if (!annotOn) {
+                this.props.focus(doc);
+            } else {
+                let contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn.height);
+                let offset = annotOn && (contextHgt / 2 * 96 / 72);
+                this.props.Document.scrollY = NumCast(doc.y) - offset;
+            }
         } else {
             const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
             const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
@@ -416,6 +421,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
             this.setPan(newPanX, newPanY);
             this.Document.panTransformType = "Ease";
+            Doc.BrushDoc(this.props.Document);
             this.props.focus(this.props.Document);
             willZoom && this.setScaleToZoom(doc, scale);
 
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 72fff8e53..53b720a9e 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -171,7 +171,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @undoBatch
     openFullScreen = () => {
         if (LinkFollowBox.destinationDoc) {
-            let view: DocumentView | null = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
+            let view = DocumentManager.Instance.getDocumentView(LinkFollowBox.destinationDoc);
             view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
         }
     }
@@ -250,10 +250,10 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             let dockingFunc = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
 
             if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
+                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, async document => dockingFunc(document), targetContext);
             }
             else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
+                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, document => dockingFunc(sourceContext!));
                 if (LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc) {
                     if (guid) {
                         let views = DocumentManager.Instance.getDocumentViews(jumpToDoc);
@@ -264,12 +264,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 }
             }
             else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined,
-                    NumCast((LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 ? LinkFollowBox.linkDoc.anchor2Page : LinkFollowBox.linkDoc.anchor1Page)));
+                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom);
 
             }
             else {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc);
+                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, dockingFunc);
             }
 
             this.highlightDoc();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a903f8fe2..3273abc1d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -206,7 +206,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
         let maximizedDocs = await DocListCastAsync(this.props.Document.maximizedDocs);
         let summarizedDocs = await DocListCastAsync(this.props.Document.summarizedDocs);
-        let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
+        let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
         let expandedDocs: Doc[] = [];
         expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
         expandedDocs = summarizedDocs ? [...summarizedDocs, ...expandedDocs] : expandedDocs;
@@ -223,28 +223,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
             }
         }
-        else if (linkedDocs.length) {
-            SelectionManager.DeselectAll();
-            let first = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor1 as Doc, this.props.Document) && !d.anchor1anchored);
-            let second = linkedDocs.filter(d => Doc.AreProtosEqual(d.anchor2 as Doc, this.props.Document) && !d.anchor2anchored);
-            let firstUnshown = first.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
-            let secondUnshown = second.filter(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
-            if (firstUnshown.length) first = [firstUnshown[0]];
-            if (secondUnshown.length) second = [secondUnshown[0]];
-            let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor1 as Doc] : undefined;
-
-            // @TODO: shouldn't always follow target context
-            let linkedFwdContextDocs = [first.length ? await (first[0].targetContext) as Doc : undefined, undefined];
-            let linkedFwdPage = [first.length ? NumCast(first[0].anchor2Page, undefined) : undefined, undefined];
-
-            if (linkedFwdDocs && !linkedFwdDocs.some(l => l instanceof Promise)) {
-                let maxLocation = StrCast(linkedFwdDocs[0].maximizeLocation, "inTab");
-                let targetContext = !Doc.AreProtosEqual(linkedFwdContextDocs[altKey ? 1 : 0], this.props.ContainingCollectionDoc) ? linkedFwdContextDocs[altKey ? 1 : 0] : undefined;
-                DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, false,
-                    // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
-                    doc => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
-                    linkedFwdPage[altKey ? 1 : 0], targetContext);
-            }
+        else if (linkDocs.length) {
+            DocumentManager.Instance.FollowLink(this.props.Document,
+                (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
+                ctrlKey, altKey, this.props.ContainingCollectionDoc);
         }
     }
 
@@ -352,15 +334,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (de.data instanceof DragManager.AnnotationDragData) {
             /// this whole section for handling PDF annotations looks weird.  Need to rethink this to make it cleaner
             e.stopPropagation();
-            let sourceDoc = de.data.annotationDocument;
-            let targetDoc = this.props.Document;
-            let annotations = await DocListCastAsync(sourceDoc.annotations);
-            sourceDoc.linkedToDoc = true;
-            de.data.targetContext = this.props.ContainingCollectionDoc;
-            targetDoc.targetContext = de.data.targetContext;
-            annotations && annotations.forEach(anno => anno.target = targetDoc);
-
-            DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionDoc, `Link from ${StrCast(sourceDoc.title)}`);
+            (de.data as any).linkedToDoc = true;
+
+            DocUtils.MakeLink({ doc: de.data.annotationDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, `Link from ${StrCast(de.data.annotationDocument.title)}`);
         }
         if (de.data instanceof DragManager.DocumentDragData && de.data.applyAsTemplate) {
             Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document);
@@ -371,7 +347,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             // const docs = await SearchUtil.Search(`data_l:"${destDoc[Id]}"`, true);
             // const views = docs.map(d => DocumentManager.Instance.getDocumentView(d)).filter(d => d).map(d => d as DocumentView);
             de.data.linkSourceDocument !== this.props.Document &&
-                (de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc, undefined, "in-text link being created")); // TODODO this is where in text links get passed
+                (de.data.linkDocument = DocUtils.MakeLink({ doc: de.data.linkSourceDocument }, { doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, "in-text link being created")); // TODODO this is where in text links get passed
         }
     }
 
@@ -407,7 +383,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             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 });
-                DocUtils.MakeLink(this.props.Document, portal, undefined, portalID);
+                DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: portal }, portalID, "portal link");
                 this.Document.isButton = true;
             });
         }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 449f56b16..749886d9a 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -9,7 +9,7 @@ import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos }
 import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
 import { Copy, Id } from '../../../new_fields/FieldSymbols';
 import { List } from '../../../new_fields/List';
 import { RichTextField } from "../../../new_fields/RichTextField";
@@ -230,7 +230,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
                     DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
                     if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
-                    else DocUtils.MakeLink(this.dataDoc, this.dataDoc[key] as Doc, undefined, "Ref:" + value, undefined, undefined, id, true);
+                    else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id, true);
                 });
             });
         });
@@ -342,7 +342,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
             this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
-            DocUtils.MakeLink(this.dataDoc, target, undefined, "ImgRef:" + target.title, undefined, undefined, target[Id]);
+            DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
             this.tryUpdateHeight();
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
@@ -667,55 +667,42 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
         let cbe = event as ClipboardEvent;
-        let docId: string;
-        let regionId: string;
-        if (!cbe.clipboardData) {
-            return false;
-        }
-        let linkId: string;
-        docId = cbe.clipboardData.getData("dash/pdfOrigin");
-        regionId = cbe.clipboardData.getData("dash/pdfRegion");
-        if (!docId || !regionId) {
-            return false;
-        }
-
-        DocServer.GetRefField(docId).then(doc => {
-            DocServer.GetRefField(regionId).then(region => {
-                if (!(doc instanceof Doc) || !(region instanceof Doc)) {
-                    return;
-                }
-
-                let annotations = DocListCast(region.annotations);
-                annotations.forEach(anno => anno.target = this.props.Document);
-                let fieldExtDoc = Doc.fieldExtensionDoc(doc, "data");
-                let targetAnnotations = DocListCast(fieldExtDoc.annotations);
-                if (targetAnnotations) {
-                    targetAnnotations.push(region);
-                    fieldExtDoc.annotations = new List<Doc>(targetAnnotations);
-                }
+        const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin");
+        const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion");
+        if (pdfDocId && pdfRegionId) {
+            DocServer.GetRefField(pdfDocId).then(pdfDoc => {
+                DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
+                    if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
+                        setTimeout(async () => {
+                            let targetAnnotations = await DocListCastAsync(Doc.fieldExtensionDoc(pdfDoc, "data").annotations);// bcz: NO... this assumes the pdf is using its 'data' field.  need to have the PDF's view handle updating its own annotations
+                            targetAnnotations && targetAnnotations.push(pdfRegion);
+                        });
 
-                let link = DocUtils.MakeLink(this.props.Document, region, doc);
-                if (link) {
-                    cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
-                    linkId = link[Id];
-                    let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(doc.title)));
-                    slice = new Slice(frag, slice.openStart, slice.openEnd);
-                    var tr = view.state.tr.replaceSelection(slice);
-                    view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
-                }
+                        let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
+                        if (link) {
+                            cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
+                            let linkId = link[Id];
+                            let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
+                            slice = new Slice(frag, slice.openStart, slice.openEnd);
+                            var tr = view.state.tr.replaceSelection(slice);
+                            view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
+                        }
+                    }
+                });
             });
-        });
+            return true;
+        }
+        return false;
 
-        return true;
 
         function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
             const nodes: Node[] = [];
             frag.forEach(node => nodes.push(marker(node)));
             return Fragment.fromArray(nodes);
         }
-        function addLinkMark(node: Node, title: string) {
+        function addLinkMark(node: Node, title: string, linkId: string) {
             if (!node.isText) {
-                const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title));
+                const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
                 return node.copy(content);
             }
             const marks = [...node.marks];
@@ -879,16 +866,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
                                 if (jumpToDoc) {
                                     if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-                                        DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+                                        DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
                                         return;
                                     }
                                 }
                                 if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
-                                    DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), undefined, targetContext);
+                                    DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), targetContext);
                                 } else if (jumpToDoc) {
-                                    DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
+                                    DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
                                 } else {
-                                    DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
+                                    DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
                                 }
                             }
                         });
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 9e9fe1324..fe4f75cad 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -299,7 +299,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
                 let rotation = NumCast(this.dataDoc.rotation) % 180;
                 let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
                 let aspect = realsize.height / realsize.width;
-                if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(NumCast(layoutdoc.height) - realsize.height) > 1 || Math.abs(NumCast(layoutdoc.width) - realsize.width) > 1)) {
+                if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(1 - NumCast(layoutdoc.nativeHeight) / NumCast(layoutdoc.nativeWidth) / (realsize.height / realsize.width)) > 0.1)) {
                     setTimeout(action(() => {
                         layoutdoc.height = layoutdoc[WidthSym]() * aspect;
                         layoutdoc.nativeHeight = realsize.height;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 617b580dd..88cd2cdc4 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -57,11 +57,8 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     }
     loaded = (nw: number, nh: number, np: number) => {
         this.dataDoc.numPages = np;
-        if (!this.Document.nativeWidth || !this.Document.nativeHeight || !this.Document.scrollHeight) {
-            let oldaspect = (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
-            this.Document.nativeWidth = nw * 96 / 72;
-            this.Document.nativeHeight = this.Document.nativeHeight ? nw * 96 / 72 * oldaspect : nh * 96 / 72;
-        }
+        this.Document.nativeWidth = nw * 96 / 72;
+        this.Document.nativeHeight = nh * 96 / 72;
         !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
     }
 
@@ -177,12 +174,14 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
 
         ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
     }
+    _initialScale: number | undefined;
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
         let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf;
-        if (!noPdf && (this.props.isSelected() || this.props.ScreenToLocalTransform().Scale < 2.5)) this._everActive = true;
-        return (!this._everActive ?
+        if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale;
+        if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
+        return (noPdf || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
             <div className="pdfBox-title-outer" >
                 <div className={classname} >
                     <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong>
@@ -203,11 +202,11 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                     setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
                     renderDepth={this.props.renderDepth} PanelHeight={this.props.PanelHeight} PanelWidth={this.props.PanelWidth}
                     Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
-                    addDocTab={this.props.addDocTab} GoToPage={this.gotoPage}
+                    addDocTab={this.props.addDocTab} GoToPage={this.gotoPage} focus={this.props.focus}
                     pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                     isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
-                    fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this.props.ScreenToLocalTransform().Scale < 2.5 ? true : false} />
+                    fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
                 {this.settingsPanel()}
             </div>);
     }
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index f44ca99b9..180ed9032 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -43,8 +43,8 @@ export class PresBox extends React.Component<FieldViewProps> {
                 value.forEach((item, i) => {
                     if (item instanceof Doc && item.type !== DocumentType.PRESELEMENT) {
                         let pinDoc = Docs.Create.PresElementBoxDocument({ backgroundColor: "transparent" });
-                        Doc.GetProto(pinDoc).target = item;
-                        Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.target instanceof Doc) && this.target.title.toString()');
+                        Doc.GetProto(pinDoc).presentationTargetDoc = item;
+                        Doc.GetProto(pinDoc).title = ComputedField.MakeFunction('(this.presentationTargetDoc instanceof Doc) && this.presentationTargetDoc.title.toString()');
                         value.splice(i, 1, pinDoc);
                     }
                 });
@@ -124,13 +124,13 @@ export class PresBox extends React.Component<FieldViewProps> {
         this.childDocs.forEach((doc, ind) => {
             //the order of cases is aligned based on priority
             if (doc.hideTillShownButton && ind <= index) {
-                (doc.target as Doc).opacity = 1;
+                (doc.presentationTargetDoc as Doc).opacity = 1;
             }
             if (doc.hideAfterButton && ind < index) {
-                (doc.target as Doc).opacity = 0;
+                (doc.presentationTargetDoc as Doc).opacity = 0;
             }
             if (doc.fadeButton && ind < index) {
-                (doc.target as Doc).opacity = 0.5;
+                (doc.presentationTargetDoc as Doc).opacity = 0.5;
             }
         });
     }
@@ -145,13 +145,13 @@ export class PresBox extends React.Component<FieldViewProps> {
             //the order of cases is aligned based on priority
 
             if (key.hideAfterButton && ind >= index) {
-                (key.target as Doc).opacity = 1;
+                (key.presentationTargetDoc as Doc).opacity = 1;
             }
             if (key.fadeButton && ind >= index) {
-                (key.target as Doc).opacity = 1;
+                (key.presentationTargetDoc as Doc).opacity = 1;
             }
             if (key.hideTillShownButton && ind > index) {
-                (key.target as Doc).opacity = 0;
+                (key.presentationTargetDoc as Doc).opacity = 0;
             }
         });
     }
@@ -162,7 +162,7 @@ export class PresBox extends React.Component<FieldViewProps> {
      * te option open, navigates to that element.
      */
     navigateToElement = async (curDoc: Doc, fromDocIndex: number) => {
-        let fromDoc = this.childDocs[fromDocIndex].target as Doc;
+        let fromDoc = this.childDocs[fromDocIndex].presentationTargetDoc as Doc;
         let docToJump = curDoc;
         let willZoom = false;
 
@@ -190,7 +190,7 @@ export class PresBox extends React.Component<FieldViewProps> {
         //docToJump stayed same meaning, it was not in the group or was the last element in the group
         if (docToJump === curDoc) {
             //checking if curDoc has navigation open
-            let target = await curDoc.target as Doc;
+            let target = await curDoc.presentationTargetDoc as Doc;
             if (curDoc.navButton) {
                 DocumentManager.Instance.jumpToDocument(target, false);
             } else if (curDoc.showButton) {
@@ -210,8 +210,8 @@ export class PresBox extends React.Component<FieldViewProps> {
         let curScale = DocumentManager.Instance.getScaleOfDocView(fromDoc);
 
         //awaiting jump so that new scale can be found, since jumping is async
-        await DocumentManager.Instance.jumpToDocument(await docToJump.target as Doc, willZoom);
-        let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.target as Doc);
+        await DocumentManager.Instance.jumpToDocument(await docToJump.presentationTargetDoc as Doc, willZoom);
+        let newScale = DocumentManager.Instance.getScaleOfDocView(await curDoc.presentationTargetDoc as Doc);
         curDoc.viewScale = newScale;
         //saving the scale that user was on
         if (curScale !== 1) {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index b7d9a1eab..573197117 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -129,7 +129,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                         width: 150, height: height / width * 150, title: "--snapshot" + NumCast(this.props.Document.curPage) + " image-"
                     });
                     this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.addDocument && this.props.ContainingCollectionView.props.addDocument(imageSummary, false);
-                    DocUtils.MakeLink(imageSummary, this.props.Document);
+                    DocUtils.MakeLink({doc:imageSummary}, {doc: this.props.Document}, "snapshot from " + this.props.Document.title, "video frame snapshot");
                 }
             });
         }
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 3ed85f6a5..f7f52b3ef 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -1,7 +1,7 @@
 import React = require("react");
 import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym, Opt } from "../../../new_fields/Doc";
+import { Doc, DocListCast, HeightSym, WidthSym, Opt, DocListCastAsync } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
@@ -14,6 +14,7 @@ interface IAnnotationProps {
     fieldExtensionDoc: Doc;
     addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
     pinToPres: (document: Doc) => void;
+    focus: (doc: Doc) => void;
 }
 
 export default class Annotation extends React.Component<IAnnotationProps> {
@@ -60,6 +61,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     }
 
     componentWillUnmount() {
+        this._brushDisposer && this._brushDisposer();
         this._reactionDisposer && this._reactionDisposer();
     }
 
@@ -94,14 +96,11 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
             PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
         }
         else if (e.button === 0) {
-            let targetDoc = await Cast(this.props.document.target, Doc);
-            if (targetDoc) {
-                let context = await Cast(targetDoc.targetContext, Doc);
-                if (context) {
-                    DocumentManager.Instance.jumpToDocument(targetDoc, false, false,
-                        ((doc) => this.props.addDocTab(targetDoc!, undefined, e.ctrlKey ? "onRight" : "inTab")),
-                        undefined, undefined);
-                }
+            let annoGroup = await Cast(this.props.document.group, Doc);
+            if (annoGroup) {
+                DocumentManager.Instance.FollowLink(annoGroup,
+                    (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "onRight" : "inTab"),
+                    false, false, undefined);
             }
         }
     }
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 2202351ee..e62542014 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -31,7 +31,7 @@ export default class PDFMenu extends React.Component {
     @observable public Pinned: boolean = false;
 
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction;
-    public Highlight: (d: Doc | undefined, color: string) => void = emptyFunction;
+    public Highlight: (color: string) => void = emptyFunction;
     public Delete: () => void = emptyFunction;
     public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
     public AddTag: (key: string, value: string) => boolean = returnFalse;
@@ -156,11 +156,11 @@ export default class PDFMenu extends React.Component {
     @action
     highlightClicked = (e: React.MouseEvent) => {
         if (!this.Pinned) {
-            this.Highlight(undefined, "#f4f442");
+            this.Highlight("#f4f442");
         }
         else {
             this.Highlighting = !this.Highlighting;
-            this.Highlight(undefined, "#f4f442");
+            this.Highlight("#f4f442");
         }
     }
 
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 33226eac4..20dfc4d8c 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -10,7 +10,6 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import smoothScroll, { Utils, emptyFunction, returnOne } from "../../../Utils";
-import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
 import { CompiledScript, CompileScript } from "../../util/Scripting";
@@ -44,6 +43,7 @@ interface IViewerProps {
     select: (isCtrlPressed: boolean) => void;
     startupLive: boolean;
     renderDepth: number;
+    focus: (doc: Doc) => void;
     isSelected: () => boolean;
     loaded: (nw: number, nh: number, np: number) => void;
     active: () => boolean;
@@ -108,7 +108,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
         // file address of the pdf
         this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${NumCast(this.props.Document.curPage, 1)}.PNG`)));
         runInAction(() => this._showWaiting = this._showCover = true);
-        this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1 && this.setupPdfJsViewer(), { fireImmediately: this.props.startupLive });
+        this.props.startupLive && this.setupPdfJsViewer();
+        this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => (this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), { fireImmediately: true });
         this._reactionDisposer = reaction(
             () => this.props.Document.scrollY,
             (scrollY) => {
@@ -136,19 +137,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
         if (this.props.active() && e.clipboardData) {
             e.clipboardData.setData("text/plain", this._selectionText);
             e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
-            e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument(undefined, "#0390fc")[Id]);
+            e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument("#0390fc")[Id]);
             e.preventDefault();
         }
     }
 
-    paste = (e: ClipboardEvent) => {
-        if (e.clipboardData && e.clipboardData.getData("dash/pdfOrigin") === this.props.Document[Id]) {
-            let linkDocId = e.clipboardData.getData("dash/linkDoc");
-            linkDocId && DocServer.GetRefField(linkDocId).then(async (link) =>
-                (link instanceof Doc) && (Doc.GetProto(link).anchor2 = this.makeAnnotationDocument(await Cast(Doc.GetProto(link), Doc), "#0390fc", false)));
-        }
-    }
-
     setSelectionText = (text: string) => this._selectionText = text;
 
     @action
@@ -194,13 +187,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
             { fireImmediately: true }
         );
 
-        document.removeEventListener("copy", this.copy);
-        document.addEventListener("copy", this.copy);
-        document.addEventListener("pagesinit", action(() => {
-            this.pdfViewer.currentScaleValue = this._zoomed = 1;
-            this.gotoPage(NumCast(this.props.Document.curPage, 1));
-        }));
-        document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
         this.createPdfViewer();
     }
 
@@ -212,6 +198,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
             }
             return;
         }
+        document.removeEventListener("copy", this.copy);
+        document.addEventListener("copy", this.copy);
+        document.addEventListener("pagesinit", action(() => {
+            this.pdfViewer.currentScaleValue = this._zoomed = 1;
+            this.gotoPage(NumCast(this.props.Document.curPage, 1));
+        }));
+        document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
         var pdfLinkService = new PDFJSViewer.PDFLinkService();
         let pdfFindController = new PDFJSViewer.PDFFindController({
             linkService: pdfLinkService,
@@ -229,19 +222,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
     }
 
     @action
-    makeAnnotationDocument = (sourceDoc: Doc | undefined, color: string, createLink: boolean = true): Doc => {
+    makeAnnotationDocument = (color: string): Doc => {
         let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
         let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
         let annoDocs: Doc[] = [];
         let minY = Number.MAX_VALUE;
-        if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1 && !createLink) {
+        if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1) {
             let anno = this._savedAnnotations.values()[0][0];
             let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) });
             if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
             if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
             if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
             if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
-            annoDoc.target = sourceDoc;
             annoDoc.group = mainAnnoDoc;
             annoDoc.color = color;
             annoDoc.type = AnnotationTypes.Region;
@@ -258,7 +250,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
                 if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
                 if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
-                annoDoc.target = sourceDoc;
                 annoDoc.group = mainAnnoDoc;
                 annoDoc.color = color;
                 annoDoc.type = AnnotationTypes.Region;
@@ -272,9 +263,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
         mainAnnoDocProto.title = "Annotation on " + StrCast(this.props.Document.title);
         mainAnnoDocProto.annotationOn = this.props.Document;
-        if (sourceDoc && createLink) {
-            DocUtils.MakeLink(sourceDoc, mainAnnoDocProto, undefined, `Annotation from ${StrCast(this.props.Document.title)}`);
-        }
         this._savedAnnotations.clear();
         this.Index = -1;
         return mainAnnoDoc;
@@ -529,7 +517,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
 
         if (PDFMenu.Instance.Highlighting) {
-            this.highlight(undefined, "goldenrod");
+            this.highlight("goldenrod");
         }
         else {
             PDFMenu.Instance.StartDrag = this.startDrag;
@@ -540,9 +528,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
     }
 
     @action
-    highlight = (targetDoc: Doc | undefined, color: string) => {
+    highlight = (color: string) => {
         // creates annotation documents for current highlights
-        let annotationDoc = this.makeAnnotationDocument(targetDoc, color, false);
+        let annotationDoc = this.makeAnnotationDocument(color);
         Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
         return annotationDoc;
     }
@@ -555,20 +543,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
     startDrag = (e: PointerEvent, ele: HTMLElement): void => {
         e.preventDefault();
         e.stopPropagation();
-        let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "New Annotation" });
-        targetDoc.targetPage = this.getPageFromScroll(this._marqueeY);
-        let annotationDoc = this.highlight(undefined, "red");
-        annotationDoc.linkedToDoc = false;
+        let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
+        let annotationDoc = this.highlight("red");
         let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
         DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
             handlers: {
-                dragComplete: () => {
-                    if (!annotationDoc.linkedToDoc) {
-                        let annotations = DocListCast(annotationDoc.annotations);
-                        annotations && annotations.forEach(anno => anno.target = targetDoc);
-                        DocUtils.MakeLink(annotationDoc, targetDoc, dragData.targetContext, `Annotation from ${StrCast(this.props.Document.title)}`);
-                    }
-                }
+                dragComplete: () => !(dragData as any).linkedToDoc &&
+                    DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF")
+
             },
             hideSource: false
         });
@@ -602,6 +584,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action.bound
     removeDocument(doc: Doc): boolean {
+        Doc.GetProto(doc).annotationOn = undefined;
         //TODO This won't create the field if it doesn't already exist
         let targetDataDoc = this.props.fieldExtensionDoc;
         let targetField = this.props.fieldExt;
@@ -634,10 +617,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     getCoverImage = () => {
         if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
-            setTimeout(() => {
+            setTimeout((() => {
                 this.props.Document.height = this.props.Document[WidthSym]() * this._coverPath.height / this._coverPath.width;
                 this.props.Document.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
-            }, 0);
+            }).bind(this), 0);
         }
         let nativeWidth = NumCast(this.props.Document.nativeWidth);
         let nativeHeight = NumCast(this.props.Document.nativeHeight);
@@ -659,7 +642,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @computed get annotationLayer() {
         return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
             {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
-                <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)}
+                <Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)}
         </div>;
     }
     @computed get pdfViewerDiv() {
@@ -686,7 +669,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     setPreviewCursor={this.setPreviewCursor}
                     PanelHeight={() => NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))}
                     PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.props.Document.nativeWidth)}
-                    focus={emptyFunction}
+                    VisibleHeight={() => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96}
+                    focus={this.props.focus}
                     isSelected={this.props.isSelected}
                     select={emptyFunction}
                     active={this.active}
@@ -694,7 +678,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     whenActiveChanged={this.whenActiveChanged}
                     removeDocument={this.removeDocument}
                     moveDocument={this.moveDocument}
-                    addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
+                    addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.GetProto(doc).annotationOn = this.props.Document; Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
                     CollectionView={this.props.ContainingCollectionView}
                     ScreenToLocalTransform={this.scrollXf}
                     ruleProvider={undefined}
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index de3242d32..daf000dc7 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -63,13 +63,11 @@ export class PresElementBox extends React.Component<FieldViewProps> {
         this.hideTillShownButton = !this.hideTillShownButton;
         if (!this.hideTillShownButton) {
             if (this.myIndex >= this.currentIndex) {
-                (this.props.Document.target as Doc).opacity = 1;
+                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
             }
         } else {
-            if (this.presentationDoc.presStatus) {
-                if (this.myIndex > this.currentIndex) {
-                    (this.props.Document.target as Doc).opacity = 0;
-                }
+            if (this.presentationDoc.presStatus && this.myIndex > this.currentIndex) {
+                (this.props.Document.presentationTargetDoc as Doc).opacity = 0;
             }
         }
     }
@@ -85,14 +83,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
         this.hideAfterButton = !this.hideAfterButton;
         if (!this.hideAfterButton) {
             if (this.myIndex <= this.currentIndex) {
-                (this.props.Document.target as Doc).opacity = 1;
+                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
             }
         } else {
             if (this.fadeButton) this.fadeButton = false;
-            if (this.presentationDoc.presStatus) {
-                if (this.myIndex < this.currentIndex) {
-                    (this.props.Document.target as Doc).opacity = 0;
-                }
+            if (this.presentationDoc.presStatus && this.myIndex < this.currentIndex) {
+                (this.props.Document.presentationTargetDoc as Doc).opacity = 0;
             }
         }
     }
@@ -108,14 +104,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
         this.fadeButton = !this.fadeButton;
         if (!this.fadeButton) {
             if (this.myIndex <= this.currentIndex) {
-                (this.props.Document.target as Doc).opacity = 1;
+                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
             }
         } else {
             this.hideAfterButton = false;
-            if (this.presentationDoc.presStatus) {
-                if (this.myIndex < this.currentIndex) {
-                    (this.props.Document.target as Doc).opacity = 0.5;
-                }
+            if (this.presentationDoc.presStatus && (this.myIndex < this.currentIndex)) {
+                (this.props.Document.presentationTargetDoc as Doc).opacity = 0.5;
             }
         }
     }
@@ -162,7 +156,7 @@ export class PresElementBox extends React.Component<FieldViewProps> {
      * presentation element.
      */
     renderEmbeddedInline = () => {
-        if (!this.embedOpen || !(this.props.Document.target instanceof Doc)) {
+        if (!this.embedOpen || !(this.props.Document.presentationTargetDoc instanceof Doc)) {
             return (null);
         }
 
@@ -175,8 +169,8 @@ export class PresElementBox extends React.Component<FieldViewProps> {
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <CollectionSchemaPreview
-                    fitToBox={StrCast(this.props.Document.target.type).indexOf(DocumentType.COL) !== -1}
-                    Document={this.props.Document.target}
+                    fitToBox={StrCast(this.props.Document.presentationTargetDoc.type).indexOf(DocumentType.COL) !== -1}
+                    Document={this.props.Document.presentationTargetDoc}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
                     ruleProvider={undefined}
@@ -205,7 +199,7 @@ export class PresElementBox extends React.Component<FieldViewProps> {
         let pbi = "presElementBox-interaction";
         return (
             <div className={className} key={p.Document[Id] + this.myIndex}
-                style={{ outlineWidth: Doc.IsBrushed(p.Document.target as Doc) ? `1px` : "0px", }}
+                style={{ outlineWidth: Doc.IsBrushed(p.Document.presentationTargetDoc as Doc) ? `1px` : "0px", }}
                 onClick={e => { p.focus(p.Document); e.stopPropagation(); }}>
                 {treecontainer ? (null) : <>
                     <strong className="presElementBox-name">
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index aeffc81c4..58304cebb 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -665,10 +665,12 @@ export namespace Doc {
     export function BrushDoc(doc: Doc) {
         brushManager.BrushedDoc.set(doc, true);
         brushManager.BrushedDoc.set(Doc.GetDataDoc(doc), true);
+        return doc;
     }
     export function UnBrushDoc(doc: Doc) {
         brushManager.BrushedDoc.delete(doc);
         brushManager.BrushedDoc.delete(Doc.GetDataDoc(doc));
+        return doc;
     }
 
     export class HighlightBrush {
-- 
cgit v1.2.3-70-g09d2


From 456e9120857f20fb609ab13bb07cbd8a2d2f850b Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 3 Oct 2019 00:25:44 -0400
Subject: cleaned up link following code.  changed opening in place behavior to
 not open if view already exists.  fixed formattedText box scrolling.  fixed
 clicking on image in text box. more...

---
 src/client/util/DocumentManager.ts          |  49 +++++----
 src/client/util/RichTextSchema.tsx          |  26 +----
 src/client/util/SearchUtil.ts               |   1 -
 src/client/views/MetadataEntryMenu.tsx      |   2 +-
 src/client/views/linking/LinkFollowBox.tsx  |  82 ++++-----------
 src/client/views/nodes/DocumentView.scss    |   1 +
 src/client/views/nodes/DocumentView.tsx     |  10 +-
 src/client/views/nodes/FormattedTextBox.tsx | 152 ++++++++++++----------------
 src/client/views/nodes/ImageBox.tsx         |   2 +-
 src/client/views/pdf/Annotation.tsx         |   2 +-
 src/new_fields/Doc.ts                       |  17 +++-
 11 files changed, 148 insertions(+), 196 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4ebcdf83c..305a77b14 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -12,6 +12,7 @@ import { undoBatch, UndoManager } from './UndoManager';
 import { Scripting } from './Scripting';
 import { List } from '../../new_fields/List';
 import { SelectionManager } from './SelectionManager';
+import { notDeepEqual } from 'assert';
 
 
 export class DocumentManager {
@@ -131,12 +132,12 @@ export class DocumentManager {
         return pairs;
     }
 
-    public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, closeContextIfNotFound: boolean = false): Promise<void> => {
+    public jumpToDocument = async (targetDoc: Doc, willZoom: boolean, dockFunc?: (doc: Doc) => void, docContext?: Doc, linkId?: string, closeContextIfNotFound: boolean = false): Promise<void> => {
         const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
         const annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
         if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
             annotatedDoc && docView.props.focus(annotatedDoc, false);
-            docView.props.focus(targetDoc, willZoom);
+            docView.props.focus(docView.props.Document, willZoom);
         } else {
             const contextDocs = docContext ? await DocListCastAsync(docContext.data) : undefined;
             const contextDoc = contextDocs && contextDocs.find(doc => Doc.AreProtosEqual(doc, targetDoc)) ? docContext : undefined;
@@ -160,38 +161,52 @@ export class DocumentManager {
                             if (closeContextIfNotFound && targetDocContextView.props.removeDocument) targetDocContextView.props.removeDocument(targetDocContextView.props.Document);
                             (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
                         }
+                        const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+                        finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
+                        finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
                     }, 0);
                 } else {  // there's no context view so we need to create one first and try again
                     targetDocContext.scrollY = 0;
                     (dockFunc || CollectionDockingView.AddRightSplit)(targetDocContext, undefined);
                     setTimeout(() => {
                         const foundTargetDocContextView = DocumentManager.Instance.getDocumentView(targetDocContext);
-                        if (foundTargetDocContextView) { // we should always find a target context here....
-                            this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, true); // so call jump to doc again and if the doc isn't found, it will be created.
+                        if (foundTargetDocContextView) { // we might be lucky and the context loads right away
+                            this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, linkId, true); // so call jump to doc again and if the doc isn't found, it will be created.
+                        } else {
+                            setTimeout(() => { // if not, wait a bit to see if the context can be loaded (e.g., a PDF).
+                                const foundTargetDocContextView = DocumentManager.Instance.getDocumentView(targetDocContext);
+                                if (foundTargetDocContextView) { // now we should always find a target context here....
+                                    this.jumpToDocument(targetDoc, willZoom, dockFunc, undefined, linkId, true); // so call jump to doc again and if the doc isn't found, it will be created.
+                                }
+                            }, 2000)
                         }
-                    }, 2000); // the long timeout gives the context view a chance to create its children.  think pdf's which need to be activated to render their annotations.
+                    }, 0);
                 }
             }
         }
+        const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+        finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
+        finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
     }
 
-    public async FollowLink(doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
-        let linkDocs = LinkManager.Instance.getAllRelatedLinks(doc);
+    public async FollowLink(link: Doc | undefined, doc: Doc, focus: (doc: Doc, maxLocation: string) => void, zoom: boolean = false, reverse: boolean = false, currentContext?: Doc) {
+        const linkDocs = link ? [link] : LinkManager.Instance.getAllRelatedLinks(doc);
         SelectionManager.DeselectAll();
-        let firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) && !linkDoc.anchor1anchored);
-        let secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) && !linkDoc.anchor2anchored);
+        const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc) && !linkDoc.anchor1anchored);
+        const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc) && !linkDoc.anchor2anchored);
         const firstDocWithoutView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
         const secondDocWithoutView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
-        let first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
-        let second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
-        let linkFollowDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : second.length ? [second[0].anchor1 as Doc, second[0].anchor2 as Doc] : undefined;
-        let linkFollowDocContexts = first.length ? [await (first[0].targetContext) as Doc, await (first[0].sourceContext) as Doc] : second.length ? [await (second[0].sourceContext) as Doc, await (second[0].targetContext) as Doc] : [undefined, undefined];
-        if (linkFollowDocs && !linkFollowDocs.some(l => l instanceof Promise)) {
-            let maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
-            let targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
+        const first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
+        const second = secondDocWithoutView ? [secondDocWithoutView] : secondDocs;
+        const linkDoc = first.length ? first[0] : second.length ? second[0] : undefined;
+        const linkFollowDocs = first.length ? [await first[0].anchor2 as Doc, await first[0].anchor1 as Doc] : second.length ? [await second[0].anchor1 as Doc, await second[0].anchor2 as Doc] : undefined;
+        const linkFollowDocContexts = first.length ? [await first[0].targetContext as Doc, await first[0].sourceContext as Doc] : second.length ? [await second[0].sourceContext as Doc, await second[0].targetContext as Doc] : [undefined, undefined];
+        if (linkFollowDocs && linkDoc) {
+            const maxLocation = StrCast(linkFollowDocs[0].maximizeLocation, "inTab");
+            const targetContext = !Doc.AreProtosEqual(linkFollowDocContexts[reverse ? 1 : 0], currentContext) ? linkFollowDocContexts[reverse ? 1 : 0] : undefined;
             DocumentManager.Instance.jumpToDocument(linkFollowDocs[reverse ? 1 : 0], zoom,
                 // open up target if it's not already in view ... by zooming into the button document first and setting flag to reset zoom afterwards
-                (doc: Doc) => focus(doc, maxLocation), targetContext);
+                (doc: Doc) => focus(doc, maxLocation), targetContext, linkDoc[Id]);
         }
     }
 
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 49bd93942..066266873 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -13,6 +13,7 @@ import { Cast, NumCast } from "../../new_fields/Types";
 import { DocumentManager } from "./DocumentManager";
 import ParagraphNodeSpec from "./ParagraphNodeSpec";
 import { times } from "async";
+import { LinkManager } from "./LinkManager";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -619,27 +620,10 @@ export class ImageResizeView {
             if (!view.isOverlay || e.ctrlKey) {
                 e.preventDefault();
                 e.stopPropagation();
-                DocServer.GetRefField(node.attrs.docid).then(async linkDoc => {
-                    const location = node.attrs.location;
-                    if (linkDoc instanceof Doc) {
-                        let proto = Doc.GetProto(linkDoc);
-                        let targetContext = await Cast(proto.targetContext, Doc);
-                        let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
-                        if (jumpToDoc) {
-                            if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-                                DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
-                                return;
-                            }
-                        }
-                        if (targetContext) {
-                            DocumentManager.Instance.jumpToDocument(targetContext, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
-                        } else if (jumpToDoc) {
-                            DocumentManager.Instance.jumpToDocument(jumpToDoc, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
-                        } else {
-                            DocumentManager.Instance.jumpToDocument(linkDoc, e.ctrlKey, document => addDocTab(document, undefined, location ? location : "inTab"));
-                        }
-                    }
-                });
+                DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
+                    (linkDoc instanceof Doc) &&
+                    DocumentManager.Instance.FollowLink(linkDoc, (view.state.schema as any).Document,
+                        document => addDocTab(document, undefined, node.attrs.location ? node.attrs.location : "inTab"), false));
             }
         };
         this._handle.onpointerdown = function (e: any) {
diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index d8b9dbec6..e37f1f90d 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -3,7 +3,6 @@ import { DocServer } from '../DocServer';
 import { Doc } from '../../new_fields/Doc';
 import { Id } from '../../new_fields/FieldSymbols';
 import { Utils } from '../../Utils';
-import { ResultParameters } from '../northstar/model/idea/idea';
 import { DocumentType } from '../documents/DocumentTypes';
 
 export namespace SearchUtil {
diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx
index f1b101b8e..41453f8b2 100644
--- a/src/client/views/MetadataEntryMenu.tsx
+++ b/src/client/views/MetadataEntryMenu.tsx
@@ -3,7 +3,7 @@ import "./MetadataEntryMenu.scss";
 import { observer } from 'mobx-react';
 import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx';
 import { KeyValueBox } from './nodes/KeyValueBox';
-import { Doc, Field, DocListCast, DocListCastAsync } from '../../new_fields/Doc';
+import { Doc, Field, DocListCastAsync } from '../../new_fields/Doc';
 import * as Autosuggest from 'react-autosuggest';
 import { undoBatch } from '../util/UndoManager';
 import { emptyFunction } from '../../Utils';
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 53b720a9e..2bff3ded4 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -152,21 +152,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         this.resetPan();
     }
 
-    unhighlight = () => {
-        Doc.UnhighlightAll();
-        document.removeEventListener("pointerdown", this.unhighlight);
-    }
-
-    @action
-    highlightDoc = () => {
-        if (LinkFollowBox.destinationDoc) {
-            document.removeEventListener("pointerdown", this.unhighlight);
-            Doc.HighlightDoc(LinkFollowBox.destinationDoc);
-            window.setTimeout(() => {
-                document.addEventListener("pointerdown", this.unhighlight);
-            }, 10000);
-        }
-    }
+    highlightDoc = () => LinkFollowBox.destinationDoc && Doc.linkFollowHighlight(LinkFollowBox.destinationDoc);
 
     @undoBatch
     openFullScreen = () => {
@@ -235,44 +221,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     @undoBatch
     jumpToLink = async (options: { shouldZoom: boolean }) => {
-        if (LinkFollowBox.destinationDoc && LinkFollowBox.linkDoc) {
-            let jumpToDoc: Doc = LinkFollowBox.destinationDoc;
-            let pdfDoc = FieldValue(Cast(LinkFollowBox.destinationDoc, Doc));
-            if (pdfDoc) {
-                jumpToDoc = pdfDoc;
-            }
-            let proto = Doc.GetProto(LinkFollowBox.linkDoc);
-            let targetContext = await Cast(proto.targetContext, Doc);
-            let sourceContext = await Cast(proto.sourceContext, Doc);
-            let guid = StrCast(LinkFollowBox.linkDoc[Id]);
-            const shouldZoom = options ? options.shouldZoom : false;
-
-            let dockingFunc = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
-
-            if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor2 && targetContext) {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, async document => dockingFunc(document), targetContext);
-            }
-            else if (LinkFollowBox.destinationDoc === LinkFollowBox.linkDoc.anchor1 && sourceContext) {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, document => dockingFunc(sourceContext!));
-                if (LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc) {
-                    if (guid) {
-                        let views = DocumentManager.Instance.getDocumentViews(jumpToDoc);
-                        views.length && (views[0].props.Document.scrollToLinkID = guid);
-                    } else {
-                        jumpToDoc.linkHref = Utils.prepend("/doc/" + StrCast(LinkFollowBox.linkDoc[Id]));
-                    }
-                }
-            }
-            else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom);
-
-            }
-            else {
-                DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, dockingFunc);
-            }
+        if (LinkFollowBox.sourceDoc && LinkFollowBox.linkDoc) {
+            let focus = (document: Doc) => { (LinkFollowBox._addDocTab || this.props.addDocTab)(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
+            //let focus = (doc: Doc, maxLocation: string) => this.props.focus(docthis.props.focus(LinkFollowBox.destinationDoc, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation));
 
-            this.highlightDoc();
-            SelectionManager.DeselectAll();
+            DocumentManager.Instance.FollowLink(LinkFollowBox.linkDoc, LinkFollowBox.sourceDoc, focus, options && options.shouldZoom, false, undefined);
         }
     }
 
@@ -310,20 +263,23 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     openLinkInPlace = (options: { shouldZoom: boolean }) => {
 
         if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
-            let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
-            let y = NumCast(LinkFollowBox.sourceDoc.y);
-            let x = NumCast(LinkFollowBox.sourceDoc.x);
+            if (this.sourceView && this.sourceView.props.addDocument) {
+                let destViews = DocumentManager.Instance.getDocumentViews(LinkFollowBox.destinationDoc);
+                if (!destViews.find(dv => dv.props.ContainingCollectionView === this.sourceView!.props.ContainingCollectionView)) {
+                    let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
+                    let y = NumCast(LinkFollowBox.sourceDoc.y);
+                    let x = NumCast(LinkFollowBox.sourceDoc.x);
 
-            let width = NumCast(LinkFollowBox.sourceDoc.width);
-            let height = NumCast(LinkFollowBox.sourceDoc.height);
+                    let width = NumCast(LinkFollowBox.sourceDoc.width);
+                    let height = NumCast(LinkFollowBox.sourceDoc.height);
 
-            alias.x = x + width + 30;
-            alias.y = y;
-            alias.width = width;
-            alias.height = height;
+                    alias.x = x + width + 30;
+                    alias.y = y;
+                    alias.width = width;
+                    alias.height = height;
 
-            if (this.sourceView && this.sourceView.props.addDocument) {
-                this.sourceView.props.addDocument(alias, false);
+                    this.sourceView.props.addDocument(alias, false);
+                }
             }
 
             this.jumpToLink({ shouldZoom: options.shouldZoom });
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 4ea200e6d..b3e7898c1 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -5,6 +5,7 @@
   top: 0;
   left:0;
   border-radius: inherit;
+  transition : outline .3s linear;
                     
  // background: $light-color; //overflow: hidden;
   transform-origin: left top;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 3273abc1d..67c3fe6e7 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
             }
         }
         else if (linkDocs.length) {
-            DocumentManager.Instance.FollowLink(this.props.Document,
+            DocumentManager.Instance.FollowLink(undefined, this.props.Document,
                 (doc: Doc, maxLocation: string) => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
                 ctrlKey, altKey, this.props.ContainingCollectionDoc);
         }
@@ -658,6 +658,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         let animheight = animDims ? animDims[1] : nativeHeight;
         let animwidth = animDims ? animDims[0] : nativeWidth;
 
+        const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
+        const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"];
         return (
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
@@ -665,10 +667,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
                     pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
                     color: StrCast(this.Document.color),
-                    outlineColor: ["transparent", "maroon", "maroon", "yellow"][fullDegree],
-                    outlineStyle: ["none", "dashed", "solid", "solid"][fullDegree],
-                    outlineWidth: fullDegree && !borderRounding ? `${localScale}px` : "0px",
-                    border: fullDegree && borderRounding ? `${["none", "dashed", "solid", "solid"][fullDegree]} ${["transparent", "maroon", "maroon", "yellow"][fullDegree]} ${localScale}px` : undefined,
+                    outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
+                    border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
                     background: backgroundColor,
                     width: animwidth,
                     height: animheight,
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 749886d9a..9347868b3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,47 +1,46 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import _ from "lodash";
 import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
+import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from "prosemirror-keymap";
-import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
+import { Fragment, Mark, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model";
+import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
+import { ReplaceStep } from 'prosemirror-transform';
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCast, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc";
 import { Copy, Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
 import { RichTextField } from "../../../new_fields/RichTextField";
-import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types";
+import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Utils, numberRange, timenow } from '../../../Utils';
+import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
+import { numberRange, timenow, Utils } from '../../../Utils';
+import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from "../../util/DragManager";
 import buildKeymap from "../../util/ProsemirrorExampleTransfer";
 import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
+import { FootnoteView, ImageResizeView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema";
 import { SelectionManager } from "../../util/SelectionManager";
 import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
 import { TooltipTextMenu } from "../../util/TooltipTextMenu";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
 import { DocComponent } from "../DocComponent";
+import { DocumentButtonBar } from '../DocumentButtonBar';
+import { DocumentDecorations } from '../DocumentDecorations';
 import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from "./FieldView";
 import "./FormattedTextBox.scss";
+import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
 import React = require("react");
-import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
-import { DocumentDecorations } from '../DocumentDecorations';
-import { DictationManager } from '../../util/DictationManager';
-import { ReplaceStep } from 'prosemirror-transform';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { RichTextUtils } from '../../../new_fields/RichTextUtils';
-import _ from "lodash";
-import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
-import { inputRules } from 'prosemirror-inputrules';
-import { DocumentButtonBar } from '../DocumentButtonBar';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -142,51 +141,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
         }
         FormattedTextBox.Instance = this;
-        this._scrollToRegionReactionDisposer = reaction(
-            () => StrCast(this.props.Document.scrollToLinkID),
-            async (scrollToLinkID) => {
-                let findLinkFrag = (frag: Fragment, editor: EditorView) => {
-                    const nodes: Node[] = [];
-                    frag.forEach((node, index) => {
-                        let examinedNode = findLinkNode(node, editor);
-                        if (examinedNode && examinedNode.textContent) {
-                            nodes.push(examinedNode);
-                            start += index;
-                        }
-                    });
-                    return { frag: Fragment.fromArray(nodes), start: start };
-                };
-                let findLinkNode = (node: Node, editor: EditorView) => {
-                    if (!node.isText) {
-                        const content = findLinkFrag(node.content, editor);
-                        return node.copy(content.frag);
-                    }
-                    const marks = [...node.marks];
-                    const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
-                    return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
-                };
-
-                let start = -1;
-                if (this._editorView && scrollToLinkID) {
-                    let editor = this._editorView;
-                    let ret = findLinkFrag(editor.state.doc.content, editor);
-
-                    if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
-                        let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
-                        if (ret.frag.firstChild) {
-                            selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
-                        }
-                        editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
-                        const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
-                        setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
-                        setTimeout(() => this.unhighlightSearchTerms(), 2000);
-                    }
-                    this.props.Document.scrollToLinkID = undefined;
-                }
-
-            },
-            { fireImmediately: true }
-        );
     }
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
@@ -341,8 +295,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let url = de.data.urlField.url.href;
             let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
-            this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
-            DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+            let link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+            link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: link[Id] })));
             this.tryUpdateHeight();
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
@@ -424,6 +378,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     _keymap: any = undefined;
     @computed get config() {
         this._keymap = buildKeymap(schema);
+        (schema as any).Document = this.props.Document;
         return {
             schema,
             plugins: this.props.isOverlay ? [
@@ -563,6 +518,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }, 0);
             }), { fireImmediately: true }
         );
+        this._scrollToRegionReactionDisposer = reaction(
+            () => StrCast(this.props.Document.scrollToLinkID),
+            async (scrollToLinkID) => {
+                let findLinkFrag = (frag: Fragment, editor: EditorView) => {
+                    const nodes: Node[] = [];
+                    frag.forEach((node, index) => {
+                        let examinedNode = findLinkNode(node, editor);
+                        if (examinedNode && examinedNode.textContent) {
+                            nodes.push(examinedNode);
+                            start += index;
+                        }
+                    });
+                    return { frag: Fragment.fromArray(nodes), start: start };
+                };
+                let findLinkNode = (node: Node, editor: EditorView) => {
+                    if (!node.isText) {
+                        const content = findLinkFrag(node.content, editor);
+                        return node.copy(content.frag);
+                    }
+                    const marks = [...node.marks];
+                    const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+                    return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+                };
+
+                let start = -1;
+                if (this._editorView && scrollToLinkID) {
+                    let editor = this._editorView;
+                    let ret = findLinkFrag(editor.state.doc.content, editor);
+
+                    if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
+                        let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
+                        if (ret.frag.firstChild) {
+                            selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+                        }
+                        editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+                        const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+                        setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+                        setTimeout(() => this.unhighlightSearchTerms(), 2000);
+                    }
+                    this.props.Document.scrollToLinkID = undefined;
+                }
+
+            },
+            { fireImmediately: true }
+        );
 
         setTimeout(() => this.tryUpdateHeight(), 0);
     }
@@ -858,27 +858,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 if (href.indexOf(Utils.prepend("/doc/")) === 0) {
                     this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     if (this._linkClicked) {
-                        DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
-                            if (linkDoc instanceof Doc) {
-                                let proto = Doc.GetProto(linkDoc);
-                                let targetContext = await Cast(proto.targetContext, Doc);
-                                let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
-
-                                if (jumpToDoc) {
-                                    if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-                                        DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
-                                        return;
-                                    }
-                                }
-                                if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
-                                    DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), targetContext);
-                                } else if (jumpToDoc) {
-                                    DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
-                                } else {
-                                    DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
-                                }
-                            }
-                        });
+                        DocServer.GetRefField(this._linkClicked).then(async linkDoc =>
+                            (linkDoc instanceof Doc) &&
+                            DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false));
                         e.stopPropagation();
                         e.preventDefault();
                     }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index fe4f75cad..a198a0764 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -299,7 +299,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
                 let rotation = NumCast(this.dataDoc.rotation) % 180;
                 let realsize = rotation === 90 || rotation === 270 ? { height: size.width, width: size.height } : size;
                 let aspect = realsize.height / realsize.width;
-                if (layoutdoc.nativeHeight !== 0 && layoutdoc.nativeWidth !== 0 && (Math.abs(1 - NumCast(layoutdoc.nativeHeight) / NumCast(layoutdoc.nativeWidth) / (realsize.height / realsize.width)) > 0.1)) {
+                if (layoutdoc.width && (Math.abs(1 - NumCast(layoutdoc.height) / NumCast(layoutdoc.width) / (realsize.height / realsize.width)) > 0.1)) {
                     setTimeout(action(() => {
                         layoutdoc.height = layoutdoc[WidthSym]() * aspect;
                         layoutdoc.nativeHeight = realsize.height;
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index f7f52b3ef..98e04d93e 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -98,7 +98,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
         else if (e.button === 0) {
             let annoGroup = await Cast(this.props.document.group, Doc);
             if (annoGroup) {
-                DocumentManager.Instance.FollowLink(annoGroup,
+                DocumentManager.Instance.FollowLink(undefined, annoGroup,
                     (doc: Doc, maxLocation: string) => this.props.addDocTab(doc, undefined, e.ctrlKey ? "onRight" : "inTab"),
                     false, false, undefined);
             }
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 58304cebb..6acc6e1ca 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -637,7 +637,7 @@ export namespace Doc {
 
     export function isBrushedHighlightedDegree(doc: Doc) {
         if (Doc.IsHighlighted(doc)) {
-            return 3;
+            return 6;
         }
         else {
             return Doc.IsBrushedDegree(doc);
@@ -673,6 +673,21 @@ export namespace Doc {
         return doc;
     }
 
+    export function linkFollowUnhighlight() {
+        Doc.UnhighlightAll();
+        document.removeEventListener("pointerdown", linkFollowUnhighlight);
+    }
+
+    let dt = 0;
+    export function linkFollowHighlight(destDoc: Doc) {
+        linkFollowUnhighlight();
+        Doc.HighlightDoc(destDoc);
+        document.removeEventListener("pointerdown", linkFollowUnhighlight);
+        document.addEventListener("pointerdown", linkFollowUnhighlight);
+        let x = dt = Date.now();
+        window.setTimeout(() => dt == x && linkFollowUnhighlight(), 5000);
+    }
+
     export class HighlightBrush {
         @observable HighlightedDoc: Map<Doc, boolean> = new Map();
     }
-- 
cgit v1.2.3-70-g09d2


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/util/RichTextSchema.tsx                 | 143 ++++++++++++++++++++-
 src/client/views/DocumentButtonBar.tsx             |   2 +-
 src/client/views/DocumentDecorations.tsx           |  36 ------
 .../views/collections/CollectionDockingView.tsx    |   1 -
 src/client/views/nodes/FormattedTextBox.tsx        |  24 +++-
 5 files changed, 158 insertions(+), 48 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 066266873..7be773475 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -9,11 +9,16 @@ import { EditorView } from "prosemirror-view";
 import { Doc } from "../../new_fields/Doc";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 import { DocServer } from "../DocServer";
-import { Cast, NumCast } from "../../new_fields/Types";
 import { DocumentManager } from "./DocumentManager";
 import ParagraphNodeSpec from "./ParagraphNodeSpec";
-import { times } from "async";
-import { LinkManager } from "./LinkManager";
+import React = require("react");
+import { action, Lambda, observable, reaction, computed, runInAction, trace } from "mobx";
+import { observer } from "mobx-react";
+import * as ReactDOM from 'react-dom';
+import { DocumentView } from "../views/nodes/DocumentView";
+import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils";
+import { Transform } from "./Transform";
+import { NumCast } from "../../new_fields/Types";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -158,6 +163,35 @@ export const nodes: { [index: string]: NodeSpec } = {
         }
     },
 
+    dashDoc: {
+        inline: true,
+        attrs: {
+            width: { default: 200 },
+            height: { default: 100 },
+            title: { default: null },
+            float: { default: "left" },
+            location: { default: "onRight" },
+            docid: { default: "" }
+        },
+        group: "inline",
+        draggable: true,
+        // parseDOM: [{
+        //     tag: "img[src]", getAttrs(dom: any) {
+        //         return {
+        //             src: dom.getAttribute("src"),
+        //             title: dom.getAttribute("title"),
+        //             alt: dom.getAttribute("alt"),
+        //             width: Math.min(100, Number(dom.getAttribute("width"))),
+        //         };
+        //     }
+        // }],
+        // TODO if we don't define toDom, dragging the image crashes. Why?
+        toDOM(node) {
+            const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+            return ["div", { ...node.attrs, ...attrs }];
+        }
+    },
+
     video: {
         inline: true,
         attrs: {
@@ -671,6 +705,109 @@ export class ImageResizeView {
     }
 }
 
+export class DashDocView {
+    _handle: HTMLElement;
+    _dashSpan: HTMLDivElement;
+    _outer: HTMLElement;
+    constructor(node: any, view: any, getPos: any, addDocTab: any) {
+        this._handle = document.createElement("span");
+        this._dashSpan = document.createElement("div");
+        this._outer = document.createElement("span");
+        this._outer.style.position = "relative";
+        this._outer.style.width = node.attrs.width;
+        this._outer.style.height = node.attrs.height;
+        this._outer.style.display = "inline-block";
+        this._outer.style.overflow = "hidden";
+        (this._outer.style as any).float = node.attrs.float;
+
+        this._dashSpan.style.width = "100%";
+        this._dashSpan.style.height = "100%";
+        this._dashSpan.style.position = "absolute";
+        this._dashSpan.style.display = "inline-block"
+        this._handle.style.position = "absolute";
+        this._handle.style.width = "20px";
+        this._handle.style.height = "20px";
+        this._handle.style.backgroundColor = "blue";
+        this._handle.style.borderRadius = "15px";
+        this._handle.style.display = "none";
+        this._handle.style.bottom = "-10px";
+        this._handle.style.right = "-10px";
+        DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
+            if (dashDoc instanceof Doc) {
+                let scale = () => 100 / NumCast(dashDoc.nativeWidth, 100);
+                ReactDOM.render(<DocumentView
+                    fitToBox={true}
+                    Document={dashDoc}
+                    addDocument={returnFalse}
+                    removeDocument={returnFalse}
+                    ruleProvider={undefined}
+                    ScreenToLocalTransform={Transform.Identity}
+                    addDocTab={returnFalse}
+                    pinToPres={returnFalse}
+                    renderDepth={1}
+                    PanelWidth={() => 100}
+                    PanelHeight={() => 100}
+                    focus={emptyFunction}
+                    backgroundColor={returnEmptyString}
+                    parentActive={returnFalse}
+                    whenActiveChanged={returnFalse}
+                    bringToFront={emptyFunction}
+                    zoomToScale={emptyFunction}
+                    getScale={returnOne}
+                    ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
+                    ContentScaling={scale}
+                ></DocumentView>, this._dashSpan);
+            }
+        });
+        let self = this;
+        this._dashSpan.onpointerdown = function (e: any) {
+        };
+        this._handle.onpointerdown = function (e: any) {
+            e.preventDefault();
+            e.stopPropagation();
+            const startX = e.pageX;
+            const startWidth = parseFloat(node.attrs.width);
+            const onpointermove = (e: any) => {
+                const currentX = e.pageX;
+                const diffInPx = currentX - startX;
+                self._outer.style.width = `${startWidth + diffInPx}`;
+                //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
+                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
+            };
+
+            const onpointerup = () => {
+                document.removeEventListener("pointermove", onpointermove);
+                document.removeEventListener("pointerup", onpointerup);
+                view.dispatch(
+                    view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
+                        { ...node.attrs, width: self._outer.style.width })
+                );
+                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
+            };
+
+            document.addEventListener("pointermove", onpointermove);
+            document.addEventListener("pointerup", onpointerup);
+        };
+
+        this._outer.appendChild(this._handle);
+        this._outer.appendChild(this._dashSpan);
+        (this as any).dom = this._outer;
+    }
+
+    selectNode() {
+        this._dashSpan.classList.add("ProseMirror-selectednode");
+
+        this._handle.style.display = "";
+    }
+
+    deselectNode() {
+        this._dashSpan.classList.remove("ProseMirror-selectednode");
+
+        this._handle.style.display = "none";
+    }
+}
+
 export class OrderedListView {
     update(node: any) {
         return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
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}>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9d42eb719..26ffaf3a6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -48,7 +48,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     private _resizeBorderWidth = 16;
     private _linkBoxHeight = 20 + 3; // link button height + margin
     private _titleHeight = 20;
-    private _embedButton = React.createRef<HTMLDivElement>();
     private _downX = 0;
     private _downY = 0;
     private _iconDoc?: Doc = undefined;
@@ -414,41 +413,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     }
 
 
-    onEmbedButtonDown = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        document.removeEventListener("pointermove", this.onEmbedButtonMoved);
-        document.addEventListener("pointermove", this.onEmbedButtonMoved);
-        document.removeEventListener("pointerup", this.onEmbedButtonUp);
-        document.addEventListener("pointerup", this.onEmbedButtonUp);
-    }
-
-
-
-    onEmbedButtonUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onEmbedButtonMoved);
-        document.removeEventListener("pointerup", this.onEmbedButtonUp);
-        e.stopPropagation();
-    }
-
-    @action
-    onEmbedButtonMoved = (e: PointerEvent): void => {
-        if (this._embedButton.current !== null) {
-            document.removeEventListener("pointermove", this.onEmbedButtonMoved);
-            document.removeEventListener("pointerup", this.onEmbedButtonUp);
-
-            let dragDocView = SelectionManager.SelectedDocuments()[0];
-            let dragData = new DragManager.EmbedDragData(dragDocView.props.Document);
-
-            DragManager.StartEmbedDrag(dragDocView.ContentDiv!, dragData, e.x, e.y, {
-                handlers: {
-                    dragComplete: action(emptyFunction),
-                },
-                hideSource: false
-            });
-        }
-        e.stopPropagation();
-    }
-
     onPointerMove = (e: PointerEvent): void => {
         e.stopPropagation();
         e.preventDefault();
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 14e513157..fe805a980 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -31,7 +31,6 @@ import { SubCollectionViewProps } from "./CollectionSubView";
 import React = require("react");
 import { ButtonSelector } from './ParentDocumentSelector';
 import { DocumentType } from '../../documents/DocumentTypes';
-import { compileFunction } from 'vm';
 import { ComputedField } from '../../../new_fields/ScriptField';
 library.add(faFile);
 const _global = (window /* browser */ || global /* node */) as any;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f84301c10..819accf20 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -12,7 +12,7 @@ import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "
 import { ReplaceStep } from 'prosemirror-transform';
 import { EditorView } from "prosemirror-view";
 import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCastAsync, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
 import { Copy, Id } from '../../../new_fields/FieldSymbols';
 import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from '../../../new_fields/RichTextUtils';
@@ -28,7 +28,7 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from "../../util/DragManager";
 import buildKeymap from "../../util/ProsemirrorExampleTransfer";
 import { inpRules } from "../../util/RichTextRules";
-import { FootnoteView, ImageResizeView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema";
+import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema";
 import { SelectionManager } from "../../util/SelectionManager";
 import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
 import { TooltipTextMenu } from "../../util/TooltipTextMenu";
@@ -289,14 +289,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @action
     drop = async (e: Event, de: DragManager.DropEvent) => {
         // We're dealing with a link to a document
-        if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
+        if (de.data instanceof DragManager.EmbedDragData) {
             let target = de.data.embeddableSourceDoc;
             // We're dealing with an internal document drop
-            let url = de.data.urlField.url.href;
-            let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
+            const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+            let node: Node<any>;
+            if (de.data.urlField && link) {
+                let url: string = de.data.urlField.url.href;
+                let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
+                node = model.create({ src: url, docid: link[Id] })
+            } else {
+                node = schema.nodes.dashDoc.create({
+                    width: this.props.Document[WidthSym](), height: this.props.Document[HeightSym](),
+                    title: "dashDoc", docid: target[Id]
+                });
+            }
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
-            let link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
-            link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: link[Id] })));
+            link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
             this.tryUpdateHeight();
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
@@ -736,6 +745,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 },
                 dispatchTransaction: this.dispatchTransaction,
                 nodeViews: {
+                    dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self.props.addDocTab); },
                     image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
                     star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
                     ordered_list(node, view, getPos) { return new OrderedListView(); },
-- 
cgit v1.2.3-70-g09d2


From 53685a27139886c1df74840cc9f451c046a32de6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 4 Oct 2019 01:50:57 -0400
Subject: got rid of MainOverlayTextBox!  fixed some things with embedding docs
 in text

---
 src/client/util/RichTextSchema.tsx                 |   4 +-
 src/client/views/InkingControl.tsx                 |   3 +-
 src/client/views/MainOverlayTextBox.scss           |  29 ----
 src/client/views/MainOverlayTextBox.tsx            | 156 ---------------------
 src/client/views/MainView.tsx                      |   5 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |   6 -
 .../collectionFreeForm/MarqueeView.scss            |   7 +
 .../collections/collectionFreeForm/MarqueeView.tsx |   4 +-
 src/client/views/nodes/FormattedTextBox.scss       |   7 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  52 ++-----
 10 files changed, 31 insertions(+), 242 deletions(-)
 delete mode 100644 src/client/views/MainOverlayTextBox.scss
 delete mode 100644 src/client/views/MainOverlayTextBox.tsx

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 7be773475..12cf40b32 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -720,8 +720,8 @@ export class DashDocView {
         this._outer.style.overflow = "hidden";
         (this._outer.style as any).float = node.attrs.float;
 
-        this._dashSpan.style.width = "100%";
-        this._dashSpan.style.height = "100%";
+        this._dashSpan.style.width = node.attrs.width;
+        this._dashSpan.style.height = node.attrs.height;
         this._dashSpan.style.position = "absolute";
         this._dashSpan.style.display = "inline-block"
         this._handle.style.position = "absolute";
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index a10df0e75..ee8b77050 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -10,7 +10,6 @@ import { InkTool } from "../../new_fields/InkField";
 import { Doc } from "../../new_fields/Doc";
 import { undoBatch, UndoManager } from "../util/UndoManager";
 import { StrCast, NumCast, Cast } from "../../new_fields/Types";
-import { MainOverlayTextBox } from "./MainOverlayTextBox";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
 import { Utils } from "../../Utils";
@@ -46,7 +45,7 @@ export class InkingControl extends React.Component {
     switchColor = action((color: ColorResult): void => {
         this._selectedColor = color.hex + (color.rgb.a !== undefined ? this.decimalToHexString(Math.round(color.rgb.a * 255)) : "ff");
         if (InkingControl.Instance.selectedTool === InkTool.None) {
-            if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
+            // if (MainOverlayTextBox.Instance.SetColor(color.hex)) return;
             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);
diff --git a/src/client/views/MainOverlayTextBox.scss b/src/client/views/MainOverlayTextBox.scss
deleted file mode 100644
index c9d44e194..000000000
--- a/src/client/views/MainOverlayTextBox.scss
+++ /dev/null
@@ -1,29 +0,0 @@
-@import "globalCssVariables";
-
-.mainOverlayTextBox-textInput {
-    background-color: rgba(248, 6, 6, 0.001);
-    width: 400px;
-    height: 200px;
-    position: absolute;
-    overflow: visible;
-    top: 0;
-    left: 0;
-    pointer-events: none;
-    z-index: $mainTextInput-zindex;
-
-    .formattedTextBox-cont {
-        background-color: rgba(248, 6, 6, 0.001);
-        width: 100%;
-        height: 100%;
-        position: absolute;
-        top: 0;
-        left: 0;
-    }
-}
-
-.mainOverlayTextBox-unscaled_div {
-    // width: 0px;
-    z-index: 10000;
-    position: absolute;
-    pointer-events: none;
-}
\ No newline at end of file
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
deleted file mode 100644
index 73eef9478..000000000
--- a/src/client/views/MainOverlayTextBox.tsx
+++ /dev/null
@@ -1,156 +0,0 @@
-import { action, observable, reaction, trace } from 'mobx';
-import { observer } from 'mobx-react';
-import "normalize.css";
-import * as React from 'react';
-import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { BoolCast } from '../../new_fields/Types';
-import { emptyFunction, returnTrue, returnZero, Utils, returnOne } from '../../Utils';
-import { DragManager } from '../util/DragManager';
-import { Transform } from '../util/Transform';
-import { CollectionDockingView } from './collections/CollectionDockingView';
-import "./MainOverlayTextBox.scss";
-import { FormattedTextBox } from './nodes/FormattedTextBox';
-
-interface MainOverlayTextBoxProps {
-    firstinstance?: boolean;
-}
-
-@observer
-export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps> {
-    public static Instance: MainOverlayTextBox;
-    @observable _textXf: () => Transform = () => Transform.Identity();
-    public TextFieldKey: string = "data";
-    private _textColor: string | null = null;
-    private _textHideOnLeave?: boolean;
-    private _textTargetDiv: HTMLDivElement | undefined;
-    private _textProxyDiv: React.RefObject<HTMLDivElement>;
-    private _textBottom: boolean | undefined;
-    private _setouterdiv = (outerdiv: HTMLElement | null) => { this._outerdiv = outerdiv; this.updateTooltip(); };
-    private _outerdiv: HTMLElement | null = null;
-    private _textBox: FormattedTextBox | undefined;
-    private _tooltip?: HTMLElement;
-    ChromeHeight?: () => number;
-    @observable public TextDoc?: Doc;
-    @observable public TextDataDoc?: Doc;
-
-    updateTooltip = () => {
-        this._outerdiv && this._tooltip && !this._outerdiv.contains(this._tooltip) && this._outerdiv.appendChild(this._tooltip);
-    }
-
-    public SetColor(color: string) {
-        return this._textBox && this._textBox.setFontColor(color);
-    }
-
-    constructor(props: MainOverlayTextBoxProps) {
-        super(props);
-        this._textProxyDiv = React.createRef();
-        MainOverlayTextBox.Instance = this;
-        reaction(() => FormattedTextBox.InputBoxOverlay,
-            (box?: FormattedTextBox) => {
-                this._textBox = box;
-                if (box) {
-                    this.ChromeHeight = box.props.ChromeHeight;
-                    this.TextDoc = box.props.Document;
-                    this.TextDataDoc = box.props.DataDoc;
-                    let xf = () => {
-                        box.props.ScreenToLocalTransform();
-                        let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
-                        return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale);
-                    };
-                    this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight) || box.props.height === "min-content");
-                }
-                else {
-                    this.TextDoc = undefined;
-                    this.TextDataDoc = undefined;
-                    this.setTextDoc();
-                }
-            });
-    }
-
-    @action
-    private setTextDoc(textFieldKey?: string, div?: HTMLDivElement, tx?: () => Transform, autoHeight?: boolean) {
-        if (this._textTargetDiv) {
-            this._textTargetDiv.style.color = this._textColor;
-        }
-        this.TextFieldKey = textFieldKey!;
-        let txf = tx ? tx : () => Transform.Identity();
-        this._textXf = txf;
-        this._textTargetDiv = div;
-        this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
-        if (div) {
-            let parTop = div.parentElement && getComputedStyle(div.parentElement).top;
-            this._textBottom = parTop && parTop !== "0px" && parTop != "auto" ? true : false;
-            this._textColor = (getComputedStyle(div) as any).color;
-            div.style.color = "transparent";
-        }
-    }
-
-    @action
-    textScroll = (e: React.UIEvent) => {
-        if (this._textProxyDiv.current && this._textTargetDiv) {
-            this._textTargetDiv.scrollTop = (e as any)._targetInst.stateNode.scrollTop;
-        }
-    }
-
-    textBoxDown = (e: React.PointerEvent) => {
-        if (e.button !== 0 || e.metaKey || e.altKey) {
-            document.addEventListener("pointermove", this.textBoxMove);
-            document.addEventListener('pointerup', this.textBoxUp);
-        }
-    }
-    @action
-    textBoxMove = (e: PointerEvent) => {
-        if ((e.movementX > 1 || e.movementY > 1) && FormattedTextBox.InputBoxOverlay) {
-            document.removeEventListener("pointermove", this.textBoxMove);
-            document.removeEventListener('pointerup', this.textBoxUp);
-            let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document]);
-            const [left, top] = this._textXf().inverse().transformPoint(0, 0);
-            dragData.offset = [e.clientX - left, e.clientY - top];
-            DragManager.StartDocumentDrag([this._textTargetDiv!], dragData, e.clientX, e.clientY, {
-                handlers: {
-                    dragComplete: action(emptyFunction),
-                },
-                hideSource: false
-            });
-        }
-    }
-    textBoxUp = (e: PointerEvent) => {
-        document.removeEventListener("pointermove", this.textBoxMove);
-        document.removeEventListener('pointerup', this.textBoxUp);
-    }
-
-    addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
-        return this._textBox && this._textBox.props.addDocTab(doc, dataDoc, location) ? true : false;
-    }
-    render() {
-        this.TextDoc; this.TextDataDoc;
-        if (FormattedTextBox.InputBoxOverlay && this._textTargetDiv) {
-            let wid = FormattedTextBox.InputBoxOverlay.props.Document.width; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
-            let hgtx = FormattedTextBox.InputBoxOverlay.props.Document.height; // need to force overlay to render when underlying text box is resized (eg, w/ DocDecorations)
-            let textRect = this._textTargetDiv.getBoundingClientRect();
-            let s = this._textXf().Scale;
-            let location = this._textBottom ? textRect.bottom : textRect.top;
-            let hgt = (this._textBox && this._textBox.props.Document.autoHeight) || this._textBottom ? "auto" : this._textTargetDiv.clientHeight;
-            return <div ref={this._setouterdiv} className="mainOverlayTextBox-unscaled_div" style={{ transform: `translate(${textRect.left}px, ${location}px)` }} >
-                <div className="mainOverlayTextBox-textInput" style={{ transform: `scale(${1 / s})`, width: "auto", height: "0px" }} >
-                    <div className="mainOverlayTextBox-textInput" onPointerDown={this.textBoxDown} ref={this._textProxyDiv} onScroll={this.textScroll}
-                        style={{ width: `${textRect.width * s}px`, height: "0px" }}>
-                        <div style={{ height: hgt, width: "100%", position: "absolute", bottom: this._textBottom ? "0px" : undefined }}>
-                            <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} fieldExt="" hideOnLeave={this._textHideOnLeave} isOverlay={true}
-                                Document={FormattedTextBox.InputBoxOverlay.props.Document}
-                                DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
-                                onClick={undefined}
-                                ruleProvider={this._textBox ? this._textBox.props.ruleProvider : undefined}
-                                ChromeHeight={this.ChromeHeight} isSelected={returnTrue} select={emptyFunction} renderDepth={0}
-                                ContainingCollectionDoc={undefined} ContainingCollectionView={undefined}
-                                whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
-                                ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction}
-                                pinToPres={returnZero} addDocTab={this.addDocTab} outer_div={(tooltip: HTMLElement) => { this._tooltip = tooltip; this.updateTooltip(); }} />
-                        </div>
-                    </div>
-                </div>
-            </ div>;
-        }
-        else return (null);
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 660b42290..545f99a41 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -34,14 +34,12 @@ import { DocumentDecorations } from './DocumentDecorations';
 import KeyManager from './GlobalKeyHandler';
 import { InkingControl } from './InkingControl';
 import "./Main.scss";
-import { MainOverlayTextBox } from './MainOverlayTextBox';
 import MainViewModal from './MainViewModal';
 import { DocumentView } from './nodes/DocumentView';
-import { PresBox } from './nodes/PresBox';
-import { OverlayView } from './OverlayView';
 import PDFMenu from './pdf/PDFMenu';
 import { PreviewCursor } from './PreviewCursor';
 import { FilterBox } from './search/FilterBox';
+import { OverlayView } from './OverlayView';
 
 @observer
 export class MainView extends React.Component {
@@ -687,7 +685,6 @@ export class MainView extends React.Component {
                 {this.nodesMenu()}
                 {this.miscButtons}
                 <PDFMenu />
-                <MainOverlayTextBox firstinstance={true} />
                 <OverlayView />
             </div >
         );
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index c4311fa52..bb1a12f88 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -46,12 +46,6 @@
     border-radius: inherit;
     box-sizing: border-box;
     position: absolute;
-    overflow: hidden;
-
-    .marqueeView {
-        overflow: hidden;
-    }
-
     top: 0;
     left: 0;
     width: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 9fc2e44fb..7dc54ea79 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -6,6 +6,13 @@
     width:100%;
     height:100%;
 }
+.marqueeView {
+    overflow: hidden;
+}
+
+.marqueeView:focus-within {
+    overflow: visible;
+}
 .marquee {
     border-style: dashed;
     box-sizing: border-box;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 82193aefa..bf154d37d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -442,8 +442,8 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
 
     render() {
         let p: [number, number] = this._visible ? this.props.getContainerTransform().transformPoint(this._downX < this._lastX ? this._downX : this._lastX, this._downY < this._lastY ? this._downY : this._lastY) : [0, 0];
-        return <div className="marqueeView" style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
-            <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} >
+        return <div className="marqueeView" onScroll={(e) => e.currentTarget.scrollLeft = 0} style={{ borderRadius: "inherit" }} onClick={this.onClick} onPointerDown={this.onPointerDown}>
+            <div style={{ position: "relative", transform: `translate(${p[0]}px, ${p[1]}px)` }} onScroll={(e) => e.currentTarget.scrollLeft = 0}  >
                 {this._visible ? this.marqueeDiv : null}
                 <div ref={this._mainCont} style={{ transform: `translate(${-p[0]}px, ${-p[1]}px)` }} >
                     {this.props.children}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 45e516015..541c29faa 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -28,7 +28,7 @@
 }
 
 .formattedTextBox-cont-hidden {
-    pointer-events: none;
+   // pointer-events: none;
 }
 
 .formattedTextBox-inner-rounded {
@@ -40,9 +40,10 @@
     left: 10%;
 }
 
-.formattedTextBox-inner-rounded div,
-.formattedTextBox-inner div {
+.formattedTextBox-inner-rounded ,
+.formattedTextBox-inner  {
     padding: 10px 10px;
+    height: 100%;
 }
 
 .menuicon {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 819accf20..13eb78f48 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -46,7 +46,6 @@ library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
 
 export interface FormattedTextBoxProps {
-    isOverlay?: boolean;
     hideOnLeave?: boolean;
     height?: string;
     color?: string;
@@ -95,9 +94,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @observable private _fontFamily = "Arial";
     @observable private _fontAlign = "";
     @observable private _entered = false;
-    @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
     public static SelectOnLoad = "";
-    public static InputBoxOverlayScroll: number = 0;
     public static IsFragment(html: string) {
         return html.indexOf("data-pm-slice") !== -1;
     }
@@ -137,9 +134,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     constructor(props: FieldViewProps) {
         super(props);
-        if (this.props.isOverlay) {
-            DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
-        }
         FormattedTextBox.Instance = this;
     }
 
@@ -300,8 +294,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 node = model.create({ src: url, docid: link[Id] })
             } else {
                 node = schema.nodes.dashDoc.create({
-                    width: this.props.Document[WidthSym](), height: this.props.Document[HeightSym](),
-                    title: "dashDoc", docid: target[Id]
+                    width: target[WidthSym](), height: target[HeightSym](),
+                    title: "dashDoc", docid: target[Id],
+                    float: "none"
                 });
             }
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
@@ -390,7 +385,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         (schema as any).Document = this.props.Document;
         return {
             schema,
-            plugins: this.props.isOverlay ? [
+            plugins: [
                 inputRules(inpRules),
                 this.tooltipTextMenuPlugin(),
                 history(),
@@ -403,26 +398,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     }
                 }),
                 formattedTextBoxCommentPlugin
-            ] : [
-                    history(),
-                    keymap(this._keymap),
-                    keymap(baseKeymap),
-                ]
+            ]
         };
     }
 
     componentDidMount() {
-
-        if (!this.props.isOverlay) {
-            this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
-                () => {
-                    if (this.props.isSelected()) {
-                        FormattedTextBox.InputBoxOverlay = this;
-                        FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
-                    }
-                }, { fireImmediately: true });
-        }
-
         this.pullFromGoogleDoc(this.checkState);
         this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
 
@@ -551,7 +531,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     let editor = this._editorView;
                     let ret = findLinkFrag(editor.state.doc.content, editor);
 
-                    if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
+                    if (ret.frag.size > 2) {
                         let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
                         if (ret.frag.firstChild) {
                             selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
@@ -754,7 +734,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
             });
-            (this._editorView as any).isOverlay = this.props.isOverlay;
             if (startup) {
                 Doc.GetProto(doc).documentText = undefined;
                 this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
@@ -766,7 +745,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             FormattedTextBox.SelectOnLoad = "";
             this.props.select(false);
         }
-        else if (this.props.isOverlay) this._editorView!.focus();
+        this._editorView!.focus();
         // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
         this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
     }
@@ -824,13 +803,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         document.removeEventListener("keypress", this.recordKeyHandler);
         document.addEventListener("keypress", this.recordKeyHandler);
         this.tryUpdateHeight();
-        if (!this.props.isOverlay) {
-            FormattedTextBox.InputBoxOverlay = this;
-        } else {
-            if (this._ref.current) {
-                this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
-            }
-        }
     }
     onPointerWheel = (e: React.WheelEvent): void => {
         // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
@@ -901,7 +873,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }
             }
         }
-        this._proseRef!.focus();
+        let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+        if (pos && pos.pos > 0) {
+            this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new TextSelection(this._editorView!.state.doc.resolve(pos.pos))));
+        }
+        this._editorView!.focus();
         if (this._linkClicked) {
             this._linkClicked = "";
             e.preventDefault();
@@ -959,7 +935,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     tryUpdateHeight() {
         const ChromeHeight = this.props.ChromeHeight;
         let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
-        if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
+        if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
             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 * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
@@ -968,7 +944,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     render() {
-        let style = this.props.isOverlay ? "scroll" : "hidden";
+        let style = "hidden";
         let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
         let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
             ? "none" : "all";
-- 
cgit v1.2.3-70-g09d2


From 850ab4b13f3402464b7ff9a89261bc278031f961 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 4 Oct 2019 12:49:42 -0400
Subject: fixes for embedded text boxes.

---
 src/client/util/RichTextSchema.tsx          | 60 ++++++++++++++++++-----------
 src/client/views/nodes/FormattedTextBox.tsx | 20 ++++++----
 2 files changed, 51 insertions(+), 29 deletions(-)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 12cf40b32..e6b456313 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -3,7 +3,7 @@ import { redo, undo } from "prosemirror-history";
 import { keymap } from "prosemirror-keymap";
 import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
 import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { EditorState, TextSelection } from "prosemirror-state";
+import { EditorState, TextSelection, NodeSelection } from "prosemirror-state";
 import { StepMap } from "prosemirror-transform";
 import { EditorView } from "prosemirror-view";
 import { Doc } from "../../new_fields/Doc";
@@ -635,6 +635,7 @@ export class ImageResizeView {
         this._outer = document.createElement("span");
         this._outer.style.position = "relative";
         this._outer.style.width = node.attrs.width;
+        this._outer.style.height = node.attrs.height;
         this._outer.style.display = "inline-block";
         this._outer.style.overflow = "hidden";
         (this._outer.style as any).float = node.attrs.float;
@@ -650,8 +651,15 @@ export class ImageResizeView {
         this._handle.style.bottom = "-10px";
         this._handle.style.right = "-10px";
         let self = this;
+        this._img.onclick = function (e: any) {
+            e.stopPropagation();
+            e.preventDefault();
+            if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image)
+                view.dispatch(
+                    view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
+        }
         this._img.onpointerdown = function (e: any) {
-            if (!view.isOverlay || e.ctrlKey) {
+            if (e.ctrlKey) {
                 e.preventDefault();
                 e.stopPropagation();
                 DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
@@ -663,32 +671,31 @@ export class ImageResizeView {
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
+            let wid = Number(getComputedStyle(self._img).width!.replace(/px/, ""));
+            let hgt = Number(getComputedStyle(self._img).height!.replace(/px/, ""));
             const startX = e.pageX;
             const startWidth = parseFloat(node.attrs.width);
             const onpointermove = (e: any) => {
                 const currentX = e.pageX;
                 const diffInPx = currentX - startX;
                 self._outer.style.width = `${startWidth + diffInPx}`;
-                //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
-                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
+                self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
             };
 
             const onpointerup = () => {
                 document.removeEventListener("pointermove", onpointermove);
                 document.removeEventListener("pointerup", onpointerup);
-                view.dispatch(
-                    view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
-                        { ...node.attrs, width: self._outer.style.width })
-                );
-                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
+                let pos = view.state.selection.from;
+                view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
+                view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
             };
 
             document.addEventListener("pointermove", onpointermove);
             document.addEventListener("pointerup", onpointerup);
         };
 
-        this._outer.appendChild(this._handle);
         this._outer.appendChild(this._img);
+        this._outer.appendChild(this._handle);
         (this as any).dom = this._outer;
     }
 
@@ -761,37 +768,46 @@ export class DashDocView {
             }
         });
         let self = this;
-        this._dashSpan.onpointerdown = function (e: any) {
-        };
+        this._dashSpan.onclick = function (e: any) {
+            FormattedTextBox.firstTarget && FormattedTextBox.firstTarget();
+        }
+        this._dashSpan.onkeydown = function (e: any) {
+            e.stopPropagation();
+        }
+        this._dashSpan.onkeypress = function (e: any) {
+            e.stopPropagation();
+        }
+        this._dashSpan.onkeyup = function (e: any) {
+            e.stopPropagation();
+        }
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
             const startX = e.pageX;
+            const startY = e.pageY;
             const startWidth = parseFloat(node.attrs.width);
+            const startHeight = parseFloat(node.attrs.height);
             const onpointermove = (e: any) => {
-                const currentX = e.pageX;
-                const diffInPx = currentX - startX;
+                const diffInPx = e.pageX - startX;
+                const diffInPy = e.pageY - startY;
                 self._outer.style.width = `${startWidth + diffInPx}`;
-                //Array.from(FormattedTextBox.InputBoxOverlay!.CurrentDiv.getElementsByTagName("img")).map((img: any) => img.opacity = "0.1");
-                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "0";
+                self._outer.style.height = `${startHeight + diffInPy}`;
             };
 
             const onpointerup = () => {
                 document.removeEventListener("pointermove", onpointermove);
                 document.removeEventListener("pointerup", onpointerup);
-                view.dispatch(
-                    view.state.tr.setSelection(view.state.selection).setNodeMarkup(getPos(), null,
-                        { ...node.attrs, width: self._outer.style.width })
-                );
-                FormattedTextBox.InputBoxOverlay!.CurrentDiv.style.opacity = "1";
+                let pos = view.state.selection.from;
+                view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
+                view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
             };
 
             document.addEventListener("pointermove", onpointermove);
             document.addEventListener("pointerup", onpointerup);
         };
 
-        this._outer.appendChild(this._handle);
         this._outer.appendChild(this._dashSpan);
+        this._outer.appendChild(this._handle);
         (this as any).dom = this._outer;
     }
 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 13eb78f48..05904e1e7 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -774,9 +774,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._searchReactionDisposer && this._searchReactionDisposer();
         this._editorView && this._editorView.destroy();
     }
-
-
+    public static firstTarget: () => void;
     onPointerDown = (e: React.PointerEvent): void => {
+        if ((e.nativeEvent as any).formattedHandled) return;
+        (e.nativeEvent as any).formattedHandled = true;
         let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
         pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos));
         if (this.props.onClick && e.button === 0) {
@@ -785,10 +786,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
             e.stopPropagation();
         }
-        let ctrlKey = e.ctrlKey;
         if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
             e.preventDefault();
         }
+        FormattedTextBox.firstTarget = () => {  // this is here to support nested text boxes.  when that happens, the click event will propagate through prosemirror to the outer editor.  In RichTextSchema, the outer editor calls this function to revert the focus/selection
+            if (pos && pos.pos > 0) {
+                let node = this._editorView!.state.doc.nodeAt(pos.pos);
+                if (!node || (node.type !== this._editorView!.state.schema.nodes.dashDoc && node.type !== this._editorView!.state.schema.nodes.image &&
+                    pos.pos !== this._editorView!.state.selection.from)) {
+                    this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new TextSelection(this._editorView!.state.doc.resolve(pos!.pos))));
+                    this._editorView!.focus();
+                }
+            }
+        }
     }
 
     onPointerUp = (e: React.PointerEvent): void => {
@@ -873,10 +883,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }
             }
         }
-        let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
-        if (pos && pos.pos > 0) {
-            this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new TextSelection(this._editorView!.state.doc.resolve(pos.pos))));
-        }
         this._editorView!.focus();
         if (this._linkClicked) {
             this._linkClicked = "";
-- 
cgit v1.2.3-70-g09d2


From dcaad9277d2b25e1707964c442c4d19aae59d533 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 4 Oct 2019 13:40:14 -0400
Subject: fixed selection within nested textbox.

---
 src/client/util/RichTextSchema.tsx | 1 +
 1 file changed, 1 insertion(+)

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index e6b456313..53eaf9ce2 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -770,6 +770,7 @@ export class DashDocView {
         let self = this;
         this._dashSpan.onclick = function (e: any) {
             FormattedTextBox.firstTarget && FormattedTextBox.firstTarget();
+            e.stopPropagation();
         }
         this._dashSpan.onkeydown = function (e: any) {
             e.stopPropagation();
-- 
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/util/RichTextSchema.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 d611773fc805082a935cae49723d516ce66e1a14 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 4 Oct 2019 23:45:01 -0400
Subject: more pdf cleanup.   fix to mix-multiply-mode for better
 highlighters/opacity.  small text box fixes.

---
 src/client/util/DocumentManager.ts                 |   2 +-
 src/client/util/RichTextSchema.tsx                 |  26 ++-
 src/client/util/SelectionManager.ts                |   2 -
 src/client/views/DocumentButtonBar.tsx             |   1 -
 src/client/views/DocumentDecorations.tsx           |   1 -
 src/client/views/MainView.tsx                      |   3 +-
 .../collectionFreeForm/MarqueeView.scss            |   2 +-
 src/client/views/nodes/Annotation.tsx              | 117 ------------
 src/client/views/nodes/FormattedTextBox.tsx        |   7 +-
 src/client/views/pdf/Annotation.scss               |   3 +-
 src/client/views/pdf/Annotation.tsx                |   4 +-
 src/client/views/pdf/PDFMenu.tsx                   |  10 +-
 src/client/views/pdf/PDFViewer.scss                |   6 +-
 src/client/views/pdf/PDFViewer.tsx                 | 198 +++++++++------------
 src/new_fields/Doc.ts                              |   2 +-
 .../authentication/models/current_user_utils.ts    |   2 +-
 16 files changed, 117 insertions(+), 269 deletions(-)
 delete mode 100644 src/client/views/nodes/Annotation.tsx

(limited to 'src/client/util/RichTextSchema.tsx')

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ffd311665..6fe97edbb 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -139,7 +139,7 @@ export class DocumentManager {
             const finalDocView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
             finalDocView && (finalDocView.Document.scrollToLinkID = linkId);
             finalDocView && Doc.linkFollowHighlight(finalDocView.props.Document);
-        }
+        };
         const docView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
         const annotatedDoc = await Cast(targetDoc.annotationOn, Doc);
         if (docView) {  // we have a docView already and aren't forced to create a new one ... just focus on the document.  TODO move into view if necessary otherwise just highlight?
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 53eaf9ce2..528c0000b 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -654,17 +654,17 @@ export class ImageResizeView {
         this._img.onclick = function (e: any) {
             e.stopPropagation();
             e.preventDefault();
-            if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image)
-                view.dispatch(
-                    view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
-        }
+            if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) {
+                view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
+            }
+        };
         this._img.onpointerdown = function (e: any) {
             if (e.ctrlKey) {
                 e.preventDefault();
                 e.stopPropagation();
                 DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
                     (linkDoc instanceof Doc) &&
-                    DocumentManager.Instance.FollowLink(linkDoc, (view.state.schema as any).Document,
+                    DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
                         document => addDocTab(document, undefined, node.attrs.location ? node.attrs.location : "inTab"), false));
             }
         };
@@ -730,7 +730,7 @@ export class DashDocView {
         this._dashSpan.style.width = node.attrs.width;
         this._dashSpan.style.height = node.attrs.height;
         this._dashSpan.style.position = "absolute";
-        this._dashSpan.style.display = "inline-block"
+        this._dashSpan.style.display = "inline-block";
         this._handle.style.position = "absolute";
         this._handle.style.width = "20px";
         this._handle.style.height = "20px";
@@ -771,16 +771,10 @@ export class DashDocView {
         this._dashSpan.onclick = function (e: any) {
             FormattedTextBox.firstTarget && FormattedTextBox.firstTarget();
             e.stopPropagation();
-        }
-        this._dashSpan.onkeydown = function (e: any) {
-            e.stopPropagation();
-        }
-        this._dashSpan.onkeypress = function (e: any) {
-            e.stopPropagation();
-        }
-        this._dashSpan.onkeyup = function (e: any) {
-            e.stopPropagation();
-        }
+        };
+        this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
+        this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+        this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
             e.stopPropagation();
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/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..9acb77ce2 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -466,7 +466,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);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 545f99a41..3b0457dff 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,6 +1,5 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-import { faLink, faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
-import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faClone, faCloudUploadAlt, faCommentAlt, faCut, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight, faMusic, faObjectGroup, faPause, faPenNib, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faUndoAlt, faTv, faChevronRight, faEllipsisV } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.scss b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
index 7dc54ea79..04f6ec2ad 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.scss
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.scss
@@ -11,7 +11,7 @@
 }
 
 .marqueeView:focus-within {
-    overflow: visible;
+    overflow: hidden;
 }
 .marquee {
     border-style: dashed;
diff --git a/src/client/views/nodes/Annotation.tsx b/src/client/views/nodes/Annotation.tsx
deleted file mode 100644
index 3e4ed6bf1..000000000
--- a/src/client/views/nodes/Annotation.tsx
+++ /dev/null
@@ -1,117 +0,0 @@
-import "./ImageBox.scss";
-import React = require("react");
-import { observer } from "mobx-react";
-import { observable, action } from 'mobx';
-import 'react-pdf/dist/Page/AnnotationLayer.css';
-
-interface IProps {
-    Span: HTMLSpanElement;
-    X: number;
-    Y: number;
-    Highlights: any[];
-    Annotations: any[];
-    CurrAnno: any[];
-
-}
-
-/**
- * Annotation class is used to take notes on a particular highlight. You can also change highlighted span's color
- * Improvements to be made: Removing the annotation when onRemove is called. (Removing this, not just the highlighted span). 
- * Also need to support multiline highlighting
- * 
- * Written by: Andrew Kim
- */
-@observer
-export class Annotation extends React.Component<IProps> {
-
-    /**
-     * changes color of the span (highlighted section)
-     */
-    onColorChange = (e: React.PointerEvent) => {
-        if (e.currentTarget.innerHTML === "r") {
-            this.props.Span.style.backgroundColor = "rgba(255,0,0, 0.3)";
-        } else if (e.currentTarget.innerHTML === "b") {
-            this.props.Span.style.backgroundColor = "rgba(0,255, 255, 0.3)";
-        } else if (e.currentTarget.innerHTML === "y") {
-            this.props.Span.style.backgroundColor = "rgba(255,255,0, 0.3)";
-        } else if (e.currentTarget.innerHTML === "g") {
-            this.props.Span.style.backgroundColor = "rgba(76, 175, 80, 0.3)";
-        }
-
-    }
-
-    /**
-     * removes the highlighted span. Supposed to remove Annotation too, but I don't know how to unmount this
-     */
-    @action
-    onRemove = (e: any) => {
-        let index: number = -1;
-        //finding the highlight in the highlight array 
-        this.props.Highlights.forEach((e) => {
-            for (const span of e.spans) {
-                if (span === this.props.Span) {
-                    index = this.props.Highlights.indexOf(e);
-                    this.props.Highlights.splice(index, 1);
-                }
-            }
-        });
-
-        //removing from CurrAnno and Annotation array 
-        this.props.Annotations.splice(index, 1);
-        this.props.CurrAnno.pop();
-
-        //removing span from div
-        if (this.props.Span.parentElement) {
-            let nodesArray = this.props.Span.parentElement.childNodes;
-            nodesArray.forEach((e) => {
-                if (e === this.props.Span) {
-                    if (this.props.Span.parentElement) {
-                        this.props.Highlights.forEach((item) => {
-                            if (item === e) {
-                                item.remove();
-                            }
-                        });
-                        e.remove();
-                    }
-                }
-            });
-        }
-
-
-    }
-
-    render() {
-        return (
-            <div
-                style={{
-                    position: "absolute",
-                    top: "20px",
-                    left: "0px",
-                    zIndex: 1,
-                    transform: `translate(${this.props.X}px, ${this.props.Y}px)`,
-
-                }}>
-                <div style={{ width: "200px", height: "50px", backgroundColor: "orange" }}>
-                    <button
-                        style={{ borderRadius: "25px", width: "25%", height: "100%" }}
-                        onClick={this.onRemove}
-                    >x</button>
-                    <div style={{ width: "75%", height: "100%", display: "inline-block" }}>
-                        <button onPointerDown={this.onColorChange} style={{ backgroundColor: "red", borderRadius: "50%", color: "transparent" }}>r</button>
-                        <button onPointerDown={this.onColorChange} style={{ backgroundColor: "blue", borderRadius: "50%", color: "transparent" }}>b</button>
-                        <button onPointerDown={this.onColorChange} style={{ backgroundColor: "yellow", borderRadius: "50%", color: "transparent" }}>y</button>
-                        <button onPointerDown={this.onColorChange} style={{ backgroundColor: "green", borderRadius: "50%", color: "transparent" }}>g</button>
-                    </div>
-
-                </div>
-                <div style={{ width: "200px", height: "200" }}>
-                    <textarea style={{ width: "100%", height: "100%" }}
-                        defaultValue="Enter Text Here..."
-
-                    ></textarea>
-                </div>
-            </div>
-
-        );
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 05904e1e7..2b6a86aed 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -291,7 +291,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             if (de.data.urlField && link) {
                 let url: string = de.data.urlField.url.href;
                 let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
-                node = model.create({ src: url, docid: link[Id] })
+                node = model.create({ src: url, docid: link[Id] });
             } else {
                 node = schema.nodes.dashDoc.create({
                     width: target[WidthSym](), height: target[HeightSym](),
@@ -798,7 +798,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     this._editorView!.focus();
                 }
             }
-        }
+        };
     }
 
     onPointerUp = (e: React.PointerEvent): void => {
@@ -807,9 +807,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             e.stopPropagation();
         }
     }
-
+    static InputBoxOverlay: any = null;
     @action
     onFocused = (e: React.FocusEvent): void => {
+        FormattedTextBox.InputBoxOverlay = this;
         document.removeEventListener("keypress", this.recordKeyHandler);
         document.addEventListener("keypress", this.recordKeyHandler);
         this.tryUpdateHeight();
diff --git a/src/client/views/pdf/Annotation.scss b/src/client/views/pdf/Annotation.scss
index 0c6df74f0..cc326eb93 100644
--- a/src/client/views/pdf/Annotation.scss
+++ b/src/client/views/pdf/Annotation.scss
@@ -2,6 +2,5 @@
     pointer-events: all;
     user-select: none;
     position: absolute;
-    background-color: red;
-    opacity: 0.1;
+    background-color: rgba(146, 245, 95, 0.467);
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 134e757d1..4bb166ffe 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -122,9 +122,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
                 left: this.props.x,
                 width: this.props.width,
                 height: this.props.height,
-                transition: "opacity 0.5s",
                 opacity: this._brushed ? 0.5 : undefined,
-                backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.color)
+                backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.backgroundColor),
+                transition: "opacity 0.5s",
             }} />);
     }
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index e62542014..1e3320069 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -4,7 +4,7 @@ import { observable, action, } from "mobx";
 import { observer } from "mobx-react";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { emptyFunction, returnFalse } from "../../../Utils";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, Opt } from "../../../new_fields/Doc";
 
 @observer
 export default class PDFMenu extends React.Component {
@@ -31,7 +31,7 @@ export default class PDFMenu extends React.Component {
     @observable public Pinned: boolean = false;
 
     public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = emptyFunction;
-    public Highlight: (color: string) => void = emptyFunction;
+    public Highlight: (color: string) => Opt<Doc> = (color: string) => undefined;
     public Delete: () => void = emptyFunction;
     public Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
     public AddTag: (key: string, value: string) => boolean = returnFalse;
@@ -155,12 +155,8 @@ export default class PDFMenu extends React.Component {
 
     @action
     highlightClicked = (e: React.MouseEvent) => {
-        if (!this.Pinned) {
-            this.Highlight("#f4f442");
-        }
-        else {
+        if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) {
             this.Highlighting = !this.Highlighting;
-            this.Highlight("#f4f442");
         }
     }
 
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a71e4f81e..c77cee792 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -16,6 +16,7 @@
         mix-blend-mode: multiply;
         opacity: 0.9;
     }
+    .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation()
     .textLayer .highlight {
         background-color: yellow;
     }
@@ -51,10 +52,9 @@
         pointer-events: none;
         mix-blend-mode: multiply;
 
-        .pdfPage-annotationBox {
+        .pdfViewer-annotationBox {
             position: absolute;
-            background-color: red;
-            opacity: 0.1;
+            background-color: rgba(245, 230, 95, 0.616);
         }
     }
     .pdfViewer-waiting {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 1b76ddbdc..4516e9904 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -75,8 +75,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @observable private _showWaiting = true;
     @observable private _showCover = false;
     @observable private _zoomed = 1;
+    @observable private _scrollTop = 0;
 
-    public pdfViewer: any;
+    private _pdfViewer: any;
     private _retries = 0; // number of times tried to create the PDF viewer 
     private _isChildActive = false;
     private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
@@ -149,15 +150,16 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     copy = (e: ClipboardEvent) => {
         if (this.props.active() && e.clipboardData) {
-            e.clipboardData.setData("text/plain", this._selectionText);
-            e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
-            e.clipboardData.setData("dash/pdfRegion", this.makeAnnotationDocument("#0390fc")[Id]);
+            let annoDoc = this.makeAnnotationDocument("#0390fc");
+            if (annoDoc) {
+                e.clipboardData.setData("text/plain", this._selectionText);
+                e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
+                e.clipboardData.setData("dash/pdfRegion", annoDoc[Id]);
+            }
             e.preventDefault();
         }
     }
 
-    setSelectionText = (text: string) => this._selectionText = text;
-
     @action
     initialLoad = async () => {
         if (this._pageSizes.length === 0) {
@@ -215,7 +217,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         document.removeEventListener("copy", this.copy);
         document.addEventListener("copy", this.copy);
         document.addEventListener("pagesinit", action(() => {
-            this.pdfViewer.currentScaleValue = this._zoomed = 1;
+            this._pdfViewer.currentScaleValue = this._zoomed = 1;
             this.gotoPage(NumCast(this.props.Document.curPage, 1));
         }));
         document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
@@ -223,36 +225,35 @@ export class PDFViewer extends React.Component<IViewerProps> {
         let pdfFindController = new PDFJSViewer.PDFFindController({
             linkService: pdfLinkService,
         });
-        this.pdfViewer = new PDFJSViewer.PDFViewer({
+        this._pdfViewer = new PDFJSViewer.PDFViewer({
             container: this._mainCont.current,
             viewer: this._viewer.current,
             linkService: pdfLinkService,
             findController: pdfFindController,
             renderer: "canvas",
         });
-        pdfLinkService.setViewer(this.pdfViewer);
+        pdfLinkService.setViewer(this._pdfViewer);
         pdfLinkService.setDocument(this.props.pdf, null);
-        this.pdfViewer.setDocument(this.props.pdf);
+        this._pdfViewer.setDocument(this.props.pdf);
     }
 
     @action
-    makeAnnotationDocument = (color: string): Doc => {
+    makeAnnotationDocument = (color: string): Opt<Doc> => {
+        if (this._savedAnnotations.size() === 0) return undefined;
         let mainAnnoDoc = Docs.Create.InstanceFromProto(new Doc(), "", {});
         let mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
         let annoDocs: Doc[] = [];
         let minY = Number.MAX_VALUE;
-        if (this._savedAnnotations.size() === 1 && this._savedAnnotations.values()[0].length === 1) {
+        if ((this._savedAnnotations.values()[0][0] as any).marqueeing) {
             let anno = this._savedAnnotations.values()[0][0];
-            let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: "rgba(255, 0, 0, 0.1)", title: "Annotation on " + StrCast(this.props.Document.title) });
+            let annoDoc = Docs.Create.FreeformDocument([], { backgroundColor: color, title: "Annotation on " + StrCast(this.props.Document.title) });
             if (anno.style.left) annoDoc.x = parseInt(anno.style.left);
             if (anno.style.top) annoDoc.y = parseInt(anno.style.top);
             if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
             if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
             annoDoc.group = mainAnnoDoc;
-            annoDoc.color = color;
-            annoDoc.type = AnnotationTypes.Region;
-            annoDocs.push(annoDoc);
             annoDoc.isButton = true;
+            annoDocs.push(annoDoc);
             anno.remove();
             mainAnnoDoc = annoDoc;
             mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
@@ -265,8 +266,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 if (anno.style.height) annoDoc.height = parseInt(anno.style.height);
                 if (anno.style.width) annoDoc.width = parseInt(anno.style.width);
                 annoDoc.group = mainAnnoDoc;
-                annoDoc.color = color;
-                annoDoc.type = AnnotationTypes.Region;
+                annoDoc.backgroundColor = color;
                 annoDocs.push(annoDoc);
                 anno.remove();
                 (annoDoc.y !== undefined) && (minY = Math.min(NumCast(annoDoc.y), minY));
@@ -307,7 +307,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     gotoPage = (p: number) => {
-        this.pdfViewer && this.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
+        this._pdfViewer && this._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(Math.max(1, p), this._pageSizes.length) });
     }
 
     @action
@@ -317,25 +317,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
         Doc.linkFollowHighlight(scrollToAnnotation);
     }
 
-    sendAnnotations = (page: number) => {
-        return this._savedAnnotations.getValue(page);
-    }
-
-    receiveAnnotations = (annotations: HTMLDivElement[], page: number) => {
-        if (page === -1) {
-            this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
-            this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, annotations));
-        }
-        else {
-            this._savedAnnotations.setValue(page, annotations);
-        }
-    }
 
-    @observable scrollTop = 0;
     @action
     onScroll = (e: React.UIEvent<HTMLElement>) => {
-        this.scrollTop = this._mainCont.current!.scrollTop;
-        this.pdfViewer && (this.props.Document.curPage = this.pdfViewer.currentPageNumber);
+        this._scrollTop = this._mainCont.current!.scrollTop;
+        this._pdfViewer && (this.props.Document.curPage = this._pdfViewer.currentPageNumber);
     }
 
     // get the page index that the vertical offset passed in is on
@@ -355,6 +341,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 div.style.top = (parseInt(div.style.top)/*+ this.getScrollFromPage(page)*/).toString();
             }
             this._annotationLayer.current.append(div);
+            div.style.backgroundColor = "yellow";
+            div.style.opacity = "0.5";
             let savedPage = this._savedAnnotations.getValue(page);
             if (savedPage) {
                 savedPage.push(div);
@@ -371,8 +359,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
         if (!searchString) {
             fwd ? this.nextAnnotation() : this.prevAnnotation();
         }
-        else if (this.pdfViewer._pageViewsReady) {
-            this.pdfViewer.findController.executeCommand('findagain', {
+        else if (this._pdfViewer._pageViewsReady) {
+            this._pdfViewer.findController.executeCommand('findagain', {
                 caseSensitive: false,
                 findPrevious: !fwd,
                 highlightAll: true,
@@ -382,7 +370,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
         else if (this._mainCont.current) {
             let executeFind = () => {
-                this.pdfViewer.findController.executeCommand('find', {
+                this._pdfViewer.findController.executeCommand('find', {
                     caseSensitive: false,
                     findPrevious: !fwd,
                     highlightAll: true,
@@ -406,30 +394,24 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
         this._marqueeing = false;
         if (!e.altKey && e.button === 0 && this.active()) {
+            // clear out old marquees and initialize menu for new selection
             PDFMenu.Instance.StartDrag = this.startDrag;
             PDFMenu.Instance.Highlight = this.highlight;
             PDFMenu.Instance.Snippet = this.createSnippet;
             PDFMenu.Instance.Status = "pdf";
             PDFMenu.Instance.fadeOut(true);
+            this._savedAnnotations.values().forEach(v => v.forEach(a => a.remove()));
+            this._savedAnnotations.keys().forEach(k => this._savedAnnotations.setValue(k, []));
             if (e.target && (e.target as any).parentElement.className === "textLayer") {
-                if (!e.ctrlKey) {
-                    this.receiveAnnotations([], -1);
-                }
+                // start selecting text if mouse down on textLayer spans
             }
-            else {
+            else if (this._mainCont.current) {
                 // set marquee x and y positions to the spatially transformed position
-                if (this._mainCont.current) {
-                    let boundingRect = this._mainCont.current.getBoundingClientRect();
-                    this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
-                    this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
-                }
+                let boundingRect = this._mainCont.current.getBoundingClientRect();
+                this._startX = this._marqueeX = (e.clientX - boundingRect.left) * (this._mainCont.current.offsetWidth / boundingRect.width);
+                this._startY = this._marqueeY = (e.clientY - boundingRect.top) * (this._mainCont.current.offsetHeight / boundingRect.height) + this._mainCont.current.scrollTop;
+                this._marqueeHeight = this._marqueeWidth = 0;
                 this._marqueeing = true;
-                let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
-                if (marquees && marquees.length) { // make a copy of the marquee
-                    let marquee = marquees[0] as HTMLDivElement;
-                    marquee.style.opacity = "0.2";
-                }
-                this.receiveAnnotations([], -1);
             }
             document.removeEventListener("pointermove", this.onSelectMove);
             document.addEventListener("pointermove", this.onSelectMove);
@@ -464,13 +446,13 @@ export class PDFViewer extends React.Component<IViewerProps> {
             let clientRects = selRange.getClientRects();
             for (let i = 0; i < clientRects.length; i++) {
                 let rect = clientRects.item(i);
-                if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) {
+                if (rect) {
                     let scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
                     let scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
                     if (rect.width !== this._mainCont.current.clientWidth &&
-                        (i == 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
+                        (i === 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
                         let annoBox = document.createElement("div");
-                        annoBox.className = "pdfPage-annotationBox";
+                        annoBox.className = "pdfViewer-annotationBox";
                         // transforms the positions from screen onto the pdf div
                         annoBox.style.top = ((rect.top - boundingRect.top) * scaleY + this._mainCont.current.scrollTop).toString();
                         annoBox.style.left = ((rect.left - boundingRect.left) * scaleX).toString();
@@ -481,8 +463,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 }
             }
         }
-        let text = selRange.cloneContents().textContent;
-        text && this.setSelectionText(text);
+        this._selectionText = selRange.cloneContents().textContent || "";
 
         // clear selection
         if (sel.empty) {  // Chrome
@@ -494,22 +475,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     onSelectEnd = (e: PointerEvent): void => {
+        this._savedAnnotations.clear();
         if (this._marqueeing) {
             if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
                 let marquees = this._mainCont.current!.getElementsByClassName("pdfViewer-dragAnnotationBox");
-                if (marquees && marquees.length) { // make a copy of the marquee
+                if (marquees && marquees.length) { // copy the marquee and convert it to a permanent annotation. 
+                    let style = (marquees[0] as HTMLDivElement).style;
                     let copy = document.createElement("div");
-                    let marquee = marquees[0] as HTMLDivElement;
-                    let style = marquee.style;
                     copy.style.left = style.left;
                     copy.style.top = style.top;
                     copy.style.width = style.width;
                     copy.style.height = style.height;
                     copy.style.border = style.border;
                     copy.style.opacity = style.opacity;
-                    copy.className = "pdfPage-annotationBox";
+                    (copy as any).marqueeing = true;
+                    copy.className = "pdfViewer-annotationBox";
                     this.createAnnotation(copy, this.getPageFromScroll(this._marqueeY));
-                    marquee.style.opacity = "0";
                 }
 
                 if (!e.ctrlKey) {
@@ -518,8 +499,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 }
                 PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
             }
-
-            this._marqueeHeight = this._marqueeWidth = 0;
+            this._marqueeing = false;
         }
         else {
             let sel = window.getSelection();
@@ -531,7 +511,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
 
         if (PDFMenu.Instance.Highlighting) {
-            this.highlight("goldenrod");
+            this.highlight("rgba(245, 230, 95, 0.616)");  // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
         }
         else {
             PDFMenu.Instance.StartDrag = this.startDrag;
@@ -545,7 +525,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     highlight = (color: string) => {
         // creates annotation documents for current highlights
         let annotationDoc = this.makeAnnotationDocument(color);
-        Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+        annotationDoc && Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
         return annotationDoc;
     }
 
@@ -558,16 +538,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
         e.preventDefault();
         e.stopPropagation();
         let targetDoc = Docs.Create.TextDocument({ width: 200, height: 200, title: "Note linked to " + this.props.Document.title });
-        let annotationDoc = this.highlight("red");
-        let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
-        DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
-            handlers: {
-                dragComplete: () => !(dragData as any).linkedToDoc &&
-                    DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF")
-
-            },
-            hideSource: false
-        });
+        const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)");
+        if (annotationDoc) {
+            let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
+            DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
+                handlers: {
+                    dragComplete: () => !(dragData as any).linkedToDoc &&
+                        DocUtils.MakeLink({ doc: annotationDoc }, { doc: dragData.dropDocument, ctx: dragData.targetContext }, `Annotation from ${StrCast(this.props.Document.title)}`, "link from PDF")
+
+                },
+                hideSource: false
+            });
+        }
     }
 
     createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
@@ -609,7 +591,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return true;
     }
     scrollXf = () => {
-        return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this.scrollTop) : this.props.ScreenToLocalTransform();
+        return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._scrollTop) : this.props.ScreenToLocalTransform();
     }
     setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => {
         this._setPreviewCursor = func;
@@ -647,9 +629,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
     onZoomWheel = (e: React.WheelEvent) => {
         e.stopPropagation();
         if (e.ctrlKey) {
-            let curScale = Number(this.pdfViewer.currentScaleValue);
-            this.pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
-            this._zoomed = Number(this.pdfViewer.currentScaleValue);
+            let curScale = Number(this._pdfViewer.currentScaleValue);
+            this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale + curScale * e.deltaY / 1000));
+            this._zoomed = Number(this._pdfViewer.currentScaleValue);
         }
     }
 
@@ -657,29 +639,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
             {this.nonDocAnnotations.sort((a, b) => NumCast(a.y) - NumCast(b.y)).map((anno, index) =>
                 <Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)}
-        </div>;
-    }
-    @computed get pdfViewerDiv() {
-        return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
-    }
-    @computed get standinViews() {
-        return <>
-            {this._showCover ? this.getCoverImage() : (null)}
-            {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
-        </>;
-    }
-    marqueeWidth = () => this._marqueeWidth;
-    marqueeHeight = () => this._marqueeHeight;
-    marqueeX = () => this._marqueeX;
-    marqueeY = () => this._marqueeY;
-    marqueeing = () => this._marqueeing;
-    visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
-    render() {
-        return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
-            {this.pdfViewerDiv}
-            <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
-            {this.annotationLayer}
-            <div className="pdfViewer-overlay" style={{ transform: `scale(${this._zoomed})` }}>
+            <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
                 <CollectionFreeFormView {...this.props}
                     setPreviewCursor={this.setPreviewCursor}
                     PanelHeight={() => NumCast(this.props.Document.scrollHeight, NumCast(this.props.Document.nativeHeight))}
@@ -702,7 +662,29 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     chromeCollapsed={true}>
                 </CollectionFreeFormView>
             </div>
+        </div>;
+    }
+    @computed get pdfViewerDiv() {
+        return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
+    }
+    @computed get standinViews() {
+        return <>
+            {this._showCover ? this.getCoverImage() : (null)}
+            {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
+        </>;
+    }
+    marqueeWidth = () => this._marqueeWidth;
+    marqueeHeight = () => this._marqueeHeight;
+    marqueeX = () => this._marqueeX;
+    marqueeY = () => this._marqueeY;
+    marqueeing = () => this._marqueeing;
+    visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
+    render() {
+        return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")} onScroll={this.onScroll} onWheel={this.onZoomWheel} onPointerDown={this.onPointerDown} onClick={this.onClick} ref={this._mainCont}>
+            {this.pdfViewerDiv}
+            {this.annotationLayer}
             {this.standinViews}
+            <PdfViewerMarquee isMarqueeing={this.marqueeing} width={this.marqueeWidth} height={this.marqueeHeight} x={this.marqueeX} y={this.marqueeY} />
         </div >);
     }
 }
@@ -722,11 +704,9 @@ class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
             style={{
                 left: `${this.props.x()}px`, top: `${this.props.y()}px`,
                 width: `${this.props.width()}px`, height: `${this.props.height()}px`,
-                border: `${this.props.width() === 0 ? "" : "2px dashed black"}`
+                border: `${this.props.width() === 0 ? "" : "2px dashed black"}`,
+                opacity: 0.2
             }}>
         </div>;
     }
-}
-
-
-export enum AnnotationTypes { Region }
+}
\ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 6acc6e1ca..7e37eba84 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -685,7 +685,7 @@ export namespace Doc {
         document.removeEventListener("pointerdown", linkFollowUnhighlight);
         document.addEventListener("pointerdown", linkFollowUnhighlight);
         let x = dt = Date.now();
-        window.setTimeout(() => dt == x && linkFollowUnhighlight(), 5000);
+        window.setTimeout(() => dt === x && linkFollowUnhighlight(), 5000);
     }
 
     export class HighlightBrush {
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index b2509a4f1..0fbfbf2f3 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -112,7 +112,7 @@ export class CurrentUserUtils {
             if (sidebar) {
                 sidebar.backgroundColor = "lightgrey";
             }
-        })
+        });
 
         if (doc.overlays === undefined) {
             const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
-- 
cgit v1.2.3-70-g09d2

