From 187a411024668a46e7a80022d3d549118b81abbc Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 27 Aug 2019 12:49:04 -0400
Subject: can push links to google docs

---
 src/client/views/nodes/FormattedTextBox.tsx | 51 +++++++++++++++--------------
 1 file changed, 26 insertions(+), 25 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d6ba1700a..02bee2f82 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -12,7 +12,7 @@ import { DateField } from '../../../new_fields/DateField';
 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 { RichTextField } from "../../../new_fields/RichTextField";
 import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
 import { Utils } from '../../../Utils';
@@ -37,12 +37,11 @@ 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';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
 
-export const Blank = `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
-
 export interface FormattedTextBoxProps {
     isOverlay?: boolean;
     hideOnLeave?: boolean;
@@ -61,7 +60,7 @@ export const GoogleRef = "googleDocId";
 type RichTextDocument = makeInterface<[typeof richTextSchema]>;
 const RichTextDocument = makeInterface(richTextSchema);
 
-type PullHandler = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => void;
+type PullHandler = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => void;
 
 @observer
 export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
@@ -363,7 +362,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._reactionDisposer = reaction(
             () => {
                 const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
-                return field ? field.Data : Blank;
+                return field ? field.Data : RichTextUtils.Initialize();
             },
             incomingValue => {
                 if (this._editorView && !this._applyingChange) {
@@ -431,7 +430,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     pushToGoogleDoc = async () => {
-        this.pullFromGoogleDoc(async (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+        this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
             let modes = GoogleApiClientUtils.WriteMode;
             let mode = modes.Replace;
             let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
@@ -440,9 +439,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
             }
             let redo = async () => {
-                let data = Cast(this.dataDoc.data, RichTextField);
-                if (this._editorView && reference && data) {
-                    let content = data[ToPlainText]();
+                if (this._editorView && reference) {
+                    let content = RichTextUtils.GoogleDocs.Convert(this._editorView.state);
                     let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
                     response && (this.dataDoc[GoogleRef] = response.documentId);
                     let pushSuccess = response !== undefined && !("errors" in response);
@@ -451,7 +449,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }
             };
             let undo = () => {
-                let content = exportState.body;
+                if (!exportState) {
+                    return;
+                }
+                let content = {
+                    text: exportState.body,
+                    links: []
+                };
                 if (reference && content) {
                     GoogleApiClientUtils.Docs.write({ reference, content, mode });
                 }
@@ -464,20 +468,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     pullFromGoogleDoc = async (handler: PullHandler) => {
         let dataDoc = this.dataDoc;
         let documentId = StrCast(dataDoc[GoogleRef]);
-        let exportState: GoogleApiClientUtils.ReadResult = {};
+        let exportState: Opt<GoogleApiClientUtils.ReadResult>;
         if (documentId) {
             exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
         }
         UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
     }
 
-    updateState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
+    updateState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
         let pullSuccess = false;
         if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
             const data = Cast(dataDoc.data, RichTextField);
             if (data instanceof RichTextField) {
                 pullSuccess = true;
-                dataDoc.data = new RichTextField(data[FromPlainText](exportState.body));
+                dataDoc.data = RichTextUtils.Synthesize(exportState.body, data);
                 setTimeout(() => {
                     if (this._editorView) {
                         let state = this._editorView.state;
@@ -495,18 +499,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         DocumentDecorations.Instance.startPullOutcome(pullSuccess);
     }
 
-    checkState = (exportState: GoogleApiClientUtils.ReadResult, dataDoc: Doc) => {
-        if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
-            let data = Cast(dataDoc.data, RichTextField);
-            if (data) {
-                let storedPlainText = data[ToPlainText]() + "\n";
-                let receivedPlainText = exportState.body;
-                let storedTitle = dataDoc.title;
-                let receivedTitle = exportState.title;
-                let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
-                dataDoc.unchanged = unchanged;
-                DocumentDecorations.Instance.setPullState(unchanged);
-            }
+    checkState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
+        if (exportState && this._editorView) {
+            let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n";
+            let receivedPlainText = exportState.body;
+            let storedTitle = dataDoc.title;
+            let receivedTitle = exportState.title;
+            let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
+            dataDoc.unchanged = unchanged;
+            DocumentDecorations.Instance.setPullState(unchanged);
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 916aa5377a9f105cd128264f46c83b987861c713 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 28 Aug 2019 17:45:44 -0400
Subject: separated docs and other apis, beginning less hacky import

---
 .../apis/google_docs/GoogleApiClientUtils.ts       | 204 ++++++++-------------
 src/client/views/nodes/FormattedTextBox.tsx        |  35 ++--
 src/new_fields/RichTextUtils.ts                    |  56 +++++-
 3 files changed, 146 insertions(+), 149 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 689009254..ae7c2f997 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -9,108 +9,104 @@ export const Pushes = "googleDocsPushCount";
 
 export namespace GoogleApiClientUtils {
 
-    export enum Service {
-        Documents = "Documents",
-        Slides = "Slides"
-    }
-
     export enum Actions {
         Create = "create",
         Retrieve = "retrieve",
         Update = "update"
     }
 
-    export enum WriteMode {
-        Insert,
-        Replace
-    }
-
-    export type Identifier = string;
-    export type Reference = Identifier | CreateOptions;
-    export interface Content {
-        text: string | string[];
-        links: docs_v1.Schema$Request[];
-    }
-    export type IdHandler = (id: Identifier) => any;
-    export type CreationResult = Opt<Identifier>;
-    export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
-    export type ReadResult = { title: string, body: string };
+    export namespace Docs {
 
-    export interface CreateOptions {
-        service: Service;
-        title?: string; // if excluded, will use a default title annotated with the current date
-    }
+        export type RetrievalResult = Opt<docs_v1.Schema$Document>;
+        export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
 
-    export interface RetrieveOptions {
-        service: Service;
-        identifier: Identifier;
-    }
+        export interface UpdateOptions {
+            documentId: DocumentId;
+            requests: docs_v1.Schema$Request[];
+        }
 
-    export interface ReadOptions {
-        identifier: Identifier;
-        removeNewlines?: boolean;
-    }
+        export enum WriteMode {
+            Insert,
+            Replace
+        }
 
-    export interface WriteOptions {
-        mode: WriteMode;
-        content: Content;
-        reference: Reference;
-        index?: number; // if excluded, will compute the last index of the document and append the content there
-    }
+        export type DocumentId = string;
+        export type Reference = DocumentId | CreateOptions;
+        export interface Content {
+            text: string | string[];
+            requests: docs_v1.Schema$Request[];
+        }
+        export type IdHandler = (id: DocumentId) => any;
+        export type CreationResult = Opt<DocumentId>;
+        export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
+        export type ReadResult = { title: string, body: string };
 
-    /**
-    * After following the authentication routine, which connects this API call to the current signed in account
-    * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
-    * should appear in the user's Google Doc library instantaneously.
-    * 
-    * @param options the title to assign to the new document, and the information necessary
-    * to store the new documentId returned from the creation process
-    * @returns the documentId of the newly generated document, or undefined if the creation process fails.
-    */
-    export const create = async (options: CreateOptions): Promise<CreationResult> => {
-        const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Create}`;
-        const parameters = {
-            requestBody: {
-                title: options.title || `Dash Export (${new Date().toDateString()})`
-            }
-        };
-        try {
-            const schema: any = await PostToServer(path, parameters);
-            let key = ["document", "presentation"].find(prefix => `${prefix}Id` in schema) + "Id";
-            return schema[key];
-        } catch {
-            return undefined;
+        export interface CreateOptions {
+            title?: string; // if excluded, will use a default title annotated with the current date
         }
-    };
 
-    export namespace Docs {
+        export interface RetrieveOptions {
+            documentId: DocumentId;
+        }
 
-        export type RetrievalResult = Opt<docs_v1.Schema$Document | slides_v1.Schema$Presentation>;
-        export type UpdateResult = Opt<docs_v1.Schema$BatchUpdateDocumentResponse>;
+        export interface ReadOptions {
+            documentId: DocumentId;
+            removeNewlines?: boolean;
+        }
 
-        export interface UpdateOptions {
-            documentId: Identifier;
-            requests: docs_v1.Schema$Request[];
+        export interface WriteOptions {
+            mode: WriteMode;
+            content: Content;
+            reference: Reference;
+            index?: number; // if excluded, will compute the last index of the document and append the content there
         }
 
+        /**
+        * After following the authentication routine, which connects this API call to the current signed in account
+        * and grants the appropriate permissions, this function programmatically creates an arbitrary Google Doc which
+        * should appear in the user's Google Doc library instantaneously.
+        * 
+        * @param options the title to assign to the new document, and the information necessary
+        * to store the new documentId returned from the creation process
+        * @returns the documentId of the newly generated document, or undefined if the creation process fails.
+        */
+        export const create = async (options: CreateOptions): Promise<CreationResult> => {
+            const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+            const parameters = {
+                requestBody: {
+                    title: options.title || `Dash Export (${new Date().toDateString()})`
+                }
+            };
+            try {
+                const schema: docs_v1.Schema$Document = await PostToServer(path, parameters);
+                return schema.documentId;
+            } catch {
+                return undefined;
+            }
+        };
+
         export namespace Utils {
 
             export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
-                const fragments: string[] = [];
+                let runs = extractTextRuns(document);
+                const text = runs.map(run => run.content).join("");
+                return removeNewlines ? text.ReplaceAll("\n", "") : text;
+            };
+
+            export const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => {
+                const fragments: docs_v1.Schema$TextRun[] = [];
                 if (document.body && document.body.content) {
                     for (const element of document.body.content) {
                         if (element.paragraph && element.paragraph.elements) {
                             for (const inner of element.paragraph.elements) {
                                 if (inner && inner.textRun) {
-                                    const fragment = inner.textRun.content;
-                                    fragment && fragments.push(fragment);
+                                    fragments.push(inner.textRun);
                                 }
                             }
                         }
                     }
                 }
-                const text = fragments.join("");
-                return removeNewlines ? text.ReplaceAll("\n", "") : text;
+                return filterEmpty ? fragments.filter(run => run.content) : fragments;
             };
 
             export const endOf = (schema: docs_v1.Schema$Document): number | undefined => {
@@ -133,27 +129,19 @@ export namespace GoogleApiClientUtils {
 
         }
 
-        const KeyMapping = new Map<Service, string>([
-            [Service.Documents, "documentId"],
-            [Service.Slides, "presentationId"]
-        ]);
-
         export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
-            const path = `${RouteStore.googleDocs}/${options.service}/${Actions.Retrieve}`;
+            const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
             try {
-                let parameters: any = {}, key: string | undefined;
-                if ((key = KeyMapping.get(options.service))) {
-                    parameters[key] = options.identifier;
-                    const schema: RetrievalResult = await PostToServer(path, parameters);
-                    return schema;
-                }
+                const parameters = { documentId: options.documentId };
+                const schema: RetrievalResult = await PostToServer(path, parameters);
+                return schema;
             } catch {
                 return undefined;
             }
         };
 
         export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
-            const path = `${RouteStore.googleDocs}/${Service.Documents}/${Actions.Update}`;
+            const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
             const parameters = {
                 documentId: options.documentId,
                 requestBody: {
@@ -169,7 +157,7 @@ export namespace GoogleApiClientUtils {
         };
 
         export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
-            return retrieve({ ...options, service: Service.Documents }).then(document => {
+            return retrieve({ documentId: options.documentId }).then(document => {
                 if (document) {
                     let title = document.title!;
                     let body = Utils.extractText(document, options.removeNewlines);
@@ -179,7 +167,7 @@ export namespace GoogleApiClientUtils {
         };
 
         export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
-            return retrieve({ ...options, service: Service.Documents }).then(document => {
+            return retrieve({ documentId: options.documentId }).then(document => {
                 if (document) {
                     let title = document.title;
                     let bodyLines = Utils.extractText(document).split("\n");
@@ -203,14 +191,14 @@ export namespace GoogleApiClientUtils {
 
         export const write = async (options: WriteOptions): Promise<UpdateResult> => {
             const requests: docs_v1.Schema$Request[] = [];
-            const identifier = await Utils.initialize(options.reference);
-            if (!identifier) {
+            const documentId = await Utils.initialize(options.reference);
+            if (!documentId) {
                 return undefined;
             }
             let index = options.index;
             const mode = options.mode;
             if (!(index && mode === WriteMode.Insert)) {
-                let schema = await retrieve({ identifier, service: Service.Documents });
+                let schema = await retrieve({ documentId });
                 if (!schema || !(index = Utils.endOf(schema))) {
                     return undefined;
                 }
@@ -236,8 +224,8 @@ export namespace GoogleApiClientUtils {
             if (!requests.length) {
                 return undefined;
             }
-            requests.push(...options.content.links);
-            let replies: any = await update({ documentId: identifier, requests });
+            requests.push(...options.content.requests);
+            let replies: any = await update({ documentId: documentId, requests });
             if ("errors" in replies) {
                 console.log("Write operation failed:");
                 console.log(replies.errors.map((error: any) => error.message));
@@ -247,36 +235,4 @@ export namespace GoogleApiClientUtils {
 
     }
 
-    export namespace Slides {
-
-        export namespace Utils {
-
-            export const extractTextBoxes = (slides: slides_v1.Schema$Page[]) => {
-                slides.map(slide => {
-                    let elements = slide.pageElements;
-                    if (elements) {
-                        let textboxes: slides_v1.Schema$TextContent[] = [];
-                        for (let element of elements) {
-                            if (element && element.shape && element.shape.shapeType === "TEXT_BOX" && element.shape.text) {
-                                textboxes.push(element.shape.text);
-                            }
-                        }
-                        textboxes.map(text => {
-                            if (text.textElements) {
-                                text.textElements.map(element => {
-
-                                });
-                            }
-                            if (text.lists) {
-
-                            }
-                        });
-                    }
-                });
-            };
-
-        }
-
-    }
-
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 02bee2f82..eefac2285 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -60,14 +60,18 @@ export const GoogleRef = "googleDocId";
 type RichTextDocument = makeInterface<[typeof richTextSchema]>;
 const RichTextDocument = makeInterface(richTextSchema);
 
-type PullHandler = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => void;
+type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => void;
 
 @observer
 export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
     public static LayoutString(fieldStr: string = "data") {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
+    public static blankState = () => {
+        return EditorState.create(FormattedTextBox.Instance._configuration);
+    }
     public static Instance: FormattedTextBox;
+    private _configuration: any;
     private _ref: React.RefObject<HTMLDivElement>;
     private _proseRef?: HTMLDivElement;
     private _editorView: Opt<EditorView>;
@@ -325,7 +329,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     componentDidMount() {
-        const config = {
+        this._configuration = {
             schema,
             inpRules, //these currently don't do anything, but could eventually be helpful
             plugins: this.props.isOverlay ? [
@@ -367,7 +371,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             incomingValue => {
                 if (this._editorView && !this._applyingChange) {
                     let updatedState = JSON.parse(incomingValue);
-                    this._editorView.updateState(EditorState.fromJSON(config, updatedState));
+                    this._editorView.updateState(EditorState.fromJSON(this._configuration, updatedState));
                     this.tryUpdateHeight();
                 }
             }
@@ -409,7 +413,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     this.dataDoc.lastModified = undefined;
                 }
             }, { fireImmediately: true });
-        this.setupEditor(config, this.dataDoc, this.props.fieldKey);
+        this.setupEditor(this._configuration, this.dataDoc, this.props.fieldKey);
 
         this._searchReactionDisposer = reaction(() => {
             return StrCast(this.props.Document.search_string);
@@ -430,17 +434,17 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     pushToGoogleDoc = async () => {
-        this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
-            let modes = GoogleApiClientUtils.WriteMode;
+        this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
+            let modes = GoogleApiClientUtils.Docs.WriteMode;
             let mode = modes.Replace;
-            let reference: Opt<GoogleApiClientUtils.Reference> = Cast(this.dataDoc[GoogleRef], "string");
+            let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
             if (!reference) {
                 mode = modes.Insert;
-                reference = { service: GoogleApiClientUtils.Service.Documents, title: StrCast(this.dataDoc.title) };
+                reference = { title: StrCast(this.dataDoc.title) };
             }
             let redo = async () => {
                 if (this._editorView && reference) {
-                    let content = RichTextUtils.GoogleDocs.Convert(this._editorView.state);
+                    let content = RichTextUtils.GoogleDocs.Export(this._editorView.state);
                     let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
                     response && (this.dataDoc[GoogleRef] = response.documentId);
                     let pushSuccess = response !== undefined && !("errors" in response);
@@ -452,9 +456,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 if (!exportState) {
                     return;
                 }
-                let content = {
+                let content: GoogleApiClientUtils.Docs.Content = {
                     text: exportState.body,
-                    links: []
+                    requests: []
                 };
                 if (reference && content) {
                     GoogleApiClientUtils.Docs.write({ reference, content, mode });
@@ -468,14 +472,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     pullFromGoogleDoc = async (handler: PullHandler) => {
         let dataDoc = this.dataDoc;
         let documentId = StrCast(dataDoc[GoogleRef]);
-        let exportState: Opt<GoogleApiClientUtils.ReadResult>;
+        let test = await RichTextUtils.GoogleDocs.Import(documentId);
+        let exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>;
         if (documentId) {
-            exportState = await GoogleApiClientUtils.Docs.read({ identifier: documentId });
+            exportState = await GoogleApiClientUtils.Docs.read({ documentId });
         }
         UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
     }
 
-    updateState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
+    updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
         let pullSuccess = false;
         if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
             const data = Cast(dataDoc.data, RichTextField);
@@ -499,7 +504,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         DocumentDecorations.Instance.startPullOutcome(pullSuccess);
     }
 
-    checkState = (exportState: Opt<GoogleApiClientUtils.ReadResult>, dataDoc: Doc) => {
+    checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
         if (exportState && this._editorView) {
             let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n";
             let receivedPlainText = exportState.body;
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index b2b1dbaee..189819591 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -3,6 +3,8 @@ import { Node } from "prosemirror-model";
 import { RichTextField } from "./RichTextField";
 import { docs_v1 } from "googleapis";
 import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
+import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
+import { Opt } from "./Doc";
 
 export namespace RichTextUtils {
 
@@ -81,34 +83,68 @@ export namespace RichTextUtils {
 
     export namespace GoogleDocs {
 
-        export const Convert = (state: EditorState): GoogleApiClientUtils.Content => {
+        export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => {
             let textNodes: Node<any>[] = [];
             let text = ToPlainText(state);
             let content = state.doc.content;
             content.forEach(node => node.content.forEach(node => node.type.name === "text" && textNodes.push(node)));
+            let linkRequests = ExtractLinks(textNodes);
+            return {
+                text,
+                requests: [...linkRequests]
+            };
+        };
+
+        export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId) => {
+            let document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
+            if (!document) {
+                return;
+            }
+            // let title = document.title!;
+            let runs = GoogleApiClientUtils.Docs.Utils.extractTextRuns(document);
+            let state = FormattedTextBox.blankState();
+            let from = 0;
+            runs.map(run => {
+                let text = run.content!;
+                state = state.apply(state.tr.insertText(text, from));
+                let to = from + text.length + 1;
+                let href: Opt<string>;
+                if (run.textStyle && run.textStyle.link && (href = run.textStyle.link.url)) {
+                    let mark = state.schema.mark(state.schema.marks.link, { href });
+                    state = state.apply(state.tr.addMark(from, to, mark));
+                }
+                from = to;
+            });
+            // return { title, body };
+        };
+
+        interface LinkInformation {
+            startIndex: number;
+            endIndex: number;
+            bold: boolean;
+            url: string;
+        }
+
+        const ExtractLinks = (nodes: Node<any>[]) => {
             let links: docs_v1.Schema$Request[] = [];
             let position = 1;
-            for (let node of textNodes) {
+            for (let node of nodes) {
                 let link, length = node.nodeSize;
                 let marks = node.marks;
                 if (marks.length && (link = marks.find(mark => mark.type.name === "link"))) {
-                    links.push(encode({
+                    links.push(Encode({
                         startIndex: position,
                         endIndex: position + length,
                         url: link.attrs.href,
+                        bold: false
                     }));
                 }
                 position += length;
             }
-            return { text, links };
+            return links;
         };
 
-        interface LinkInformation {
-            startIndex: number;
-            endIndex: number;
-            url: string;
-        }
-        const encode = (information: LinkInformation) => {
+        const Encode = (information: LinkInformation) => {
             return {
                 updateTextStyle: {
                     fields: "*",
-- 
cgit v1.2.3-70-g09d2


From c53d721dd95bdb4ccddc7a89dbdda72a88d29058 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 30 Aug 2019 13:43:04 -0400
Subject: model sharing

---
 .../apis/google_docs/GoogleApiClientUtils.ts       | 22 ++++--
 src/client/northstar/utils/Extensions.ts           |  2 +
 src/client/views/DocumentDecorations.scss          | 31 +++++++-
 src/client/views/DocumentDecorations.tsx           |  7 +-
 src/client/views/Main.tsx                          | 20 ++++++
 src/client/views/nodes/FormattedTextBox.tsx        | 50 ++++++-------
 src/new_fields/RichTextUtils.ts                    | 82 +++++++++++++++++-----
 7 files changed, 159 insertions(+), 55 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index ae7c2f997..9bf3cae38 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -3,6 +3,8 @@ import { PostToServer } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { Opt } from "../../../new_fields/Doc";
 import { isArray } from "util";
+import { EditorState } from "prosemirror-state";
+import { RichTextField } from "../../../new_fields/RichTextField";
 
 export const Pulls = "googleDocsPullCount";
 export const Pushes = "googleDocsPushCount";
@@ -40,6 +42,11 @@ export namespace GoogleApiClientUtils {
         export type CreationResult = Opt<DocumentId>;
         export type ReadLinesResult = Opt<{ title?: string, bodyLines?: string[] }>;
         export type ReadResult = { title: string, body: string };
+        export interface ImportResult {
+            title: string;
+            text: string;
+            data: RichTextField;
+        }
 
         export interface CreateOptions {
             title?: string; // if excluded, will use a default title annotated with the current date
@@ -87,13 +94,16 @@ export namespace GoogleApiClientUtils {
 
         export namespace Utils {
 
-            export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): string => {
+            export type ExtractResult = { text: string, runs: docs_v1.Schema$TextRun[] };
+            export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
                 let runs = extractTextRuns(document);
-                const text = runs.map(run => run.content).join("");
-                return removeNewlines ? text.ReplaceAll("\n", "") : text;
+                let text = runs.map(run => run.content).join("");
+                text = text.substring(0, text.length - 1);
+                removeNewlines && text.ReplaceAll("\n", "");
+                return { text, runs };
             };
 
-            export const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => {
+            const extractTextRuns = (document: docs_v1.Schema$Document, filterEmpty = true) => {
                 const fragments: docs_v1.Schema$TextRun[] = [];
                 if (document.body && document.body.content) {
                     for (const element of document.body.content) {
@@ -160,7 +170,7 @@ export namespace GoogleApiClientUtils {
             return retrieve({ documentId: options.documentId }).then(document => {
                 if (document) {
                     let title = document.title!;
-                    let body = Utils.extractText(document, options.removeNewlines);
+                    let body = Utils.extractText(document, options.removeNewlines).text;
                     return { title, body };
                 }
             });
@@ -170,7 +180,7 @@ export namespace GoogleApiClientUtils {
             return retrieve({ documentId: options.documentId }).then(document => {
                 if (document) {
                     let title = document.title;
-                    let bodyLines = Utils.extractText(document).split("\n");
+                    let bodyLines = Utils.extractText(document).text.split("\n");
                     options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
                     return { title, bodyLines };
                 }
diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
index df14d4da0..ab9384f1f 100644
--- a/src/client/northstar/utils/Extensions.ts
+++ b/src/client/northstar/utils/Extensions.ts
@@ -1,6 +1,8 @@
 interface String {
     ReplaceAll(toReplace: string, replacement: string): string;
     Truncate(length: number, replacement: string): String;
+    removeTrailingNewlines(): string;
+    hasNewline(): boolean;
 }
 
 String.prototype.ReplaceAll = function (toReplace: string, replacement: string): string {
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index ac8497bd0..470365627 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -266,6 +266,31 @@ $linkGap : 3px;
     }
 }
 
-@-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
+@-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);
+    }
+}
+
+@keyframes shadow-pulse {
+    0% {
+        box-shadow: 0 0 0 0px rgba(0, 0, 0, 0.2);
+    }
+
+    100% {
+        box-shadow: 0 0 0 35px rgba(0, 0, 0, 0);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 579806a89..109228b54 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -82,6 +82,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @observable public pullIcon: IconProp = "arrow-alt-circle-down";
     @observable public pullColor: string = "white";
     @observable public isAnimatingFetch = false;
+    @observable public isAnimatingPulse = false;
     @observable public openHover = false;
     public pullColorAnimating = false;
 
@@ -102,6 +103,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     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";
@@ -698,9 +700,12 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return (
             <div className={"linkButtonWrapper"}>
                 <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
+                    if (!published) {
+                        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>
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 0e687737d..271326f70 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -8,6 +8,26 @@ import { Doc, DocListCastAsync } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
 import { DocServer } from "../DocServer";
 
+String.prototype.removeTrailingNewlines = function () {
+    let sliced = this;
+    while (sliced.endsWith("\n")) {
+        sliced = sliced.substring(0, this.length - 1);
+    }
+    return sliced as string;
+};
+
+String.prototype.hasNewline = function () {
+    return this.endsWith("\n");
+};
+
+(Array.prototype as any).lastElement = function (this: any[]) {
+    if (!this.length) {
+        return undefined;
+    }
+    return this[this.length - 1];
+};
+
+
 let swapDocs = async () => {
     let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
     // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index eefac2285..83011e590 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -60,16 +60,14 @@ export const GoogleRef = "googleDocId";
 type RichTextDocument = makeInterface<[typeof richTextSchema]>;
 const RichTextDocument = makeInterface(richTextSchema);
 
-type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => void;
+type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
 
 @observer
 export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
     public static LayoutString(fieldStr: string = "data") {
         return FieldView.LayoutString(FormattedTextBox, fieldStr);
     }
-    public static blankState = () => {
-        return EditorState.create(FormattedTextBox.Instance._configuration);
-    }
+    public static blankState = () => EditorState.create(FormattedTextBox.Instance._configuration);
     public static Instance: FormattedTextBox;
     private _configuration: any;
     private _ref: React.RefObject<HTMLDivElement>;
@@ -434,7 +432,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     pushToGoogleDoc = async () => {
-        this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
+        this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
             let modes = GoogleApiClientUtils.Docs.WriteMode;
             let mode = modes.Replace;
             let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string");
@@ -457,7 +455,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     return;
                 }
                 let content: GoogleApiClientUtils.Docs.Content = {
-                    text: exportState.body,
+                    text: exportState.text,
                     requests: []
                 };
                 if (reference && content) {
@@ -472,42 +470,38 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     pullFromGoogleDoc = async (handler: PullHandler) => {
         let dataDoc = this.dataDoc;
         let documentId = StrCast(dataDoc[GoogleRef]);
-        let test = await RichTextUtils.GoogleDocs.Import(documentId);
-        let exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>;
+        let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
         if (documentId) {
-            exportState = await GoogleApiClientUtils.Docs.read({ documentId });
+            exportState = await RichTextUtils.GoogleDocs.Import(documentId);
         }
         UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
     }
 
-    updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
+    updateState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
         let pullSuccess = false;
-        if (exportState !== undefined && exportState.body !== undefined && exportState.title !== undefined) {
-            const data = Cast(dataDoc.data, RichTextField);
-            if (data instanceof RichTextField) {
-                pullSuccess = true;
-                dataDoc.data = RichTextUtils.Synthesize(exportState.body, data);
-                setTimeout(() => {
-                    if (this._editorView) {
-                        let state = this._editorView.state;
-                        let end = state.doc.content.size - 1;
-                        this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
-                    }
-                }, 0);
-                dataDoc.title = exportState.title;
-                this.Document.customTitle = true;
-                dataDoc.unchanged = true;
-            }
+        if (exportState !== undefined) {
+            pullSuccess = true;
+            dataDoc.data = exportState.data;
+            setTimeout(() => {
+                if (this._editorView) {
+                    let state = this._editorView.state;
+                    let end = state.doc.content.size - 1;
+                    this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+                }
+            }, 0);
+            dataDoc.title = exportState.title;
+            this.Document.customTitle = true;
+            dataDoc.unchanged = true;
         } else {
             delete dataDoc[GoogleRef];
         }
         DocumentDecorations.Instance.startPullOutcome(pullSuccess);
     }
 
-    checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ReadResult>, dataDoc: Doc) => {
+    checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
         if (exportState && this._editorView) {
             let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n";
-            let receivedPlainText = exportState.body;
+            let receivedPlainText = exportState.text;
             let storedTitle = dataDoc.title;
             let receivedTitle = exportState.title;
             let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 189819591..4ca51d311 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -1,5 +1,5 @@
 import { EditorState } from "prosemirror-state";
-import { Node } from "prosemirror-model";
+import { Node, Fragment, Mark } from "prosemirror-model";
 import { RichTextField } from "./RichTextField";
 import { docs_v1 } from "googleapis";
 import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
@@ -95,27 +95,75 @@ export namespace RichTextUtils {
             };
         };
 
-        export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId) => {
-            let document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
+        export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
+            let Docs = GoogleApiClientUtils.Docs;
+            let document = await Docs.retrieve({ documentId });
+
             if (!document) {
-                return;
+                return undefined;
             }
-            // let title = document.title!;
-            let runs = GoogleApiClientUtils.Docs.Utils.extractTextRuns(document);
+
+            let title = document.title!;
+
+            let { text, runs } = Docs.Utils.extractText(document);
+            let segments = runs[Symbol.iterator]();
+
             let state = FormattedTextBox.blankState();
+            let breaks: number[] = [];
             let from = 0;
-            runs.map(run => {
-                let text = run.content!;
-                state = state.apply(state.tr.insertText(text, from));
-                let to = from + text.length + 1;
-                let href: Opt<string>;
-                if (run.textStyle && run.textStyle.link && (href = run.textStyle.link.url)) {
-                    let mark = state.schema.mark(state.schema.marks.link, { href });
-                    state = state.apply(state.tr.addMark(from, to, mark));
+            let result = segments.next();
+            while (!result.done) {
+                let run = result.value;
+                let fragment = run.content!;
+                if (fragment.hasNewline()) {
+                    let trimmed = fragment.removeTrailingNewlines();
+                    if (fragment.length === 1) {
+                        breaks.push(from);
+                    } else {
+                        let content = Fragment.from(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle)));
+                        let node = state.schema.node("paragraph", null, content);
+                        state = state.apply(state.tr.insert(from, node));
+                        from += node.nodeSize;
+                    }
+                    result = segments.next();
+                } else {
+                    let nodes: Node[] = [];
+                    nodes.push(state.schema.text(fragment, styleToMarks(state.schema, run.textStyle)));
+                    result = segments.next();
+                    while (!result.done) {
+                        run = result.value;
+                        fragment = run.content!;
+                        let trimmed = fragment.removeTrailingNewlines();
+                        nodes.push(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle)));
+                        if (fragment.hasNewline()) {
+                            let node = state.schema.node("paragraph", null, Fragment.fromArray(nodes));
+                            state = state.apply(state.tr.insert(from, node));
+                            from += node.nodeSize;
+                            result = segments.next();
+                            break;
+                        }
+                        result = segments.next();
+                    }
+                    if (result.done) {
+                        break;
+                    }
                 }
-                from = to;
-            });
-            // return { title, body };
+            }
+            breaks.forEach(position => state = state.apply(state.tr.insert(position, state.schema.node("paragraph"))));
+            let data = new RichTextField(JSON.stringify(state.toJSON()));
+            return { title, text, data };
+        };
+
+        const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => {
+            if (!textStyle) {
+                return undefined;
+            }
+            let marks: Mark[] = [];
+            if (textStyle.link) {
+                let href = textStyle.link.url;
+                marks.push(schema.mark(schema.marks.link, { href }));
+            }
+            return marks;
         };
 
         interface LinkInformation {
-- 
cgit v1.2.3-70-g09d2


From c9a7d747916c31730f71479d8e516ba0ed2c658f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 30 Aug 2019 15:51:13 -0400
Subject: complete transfer except for list items

---
 package.json                                       |  2 +
 .../apis/google_docs/GoogleApiClientUtils.ts       |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 11 ++-
 src/new_fields/RichTextUtils.ts                    | 89 +++++++++++++---------
 4 files changed, 59 insertions(+), 45 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/package.json b/package.json
index cd60b7b55..f98224469 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
         "@types/bluebird": "^3.5.25",
         "@types/body-parser": "^1.17.0",
         "@types/classnames": "^2.2.8",
+        "@types/color": "^3.0.0",
         "@types/connect-flash": "0.0.34",
         "@types/cookie-parser": "^1.4.1",
         "@types/cookie-session": "^2.0.36",
@@ -121,6 +122,7 @@
         "canvas": "^2.5.0",
         "child_process": "^1.0.2",
         "class-transformer": "^0.2.0",
+        "color": "^3.1.2",
         "connect-flash": "^0.1.1",
         "connect-mongo": "^2.0.3",
         "cookie-parser": "^1.4.4",
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 9bf3cae38..fdd708e31 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -45,7 +45,7 @@ export namespace GoogleApiClientUtils {
         export interface ImportResult {
             title: string;
             text: string;
-            data: RichTextField;
+            state: EditorState;
         }
 
         export interface CreateOptions {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 83011e590..6af5c8f0b 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 { RichTextUtils } from '../../../new_fields/RichTextUtils';
+import * as _ from "lodash";
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -481,7 +482,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let pullSuccess = false;
         if (exportState !== undefined) {
             pullSuccess = true;
-            dataDoc.data = exportState.data;
+            dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
             setTimeout(() => {
                 if (this._editorView) {
                     let state = this._editorView.state;
@@ -500,11 +501,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => {
         if (exportState && this._editorView) {
-            let storedPlainText = RichTextUtils.ToPlainText(this._editorView.state) + "\n";
-            let receivedPlainText = exportState.text;
-            let storedTitle = dataDoc.title;
-            let receivedTitle = exportState.title;
-            let unchanged = storedPlainText === receivedPlainText && storedTitle === receivedTitle;
+            let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc);
+            let equalTitles = dataDoc.title === exportState.title;
+            let unchanged = equalContent && equalTitles;
             dataDoc.unchanged = unchanged;
             DocumentDecorations.Instance.setPullState(unchanged);
         }
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 4ca51d311..bc338e45b 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -5,6 +5,7 @@ import { docs_v1 } from "googleapis";
 import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
 import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
 import { Opt } from "./Doc";
+import * as Color from "color";
 
 export namespace RichTextUtils {
 
@@ -96,73 +97,85 @@ export namespace RichTextUtils {
         };
 
         export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
-            let Docs = GoogleApiClientUtils.Docs;
-            let document = await Docs.retrieve({ documentId });
-
+            const Docs = GoogleApiClientUtils.Docs;
+            const document = await Docs.retrieve({ documentId });
             if (!document) {
                 return undefined;
             }
 
-            let title = document.title!;
-
-            let { text, runs } = Docs.Utils.extractText(document);
-            let segments = runs[Symbol.iterator]();
+            const title = document.title!;
+            const { text, runs } = Docs.Utils.extractText(document);
+            const segments = runs[Symbol.iterator]();
 
             let state = FormattedTextBox.blankState();
-            let breaks: number[] = [];
-            let from = 0;
+            const schema = state.schema;
+            const nodes: Node[] = [];
+
             let result = segments.next();
             while (!result.done) {
                 let run = result.value;
-                let fragment = run.content!;
-                if (fragment.hasNewline()) {
-                    let trimmed = fragment.removeTrailingNewlines();
-                    if (fragment.length === 1) {
-                        breaks.push(from);
-                    } else {
-                        let content = Fragment.from(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle)));
-                        let node = state.schema.node("paragraph", null, content);
-                        state = state.apply(state.tr.insert(from, node));
-                        from += node.nodeSize;
-                    }
+                if (run.content!.hasNewline()) {
+                    addParagraph(nodes, schema, textNode(schema, run));
                     result = segments.next();
                 } else {
-                    let nodes: Node[] = [];
-                    nodes.push(state.schema.text(fragment, styleToMarks(state.schema, run.textStyle)));
+                    const inner: Node[] = [];
+                    inner.push(textNode(schema, run));
                     result = segments.next();
                     while (!result.done) {
                         run = result.value;
-                        fragment = run.content!;
-                        let trimmed = fragment.removeTrailingNewlines();
-                        nodes.push(state.schema.text(trimmed, styleToMarks(state.schema, run.textStyle)));
-                        if (fragment.hasNewline()) {
-                            let node = state.schema.node("paragraph", null, Fragment.fromArray(nodes));
-                            state = state.apply(state.tr.insert(from, node));
-                            from += node.nodeSize;
-                            result = segments.next();
+                        inner.push(textNode(schema, run));
+                        result = segments.next();
+                        if (run.content!.hasNewline()) {
+                            addParagraph(nodes, schema, inner);
                             break;
                         }
-                        result = segments.next();
                     }
                     if (result.done) {
                         break;
                     }
                 }
             }
-            breaks.forEach(position => state = state.apply(state.tr.insert(position, state.schema.node("paragraph"))));
-            let data = new RichTextField(JSON.stringify(state.toJSON()));
-            return { title, text, data };
+            state = state.apply(state.tr.replaceWith(0, 2, nodes));
+            return { title, text, state };
+        };
+
+        const addParagraph = (list: Node[], schema: any, content?: Node[] | Node) => {
+            list.push(schema.node("paragraph", null, content ? Fragment.from(content) : null));
         };
 
+        const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
+            let text = run.content!.removeTrailingNewlines();
+            return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined;
+        };
+
+        const MarkMapping = new Map<keyof docs_v1.Schema$TextStyle, string>([
+            ["bold", "strong"],
+            ["italic", "em"],
+            ["foregroundColor", "pFontColor"]
+        ]);
+
         const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => {
             if (!textStyle) {
                 return undefined;
             }
             let marks: Mark[] = [];
-            if (textStyle.link) {
-                let href = textStyle.link.url;
-                marks.push(schema.mark(schema.marks.link, { href }));
-            }
+            Object.keys(textStyle).forEach(key => {
+                let value: any;
+                let targeted = key as keyof docs_v1.Schema$TextStyle;
+                if (value = textStyle[targeted]) {
+                    let attributes: any = {};
+                    let converted = MarkMapping.get(targeted) || targeted;
+
+                    value.url && (attributes.href = value.url);
+                    if (value.color) {
+                        let object: { [key: string]: number } = value.color.rgbColor;
+                        attributes.color = Color.rgb(Object.values(object).map(value => value * 255)).hex();
+                    }
+
+                    let mark = schema.mark(schema.marks[converted], attributes);
+                    mark && marks.push(mark);
+                }
+            });
             return marks;
         };
 
-- 
cgit v1.2.3-70-g09d2


From 5d59e9a379417140e10778cd43e8f87ecb816c37 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Fri, 6 Sep 2019 06:29:26 -0400
Subject: lightly tested functional export of any image doc (web url or stored
 in server public folder) to google photos, optionally stored in a given
 target album

---
 package.json                                       |   2 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  73 +++++-----
 src/client/views/MainView.tsx                      |  17 ++-
 src/client/views/nodes/FormattedTextBox.tsx        |   1 +
 src/server/RouteStore.ts                           |   2 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |  28 ++--
 src/server/apis/google/GooglePhotosServerUtils.ts  |  68 ----------
 src/server/apis/google/GooglePhotosUploadUtils.ts  | 122 +++++++++++++++--
 src/server/apis/google/typings/albums.ts           | 150 ---------------------
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  75 +++++------
 11 files changed, 206 insertions(+), 334 deletions(-)
 delete mode 100644 src/server/apis/google/GooglePhotosServerUtils.ts
 delete mode 100644 src/server/apis/google/typings/albums.ts

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/package.json b/package.json
index f0f2b467e..f56e34ce0 100644
--- a/package.json
+++ b/package.json
@@ -224,4 +224,4 @@
         "xoauth2": "^1.2.0",
         "youtube": "^0.1.0"
     }
-}
+}
\ No newline at end of file
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index b95cc98c9..2b72800a9 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,53 +1,42 @@
-import { Album } from "../../../server/apis/google/typings/albums";
-import { PostToServer } from "../../../Utils";
+import { PostToServer, Utils } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { ImageField } from "../../../new_fields/URLField";
+import { StrCast, Cast } from "../../../new_fields/Types";
+import { Doc, Opt } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import requestImageSize = require('../../util/request-image-size');
+import Photos = require('googlephotos');
 
 export namespace GooglePhotosClientUtils {
 
-    export const Create = async (title: string) => {
-        let parameters = {
-            action: Album.Action.Create,
-            body: { album: { title } }
-        } as Album.Create;
-        return PostToServer(RouteStore.googlePhotosQuery, parameters);
-    };
+    export type AlbumReference = { id: string } | { title: string };
+    export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text()));
 
-    export const List = async (options?: Partial<Album.ListOptions>) => {
-        let parameters = {
-            action: Album.Action.List,
-            parameters: {
-                pageSize: (options ? options.pageSize : 20) || 20,
-                pageToken: (options ? options.pageToken : undefined) || undefined,
-                excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false,
-            } as Album.ListOptions
-        } as Album.List;
-        return PostToServer(RouteStore.googlePhotosQuery, parameters);
-    };
+    export interface MediaInput {
+        description: string;
+        source: string;
+    }
 
-    export const Get = async (albumId: string) => {
-        let parameters = {
-            action: Album.Action.Get,
-            albumId
-        } as Album.Get;
-        return PostToServer(RouteStore.googlePhotosQuery, parameters);
-    };
-
-    export const toDataURL = (field: ImageField | undefined) => {
-        if (!field) {
-            return undefined;
+    export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => {
+        if (album && "title" in album) {
+            album = (await endpoint()).albums.create(album.title);
+        }
+        const media: MediaInput[] = [];
+        sources.forEach(document => {
+            const data = Cast(Doc.GetProto(document).data, ImageField);
+            const description = StrCast(document.caption);
+            if (!data) {
+                return undefined;
+            }
+            media.push({
+                source: data.url.href,
+                description,
+            } as MediaInput);
+        });
+        if (media.length) {
+            return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
         }
-        const image = document.createElement("img");
-        image.src = field.url.href;
-        image.width = 200;
-        image.height = 200;
-        const canvas = document.createElement("canvas");
-        canvas.width = image.width;
-        canvas.height = image.height;
-        const ctx = canvas.getContext("2d")!;
-        ctx.drawImage(image, 0, 0);
-        const dataUrl = canvas.toDataURL("image/png");
-        return dataUrl.replace(/^data:image\/(png|jpg);base64,/, "");
+        return undefined;
     };
 
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c1c95fc88..6d366216e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -40,11 +40,10 @@ import { PreviewCursor } from './PreviewCursor';
 import { FilterBox } from './search/FilterBox';
 import PresModeMenu from './presentationview/PresentationModeMenu';
 import { PresBox } from './nodes/PresBox';
-import { docs_v1 } from 'googleapis';
-import { Album } from '../../server/apis/google/typings/albums';
 import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../new_fields/URLField';
 import { LinkFollowBox } from './linking/LinkFollowBox';
+import { DocumentManager } from '../util/DocumentManager';
 
 @observer
 export class MainView extends React.Component {
@@ -131,10 +130,7 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
-        let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
-        let image = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" });
-        let parameters = { title: StrCast(image.title), MEDIA_BINARY_DATA: GooglePhotosClientUtils.toDataURL(Cast(image.data, ImageField)) };
-        // PostToServer(RouteStore.googlePhotosMediaUpload, parameters).then(console.log);
+        this.executeGooglePhotosRoutine();
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
@@ -153,6 +149,15 @@ export class MainView extends React.Component {
         }, { fireImmediately: true });
     }
 
+    executeGooglePhotosRoutine = async () => {
+        let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+        let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" });
+        doc.caption = "Well isn't this a nice cat image!";
+        let photos = await GooglePhotosClientUtils.endpoint();
+        let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id;
+        console.log(await GooglePhotosClientUtils.UploadMedia([doc], { id: albumId }));
+    }
+
     componentWillUnMount() {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         //close presentation 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index b671d06ea..fda9ea33f 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -133,6 +133,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (this.props.isOverlay) {
             DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
         }
+        FormattedTextBox.Instance = this;
     }
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index 3b3fd9b4a..b221b71bc 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -32,7 +32,7 @@ export enum RouteStore {
     // APIS
     cognitiveServices = "/cognitiveservices",
     googleDocs = "/googleDocs",
-    googlePhotosQuery = "/googlePhotosQuery",
+    googlePhotosAccessToken = "/googlePhotosAccessToken",
     googlePhotosMediaUpload = "/googlePhotosMediaUpload"
 
 }
\ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index b6330a609..ac8023ce1 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -7,6 +7,7 @@ import { GlobalOptions } from "googleapis-common";
 import { GaxiosResponse } from "gaxios";
 import request = require('request-promise');
 import * as qs from 'query-string';
+import Photos = require('googlephotos');
 
 /**
  * Server side authentication for Google Api queries.
@@ -35,19 +36,19 @@ export namespace GoogleApiServerUtils {
     }
 
     export interface CredentialPaths {
-        credentials: string;
-        token: string;
+        credentialsPath: string;
+        tokenPath: string;
     }
 
     export type ApiResponse = Promise<GaxiosResponse>;
-    export type ApiRouter = (endpoint: Endpoint, paramters: any) => ApiResponse;
+    export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse;
     export type ApiHandler = (parameters: any) => ApiResponse;
     export type Action = "create" | "retrieve" | "update";
 
     export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler };
     export type EndpointParameters = GlobalOptions & { version: "v1" };
 
-    export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
+    export const GetEndpoint = (sector: string, paths: CredentialPaths) => {
         return new Promise<Opt<Endpoint>>(resolve => {
             RetrieveCredentials(paths).then(authentication => {
                 let routed: Opt<Endpoint>;
@@ -65,19 +66,19 @@ export namespace GoogleApiServerUtils {
         });
     };
 
-    export const RetrieveCredentials = async (paths: CredentialPaths) => {
+    export const RetrieveCredentials = (paths: CredentialPaths) => {
         return new Promise<TokenResult>((resolve, reject) => {
-            readFile(paths.credentials, async (err, credentials) => {
+            readFile(paths.credentialsPath, async (err, credentials) => {
                 if (err) {
                     reject(err);
                     return console.log('Error loading client secret file:', err);
                 }
-                authorize(parseBuffer(credentials), paths.token).then(resolve, reject);
+                authorize(parseBuffer(credentials), paths.tokenPath).then(resolve, reject);
             });
         });
     };
 
-    export const RetrieveAccessToken = async (paths: CredentialPaths) => {
+    export const RetrieveAccessToken = (paths: CredentialPaths) => {
         return new Promise<string>((resolve, reject) => {
             RetrieveCredentials(paths).then(
                 credentials => resolve(credentials.token.access_token!),
@@ -86,6 +87,15 @@ export namespace GoogleApiServerUtils {
         });
     };
 
+    export const RetrievePhotosEndpoint = (paths: CredentialPaths) => {
+        return new Promise<any>((resolve, reject) => {
+            RetrieveAccessToken(paths).then(
+                token => resolve(new Photos(token)),
+                reject
+            );
+        });
+    };
+
     type TokenResult = { token: Credentials, client: OAuth2Client };
     /**
      * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
@@ -126,7 +136,7 @@ export namespace GoogleApiServerUtils {
             request.post(url, headerParameters).then(response => {
                 let parsed = JSON.parse(response);
                 credentials.access_token = parsed.access_token;
-                credentials.expiry_date = new Date().getTime() + parsed.expires_in * 1000;
+                credentials.expiry_date = new Date().getTime() + (parsed.expires_in * 1000);
                 writeFile(token_path, JSON.stringify(credentials), (err) => {
                     if (err) {
                         console.error(err);
diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts
deleted file mode 100644
index cb5464abc..000000000
--- a/src/server/apis/google/GooglePhotosServerUtils.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import request = require('request-promise');
-import { Album } from './typings/albums';
-import * as qs from 'query-string';
-
-const apiEndpoint = "https://photoslibrary.googleapis.com/v1/";
-
-export interface Authorization {
-    token: string;
-}
-
-export namespace GooglePhotos {
-
-    export type Query = Album.Query;
-    export type QueryParameters = { query: GooglePhotos.Query };
-    interface DispatchParameters {
-        required: boolean;
-        method: "GET" | "POST";
-        ignore?: boolean;
-    }
-
-    export const ExecuteQuery = async (parameters: Authorization & QueryParameters): Promise<any> => {
-        let action = parameters.query.action;
-        let dispatch = SuffixMap.get(action)!;
-        let suffix = Suffix(parameters, dispatch, action);
-        if (suffix) {
-            let query: any = parameters.query;
-            let options: any = {
-                headers: { 'Content-Type': 'application/json' },
-                auth: { 'bearer': parameters.token },
-            };
-            if (query.body) {
-                options.body = query.body;
-                options.json = true;
-            }
-            let queryParameters = query.parameters;
-            if (queryParameters) {
-                suffix += `?${qs.stringify(queryParameters)}`;
-            }
-            let dispatcher = dispatch.method === "POST" ? request.post : request.get;
-            return dispatcher(apiEndpoint + suffix, options);
-        }
-    };
-
-    const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => {
-        let query: any = parameters.query;
-        let id = query.albumId;
-        let suffix = 'albums';
-        if (dispatch.required) {
-            if (!id) {
-                return undefined;
-            }
-            suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`;
-        }
-        return suffix;
-    };
-
-    const SuffixMap = new Map<Album.Action, DispatchParameters>([
-        [Album.Action.AddEnrichment, { required: true, method: "POST" }],
-        [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }],
-        [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }],
-        [Album.Action.Create, { required: false, method: "POST" }],
-        [Album.Action.Get, { required: true, ignore: true, method: "GET" }],
-        [Album.Action.List, { required: false, method: "GET" }],
-        [Album.Action.Share, { required: true, method: "POST" }],
-        [Album.Action.Unshare, { required: true, method: "POST" }]
-    ]);
-
-}
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index cd2a586eb..3b513aaf1 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,28 +1,122 @@
 import request = require('request-promise');
-import { Authorization } from './GooglePhotosServerUtils';
+import { GoogleApiServerUtils } from './GoogleApiServerUtils';
+import * as fs from 'fs';
+import { Utils } from '../../../Utils';
+import * as path from 'path';
+import { Opt } from '../../../new_fields/Doc';
 
 export namespace GooglePhotosUploadUtils {
 
-    interface UploadInformation {
-        title: string;
-        MEDIA_BINARY_DATA: string;
+    export interface Paths {
+        uploadDirectory: string;
+        credentialsPath: string;
+        tokenPath: string;
     }
 
-    const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads";
+    export interface MediaInput {
+        description: string;
+        source: string;
+    }
+
+    export interface DownloadInformation {
+        mediaPath: string;
+        contentType?: string;
+        contentSize?: string;
+    }
+
+    const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`;
+    const headers = (type: string) => ({
+        'Content-Type': `application/${type}`,
+        'Authorization': Bearer,
+    });
+
+    let Bearer: string;
+    let Paths: Paths;
 
-    export const SubmitUpload = async (parameters: Authorization & UploadInformation) => {
-        let options = {
+    export const initialize = async (paths: Paths) => {
+        Paths = paths;
+        const { tokenPath, credentialsPath } = paths;
+        const token = await GoogleApiServerUtils.RetrieveAccessToken({ tokenPath, credentialsPath });
+        Bearer = `Bearer ${token}`;
+    };
+
+    export const DispatchGooglePhotosUpload = async (filename: string) => {
+        let body: Buffer;
+        if (filename.includes('upload_')) {
+            const mediaPath = Paths.uploadDirectory + filename;
+            body = await new Promise<Buffer>((resolve, reject) => {
+                fs.readFile(mediaPath, (error, data) => error ? reject(error) : resolve(data));
+            });
+        } else {
+            body = await request(filename, { encoding: null });
+        }
+        const parameters = {
+            method: 'POST',
             headers: {
-                'Content-Type': 'application/octet-stream',
-                Authorization: `Bearer ${parameters.token}`,
-                'X-Goog-Upload-File-Name': parameters.title,
+                ...headers('octet-stream'),
+                'X-Goog-Upload-File-Name': filename,
                 'X-Goog-Upload-Protocol': 'raw'
             },
-            body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA },
-            json: true
+            uri: prepend('uploads'),
+            body
         };
-        const result = await request.post(apiEndpoint, options);
-        return result;
+        return new Promise<any>(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body)));
+    };
+
+    export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => {
+        return new Promise<any>((resolve, reject) => {
+            const parameters = {
+                method: 'POST',
+                headers: headers('json'),
+                uri: prepend('mediaItems:batchCreate'),
+                body: { newMediaItems } as any,
+                json: true
+            };
+            album && (parameters.body.albumId = album.id);
+            request(parameters, (error, _response, body) => {
+                if (error) {
+                    reject(error);
+                } else {
+                    resolve(body);
+                }
+            });
+        });
     };
 
+    export namespace IOUtils {
+
+        export const Download = async (url: string): Promise<Opt<DownloadInformation>> => {
+            const filename = `temporary_upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
+            const temporaryDirectory = Paths.uploadDirectory + "temporary/";
+            const mediaPath = temporaryDirectory + filename;
+
+            if (!(await createIfNotExists(temporaryDirectory))) {
+                return undefined;
+            }
+
+            return new Promise<DownloadInformation>((resolve, reject) => {
+                request.head(url, (error, res) => {
+                    if (error) {
+                        return reject(error);
+                    }
+                    const information: DownloadInformation = {
+                        mediaPath,
+                        contentType: res.headers['content-type'],
+                        contentSize: res.headers['content-length'],
+                    };
+                    request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information));
+                });
+            });
+        };
+
+        export const createIfNotExists = async (path: string) => {
+            if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
+                return true;
+            }
+            return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
+        };
+
+        export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
+    }
+
 }
\ No newline at end of file
diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts
deleted file mode 100644
index f3025567d..000000000
--- a/src/server/apis/google/typings/albums.ts
+++ /dev/null
@@ -1,150 +0,0 @@
-export namespace Album {
-
-    export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare);
-
-    export enum Action {
-        AddEnrichment = "addEnrichment",
-        BatchAddMediaItems = "batchAddMediaItems",
-        BatchRemoveMediaItems = "batchRemoveMediaItems",
-        Create = "create",
-        Get = "get",
-        List = "list",
-        Share = "share",
-        Unshare = "unshare"
-    }
-
-    export interface AddEnrichment {
-        action: Action.AddEnrichment;
-        albumId: string;
-        body: {
-            newEnrichmentItem: NewEnrichmentItem;
-            albumPosition: MediaRelativeAlbumPosition;
-        };
-    }
-
-    export interface BatchAddMediaItems {
-        action: Action.BatchAddMediaItems;
-        albumId: string;
-        body: {
-            mediaItemIds: string[];
-        };
-    }
-
-    export interface BatchRemoveMediaItems {
-        action: Action.BatchRemoveMediaItems;
-        albumId: string;
-        body: {
-            mediaItemIds: string[];
-        };
-    }
-
-    export interface Create {
-        action: Action.Create;
-        body: {
-            album: Template;
-        };
-    }
-
-    export interface Get {
-        action: Action.Get;
-        albumId: string;
-    }
-
-    export interface List {
-        action: Action.List;
-        parameters: ListOptions;
-    }
-
-    export interface ListOptions {
-        pageSize: number;
-        pageToken: string;
-        excludeNonAppCreatedData: boolean;
-    }
-
-    export interface Share {
-        action: Action.Share;
-        albumId: string;
-        body: {
-            sharedAlbumOptions: SharedOptions;
-        };
-    }
-
-    export interface Unshare {
-        action: Action.Unshare;
-        albumId: string;
-    }
-
-    export interface Template {
-        title: string;
-    }
-
-    export interface Model {
-        id: string;
-        title: string;
-        productUrl: string;
-        isWriteable: boolean;
-        shareInfo: ShareInfo;
-        mediaItemsCount: string;
-        coverPhotoBaseUrl: string;
-        coverPhotoMediaItemId: string;
-    }
-
-    export interface ShareInfo {
-        sharedAlbumOptions: SharedOptions;
-        shareableUrl: string;
-        shareToken: string;
-        isJoined: boolean;
-        isOwned: boolean;
-    }
-
-    export interface SharedOptions {
-        isCollaborative: boolean;
-        isCommentable: boolean;
-    }
-
-    export enum PositionType {
-        POSITION_TYPE_UNSPECIFIED,
-        FIRST_IN_ALBUM,
-        LAST_IN_ALBUM,
-        AFTER_MEDIA_ITEM,
-        AFTER_ENRICHMENT_ITEM
-    }
-
-    export type Position = GeneralAlbumPosition | MediaRelativeAlbumPosition | EnrichmentRelativeAlbumPosition;
-
-    interface GeneralAlbumPosition {
-        position: PositionType.FIRST_IN_ALBUM | PositionType.LAST_IN_ALBUM | PositionType.POSITION_TYPE_UNSPECIFIED;
-    }
-
-    interface MediaRelativeAlbumPosition {
-        position: PositionType.AFTER_MEDIA_ITEM;
-        relativeMediaItemId: string;
-    }
-
-    interface EnrichmentRelativeAlbumPosition {
-        position: PositionType.AFTER_ENRICHMENT_ITEM;
-        relativeEnrichmentItemId: string;
-    }
-
-    export interface Location {
-        locationName: string;
-        latlng: {
-            latitude: number,
-            longitude: number
-        };
-    }
-
-    export interface NewEnrichmentItem {
-        textEnrichment: {
-            text: string;
-        };
-        locationEnrichment: {
-            location: Location
-        };
-        mapEnrichment: {
-            origin: { location: Location },
-            destination: { location: Location }
-        };
-    }
-
-}
\ 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 2ac972ed8..f3c8cf82a 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glt5ByifP30HMz6a1fEG77qZlz9fBEOCz4PQ1VA8t_Ck2ZTPJKoyr6xc3-GFZISAqrrw2U8XpMyZv02_URfPwUX0Z_tMdlIFqsygowR-uClukbgQPNtgxp2LS1oW","refresh_token":"1/wK1cUVLnt581ba_pYGPdlTvAa-OS64nB5m5XOXEBJ8Q","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.appendonly https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567556163894}
\ No newline at end of file
+{"access_token":"ya29.Glx7B9S6zCKDE0EgYk9xX9-RhcN8j4IwG9ONopTl1NkPX9FUOw0GI_81mY9bhaouuyOTnrc6FrZD5SDHolWwp3ABNT6l7TmhTLDILgGXIixZkWFRBPpF-xHC8lUd8A","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":1567765465073}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 3940bbd58..fab00a02d 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -42,7 +42,6 @@ var AdmZip = require('adm-zip');
 import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
 import { Response } from 'express-serve-static-core';
 import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
-import { GooglePhotos } from './apis/google/GooglePhotosServerUtils';
 import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
 const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
@@ -418,10 +417,10 @@ app.get("/thumbnail/:filename", (req, res) => {
     let filename = req.params.filename;
     let noExt = filename.substring(0, filename.length - ".png".length);
     let pagenumber = parseInt(noExt.split('-')[1]);
-    fs.exists(uploadDir + filename, (exists: boolean) => {
-        console.log(`${uploadDir + filename} ${exists ? "exists" : "does not exist"}`);
+    fs.exists(uploadDirectory + filename, (exists: boolean) => {
+        console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`);
         if (exists) {
-            let input = fs.createReadStream(uploadDir + filename);
+            let input = fs.createReadStream(uploadDirectory + filename);
             probe(input, (err: any, result: any) => {
                 if (err) {
                     console.log(err);
@@ -432,7 +431,7 @@ app.get("/thumbnail/:filename", (req, res) => {
             });
         }
         else {
-            LoadPage(uploadDir + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
+            LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res);
         }
     });
 });
@@ -556,13 +555,13 @@ class NodeCanvasFactory {
 const pngTypes = [".png", ".PNG"];
 const pdfTypes = [".pdf", ".PDF"];
 const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
-const uploadDir = __dirname + "/public/files/";
+const uploadDirectory = __dirname + "/public/files/";
 // SETTERS
 app.post(
     RouteStore.upload,
     (req, res) => {
         let form = new formidable.IncomingForm();
-        form.uploadDir = uploadDir;
+        form.uploadDir = uploadDirectory;
         form.keepExtensions = true;
         // let path = req.body.path;
         console.log("upload");
@@ -592,7 +591,7 @@ app.post(
                 }
                 if (isImage) {
                     resizers.forEach(resizer => {
-                        fs.createReadStream(uploadDir + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
+                        fs.createReadStream(uploadDirectory + file).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + file.substring(0, file.length - ext.length) + resizer.suffix + ext));
                     });
                 }
                 names.push(`/files/` + file);
@@ -611,7 +610,7 @@ addSecureRoute(
             res.status(401).send("incorrect parameters specified");
             return;
         }
-        imageDataUri.outputFile(uri, uploadDir + filename).then((savedName: string) => {
+        imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => {
             const ext = path.extname(savedName);
             let resizers = [
                 { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
@@ -632,7 +631,7 @@ addSecureRoute(
             }
             if (isImage) {
                 resizers.forEach(resizer => {
-                    fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDir + filename + resizer.suffix + ext));
+                    fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext));
                 });
             }
             res.send("/files/" + filename + ext);
@@ -799,8 +798,8 @@ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any
     }
 }
 
-const credentials = path.join(__dirname, "./credentials/google_docs_credentials.json");
-const token = path.join(__dirname, "./credentials/google_docs_token.json");
+const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json");
+const tokenPath = path.join(__dirname, "./credentials/google_docs_token.json");
 
 const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
     ["create", (api, params) => api.create(params)],
@@ -811,7 +810,7 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU
 app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     let sector: any = req.params.sector;
     let action: any = req.params.action;
-    GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentials, token }).then(endpoint => {
+    GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, tokenPath }).then(endpoint => {
         let handler = EndpointHandlerMap.get(action);
         if (endpoint && handler) {
             let execute = handler(endpoint, req.body).then(
@@ -825,36 +824,28 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     });
 });
 
-app.post(RouteStore.googlePhotosQuery, (req, res) => {
-    GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then(
-        token => {
-            GooglePhotos.ExecuteQuery({ token, query: req.body })
-                .then(response => {
-                    if (response === undefined) {
-                        res.send("Error: unable to build suffix for Google Photos API request");
-                        return;
-                    }
-                    res.send(response);
-                })
-                .catch(error => {
-                    res.send(`Error: an exception occurred in the execution of this Google Photos API request\n${error}`);
-                });
-        },
-        error => res.send(error)
-    );
-});
+app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath }).then(token => res.send(token)));
 
-app.post(RouteStore.googlePhotosMediaUpload, (req, res) => {
-    GoogleApiServerUtils.RetrieveAccessToken({ credentials, token }).then(
-        token => {
-            GooglePhotosUploadUtils.SubmitUpload({ token, ...req.body })
-                .then(response => {
-                    res.send(response);
-                }).catch(error => {
-                    res.send(`Error: an exception occurred in uploading the given media\n${error}`);
-                });
-        },
-        error => res.send(error));
+const tokenError = "Unable to successfully upload bytes for all images!";
+const mediaError = "Unable to convert all uploaded bytes to media items!";
+
+app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
+    const media: GooglePhotosUploadUtils.MediaInput[] = req.body.media;
+    await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath });
+    const newMediaItems = await Promise.all(media.map(async element => {
+        const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.source);
+        return !uploadToken ? undefined : {
+            description: element.description,
+            simpleMediaItem: { uploadToken }
+        };
+    }));
+    if (!newMediaItems.every(item => item)) {
+        return res.send(tokenError);
+    }
+    GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
+        success => res.send(success),
+        () => res.send(mediaError)
+    );
 });
 
 const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
-- 
cgit v1.2.3-70-g09d2


From 628eef55533118b1f2312b86b2ac5f7b64f7fc4a Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 10 Sep 2019 20:01:55 -0400
Subject: lots of refactoring, beginning autotagging

---
 src/Utils.ts                                       |   5 +
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 339 ++++++++++++++-------
 .../util/Import & Export/DirectoryImportBox.tsx    |   5 +-
 src/client/views/MainView.tsx                      |  18 +-
 src/client/views/nodes/DocumentView.tsx            |   7 +-
 src/client/views/nodes/FormattedTextBox.tsx        |   6 +-
 src/new_fields/RichTextUtils.ts                    |  13 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  |   7 +-
 src/server/apis/google/SharedTypes.ts              |  21 ++
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  21 +-
 11 files changed, 284 insertions(+), 160 deletions(-)
 create mode 100644 src/server/apis/google/SharedTypes.ts

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 959b89fe5..71d88683a 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -37,6 +37,11 @@ export class Utils {
     public static prepend(extension: string): string {
         return window.location.origin + extension;
     }
+
+    public static fileUrl(filename: string): string {
+        return this.prepend(`/file/${filename}`);
+    }
+
     public static CorsProxy(url: string): string {
         return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
     }
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index fddcf3aa5..b1de24d1a 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,8 +1,8 @@
 import { PostToServer, Utils } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { ImageField } from "../../../new_fields/URLField";
-import { Cast } from "../../../new_fields/Types";
-import { Doc, Opt } from "../../../new_fields/Doc";
+import { Cast, StrCast } from "../../../new_fields/Types";
+import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import Photos = require('googlephotos');
 import { RichTextField } from "../../../new_fields/RichTextField";
@@ -10,32 +10,15 @@ import { RichTextUtils } from "../../../new_fields/RichTextUtils";
 import { EditorState } from "prosemirror-state";
 import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
 import { Docs, DocumentOptions } from "../../documents/Documents";
-import { type } from "os";
-
-export namespace GooglePhotosClientUtils {
-
-    export enum ContentCategories {
-        NONE = 'NONE',
-        LANDSCAPES = 'LANDSCAPES',
-        RECEIPTS = 'RECEIPTS',
-        CITYSCAPES = 'CITYSCAPES',
-        LANDMARKS = 'LANDMARKS',
-        SELFIES = 'SELFIES',
-        PEOPLE = 'PEOPLE',
-        PETS = 'PETS',
-        WEDDINGS = 'WEDDINGS',
-        BIRTHDAYS = 'BIRTHDAYS',
-        DOCUMENTS = 'DOCUMENTS',
-        TRAVEL = 'TRAVEL',
-        ANIMALS = 'ANIMALS',
-        FOOD = 'FOOD',
-        SPORT = 'SPORT',
-        NIGHT = 'NIGHT',
-        PERFORMANCES = 'PERFORMANCES',
-        WHITEBOARDS = 'WHITEBOARDS',
-        SCREENSHOTS = 'SCREENSHOTS',
-        UTILITY = 'UTILITY'
-    }
+import { MediaItemCreationResult, NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
+
+export namespace GooglePhotos {
+
+    const endpoint = async () => {
+        const getToken = Utils.prepend(RouteStore.googlePhotosAccessToken);
+        const token = await (await fetch(getToken)).text();
+        return new Photos(token);
+    };
 
     export enum MediaType {
         ALL_MEDIA = 'ALL_MEDIA',
@@ -44,118 +27,238 @@ export namespace GooglePhotosClientUtils {
     }
 
     export type AlbumReference = { id: string } | { title: string };
-    export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text()));
 
     export interface MediaInput {
         url: string;
         description: string;
     }
 
-    export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => {
-        if (album && "title" in album) {
-            album = await (await endpoint()).albums.create(album.title);
+    export const ContentCategories = {
+        NONE: 'NONE',
+        LANDSCAPES: 'LANDSCAPES',
+        RECEIPTS: 'RECEIPTS',
+        CITYSCAPES: 'CITYSCAPES',
+        LANDMARKS: 'LANDMARKS',
+        SELFIES: 'SELFIES',
+        PEOPLE: 'PEOPLE',
+        PETS: 'PETS',
+        WEDDINGS: 'WEDDINGS',
+        BIRTHDAYS: 'BIRTHDAYS',
+        DOCUMENTS: 'DOCUMENTS',
+        TRAVEL: 'TRAVEL',
+        ANIMALS: 'ANIMALS',
+        FOOD: 'FOOD',
+        SPORT: 'SPORT',
+        NIGHT: 'NIGHT',
+        PERFORMANCES: 'PERFORMANCES',
+        WHITEBOARDS: 'WHITEBOARDS',
+        SCREENSHOTS: 'SCREENSHOTS',
+        UTILITY: 'UTILITY'
+    };
+
+    export namespace Export {
+
+        export interface AlbumCreationResult {
+            albumId: string;
+            mediaItems: MediaItem[];
         }
-        const media: MediaInput[] = [];
-        sources.forEach(source => {
-            let url: string;
-            let description: string;
-            if (source instanceof Doc) {
-                const data = Cast(Doc.GetProto(source).data, ImageField);
-                if (!data) {
-                    return;
+
+        export const CollectionToAlbum = async (collection: Doc, title?: string, descriptionKey?: string): Promise<Opt<AlbumCreationResult>> => {
+            const dataDocument = Doc.GetProto(collection);
+            const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
+            if (!images || !images.length) {
+                return undefined;
+            }
+            const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`);
+            const { id } = await Create.Album(resolved);
+            const result = await Transactions.UploadImages(images, { id }, descriptionKey);
+            if (result) {
+                const mediaItems = result.newMediaItemResults.map(item => item.mediaItem);
+                return { albumId: id, mediaItems };
+            }
+        };
+
+    }
+
+    export namespace Import {
+
+        export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc;
+
+        export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => {
+            let response = await Query.Search(requested);
+            let uploads = await Transactions.WriteMediaItemsToServer(response);
+            const children = uploads.map((upload: Transactions.UploadInformation) => {
+                let document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean));
+                document.fillColumn = true;
+                document.contentSize = upload.contentSize;
+                return document;
+            });
+            const options = { width: 500, height: 500 };
+            return constructor(children, options);
+        };
+
+    }
+
+    export namespace Query {
+
+        export const AppendImageMetadata = (sources: (Doc | string)[]) => {
+            let keys = Object.keys(ContentCategories);
+            let included: string[] = [];
+            let excluded: string[] = [];
+            for (let i = 0; i < keys.length; i++) {
+                for (let j = 0; j < keys.length; j++) {
+                    let value = ContentCategories[keys[i] as keyof typeof ContentCategories];
+                    if (j === i) {
+                        included.push(value);
+                    } else {
+                        excluded.push(value);
+                    }
                 }
-                url = data.url.href;
-                description = parseDescription(source, descriptionKey);
-            } else {
-                url = source;
-                description = Utils.GenerateGuid();
+                //...
+                included = excluded = [];
             }
-            media.push({ url, description });
-        });
-        if (media.length) {
-            return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+        };
+
+        interface DateRange {
+            after: Date;
+            before: Date;
         }
-    };
 
-    const parseDescription = (document: Doc, descriptionKey: string) => {
-        let description: string = Utils.prepend("/doc/" + document[Id]);
-        const target = document[descriptionKey];
-        if (typeof target === "string") {
-            description = target;
-        } else if (target instanceof RichTextField) {
-            description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data)));
+        const DefaultSearchOptions: SearchOptions = {
+            pageSize: 20,
+            included: [],
+            excluded: [],
+            date: undefined,
+            includeArchivedMedia: true,
+            type: MediaType.ALL_MEDIA,
+        };
+
+        export interface SearchOptions {
+            pageSize: number;
+            included: ContentCategories[];
+            excluded: ContentCategories[];
+            date: Opt<Date | DateRange>;
+            includeArchivedMedia: boolean;
+            type: MediaType;
         }
-        return description;
-    };
 
-    export interface DateRange {
-        after: Date;
-        before: Date;
-    }
-    export interface SearchOptions {
-        pageSize: number;
-        included: ContentCategories[];
-        excluded: ContentCategories[];
-        date: Opt<Date | DateRange>;
-        includeArchivedMedia: boolean;
-        type: MediaType;
+        export interface SearchResponse {
+            mediaItems: any[];
+            nextPageToken: string;
+        }
+
+        export const Search = async (requested: Opt<Partial<SearchOptions>>): Promise<SearchResponse> => {
+            const options = requested || DefaultSearchOptions;
+            const photos = await endpoint();
+            const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia);
+
+            const included = options.included || [];
+            const excluded = options.excluded || [];
+            const contentFilter = new photos.ContentFilter();
+            included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category));
+            excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category));
+            filters.setContentFilter(contentFilter);
+
+            const date = options.date;
+            if (date) {
+                const dateFilter = new photos.DateFilter();
+                if (date instanceof Date) {
+                    dateFilter.addDate(date);
+                } else {
+                    dateFilter.addRange(date.after, date.before);
+                }
+                filters.setDateFilter(dateFilter);
+            }
+
+            filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA));
+
+            return new Promise<SearchResponse>(resolve => {
+                photos.mediaItems.search(filters, options.pageSize || 20).then(resolve);
+            });
+        };
+
+        export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => {
+            return (await endpoint()).mediaItems.get(mediaItemId);
+        };
+
     }
 
-    const DefaultSearchOptions: SearchOptions = {
-        pageSize: 20,
-        included: [],
-        excluded: [],
-        date: undefined,
-        includeArchivedMedia: true,
-        type: MediaType.ALL_MEDIA,
-    };
+    export namespace Create {
+
+        export const Album = async (title: string) => {
+            return (await endpoint()).albums.create(title);
+        };
 
-    export interface SearchResponse {
-        mediaItems: any[];
-        nextPageToken: string;
     }
 
-    export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc;
-    export const CollectionFromSearch = async (provider: CollectionConstructor, requested: Opt<Partial<SearchOptions>>): Promise<Doc> => {
-        let downloads = await Search(requested);
-        return provider(downloads.map((download: any) => {
-            let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileNames.clean}`));
-            document.fillColumn = true;
-            document.contentSize = download.contentSize;
-            return document;
-        }), { width: 500, height: 500 });
-    };
+    export namespace Transactions {
 
-    export const Search = async (requested: Opt<Partial<SearchOptions>>): Promise<any> => {
-        const options = requested || DefaultSearchOptions;
-        const photos = await endpoint();
-        const filters = new photos.Filters(options.includeArchivedMedia === undefined ? true : options.includeArchivedMedia);
-
-        const included = options.included || [];
-        const excluded = options.excluded || [];
-        const contentFilter = new photos.ContentFilter();
-        included.length && included.forEach(category => contentFilter.addIncludedContentCategories(category));
-        excluded.length && excluded.forEach(category => contentFilter.addExcludedContentCategories(category));
-        filters.setContentFilter(contentFilter);
-
-        const date = options.date;
-        if (date) {
-            const dateFilter = new photos.DateFilter();
-            if (date instanceof Date) {
-                dateFilter.addDate(date);
-            } else {
-                dateFilter.addRange(date.after, date.before);
-            }
-            filters.setDateFilter(dateFilter);
+        export interface UploadInformation {
+            mediaPaths: string[];
+            fileNames: { [key: string]: string };
+            contentSize?: number;
+            contentType?: string;
+        }
+
+        export interface MediaItem {
+            id: string;
+            filename: string;
+            baseUrl: string;
         }
 
-        filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA));
+        export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
+            const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, body);
+            return uploads;
+        };
 
-        return new Promise<Doc>(resolve => {
-            photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => {
-                response && resolve(await PostToServer(RouteStore.googlePhotosMediaDownload, response));
+        export const UploadThenFetch = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => {
+            const result = await UploadImages(sources, album, descriptionKey);
+            if (!result) {
+                return undefined;
+            }
+            const baseUrls: string[] = await Promise.all(result.newMediaItemResults.map((result: any) => {
+                return new Promise<string>(resolve => Query.GetImage(result.mediaItem.id).then(item => resolve(item.baseUrl)));
+            }));
+            return baseUrls;
+        };
+
+        export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<MediaItemCreationResult>> => {
+            if (album && "title" in album) {
+                album = await Create.Album(album.title);
+            }
+            const media: MediaInput[] = [];
+            sources.forEach(source => {
+                let url: string;
+                let description: string;
+                if (source instanceof Doc) {
+                    const data = Cast(Doc.GetProto(source).data, ImageField);
+                    if (!data) {
+                        return;
+                    }
+                    url = data.url.href;
+                    description = parseDescription(source, descriptionKey);
+                } else {
+                    url = source;
+                    description = Utils.GenerateGuid();
+                }
+                media.push({ url, description });
             });
-        });
-    };
+            if (media.length) {
+                return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+            }
+        };
+
+        const parseDescription = (document: Doc, descriptionKey: string) => {
+            let description: string = Utils.prepend("/doc/" + document[Id]);
+            const target = document[descriptionKey];
+            if (typeof target === "string") {
+                description = target;
+            } else if (target instanceof RichTextField) {
+                description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data)));
+            }
+            return description;
+        };
+
+    }
 
 }
\ No newline at end of file
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index a19fd39b7..d58c02ce5 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -18,7 +18,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { Cast, BoolCast, NumCast } from "../../../new_fields/Types";
 import { listSpec } from "../../../new_fields/Schema";
-import { GooglePhotosClientUtils } from "../../apis/google_docs/GooglePhotosClientUtils";
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 
 const unsupported = ["text/html", "text/plain"];
 interface FileResponse {
@@ -117,8 +117,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`);
         }));
 
-        await GooglePhotosClientUtils.UploadImages(docs, { title: directory });
-        console.log("Finished upload!");
+        await GooglePhotos.Transactions.UploadImages(docs, { title: directory });
 
         for (let i = 0; i < docs.length; i++) {
             let doc = docs[i];
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0c0ed9072..8d10a91ce 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -40,7 +40,7 @@ import { PreviewCursor } from './PreviewCursor';
 import { FilterBox } from './search/FilterBox';
 import PresModeMenu from './presentationview/PresentationModeMenu';
 import { PresBox } from './nodes/PresBox';
-import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils';
+import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../new_fields/URLField';
 import { LinkFollowBox } from './linking/LinkFollowBox';
 import { DocumentManager } from '../util/DocumentManager';
@@ -149,14 +149,14 @@ export class MainView extends React.Component {
         }, { fireImmediately: true });
     }
 
-    executeGooglePhotosRoutine = async () => {
-        let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
-        let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" });
-        doc.caption = "Well isn't this a nice cat image!";
-        let photos = await GooglePhotosClientUtils.endpoint();
-        let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id;
-        console.log(await GooglePhotosClientUtils.UploadImages([doc], { id: albumId }));
-    }
+    // executeGooglePhotosRoutine = async () => {
+    //     let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
+    //     let doc = Docs.Create.ImageDocument(imgurl, { width: 200, title: "an image of a cat" });
+    //     doc.caption = "Well isn't this a nice cat image!";
+    //     let photos = await GooglePhotos.endpoint();
+    //     let albumId = (await photos.albums.list(50)).albums.filter((album: any) => album.title === "This is a generically created album!")[0].id;
+    //     console.log(await GooglePhotos.UploadImages([doc], { id: albumId }));
+    // }
 
     componentWillUnMount() {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index cb9346a8b..a51f783ad 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,7 +41,7 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
 import { DocumentType } from '../../documents/DocumentTypes';
-import { GooglePhotosClientUtils } from '../../apis/google_docs/GooglePhotosClientUtils';
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../../new_fields/URLField';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
@@ -591,7 +591,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         subitems.push({ description: "Open Fields", event: this.fieldsClicked, 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: () => GooglePhotosClientUtils.UploadImages([this.props.Document]), icon: "caret-square-right" });
+            cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: "caret-square-right" });
+        }
+        if (Cast(Doc.GetProto(this.props.Document).data, listSpec(Doc))) {
+            cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum(this.props.Document).then(console.log), icon: "caret-square-right" });
         }
         let existingMake = ContextMenu.Instance.findByDescription("Make...");
         let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index fda9ea33f..9d83fbd04 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -177,7 +177,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;
@@ -463,7 +463,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
             let redo = async () => {
                 if (this._editorView && reference) {
-                    let content = RichTextUtils.GoogleDocs.Export(this._editorView.state);
+                    let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
                     let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
                     response && (this.dataDoc[GoogleRef] = response.documentId);
                     let pushSuccess = response !== undefined && !("errors" in response);
@@ -636,7 +636,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     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) }
+                    footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); }
                 },
                 clipboardTextSerializer: this.clipboardTextSerializer,
                 handlePaste: this.handlePaste,
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 500b93676..27737782b 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -11,7 +11,7 @@ import { Utils, PostToServer } from "../Utils";
 import { RouteStore } from "../server/RouteStore";
 import { Docs } from "../client/documents/Documents";
 import { schema } from "../client/util/RichTextSchema";
-import { GooglePhotosClientUtils } from "../client/apis/google_docs/GooglePhotosClientUtils";
+import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
 
 export namespace RichTextUtils {
 
@@ -90,7 +90,7 @@ export namespace RichTextUtils {
 
     export namespace GoogleDocs {
 
-        export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => {
+        export const Export = async (state: EditorState): Promise<GoogleApiClientUtils.Docs.Content> => {
             let nodes: { [type: string]: Node<any>[] } = {
                 text: [],
                 image: []
@@ -107,7 +107,7 @@ export namespace RichTextUtils {
                 }
             }));
             let linkRequests = ExtractLinks(nodes.text);
-            let imageRequests = ExtractImages(nodes.image);
+            let imageRequests = await ExtractImages(nodes.image);
             return {
                 text,
                 requests: [...linkRequests, ...imageRequests]
@@ -298,10 +298,13 @@ export namespace RichTextUtils {
                 const length = node.nodeSize;
                 const attrs = node.attrs;
                 const uri = attrs.src;
-                const result = (await GooglePhotosClientUtils.UploadImages([uri])).newMediaItemResults;
+                const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([uri]);
+                if (!baseUrls) {
+                    continue;
+                }
                 images.push({
                     insertInlineImage: {
-                        uri: result[0].mediaItem.productUrl,
+                        uri: baseUrls[0],
                         objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } },
                         location: { index: position + length }
                     }
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index d1f1f81bd..447ed23ac 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -5,6 +5,7 @@ import { Utils } from '../../../Utils';
 import * as path from 'path';
 import { Opt } from '../../../new_fields/Doc';
 import * as sharp from 'sharp';
+import { MediaItemCreationResult } from './SharedTypes';
 
 const uploadDirectory = path.join(__dirname, "../../public/files/");
 
@@ -52,8 +53,10 @@ export namespace GooglePhotosUploadUtils {
         return new Promise<any>(resolve => request(parameters, (error, _response, body) => resolve(error ? undefined : body)));
     };
 
-    export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }) => {
-        return new Promise<any>((resolve, reject) => {
+
+
+    export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }): Promise<MediaItemCreationResult> => {
+        return new Promise<MediaItemCreationResult>((resolve, reject) => {
             const parameters = {
                 method: 'POST',
                 headers: headers('json'),
diff --git a/src/server/apis/google/SharedTypes.ts b/src/server/apis/google/SharedTypes.ts
new file mode 100644
index 000000000..9ad6130b6
--- /dev/null
+++ b/src/server/apis/google/SharedTypes.ts
@@ -0,0 +1,21 @@
+export interface NewMediaItemResult {
+    uploadToken: string;
+    status: { code: number, message: string };
+    mediaItem: MediaItem;
+}
+
+export interface MediaItem {
+    id: string;
+    description: string;
+    productUrl: string;
+    baseUrl: string;
+    mimeType: string;
+    mediaMetadata: {
+        creationTime: string;
+        width: string;
+        height: string;
+    };
+    filename: string;
+}
+
+export type MediaItemCreationResult = { newMediaItemResults: NewMediaItemResult[] };
\ 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 5c142fba1..22d57d744 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx_B6G7Q_FYs1LK5VcyV6P6Zg9JkoHO2aC_TsnN7AVxPYWHEpsBSC0WyWX7Ztr8HWhOUYA5JXqnZDkLrK1V3Hb-0GgtyApLRNtEPOWf1dJ7lOm_iKVw2tRvPe7XDQ","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":1568078116605}
\ No newline at end of file
+{"access_token":"ya29.GlyAB5T3dgJqWuYBcLaT94wQo7MZkmzJQZxDB2sSU95mdhW24E3diuFdLeNsUDVI57D3S765RweMnL98d-fdgu1dRxpzkV_J_3rLih99pZ8A4d6jVdm1354UT4py_w","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":1568161931458}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 8469770d5..2c3e76c55 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -795,19 +795,6 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU
     ["update", (api, params) => api.batchUpdate(params)],
 ]);
 
-// app.post(RouteStore.googleDocsGet, async (req, res) => {
-//     const token = await GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, tokenPath });
-//     request_promise.get({
-//         uri: `https://docs.googleapis.com/v1/documents/${req.body.documentId}?fields=inlineObjects`,
-//         headers: {
-//             'Authorization': `Bearer ${token}`
-//         }
-//     }).then(response => {
-//         console.log(response);
-//         res.send(response);
-//     });
-// });
-
 app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     let sector: GoogleApiServerUtils.Service = req.params.sector;
     let action: GoogleApiServerUtils.Action = req.params.action;
@@ -841,11 +828,11 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
         };
     }));
     if (!newMediaItems.every(item => item)) {
-        return res.status(STATUS.EXECUTION_ERROR).send(tokenError);
+        return _error(res, tokenError);
     }
     GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
-        success => res.status(STATUS.OK).send(success),
-        () => res.status(STATUS.EXECUTION_ERROR).send(mediaError)
+        mediaItems => _success(res, mediaItems),
+        error => _error(res, mediaError, error)
     );
 });
 
@@ -871,7 +858,7 @@ app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
     _invalid(res, requestError);
 });
 
-const _error = (res: Response, message: string, error: any) => {
+const _error = (res: Response, message: string, error?: any) => {
     res.statusMessage = message;
     res.status(STATUS.EXECUTION_ERROR).send(error);
 };
-- 
cgit v1.2.3-70-g09d2


From 42500ed737e17c2f973a653d59491eb33d6ba66b Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 14 Sep 2019 13:49:34 -0400
Subject: beginnings of formatting conversion on push

---
 src/client/views/nodes/FormattedTextBox.tsx   |   3 +-
 src/new_fields/RichTextUtils.ts               | 153 ++++++++++++++------------
 src/server/credentials/google_docs_token.json |   2 +-
 3 files changed, 83 insertions(+), 75 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 8a623e648..23f615f32 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -284,10 +284,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             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 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]);
+            this.tryUpdateHeight();
             e.stopPropagation();
         } else if (de.data instanceof DragManager.DocumentDragData) {
             const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0];
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 6afe4ddfd..178cce839 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -5,7 +5,7 @@ import { docs_v1 } from "googleapis";
 import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
 import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
 import { Opt } from "./Doc";
-import * as Color from "color";
+import Color from "color";
 import { sinkListItem } from "prosemirror-schema-list";
 import { Utils, PostToServer } from "../Utils";
 import { RouteStore } from "../server/RouteStore";
@@ -91,27 +91,11 @@ export namespace RichTextUtils {
     export namespace GoogleDocs {
 
         export const Export = async (state: EditorState): Promise<GoogleApiClientUtils.Docs.Content> => {
-            let nodes: { [type: string]: Node<any>[] } = {
-                text: [],
-                image: []
-            };
+            const nodes: Node<any>[] = [];
             let text = ToPlainText(state);
-            let content = state.doc.content;
-            content.forEach(node => node.content.forEach(node => {
-                const type = node.type.name;
-                let existing = nodes[type];
-                if (existing) {
-                    existing.push(node);
-                } else {
-                    nodes[type] = [node];
-                }
-            }));
-            let linkRequests = ExtractLinks(nodes.text);
-            let imageRequests = await ExtractImages(nodes.image);
-            return {
-                text,
-                requests: [...linkRequests, ...imageRequests]
-            };
+            state.doc.content.forEach(node => node.content.forEach(child => nodes.push(child)));
+            const requests = await marksToStyle(nodes);
+            return { text, requests };
         };
 
         type BulletPosition = { value: number, sinks: number };
@@ -186,8 +170,10 @@ export namespace RichTextUtils {
 
             const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems: inlineUrls });
             for (let i = 0; i < uploads.length; i++) {
-                const src = Utils.prepend(`/files/${uploads[i].fileNames.clean}`);
-                state = state.apply(state.tr.insert(0, schema.nodes.image.create({ src, width: inlineUrls[i].width })));
+                const src = Utils.fileUrl(uploads[i].fileNames.clean);
+                const width = inlineUrls[i].width;
+                const imageNode = schema.nodes.image.create({ src, width });
+                state = state.apply(state.tr.insert(0, imageNode));
             }
 
             return { title, text, state };
@@ -265,71 +251,92 @@ export namespace RichTextUtils {
             return marks;
         };
 
-        interface LinkInformation {
-            startIndex: number;
-            endIndex: number;
-            bold: boolean;
-            url: string;
-        }
-
-        const ExtractLinks = (nodes: Node<any>[]) => {
-            let links: docs_v1.Schema$Request[] = [];
+        const marksToStyle = async (nodes: Node<any>[]) => {
+            let requests: docs_v1.Schema$Request[] = [];
             let position = 1;
             for (let node of nodes) {
-                let link, length = node.nodeSize;
-                let marks = node.marks;
-                if (marks.length && (link = marks.find(mark => mark.type.name === "link"))) {
-                    links.push(Encode({
-                        startIndex: position,
-                        endIndex: position + length,
-                        url: link.attrs.href,
-                        bold: false
+                const length = node.nodeSize;
+                const marks = node.marks;
+                const attrs = node.attrs;
+                const textStyle: docs_v1.Schema$TextStyle = {};
+                const information: LinkInformation = {
+                    startIndex: position,
+                    endIndex: position + length,
+                    textStyle
+                };
+                if (marks.length) {
+
+                    const link = marks.find(mark => mark.type.name === "link");
+                    if (link) {
+                        textStyle.link = { url: link.attrs.href };
+                        textStyle.foregroundColor = fromRgb(0, 0, 1);
+                        textStyle.bold = true;
+                    }
+                    const bold = marks.find(mark => mark.type.name === "strong");
+                    bold && (textStyle.bold = true);
+                    const foregroundColor = marks.find(mark => mark.type.name === "pFontColor");
+                    foregroundColor && (textStyle.foregroundColor = fromHex(foregroundColor.attrs.color));
+                }
+                requests.push(EncodeStyleUpdate(information));
+                if (node.type.name === "image") {
+                    requests.push(await EncodeImage({
+                        startIndex: position + length,
+                        uri: attrs.src,
+                        width: attrs.width
                     }));
                 }
                 position += length;
             }
-            return links;
+            return requests;
         };
 
-        const ExtractImages = async (nodes: Node<any>[]) => {
-            const images: docs_v1.Schema$Request[] = [];
-            let position = 1;
-            for (let node of nodes) {
-                const length = node.nodeSize;
-                const attrs = node.attrs;
-                const uri = attrs.src;
-                const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([Docs.Create.ImageDocument(uri)]);
-                if (!baseUrls) {
-                    continue;
-                }
-                images.push({
-                    insertInlineImage: {
-                        uri: baseUrls[0],
-                        objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } },
-                        location: { index: position + length }
-                    }
-                });
-                position += length;
-            }
-            return images;
+        interface LinkInformation {
+            startIndex: number;
+            endIndex: number;
+            textStyle: docs_v1.Schema$TextStyle;
+        }
+
+        interface ImageInformation {
+            startIndex: number;
+            width: number;
+            uri: string;
+        }
+
+        const fromRgb = (red: number, green: number, blue: number): docs_v1.Schema$OptionalColor => {
+            return { color: { rgbColor: { red, green, blue } } };
+        };
+
+        const fromHex = (color: string): docs_v1.Schema$OptionalColor => {
+            const converted = new Color().hex(color).rgb();
+            const { red, blue, green } = converted;
+            return fromRgb(red(), blue(), green());
         };
 
-        const Encode = (information: LinkInformation) => {
+        const EncodeStyleUpdate = (information: LinkInformation): docs_v1.Schema$Request => {
+            const { startIndex, endIndex, textStyle } = information;
             return {
                 updateTextStyle: {
                     fields: "*",
-                    range: {
-                        startIndex: information.startIndex,
-                        endIndex: information.endIndex
-                    },
-                    textStyle: {
-                        bold: true,
-                        link: { url: information.url },
-                        foregroundColor: { color: { rgbColor: { red: 0.0, green: 0.0, blue: 1.0 } } }
-                    }
-                }
+                    range: { startIndex, endIndex },
+                    textStyle
+                } as docs_v1.Schema$UpdateTextStyleRequest
             };
         };
+
+        const EncodeImage = async (information: ImageInformation) => {
+            const source = [Docs.Create.ImageDocument(information.uri)];
+            const baseUrls = await GooglePhotos.Transactions.UploadThenFetch(source);
+            if (baseUrls) {
+                return {
+                    insertInlineImage: {
+                        uri: baseUrls[0],
+                        objectSize: { width: { magnitude: information.width, unit: "PT" } },
+                        location: { index: information.startIndex }
+                    }
+                };
+            }
+            return {};
+        };
     }
 
 }
\ 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 293404853..c8fd3bbf5 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.ImCDB68jWP3W09KYIKQwzO_9eizRTpSx9UhP4tIPbSeMvjDsDyNOOl2hmffWHBrRFqz8kgaDxl_6lp-rlfbArI0x1Wm13J2h7wm5kBvOlCToqR-5qoINHetlWisCb11ig9A","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":1568480323588}
\ No newline at end of file
+{"access_token":"ya29.GlyDBwCmpO9R1fAOMIzpdZiCWhEeaDHiJOPy7sNRAo-vAIqzIk7zy1DLdOhSFWaBQrbmewSOJZPvbBUAxqdDELc_aW_BsjwXFbxiTd4Us_N8IWkPDCtUeBmLAZjodA","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":1568484185591}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From c2f749238e9db63b81c3bec08a14dd6006ab876f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 14 Sep 2019 21:00:37 -0400
Subject: sharing routine for docs and final formatting fixes

---
 src/Utils.ts                                       |   4 +
 .../CollectionFreeFormLinkView.tsx                 |   2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |   9 +-
 src/new_fields/RichTextUtils.ts                    | 153 +++++++++++++--------
 src/server/credentials/google_docs_token.json      |   2 +-
 5 files changed, 108 insertions(+), 62 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 60f18eac2..a842f5a20 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -42,6 +42,10 @@ export class Utils {
         return this.prepend(`/files/${filename}`);
     }
 
+    public static shareUrl(documentId: string): string {
+        return this.prepend(`/doc/${documentId}?sharing=true`);
+    }
+
     public static CorsProxy(url: string): string {
         return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f19243bd6..12771d11e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -50,7 +50,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         return (
             <>
                 <line key="linkLine" className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${5}` }}
+                    style={{ strokeWidth: `${3}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 23f615f32..fc5b27220 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -749,11 +749,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                                 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 (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
+                                    DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, document => this.props.addDocTab(document, undefined, location ? location : "onRight"), NumCast((jumpToDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+                                    return;
+                                    // }
                                 }
                                 if (targetContext) {
                                     DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 84744db2f..555c41b67 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -4,7 +4,7 @@ import { RichTextField } from "./RichTextField";
 import { docs_v1 } from "googleapis";
 import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
 import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
-import { Opt } from "./Doc";
+import { Opt, Doc } from "./Doc";
 import Color = require('color');
 import { sinkListItem } from "prosemirror-schema-list";
 import { Utils, PostToServer } from "../Utils";
@@ -12,6 +12,11 @@ import { RouteStore } from "../server/RouteStore";
 import { Docs } from "../client/documents/Documents";
 import { schema } from "../client/util/RichTextSchema";
 import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
+import { SchemaHeaderField } from "./SchemaHeaderField";
+import { DocServer } from "../client/DocServer";
+import { Cast } from "./Types";
+import { Id } from "./FieldSymbols";
+import { DocumentView } from "../client/views/nodes/DocumentView";
 
 export namespace RichTextUtils {
 
@@ -91,9 +96,15 @@ export namespace RichTextUtils {
     export namespace GoogleDocs {
 
         export const Export = async (state: EditorState): Promise<GoogleApiClientUtils.Docs.Content> => {
-            const nodes: Node<any>[] = [];
+            const nodes: (Node<any> | null)[] = [];
             let text = ToPlainText(state);
-            state.doc.content.forEach(node => node.content.forEach(child => nodes.push(child)));
+            state.doc.content.forEach(node => {
+                if (!node.childCount) {
+                    nodes.push(null);
+                } else {
+                    node.content.forEach(child => nodes.push(child));
+                }
+            });
             const requests = await marksToStyle(nodes);
             return { text, requests };
         };
@@ -223,30 +234,11 @@ export namespace RichTextUtils {
         const StyleToMark = new Map<keyof docs_v1.Schema$TextStyle, keyof typeof schema.marks>([
             ["bold", "strong"],
             ["italic", "em"],
-            ["foregroundColor", "pFontColor"]
-        ]);
-
-        const MarkToStyle = new Map<keyof typeof schema.marks, keyof docs_v1.Schema$TextStyle>([
-            ["strong", "bold"],
-            ["em", "italic"],
-            ["pFontColor", "foregroundColor"],
-            ["timesNewRoman", "weightedFontFamily"],
-            ["georgia", "weightedFontFamily"],
-            ["comicSans", "weightedFontFamily"],
-            ["tahoma", "weightedFontFamily"],
-            ["impact", "weightedFontFamily"]
+            ["foregroundColor", "pFontColor"],
+            ["fontSize", "pFontSize"]
         ]);
 
-        const FontFamilyMapping = new Map<string, string>([
-            ["timesNewRoman", "Times New Roman"],
-            ["arial", "Arial"],
-            ["georgia", "Georgia"],
-            ["comicSans", "Comic Sans MS"],
-            ["tahoma", "Tahoma"],
-            ["impact", "Impact"]
-        ]);
-
-        const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle): Opt<Mark[]> => {
+        const styleToMarks = (schema: any, textStyle?: docs_v1.Schema$TextStyle) => {
             if (!textStyle) {
                 return undefined;
             }
@@ -263,20 +255,53 @@ export namespace RichTextUtils {
                         let object = value.color.rgbColor;
                         attributes.color = Color.rgb(["red", "green", "blue"].map(color => object[color] * 255 || 0)).hex();
                     }
+                    if (value.magnitude) {
+                        attributes.fontSize = value.magnitude;
+                    }
 
-                    let mark = schema.mark(schema.marks[converted], attributes);
+                    let mapped = schema.marks[converted];
+                    if (!mapped) {
+                        alert(`No mapping found for ${converted}!`);
+                        return;
+                    }
+
+                    let mark = schema.mark(mapped, attributes);
                     mark && marks.push(mark);
                 }
             });
             return marks;
         };
 
+        const MarkToStyle = new Map<keyof typeof schema.marks, keyof docs_v1.Schema$TextStyle>([
+            ["strong", "bold"],
+            ["em", "italic"],
+            ["pFontColor", "foregroundColor"],
+            ["timesNewRoman", "weightedFontFamily"],
+            ["georgia", "weightedFontFamily"],
+            ["comicSans", "weightedFontFamily"],
+            ["tahoma", "weightedFontFamily"],
+            ["impact", "weightedFontFamily"]
+        ]);
+
+        const FontFamilyMapping = new Map<string, string>([
+            ["timesNewRoman", "Times New Roman"],
+            ["arial", "Arial"],
+            ["georgia", "Georgia"],
+            ["comicSans", "Comic Sans MS"],
+            ["tahoma", "Tahoma"],
+            ["impact", "Impact"]
+        ]);
+
         const ignored = ["user_mark"];
 
-        const marksToStyle = async (nodes: Node<any>[]): Promise<docs_v1.Schema$Request[]> => {
+        const marksToStyle = async (nodes: (Node<any> | null)[]): Promise<docs_v1.Schema$Request[]> => {
             let requests: docs_v1.Schema$Request[] = [];
             let position = 1;
             for (let node of nodes) {
+                if (node === null) {
+                    position += 2;
+                    continue;
+                }
                 const { marks, attrs, nodeSize } = node;
                 const textStyle: docs_v1.Schema$TextStyle = {};
                 const information: LinkInformation = {
@@ -284,37 +309,55 @@ export namespace RichTextUtils {
                     endIndex: position + nodeSize,
                     textStyle
                 };
-                if (marks.length) {
-                    let mark: Mark<any>;
-                    const markMap = BuildMarkMap(marks);
-                    Object.keys(schema.marks).map(markName => {
-                        if (!ignored.includes(markName) && (mark = markMap[markName])) {
-                            const converted = MarkToStyle.get(markName) || markName as keyof docs_v1.Schema$TextStyle;
-                            let value: any = true;
-                            if (converted) {
-                                const { attrs } = mark;
-                                switch (converted) {
-                                    case "link":
-                                        value = { url: attrs.href };
-                                        textStyle.foregroundColor = fromRgb.blue;
-                                        textStyle.bold = true;
-                                        break;
-                                    case "fontSize":
-                                        value = attrs.fontSize;
-                                        break;
-                                    case "foregroundColor":
-                                        value = fromHex(attrs.color);
-                                        break;
-                                    case "weightedFontFamily":
-                                        value = { fontFamily: FontFamilyMapping.get(markName) };
+                let mark: Mark<any>;
+                const markMap = BuildMarkMap(marks);
+                for (let markName of Object.keys(schema.marks)) {
+                    if (ignored.includes(markName) || !(mark = markMap[markName])) {
+                        continue;
+                    }
+                    let converted = MarkToStyle.get(markName) || markName as keyof docs_v1.Schema$TextStyle;
+                    let value: any = true;
+                    if (!converted) {
+                        continue;
+                    }
+                    const { attrs } = mark;
+                    switch (converted) {
+                        case "link":
+                            let url = attrs.href;
+                            const delimiter = "/doc/";
+                            const alreadyShared = "?sharing=true";
+                            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);
+                                    target && (url = Utils.shareUrl(exported[Id]));
+                                    linkDoc.anchor2 = exported;
                                 }
-                                textStyle[converted] = value;
                             }
-                        }
-                    });
-                    if (Object.keys(textStyle).length) {
-                        requests.push(EncodeStyleUpdate(information));
+                            value = { url };
+                            textStyle.foregroundColor = fromRgb.blue;
+                            textStyle.bold = true;
+                            break;
+                        case "fontSize":
+                            value = attrs.fontSize;
+                            break;
+                        case "foregroundColor":
+                            value = fromHex(attrs.color);
+                            break;
+                        case "weightedFontFamily":
+                            value = { fontFamily: FontFamilyMapping.get(markName) };
+                    }
+                    let matches: RegExpExecArray | null;
+                    if ((matches = /p(\d+)/g.exec(markName)) !== null) {
+                        converted = "fontSize";
+                        value = { magnitude: parseInt(matches[1]), unit: "PT" };
                     }
+                    textStyle[converted] = value;
+                }
+                if (Object.keys(textStyle).length) {
+                    requests.push(EncodeStyleUpdate(information));
                 }
                 if (node.type.name === "image") {
                     requests.push(await EncodeImage({
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 330d27141..265c07c69 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GlyEBxgqsCRjX9SAJGGss3EtfrPgwSjeMsfsuwJqTk7o4GRrBpwU0eQXXBNgPdAPRSrJzuVgAqWxap9kKrtkpf2tuHxk7Ml9Jblj48tU0BN2X0lMh66S2EIRhLnQnw","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":1568501067486}
\ No newline at end of file
+{"access_token":"ya29.ImCEByJgpv8e3CNTi-EwwqOtXUB1sNsOyOxM4WEyTybrQzCKc80SkjQZgb9gFCChbA7fFsdvewVAS_SiohfFziPOV4-YffeO417NS2CQf1cksmCQQBWxmL3i7qvQgz4VkdI","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":1568509688650}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From d7012323e87c21a25c29d89d66a1c54b99c8b458 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 15 Sep 2019 19:28:10 -0400
Subject: syncing of images in imported google doc

---
 .../apis/google_docs/GoogleApiClientUtils.ts       |  19 ++--
 src/client/views/nodes/FormattedTextBox.tsx        |   6 +-
 src/new_fields/RichTextUtils.ts                    | 115 +++++++++++++++------
 src/server/apis/google/GooglePhotosUploadUtils.ts  |  56 ++++++----
 src/server/apis/google/existing_uploads.json       |   1 +
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  30 ++++--
 7 files changed, 162 insertions(+), 67 deletions(-)
 create mode 100644 src/server/apis/google/existing_uploads.json

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 828d4451a..cbc5da15b 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -97,25 +97,30 @@ export namespace GoogleApiClientUtils {
             export type ExtractResult = { text: string, paragraphs: DeconstructedParagraph[] };
             export const extractText = (document: docs_v1.Schema$Document, removeNewlines = false): ExtractResult => {
                 let paragraphs = extractParagraphs(document);
-                let text = paragraphs.map(paragraph => paragraph.runs.map(run => run.content).join("")).join("");
+                let text = paragraphs.map(paragraph => paragraph.contents.filter(content => !("inlineObjectId" in content)).map(run => run as docs_v1.Schema$TextRun).join("")).join("");
                 text = text.substring(0, text.length - 1);
                 removeNewlines && text.ReplaceAll("\n", "");
                 return { text, paragraphs };
             };
 
-            export type DeconstructedParagraph = { runs: docs_v1.Schema$TextRun[], bullet: Opt<number> };
+            export type ContentArray = (docs_v1.Schema$TextRun | docs_v1.Schema$InlineObjectElement)[];
+            export type DeconstructedParagraph = { contents: ContentArray, bullet: Opt<number> };
             const extractParagraphs = (document: docs_v1.Schema$Document, filterEmpty = true): DeconstructedParagraph[] => {
                 const fragments: DeconstructedParagraph[] = [];
                 if (document.body && document.body.content) {
                     for (const element of document.body.content) {
-                        let runs: docs_v1.Schema$TextRun[] = [];
+                        let runs: ContentArray = [];
                         let bullet: Opt<number>;
                         if (element.paragraph) {
                             if (element.paragraph.elements) {
                                 for (const inner of element.paragraph.elements) {
-                                    if (inner && inner.textRun) {
-                                        let run = inner.textRun;
-                                        (run.content || !filterEmpty) && runs.push(inner.textRun);
+                                    if (inner) {
+                                        if (inner.textRun) {
+                                            let run = inner.textRun;
+                                            (run.content || !filterEmpty) && runs.push(inner.textRun);
+                                        } else if (inner.inlineObjectElement) {
+                                            runs.push(inner.inlineObjectElement);
+                                        }
                                     }
                                 }
                             }
@@ -123,7 +128,7 @@ export namespace GoogleApiClientUtils {
                                 bullet = element.paragraph.bullet.nestingLevel || 0;
                             }
                         }
-                        (runs.length || !filterEmpty) && fragments.push({ runs, bullet });
+                        (runs.length || !filterEmpty) && fragments.push({ contents: runs, bullet });
                     }
                 }
                 return fragments;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index fc5b27220..8f0f142c4 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -193,8 +193,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                             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();
+                    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);
                     this.dataDoc[key] = value;
@@ -506,7 +506,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let documentId = StrCast(dataDoc[GoogleRef]);
         let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>;
         if (documentId) {
-            exportState = await RichTextUtils.GoogleDocs.Import(documentId);
+            exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
         }
         UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
     }
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 555c41b67..ab5e677c8 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -14,9 +14,10 @@ import { schema } from "../client/util/RichTextSchema";
 import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
 import { SchemaHeaderField } from "./SchemaHeaderField";
 import { DocServer } from "../client/DocServer";
-import { Cast } from "./Types";
+import { Cast, StrCast } from "./Types";
 import { Id } from "./FieldSymbols";
 import { DocumentView } from "../client/views/nodes/DocumentView";
+import { AssertionError } from "assert";
 
 export namespace RichTextUtils {
 
@@ -109,45 +110,78 @@ export namespace RichTextUtils {
             return { text, requests };
         };
 
+        interface ImageTemplate {
+            width: number;
+            title: string;
+            url: string;
+        }
+
+        const parseInlineObjects = async (document: docs_v1.Schema$Document): Promise<Map<string, ImageTemplate>> => {
+            const inlineObjectMap = new Map<string, ImageTemplate>();
+            const inlineObjects = document.inlineObjects;
+
+            if (inlineObjects) {
+                const objects = Object.keys(inlineObjects).map(objectId => inlineObjects[objectId]);
+                const mediaItems: MediaItem[] = objects.map(object => {
+                    const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
+                    const baseUrl = embeddedObject.imageProperties!.contentUri!;
+                    const filename = `upload_${Utils.GenerateGuid()}.png`;
+                    return { baseUrl, filename };
+                });
+
+                const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems });
+
+                if (uploads.length !== mediaItems.length) {
+                    throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
+                }
+
+                for (let i = 0; i < objects.length; i++) {
+                    const object = objects[i];
+                    const { fileNames } = uploads[i];
+                    const embeddedObject = object.inlineObjectProperties!.embeddedObject!;
+                    const size = embeddedObject.size!;
+                    const width = size.width!.magnitude!;
+                    const url = Utils.fileUrl(fileNames.clean);
+
+                    inlineObjectMap.set(object.objectId!, {
+                        title: embeddedObject.title || `Imported Image from ${document.title}`,
+                        width,
+                        url
+                    });
+                }
+            }
+            return inlineObjectMap;
+        };
+
         type BulletPosition = { value: number, sinks: number };
 
         interface MediaItem {
             baseUrl: string;
             filename: string;
-            width: number;
         }
-        export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
+
+        export const Import = async (documentId: GoogleApiClientUtils.Docs.DocumentId, textNote: Doc): Promise<Opt<GoogleApiClientUtils.Docs.ImportResult>> => {
             const document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
             if (!document) {
                 return undefined;
             }
-
+            const inlineObjectMap = await parseInlineObjects(document);
             const title = document.title!;
             const { text, paragraphs } = GoogleApiClientUtils.Docs.Utils.extractText(document);
             let state = FormattedTextBox.blankState();
             let structured = parseLists(paragraphs);
-            const inline = document.inlineObjects;
-            let inlineUrls: MediaItem[] = [];
-            if (inline) {
-                inlineUrls = Object.keys(inline).map(key => {
-                    const embedded = inline[key].inlineObjectProperties!.embeddedObject!;
-                    const baseUrl = embedded.imageProperties!.contentUri!;
-                    const filename = `upload_${Utils.GenerateGuid()}.png`;
-                    const width = embedded.size!.width!.magnitude!;
-                    return { baseUrl, filename, width };
-                });
-            }
 
             let position = 3;
             let lists: ListGroup[] = [];
             const indentMap = new Map<ListGroup, BulletPosition[]>();
             let globalOffset = 0;
-            const nodes = structured.map(element => {
+            const nodes: Node<any>[] = [];
+            for (let element of structured) {
                 if (Array.isArray(element)) {
                     lists.push(element);
                     let positions: BulletPosition[] = [];
                     let items = element.map(paragraph => {
-                        let item = listItem(state.schema, paragraph.runs);
+                        let item = listItem(state.schema, paragraph.contents);
                         let sinks = paragraph.bullet!;
                         positions.push({
                             value: position + globalOffset,
@@ -158,13 +192,26 @@ export namespace RichTextUtils {
                         return item;
                     });
                     indentMap.set(element, positions);
-                    return list(state.schema, items);
+                    nodes.push(list(state.schema, items));
                 } else {
-                    let paragraph = paragraphNode(state.schema, element.runs);
-                    position += paragraph.nodeSize;
-                    return paragraph;
+                    if (element.contents.some(child => "inlineObjectId" in child)) {
+                        let node: Node<any>;
+                        for (const child of element.contents) {
+                            if ("inlineObjectId" in child) {
+                                node = imageNode(state.schema, inlineObjectMap.get(child.inlineObjectId!)!, textNote);
+                            } else {
+                                node = paragraphNode(state.schema, [child]);
+                            }
+                            nodes.push(node);
+                            position += node.nodeSize;
+                        }
+                    } else {
+                        let paragraph = paragraphNode(state.schema, element.contents);
+                        nodes.push(paragraph);
+                        position += paragraph.nodeSize;
+                    }
                 }
-            });
+            }
             state = state.apply(state.tr.replaceWith(0, 2, nodes));
 
             let sink = sinkListItem(state.schema.nodes.list_item);
@@ -179,14 +226,6 @@ export namespace RichTextUtils {
                 }
             }
 
-            const uploads = await PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems: inlineUrls });
-            for (let i = 0; i < uploads.length; i++) {
-                const src = Utils.fileUrl(uploads[i].fileNames.clean);
-                const width = inlineUrls[i].width;
-                const imageNode = schema.nodes.image.create({ src, width });
-                state = state.apply(state.tr.insert(0, imageNode));
-            }
-
             return { title, text, state };
         };
 
@@ -226,6 +265,22 @@ export namespace RichTextUtils {
             return schema.node("paragraph", null, fragment);
         };
 
+        const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
+            const { url: src, width } = image;
+            let docid: string;
+            const guid = Utils.GenerateDeterministicGuid(src);
+            const backingDocId = StrCast(textNote[guid]);
+            if (!backingDocId) {
+                const backingDoc = Docs.Create.ImageDocument(src, { width: 300, height: 300 });
+                DocumentView.makeCustomViewClicked(backingDoc);
+                docid = backingDoc[Id];
+                textNote[guid] = docid;
+            } else {
+                docid = backingDocId;
+            }
+            return schema.node("image", { src, width, docid });
+        };
+
         const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
             let text = run.content!.removeTrailingNewlines();
             return text.length ? schema.text(text, styleToMarks(schema, run.textStyle)) : undefined;
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index f582cebd2..3ab9ba90f 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -118,28 +118,44 @@ export namespace DownloadUtils {
     const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
     const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
 
-    export const UploadImage = async (url: string, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => {
-        const resolved = filename ? sanitize(filename) : generate(prefix, url);
-        let extension = path.extname(url) || path.extname(resolved);
+    export interface InspectionResults {
+        isLocal: boolean;
+        stream: any;
+        normalizedUrl: string;
+        contentSize: number;
+        contentType: string;
+    }
+
+    export const InspectImage = async (url: string) => {
+        const { isLocal, stream, normalized: normalizedUrl } = classify(url);
+        const metadata = (await new Promise<any>((resolve, reject) => {
+            request.head(url, async (error, res) => {
+                if (error) {
+                    return reject(error);
+                }
+                resolve(res);
+            });
+        })).headers;
+        return {
+            contentSize: parseInt(metadata[size]),
+            contentType: metadata[type],
+            isLocal,
+            stream,
+            normalizedUrl
+        };
+    };
+
+    export const UploadImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<Opt<UploadInformation>> => {
+        const { isLocal, stream, normalizedUrl, contentSize, contentType } = metadata;
+        const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
+        let extension = path.extname(normalizedUrl) || path.extname(resolved);
         extension && (extension = extension.toLowerCase());
         let information: UploadInformation = {
             mediaPaths: [],
-            fileNames: { clean: resolved }
+            fileNames: { clean: resolved },
+            contentSize,
+            contentType,
         };
-        const { isLocal, stream, normalized } = classify(url);
-        url = normalized;
-        if (!isLocal) {
-            const metadata = (await new Promise<any>((resolve, reject) => {
-                request.head(url, async (error, res) => {
-                    if (error) {
-                        return reject(error);
-                    }
-                    resolve(res);
-                });
-            })).headers;
-            information.contentSize = parseInt(metadata[size]);
-            information.contentType = metadata[type];
-        }
         return new Promise<UploadInformation>(async (resolve, reject) => {
             const resizers = [
                 { resizer: sharp().rotate(), suffix: "_o" },
@@ -164,7 +180,7 @@ export namespace DownloadUtils {
                         const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
                         information.mediaPaths.push(mediaPath = uploadDirectory + filename);
                         information.fileNames[suffix] = filename;
-                        stream(url).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
+                        stream(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
                             .on('close', resolve)
                             .on('error', reject);
                     });
@@ -172,7 +188,7 @@ export namespace DownloadUtils {
             }
             if (!isLocal || nonVisual) {
                 await new Promise<void>(resolve => {
-                    stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+                    stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
                 });
             }
             resolve(information);
diff --git a/src/server/apis/google/existing_uploads.json b/src/server/apis/google/existing_uploads.json
new file mode 100644
index 000000000..05c20c33b
--- /dev/null
+++ b/src/server/apis/google/existing_uploads.json
@@ -0,0 +1 @@
+{"23625":{"mediaPaths":["C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_o.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_s.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_m.png","C:\\Users\\avd\\Desktop\\Sam\\Dash-Web\\src\\server\\public\\files\\upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_l.png"],"fileNames":{"clean":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180.png","_o":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_o.png","_s":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_s.png","_m":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_m.png","_l":"upload_7e2d5fef-860a-49a8-b9ec-b91f28073180_l.png"},"contentSize":23625,"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 4f2fb0f9d..c10b0797f 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GlyEB-6kaRm7dCD9x3j1b5AyujXvfpS5NWuJQwy6UKLO06KYXcF2e5XaCxvR7QJgH3Pn2iu3btjYrrJxNNaLffgEszcJHNsN_5IIWJBA4sdG6KLW63MmFwfV4U1hyQ","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":1568573667294}
\ No newline at end of file
+{"access_token":"ya29.ImCFB_ghOybVB6A4HvIIwIlyGyZw6wOymdwJyWJJECIpCmFTHNEzOAfP98KFzm5OUV2zZNS5Wx1iUT1xYWW35PY7NoZc7PWwjzmOaGkMzDm7_fxpsgjT0StdvEwTJprFIv0","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":1568590984976}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 2e60d9be7..07ce4b6f0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -46,6 +46,7 @@ const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
 const probe = require("probe-image-size");
 import * as qs from 'query-string';
+import { Opt } from '../new_fields/Doc';
 const extensions = require("../client/util/UtilExtensions");
 
 const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
@@ -580,7 +581,8 @@ app.post(
             for (const key in files) {
                 const { type, path: location, name } = files[key];
                 const filename = path.basename(location);
-                await UploadUtils.UploadImage(uploadDirectory + filename, filename).catch(() => console.log(`Unable to process ${filename}`));
+                const metadata = await UploadUtils.InspectImage(uploadDirectory + filename);
+                await UploadUtils.UploadImage(metadata, filename).catch(() => console.log(`Unable to process ${filename}`));
                 results.push({ name, type, path: `/files/${filename}` });
             }
             _success(res, results);
@@ -884,14 +886,30 @@ const prefix = "google_photos_";
 const downloadError = "Encountered an error while executing downloads.";
 const requestError = "Unable to execute download: the body's media items were malformed.";
 
+app.get("/gapiCleanup", (req, res) => {
+    write_text_file(file, "");
+    res.redirect(RouteStore.delete);
+});
+
+const file = "./apis/google/existing_uploads.json";
 app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
     const contents: { mediaItems: MediaItem[] } = req.body;
     if (contents) {
-        const pending = contents.mediaItems.map(item =>
-            UploadUtils.UploadImage(item.baseUrl, item.filename, prefix)
-        );
-        const completed = await Promise.all(pending).catch(error => _error(res, downloadError, error));
-        Array.isArray(completed) && _success(res, completed);
+        const completed: Opt<UploadUtils.UploadInformation>[] = [];
+        const content = await read_text_file(file);
+        let existing = content.length ? JSON.parse(content) : {};
+        for (let item of contents.mediaItems) {
+            const { contentSize, ...attributes } = await UploadUtils.InspectImage(item.baseUrl);
+            const found: UploadUtils.UploadInformation = existing[contentSize];
+            if (!found) {
+                const upload = await UploadUtils.UploadImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
+                upload && completed.push(existing[contentSize] = upload);
+            } else {
+                completed.push(found);
+            }
+        }
+        await write_text_file(file, JSON.stringify(existing));
+        _success(res, completed);
         return;
     }
     _invalid(res, requestError);
-- 
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/views/nodes/FormattedTextBox.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 aac21c9c7471772125387a1df24853c6b109a76e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 30 Sep 2019 00:11:41 -0400
Subject: fixed default link following and following from textbox link.

---
 src/client/views/linking/LinkFollowBox.tsx  | 17 +++++++++--------
 src/client/views/linking/LinkMenuItem.tsx   |  3 ++-
 src/client/views/nodes/FormattedTextBox.tsx |  2 +-
 3 files changed, 12 insertions(+), 10 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index cad404d1f..72fff8e53 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -19,6 +19,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faTimes } from '@fortawesome/free-solid-svg-icons';
 import { docs_v1 } from "googleapis";
 import { Utils } from "../../../Utils";
+import { Link } from "@react-pdf/renderer";
 
 enum FollowModes {
     OPENTAB = "Open in Tab",
@@ -196,10 +197,10 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     }
 
-    _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
+    static _addDocTab: (undefined | ((doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean));
 
-    setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
-        this._addDocTab = addFunc;
+    static setAddDocTab = (addFunc: (doc: Doc, dataDoc: Opt<Doc>, where: string) => boolean) => {
+        LinkFollowBox._addDocTab = addFunc;
     }
 
     @undoBatch
@@ -212,7 +213,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 options.context.panX = newPanX;
                 options.context.panY = newPanY;
             }
-            (this._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
+            (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "onRight");
 
             if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
 
@@ -225,7 +226,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     openLinkRight = () => {
         if (LinkFollowBox.destinationDoc) {
             let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
-            (this._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
+            (LinkFollowBox._addDocTab || this.props.addDocTab)(alias, undefined, "onRight");
             this.highlightDoc();
             SelectionManager.DeselectAll();
         }
@@ -246,7 +247,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             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(); };
+            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);
@@ -281,7 +282,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.props.addDocTab)(fullScreenAlias, undefined, "inTab");
+            (LinkFollowBox._addDocTab || this.props.addDocTab)(fullScreenAlias, undefined, "inTab");
 
             this.highlightDoc();
             SelectionManager.DeselectAll();
@@ -298,7 +299,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 options.context.panX = newPanX;
                 options.context.panY = newPanY;
             }
-            (this._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
+            (LinkFollowBox._addDocTab || this.props.addDocTab)(options.context, undefined, "inTab");
             if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
 
             this.highlightDoc();
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 77fa063f3..e5a4a68bf 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -70,7 +70,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         if (LinkFollowBox.Instance !== undefined) {
             LinkFollowBox.Instance.props.Document.isMinimized = false;
             LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
-            LinkFollowBox.Instance.setAddDocTab(this.props.addDocTab);
+            LinkFollowBox.setAddDocTab(this.props.addDocTab);
         }
         e.stopPropagation();
     }
@@ -95,6 +95,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     @action.bound
     async followDefault() {
         if (LinkFollowBox.Instance !== undefined) {
+            LinkFollowBox.setAddDocTab(this.props.addDocTab);;
             LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
             LinkFollowBox.Instance.defaultLinkBehavior();
         }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index db5814e7c..449f56b16 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -884,7 +884,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                                     }
                                 }
                                 if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
-                                    DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
+                                    DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), undefined, targetContext);
                                 } else if (jumpToDoc) {
                                     DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
                                 } else {
-- 
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/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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 2413d93a31ad4c97e09f79b97bc19346e72a1537 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 3 Oct 2019 16:31:31 -0400
Subject: improved search results to avoid showing aliases.  improved Pdf
 results display.

---
 src/client/util/SearchUtil.ts               | 34 ++++++++++++++++++++++++-----
 src/client/views/nodes/FormattedTextBox.tsx |  5 -----
 src/client/views/pdf/Annotation.tsx         |  2 +-
 src/client/views/pdf/PDFViewer.scss         | 17 ++++++++++-----
 src/client/views/pdf/PDFViewer.tsx          | 33 +++++++++++++++++++---------
 src/client/views/search/SearchItem.scss     | 13 +++++++++--
 src/client/views/search/SearchItem.tsx      | 19 ++++++++++------
 7 files changed, 87 insertions(+), 36 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts
index e37f1f90d..6706dcb89 100644
--- a/src/client/util/SearchUtil.ts
+++ b/src/client/util/SearchUtil.ts
@@ -41,23 +41,45 @@ export namespace SearchUtil {
         }
 
         let { ids, numFound, highlighting } = result;
-        let lines: string[][] = ids.map(i => []);
 
         let txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), {
             qs: { ...options, q: query },
         }));
+
         let fileids = txtresult ? txtresult.ids : [];
+        let newIds: string[] = [];
+        let newLines: string[][] = [];
         await Promise.all(fileids.map(async (tr: string, i: number) => {
             let docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query
             let docResult = JSON.parse(await rp.get(Utils.prepend("/search"), { qs: { ...options, q: docQuery } }));
-            ids.push(...docResult.ids);
-            lines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
-            numFound += docResult.numFound;
+            newIds.push(...docResult.ids);
+            newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i]));
         }));
 
+
+        let theDocs: Doc[] = [];
+        let theLines: string[][] = [];
+        const textDocMap = await DocServer.GetRefFields(newIds);
+        const textDocs = newIds.map((id: string) => textDocMap[id]).map(doc => doc as Doc);
+        for (let i = 0; i < textDocs.length; i++) {
+            let testDoc = textDocs[i];
+            if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+                theDocs.push(Doc.GetProto(testDoc));
+                theLines.push(newLines[i].map(line => line.replace(query, query.toUpperCase())));
+            }
+        }
+
         const docMap = await DocServer.GetRefFields(ids);
-        const docs = ids.map((id: string) => docMap[id]).filter((doc: any) => doc instanceof Doc && doc.type !== DocumentType.KVP);
-        return { docs, numFound, highlighting, lines };
+        const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc);
+        for (let i = 0; i < ids.length; i++) {
+            let testDoc = docs[i];
+            if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1) {
+                theDocs.push(testDoc);
+                theLines.push([]);
+            }
+        }
+
+        return { docs: theDocs, numFound: theDocs.length, highlighting, lines: theLines };
     }
 
     export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 9347868b3..c37258f50 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -474,11 +474,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._searchReactionDisposer = reaction(() => {
             return StrCast(this.props.Document.search_string);
         }, searchString => {
-            const fieldkey = 'preview';
-            let preview = false;
-            // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
-            //     preview = true;
-            // }
             if (searchString) {
                 this.highlightSearchTerms([searchString]);
             }
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 26de12a0d..134e757d1 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -122,7 +122,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
                 left: this.props.x,
                 width: this.props.width,
                 height: this.props.height,
-                transition: "background-color 0.5s, opacity 0.5s",
+                transition: "opacity 0.5s",
                 opacity: this._brushed ? 0.5 : undefined,
                 backgroundColor: this._brushed ? "orange" : StrCast(this.props.document.color)
             }} />);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 8027e93a3..a71e4f81e 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -11,10 +11,17 @@
     //     transform: scale(0.75);
     //     transform-origin: top left;
     // }
-    // .textLayer {
-    //     transform: scale(0.75);
-    //     transform-origin: top left;
-    // }
+    .textLayer {
+        
+        mix-blend-mode: multiply;
+        opacity: 0.9;
+    }
+    .textLayer .highlight {
+        background-color: yellow;
+    }
+    .textLayer .highlight.selected {
+        background-color: orange;
+    }
 
     .page {
         position: relative;
@@ -30,7 +37,6 @@
     }
 
     .pdfViewer-overlay {
-        transform: scale(2.14359);
         transform-origin: left top;
         position: absolute;
         top: 0px;
@@ -43,6 +49,7 @@
         top: 0;
         width: 100%;
         pointer-events: none;
+        mix-blend-mode: multiply;
 
         .pdfPage-annotationBox {
             position: absolute;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 20dfc4d8c..9ff3e1bd1 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -9,7 +9,7 @@ import { List } from "../../../new_fields/List";
 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 smoothScroll, { Utils, emptyFunction, returnOne, intersectRect } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
 import { CompiledScript, CompileScript } from "../../util/Scripting";
@@ -85,6 +85,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _selectionReactionDisposer?: IReactionDisposer;
     private _annotationReactionDisposer?: IReactionDisposer;
     private _filterReactionDisposer?: IReactionDisposer;
+    private _searchReactionDisposer?: IReactionDisposer;
     private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _selectionText: string = "";
@@ -103,12 +104,24 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this._annotations.filter(anno => this._script.run({ this: anno }, console.log, true).result);
     }
 
+    _lastSearch: string = "";
     componentDidMount = async () => {
         // change the address to be the file address of the PNG version of each page
         // 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.props.startupLive && this.setupPdfJsViewer();
+        this._searchReactionDisposer = reaction(() => StrCast(this.props.Document.search_string), searchString => {
+            if (searchString) {
+                this.search(searchString, true);
+                this._lastSearch = searchString;
+            }
+            else {
+                setTimeout(() => this._lastSearch === "mxytzlaf" && this.search("mxytzlaf", true), 200); // bcz: how do we clear search highlights?
+                this._lastSearch && (this._lastSearch = "mxytzlaf");
+            }
+        }, { fireImmediately: true });
+
         this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => (this.props.isSelected() && SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(), { fireImmediately: true });
         this._reactionDisposer = reaction(
             () => this.props.Document.scrollY,
@@ -130,6 +143,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         this._annotationReactionDisposer && this._annotationReactionDisposer();
         this._filterReactionDisposer && this._filterReactionDisposer();
         this._selectionReactionDisposer && this._selectionReactionDisposer();
+        this._searchReactionDisposer && this._searchReactionDisposer();
         document.removeEventListener("copy", this.copy);
     }
 
@@ -298,12 +312,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
-        this.allAnnotations.forEach(d => Doc.UnBrushDoc(d));
-        let windowHgt = this.props.PanelHeight() / this.props.ContentScaling();
-        let scrollRange = this._mainCont.current!.scrollHeight - windowHgt;
-        let pgScroll = scrollRange / this._pageSizes.length;
-        this._mainCont.current!.scrollTo(0, NumCast(scrollToAnnotation.y) - pgScroll / 2);
-        Doc.BrushDoc(scrollToAnnotation);
+        let offset = this.visibleHeight() / 2 * 96 / 72;
+        this._mainCont.current && smoothScroll(500, this._mainCont.current, NumCast(scrollToAnnotation.y) - offset);
+        Doc.linkFollowHighlight(scrollToAnnotation);
     }
 
     sendAnnotations = (page: number) => {
@@ -454,7 +465,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 if (rect/* && rect.width !== this._mainCont.current.getBoundingClientRect().width && rect.height !== this._mainCont.current.getBoundingClientRect().height / this.props.pdf.numPages*/) {
                     let scaleY = this._mainCont.current.offsetHeight / boundingRect.height;
                     let scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
-                    if (rect.width !== this._mainCont.current.clientWidth) {
+                    if (rect.width !== this._mainCont.current.clientWidth &&
+                        (i == 0 || !intersectRect(clientRects[i], clientRects[i - 1]))) {
                         let annoBox = document.createElement("div");
                         annoBox.className = "pdfPage-annotationBox";
                         // transforms the positions from screen onto the pdf div
@@ -659,17 +671,18 @@ export class PDFViewer extends React.Component<IViewerProps> {
     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})` }}>
-                {this.annotationLayer}
                 <CollectionFreeFormView {...this.props}
                     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)}
-                    VisibleHeight={() => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96}
+                    VisibleHeight={this.visibleHeight}
                     focus={this.props.focus}
                     isSelected={this.props.isSelected}
                     select={emptyFunction}
diff --git a/src/client/views/search/SearchItem.scss b/src/client/views/search/SearchItem.scss
index 273d49349..62715c5eb 100644
--- a/src/client/views/search/SearchItem.scss
+++ b/src/client/views/search/SearchItem.scss
@@ -4,7 +4,6 @@
     display: flex;
     flex-direction: row-reverse;
     justify-content: flex-end;
-    height: 70px;
     z-index: 0;
 }
 
@@ -15,9 +14,12 @@
     border-color: $intermediate-color;
     border-bottom-style: solid;
     padding: 10px;
-    height: 70px;
+    min-height: 70px;
+    max-height: 150px;
+    height: auto;
     z-index: 0;
     display: inline-block;
+    overflow: auto;
 
     .main-search-info {
         display: flex;
@@ -26,6 +28,7 @@
 
         .search-title-container {
             width: 100%;
+            overflow: hidden;
 
             .search-title {
                 text-transform: uppercase;
@@ -181,6 +184,12 @@
     background: $lighter-alt-accent;
 }
 
+.search-highlighting {
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: pre;
+}
+
 .searchBox-instances {
     float: left;
     opacity: 1;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index 4d021216d..a7822ed46 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -4,13 +4,10 @@ import { faCaretUp, faChartBar, faFile, faFilePdf, faFilm, faFingerprint, faGlob
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, HeightSym, WidthSym } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import { RichTextField } from "../../../new_fields/RichTextField";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../../Utils";
-import { DocServer } from "../../DocServer";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragManager, SetupDrag } from "../../util/DragManager";
@@ -228,6 +225,12 @@ export class SearchItem extends React.Component<SearchItemProps> {
     @action
     pointerDown = (e: React.PointerEvent) => { e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e); }
 
+    nextHighlight = (e: React.PointerEvent) => {
+        e.preventDefault(); e.button === 0 && SearchBox.Instance.openSearch(e);
+        let sstring = StrCast(this.props.doc.search_string);
+        this.props.doc.search_string = "";
+        setTimeout(() => this.props.doc.search_string = sstring, 0);
+    }
     highlightDoc = (e: React.PointerEvent) => {
         if (this.props.doc.type === DocumentType.LINK) {
             if (this.props.doc.anchor1 && this.props.doc.anchor2) {
@@ -240,6 +243,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
         } else {
             Doc.BrushDoc(this.props.doc);
         }
+        e.stopPropagation();
     }
 
     unHighlightDoc = (e: React.PointerEvent) => {
@@ -283,13 +287,14 @@ export class SearchItem extends React.Component<SearchItemProps> {
         const doc2 = Cast(this.props.doc.anchor2, Doc);
         return (
             <div className="search-overview" onPointerDown={this.pointerDown} onContextMenu={this.onContextMenu}>
-                <div className="search-item" onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
-                    onClick={this.onClick} onPointerDown={this.pointerDown} >
+                <div className="search-item" onPointerDown={this.nextHighlight} onPointerEnter={this.highlightDoc} onPointerLeave={this.unHighlightDoc} id="result"
+                    onClick={this.onClick}>
                     <div className="main-search-info">
                         <div title="Drag as document" onPointerDown={this.onPointerDown} style={{ marginRight: "7px" }}> <FontAwesomeIcon icon="file" size="lg" /> </div>
                         <div className="search-title-container">
                             <div className="search-title">{StrCast(this.props.doc.title)}</div>
-                            <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? "Text:" + this.props.lines[0] : ""}</div>
+                            <div className="search-highlighting">{this.props.highlighting.length ? "Matched fields:" + this.props.highlighting.join(", ") : this.props.lines.length ? this.props.lines[0] : ""}</div>
+                            {this.props.lines.filter((m, i) => i).map((l, i) => <div id={i.toString()} className="search-highlighting">`${l}`</div>)}
                         </div>
                         <div className="search-info" style={{ width: this._useIcons ? "15%" : "400px" }}>
                             <div className={`icon-${this._useIcons ? "icons" : "live"}`}>
-- 
cgit v1.2.3-70-g09d2


From 48ac0b54cfe29b97a7add72b2369bfc2896f98f7 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 3 Oct 2019 17:07:33 -0400
Subject: partially fixed editing text boxes in stacking views.  tweaked link
 following from text boxes.

---
 src/client/documents/Documents.ts           | 3 +--
 src/client/util/DocumentManager.ts          | 4 ++--
 src/client/views/MainOverlayTextBox.tsx     | 3 ++-
 src/client/views/nodes/FormattedTextBox.tsx | 4 ++--
 4 files changed, 7 insertions(+), 7 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2d323ea4b..71b9038d4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -653,7 +653,7 @@ export namespace DocUtils {
             }
         });
     }
-    export function MakeLink(source: {doc:Doc,ctx?:Doc}, target: {doc:Doc,ctx?:Doc}, title: string = "", description: string = "", id?: string, anchored1?: boolean) {
+    export function MakeLink(source: { doc: Doc, ctx?: Doc }, target: { doc: Doc, ctx?: Doc }, title: string = "", description: string = "", id?: string) {
         let sv = DocumentManager.Instance.getDocumentView(source.doc);
         if (sv && sv.props.ContainingCollectionDoc === target.doc) return;
         if (target.doc === CurrentUserUtils.UserDocument) return undefined;
@@ -669,7 +669,6 @@ export namespace DocUtils {
 
             linkDocProto.anchor1 = source.doc;
             linkDocProto.anchor1Groups = new List<Doc>([]);
-            linkDocProto.anchor1anchored = anchored1;
             linkDocProto.anchor2 = target.doc;
             linkDocProto.anchor2Groups = new List<Doc>([]);
 
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5130db131..ffd311665 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -188,8 +188,8 @@ export class DocumentManager {
     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();
-        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 firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, doc));
+        const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, doc));
         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);
         const first = firstDocWithoutView ? [firstDocWithoutView] : firstDocs;
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 335cc609f..73eef9478 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -78,7 +78,8 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
         this._textTargetDiv = div;
         this._textHideOnLeave = FormattedTextBox.InputBoxOverlay && FormattedTextBox.InputBoxOverlay.props.hideOnLeave;
         if (div) {
-            this._textBottom = div.parentElement && getComputedStyle(div.parentElement).top !== "0px" ? true : false;
+            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";
         }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index c37258f50..f84301c10 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -184,7 +184,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({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", 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);
                 });
             });
         });
@@ -307,7 +307,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
                         e.stopPropagation();
                     }
-                } else {
+                } else if (de.mods === "CtrlKey") {
                     draggedDoc.isTemplate = true;
                     if (typeof (draggedDoc.layout) === "string") {
                         let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
-- 
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/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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 54f2067dbadb66e22249c1572bdc5d6d097f41d1 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 4 Oct 2019 17:01:09 -0400
Subject: restored tooltiptextmenu

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

(limited to 'src/client/views/nodes/FormattedTextBox.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/views/nodes/FormattedTextBox.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


From c4b4b8f8961bfd6deddd9da7e6a02990ad526c2f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 7 Oct 2019 01:27:52 -0400
Subject: small fixes to link clicking in text box.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  1 +
 src/client/views/nodes/FormattedTextBox.tsx        | 28 +++++++---------------
 2 files changed, 9 insertions(+), 20 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index bfd3e6481..38488f033 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -267,6 +267,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        if (e.nativeEvent.cancelBubble) return;
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
         if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
             document.removeEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index bdb7c2941..c87185a3e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -18,7 +18,7 @@ import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
 import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { numberRange, timenow, Utils } from '../../../Utils';
+import { numberRange, timenow, Utils, emptyFunction } from '../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -76,7 +76,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _proseRef?: HTMLDivElement;
     private _editorView: Opt<EditorView>;
     private _applyingChange: boolean = false;
-    private _linkClicked = "";
     private _nodeClicked: any;
     private _undoTyping?: UndoManager.Batch;
     private _searchReactionDisposer?: Lambda;
@@ -117,7 +116,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     public static getToolTip(ev: EditorView) {
-        return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev, undefined);
+        return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev);
     }
 
     @undoBatch
@@ -750,7 +749,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._searchReactionDisposer && this._searchReactionDisposer();
         this._editorView && this._editorView.destroy();
     }
-    public static firstTarget: () => void;
+    public static firstTarget: () => void = emptyFunction;
     onPointerDown = (e: React.PointerEvent): void => {
         if ((e.nativeEvent as any).formattedHandled) return;
         (e.nativeEvent as any).formattedHandled = true;
@@ -800,16 +799,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     onClick = (e: React.MouseEvent): void => {
-        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 location: string;
             if ((e.target as any).attributes.location) {
                 location = (e.target as any).attributes.location.value;
             }
-            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 pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
             let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
             if (node) {
@@ -821,18 +816,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
             if (href) {
                 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 =>
+                    let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+                    if (linkClicked) {
+                        DocServer.GetRefField(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();
+                                DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false);
+                        });
                     }
                 } else {
                     let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
                     this.props.addDocument && this.props.addDocument(webDoc);
-                    this._linkClicked = webDoc[Id];
                 }
                 e.stopPropagation();
                 e.preventDefault();
@@ -862,11 +855,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
         }
         this._editorView!.focus();
-        if (this._linkClicked) {
-            this._linkClicked = "";
-            e.preventDefault();
-            e.stopPropagation();
-        }
     }
     onMouseDown = (e: React.MouseEvent): void => {
         if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
-- 
cgit v1.2.3-70-g09d2


From c7746aea51bd6891ac10f152bd8f09e5d8ddcfd9 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 7 Oct 2019 08:16:34 -0400
Subject: initial tests

---
 src/client/util/RichTextSchema.tsx                                 | 7 +++++++
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx      | 2 +-
 src/client/views/nodes/DocumentView.tsx                            | 3 +++
 src/client/views/nodes/FormattedTextBox.tsx                        | 4 +++-
 4 files changed, 14 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 1109fd292..f5749a01b 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -770,8 +770,15 @@ export class DashDocView {
         let self = this;
         this._dashSpan.onclick = function (e: any) {
             FormattedTextBox.firstTarget && FormattedTextBox.firstTarget();
+        };
+        this._dashSpan.onpointermove = function (e: any) {
+            (e as any).formattedHandled = true;
+        };
+        this._dashSpan.onpointerup = function (e: any) {
             e.stopPropagation();
         };
+        this._dashSpan.onpointerdown = function (e: any) {
+        };
         this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 38488f033..cf76945e0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -267,7 +267,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble) return;
+        if (e.nativeEvent.cancelBubble || (e.nativeEvent as any).formattedHandled) return;
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
         if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
             document.removeEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e50008fdf..5a9202d40 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -202,6 +202,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
             preventDefault && e.preventDefault();
         }
+        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
     }
 
     buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
@@ -249,8 +250,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         document.removeEventListener("pointerup", this.onPointerUp);
         document.addEventListener("pointermove", this.onPointerMove);
         document.addEventListener("pointerup", this.onPointerUp);
+        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
     }
     onPointerMove = (e: PointerEvent): void => {
+        if ((e as any).formattedHandled) { e.stopPropagation(); return; }
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove); // stop listening to pointerMove if something else has stopPropagated it (e.g., the MarqueeView)
         }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index c87185a3e..9396e3ea1 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -751,7 +751,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
     public static firstTarget: () => void = emptyFunction;
     onPointerDown = (e: React.PointerEvent): void => {
-        if ((e.nativeEvent as any).formattedHandled) return;
+        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); 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));
@@ -799,6 +799,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     onClick = (e: React.MouseEvent): void => {
+        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
+        (e.nativeEvent as any).formattedHandled = true;
         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 location: string;
-- 
cgit v1.2.3-70-g09d2


From 8c9d58c506d986d5dbdd087b429a449c8283ac12 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 7 Oct 2019 12:59:41 -0400
Subject: many changes to support nested documents within a  text box

---
 src/client/util/RichTextSchema.tsx                 | 86 ++++++++++++----------
 src/client/util/TooltipTextMenu.tsx                |  9 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 39 +++++-----
 5 files changed, 73 insertions(+), 66 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index f5749a01b..29ec5b23c 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,24 +1,23 @@
+import { action, observable, runInAction, reaction, IReactionDisposer } from "mobx";
 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 { EditorState, TextSelection, NodeSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
 import { StepMap } from "prosemirror-transform";
 import { EditorView } from "prosemirror-view";
-import { Doc } from "../../new_fields/Doc";
-import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import * as ReactDOM from 'react-dom';
+import { Doc, WidthSym, HeightSym } from "../../new_fields/Doc";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils } from "../../Utils";
 import { DocServer } from "../DocServer";
+import { DocumentView } from "../views/nodes/DocumentView";
 import { DocumentManager } from "./DocumentManager";
 import ParagraphNodeSpec from "./ParagraphNodeSpec";
-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";
+import React = require("react");
+import { BoolCast } from "../../new_fields/Types";
+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];
@@ -716,7 +715,16 @@ export class DashDocView {
     _handle: HTMLElement;
     _dashSpan: HTMLDivElement;
     _outer: HTMLElement;
-    constructor(node: any, view: any, getPos: any, addDocTab: any) {
+    _dashDoc: Doc | undefined;
+    _reactionDisposer: IReactionDisposer | undefined;
+    _textBox: FormattedTextBox;
+
+    getDocTransform = () => {
+        let { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
+        return new Transform(-translateX, -translateY, 1).scale(1 / scale);
+    }
+    constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        this._textBox = tbox;
         this._handle = document.createElement("span");
         this._dashSpan = document.createElement("div");
         this._outer = document.createElement("span");
@@ -741,19 +749,28 @@ export class DashDocView {
         this._handle.style.right = "-10px";
         DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
             if (dashDoc instanceof Doc) {
-                let scale = () => 100 / NumCast(dashDoc.nativeWidth, 100);
+                self._dashDoc = dashDoc;
+                if (node.attrs.width !== dashDoc.width + "px" || node.attrs.height !== dashDoc.height + "px") {
+                    view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
+                }
+                this._reactionDisposer && this._reactionDisposer();
+                this._reactionDisposer = reaction(() => {
+                    return dashDoc[HeightSym]();
+                }, () => {
+                    this._dashSpan.style.height = this._outer.style.height = dashDoc[HeightSym]() + "px";
+                });
                 ReactDOM.render(<DocumentView
-                    fitToBox={true}
+                    fitToBox={BoolCast(dashDoc.fitToBox)}
                     Document={dashDoc}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
                     ruleProvider={undefined}
-                    ScreenToLocalTransform={Transform.Identity}
-                    addDocTab={returnFalse}
+                    ScreenToLocalTransform={this.getDocTransform}
+                    addDocTab={self._textBox.props.addDocTab}
                     pinToPres={returnFalse}
                     renderDepth={1}
-                    PanelWidth={() => 100}
-                    PanelHeight={() => 100}
+                    PanelWidth={self._dashDoc![WidthSym]}
+                    PanelHeight={self._dashDoc![HeightSym]}
                     focus={emptyFunction}
                     backgroundColor={returnEmptyString}
                     parentActive={returnFalse}
@@ -763,24 +780,16 @@ export class DashDocView {
                     getScale={returnOne}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined}
-                    ContentScaling={scale}
-                ></DocumentView>, this._dashSpan);
+                    ContentScaling={returnOne}
+                />, this._dashSpan);
             }
         });
         let self = this;
-        this._dashSpan.onclick = function (e: any) {
-            FormattedTextBox.firstTarget && FormattedTextBox.firstTarget();
-        };
-        this._dashSpan.onpointermove = function (e: any) {
-            (e as any).formattedHandled = true;
-        };
-        this._dashSpan.onpointerup = function (e: any) {
-            e.stopPropagation();
-        };
-        this._dashSpan.onpointerdown = function (e: any) {
-        };
+        this._outer.onpointerenter = function (e: any) { self.selectNode(); };
+        this._outer.onpointerleave = function (e: any) { self.deselectNode(); };
         this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+        this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
         this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
         this._handle.onpointerdown = function (e: any) {
             e.preventDefault();
@@ -789,19 +798,20 @@ export class DashDocView {
             const startY = e.pageY;
             const startWidth = parseFloat(node.attrs.width);
             const startHeight = parseFloat(node.attrs.height);
-            const onpointermove = (e: any) => {
-                const diffInPx = e.pageX - startX;
-                const diffInPy = e.pageY - startY;
-                self._outer.style.width = `${startWidth + diffInPx}`;
-                self._outer.style.height = `${startHeight + diffInPy}`;
-            };
+            const onpointermove = action((e: any) => {
+                let { scale } = Utils.GetScreenTransform(self._handle as HTMLElement);
+                const diffInPx = (e.pageX - startX) / scale;
+                const diffInPy = (e.pageY - startY) / scale;
+                self._dashDoc!.width = startWidth + diffInPx;
+                self._dashDoc!.height = startHeight + diffInPy;
+                self._outer.style.width = `${self._dashDoc!.width}`;
+                self._outer.style.height = `${self._dashDoc!.height}`;
+            });
 
             const onpointerup = () => {
                 document.removeEventListener("pointermove", onpointermove);
                 document.removeEventListener("pointerup", onpointerup);
-                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);
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 9116ef995..55c6e6609 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -186,7 +186,7 @@ export class TooltipTextMenu {
 
         this.updateListItemDropdown(":", this.listTypeBtnDom);
 
-        this.update(view, undefined, undefined);
+        this.updateInternal(view, undefined, undefined);
         TooltipTextMenu.Toolbar = this.wrapper;
     }
     public static Toolbar: HTMLDivElement | undefined;
@@ -439,7 +439,7 @@ export class TooltipTextMenu {
         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() });
+        let newNode = state.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;
     }
@@ -587,7 +587,7 @@ export class TooltipTextMenu {
             class: "summarize",
             execEvent: "",
             run: (state, dispatch) => {
-                TooltipTextMenu.insertStar(state, dispatch);
+                TooltipTextMenu.insertStar(this.view.state, this.view.dispatch);
             }
 
         });
@@ -849,8 +849,9 @@ export class TooltipTextMenu {
         }
     }
 
+    update(view: EditorView, lastState: EditorState | undefined) { this.updateInternal(view, lastState, this.editorProps) }
     //updates the tooltip menu when the selection changes
-    update(view: EditorView, lastState: EditorState | undefined, props: any) {
+    private updateInternal(view: EditorView, lastState: EditorState | undefined, props: any) {
         this.view = view;
         let state = view.state;
         props && (this.editorProps = props);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cf76945e0..38488f033 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -267,7 +267,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble || (e.nativeEvent as any).formattedHandled) return;
+        if (e.nativeEvent.cancelBubble) return;
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
         if (e.button === 0 && !e.shiftKey && !e.altKey && (!this.isAnnotationOverlay || this.zoomScaling() !== 1) && this.props.active()) {
             document.removeEventListener("pointermove", this.onPointerMove);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5a9202d40..a670d280d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -202,7 +202,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             }
             preventDefault && e.preventDefault();
         }
-        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); }
     }
 
     buttonClick = async (altKey: boolean, ctrlKey: boolean) => {
@@ -234,7 +233,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     onPointerDown = (e: React.PointerEvent): void => {
-        if (e.nativeEvent.cancelBubble) return;
+        if (e.nativeEvent.cancelBubble && e.button === 0) return;
         this._downX = e.clientX;
         this._downY = e.clientY;
         this._hitTemplateDrag = false;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 9396e3ea1..c030d4bce 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -268,9 +268,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
                 node = model.create({ src: url, docid: link[Id] });
             } else {
+                let alias = Doc.MakeAlias(target);
+                alias.fitToBox = true;
                 node = schema.nodes.dashDoc.create({
                     width: target[WidthSym](), height: target[HeightSym](),
-                    title: "dashDoc", docid: target[Id],
+                    title: "dashDoc", docid: alias[Id],
                     float: "none"
                 });
             }
@@ -700,7 +702,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); },
+                    dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); },
                     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(); },
@@ -749,10 +751,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._searchReactionDisposer && this._searchReactionDisposer();
         this._editorView && this._editorView.destroy();
     }
-    public static firstTarget: () => void = emptyFunction;
     onPointerDown = (e: React.PointerEvent): void => {
-        if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); 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) {
@@ -764,16 +763,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         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 => {
@@ -832,7 +821,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 e.stopPropagation();
                 e.preventDefault();
             }
-
         }
         // 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) {
@@ -858,14 +846,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
         this._editorView!.focus();
     }
-    onMouseDown = (e: React.MouseEvent): void => {
-        if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
-            e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+    onMouseUp = (e: React.MouseEvent): void => {
+        e.stopPropagation();
+
+        // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there are nested prosemirrors.  We only want the lowest level prosemirror to be invoked.
+        if ((this._editorView as any).mouseDown) {
+            let originalUpHandler = (this._editorView as any).mouseDown.up;
+            (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler);
+            (this._editorView as any).mouseDown.up = (e: MouseEvent) => {
+                !(e as any).formattedHandled && originalUpHandler(e);
+                e.stopPropagation();
+                (e as any).formattedHandled = true;
+            };
+            (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up);
         }
     }
 
     tooltipTextMenuPlugin() {
-        let myprops = this.props;
         let self = FormattedTextBox;
         return new Plugin({
             view(_editorView) {
@@ -944,7 +941,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 onBlur={this.onBlur}
                 onPointerUp={this.onPointerUp}
                 onPointerDown={this.onPointerDown}
-                onMouseDown={this.onMouseDown}
+                onMouseUp={this.onMouseUp}
                 onWheel={this.onPointerWheel}
                 onPointerEnter={action(() => this._entered = true)}
                 onPointerLeave={action(() => this._entered = false)}
-- 
cgit v1.2.3-70-g09d2


From 463cf0c4a6225fe01492510238973eabc1577fd5 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 7 Oct 2019 14:48:54 -0400
Subject: added deleting ndested nodes from text boxes

---
 src/client/util/RichTextSchema.tsx          | 72 ++++++-----------------------
 src/client/views/nodes/FormattedTextBox.tsx | 21 +++------
 2 files changed, 21 insertions(+), 72 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 29ec5b23c..432e0c2fb 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -16,7 +16,7 @@ import { DocumentManager } from "./DocumentManager";
 import ParagraphNodeSpec from "./ParagraphNodeSpec";
 import { Transform } from "./Transform";
 import React = require("react");
-import { BoolCast } from "../../new_fields/Types";
+import { BoolCast, NumCast } from "../../new_fields/Types";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
@@ -712,7 +712,6 @@ export class ImageResizeView {
 }
 
 export class DashDocView {
-    _handle: HTMLElement;
     _dashSpan: HTMLDivElement;
     _outer: HTMLElement;
     _dashDoc: Doc | undefined;
@@ -721,11 +720,11 @@ export class DashDocView {
 
     getDocTransform = () => {
         let { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
-        return new Transform(-translateX, -translateY, 1).scale(1 / scale);
+        return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
     }
+    contentScaling = () => NumCast(this._dashDoc!.nativeWidth) > 0 && !this._dashDoc!.ignoreAspect ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!.nativeWidth) : 1;
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this._textBox = tbox;
-        this._handle = document.createElement("span");
         this._dashSpan = document.createElement("div");
         this._outer = document.createElement("span");
         this._outer.style.position = "relative";
@@ -739,14 +738,11 @@ export class DashDocView {
         this._dashSpan.style.height = node.attrs.height;
         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";
+        let removeDoc = () => {
+            let ns = new NodeSelection(view.state.doc.resolve(getPos()));
+            view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+            return true;
+        }
         DocServer.GetRefField(node.attrs.docid).then(async dashDoc => {
             if (dashDoc instanceof Doc) {
                 self._dashDoc = dashDoc;
@@ -754,16 +750,15 @@ export class DashDocView {
                     view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc.width + "px", height: dashDoc.height + "px" }));
                 }
                 this._reactionDisposer && this._reactionDisposer();
-                this._reactionDisposer = reaction(() => {
-                    return dashDoc[HeightSym]();
-                }, () => {
+                this._reactionDisposer = reaction(() => dashDoc[HeightSym]() + dashDoc[WidthSym](), () => {
                     this._dashSpan.style.height = this._outer.style.height = dashDoc[HeightSym]() + "px";
+                    this._dashSpan.style.width = this._outer.style.width = dashDoc[WidthSym]() + "px";
                 });
                 ReactDOM.render(<DocumentView
                     fitToBox={BoolCast(dashDoc.fitToBox)}
                     Document={dashDoc}
                     addDocument={returnFalse}
-                    removeDocument={returnFalse}
+                    removeDocument={removeDoc}
                     ruleProvider={undefined}
                     ScreenToLocalTransform={this.getDocTransform}
                     addDocTab={self._textBox.props.addDocTab}
@@ -780,59 +775,20 @@ export class DashDocView {
                     getScale={returnOne}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined}
-                    ContentScaling={returnOne}
+                    ContentScaling={this.contentScaling}
                 />, this._dashSpan);
             }
         });
         let self = this;
-        this._outer.onpointerenter = function (e: any) { self.selectNode(); };
-        this._outer.onpointerleave = function (e: any) { self.deselectNode(); };
         this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
         this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
         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 = action((e: any) => {
-                let { scale } = Utils.GetScreenTransform(self._handle as HTMLElement);
-                const diffInPx = (e.pageX - startX) / scale;
-                const diffInPy = (e.pageY - startY) / scale;
-                self._dashDoc!.width = startWidth + diffInPx;
-                self._dashDoc!.height = startHeight + diffInPy;
-                self._outer.style.width = `${self._dashDoc!.width}`;
-                self._outer.style.height = `${self._dashDoc!.height}`;
-            });
-
-            const onpointerup = () => {
-                document.removeEventListener("pointermove", onpointermove);
-                document.removeEventListener("pointerup", onpointerup);
-                view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
-            };
-
-            document.addEventListener("pointermove", onpointermove);
-            document.addEventListener("pointerup", onpointerup);
-        };
-
         this._outer.appendChild(this._dashSpan);
-        this._outer.appendChild(this._handle);
         (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";
+    destroy() {
+        this._reactionDisposer && this._reactionDisposer();
     }
 }
 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index c030d4bce..966f183b0 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -262,20 +262,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let target = de.data.embeddableSourceDoc;
             // We're dealing with an internal document drop
             const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
-            let 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 {
-                let alias = Doc.MakeAlias(target);
-                alias.fitToBox = true;
-                node = schema.nodes.dashDoc.create({
-                    width: target[WidthSym](), height: target[HeightSym](),
-                    title: "dashDoc", docid: alias[Id],
-                    float: "none"
-                });
-            }
+            let alias = Doc.MakeAlias(target);
+            alias.fitToBox = true;
+            let node = schema.nodes.dashDoc.create({
+                width: target[WidthSym](), height: target[HeightSym](),
+                title: "dashDoc", docid: alias[Id],
+                float: "none"
+            });
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
             link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
             this.tryUpdateHeight();
-- 
cgit v1.2.3-70-g09d2


From 2485a9f0f32968040a79f757118dfb6fad8930fd Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 7 Oct 2019 17:13:48 -0400
Subject: fixes for toolbar and text footnote/comments.

---
 src/client/util/RichTextSchema.tsx                 |  2 +-
 src/client/util/TooltipTextMenu.tsx                |  8 +++++---
 src/client/views/DocumentDecorations.tsx           |  9 ++++++++-
 src/client/views/nodes/FormattedTextBox.tsx        |  9 +++++----
 src/client/views/nodes/FormattedTextBoxComment.tsx | 10 ++++++----
 5 files changed, 25 insertions(+), 13 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 432e0c2fb..06b97fa68 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -168,7 +168,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             width: { default: 200 },
             height: { default: 100 },
             title: { default: null },
-            float: { default: "left" },
+            float: { default: "right" },
             location: { default: "onRight" },
             docid: { default: "" }
         },
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 55c6e6609..2732b708d 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -19,6 +19,7 @@ import { schema } from "./RichTextSchema";
 import "./TooltipTextMenu.scss";
 import { Cast, NumCast } from '../../new_fields/Types';
 import { updateBullets } from './ProsemirrorExampleTransfer';
+import { DocumentDecorations } from '../views/DocumentDecorations';
 const { toggleMark, setBlockType } = require("prosemirror-commands");
 const { openPrompt, TextField } = require("./ProsemirrorCopy/prompt.js");
 
@@ -186,7 +187,7 @@ export class TooltipTextMenu {
 
         this.updateListItemDropdown(":", this.listTypeBtnDom);
 
-        this.updateInternal(view, undefined, undefined);
+        this.updateFromDash(view, undefined, undefined);
         TooltipTextMenu.Toolbar = this.wrapper;
     }
     public static Toolbar: HTMLDivElement | undefined;
@@ -849,11 +850,12 @@ export class TooltipTextMenu {
         }
     }
 
-    update(view: EditorView, lastState: EditorState | undefined) { this.updateInternal(view, lastState, this.editorProps) }
+    update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps) }
     //updates the tooltip menu when the selection changes
-    private updateInternal(view: EditorView, lastState: EditorState | undefined, props: any) {
+    public updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
         this.view = view;
         let state = view.state;
+        DocumentDecorations.Instance.setTextBar(DocumentDecorations.Instance.TextBar);
         props && (this.editorProps = props);
         // Don't do anything if the document/selection didn't change
         if (lastState && lastState.doc.eq(state.doc) &&
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index b7ec27957..4f9bdbe9c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -546,6 +546,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         return "-unset-";
     }
 
+    TextBar: HTMLDivElement | undefined;
+    setTextBar = (ele: HTMLDivElement) => {
+        if (ele) {
+            this.TextBar = ele;
+            TooltipTextMenu.Toolbar && Array.from(ele.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && ele.appendChild(TooltipTextMenu.Toolbar);
+        }
+    }
     render() {
         var bounds = this.Bounds;
         let seldoc = SelectionManager.SelectedDocuments().length ? SelectionManager.SelectedDocuments()[0] : undefined;
@@ -578,7 +585,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" ref={(r) => r && TooltipTextMenu.Toolbar && Array.from(r.childNodes).indexOf(TooltipTextMenu.Toolbar) === -1 && r.appendChild(TooltipTextMenu.Toolbar)} style={{
+            <div className="documentDecorations-container" ref={this.setTextBar} 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 966f183b0..20edbdb23 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -267,7 +267,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let node = schema.nodes.dashDoc.create({
                 width: target[WidthSym](), height: target[HeightSym](),
                 title: "dashDoc", docid: alias[Id],
-                float: "none"
+                float: "right"
             });
             let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
             link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
@@ -759,7 +759,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     onPointerUp = (e: React.PointerEvent): void => {
-        FormattedTextBoxComment.textBox = this;
+        if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; }
+        (e.nativeEvent as any).formattedHandled = true;
+
         if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
             e.stopPropagation();
         }
@@ -837,7 +839,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }
             }
         }
-        this._editorView!.focus();
     }
     onMouseUp = (e: React.MouseEvent): void => {
         e.stopPropagation();
@@ -914,7 +915,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             ? "none" : "all";
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         if (this.props.isSelected()) {
-            FormattedTextBox._toolTipTextMenu!.update(this._editorView!, undefined, this.props);
+            FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
         }
         return (
             <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 2d2fff06d..a75bfd373 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -7,6 +7,7 @@ import { schema } from "../../util/RichTextSchema";
 import { DocServer } from "../../DocServer";
 import { Utils } from "../../../Utils";
 import { StrCast } from "../../../new_fields/Types";
+import { FormattedTextBox } from "./FormattedTextBox";
 
 export let formattedTextBoxCommentPlugin = new Plugin({
     view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -49,7 +50,7 @@ export class FormattedTextBoxComment {
     static end: number;
     static mark: Mark;
     static opened: boolean;
-    static textBox: any;
+    static textBox: FormattedTextBox | undefined;
     constructor(view: any) {
         if (!FormattedTextBoxComment.tooltip) {
             const root = document.getElementById("root");
@@ -62,9 +63,9 @@ export class FormattedTextBoxComment {
             FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
             FormattedTextBoxComment.tooltip.appendChild(input);
             FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
-                let keep = e.target && (e.target as any).type === "checkbox";
+                let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
                 FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
-                FormattedTextBoxComment.textBox && FormattedTextBoxComment.textBox.setAnnotation(
+                FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation(
                     FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
                     FormattedTextBoxComment.opened, keep);
             };
@@ -92,8 +93,9 @@ export class FormattedTextBoxComment {
         if (lastState && lastState.doc.eq(state.doc) &&
             lastState.selection.eq(state.selection)) return;
 
+        if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props.isSelected()) return;
         let set = "none";
-        if (state.selection.$from) {
+        if (FormattedTextBoxComment.textBox && state.selection.$from) {
             let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
             let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
             const spos = state.selection.$from.pos - nbef;
-- 
cgit v1.2.3-70-g09d2


From ebc1e7bd1c34e948fd7288cde19f600a8e2985ee Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 7 Oct 2019 18:50:33 -0400
Subject: fixed-up captions to work with text again.

---
 src/client/util/TooltipTextMenu.tsx         | 2 +-
 src/client/views/nodes/FormattedTextBox.tsx | 8 ++++----
 2 files changed, 5 insertions(+), 5 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 55c6e6609..0b1613ed6 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -851,7 +851,7 @@ export class TooltipTextMenu {
 
     update(view: EditorView, lastState: EditorState | undefined) { this.updateInternal(view, lastState, this.editorProps) }
     //updates the tooltip menu when the selection changes
-    private updateInternal(view: EditorView, lastState: EditorState | undefined, props: any) {
+    public updateInternal(view: EditorView, lastState: EditorState | undefined, props: any) {
         this.view = view;
         let state = view.state;
         props && (this.editorProps = props);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 966f183b0..3082edbed 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -899,7 +899,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.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
+        if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).bottom !== "0px") {
             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));
@@ -913,8 +913,8 @@ 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);
+        if (this._editorView && this.props.isSelected()) {
+            FormattedTextBox._toolTipTextMenu!.updateInternal(this._editorView, undefined, this.props);
         }
         return (
             <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
@@ -922,7 +922,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
                     height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
                     background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
-                    opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
+                    opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                     color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
                     pointerEvents: interactive,
                     fontSize: this._fontSize,
-- 
cgit v1.2.3-70-g09d2


From 37091a45ce38c1fade316b50ec891c37ed25227e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 7 Oct 2019 20:01:28 -0400
Subject: switched recording over to a menu item.

---
 src/client/views/nodes/FormattedTextBox.tsx | 36 ++++++++++++++---------------
 1 file changed, 18 insertions(+), 18 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 3082edbed..cab408a31 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -41,6 +41,8 @@ import { FieldView, FieldViewProps } from "./FieldView";
 import "./FormattedTextBox.scss";
 import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
 import React = require("react");
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -296,14 +298,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    recordKeyHandler = (e: KeyboardEvent) => {
-        if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
-            if (e.key === "R" && e.altKey) {
-                e.stopPropagation();
-                e.preventDefault();
-                this.recordBullet();
-            }
-        }
+    specificContextMenu = (e: React.MouseEvent): void => {
+        let funcs: ContextMenuProps[] = [];
+        funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
+
+        ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
     }
 
     recordBullet = async () => {
@@ -336,12 +335,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     nextBullet = (pos: number) => {
         if (this._editorView) {
             let frag = Fragment.fromArray(this.newListItems(2));
-            let slice = new Slice(frag, 2, 2);
-            let state = this._editorView.state;
-            this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
-            pos += 4;
-            state = this._editorView.state;
-            this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+            if (this._editorView.state.doc.resolve(pos).depth >= 2) {
+                let slice = new Slice(frag, 2, 2);
+                let state = this._editorView.state;
+                this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+                pos += 4;
+                state = this._editorView.state;
+                this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+            }
         }
     }
 
@@ -769,8 +770,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     @action
     onFocused = (e: React.FocusEvent): void => {
         FormattedTextBox.InputBoxOverlay = this;
-        document.removeEventListener("keypress", this.recordKeyHandler);
-        document.addEventListener("keypress", this.recordKeyHandler);
         this.tryUpdateHeight();
     }
     onPointerWheel = (e: React.WheelEvent): void => {
@@ -873,7 +872,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         });
     }
     onBlur = (e: any) => {
-        document.removeEventListener("keypress", this.recordKeyHandler);
+        DictationManager.Controls.stop(false);
         if (this._undoTyping) {
             this._undoTyping.end();
             this._undoTyping = undefined;
@@ -899,7 +898,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.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).bottom !== "0px") {
+        if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") {
             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));
@@ -928,6 +927,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     fontSize: this._fontSize,
                     fontFamily: this._fontFamily,
                 }}
+                onContextMenu={this.specificContextMenu}
                 onKeyDown={this.onKeyPress}
                 onFocus={this.onFocused}
                 onClick={this.onClick}
-- 
cgit v1.2.3-70-g09d2


From d970ad82bc7b0b1c8dae27b5f55ad78bddb0f7cd Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 7 Oct 2019 21:48:01 -0400
Subject: added commeting with (( and ##

---
 src/client/documents/Documents.ts           |  1 +
 src/client/util/RichTextRules.ts            | 46 ++++++++++++++++++++++++++---
 src/client/util/RichTextSchema.tsx          |  3 +-
 src/client/views/nodes/FormattedTextBox.tsx |  2 +-
 4 files changed, 46 insertions(+), 6 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 98f56af01..34e146f0e 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -73,6 +73,7 @@ export interface DocumentOptions {
     dropAction?: dropActionType;
     backgroundLayout?: string;
     chromeStatus?: string;
+    fontSize?: number;
     curPage?: number;
     currentTimecode?: number;
     documentText?: string;
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 2d5ec1743..2ff758fbc 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -1,10 +1,12 @@
 import { textblockTypeInputRule, smartQuotes, emDash, ellipsis, InputRule } from "prosemirror-inputrules";
 import { schema } from "./RichTextSchema";
 import { wrappingInputRule } from "./prosemirrorPatches";
-import { NodeSelection } from "prosemirror-state";
+import { NodeSelection, TextSelection } from "prosemirror-state";
 import { NumCast, Cast } from "../../new_fields/Types";
 import { Doc } from "../../new_fields/Doc";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { Docs } from "../documents/Documents";
+import { Id } from "../../new_fields/FieldSymbols";
 
 export const inpRules = {
     rules: [
@@ -80,8 +82,9 @@ export const inpRules = {
                     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 : [])]) :
+                let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
         new InputRule(
             new RegExp(/^\[\[\s$/),
@@ -94,8 +97,9 @@ export const inpRules = {
                     ruleProvider["ruleAlign_" + heading] = "left";
                     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 : [])]) :
+                let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
             }),
         new InputRule(
             new RegExp(/^\]\]\s$/),
@@ -108,8 +112,42 @@ export const inpRules = {
                     ruleProvider["ruleAlign_" + heading] = "right";
                     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 : [])]) :
+                let replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
                     state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+            }),
+        new InputRule(
+            new RegExp(/##\s$/),
+            (state, match, start, end) => {
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                let sm = state.storedMarks || undefined;
+                let target = Docs.Create.TextDocument({ width: 75, height: 35, autoHeight: true, fontSize: 9, title: "inline comment" });
+                let replaced = node ? state.tr.insertText("←", start).replaceRangeWith(start + 1, end + 1, schema.nodes.dashDoc.create({
+                    width: 75, height: 35,
+                    title: "dashDoc", docid: target[Id],
+                    float: "right"
+                })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                    state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 1)));
+            }),
+        new InputRule(
+            new RegExp(/\(\(/),
+            (state, match, start, end) => {
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                let sm = state.storedMarks || undefined;
+                let mark = state.schema.marks.highlight.create();
+                let selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+                let content = selected.selection.content();
+                let replaced = node ? selected.replaceRangeWith(start, start,
+                    schema.nodes.star.create({ visibility: true, text: content, textslice: content.toJSON() })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+                    state.tr;
+                return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1)));
+            }),
+        new InputRule(
+            new RegExp(/\)\)/),
+            (state, match, start, end) => {
+                let mark = state.schema.marks.highlight.create();
+                return state.tr.removeStoredMark(mark);
             }),
         new InputRule(
             new RegExp(/\^f\s$/),
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 8d0f59ddc..047706368 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -739,7 +739,8 @@ export class DashDocView {
         this._dashSpan.style.position = "absolute";
         this._dashSpan.style.display = "inline-block";
         let removeDoc = () => {
-            let ns = new NodeSelection(view.state.doc.resolve(getPos()));
+            let pos = getPos();
+            let ns = new NodeSelection(view.state.doc.resolve(pos));
             view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
             return true;
         };
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index cab408a31..cacef1ac3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -457,7 +457,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         },
             action((rules: any) => {
                 this._fontFamily = rules ? rules.font : "Arial";
-                this._fontSize = rules ? rules.size : 13;
+                this._fontSize = rules ? rules.size : NumCast(this.props.Document.fontSize, 13);
                 rules && setTimeout(() => {
                     const view = this._editorView!;
                     if (this._proseRef) {
-- 
cgit v1.2.3-70-g09d2


From 00ada8bd463f15f69ae604b79445c69167f8425a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 8 Oct 2019 01:11:18 -0400
Subject: a bunch of fixes to text outline mode.

---
 src/client/util/ProsemirrorExampleTransfer.ts |  4 ++--
 src/client/util/RichTextSchema.tsx            |  6 ++++--
 src/client/util/TooltipTextMenu.tsx           | 29 +++++++++------------------
 src/client/views/nodes/FormattedTextBox.scss  | 22 +++++++++++---------
 src/client/views/nodes/FormattedTextBox.tsx   | 25 +++++++++++++++++++----
 5 files changed, 49 insertions(+), 37 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index dd0f72af0..003ff6272 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -11,7 +11,7 @@ const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) :
 
 export type KeyMap = { [key: string]: any };
 
-export let updateBullets = (tx2: Transaction, schema: Schema) => {
+export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => {
     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) {
@@ -20,7 +20,7 @@ export let updateBullets = (tx2: Transaction, schema: Schema) => {
             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);
+            tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
         }
     });
     return tx2;
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 047706368..46c2740f4 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -234,6 +234,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             bulletStyle: { default: 0 },
             mapStyle: { default: "decimal" },
             setFontSize: { default: undefined },
+            setFontFamily: { default: undefined },
             inheritedFontSize: { default: undefined },
             visibility: { default: true }
         },
@@ -243,8 +244,9 @@ 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;
             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}` }];
+            let ffam = node.attrs.setFontFamily;
+            return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }, 0] :
+                ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}` }];
         }
     },
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 765c2fe92..75943e537 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -521,23 +521,14 @@ export class TooltipTextMenu {
             }
             //actually apply font
             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));
+                let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+                    { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: markType.name, setFontSize: Number(markType.name.replace(/p/, "")) }), view.state.schema);
+                view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
             }
             else toggleMark(markType)(view.state, view.dispatch, view);
         }
     }
 
-    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.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
@@ -546,18 +537,18 @@ export class TooltipTextMenu {
         } else {
             var marks = view.state.storedMarks || (view.state.selection.$to.parentOffset && view.state.selection.$from.marks());
             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]);
+                let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle);
+                marks && tx3.ensureMarks([...marks]);
+                marks && tx3.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]);
+                let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle);
+                marks && tx3.ensureMarks([...marks]);
+                marks && tx3.setStoredMarks([...marks]);
 
-                view.dispatch(tx2);
+                view.dispatch(tx3);
             }
         }
     }
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 541c29faa..29e8b14a8 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -154,17 +154,18 @@ footnote::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 }
-.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 }
+.ProseMirror {
+ol { counter-reset: deci1 0; padding-left: 0px; }
+.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 ; ul, ol {  padding-left:30px; } }
+.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 ; ul, ol {  padding-left:30px; } }
+.decimal3-ol {counter-reset: deci3; p { display: inline }; font-size: 14 ; ul, ol {  padding-left:30px; } }
+.decimal4-ol {counter-reset: deci4; p { display: inline }; font-size: 10 ; ul, ol {  padding-left:30px; } }
+.decimal5-ol {counter-reset: deci5; p { display: inline }; font-size: 10 ; ul, ol {  padding-left:30px; } }
+.decimal6-ol {counter-reset: deci6; p { display: inline }; font-size: 10 ; ul, ol {  padding-left:30px; } }
+.decimal7-ol {counter-reset: deci7; p { display: inline }; font-size: 10 ; ul, ol {  padding-left:30px; } } 
+.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;}
+.lower-alpha-ol {counter-reset: lalpha;  p { display: inline }; font-size: 10; }
 .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}
@@ -175,3 +176,4 @@ ol { counter-reset: deci1 0;}
 .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 cacef1ac3..041675bef 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -779,6 +779,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    static _sheet: any = undefined;
+    static addRule = ((style) => {
+        style.type = "text/css"
+        var sheets = document.head.appendChild(style);
+        FormattedTextBox._sheet = (sheets as any).sheet;
+        return function (selector: any, css: any) {
+            var propText = typeof css === "string" ? css : Object.keys(css).map(function (p) {
+                return p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p]);
+            }).join(";");
+            return FormattedTextBox._sheet.insertRule("." + selector + "{" + propText + "}", FormattedTextBox._sheet.cssRules.length);
+        };
+    })(document.createElement("style"));
+
     onClick = (e: React.MouseEvent): void => {
         if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
         (e.nativeEvent as any).formattedHandled = true;
@@ -814,6 +827,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 e.preventDefault();
             }
         }
+
+        if (FormattedTextBox._sheet.rules.length) {
+            FormattedTextBox._sheet.removeRule(0);
+        }
         // 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 });
@@ -821,14 +838,14 @@ 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)) {
-                    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 hit = this._editorView!.domAtPos(pos.pos).node as any;   // let beforeEle = document.querySelector("." + hit.className) as Element;
+                    let before = hit ? window.getComputedStyle(hit, ':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) {
+                        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))));
+                            FormattedTextBox.addRule(hit.className + ":before", { background: "gray" });
                         } else {
                             this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
                         }
-- 
cgit v1.2.3-70-g09d2


From de0b723551eb033f7ad1999dd23309b4f306f344 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 8 Oct 2019 10:08:36 -0400
Subject: moved style sheet stuff into Utils.  fixed menu/footnote interaction

---
 src/Utils.ts                                | 13 ++++++++++
 src/client/util/RichTextSchema.tsx          | 11 +++++++--
 src/client/views/nodes/FormattedTextBox.tsx | 37 ++++++++++++-----------------
 3 files changed, 37 insertions(+), 24 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 4b892aa70..f6ab434a8 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -337,4 +337,17 @@ export default function smoothScroll(duration: number, element: HTMLElement, to:
         }
     };
     animateScroll();
+}
+export function addStyleSheet(styleType: string = "text/css") {
+    let style = document.createElement("style");
+    style.type = styleType;
+    var sheets = document.head.appendChild(style);
+    return (sheets as any).sheet;
+}
+export function addStyleSheetRule(sheet: any, selector: any, css: any) {
+    var propText = typeof css === "string" ? css : Object.keys(css).map(p => p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p])).join(";");
+    return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length);
+}
+export function removeStyleSheetRule(sheet: any, rule: number) {
+    sheet.removeRule(rule);
 }
\ No newline at end of file
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 836713c01..bc1ad5070 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -4,7 +4,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, NodeSelection, TextSelection } from "prosemirror-state";
+import { EditorState, NodeSelection, TextSelection, Plugin } from "prosemirror-state";
 import { StepMap } from "prosemirror-transform";
 import { EditorView } from "prosemirror-view";
 import * as ReactDOM from 'react-dom';
@@ -847,7 +847,14 @@ export class FootnoteView {
                     "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
                     "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
                     "Mod-b": toggleMark(schema.marks.strong)
-                })]
+                }),
+                new Plugin({
+                    view(newView) {
+                        return FormattedTextBox.getToolTip(newView);
+                    }
+                })
+                ],
+
             }),
             // This is the magic part
             dispatchTransaction: this.dispatchInner.bind(this),
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 41bd29422..96b434708 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -18,7 +18,7 @@ import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
 import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { numberRange, timenow, Utils, emptyFunction } from '../../../Utils';
+import { numberRange, timenow, Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from '../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -781,18 +781,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    static _sheet: any = undefined;
-    static addRule = ((style) => {
-        style.type = "text/css"
-        var sheets = document.head.appendChild(style);
-        FormattedTextBox._sheet = (sheets as any).sheet;
-        return function (selector: any, css: any) {
-            var propText = typeof css === "string" ? css : Object.keys(css).map(function (p) {
-                return p + ":" + (p === "content" ? "'" + css[p] + "'" : css[p]);
-            }).join(";");
-            return FormattedTextBox._sheet.insertRule("." + selector + "{" + propText + "}", FormattedTextBox._sheet.cssRules.length);
-        };
-    })(document.createElement("style"));
+    static _sheet: any = addStyleSheet();
 
     onClick = (e: React.MouseEvent): void => {
         if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
@@ -830,12 +819,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
         }
 
+        this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
+    }
+
+    // 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.
+    hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) {
         if (FormattedTextBox._sheet.rules.length) {
-            FormattedTextBox._sheet.removeRule(0);
+            removeStyleSheetRule(FormattedTextBox._sheet, 0);
         }
-        // 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 (this.props.isSelected() && offsetX < 40) {
+            let pos = this._editorView!.posAtCoords({ left: x, top: y });
             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;
@@ -843,11 +836,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     let hit = this._editorView!.domAtPos(pos.pos).node as any;   // let beforeEle = document.querySelector("." + hit.className) as Element;
                     let before = hit ? window.getComputedStyle(hit, ':before') : undefined;
                     let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined;
-                    if (beforeWidth && e.nativeEvent.offsetX < beforeWidth) {
+                    if (beforeWidth && 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) {
+                        if (ol && ol.type === schema.nodes.ordered_list && select) {
                             this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
-                            FormattedTextBox.addRule(hit.className + ":before", { background: "gray" });
+                            addStyleSheetRule(FormattedTextBox._sheet, hit.className + ":before", { background: "gray" });
                         } else {
                             this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
                         }
@@ -875,8 +868,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     tooltipTextMenuPlugin() {
         let self = FormattedTextBox;
         return new Plugin({
-            view(_editorView) {
-                return self._toolTipTextMenu = FormattedTextBox.getToolTip(_editorView);
+            view(newView) {
+                return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView);
             }
         });
     }
-- 
cgit v1.2.3-70-g09d2


From 84211163c9f085a521b3ca3af0dc9103640e3622 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 8 Oct 2019 14:07:32 -0400
Subject: cleaned up tagging of text spans with data and timestamps

---
 src/Utils.ts                                | 13 ++++++-
 src/client/util/DocumentManager.ts          |  2 +-
 src/client/util/RichTextRules.ts            | 21 ++++++++++
 src/client/util/RichTextSchema.tsx          | 34 ++++++++++++-----
 src/client/views/nodes/FormattedTextBox.tsx | 59 +++++++++++++++++++++++++----
 5 files changed, 109 insertions(+), 20 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index f6ab434a8..489de3b50 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -349,5 +349,16 @@ export function addStyleSheetRule(sheet: any, selector: any, css: any) {
     return sheet.insertRule("." + selector + "{" + propText + "}", sheet.cssRules.length);
 }
 export function removeStyleSheetRule(sheet: any, rule: number) {
-    sheet.removeRule(rule);
+    if (sheet.rules.length) {
+        sheet.removeRule(rule);
+        return true;
+    }
+    return false;
+}
+export function clearStyleSheetRules(sheet: any) {
+    if (sheet.rules.length) {
+        numberRange(sheet.rules.length).map(n => sheet.removeRule(0));
+        return true;
+    }
+    return false;
 }
\ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 83a69465f..c45d3a75f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -168,7 +168,7 @@ export class DocumentManager {
                             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
+                            targetDoc.layout && (dockFunc || CollectionDockingView.AddRightSplit)(Doc.BrushDoc(Doc.MakeAlias(targetDoc)), undefined); // otherwise create a new view of the target
                         }
                         highlight();
                     }, 0);
diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 2ff758fbc..346d5ab99 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -71,6 +71,27 @@ export const inpRules = {
                 }
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
             }),
+        new InputRule(
+            new RegExp(/\t/),
+            (state, match, start, end) => {
+                if (state.selection.to === state.selection.from) return null;
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+            }),
+        new InputRule(
+            new RegExp(/\!/),
+            (state, match, start, end) => {
+                if (state.selection.to === state.selection.from) return null;
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "important", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+            }),
+        new InputRule(
+            new RegExp(/\x/),
+            (state, match, start, end) => {
+                if (state.selection.to === state.selection.from) return null;
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "disagree", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+            }),
         new InputRule(
             new RegExp(/^\^\^\s$/),
             (state, match, start, end) => {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index bc1ad5070..80bcf25ad 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -439,20 +439,34 @@ export const marks: { [index: string]: MarkSpec } = {
     user_mark: {
         attrs: {
             userid: { default: "" },
-            hide_users: { default: [] },
             opened: { default: true },
-            modified: { default: "when?" }
+            modified: { default: "when?" }, // 5 second intervals since 1970
         },
         group: "inline",
         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);
-            return hidden ?
-                (node.attrs.opened ?
-                    ['span', { class: "userMarkOpen" }, 0] :
-                    ['span', { class: "userMark" }, ['span', 0]]
-                ) :
-                ['span', 0];
+            let uid = node.attrs.userid.replace(".", "").replace("@", "");
+            let min = Math.round(node.attrs.modified / 12);
+            let hr = Math.round(min / 60);
+            let day = Math.round(hr / 60 / 24);
+            return node.attrs.opened ?
+                ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
+                ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
+        }
+    },
+    // the id of the user who entered the text
+    user_tag: {
+        attrs: {
+            userid: { default: "" },
+            opened: { default: true },
+            modified: { default: "when?" }, // 5 second intervals since 1970
+            tag: { default: "" }
+        },
+        group: "inline",
+        toDOM(node: any) {
+            let uid = node.attrs.userid.replace(".", "").replace("@", "");
+            return node.attrs.opened ?
+                ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0] :
+                ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, ['span', 0]];
         }
     },
 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 96b434708..d878f2b74 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -18,7 +18,7 @@ import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from '../../../new_fields/RichTextUtils';
 import { createSchema, makeInterface } from "../../../new_fields/Schema";
 import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { numberRange, timenow, Utils, addStyleSheet, addStyleSheetRule, removeStyleSheetRule } from '../../../Utils';
+import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -43,6 +43,7 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format
 import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -298,9 +299,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+
+    static _highlights: string[] = [];
+
+    updateHighlights = () => {
+        clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+        if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "yellow" });
+        }
+        if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+        }
+        if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+        }
+        if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+        }
+        if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+            let min = Math.round(Date.now() / 1000 / 60);
+            numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
+            setTimeout(() => this.updateHighlights());
+        }
+        if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+            let hr = Math.round(Date.now() / 1000 / 60 / 60);
+            numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
+        }
+    }
+
     specificContextMenu = (e: React.MouseEvent): void => {
         let funcs: ContextMenuProps[] = [];
         funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
+        ["My Text", "Todo Items", "Important Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
+            funcs.push({
+                description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
+                    e.stopPropagation();
+                    if (FormattedTextBox._highlights.indexOf(option) === -1) {
+                        FormattedTextBox._highlights.push(option);
+                    } else {
+                        FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+                    }
+                    this.updateHighlights();
+                }, icon: "expand-arrows-alt"
+            }));
 
         ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
     }
@@ -718,7 +761,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
         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() })];
+        this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })];
     }
     getFont(font: string) {
         switch (font) {
@@ -781,7 +824,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    static _sheet: any = addStyleSheet();
+    static _bulletStyleSheet: any = addStyleSheet();
+    static _userStyleSheet: any = addStyleSheet();
 
     onClick = (e: React.MouseEvent): void => {
         if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
@@ -824,9 +868,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     // 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.
     hitBulletTargets(x: number, y: number, offsetX: number, select: boolean = false) {
-        if (FormattedTextBox._sheet.rules.length) {
-            removeStyleSheetRule(FormattedTextBox._sheet, 0);
-        }
+        clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
         if (this.props.isSelected() && offsetX < 40) {
             let pos = this._editorView!.posAtCoords({ left: x, top: y });
             if (pos && pos.pos > 0) {
@@ -840,7 +882,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         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 && select) {
                             this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2))));
-                            addStyleSheetRule(FormattedTextBox._sheet, hit.className + ":before", { background: "gray" });
+                            addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" });
                         } else {
                             this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility }));
                         }
@@ -898,7 +940,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (e.key === "Tab" || e.key === "Enter") {
             e.preventDefault();
         }
-        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
+        let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
 
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
-- 
cgit v1.2.3-70-g09d2


From 121cbc76bf971e688398533b303e32d637265364 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 8 Oct 2019 14:26:38 -0400
Subject: added ignore items markup

---
 src/client/util/RichTextRules.ts            | 9 ++++++++-
 src/client/views/nodes/FormattedTextBox.tsx | 5 ++++-
 2 files changed, 12 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index 346d5ab99..ebb9bda8a 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -72,12 +72,19 @@ export const inpRules = {
                 return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: Number(match[1]) }));
             }),
         new InputRule(
-            new RegExp(/\t/),
+            new RegExp(/t/),
             (state, match, start, end) => {
                 if (state.selection.to === state.selection.from) return null;
                 let node = (state.doc.resolve(start) as any).nodeAfter;
                 return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "todo", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
             }),
+        new InputRule(
+            new RegExp(/i/),
+            (state, match, start, end) => {
+                if (state.selection.to === state.selection.from) return null;
+                let node = (state.doc.resolve(start) as any).nodeAfter;
+                return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: "ignore", modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+            }),
         new InputRule(
             new RegExp(/\!/),
             (state, match, start, end) => {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d878f2b74..86166b0b3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -316,6 +316,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
         }
+        if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" });
+        }
         if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
             let min = Math.round(Date.now() / 1000 / 60);
@@ -332,7 +335,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     specificContextMenu = (e: React.MouseEvent): void => {
         let funcs: ContextMenuProps[] = [];
         funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
-        ["My Text", "Todo Items", "Important Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
+        ["My Text", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
                 description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
                     e.stopPropagation();
-- 
cgit v1.2.3-70-g09d2


From 29cdf0d66df3e38408f69ac8225b4d59397ee2e6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 8 Oct 2019 19:02:35 -0400
Subject: fixes for text selection in pdfs to be less jumpy and to work through
 other annotations.

---
 src/Utils.ts                                |  2 +-
 src/client/util/TooltipTextMenu.tsx         |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx |  2 +-
 src/client/views/pdf/PDFMenu.tsx            |  2 +-
 src/client/views/pdf/PDFViewer.scss         |  4 ++++
 src/client/views/pdf/PDFViewer.tsx          | 15 ++++++++++-----
 6 files changed, 18 insertions(+), 9 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/Utils.ts b/src/Utils.ts
index 489de3b50..9a2f01f80 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -320,7 +320,7 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat
     return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
 };
 
-export default function smoothScroll(duration: number, element: HTMLElement, to: number) {
+export function smoothScroll(duration: number, element: HTMLElement, to: number) {
     const start = element.scrollTop;
     const change = to - start;
     const startDate = new Date().getTime();
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 5f2bc18b5..fe4c5ac9f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -545,7 +545,7 @@ export class TooltipTextMenu {
                 view.dispatch(tx2);
             })) {
                 let tx2 = view.state.tr;
-                let tx3 = updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle);
+                let tx3 = nodeType ? updateBullets(tx2, schema, (nodeType as any).attrs.mapStyle) : tx2;
                 marks && tx3.ensureMarks([...marks]);
                 marks && tx3.setStoredMarks([...marks]);
 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 86166b0b3..67d1bc42c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -548,7 +548,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) {
+                    if (ret.frag.size > 2 && ret.start >= 0) {
                         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
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index 1e3320069..517a99a68 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -155,7 +155,7 @@ export default class PDFMenu extends React.Component {
 
     @action
     highlightClicked = (e: React.MouseEvent) => {
-        if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) {
+        if (!this.Highlight("rgba(245, 230, 95, 0.616)") && this.Pinned) { // yellowish highlight color for a marker type highlight
             this.Highlighting = !this.Highlighting;
         }
     }
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index c77cee792..f6fedf3da 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -15,6 +15,10 @@
         
         mix-blend-mode: multiply;
         opacity: 0.9;
+        span {
+            padding-right: 5px;
+            padding-bottom: 4px;
+        }
     }
     .textLayer ::selection { background: yellow; } // should match the backgroundColor in createAnnotation()
     .textLayer .highlight {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 4516e9904..b010d16c8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -9,7 +9,7 @@ import { List } from "../../../new_fields/List";
 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, intersectRect } from "../../../Utils";
+import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { DragManager } from "../../util/DragManager";
 import { CompiledScript, CompileScript } from "../../util/Scripting";
@@ -24,6 +24,7 @@ import { CollectionView } from "../collections/CollectionView";
 import Annotation from "./Annotation";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { SelectionManager } from "../../util/SelectionManager";
+import { undoBatch } from "../../util/UndoManager";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -62,6 +63,7 @@ interface IViewerProps {
  */
 @observer
 export class PDFViewer extends React.Component<IViewerProps> {
+    static _annotationStyle: any = addStyleSheet();
     @observable private _pageSizes: { width: number, height: number }[] = [];
     @observable private _annotations: Doc[] = [];
     @observable private _savedAnnotations: Dictionary<number, HTMLDivElement[]> = new Dictionary<number, HTMLDivElement[]>();
@@ -150,7 +152,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     copy = (e: ClipboardEvent) => {
         if (this.props.active() && e.clipboardData) {
-            let annoDoc = this.makeAnnotationDocument("#0390fc");
+            let annoDoc = this.makeAnnotationDocument("rgba(3,144,152,0.3)");  // copied text markup color (blueish)
             if (annoDoc) {
                 e.clipboardData.setData("text/plain", this._selectionText);
                 e.clipboardData.setData("dash/pdfOrigin", this.props.Document[Id]);
@@ -237,6 +239,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         this._pdfViewer.setDocument(this.props.pdf);
     }
 
+    @undoBatch
     @action
     makeAnnotationDocument = (color: string): Opt<Doc> => {
         if (this._savedAnnotations.size() === 0) return undefined;
@@ -388,6 +391,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         // if alt+left click, drag and annotate
         this._downX = e.clientX;
         this._downY = e.clientY;
+        addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
         if (NumCast(this.props.Document.scale, 1) !== 1) return;
         if ((e.button !== 0 || e.altKey) && this.active()) {
             this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
@@ -475,6 +479,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     onSelectEnd = (e: PointerEvent): void => {
+        clearStyleSheetRules(PDFViewer._annotationStyle);
         this._savedAnnotations.clear();
         if (this._marqueeing) {
             if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
@@ -510,8 +515,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
             }
         }
 
-        if (PDFMenu.Instance.Highlighting) {
-            this.highlight("rgba(245, 230, 95, 0.616)");  // when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
+        if (PDFMenu.Instance.Highlighting) {// when highlighter has been toggled when menu is pinned, we auto-highlight immediately on mouse up
+            this.highlight("rgba(245, 230, 95, 0.616)");  // yellowish highlight color for highlighted text (should match PDFMenu's highlight color)
         }
         else {
             PDFMenu.Instance.StartDrag = this.startDrag;
@@ -538,7 +543,7 @@ 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 });
-        const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)");
+        const annotationDoc = this.highlight("rgba(146, 245, 95, 0.467)"); // yellowish highlight color when dragging out a text selection
         if (annotationDoc) {
             let dragData = new DragManager.AnnotationDragData(this.props.Document, annotationDoc, targetDoc);
             DragManager.StartAnnotationDrag([ele], dragData, e.pageX, e.pageY, {
-- 
cgit v1.2.3-70-g09d2


From bfdbc495c0327b87ee0f896778968e65e2f18af6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 9 Oct 2019 20:39:40 -0400
Subject: fixed rich text search highlights

---
 src/client/util/RichTextSchema.tsx          |  7 ++-
 src/client/views/nodes/FormattedTextBox.tsx | 79 ++++++++++++++++++++---------
 2 files changed, 59 insertions(+), 27 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 80bcf25ad..a5502577b 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -427,10 +427,13 @@ export const marks: { [index: string]: MarkSpec } = {
     },
 
     search_highlight: {
+        attrs: {
+            selected: { default: false }
+        },
         parseDOM: [{ style: 'background: yellow' }],
-        toDOM() {
+        toDOM(node: any) {
             return ['span', {
-                style: 'background: yellow'
+                style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
             }];
         }
     },
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 67d1bc42c..77543d2e8 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -7,7 +7,7 @@ import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
 import { inputRules } from 'prosemirror-inputrules';
 import { keymap } from "prosemirror-keymap";
-import { Fragment, Mark, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model";
+import { Fragment, Mark, Node, Node as ProsNode, Slice } from "prosemirror-model";
 import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
 import { ReplaceStep } from 'prosemirror-transform';
 import { EditorView } from "prosemirror-view";
@@ -43,7 +43,7 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format
 import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { TextShadowProperty } from 'csstype';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -80,6 +80,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     private _editorView: Opt<EditorView>;
     private _applyingChange: boolean = false;
     private _nodeClicked: any;
+    private _searchIndex = 0;
     private _undoTyping?: UndoManager.Batch;
     private _searchReactionDisposer?: Lambda;
     private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
@@ -212,37 +213,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    public highlightSearchTerms = (terms: String[]) => {
+    public highlightSearchTerms = (terms: string[]) => {
         if (this._editorView && (this._editorView as any).docView) {
-            const doc = this._editorView.state.doc;
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
-            doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
-                if (node.isLeaf && node.isText && node.text) {
-                    let nodeText: String = node.text;
-                    let tokens = nodeText.split(" ");
-                    let start = pos;
-                    tokens.forEach((word) => {
-                        if (terms.includes(word) && this._editorView) {
-                            this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark));
-                        }
-                        start += word.length + 1;
-                    });
-                }
-            });
+            const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+            let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
+            let tr = this._editorView!.state.tr;
+            let flattened: TextSelection[] = [];
+            res.map(r => r.map(h => flattened.push(h)));
+            let lastSel = Math.min(flattened.length - 1, this._searchIndex);
+            flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
+            this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+            this._editorView!.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
         }
     }
 
     public unhighlightSearchTerms = () => {
         if (this._editorView && (this._editorView as any).docView) {
-            const doc = this._editorView.state.doc;
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
-            doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
-                if (node.isLeaf && node.isText && node.text) {
-                    if (node.marks.includes(mark) && this._editorView) {
-                        this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark));
-                    }
-                }
-            });
+            const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+            let end = this._editorView!.state.doc.textContent.length - 1;
+            this._editorView!.dispatch(this._editorView!.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
         }
     }
     setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
@@ -299,7 +290,45 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
+        let offset = 0
+
+        if (context === node) return { from: offset, to: offset + node.nodeSize }
 
+        if (node.isBlock) {
+            for (let i = 0; i < (context.content as any).content.length; i++) {
+                let result = this.getNodeEndpoints((context.content as any).content[i], node)
+                if (result) {
+                    return {
+                        from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
+                        to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
+                    }
+                }
+                offset += (context.content as any).content[i].nodeSize
+            }
+            return null;
+        } else {
+            return null;
+        }
+    }
+
+
+    //Recursively finds matches within a given node
+    findInNode(pm: EditorView, node: Node, find: string) {
+        let ret: TextSelection[] = [];
+
+        if (node.isTextblock) {
+            let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node);
+            while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
+                let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1))
+                ret.push(sel)
+                index = index + foundAt + find.length;
+            }
+        } else {
+            node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)))
+        }
+        return ret
+    }
     static _highlights: string[] = [];
 
     updateHighlights = () => {
-- 
cgit v1.2.3-70-g09d2


From 3b1345616bf2f7101a21c182e0c9719b1e31f899 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 9 Oct 2019 22:35:47 -0400
Subject: semicolons and fix to unhighlighting searched text in text boxes.

---
 src/client/views/nodes/FormattedTextBox.tsx | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 77543d2e8..9ca77cc8b 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -232,7 +232,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (this._editorView && (this._editorView as any).docView) {
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
             const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
-            let end = this._editorView!.state.doc.textContent.length - 1;
+            let end = this._editorView!.state.doc.nodeSize - 2;
             this._editorView!.dispatch(this._editorView!.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
         }
     }
@@ -291,20 +291,20 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
-        let offset = 0
+        let offset = 0;
 
-        if (context === node) return { from: offset, to: offset + node.nodeSize }
+        if (context === node) return { from: offset, to: offset + node.nodeSize };
 
         if (node.isBlock) {
             for (let i = 0; i < (context.content as any).content.length; i++) {
-                let result = this.getNodeEndpoints((context.content as any).content[i], node)
+                let result = this.getNodeEndpoints((context.content as any).content[i], node);
                 if (result) {
                     return {
                         from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
                         to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
-                    }
+                    };
                 }
-                offset += (context.content as any).content[i].nodeSize
+                offset += (context.content as any).content[i].nodeSize;
             }
             return null;
         } else {
@@ -320,14 +320,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (node.isTextblock) {
             let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node);
             while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
-                let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1))
-                ret.push(sel)
+                let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
+                ret.push(sel);
                 index = index + foundAt + find.length;
             }
         } else {
-            node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)))
+            node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
         }
-        return ret
+        return ret;
     }
     static _highlights: string[] = [];
 
-- 
cgit v1.2.3-70-g09d2


From 21aab2fbf28e9cf6d08366b57d368d120d6813bb Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 10 Oct 2019 16:42:12 -0400
Subject: got rid of all CollectionView wrappers around XxxBox's  added
 DocAnnotatableComponent

---
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/RichTextSchema.tsx                 |   4 +-
 src/client/views/DocComponent.tsx                  |  50 ++++++-
 .../collections/CollectionMasonryViewFieldRow.tsx  |   2 +-
 .../CollectionStackingViewFieldColumn.tsx          |   2 +-
 src/client/views/collections/CollectionSubView.tsx |   1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  20 +--
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |   8 +-
 src/client/views/nodes/ImageBox.scss               |   8 +-
 src/client/views/nodes/ImageBox.tsx                |  44 +++++--
 src/client/views/nodes/PDFBox.tsx                  |  24 ++--
 src/client/views/nodes/VideoBox.scss               |   8 +-
 src/client/views/nodes/VideoBox.tsx                | 143 ++++++---------------
 src/client/views/nodes/WebBox.tsx                  |  69 +++++++---
 src/client/views/pdf/Annotation.tsx                |   8 +-
 src/client/views/pdf/PDFViewer.tsx                 |  65 +++-------
 src/new_fields/Doc.ts                              |   6 +-
 18 files changed, 234 insertions(+), 234 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 22aa74634..0114f82d8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -120,11 +120,11 @@ export namespace Docs {
                 options: { height: 300, backgroundColor: "black" }
             }],
             [DocumentType.IMG, {
-                layout: { view: ImageBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+                layout: { view: ImageBox, ext: anno },
                 options: {}
             }],
             [DocumentType.WEB, {
-                layout: { view: WebBox, collectionView: [CollectionView, data, anno] as CollectionViewType },
+                layout: { view: WebBox, ext: anno },
                 options: { height: 300 }
             }],
             [DocumentType.COL, {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a5502577b..063686d58 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -784,8 +784,8 @@ export class DashDocView {
                     addDocTab={self._textBox.props.addDocTab}
                     pinToPres={returnFalse}
                     renderDepth={1}
-                    PanelWidth={self._dashDoc![WidthSym]}
-                    PanelHeight={self._dashDoc![HeightSym]}
+                    PanelWidth={self._dashDoc[WidthSym]}
+                    PanelHeight={self._dashDoc[HeightSym]}
                     focus={emptyFunction}
                     backgroundColor={returnEmptyString}
                     parentActive={returnFalse}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index d6562492f..93e852bef 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,6 +1,8 @@
 import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
-import { computed } from 'mobx';
+import { computed, action } from 'mobx';
+import { Cast } from '../../new_fields/Types';
+import { listSpec } from '../../new_fields/Schema';
 
 export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
@@ -11,4 +13,50 @@ export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: D
         }
     }
     return Component;
+}
+
+interface DocAnnotatableProps {
+    Document: Doc;
+    DataDoc?: Doc;
+    fieldKey: string;
+    fieldExt: string;
+    whenActiveChanged: (isActive: boolean) => void;
+    isSelected: () => boolean, renderDepth: number
+}
+export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
+    class Component extends React.Component<P> {
+        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+        @computed
+        get Document(): T {
+            return schemaCtor(this.props.Document);
+        }
+        _isChildActive = false;
+        @action.bound
+        removeDocument(doc: Doc): boolean {
+            Doc.GetProto(doc).annotationOn = undefined;
+            let value = Cast(this.extensionDoc[this.props.fieldExt], listSpec(Doc), []);
+            let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
+            return index !== -1 && value.splice(index, 1) ? true : false;
+        }
+
+        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
+        // if the moved document is already in this overlay collection nothing needs to be done.
+        // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. 
+        @action.bound
+        moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
+            return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false;
+        }
+
+        @action.bound
+        addDocument(doc: Doc): boolean {
+            Doc.GetProto(doc).annotationOn = this.props.Document;
+            return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc);
+        }
+        whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
+        active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
+    }
+    return Component;
 }
\ No newline at end of file
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 5c1960d53..6251d7114 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -86,7 +86,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
             this.props.parent.drop(e, de);
             e.stopPropagation();
         }
-    })
+    });
 
     getValue = (value: string): any => {
         let parsed = parseInt(value);
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index e8627780d..815b28586 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -72,7 +72,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             this.props.parent.drop(e, de);
             e.stopPropagation();
         }
-    })
+    });
     getValue = (value: string): any => {
         let parsed = parseInt(value);
         if (!isNaN(parsed)) {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 689adc375..06d048383 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -38,6 +38,7 @@ export interface CollectionViewProps extends FieldViewProps {
 export interface SubCollectionViewProps extends CollectionViewProps {
     CollectionView: Opt<CollectionView>;
     ruleProvider: Doc | undefined;
+    children?: never | (() => JSX.Element[]) | React.ReactNode;
 }
 
 export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d2644480c..adbad5da5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
 import { library } from "@fortawesome/fontawesome-svg-core";
 import { faEye } from "@fortawesome/free-regular-svg-icons";
-import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faPaintBrush, faTable, faUpload, faFileUpload } from "@fortawesome/free-solid-svg-icons";
+import { faBraille, faChalkboard, faCompass, faCompressArrowsAlt, faExpandArrowsAlt, faFileUpload, faPaintBrush, faTable, faUpload } from "@fortawesome/free-solid-svg-icons";
 import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
@@ -286,7 +286,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerMove = (e: PointerEvent): void => {
-        if (!e.cancelBubble && !this.isAnnotationOverlay) {
+        if (!e.cancelBubble) {
             if (this._hitCluster && this.tryDragCluster(e)) {
                 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
                 e.preventDefault();
@@ -339,7 +339,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
-        if (this.props.Document.lockedPosition || this.props.Document.inOverlay || this.isAnnotationOverlay) return;
+        if (this.props.Document.lockedPosition || this.props.Document.inOverlay) return;
         if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming
             e.stopPropagation();
         }
@@ -477,7 +477,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             PanelWidth: layoutDoc[WidthSym],
             PanelHeight: layoutDoc[HeightSym],
             ContentScaling: returnOne,
-            ContainingCollectionView: this.props.CollectionView,
+            ContainingCollectionView: this.props.ContainingCollectionView,
             focus: this.focusDocument,
             backgroundColor: returnEmptyString,
             parentActive: this.props.active,
@@ -681,10 +681,14 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
 
-    private childViews = () => [
-        <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
-        ...this.views
-    ]
+    private childViews = () => {
+        let children = typeof this.props.children === "function" ? (this.props.children as any)() as JSX.Element[] : [];
+        return [
+            <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
+            ...children,
+            ...this.views,
+        ];
+    }
     render() {
         // update the actual dimensions of the collection so that they can inquired (e.g., by a minimap)
         this.props.Document.fitX = this.contentBounds && this.contentBounds.x;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index bb787106c..ecdd02b0f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -217,7 +217,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             this._downY = y;
             PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
         }
-    })
+    });
 
     @action
     onClick = (e: React.MouseEvent): void => {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 9ca77cc8b..cbe4945af 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -218,13 +218,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
             const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
             let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
-            let tr = this._editorView!.state.tr;
+            let tr = this._editorView.state.tr;
             let flattened: TextSelection[] = [];
             res.map(r => r.map(h => flattened.push(h)));
             let lastSel = Math.min(flattened.length - 1, this._searchIndex);
             flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
             this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
-            this._editorView!.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
+            this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
         }
     }
 
@@ -232,8 +232,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (this._editorView && (this._editorView as any).docView) {
             const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
             const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
-            let end = this._editorView!.state.doc.nodeSize - 2;
-            this._editorView!.dispatch(this._editorView!.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
+            let end = this._editorView.state.doc.nodeSize - 2;
+            this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
         }
     }
     setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => {
diff --git a/src/client/views/nodes/ImageBox.scss b/src/client/views/nodes/ImageBox.scss
index 71d718b39..97d858f58 100644
--- a/src/client/views/nodes/ImageBox.scss
+++ b/src/client/views/nodes/ImageBox.scss
@@ -1,9 +1,9 @@
-.imageBox-cont {
+.imageBox-cont, .imageBox-cont-interactive {
     padding: 0vw;
-    position: relative;
+    position: absolute;
     text-align: center;
     width: 100%;
-    height: auto;
+    height: 100%;
     max-width: 100%;
     max-height: 100%;
     pointer-events: none;
@@ -11,8 +11,6 @@
 
 .imageBox-cont-interactive {
     pointer-events: all;
-    width: 100%;
-    height: auto;
 }
 
 .imageBox-dot {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index a198a0764..9f39eccea 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -13,20 +13,21 @@ import { ComputedField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
 import { AudioField, ImageField } from '../../../new_fields/URLField';
 import { RouteStore } from '../../../server/RouteStore';
-import { Utils } from '../../../Utils';
+import { Utils, returnOne, emptyFunction } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from "../../views/ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
-import { DocComponent } from '../DocComponent';
+import { DocAnnotatableComponent } from '../DocComponent';
 import { InkingControl } from '../InkingControl';
 import { documentSchema } from './DocumentView';
 import FaceRectangles from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ImageBox.scss";
 import React = require("react");
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 var requestImageSize = require('../../util/request-image-size');
 var path = require('path');
 const { Howl } = require('howler');
@@ -54,9 +55,10 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
 const ImageDocument = makeInterface(pageSchema, documentSchema);
 
 @observer
-export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
 
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
+    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(ImageBox, "data", fieldExt); }
+    @observable static _showControls: boolean;
     private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
     private _downX: number = 0;
     private _downY: number = 0;
@@ -65,10 +67,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     private dropDisposer?: DragManager.DragDropDisposer;
     @observable private hoverActive = false;
 
-    @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) {
             this.dropDisposer();
@@ -381,7 +379,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         return (null);
     }
 
-    render() {
+    @computed
+    get content() {
         // let transform = this.props.ScreenToLocalTransform().inverse();
         let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
         // var [sptX, sptY] = transform.transformPoint(0, 0);
@@ -393,7 +392,6 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
-        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         let alts = DocListCast(this.extensionDoc.Alternates);
         let altpaths: string[] = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
         let field = this.dataDoc[this.props.fieldKey];
@@ -448,4 +446,30 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
                 <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
             </div>);
     }
+
+    render() {
+        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+        return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
+            <CollectionFreeFormView {...this.props}
+                PanelHeight={this.props.PanelHeight}
+                PanelWidth={this.props.PanelWidth}
+                focus={this.props.focus}
+                isSelected={this.props.isSelected}
+                select={emptyFunction}
+                active={this.active}
+                ContentScaling={returnOne}
+                whenActiveChanged={this.whenActiveChanged}
+                removeDocument={this.removeDocument}
+                moveDocument={this.moveDocument}
+                addDocument={this.addDocument}
+                CollectionView={undefined}
+                ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                ruleProvider={undefined}
+                renderDepth={this.props.renderDepth + 1}
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+                chromeCollapsed={true}>
+                {() => [this.content]}
+            </CollectionFreeFormView>
+        </div >);
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 57803be1f..9af6d7cad 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,17 +1,21 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from 'mobx';
+import { action, observable, runInAction } from 'mobx';
 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, Opt, WidthSym } from "../../../new_fields/Doc";
+import { Opt, WidthSym } from "../../../new_fields/Doc";
 import { makeInterface } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
-import { Cast, NumCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
 import { PdfField } from "../../../new_fields/URLField";
+import { Utils } from '../../../Utils';
 import { KeyCodes } from '../../northstar/utils/KeyCodes';
+import { undoBatch } from '../../util/UndoManager';
 import { panZoomSchema } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { DocComponent } from "../DocComponent";
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { DocAnnotatableComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
 import { PDFViewer } from "../pdf/PDFViewer";
 import { documentSchema } from "./DocumentView";
@@ -19,22 +23,17 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
-import { undoBatch } from '../../util/UndoManager';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ContextMenu } from '../ContextMenu';
-import { Utils } from '../../../Utils';
 
 type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
 
 @observer
-export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
     public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
     private _keyValue: string = "";
     private _valueValue: string = "";
     private _scriptValue: string = "";
     private _searchString: string = "";
-    private _isChildActive = false;
     private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title
     private _pdfViewer: PDFViewer | undefined;
     private _keyRef: React.RefObject<HTMLInputElement> = React.createRef();
@@ -46,9 +45,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
     @observable private _pageControls = false;
 
-    @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() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         if (pdfUrl instanceof PdfField) {
@@ -206,7 +202,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                     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._initialScale < 2.5 ? true : false} />
+                    fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
                 {this.settingsPanel()}
             </div>);
     }
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index b3cd439aa..48623eaaf 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -1,8 +1,6 @@
-// .videoBox-container {
-//     .collectionfreeformview-container {
-//         mix-blend-mode: multiply;
-//     }
-// }
+.videoBox-container {
+    pointer-events: all;
+}
 
 .videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen,
 .videoBox-content, .videoBox-content-interactive, .videoBox-cont-fullScreen {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index feb067d8f..aa9b28118 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -11,7 +11,7 @@ import { Utils, emptyFunction, returnOne } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
-import { DocComponent } from "../DocComponent";
+import { DocAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
 import { documentSchema } from "./DocumentView";
@@ -35,7 +35,7 @@ const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
 library.add(faVideo);
 
 @observer
-export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
     static _youtubeIframeCounter: number = 0;
     private _reactionDisposer?: IReactionDisposer;
     private _youtubeReactionDisposer?: IReactionDisposer;
@@ -43,16 +43,12 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     private _videoRef: HTMLVideoElement | null = null;
     private _youtubeIframeId: number = -1;
     private _youtubeContentCreated = false;
-    private _downX: number = 0;
-    private _downY: number = 0;
     private _isResetClick = 0;
-    private _isChildActive = false;
-    private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean) => void);
     @observable _forceCreateYouTubeIFrame = false;
-    @observable static _showControls: boolean;
     @observable _playTimer?: NodeJS.Timeout = undefined;
     @observable _fullScreen = false;
-    @observable public Playing: boolean = false;
+    @observable _playing = false;
+    @observable static _showControls: boolean;
     public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(VideoBox, "data", fieldExt); }
 
     public get player(): HTMLVideoElement | null {
@@ -72,7 +68,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     }
 
     @action public Play = (update: boolean = true) => {
-        this.Playing = true;
+        this._playing = true;
         update && this.player && this.player.play();
         update && this._youtubePlayer && this._youtubePlayer.playVideo();
         this._youtubePlayer && !this._playTimer && (this._playTimer = setInterval(this.updateTimecode, 5));
@@ -85,7 +81,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
     }
 
     @action public Pause = (update: boolean = true) => {
-        this.Playing = false;
+        this._playing = false;
         update && this.player && this.player.pause();
         update && this._youtubePlayer && this._youtubePlayer.pauseVideo && this._youtubePlayer.pauseVideo();
         this._youtubePlayer && this._playTimer && clearInterval(this._playTimer);
@@ -181,7 +177,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
             vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen);
             this._reactionDisposer && this._reactionDisposer();
             this._reactionDisposer = reaction(() => this.Document.currentTimecode || 0,
-                time => !this.Playing && (vref.currentTime = time), { fireImmediately: true });
+                time => !this._playing && (vref.currentTime = time), { fireImmediately: true });
         }
     }
 
@@ -218,7 +214,7 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
         let interactive = InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive";
         let style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
         return !field ? <div>Loading</div> :
-            <video className={`${style}`} ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
+            <video className={`${style}`} key="video" ref={this.setVideoRef} onCanPlay={this.videoLoad} controls={VideoBox._showControls}
                 onPlay={() => this.Play()} onSeeked={this.updateTimecode} onPause={() => this.Pause()} onClick={e => e.preventDefault()}>
                 <source src={field.url.href} type="video/mp4" />
                 Not supported.
@@ -246,13 +242,13 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
                 this.Pause();
                 return;
             }
-            if (event.data === YT.PlayerState.PLAYING && !this.Playing) this.Play(false);
-            if (event.data === YT.PlayerState.PAUSED && this.Playing) this.Pause(false);
+            if (event.data === YT.PlayerState.PLAYING && !this._playing) this.Play(false);
+            if (event.data === YT.PlayerState.PAUSED && this._playing) this.Pause(false);
         });
         let onYoutubePlayerReady = (event: any) => {
             this._reactionDisposer && this._reactionDisposer();
             this._youtubeReactionDisposer && this._youtubeReactionDisposer();
-            this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this.Playing && this.Seek(this.Document.currentTimecode || 0));
+            this._reactionDisposer = reaction(() => this.Document.currentTimecode, () => !this._playing && this.Seek(this.Document.currentTimecode || 0));
             this._youtubeReactionDisposer = reaction(() => [this.props.isSelected(), DocumentDecorations.Instance.Interacting, InkingControl.Instance.selectedTool], () => {
                 let interactive = InkingControl.Instance.selectedTool === InkTool.None && this.props.isSelected() && !DocumentDecorations.Instance.Interacting;
                 iframe.style.pointerEvents = interactive ? "all" : "none";
@@ -278,22 +274,16 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
         </div>,
         VideoBox._showControls ? (null) : [
             <div className="videoBox-play" key="play" onPointerDown={this.onPlayDown} style={{ transform: `scale(${scaling})` }}>
-                <FontAwesomeIcon icon={this.Playing ? "pause" : "play"} size="lg" />
+                <FontAwesomeIcon icon={this._playing ? "pause" : "play"} size="lg" />
             </div>,
-            <div className="collecvideoBoxtionVideoView-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
+            <div className="videoBox-full" key="full" onPointerDown={this.onFullDown} style={{ transform: `scale(${scaling})` }}>
                 F
             </div>
         ]]);
     }
 
     @action
-    onPlayDown = () => {
-        if (this.Playing) {
-            this.Pause();
-        } else {
-            this.Play();
-        }
-    }
+    onPlayDown = () => this._playing ? this.Pause() : this.Play()
 
     @action
     onFullDown = (e: React.PointerEvent) => {
@@ -342,97 +332,40 @@ export class VideoBox extends DocComponent<FieldViewProps, VideoDocument>(VideoD
         let start = untracked(() => Math.round(this.Document.currentTimecode || 0));
         return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
             onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
-            src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`}
-        ></iframe>;
-    }
-
-
-    setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => {
-        this._setPreviewCursor = func;
+            src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
     }
 
     @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.fieldExtensionDoc;
-        let targetField = this.props.fieldExt;
-        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);
-        index !== -1 && value.splice(index, 1);
-        return true;
-    }
-    // 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 {
-        if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
-            return true;
-        }
-        return this.removeDocument(doc) ? addDocument(doc) : false;
-    }
-
-    @action.bound
-    addDocument(doc: Doc): boolean {
+    addDocumentWithTimestamp(doc: Doc): boolean {
         Doc.GetProto(doc).annotationOn = this.props.Document;
         var curTime = NumCast(this.props.Document.currentTimecode, -1);
         curTime !== -1 && (doc.displayTimecode = curTime);
-        Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, doc);
-        return true;
-    }
-    whenActiveChanged = (isActive: boolean) => {
-        this._isChildActive = isActive;
-        this.props.whenActiveChanged(isActive);
-    }
-    active = () => {
-        return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
-    }
-    onClick = (e: React.MouseEvent) => {
-        this._setPreviewCursor &&
-            e.button === 0 &&
-            Math.abs(e.clientX - this._downX) < 3 &&
-            Math.abs(e.clientY - this._downY) < 3 &&
-            this._setPreviewCursor(e.clientX, e.clientY, false);
-    }
-
-    onPointerDown = (e: React.PointerEvent): void => {
-        this._downX = e.clientX;
-        this._downY = e.clientY;
-        if ((e.button !== 0 || e.altKey) && this.active()) {
-            this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
-        }
-    }
-    @computed get annotationLayer() {
-        return <CollectionFreeFormView {...this.props}
-            setPreviewCursor={this.setPreviewCursor}
-            PanelHeight={this.props.PanelHeight}
-            PanelWidth={this.props.PanelWidth}
-            focus={this.props.focus}
-            isSelected={this.props.isSelected}
-            select={emptyFunction}
-            active={this.active}
-            ContentScaling={returnOne}
-            whenActiveChanged={this.whenActiveChanged}
-            removeDocument={this.removeDocument}
-            moveDocument={this.moveDocument}
-            addDocument={this.addDocument}
-            CollectionView={undefined}
-            ScreenToLocalTransform={this.props.ScreenToLocalTransform}
-            ruleProvider={undefined}
-            renderDepth={this.props.renderDepth + 1}
-            ContainingCollectionDoc={this.props.ContainingCollectionDoc}
-            chromeCollapsed={true}>
-        </CollectionFreeFormView>
+        return Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, doc);
     }
 
     render() {
         Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
-        return (<div className={"videoBox-container"} onClick={this.onClick} onPointerDown={this.onPointerDown} onContextMenu={this.specificContextMenu}>
-            {this.youtubeVideoId ? this.youtubeContent : this.content}
-            {this.annotationLayer}
+        return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}>
+            <CollectionFreeFormView {...this.props}
+                PanelHeight={this.props.PanelHeight}
+                PanelWidth={this.props.PanelWidth}
+                focus={this.props.focus}
+                isSelected={this.props.isSelected}
+                select={emptyFunction}
+                active={this.active}
+                ContentScaling={returnOne}
+                whenActiveChanged={this.whenActiveChanged}
+                removeDocument={this.removeDocument}
+                moveDocument={this.moveDocument}
+                addDocument={this.addDocumentWithTimestamp}
+                CollectionView={undefined}
+                ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                ruleProvider={undefined}
+                renderDepth={this.props.renderDepth + 1}
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+                chromeCollapsed={true}>
+                {() => [this.youtubeVideoId ? this.youtubeContent : this.content]}
+            </CollectionFreeFormView>
             {this.uIButtons}
         </div >);
     }
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index bb4c5adc9..7c7f9fb83 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -1,34 +1,36 @@
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, observable } from "mobx";
 import { observer } from "mobx-react";
-import { FieldResult, Doc, Field } from "../../../new_fields/Doc";
+import { Doc, FieldResult } from "../../../new_fields/Doc";
 import { HtmlField } from "../../../new_fields/HtmlField";
+import { InkTool } from "../../../new_fields/InkField";
+import { makeInterface } from "../../../new_fields/Schema";
+import { Cast, NumCast } from "../../../new_fields/Types";
 import { WebField } from "../../../new_fields/URLField";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { SelectionManager } from "../../util/SelectionManager";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
 import { FieldView, FieldViewProps } from './FieldView';
+import { KeyValueBox } from "./KeyValueBox";
 import "./WebBox.scss";
 import React = require("react");
-import { InkTool } from "../../../new_fields/InkField";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { observable, action, computed } from "mobx";
-import { listSpec } from "../../../new_fields/Schema";
-import { RefField } from "../../../new_fields/RefField";
-import { ObjectField } from "../../../new_fields/ObjectField";
-import { updateSourceFile } from "typescript";
-import { KeyValueBox } from "./KeyValueBox";
-import { setReactionScheduler } from "mobx/lib/internal";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { SelectionManager } from "../../util/SelectionManager";
-import { Docs } from "../../documents/Documents";
+import { documentSchema } from "./DocumentView";
+import { DocAnnotatableComponent } from "../DocComponent";
 
 library.add(faStickyNote);
 
+type WebDocument = makeInterface<[typeof documentSchema]>;
+const WebDocument = makeInterface(documentSchema);
+
 @observer
-export class WebBox extends React.Component<FieldViewProps> {
+export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
 
-    public static LayoutString() { return FieldView.LayoutString(WebBox); }
+    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(WebBox, "data", fieldExt); }
     @observable private collapsed: boolean = true;
     @observable private url: string = "";
 
@@ -162,8 +164,10 @@ export class WebBox extends React.Component<FieldViewProps> {
             e.stopPropagation();
         }
     }
-    render() {
-        let field = this.props.Document[this.props.fieldKey];
+
+    @computed
+    get content() {
+        let field = this.dataDoc[this.props.fieldKey];
         let view;
         if (field instanceof HtmlField) {
             view = <span id="webBox-htmlSpan" dangerouslySetInnerHTML={{ __html: field.html }} />;
@@ -189,4 +193,29 @@ export class WebBox extends React.Component<FieldViewProps> {
                 {!frozen ? (null) : <div className="webBox-overlay" onWheel={this.onPreWheel} onPointerDown={this.onPrePointer} onPointerMove={this.onPrePointer} onPointerUp={this.onPrePointer} />}
             </>);
     }
+    render() {
+        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+        return (<div className={"imageBox-container"} >
+            <CollectionFreeFormView {...this.props}
+                PanelHeight={this.props.PanelHeight}
+                PanelWidth={this.props.PanelWidth}
+                focus={this.props.focus}
+                isSelected={this.props.isSelected}
+                select={emptyFunction}
+                active={this.active}
+                ContentScaling={returnOne}
+                whenActiveChanged={this.whenActiveChanged}
+                removeDocument={this.removeDocument}
+                moveDocument={this.moveDocument}
+                addDocument={this.addDocument}
+                CollectionView={undefined}
+                ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                ruleProvider={undefined}
+                renderDepth={this.props.renderDepth + 1}
+                ContainingCollectionDoc={this.props.ContainingCollectionDoc}
+                chromeCollapsed={true}>
+                {() => [this.content]}
+            </CollectionFreeFormView>
+        </div >);
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 5a07b88d9..ad6240c70 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -11,7 +11,7 @@ import "./Annotation.scss";
 
 interface IAnnotationProps {
     anno: Doc;
-    fieldExtensionDoc: Doc;
+    extensionDoc: Doc;
     addDocTab: (document: Doc, dataDoc: Opt<Doc>, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     focus: (doc: Doc) => void;
@@ -29,7 +29,7 @@ interface IRegionAnnotationProps {
     y: number;
     width: number;
     height: number;
-    fieldExtensionDoc: Doc;
+    extensionDoc: Doc;
     addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     document: Doc;
@@ -66,12 +66,12 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     }
 
     deleteAnnotation = () => {
-        let annotation = DocListCast(this.props.fieldExtensionDoc.annotations);
+        let annotation = DocListCast(this.props.extensionDoc.annotations);
         let group = FieldValue(Cast(this.props.document.group, Doc));
         if (group) {
             if (annotation.indexOf(group) !== -1) {
                 let newAnnotations = annotation.filter(a => a !== FieldValue(Cast(this.props.document.group, Doc)));
-                this.props.fieldExtensionDoc.annotations = new List<Doc>(newAnnotations);
+                this.props.extensionDoc.annotations = new List<Doc>(newAnnotations);
             }
 
             DocListCast(group.annotations).forEach(anno => anno.delete = true);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 366861144..d0759d207 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -6,7 +6,7 @@ import { Dictionary } from "typescript-collections";
 import { Doc, DocListCast, FieldResult, WidthSym, Opt, HeightSym } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
+import { makeInterface } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { smoothScroll, Utils, emptyFunction, returnOne, intersectRect, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from "../../../Utils";
@@ -23,19 +23,24 @@ import Annotation from "./Annotation";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { SelectionManager } from "../../util/SelectionManager";
 import { undoBatch } from "../../util/UndoManager";
+import { DocAnnotatableComponent } from "../DocComponent";
+import { documentSchema } from "../nodes/DocumentView";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
 pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`;
+type PdfDocument = makeInterface<[typeof documentSchema]>;
+const PdfDocument = makeInterface(documentSchema);
 
 interface IViewerProps {
     pdf: Pdfjs.PDFDocumentProxy;
     url: string;
-    Document: Doc;
-    DataDoc?: Doc;
-    fieldExtensionDoc: Doc;
     fieldKey: string;
     fieldExt: string;
+    Document: Doc;
+    DataDoc?: Doc;
+    ContainingCollectionView: Opt<CollectionView>;
+    extensionDoc: Doc;
     PanelWidth: () => number;
     PanelHeight: () => number;
     ContentScaling: () => number;
@@ -52,7 +57,6 @@ interface IViewerProps {
     addDocument?: (doc: Doc) => boolean;
     setPdfViewer: (view: PDFViewer) => void;
     ScreenToLocalTransform: () => Transform;
-    ContainingCollectionView: Opt<CollectionView>;
     whenActiveChanged: (isActive: boolean) => void;
 }
 
@@ -60,7 +64,7 @@ interface IViewerProps {
  * Handles rendering and virtualization of the pdf
  */
 @observer
-export class PDFViewer extends React.Component<IViewerProps> {
+export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) {
     static _annotationStyle: any = addStyleSheet();
     @observable private _pageSizes: { width: number, height: number }[] = [];
     @observable private _annotations: Doc[] = [];
@@ -79,7 +83,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     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);
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _reactionDisposer?: IReactionDisposer;
@@ -97,7 +100,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _coverPath: any;
 
     @computed get allAnnotations() {
-        return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
+        return DocListCast(this.props.extensionDoc.annotations).filter(
             anno => this._script.run({ this: anno }, console.log, true).result);
     }
 
@@ -186,7 +189,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         await this.initialLoad();
 
         this._annotationReactionDisposer = reaction(
-            () => this.props.fieldExtensionDoc && DocListCast(this.props.fieldExtensionDoc.annotations),
+            () => this.props.extensionDoc && DocListCast(this.props.extensionDoc.annotations),
             annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
             { fireImmediately: true });
 
@@ -530,7 +533,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     highlight = (color: string) => {
         // creates annotation documents for current highlights
         let annotationDoc = this.makeAnnotationDocument(color);
-        annotationDoc && Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
+        annotationDoc && Doc.AddDocToList(this.props.extensionDoc, this.props.fieldExt, annotationDoc);
         return annotationDoc;
     }
 
@@ -570,40 +573,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
         DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
     }
 
-    // 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 {
-        if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
-            return true;
-        }
-        return this.removeDocument(doc) ? addDocument(doc) : false;
-    }
-    @action.bound
-    addDocument(doc: Doc): boolean {
-        Doc.GetProto(doc).annotationOn = this.props.Document;
-        Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc);
-        return true;
-    }
-    @action.bound
-    removeDocument(doc: Doc): boolean {
-        Doc.GetProto(doc).annotationOn = undefined;
-        let targetDataDoc = this.props.fieldExtensionDoc;
-        let targetField = this.props.fieldExt;
-        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);
-        index !== -1 && value.splice(index, 1);
-        return true;
-    }
     scrollXf = () => {
         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;
-    }
     onClick = (e: React.MouseEvent) => {
         this._setPreviewCursor &&
             e.button === 0 &&
@@ -611,13 +583,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
             Math.abs(e.clientY - this._downY) < 3 &&
             this._setPreviewCursor(e.clientX, e.clientY, false);
     }
-    whenActiveChanged = (isActive: boolean) => {
-        this._isChildActive = isActive;
-        this.props.whenActiveChanged(isActive);
-    }
-    active = () => {
-        return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
-    }
+
+    setPreviewCursor = (func?: (x: number, y: number, drag: boolean) => void) => this._setPreviewCursor = func;
+
 
     getCoverImage = () => {
         if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
@@ -632,7 +600,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
             style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
     }
 
-
     @action
     onZoomWheel = (e: React.WheelEvent) => {
         e.stopPropagation();
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 418863bcc..eb752f8c6 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -332,8 +332,10 @@ export namespace Doc {
         return Array.from(results);
     }
 
-    export function IndexOf(toFind: Doc, list: Doc[]) {
-        return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
+    export function IndexOf(toFind: Doc, list: Doc[], allowProtos: boolean = true) {
+        let index = list.reduce((p, v, i) => (v instanceof Doc && v === toFind) ? i : p, -1);
+        index = allowProtos && index !== -1 ? index : list.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, toFind)) ? i : p, -1);
+        return index; // list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
     }
     export function RemoveDocFromList(listDoc: Doc, key: string, doc: Doc) {
         if (listDoc[key] === undefined) {
-- 
cgit v1.2.3-70-g09d2


From e6be0feda60963d28838d9fe7c79bf40cde6bc62 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 11 Oct 2019 10:15:29 -0400
Subject: added highlighting other's text.  fixed some exceptions with text
 highlighitng.

---
 src/client/util/RichTextSchema.tsx                 |  6 ++++--
 src/client/views/nodes/FormattedTextBox.tsx        | 10 ++++++----
 src/client/views/nodes/FormattedTextBoxComment.tsx |  2 +-
 3 files changed, 11 insertions(+), 7 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 063686d58..cc3548e1a 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -18,6 +18,7 @@ import { Transform } from "./Transform";
 import React = require("react");
 import { BoolCast, NumCast } from "../../new_fields/Types";
 import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 
 const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
     preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
@@ -451,9 +452,10 @@ export const marks: { [index: string]: MarkSpec } = {
             let min = Math.round(node.attrs.modified / 12);
             let hr = Math.round(min / 60);
             let day = Math.round(hr / 60 / 24);
+            let remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
             return node.attrs.opened ?
-                ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
-                ['span', { class: "userMark-" + uid + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
+                ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0] :
+                ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, ['span', 0]];
         }
     },
     // the id of the user who entered the text
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index cbe4945af..b05d0046c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -238,9 +238,8 @@ 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)));
+        view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
     }
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._proseRef = ele;
@@ -333,8 +332,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     updateHighlights = () => {
         clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+        if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-remote", { background: "yellow" });
+        }
         if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
-            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "yellow" });
+            addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
         }
         if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
             addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
@@ -364,7 +366,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     specificContextMenu = (e: React.MouseEvent): void => {
         let funcs: ContextMenuProps[] = [];
         funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
-        ["My Text", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
+        ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
                 description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
                     e.stopPropagation();
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index a75bfd373..bde278be3 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -93,7 +93,7 @@ export class FormattedTextBoxComment {
         if (lastState && lastState.doc.eq(state.doc) &&
             lastState.selection.eq(state.selection)) return;
 
-        if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props.isSelected()) return;
+        if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props || !FormattedTextBoxComment.textBox.props.isSelected()) return;
         let set = "none";
         if (FormattedTextBoxComment.textBox && state.selection.$from) {
             let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
-- 
cgit v1.2.3-70-g09d2


From 26e215b0cddbb4c14cfd8eb7a720a373e797c615 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Tue, 15 Oct 2019 00:23:00 -0400
Subject: cleaned up current_user_utils.  cleaned up some interactions with
 colorBox, textbox height

---
 src/client/documents/Documents.ts                  |  19 +-
 src/client/views/DocComponent.tsx                  |  34 ++-
 src/client/views/InkingControl.tsx                 |   8 +
 src/client/views/MainView.tsx                      |  16 +-
 src/client/views/nodes/ColorBox.scss               |   7 +-
 src/client/views/nodes/ColorBox.tsx                |  12 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  13 +-
 src/client/views/nodes/PDFBox.tsx                  |   3 +-
 .../authentication/models/current_user_utils.ts    | 295 ++++++++++-----------
 9 files changed, 229 insertions(+), 178 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 1f89d2993..13cc7815c 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -40,7 +40,7 @@ import { ButtonBox } from "../views/nodes/ButtonBox";
 import { FontIconBox } from "../views/nodes/FontIconBox";
 import { SchemaHeaderField, RandomPastel } from "../../new_fields/SchemaHeaderField";
 import { PresBox } from "../views/nodes/PresBox";
-import { ComputedField } from "../../new_fields/ScriptField";
+import { ComputedField, ScriptField } from "../../new_fields/ScriptField";
 import { ProxyField } from "../../new_fields/Proxy";
 import { DocumentType } from "./DocumentTypes";
 import { LinkFollowBox } from "../views/linking/LinkFollowBox";
@@ -65,7 +65,10 @@ export interface DocumentOptions {
     page?: number;
     scale?: number;
     fitWidth?: boolean;
+    forceActive?: boolean;
+    preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
     layout?: string | Doc;
+    hideHeadings?: boolean; // whether stacking view column headings should be hidden
     isTemplate?: boolean;
     templates?: List<string>;
     viewType?: number;
@@ -84,11 +87,21 @@ export interface DocumentOptions {
     documentText?: string;
     borderRounding?: string;
     boxShadow?: string;
+    sectionFilter?: string; // field key used to determine headings for sections in stacking and masonry views
     schemaColumns?: List<SchemaHeaderField>;
     dockingConfig?: string;
     autoHeight?: boolean;
+    removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped.  e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
     dbDoc?: Doc;
+    onClick?: ScriptField;
+    onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     icon?: string;
+    gridGap?: number; // gap between items in masonry view
+    xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
+    yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
+    panel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
+    targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
+    convertToButtons?: boolean; // whether documents dropped onto a collection should be converted to buttons that will construct the dropped document
     // [key: string]: Opt<Field>;
 }
 
@@ -457,6 +470,10 @@ export namespace Docs {
             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 LinearDocument(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.Linear }, id);
+        }
+
         export function SchemaDocument(schemaColumns: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { chromeStatus: "collapsed", schemaColumns: new List(schemaColumns), ...options, viewType: CollectionViewType.Schema });
         }
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 2c5992259..b05966bb5 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,10 +1,17 @@
 import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
 import { computed, action } from 'mobx';
-import { Cast } from '../../new_fields/Types';
+import { Cast, BoolCast } from '../../new_fields/Types';
 import { listSpec } from '../../new_fields/Schema';
+import { InkingControl } from './InkingControl';
+import { InkTool } from '../../new_fields/InkField';
 
-export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: Doc) => T) {
+
+///  DocComponents returns a generic base class for React views of document fields that are not interactive
+interface DocComponentProps {
+    Document: Doc;
+}
+export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
         @computed
@@ -15,6 +22,27 @@ export function DocComponent<P extends { Document: Doc }, T>(schemaCtor: (doc: D
     return Component;
 }
 
+
+///  DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox)
+interface DocStaticProps {
+    Document: Doc;
+    isSelected: () => boolean;
+    renderDepth: number;
+}
+export function DocStaticComponent<P extends DocStaticProps, T>(schemaCtor: (doc: Doc) => T) {
+    class Component extends React.Component<P> {
+        //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
+        @computed
+        get Document(): T {
+            return schemaCtor(this.props.Document);
+        }
+        active = () => (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0) && !InkingControl.Instance.selectedTool;
+    }
+    return Component;
+}
+
+
+///  DocAnnotatbleComponent return a base class for React views of document fields that are annotatable *and* interactive when selected (e.g., pdf, image)
 interface DocAnnotatableProps {
     Document: Doc;
     DataDoc?: Doc;
@@ -57,7 +85,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
             return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc);
         }
         whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
-        active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
+        active = () => (InkingControl.Instance.selectedTool === InkTool.None) && (BoolCast(this.props.Document.forceActive) || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0);
     }
     return Component;
 }
\ No newline at end of file
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 38734a03d..d42d8d2d9 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -48,6 +48,14 @@ export class InkingControl extends React.Component {
             let selected = SelectionManager.SelectedDocuments();
             let oldColors = selected.map(view => {
                 let targetDoc = view.props.Document.layout instanceof Doc ? view.props.Document.layout : view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+                let sel = window.getSelection();
+                if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
+                    targetDoc.color = this._selectedColor;
+                    return {
+                        target: targetDoc,
+                        previous: StrCast(targetDoc.color)
+                    };
+                }
                 let oldColor = StrCast(targetDoc.backgroundColor);
                 let matchedColor = this._selectedColor;
                 const cvd = view.props.ContainingCollectionDoc;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ddea3e223..701f094e2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -343,12 +343,12 @@ export class MainView extends React.Component {
         if (!(sidebarContent instanceof Doc)) {
             return (null);
         }
-        let libraryButtonDoc = Cast(CurrentUserUtils.UserDocument.libraryButtons, Doc) as Doc;
-        libraryButtonDoc.columnWidth = this.flyoutWidth / 3 - 30;
+        let sidebarButtonsDoc = Cast(CurrentUserUtils.UserDocument.sidebarButtons, Doc) as Doc;
+        sidebarButtonsDoc.columnWidth = this.flyoutWidth / 3 - 30;
         return <div className="mainView-flyoutContainer">
             <div className="mainView-tabButtons" style={{ height: `${this._buttonBarHeight}px` }}>
                 <DocumentView
-                    Document={libraryButtonDoc}
+                    Document={sidebarButtonsDoc}
                     DataDoc={undefined}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
@@ -443,18 +443,20 @@ export class MainView extends React.Component {
     }
 
     addButtonDoc = (doc: Doc) => {
-        Doc.AddDocToList(CurrentUserUtils.UserDocument, "docButtons", doc);
+        Doc.AddDocToList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc);
         return true;
     }
     remButtonDoc = (doc: Doc) => {
-        Doc.RemoveDocFromList(CurrentUserUtils.UserDocument, "docButtons", doc);
+        Doc.RemoveDocFromList(CurrentUserUtils.UserDocument.expandingButtons as Doc, "data", doc);
         return true;
     }
     @computed get docButtons() {
         return <div className="mainView-docButtons" style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20 }} >
             <MainViewNotifs />
-            <CollectionLinearView Document={CurrentUserUtils.UserDocument} DataDoc={undefined}
-                fieldKey={"docButtons"}
+            <CollectionLinearView 
+                Document={CurrentUserUtils.UserDocument.expandingButtons as Doc} 
+                DataDoc={undefined}
+                fieldKey={"data"}
                 fieldExt={""}
                 showHiddenControls={true}
                 select={emptyFunction}
diff --git a/src/client/views/nodes/ColorBox.scss b/src/client/views/nodes/ColorBox.scss
index 8df617fca..32a7891c6 100644
--- a/src/client/views/nodes/ColorBox.scss
+++ b/src/client/views/nodes/ColorBox.scss
@@ -1,10 +1,13 @@
-.colorBox-container {
+.colorBox-container, .colorBox-container-interactive {
      width:100%;
      height:100%;
      position: relative;
-     pointer-events:all;
+     pointer-events: none;
      
     .sketch-picker {
         margin:auto;
     }
+}
+.colorBox-container-interactive {
+    pointer-events:all;
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 4aff770f9..d280258ae 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -4,13 +4,19 @@ import { SketchPicker } from 'react-color';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ColorBox.scss";
 import { InkingControl } from "../InkingControl";
+import { DocStaticComponent } from "../DocComponent";
+import { documentSchema } from "./DocumentView";
+import { makeInterface } from "../../../new_fields/Schema";
+
+type ColorDocument = makeInterface<[typeof documentSchema]>;
+const ColorDocument = makeInterface(documentSchema);
 
 @observer
-export class ColorBox extends React.Component<FieldViewProps> {
+export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
     render() {
-        return <div className="colorBox-container" >
+        return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
             <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
         </div>;
     }
-}
\ No newline at end of file
+} 
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index b05d0046c..181f37d36 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -43,7 +43,6 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format
 import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
-import { TextShadowProperty } from 'csstype';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -490,7 +489,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         );
 
         this._heightReactionDisposer = reaction(
-            () => this.props.Document[WidthSym](),
+            () => [this.props.Document[WidthSym](), this.props.Document.autoHeight],
             () => this.tryUpdateHeight()
         );
 
@@ -984,13 +983,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @action
     tryUpdateHeight() {
-        const ChromeHeight = this.props.ChromeHeight;
-        let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
-        if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") {
+        let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
+        if (!this.props.Document.isAnimating && this.props.Document.autoHeight && scrollHeight !== 0 &&
+            getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
             let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
             let dh = NumCast(this.props.Document.height, 0);
-            this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
-            this.dataDoc.nativeHeight = nh ? sh : undefined;
+            this.props.Document.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
+            this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
         }
     }
 
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 63b412a23..19797400f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -115,7 +115,6 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => this._scriptValue = e.currentTarget.value;
 
     whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
-    active = () => this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
     setPdfViewer = (pdfViewer: PDFViewer) => { this._pdfViewer = pdfViewer; };
     searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => this._searchString = e.currentTarget.value;
 
@@ -205,7 +204,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     _initialScale: number | undefined;  // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not.  Getting bigger after startup won't make it automatically be live....
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
-        let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
+        let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
         let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf;
         if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale;
         if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 73cac879e..8af6bbfd5 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,20 +1,17 @@
-import { action, computed, observable, runInAction, reaction } from "mobx";
+import { action, computed, observable, reaction, runInAction } from "mobx";
 import * as rp from 'request-promise';
 import { DocServer } from "../../../client/DocServer";
 import { Docs } from "../../../client/documents/Documents";
 import { Attribute, AttributeGroup, Catalog, Schema } from "../../../client/northstar/model/idea/idea";
 import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil";
-import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView";
-import { CollectionView } from "../../../client/views/collections/CollectionView";
+import { UndoManager } from "../../../client/util/UndoManager";
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
-import { Cast, StrCast, PromiseValue } from "../../../new_fields/Types";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, PromiseValue } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
 import { RouteStore } from "../../RouteStore";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { ButtonBox } from "../../../client/views/nodes/ButtonBox";
-import { UndoManager } from "../../../client/util/UndoManager";
 
 export class CurrentUserUtils {
     private static curr_id: string;
@@ -22,176 +19,172 @@ export class CurrentUserUtils {
     private static mainDocId: string | undefined;
 
     public static get id() { return this.curr_id; }
-    @computed public static get UserDocument() { return Doc.UserDoc(); }
     public static get MainDocId() { return this.mainDocId; }
     public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
+    @computed public static get UserDocument() { return Doc.UserDoc(); }
 
     @observable public static GuestTarget: Doc | undefined;
     @observable public static GuestWorkspace: Doc | undefined;
 
     private static createUserDocument(id: string): Doc {
         let doc = new Doc(id, true);
-        doc.viewType = CollectionViewType.Tree;
-        doc.layout = CollectionView.LayoutString();
         doc.title = Doc.CurrentUserEmail;
-        doc.data = new List<Doc>();
-        doc.gridGap = 5;
-        doc.xMargin = 5;
-        doc.yMargin = 5;
-        doc.height = 42;
-        doc.boxShadow = "0 0";
-        doc.convertToButtons = true; // for CollectionLinearView used as the docButton layout 
-        doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
         return this.updateUserDocument(doc);// this should be the last 
     }
 
-    static updateUserDocument(doc: Doc) {
+    // a default set of note types .. not being used yet...
+    static setupNoteTypes(doc: Doc) {
+        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 })
+        ];
+        doc.noteTypes = Docs.Create.TreeDocument(notes, { title: "Note Types", height: 75 });
+    }
 
-        if (doc.undoBtn === undefined) {
-            doc.undoBtn = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "undo-alt" });
-            (doc.undoBtn as Doc).onClick = ScriptField.MakeScript('undo()');
-            Doc.AddDocToList(doc, "docButtons", doc.undoBtn as Doc);
-        }
-        if (doc.redoBtn === undefined) {
-            doc.redoBtn = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "redo-alt" });
-            (doc.redoBtn as Doc).onClick = ScriptField.MakeScript('redo()');
-            Doc.AddDocToList(doc, "docButtons", doc.redoBtn as Doc);
-        }
+    // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
+    static setupCreatorButtons() {
+        let docProtoData = [
+            { title: "collection", icon: "folder", script: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
+            { title: "web page", icon: "globe-asia", script: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
+            { title: "image", icon: "cat", script: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
+            { title: "button", icon: "bolt", script: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
+            { title: "presentation", icon: "tv", script: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
+            { title: "import folder", icon: "cloud-upload-alt", script: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
+        ];
+        return docProtoData.map(data => Docs.Create.FontIconDocument({
+            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100,
+            title: data.title, icon: data.icon, onDragStart: ScriptField.MakeFunction(data.script)
+        }));
+    }
+
+    // setup the Creator button which will display the creator panel.  This panel will include the drag creators and the color picker.  when clicked, this panel will be displayed in the target container (ie, sidebarContainer)  
+    static setupCreatePanel(sidebarContainer: Doc) {
+        // setup a masonry view of all he creators
+        const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(), {
+            width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons"
+        });
+        // setup a color picker
+        const color = Docs.Create.ColorDocument({
+            title: "color picker", width: 400, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+        });
+        color.dropAction = "alias";  // these must be set on the view document so they can't be part of the creator above.
+        color.forceActive = true;
+
+        return Docs.Create.ButtonDocument({
+            width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create", targetContainer: sidebarContainer,
+            panel: Docs.Create.StackingDocument([dragCreators, color], {
+                width: 500, height: 800, chromeStatus: "disabled", title: "creator stack"
+            }),
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+        });
+    }
+
+    // setup the Library button which will display the library panel.  This panel includes a collection of workspaces, documents, and recently closed views
+    static setupLibraryPanel(sidebarContainer: Doc, doc: Doc) {
         // setup workspaces library item
-        if (doc.workspaces === undefined) {
-            const workspaces = Docs.Create.TreeDocument([], { title: "WORKSPACES", height: 100 });
-            workspaces.boxShadow = "0 0";
-            doc.workspaces = workspaces;
-        }
-        PromiseValue(Cast(doc.workspaces, Doc)).then(workspaces => {
-            if (workspaces) {
-                workspaces.backgroundColor = "#eeeeee";
-                workspaces.preventTreeViewOpen = true;
-                workspaces.forceActive = true;
-                workspaces.lockedPosition = true;
-                if (StrCast(workspaces.title) === "Workspaces") {
-                    workspaces.title = "WORKSPACES";
-                }
-            }
+        doc.workspaces = Docs.Create.TreeDocument([], {
+            title: "WORKSPACES", height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
-        // 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 });
-            doc.noteTypes = noteTypes;
-        }
-        PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+        doc.documents = Docs.Create.TreeDocument([], {
+            title: "DOCUMENTS", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true
+        });
 
         // setup Recently Closed library item
-        if (doc.recentlyClosed === undefined) {
-            const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed".toUpperCase(), height: 75 });
-            recentlyClosed.boxShadow = "0 0";
-            doc.recentlyClosed = recentlyClosed;
-        }
-        PromiseValue(Cast(doc.recentlyClosed, Doc)).then(recent => {
-            if (recent) {
-                recent.backgroundColor = "#eeeeee";
-                recent.preventTreeViewOpen = true;
-                recent.forceActive = true;
-                recent.lockedPosition = true;
-                if (StrCast(recent.title) === "Recently Closed") {
-                    recent.title = "RECENTLY CLOSED";
-                }
-            }
+        doc.recentlyClosed = Docs.Create.TreeDocument([], {
+            title: "Recently Closed".toUpperCase(), height: 75, boxShadow: "0 0", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
+        return Docs.Create.ButtonDocument({
+            width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library",
+            panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], {
+                title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true
+            }),
+            targetContainer: sidebarContainer,
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+        });
+    }
 
-        if (doc.curPresentation === undefined) {
-            const curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation" });
-            curPresentation.boxShadow = "0 0";
-            doc.curPresentation = curPresentation;
-        }
+    // setup the Search button which will display the search panel.  
+    static setupSearchPanel(sidebarContainer: Doc) {
+        return Docs.Create.ButtonDocument({
+            width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search",
+            panel: Docs.Create.QueryDocument({
+                title: "search stack", ignoreClick: true
+            }),
+            targetContainer: sidebarContainer,
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+        });
+    }
 
-        if (doc.Library === undefined) {
-            let Search = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search" });
-            let Library = Docs.Create.ButtonDocument({ width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library" });
-            let Create = Docs.Create.ButtonDocument({ width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create" });
-            if (doc.sidebarContainer === undefined) {
-                doc.sidebarContainer = new Doc();
-                (doc.sidebarContainer as Doc).chromeStatus = "disabled";
-            }
+    // setup the list of sidebar mode buttons which determine what is displayed in the sidebar
+    static setupSidebarButtons(doc: Doc) {
+        doc.sidebarContainer = new Doc();
+        (doc.sidebarContainer as Doc).chromeStatus = "disabled";
 
-            const library = Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], { title: "Library" });
-            library.forceActive = true;
-            library.lockedPosition = true;
-            library.gridGap = 5;
-            library.xMargin = 5;
-            library.yMargin = 5;
-            library.dropAction = "alias";
-            Library.targetContainer = doc.sidebarContainer;
-            Library.library = library;
-            Library.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.library");
+        doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc);
+        doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc);
+        doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc);
 
-            const searchBox = Docs.Create.QueryDocument({ title: "search stack" });
-            searchBox.ignoreClick = true;
-            Search.searchBox = searchBox;
-            Search.targetContainer = doc.sidebarContainer;
-            Search.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.searchBox");
+        // Finally, setup the list of buttons to display in the sidebar
+        doc.sidebarButtons = Docs.Create.StackingDocument([doc.SearchBtn as Doc, doc.LibraryBtn as Doc, doc.CreateBtn as Doc], {
+            width: 500, height: 80, boxShadow: "0 0", sectionFilter: "title", hideHeadings: true, ignoreClick: true,
+            backgroundColor: "lightgrey", chromeStatus: "disabled", title: "library stack"
+        });
+    }
 
-            let createCollection = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Collection", icon: "folder" });
-            createCollection.onDragStart = ScriptField.MakeFunction('Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })');
-            let createWebPage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Web Page", icon: "globe-asia" });
-            createWebPage.onDragStart = ScriptField.MakeFunction('Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })');
-            let createCatImage = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Image", icon: "cat" });
-            createCatImage.onDragStart = ScriptField.MakeFunction('Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })');
-            let createButton = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Button", icon: "bolt" });
-            createButton.onDragStart = ScriptField.MakeFunction('Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })');
-            let createPresentation = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Presentation", icon: "tv" });
-            createPresentation.onDragStart = ScriptField.MakeFunction('Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })');
-            let createFolderImport = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, title: "Import Folder", icon: "cloud-upload-alt" });
-            createFolderImport.onDragStart = ScriptField.MakeFunction('Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })');
-            const dragCreators = Docs.Create.MasonryDocument([createCollection, createWebPage, createCatImage, createButton, createPresentation, createFolderImport], { width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons" });
-            const color = Docs.Create.ColorDocument({ title: "color picker", width: 400 });
-            color.dropAction = "alias";
-            color.ignoreClick = true;
-            color.removeDropProperties = new List<string>(["dropAction", "ignoreClick"]);
-            const creators = Docs.Create.StackingDocument([dragCreators, color], { width: 500, height: 800, chromeStatus: "disabled", title: "creator stack" });
-            Create.targetContainer = doc.sidebarContainer;
-            Create.creators = creators;
-            Create.onClick = ScriptField.MakeScript("this.targetContainer.proto = this.creators");
+    /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
+    static setupExpandingButtons(doc: Doc) {
+        doc.undoBtn = Docs.Create.FontIconDocument(
+            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
+        doc.redoBtn = Docs.Create.FontIconDocument(
+            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
 
-            const libraryButtons = Docs.Create.StackingDocument([Search, Library, Create], { width: 500, height: 80, chromeStatus: "disabled", title: "library stack" });
-            libraryButtons.sectionFilter = "title";
-            libraryButtons.boxShadow = "0 0";
-            libraryButtons.ignoreClick = true;
-            libraryButtons.hideHeadings = true;
-            libraryButtons.backgroundColor = "lightgrey";
+        doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
+            title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0",
+            backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, convertToButtons: true
+        });
+    }
 
-            doc.libraryButtons = libraryButtons;
-            doc.Library = Library;
-            doc.Create = Create;
-            doc.Search = Search;
-        }
-        PromiseValue(Cast(doc.libraryButtons, Doc)).then(libraryButtons => { });
-        PromiseValue(Cast(doc.Library, Doc)).then(library => library && library.library && library.targetContainer && (library.onClick as ScriptField).script.run({ this: library }));
-        PromiseValue(Cast(doc.Create, Doc)).then(async create => create && create.creators && create.targetContainer);
-        PromiseValue(Cast(doc.Search, Doc)).then(async search => search && search.searchBox && search.targetContainer);
+    // sets up the default set of documents to be shown in the Overlay layer
+    static setupOverlays(doc: Doc) {
+        doc.overlays = Docs.Create.FreeformDocument([], { title: "Overlays", backgroundColor: "#aca3a6" });
+        doc.linkFollowBox = Docs.Create.LinkFollowBoxDocument({ x: 250, y: 20, width: 500, height: 370, title: "Link Follower" });
+        Doc.AddDocToList(doc.overlays as Doc, "data", doc.linkFollowBox as Doc);
+    }
 
-        if (doc.overlays === undefined) {
-            const overlays = Docs.Create.FreeformDocument([], { title: "Overlays" });
-            Doc.GetProto(overlays).backgroundColor = "#aca3a6";
-            doc.overlays = overlays;
-        }
+    // the initial presentation Doc to use
+    static setupDefaultPresentation(doc: Doc) {
+        doc.curPresentation = Docs.Create.PresDocument(new List<Doc>(), { title: "Presentation", boxShadow: "0 0" });
+    }
 
-        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" })));
-        }
+    static setupMobileUploads(doc: Doc) {
+        doc.optionalRightCollection = Docs.Create.StackingDocument([], { title: "New mobile uploads" });
+    }
+
+    static updateUserDocument(doc: Doc) {
+        (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc);
+        (doc.noteTypes === undefined) && CurrentUserUtils.setupNoteTypes(doc);
+        (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc);
+        (doc.expandingButtons === undefined) && CurrentUserUtils.setupExpandingButtons(doc);
+        (doc.curPresentation === undefined) && CurrentUserUtils.setupDefaultPresentation(doc);
+        (doc.sidebarButtons === undefined) && CurrentUserUtils.setupSidebarButtons(doc);
+
+        // this is equivalent to using PrefetchProxies to make sure all the sidebarButtons and noteType internal Doc's have been retrieved.
+        PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
+        PromiseValue(Cast(doc.sidebarButtons, Doc)).then(stackingDoc => {
+            stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => {
+                sidebarButtons && sidebarButtons.map((sidebarBtn, i) => {
+                    sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => {
+                        btn && btn.panel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
+                    });
+                });
+            });
+        });
 
-        doc.title = "DOCUMENTS";
-        doc.backgroundColor = "#eeeeee";
-        doc.width = 100;
-        doc.preventTreeViewOpen = true;
-        doc.forceActive = true;
-        doc.lockedPosition = true;
+        // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
         doc.undoBtn && reaction(() => UndoManager.undoStack.slice(), () => (doc.undoBtn as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
         doc.redoBtn && reaction(() => UndoManager.redoStack.slice(), () => (doc.redoBtn as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
 
@@ -215,12 +208,8 @@ export class CurrentUserUtils {
         await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
             if (id && id !== "guest") {
                 return DocServer.GetRefField(id).then(async field => {
-                    if (field instanceof Doc) {
-                        await this.updateUserDocument(field);
-                        runInAction(() => Doc.SetUserDoc(field));
-                    } else {
-                        runInAction(() => Doc.SetUserDoc(this.createUserDocument(id)));
-                    }
+                    let userDoc = field instanceof Doc ? await this.updateUserDocument(field) : this.createUserDocument(id);
+                    runInAction(() => Doc.SetUserDoc(userDoc));
                 });
             } else {
                 throw new Error("There should be a user id! Why does Dash think there isn't one?");
-- 
cgit v1.2.3-70-g09d2


From 33811c112c7e479813908ba10f72813954a3e289 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 15 Oct 2019 16:25:10 -0400
Subject: working version of inking buttons

---
 src/client/documents/Documents.ts                  |  2 ++
 src/client/util/DragManager.ts                     |  4 +--
 src/client/util/SelectionManager.ts                | 11 --------
 src/client/views/CollectionLinearView.tsx          |  4 +--
 src/client/views/InkingControl.tsx                 |  6 ++--
 src/client/views/MainView.tsx                      |  6 ++--
 src/client/views/nodes/ColorBox.tsx                | 33 ++++++++++++++++++++--
 src/client/views/nodes/DocumentView.tsx            |  7 +++--
 src/client/views/nodes/FontIconBox.tsx             |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        | 16 ++++++-----
 src/new_fields/Doc.ts                              |  4 +++
 .../authentication/models/current_user_utils.ts    | 31 +++++++++++---------
 12 files changed, 78 insertions(+), 48 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 6df172f46..f4fce34ac 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -93,6 +93,8 @@ export interface DocumentOptions {
     autoHeight?: boolean;
     removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped.  e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
     dbDoc?: Doc;
+    unchecked?: ScriptField; // returns whether a check box is unchecked
+    activePen?: Doc; // which pen document is currently active (used as the radio button state for the 'unhecked' pen tool scripts)
     onClick?: ScriptField;
     onDragStart?: ScriptField; //script to execute at start of drag operation --  e.g., when a "creator" button is dragged this script generates a different document to drop
     icon?: string;
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 169f2edec..92666c03c 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -234,9 +234,9 @@ export namespace DragManager {
 
     export let StartDragFunctions: (() => void)[] = [];
 
-    export async function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+    export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
         runInAction(() => StartDragFunctions.map(func => func()));
-        await dragData.draggedDocuments.map(d => d.dragFactory);
+        dragData.draggedDocuments.map(d => d.dragFactory); // does this help?  trying to make sure the dragFactory Doc is loaded
         StartDrag(eles, dragData, downX, downY, options, options && options.finishDrag ? options.finishDrag :
             (dropData: { [id: string]: any }) => {
                 (dropData.droppedDocuments =
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index df1b46b33..2d717ca57 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -45,17 +45,6 @@ export namespace SelectionManager {
     }
 
     const manager = new Manager();
-    reaction(() => manager.SelectedDocuments, sel => {
-        let targetColor = "#FFFFFF";
-        if (sel.length > 0) {
-            let firstView = sel[0];
-            let doc = firstView.props.Document;
-            let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
-            let stored = StrCast(targetDoc.backgroundColor);
-            stored.length > 0 && (targetColor = stored);
-        }
-        InkingControl.Instance.updateSelectedColor(targetColor);
-    }, { fireImmediately: true });
 
     export function DeselectDoc(docView: DocumentView): void {
         manager.DeselectDoc(docView);
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 9d1dd7749..eb3c768d0 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -48,7 +48,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
     drop = action((e: Event, de: DragManager.DropEvent) => {
         (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => {
             let dbox = doc;
-            if (!doc.onDragStart && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) {
+            if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) {
                 dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: "bolt" });
                 dbox.dragFactory = doc;
                 dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
@@ -90,7 +90,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                         return <div className={`collectionLinearView-docBtn` + (pair.layout.onClick || pair.layout.onDragStart ? "-scalable" : "")} key={StrCast(pair.layout.title)} ref={dref}
                             style={{
                                 width: nested ? pair.layout[WidthSym]() : this.dimension(),
-                                height: nested ? pair.layout[HeightSym]() : this.dimension(),
+                                height: nested && pair.layout.isExpanded ? pair.layout[HeightSym]() : this.dimension(),
                                 transform: nested ? undefined : `translate(${deltaSize / 2}px, ${deltaSize / 2}px)`
                             }}  >
                             <DocumentView
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 41dec9f83..149a4fc8a 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -117,9 +117,9 @@ export class InkingControl {
     }
 
 }
-Scripting.addGlobal(function activatePen() { return InkingControl.Instance.switchTool(InkTool.Pen); });
-Scripting.addGlobal(function activateBrush() { return InkingControl.Instance.switchTool(InkTool.Highlighter); });
-Scripting.addGlobal(function activateEraser() { return InkingControl.Instance.switchTool(InkTool.Eraser); });
+Scripting.addGlobal(function activatePen(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Pen : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any) { InkingControl.Instance.switchTool(pen ? InkTool.Highlighter : InkTool.None); InkingControl.Instance.switchWidth(width); InkingControl.Instance.updateSelectedColor(color); });
+Scripting.addGlobal(function activateEraser(pen: any) { return InkingControl.Instance.switchTool(pen ? InkTool.Eraser : InkTool.None); });
 Scripting.addGlobal(function deactivateInk() { return InkingControl.Instance.switchTool(InkTool.None); });
 Scripting.addGlobal(function setInkWidth(width: any) { return InkingControl.Instance.switchWidth(width); });
 Scripting.addGlobal(function setInkColor(color: any) { return InkingControl.Instance.updateSelectedColor(color); });
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4e06763a4..8035916a2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -466,12 +466,12 @@ export class MainView extends React.Component {
     }
     _docBtnRef = React.createRef<HTMLDivElement>();
     @computed get docButtons() {
-        console.log("stuff = " + this.flyoutWidthFunc() + " " + this.getContentsHeight());
         if (CurrentUserUtils.UserDocument.expandingButtons instanceof Doc) {
-            return <div className="mainView-docButtons" ref={this._docBtnRef} style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20, height: "42px" }} >
+            return <div className="mainView-docButtons" ref={this._docBtnRef}
+                style={{ left: (this._flyoutTranslate ? this.flyoutWidth : 0) + 20, height: !CurrentUserUtils.UserDocument.expandingButtons.isExpanded ? "42px" : undefined }} >
                 <MainViewNotifs />
                 <CollectionLinearView
-                    Document={CurrentUserUtils.UserDocument.expandingButtons as Doc}
+                    Document={CurrentUserUtils.UserDocument.expandingButtons}
                     DataDoc={undefined}
                     fieldKey={"data"}
                     fieldExt={""}
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index d280258ae..87c91c121 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -7,6 +7,9 @@ import { InkingControl } from "../InkingControl";
 import { DocStaticComponent } from "../DocComponent";
 import { documentSchema } from "./DocumentView";
 import { makeInterface } from "../../../new_fields/Schema";
+import { trace, reaction, observable, action, IReactionDisposer } from "mobx";
+import { SelectionManager } from "../../util/SelectionManager";
+import { StrCast } from "../../../new_fields/Types";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
@@ -14,9 +17,35 @@ const ColorDocument = makeInterface(documentSchema);
 @observer
 export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+    _selectedDisposer: IReactionDisposer | undefined;
+    componentDidMount() {
+        this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(),
+            action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"),
+            { fireImmediately: true });
+
+        // compare to this reaction that used to be in Selection Manager
+        // reaction(() => manager.SelectedDocuments, sel => {
+        //     let targetColor = "#FFFFFF";
+        //     if (sel.length > 0) {
+        //         let firstView = sel[0];
+        //         let doc = firstView.props.Document;
+        //         let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
+        //         let stored = StrCast(targetDoc.backgroundColor);
+        //         stored.length > 0 && (targetColor = stored);
+        //     }
+        //     InkingControl.Instance.updateSelectedColor(targetColor);
+        // }, { fireImmediately: true });
+    }
+    componentWillUnmount() {
+        this._selectedDisposer && this._selectedDisposer();
+    }
+
+    @observable _startupColor = "black";
+
     render() {
-        return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`} onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
-            <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
+        return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
+            onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
+            <SketchPicker color={this._startupColor} onChange={InkingControl.Instance.switchColor} />
         </div>;
     }
 } 
\ No newline at end of file
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c0e5185c1..6f99c541f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -107,7 +107,7 @@ export const documentSchema = createSchema({
     removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
     onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
-    dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script
+    dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     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
@@ -676,6 +676,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
         const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"];
+        let highlighting = fullDegree && this.props.Document.type !== DocumentType.FONTICON && this.props.Document.viewType !== CollectionViewType.Linear
         return (
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
@@ -683,8 +684,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),
-                    outline: fullDegree && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
-                    border: fullDegree && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
+                    outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
+                    border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
                     background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
                     width: animwidth,
                     height: animheight,
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index aa442cd92..efe47d8a8 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -18,7 +18,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
 
     render() {
         return <button className="fontIconBox-outerDiv" style={{ background: StrCast(this.props.Document.backgroundColor) }}>
-            <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} size="sm" color="white" />
+            <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} size="sm" opacity={this.props.Document.unchecked ? "0.5" : "1"} />
         </button>;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 181f37d36..1bdff3ec7 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -927,16 +927,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     onMouseUp = (e: React.MouseEvent): void => {
         e.stopPropagation();
 
-        // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there are nested prosemirrors.  We only want the lowest level prosemirror to be invoked.
-        if ((this._editorView as any).mouseDown) {
-            let originalUpHandler = (this._editorView as any).mouseDown.up;
-            (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler);
-            (this._editorView as any).mouseDown.up = (e: MouseEvent) => {
+        let view = this._editorView as any;
+        // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there 
+        // are nested prosemirrors.  We only want the lowest level prosemirror to be invoked.
+        if (view.mouseDown) {
+            let originalUpHandler = view.mouseDown.up;
+            view.root.removeEventListener("mouseup", originalUpHandler);
+            view.mouseDown.up = (e: MouseEvent) => {
                 !(e as any).formattedHandled && originalUpHandler(e);
-                e.stopPropagation();
+                // e.stopPropagation();
                 (e as any).formattedHandled = true;
             };
-            (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up);
+            view.root.addEventListener("mouseup", view.mouseDown.up);
         }
     }
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 4bed113e3..276596fb8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -525,6 +525,7 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string): Doc {
         const copy = new Doc(copyProtoId, true);
         Object.keys(doc).forEach(key => {
+            let cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key]));
             const field = ProxyField.WithoutProxy(() => doc[key]);
             if (key === "proto" && copyProto) {
                 if (doc[key] instanceof Doc) {
@@ -533,6 +534,8 @@ export namespace Doc {
             } else {
                 if (field instanceof RefField) {
                     copy[key] = field;
+                } else if (cfield instanceof ComputedField) {
+                    copy[key] = ComputedField.MakeFunction(cfield.script.originalScript);
                 } else if (field instanceof ObjectField) {
                     copy[key] = ObjectField.MakeCopy(field);
                 } else if (field instanceof Promise) {
@@ -735,5 +738,6 @@ Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return Doc.Make
 Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
 Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
 Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
+Scripting.addGlobal(function sameDocs(doc1: any, doc2: any) { return Doc.AreProtosEqual(doc1, doc2) });
 Scripting.addGlobal(function undo() { return UndoManager.Undo(); });
 Scripting.addGlobal(function redo() { return UndoManager.Redo(); });
\ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index c4b91dadd..3c4a46ed8 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -8,7 +8,7 @@ import { UndoManager } from "../../../client/util/UndoManager";
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
-import { ScriptField } from "../../../new_fields/ScriptField";
+import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
 import { Cast, PromiseValue } from "../../../new_fields/Types";
 import { Utils } from "../../../Utils";
 import { RouteStore } from "../../RouteStore";
@@ -45,29 +45,32 @@ export class CurrentUserUtils {
     }
 
     // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
-    static setupCreatorButtons() {
-        let docProtoData: { title: string, icon: string, drag?: string, click?: string }[] = [
+    static setupCreatorButtons(doc: Doc) {
+        doc.activePen = doc;
+        let docProtoData: { title: string, icon: string, drag?: string, click?: string, unchecked?: string, activePen?: Doc, backgroundColor?: string }[] = [
             { title: "collection", icon: "folder", drag: 'Docs.Create.FreeformDocument([], { nativeWidth: undefined, nativeHeight: undefined, width: 150, height: 100, title: "freeform" })' },
             { title: "web page", icon: "globe-asia", drag: 'Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" })' },
             { title: "image", icon: "cat", drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { width: 200, title: "an image of a cat" })' },
             { title: "button", icon: "bolt", drag: 'Docs.Create.ButtonDocument({ width: 150, height: 50, title: "Button" })' },
             { title: "presentation", icon: "tv", drag: 'Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" })' },
             { title: "import folder", icon: "cloud-upload-alt", drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", width: 400, height: 400 })' },
-            { title: "pen", icon: "pen-nib", click: 'activatePen(); setInkWidth(2);' },
-            { title: "highlighter", icon: "pen", click: 'activateBrush(); setInkWidth(20);' },
-            { title: "eraser", icon: "eraser", click: 'activateEraser();' },
-            { title: "none", icon: "pause", click: 'deactivateInk();' },
+            { title: "pen", icon: "pen-nib", click: 'activatePen(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", unchecked: `!sameDocs(this.activePen.pen,  this)`, activePen: doc },
+            { title: "highlighter", icon: "pen", click: 'activateBrush(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "eraser", icon: "eraser", click: 'activateEraser(this.activePen.pen = sameDocs(this.activePen.pen, this) ? undefined : this);', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
+            { title: "none", icon: "pause", click: 'deactivateInk();this.activePen.pen = this;', unchecked: `!sameDocs(this.activePen.pen, this)`, activePen: doc },
         ];
         return docProtoData.map(data => Docs.Create.FontIconDocument({
-            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "alias" : undefined,
-            title: data.title, icon: data.icon, onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined
+            nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon,
+            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+            unchecked: data.unchecked ? ComputedField.MakeFunction(data.unchecked) : undefined, activePen: data.activePen,
+            backgroundColor: data.backgroundColor
         }));
     }
 
     // setup the Creator button which will display the creator panel.  This panel will include the drag creators and the color picker.  when clicked, this panel will be displayed in the target container (ie, sidebarContainer)  
-    static setupCreatePanel(sidebarContainer: Doc) {
+    static setupCreatePanel(sidebarContainer: Doc, doc: Doc) {
         // setup a masonry view of all he creators
-        const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(), {
+        const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), {
             width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons"
         });
         // setup a color picker
@@ -129,7 +132,7 @@ export class CurrentUserUtils {
         doc.sidebarContainer = new Doc();
         (doc.sidebarContainer as Doc).chromeStatus = "disabled";
 
-        doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc);
+        doc.CreateBtn = this.setupCreatePanel(doc.sidebarContainer as Doc, doc);
         doc.LibraryBtn = this.setupLibraryPanel(doc.sidebarContainer as Doc, doc);
         doc.SearchBtn = this.setupSearchPanel(doc.sidebarContainer as Doc);
 
@@ -143,9 +146,9 @@ export class CurrentUserUtils {
     /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
     static setupExpandingButtons(doc: Doc) {
         doc.undoBtn = Docs.Create.FontIconDocument(
-            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
+            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
         doc.redoBtn = Docs.Create.FontIconDocument(
-            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
+            { nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, dropAction: "alias", onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
 
         doc.expandingButtons = Docs.Create.LinearDocument([doc.undoBtn as Doc, doc.redoBtn as Doc], {
             title: "expanding buttons", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0",
-- 
cgit v1.2.3-70-g09d2


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

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

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

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


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

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

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

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


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

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

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

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


From 7763a08eb5ed931dbf854e2b72d07b7613791e2b Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 16 Oct 2019 21:27:25 -0400
Subject: some fixes to linearView for handling templates.  fixed DOCUMENTS on
 library view.   renaming a bunch of template stuff... still in progress.

---
 src/client/documents/Documents.ts                      |  1 +
 src/client/views/CollectionLinearView.tsx              | 18 ++++++++++++++----
 src/client/views/DocComponent.tsx                      |  2 +-
 src/client/views/DocumentDecorations.tsx               |  4 ++--
 src/client/views/InkingControl.tsx                     |  7 ++-----
 src/client/views/TemplateMenu.tsx                      |  2 +-
 src/client/views/collections/CollectionBaseView.tsx    | 10 +++++-----
 .../views/collections/CollectionStackingView.tsx       |  4 +++-
 src/client/views/collections/CollectionSubView.tsx     | 11 ++++++++---
 src/client/views/collections/CollectionTreeView.tsx    |  3 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx      |  2 +-
 src/client/views/nodes/ButtonBox.tsx                   |  6 +++++-
 src/client/views/nodes/DocumentView.tsx                |  6 +++---
 src/client/views/nodes/FormattedTextBox.tsx            |  2 +-
 src/client/views/nodes/VideoBox.tsx                    |  2 +-
 src/new_fields/Doc.ts                                  |  6 +++---
 src/server/authentication/models/current_user_utils.ts | 16 ++++++++--------
 17 files changed, 61 insertions(+), 41 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ccb08f4cd..2ffbc8394 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -70,6 +70,7 @@ export interface DocumentOptions {
     layout?: string | Doc;
     hideHeadings?: boolean; // whether stacking view column headings should be hidden
     isTemplate?: boolean;
+    isTemplateDoc?: boolean;
     templates?: List<string>;
     viewType?: number;
     backgroundColor?: string;
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 44d9b042e..3e2ab1459 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -1,7 +1,7 @@
 import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
+import { Doc, HeightSym, WidthSym, DocListCast } from '../../new_fields/Doc';
 import { ObjectField } from '../../new_fields/ObjectField';
 import { makeInterface } from '../../new_fields/Schema';
 import { ScriptField } from '../../new_fields/ScriptField';
@@ -51,9 +51,19 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
         (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => {
             let dbox = doc;
             if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) {
-                let template = doc.layout instanceof Doc && doc.layout.isTemplate ? doc.layout : doc;
-                template.isTemplate = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction;
-                dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplate ? "font" : "bolt" });
+                let template = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+                if (template.type === DocumentType.COL) {
+                    let layout = StrCast(template.layout).match(/fieldKey={"[^"]*"}/)![0];
+                    let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
+                    let docs = DocListCast(template[fieldKey]);
+                    docs.map(d => {
+                        Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(template));
+                    });
+                    template.isTemplateDoc = true;
+                } else {
+                    template.isTemplateDoc = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction;
+                }
+                dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplateDoc ? "font" : "bolt" });
                 dbox.dragFactory = template;
                 dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
                 dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index fbc27192c..b6b717be0 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -68,7 +68,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
             return index !== -1 && value.splice(index, 1) ? true : false;
         }
 
-        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
 
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
 
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2f40ea746..6e8ba2d3d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -380,7 +380,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             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)).
+        !usingRule && SelectionManager.SelectedDocuments().map(dv => dv.props.Document.layout instanceof Doc ? dv.props.Document.layout : dv.props.Document.isTemplateField ? dv.props.Document : Doc.GetProto(dv.props.Document)).
             map(d => d.borderRounding = `${Math.min(100, dist)}%`);
         e.stopPropagation();
         e.preventDefault();
@@ -481,7 +481,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 let actualdH = Math.max(height + (dH * scale), 20);
                 layoutDoc.x = (layoutDoc.x || 0) + dX * (actualdW - width);
                 layoutDoc.y = (layoutDoc.y || 0) + dY * (actualdH - height);
-                let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
+                let proto = doc.isTemplateField ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
                 let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
                 if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
                     layoutDoc.ignoreAspect = false;
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 46c6fae1c..bc5249acd 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -43,7 +43,7 @@ export class InkingControl {
             let oldColors = selected.map(view => {
                 let targetDoc = view.props.Document.dragFactory instanceof Doc ? view.props.Document.dragFactory :
                     view.props.Document.layout instanceof Doc ? view.props.Document.layout :
-                        view.props.Document.isTemplate ? view.props.Document : Doc.GetProto(view.props.Document);
+                        view.props.Document.isTemplateField ? view.props.Document : Doc.GetProto(view.props.Document);
                 let sel = window.getSelection();
                 if (StrCast(targetDoc.layout).indexOf("FormattedTextBox") !== -1 && (!sel || sel.toString() !== "")) {
                     targetDoc.color = this._selectedColor;
@@ -81,10 +81,7 @@ export class InkingControl {
                     ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
                     ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
                 }
-                if (!ruleProvider) {
-                    if (targetDoc)
-                        targetDoc.backgroundColor = matchedColor;
-                }
+                (!ruleProvider && targetDoc) && (targetDoc.backgroundColor = matchedColor);
 
                 return {
                     target: targetDoc,
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 9e5e62e03..da776f887 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -100,7 +100,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     clearTemplates = (event: React.MouseEvent) => {
         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) {
+            if (d.Document.isTemplateDoc && 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;
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 7798964ea..15853fcae 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -84,7 +84,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         }
     }
 
-    @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 dataDoc() { return Doc.fieldExtensionDoc(this.props.Document.isTemplateField && 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 => {
@@ -106,8 +106,8 @@ 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;
         }
-        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 targetDataDoc = this.props.fieldExt || this.props.Document.isTemplateField ? this.extensionDoc : this.props.Document;
+        let targetField = (this.props.fieldExt || this.props.Document.isTemplateField) && this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey;
         Doc.AddDocToList(targetDataDoc, targetField, doc);
         Doc.GetProto(doc).lastOpened = new DateField;
         return true;
@@ -118,8 +118,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
         docView && SelectionManager.DeselectDoc(docView);
         //TODO This won't create the field if it doesn't already exist
-        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 targetDataDoc = this.props.fieldExt || this.props.Document.isTemplateField ? this.extensionDoc : this.props.Document;
+        let targetField = (this.props.fieldExt || this.props.Document.isTemplateField) && 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 && v === doc) ? i : p, -1);
         index = index !== -1 ? index : value.reduce((p, v, i) => (v instanceof Doc && Doc.AreProtosEqual(v, doc)) ? i : p, -1);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index cde1a5036..e54374ad7 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -110,6 +110,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     componentDidMount() {
+        super.componentDidMount();
         this._heightDisposer = reaction(() => {
             if (this.props.Document.autoHeight) {
                 let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
@@ -137,6 +138,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         );
     }
     componentWillUnmount() {
+        super.componentWillUnmount();
         this._heightDisposer && this._heightDisposer();
         this._sectionFilterDisposer && this._sectionFilterDisposer();
     }
@@ -167,7 +169,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             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}
+            onClick={layoutDoc.isTemplateDoc ? this.onClickHandler : this.onChildClickHandler}
             PanelWidth={width}
             PanelHeight={height}
             getTransform={finalDxf}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 9919a9dc3..46fbb5910 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -57,8 +57,13 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
 
         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))));
+                async (args) => {
+                    if (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)));
+                    } else {
+                        this.childDocs.map(async doc => doc.layout = undefined);
+                    }
+                });
 
         }
         componentWillUnmount() {
@@ -70,7 +75,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         // 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];
+            return Doc.fieldExtensionDoc(this.props.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey];
         }
 
 
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 403da0e54..0e9c38fb4 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -520,7 +520,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     private treedropDisposer?: DragManager.DragDropDisposer;
     private _mainEle?: HTMLDivElement;
 
-    @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+    @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplateField) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
 
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this.treedropDisposer && this.treedropDisposer();
@@ -530,6 +530,7 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
 
     componentWillUnmount() {
+        super.componentWillUnmount();
         this.treedropDisposer && this.treedropDisposer();
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 229e7fffc..2b0ef8ada 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -627,7 +627,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     onContextMenu = (e: React.MouseEvent) => {
         let layoutItems: ContextMenuProps[] = [];
 
-        if (this.childDocs.some(d => BoolCast(d.isTemplate))) {
+        if (this.childDocs.some(d => BoolCast(d.isTemplateDoc))) {
             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" });
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index 3cf8c3eb3..b4d33fb0f 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -32,7 +32,11 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
     public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
     private dropDisposer?: DragManager.DragDropDisposer;
 
-    @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 dataDoc() {
+        return this.props.DataDoc &&
+            (BoolCast(this.props.Document.isTemplateField) || BoolCast(this.props.DataDoc.isTemplateField) ||
+                this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
+    }
 
 
     protected createDropTarget = (ele: HTMLDivElement) => {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a071f782a..a7e78a7e8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -110,7 +110,7 @@ export const documentSchema = createSchema({
     dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
     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
+    isTemplateField: "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) 
@@ -196,7 +196,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 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);
+                this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
             } else if (this.props.Document.type === DocumentType.BUTTON) {
                 ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
             } else if (this.Document.isButton) {
@@ -376,7 +376,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     freezeNativeDimensions = (): void => {
-        let proto = this.Document.isTemplate ? this.props.Document : Doc.GetProto(this.props.Document);
+        let proto = this.Document.isTemplateDoc ? this.props.Document : Doc.GetProto(this.props.Document);
         proto.autoHeight = this.Document.autoHeight = false;
         proto.ignoreAspect = !proto.ignoreAspect;
         if (!proto.ignoreAspect && !proto.nativeWidth) {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f157a953e..ea82b1161 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -993,7 +993,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
         if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
-            let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
+            let nh = this.props.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
             let dh = NumCast(this.layoutDoc.height, 0);
             this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
             this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index aa9b28118..19968e6e1 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -323,7 +323,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         this._isResetClick < 10 && (this.props.Document.currentTimecode = 0);
     }
     @computed get fieldExtensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 21d4d16f6..51fcb818a 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -453,7 +453,7 @@ export namespace Doc {
     // for individual layout properties to be overridden in the expanded layout.
     //
     export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
-        return BoolCast(layoutDoc.isTemplate) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc);
+        return BoolCast(layoutDoc.isTemplateField) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc);
     }
 
     //
@@ -490,7 +490,7 @@ export namespace Doc {
 
     export function GetLayoutDataDocPair(doc: Doc, dataDoc: Doc | undefined, fieldKey: string, childDocLayout: Doc) {
         let layoutDoc: Doc | undefined = childDocLayout;
-        let resolvedDataDoc = !doc.isTemplate && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
+        let resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
         if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
             Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
             let fieldExtensionDoc = Doc.fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
@@ -739,7 +739,7 @@ export namespace Doc {
 Scripting.addGlobal(function renameAlias(doc: any, n: any) { return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, "") + `(${n})`; });
 Scripting.addGlobal(function getProto(doc: any) { return Doc.GetProto(doc); });
 Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
-Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplate ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
+Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
 Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
 Scripting.addGlobal(function aliasDocs(field: any) { return new List<Doc>(field.map((d: any) => Doc.MakeAlias(d))); });
 Scripting.addGlobal(function docList(field: any) { return DocListCast(field); });
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index ef977c89a..5ce707011 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -36,11 +36,11 @@ export class CurrentUserUtils {
     // a default set of note types .. not being used yet...
     static setupNoteTypes(doc: Doc) {
         return [
-            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 }),
-            Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplate: true })
+            Docs.Create.TextDocument({ title: "Note", backgroundColor: "yellow", isTemplateDoc: true }),
+            Docs.Create.TextDocument({ title: "Idea", backgroundColor: "pink", isTemplateDoc: true }),
+            Docs.Create.TextDocument({ title: "Topic", backgroundColor: "lightBlue", isTemplateDoc: true }),
+            Docs.Create.TextDocument({ title: "Person", backgroundColor: "lightGreen", isTemplateDoc: true }),
+            Docs.Create.TextDocument({ title: "Todo", backgroundColor: "orange", isTemplateDoc: true })
         ];
     }
 
@@ -98,17 +98,17 @@ export class CurrentUserUtils {
         });
 
         doc.documents = Docs.Create.TreeDocument([], {
-            title: "DOCUMENTS", gridGap: 5, xMargin: 5, yMargin: 5, height: 42, width: 100, boxShadow: "0 0", backgroundColor: "#eeeeee", preventTreeViewOpen: true, forceActive: true, lockedPosition: true
+            title: "DOCUMENTS", height: 42, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         // setup Recently Closed library item
         doc.recentlyClosed = Docs.Create.TreeDocument([], {
-            title: "Recently Closed".toUpperCase(), height: 75, boxShadow: "0 0", preventTreeViewOpen: true, forceActive: true, lockedPosition: true, backgroundColor: "#eeeeee"
+            title: "RECENTLY CLOSED", height: 75, forceActive: true, boxShadow: "0 0", preventTreeViewOpen: true, lockedPosition: true, backgroundColor: "#eeeeee"
         });
 
         return Docs.Create.ButtonDocument({
             width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library",
-            panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc, doc.recentlyClosed as Doc], {
+            panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
                 title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true
             }),
             targetContainer: sidebarContainer,
-- 
cgit v1.2.3-70-g09d2


From caebaeca6993ff48609ab16dcc390d4d0458b03a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 16 Oct 2019 23:50:27 -0400
Subject: fixed applying templates to collections.  fixed templates containing
 collections

---
 src/client/documents/Documents.ts                  |  2 +-
 src/client/views/CollectionLinearView.tsx          | 35 ++++++++++++++--------
 src/client/views/InkingCanvas.tsx                  |  3 +-
 src/client/views/InkingControl.tsx                 |  2 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 .../views/collections/CollectionViewChromes.tsx    |  4 +--
 src/client/views/nodes/ColorBox.tsx                |  7 ++---
 src/client/views/nodes/FormattedTextBox.tsx        |  4 +--
 src/new_fields/Doc.ts                              | 14 +++++++--
 .../authentication/models/current_user_utils.ts    | 14 +++------
 10 files changed, 51 insertions(+), 36 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 2ffbc8394..62175cbe3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -69,7 +69,7 @@ export interface DocumentOptions {
     preventTreeViewOpen?: boolean; // ignores the treeViewOpen Doc flag which allows a treeViewItem's expande/collapse state to be independent of other views of the same document in the tree view
     layout?: string | Doc;
     hideHeadings?: boolean; // whether stacking view column headings should be hidden
-    isTemplate?: boolean;
+    isTemplateField?: boolean;
     isTemplateDoc?: boolean;
     templates?: List<string>;
     viewType?: number;
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 3e2ab1459..04e131135 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -47,24 +47,35 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
         }
     }
 
+    makeTemplate = (doc: Doc): boolean => {
+        let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+        let layout = StrCast(layoutDoc.layout).match(/fieldKey={"[^"]*"}/)![0];
+        let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
+        let docs = DocListCast(layoutDoc[fieldKey]);
+        let any = false;
+        docs.map(d => {
+            if (!StrCast(d.title).startsWith("-")) {
+                any = true;
+                return Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(layoutDoc));
+            }
+            if (d.type === DocumentType.COL) return this.makeTemplate(d);
+            return false;
+        });
+        return any;
+    }
+
     drop = action((e: Event, de: DragManager.DropEvent) => {
         (de.data as DragManager.DocumentDragData).draggedDocuments.map((doc, i) => {
             let dbox = doc;
             if (!doc.onDragStart && !doc.onClick && this.props.Document.convertToButtons && doc.viewType !== CollectionViewType.Linear) {
-                let template = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
-                if (template.type === DocumentType.COL) {
-                    let layout = StrCast(template.layout).match(/fieldKey={"[^"]*"}/)![0];
-                    let fieldKey = layout.replace('fieldKey={"', "").replace(/"}$/, "");
-                    let docs = DocListCast(template[fieldKey]);
-                    docs.map(d => {
-                        Doc.MakeMetadataFieldTemplate(d, Doc.GetProto(template));
-                    });
-                    template.isTemplateDoc = true;
+                let layoutDoc = doc.layout instanceof Doc && doc.layout.isTemplateField ? doc.layout : doc;
+                if (layoutDoc.type === DocumentType.COL) {
+                    layoutDoc.isTemplateDoc = this.makeTemplate(layoutDoc);
                 } else {
-                    template.isTemplateDoc = (template.type === DocumentType.TEXT || template.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction;
+                    layoutDoc.isTemplateDoc = (layoutDoc.type === DocumentType.TEXT || layoutDoc.layout instanceof Doc) && de.data instanceof DragManager.DocumentDragData && !de.data.userDropAction;
                 }
-                dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: template.isTemplateDoc ? "font" : "bolt" });
-                dbox.dragFactory = template;
+                dbox = Docs.Create.FontIconDocument({ nativeWidth: 100, nativeHeight: 100, width: 100, height: 100, backgroundColor: StrCast(doc.backgroundColor), title: "Custom", icon: layoutDoc.isTemplateDoc ? "font" : "bolt" });
+                dbox.dragFactory = layoutDoc;
                 dbox.removeDropProperties = doc.removeDropProperties instanceof ObjectField ? ObjectField.MakeCopy(doc.removeDropProperties) : undefined;
                 dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
             } else if (doc.viewType === CollectionViewType.Linear) {
diff --git a/src/client/views/InkingCanvas.tsx b/src/client/views/InkingCanvas.tsx
index 9ab320eab..920ebaedd 100644
--- a/src/client/views/InkingCanvas.tsx
+++ b/src/client/views/InkingCanvas.tsx
@@ -181,9 +181,10 @@ export class InkingCanvas extends React.Component<InkCanvasProps> {
 
     render() {
         let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None && !this.props.Document.isBackground ? "canSelect" : "noSelect";
+        let cursor = svgCanvasStyle === "canSelect" ? (InkingControl.Instance.selectedTool === InkTool.Eraser ? "pointer" : "default") : undefined;
         return (
             <div className="inkingCanvas">
-                <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} />
+                <div className={`inkingCanvas-${svgCanvasStyle}`} onPointerDown={this.onPointerDown} style={{ cursor: cursor }} />
                 {this.props.children()}
                 {this.drawnPaths}
             </div >
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index bc5249acd..b8f3c2875 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -94,7 +94,7 @@ export class InkingControl {
                 redo: () => oldColors.forEach(pair => pair.target.backgroundColor = captured)
             });
         } else {
-            CurrentUserUtils.UserDocument.activePen instanceof Doc && CurrentUserUtils.UserDocument.activePen.pen instanceof Doc && (CurrentUserUtils.UserDocument.activePen.pen.backgroundColor = this._selectedColor);
+            CurrentUserUtils.ActivePen && (CurrentUserUtils.ActivePen.backgroundColor = this._selectedColor);
         }
     });
     @action
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 46fbb5910..1f8c0b18f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -61,7 +61,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     if (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)));
                     } else {
-                        this.childDocs.map(async doc => doc.layout = undefined);
+                        this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layout = undefined);
                     }
                 });
 
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index 3a66c05f4..a5b7f0181 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -40,9 +40,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewChro
     //(!)?\(\(\(doc.(\w+) && \(doc.\w+ as \w+\).includes\(\"(\w+)\"\)
 
     _templateCommand = {
-        title: "set template", script: "this.target.childLayout = this.source ? this.source[0] : undefined", params: ["target", "source"],
+        title: "set template", script: "setChildLayout(this.target, this.source && this.source.length ? this.source[0]:undefined)", params: ["target", "source"],
         initialize: emptyFunction,
-        immediate: (draggedDocs: Doc[]) => this.props.CollectionView.props.Document.childLayout = draggedDocs.length ? draggedDocs[0] : undefined
+        immediate: (draggedDocs: Doc[]) => Doc.setChildLayout(this.props.CollectionView.props.Document, draggedDocs.length ? draggedDocs[0] : undefined)
     };
     _contentCommand = {
         // title: "set content", script: "getProto(this.target).data = aliasDocs(this.source.map(async p => await p));", params: ["target", "source"],  // bcz: doesn't look like we can do async stuff in scripting...
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index e4fef0922..30554ea36 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -25,9 +25,8 @@ export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(
         this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"),
             { fireImmediately: true });
-        this._penDisposer = reaction(() => CurrentUserUtils.UserDocument.activePen instanceof Doc && CurrentUserUtils.UserDocument.activePen.pen,
-            action(() => this._startupColor = CurrentUserUtils.UserDocument.activePen instanceof Doc && CurrentUserUtils.UserDocument.activePen.pen instanceof Doc ?
-                StrCast(CurrentUserUtils.UserDocument.activePen.pen.backgroundColor, "black") : "black"),
+        this._penDisposer = reaction(() => CurrentUserUtils.ActivePen,
+            action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"),
             { fireImmediately: true });
 
         // compare to this reaction that used to be in Selection Manager
@@ -36,7 +35,7 @@ export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(
         //     if (sel.length > 0) {
         //         let firstView = sel[0];
         //         let doc = firstView.props.Document;
-        //         let targetDoc = doc.isTemplate ? doc : Doc.GetProto(doc);
+        //         let targetDoc = doc.isTemplateField ? doc : Doc.GetProto(doc);
         //         let stored = StrCast(targetDoc.backgroundColor);
         //         stored.length > 0 && (targetColor = stored);
         //     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ea82b1161..045eee3f7 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -143,7 +143,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
 
-    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
+    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
 
     // the document containing the view layout information - will be the Document itself unless the Document has
     // a layout field.  In that case, all layout information comes from there unless overriden by Document
@@ -265,7 +265,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 }
                 // apply as template when dragging with Meta
             } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") {
-                draggedDoc.isTemplate = true;
+                draggedDoc.isTemplateDoc = true;
                 let newLayout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
                 if (typeof (draggedDoc.layout) === "string") {
                     newLayout = Doc.MakeDelegate(draggedDoc);
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 51fcb818a..04f310785 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -599,7 +599,7 @@ export namespace Doc {
         return target;
     }
 
-    export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false) {
+    export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc, suppressTitle: boolean = false): boolean {
         // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
         let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
         let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
@@ -613,7 +613,7 @@ export namespace Doc {
 
         fieldTemplate.templateField = metadataFieldName;
         fieldTemplate.title = metadataFieldName;
-        fieldTemplate.isTemplate = true;
+        fieldTemplate.isTemplateField = true;
         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.
@@ -636,6 +636,7 @@ export namespace Doc {
             if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
             fieldTemplate.proto = templateDataDoc;
         }), 0);
+        return true;
     }
 
     export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
@@ -735,9 +736,18 @@ export namespace Doc {
     export function UnBrushAllDocs() {
         manager.BrushedDoc.clear();
     }
+
+    export function setChildLayout(target: Doc, source?: Doc) {
+        target.childLayout = source && source.isTemplateDoc ? source : source &&
+            source.dragFactory instanceof Doc && source.dragFactory.isTemplateDoc ? source.dragFactory :
+            source && source.layout instanceof Doc && source.layout.isTemplateDoc ? source.layout : undefined;
+    }
 }
+
+
 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 setChildLayout(target: any, source: any) { Doc.setChildLayout(target, source); });
 Scripting.addGlobal(function getAlias(doc: any) { return Doc.MakeAlias(doc); });
 Scripting.addGlobal(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); });
 Scripting.addGlobal(function copyField(field: any) { return ObjectField.MakeCopy(field); });
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 5ce707011..e6f202685 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -23,16 +23,11 @@ export class CurrentUserUtils {
     public static get MainDocId() { return this.mainDocId; }
     public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
     @computed public static get UserDocument() { return Doc.UserDoc(); }
+    @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).pen as Doc; }
 
     @observable public static GuestTarget: Doc | undefined;
     @observable public static GuestWorkspace: Doc | undefined;
 
-    private static createUserDocument(id: string): Doc {
-        let doc = new Doc(id, true);
-        doc.title = Doc.CurrentUserEmail;
-        return this.updateUserDocument(doc);// this should be the last 
-    }
-
     // a default set of note types .. not being used yet...
     static setupNoteTypes(doc: Doc) {
         return [
@@ -174,6 +169,7 @@ export class CurrentUserUtils {
     }
 
     static updateUserDocument(doc: Doc) {
+        doc.title = Doc.CurrentUserEmail;
         new InkingControl();
         (doc.optionalRightCollection === undefined) && CurrentUserUtils.setupMobileUploads(doc);
         (doc.overlays === undefined) && CurrentUserUtils.setupOverlays(doc);
@@ -216,10 +212,8 @@ export class CurrentUserUtils {
         Doc.CurrentUserEmail = email;
         await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
             if (id && id !== "guest") {
-                return DocServer.GetRefField(id).then(async field => {
-                    let userDoc = field instanceof Doc ? await this.updateUserDocument(field) : this.createUserDocument(id);
-                    runInAction(() => Doc.SetUserDoc(userDoc));
-                });
+                return DocServer.GetRefField(id).then(async field =>
+                    Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true))));
             } else {
                 throw new Error("There should be a user id! Why does Dash think there isn't one?");
             }
-- 
cgit v1.2.3-70-g09d2


From e21810a4097e724a378416135c7cc6def7ff022c Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 18 Oct 2019 11:35:22 -0400
Subject: cleaned up dictation into text notes

---
 src/client/util/DictationManager.ts          |  3 +-
 src/client/views/MainView.tsx                |  3 +-
 src/client/views/nodes/FormattedTextBox.scss | 16 ++++++----
 src/client/views/nodes/FormattedTextBox.tsx  | 46 +++++++++++++++++++++++-----
 4 files changed, 53 insertions(+), 15 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 182cfb70a..ae991635f 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -64,7 +64,7 @@ export namespace DictationManager {
         const intraSession = ". ";
         const interSession = " ... ";
 
-        let isListening = false;
+        export let isListening = false;
         let isManuallyStopped = false;
 
         let current: string | undefined = undefined;
@@ -200,6 +200,7 @@ export namespace DictationManager {
             if (!isListening || !recognizer) {
                 return;
             }
+            isListening = false;
             isManuallyStopped = true;
             salvageSession ? recognizer.stop() : recognizer.abort();
             // let main = MainView.Instance;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 0e15784f7..9304f4bef 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,7 +1,7 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import {
     faArrowDown, faArrowUp, faBolt, faCaretUp, faCat, faCheck, faChevronRight, faClone, faCloudUploadAlt, faCommentAlt, faCut, faEllipsisV, faExclamation, faFilePdf, faFilm, faFont, faGlobeAsia, faLongArrowAltRight,
-    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter
+    faMusic, faObjectGroup, faPause, faMousePointer, faPenNib, faFileAudio, faPen, faEraser, faPlay, faPortrait, faRedoAlt, faThumbtack, faTree, faTv, faUndoAlt, faHighlighter
 } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
@@ -106,6 +106,7 @@ export class MainView extends React.Component {
         library.add(faPen);
         library.add(faHighlighter);
         library.add(faEraser);
+        library.add(faFileAudio);
         library.add(faPenNib);
         library.add(faFilm);
         library.add(faMusic);
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index 0550f9708..a4acd3b82 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -10,8 +10,7 @@
     outline: none !important;
 }
 
-.formattedTextBox-cont-scroll,
-.formattedTextBox-cont-hidden {
+.formattedTextBox-cont {
     cursor: text;
     background: inherit;
     padding: 0;
@@ -26,10 +25,15 @@
     color: initial;
     height: 100%;
     pointer-events: all;
-}
-
-.formattedTextBox-cont-hidden {
-   // pointer-events: none;
+    overflow-y: auto;
+    max-height: 100%;
+    .formattedTextBox-dictation {
+        height: 20px;
+        width: 20px;
+        top: 0px;
+        left: 0px;
+        position: absolute;
+    }
 }
 
 .formattedTextBox-inner-rounded {
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 045eee3f7..6f4ced7f3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,7 +1,7 @@
 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 { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
@@ -43,6 +43,8 @@ import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './Format
 import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { siteVerification } from 'googleapis/build/src/apis/siteVerification';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -369,7 +371,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     specificContextMenu = (e: React.MouseEvent): void => {
         let funcs: ContextMenuProps[] = [];
-        funcs.push({ description: "Dictate", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
+        funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" });
         ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
             funcs.push({
                 description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
@@ -386,6 +388,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
     }
 
+    @observable _recording = false;
+
+    recordDictation = () => {
+        //this._editorView!.focus();
+        if (this._recording) return;
+        runInAction(() => this._recording = true);
+        DictationManager.Controls.listen({
+            interimHandler: this.setCurrentBulletContent,
+            continuous: { indefinite: false },
+        }).then(results => {
+            if (results && [DictationManager.Controls.Infringed].includes(results)) {
+                DictationManager.Controls.stop();
+            }
+            this._editorView!.focus();
+        });
+    }
+    stopDictation = (abort: boolean) => {
+        runInAction(() => this._recording = false);
+        DictationManager.Controls.stop(!abort);
+    }
+
     recordBullet = async () => {
         let completedCue = "end session";
         let results = await DictationManager.Controls.listen({
@@ -902,6 +925,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
 
         this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
+        if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
     }
 
     // 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.
@@ -965,7 +989,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         });
     }
     onBlur = (e: any) => {
-        DictationManager.Controls.stop(false);
+        //DictationManager.Controls.stop(false);
         if (this._undoTyping) {
             this._undoTyping.end();
             this._undoTyping = undefined;
@@ -986,6 +1010,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (!this._undoTyping) {
             this._undoTyping = UndoManager.StartBatch("undoTyping");
         }
+        if (this._recording) { this.stopDictation(true); setTimeout(() => this.recordDictation(), 250); }
     }
 
     @action
@@ -1001,7 +1026,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
     }
 
     render() {
-        let style = "hidden";
         let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
         let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground
             ? "none" : "all";
@@ -1010,10 +1034,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
         }
         return (
-            <div className={`formattedTextBox-cont-${style}`} ref={this._ref}
+            <div className={`formattedTextBox-cont`} ref={this._ref}
                 style={{
-                    overflowY: "auto",
-                    maxHeight: "100%",
                     height: this.layoutDoc.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
                     background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
                     opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
@@ -1035,6 +1057,16 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 onPointerLeave={action(() => this._entered = false)}
             >
                 <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 className="formattedTextBox-dictation"
+                    onClick={e => {
+                        this._recording ? this.stopDictation(true) : this.recordDictation();
+                        setTimeout(() => this._editorView!.focus(), 500);
+                        e.stopPropagation();
+                    }} >
+                    <FontAwesomeIcon className="formattedTExtBox-audioFont"
+                        style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"file-audio"} size="sm" />
+                </div>
             </div>
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 3d910bae8771e67cabc1500c49b77d425fdf62e9 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 19 Oct 2019 13:49:20 -0400
Subject: cleaned up a bunch layout vs doc issues related to templates

---
 src/client/views/DocumentDecorations.tsx           |  6 +-
 src/client/views/InkingControl.tsx                 |  2 +-
 src/client/views/TemplateMenu.tsx                  |  4 +-
 .../views/collections/CollectionStackingView.tsx   |  8 +--
 .../views/collections/CollectionTreeView.tsx       |  6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 66 +++++++++++-----------
 .../collections/collectionFreeForm/MarqueeView.tsx | 24 ++++----
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 14 ++---
 src/client/views/nodes/DocumentContentsView.tsx    |  6 +-
 src/client/views/nodes/DocumentView.tsx            | 11 ++--
 src/client/views/nodes/FontIconBox.tsx             |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  6 +-
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 src/new_fields/Doc.ts                              | 24 +++++---
 14 files changed, 86 insertions(+), 95 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8409a34da..07af4799b 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -470,7 +470,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         SelectionManager.SelectedDocuments().forEach(element => {
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 let doc = PositionDocument(element.props.Document);
-                let layoutDoc = PositionDocument(element.props.Document.layout instanceof Doc ? element.props.Document.layout : element.props.Document);
+                let layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
                 let nwidth = doc.nativeWidth || 0;
                 let nheight = doc.nativeHeight || 0;
                 let width = (layoutDoc.width || 0);
@@ -478,8 +478,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
                 let actualdW = Math.max(width + (dW * scale), 20);
                 let actualdH = Math.max(height + (dH * scale), 20);
-                layoutDoc.x = (layoutDoc.x || 0) + dX * (actualdW - width);
-                layoutDoc.y = (layoutDoc.y || 0) + dY * (actualdH - height);
+                doc.x = (doc.x || 0) + dX * (actualdW - width);
+                doc.y = (doc.y || 0) + dY * (actualdH - height);
                 let proto = doc.isTemplateField ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
                 let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
                 if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index c3a617ffe..105adc03d 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -81,7 +81,7 @@ export class InkingControl {
                     ruleProvider = (view.props.Document.heading && ruleProvider) ? ruleProvider : undefined;
                     ruleProvider && ((Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb)));
                 }
-                (!ruleProvider && targetDoc) && (view.props.Document.backgroundColor = matchedColor);
+                (!ruleProvider && targetDoc) && (Doc.Layout(view.props.Document).backgroundColor = matchedColor);
 
                 return {
                     target: targetDoc,
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index da776f887..d76b033f0 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -117,13 +117,13 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     @action
     toggleChrome = (): void => {
         this.props.docs.map(dv => {
-            let layout = dv.Document.layout instanceof Doc ? dv.Document.layout : dv.Document;
+            let layout = Doc.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 layout = Doc.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} />));
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index f9f040c6b..1a578f4fc 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -74,11 +74,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         this._heightMap.set(key, sectionHeight);
     }
 
-    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;
-    }
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
 
     get Sections() {
         if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
@@ -189,7 +185,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
     getDocHeight(d?: Doc) {
         if (!d) return 0;
-        let layoutDoc = d.layout instanceof Doc ? d.layout : d;
+        let layoutDoc = Doc.Layout(d);
         let nw = NumCast(d.nativeWidth);
         let nh = NumCast(d.nativeHeight);
         let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 5d88e6290..2fbe8527e 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -251,7 +251,7 @@ class TreeView extends React.Component<TreeViewProps> {
     }
     docWidth = () => {
         let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
-        let layoutDoc = this.props.document.layout instanceof Doc ? this.props.document.layout : this.props.document;
+        let layoutDoc = Doc.Layout(this.props.document);
         if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
         return NumCast(this.props.document.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
     }
@@ -259,7 +259,7 @@ class TreeView extends React.Component<TreeViewProps> {
         let bounds = this.boundsOfCollectionDocument;
         return Math.min(this.MAX_EMBED_HEIGHT, (() => {
             let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
-            let layoutDoc = this.props.document.layout instanceof Doc ? this.props.document.layout : this.props.document;
+            let layoutDoc = Doc.Layout(this.props.document);
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
             return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
@@ -467,7 +467,6 @@ class TreeView extends React.Component<TreeViewProps> {
             if (!pair.layout || pair.data instanceof Promise) {
                 return (null);
             }
-            const childLayout = pair.layout.layout instanceof Doc ? pair.layout.layout : pair.layout;
 
             let indent = i === 0 ? undefined : () => {
                 if (StrCast(docs[i - 1].layout).indexOf("fieldKey") !== -1) {
@@ -483,6 +482,7 @@ class TreeView extends React.Component<TreeViewProps> {
             let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
                 return add(doc, relativeTo ? relativeTo : docs[i], before !== undefined ? before : false);
             };
+            const childLayout = Doc.Layout(pair.layout);
             let rowHeight = () => {
                 let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
                 return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 932b6722b..eff73b14e 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -130,22 +130,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         if (super.drop(e, de)) {
             if (de.data instanceof DragManager.DocumentDragData) {
                 if (de.data.droppedDocuments.length) {
-                    let firstLayoutDoc = de.data.droppedDocuments[0].layout instanceof Doc ? de.data.droppedDocuments[0].layout : de.data.droppedDocuments[0];
-                    let z = NumCast(firstLayoutDoc.z);
+                    let firstDoc = de.data.droppedDocuments[0];
+                    let z = NumCast(firstDoc.z);
                     let x = (z ? xpo : xp) - de.data.offset[0];
                     let y = (z ? ypo : yp) - de.data.offset[1];
-                    let dropX = NumCast(firstLayoutDoc.x);
-                    let dropY = NumCast(firstLayoutDoc.y);
+                    let dropX = NumCast(firstDoc.x);
+                    let dropY = NumCast(firstDoc.y);
                     de.data.droppedDocuments.forEach(action((d: Doc) => {
-                        let layoutDoc = d.layout instanceof Doc ? d.layout : d;
-                        layoutDoc.x = x + NumCast(layoutDoc.x) - dropX;
-                        layoutDoc.y = y + NumCast(layoutDoc.y) - dropY;
+                        let layoutDoc = Doc.Layout(d);
+                        d.x = x + NumCast(d.x) - dropX;
+                        d.y = y + NumCast(d.y) - dropY;
                         if (!NumCast(layoutDoc.width)) {
                             layoutDoc.width = 300;
                         }
-                        if (!NumCast(d.height)) {
-                            let nw = NumCast(d.nativeWidth);
-                            let nh = NumCast(d.nativeHeight);
+                        if (!NumCast(layoutDoc.height)) {
+                            let nw = NumCast(layoutDoc.nativeWidth);
+                            let nh = NumCast(layoutDoc.nativeHeight);
                             layoutDoc.height = nw && nh ? nh / nw * NumCast(layoutDoc.width) : 300;
                         }
                         this.bringToFront(d);
@@ -157,13 +157,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             else if (de.data instanceof DragManager.AnnotationDragData) {
                 if (de.data.dropDocument) {
                     let dragDoc = de.data.dropDocument;
-                    let layoutDoc = dragDoc.layout instanceof Doc ? dragDoc.layout : dragDoc;
                     let x = xp - de.data.offset[0];
                     let y = yp - de.data.offset[1];
-                    let dropX = NumCast(layoutDoc.x);
-                    let dropY = NumCast(layoutDoc.y);
-                    layoutDoc.x = x + NumCast(layoutDoc.x) - dropX;
-                    layoutDoc.y = y + NumCast(layoutDoc.y) - dropY;
+                    let dropX = NumCast(dragDoc.x);
+                    let dropY = NumCast(dragDoc.y);
+                    dragDoc.x = x + NumCast(dragDoc.x) - dropX;
+                    dragDoc.y = y + NumCast(dragDoc.y) - dropY;
                     de.data.targetContext = this.props.Document; // dropped a PDF annotation, so we need to set the targetContext on the dragData which the PDF view uses at the end of the drop operation
                     this.bringToFront(dragDoc);
                 }
@@ -174,9 +173,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     pickCluster(probe: number[]) {
         return this.childLayoutPairs.map(pair => pair.layout).reduce((cluster, cd) => {
-            let layoutDoc = cd.layout instanceof Doc ? cd.layout : cd;
-            let cx = NumCast(layoutDoc.x) - this._clusterDistance;
-            let cy = NumCast(layoutDoc.y) - this._clusterDistance;
+            let layoutDoc = Doc.Layout(cd);
+            let cx = NumCast(cd.x) - this._clusterDistance;
+            let cy = NumCast(cd.y) - this._clusterDistance;
             let cw = NumCast(layoutDoc.width) + 2 * this._clusterDistance;
             let ch = NumCast(layoutDoc.height) + 2 * this._clusterDistance;
             return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ?
@@ -218,10 +217,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
             let preferredInd = NumCast(doc.cluster);
             doc.cluster = -1;
-            let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
             this._clusterSets.map((set, i) => set.map(member => {
-                let memberLayout = member.layout instanceof Doc ? member.layout : member;
-                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(layoutDoc, memberLayout, this._clusterDistance)) {
+                if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) {
                     doc.cluster = i;
                 }
             }));
@@ -293,18 +290,19 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
             let x = this.Document.panX || 0;
             let y = this.Document.panY || 0;
-            let docs = this.childLayoutPairs.map(pair => pair.layout.layout instanceof Doc ? pair.layout.layout : pair.layout);
+            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) {
                 let minx = docs.length ? NumCast(docs[0].x) : 0;
-                let maxx = docs.length ? NumCast(docs[0].width) + minx : minx;
+                let maxx = docs.length ? NumCast(Doc.Layout(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 maxy = docs.length ? NumCast(Doc.Layout(docs[0]).height) + miny : miny;
                 let ranges = docs.filter(doc => doc).reduce((range, doc) => {
+                    let layoutDoc = Doc.Layout(doc);
                     let x = NumCast(doc.x);
-                    let xe = x + NumCast(doc.width);
+                    let xe = x + NumCast(layoutDoc.width);
                     let y = NumCast(doc.y);
-                    let ye = y + NumCast(doc.height);
+                    let ye = y + NumCast(layoutDoc.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]]);
@@ -408,9 +406,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                 this.props.Document.scrollY = NumCast(doc.y) - offset;
             }
         } else {
-            let layoutdoc = doc.layout instanceof Doc ? doc.layout : doc;
-            const newPanX = NumCast(layoutdoc.x) + NumCast(layoutdoc.width) / 2;
-            const newPanY = NumCast(layoutdoc.y) + NumCast(layoutdoc.height) / 2;
+            let layoutdoc = Doc.Layout(doc);
+            const newPanX = NumCast(doc.x) + NumCast(layoutdoc.width) / 2;
+            const newPanY = NumCast(doc.y) + NumCast(layoutdoc.height) / 2;
             const newState = HistoryUtil.getState();
             newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
             HistoryUtil.pushState(newState);
@@ -471,10 +469,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     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);
+        const layoutDoc = Doc.Layout(params.doc);
         if (result && result.success) {
             return { ...result, transition: "transform 1s" };
         }
-        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") };
+        return { x: Cast(params.doc.x, "number"), y: Cast(params.doc.y, "number"), z: Cast(params.doc.z, "number"), width: Cast(layoutDoc.width, "number"), height: Cast(layoutDoc.height, "number") };
     }
 
     viewDefsToJSX = (views: any[]) => {
@@ -530,8 +529,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let elements = initResult && initResult.success ? this.viewDefsToJSX(initResult.result.views) : [];
 
         this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => {
-            let finalLayout = pair.layout.layout instanceof Doc ? pair.layout.layout : pair.layout;
-            const pos = this.getCalculatedPositions({ doc: finalLayout, index: i, collection: this.Document, docs: layoutDocs, state });
+            const pos = this.getCalculatedPositions({ doc: pair.layout, index: i, collection: this.Document, docs: layoutDocs, state });
             state = pos.state === undefined ? state : pos.state;
             layoutPoolData.set(pair, pos);
         });
@@ -574,7 +572,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             let i = 0;
             const width = Math.max(...docs.map(doc => NumCast(doc.width)));
             const height = Math.max(...docs.map(doc => NumCast(doc.height)));
-            for (const doc of docs.map(d => d.layout instanceof Doc ? d.layout : d)) {
+            for (const doc of docs) {
                 doc.x = x;
                 doc.y = y;
                 x += width + 20;
@@ -592,7 +590,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // 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 => {
+            DocListCast(Doc.Layout(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) {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 1362736cf..637168f1b 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -281,7 +281,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             let bounds = this.Bounds;
             let selected = this.marqueeSelect(false);
             if (e.key === "c") {
-                selected.map(d => d.layout instanceof Doc ? d.layout : d).map(d => {
+                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;
@@ -296,7 +296,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.layout instanceof Doc ? child.layout.backgroundColor : child.backgroundColor);
+                let bg = StrCast(Doc.Layout(child).backgroundColor);
                 if (palette.indexOf(bg) !== -1) {
                     palette.splice(palette.indexOf(bg), 1);
                     if (usedPaletted.get(bg)) usedPaletted.set(bg, usedPaletted.get(bg)! + 1);
@@ -325,7 +325,7 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             this.marqueeInkDelete(inkData);
 
             if (e.key === "s" || e.key === "S") {
-                selected.map(d => d.layout instanceof Doc ? d.layout : d).map(d => {
+                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;
@@ -394,9 +394,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         let selRect = this.Bounds;
         let selection: Doc[] = [];
         this.props.activeDocuments().filter(doc => !doc.isBackground && doc.z === undefined).map(doc => {
-            let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
-            var x = NumCast(layoutDoc.x);
-            var y = NumCast(layoutDoc.y);
+            let layoutDoc = Doc.Layout(doc);
+            var x = NumCast(doc.x);
+            var y = NumCast(doc.y);
             var w = NumCast(layoutDoc.width);
             var h = NumCast(layoutDoc.height);
             if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
@@ -405,9 +405,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         });
         if (!selection.length && selectBackgrounds) {
             this.props.activeDocuments().filter(doc => doc.z === undefined).map(doc => {
-                let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
-                var x = NumCast(layoutDoc.x);
-                var y = NumCast(layoutDoc.y);
+                let layoutDoc = Doc.Layout(doc);
+                var x = NumCast(doc.x);
+                var y = NumCast(doc.y);
                 var w = NumCast(layoutDoc.width);
                 var h = NumCast(layoutDoc.height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, selRect)) {
@@ -422,9 +422,9 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             let size = this.props.getContainerTransform().transformDirection(this._lastX - this._downX, this._lastY - this._downY);
             let otherBounds = { left: topLeft[0], top: topLeft[1], width: Math.abs(size[0]), height: Math.abs(size[1]) };
             this.props.activeDocuments().filter(doc => doc.z !== undefined).map(doc => {
-                let layoutDoc = doc.layout instanceof Doc ? doc.layout : doc;
-                var x = NumCast(layoutDoc.x);
-                var y = NumCast(layoutDoc.y);
+                let layoutDoc = Doc.Layout(doc);
+                var x = NumCast(doc.x);
+                var y = NumCast(doc.y);
                 var w = NumCast(layoutDoc.width);
                 var h = NumCast(layoutDoc.height);
                 if (this.intersectRect({ left: x, top: y, width: w, height: h }, otherBounds)) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 8153923de..2243a44d5 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -34,13 +34,13 @@ export const PositionDocument = makeInterface(documentSchema, positionSchema);
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
     _disposer: IReactionDisposer | undefined = undefined;
     get transform() { return `scale(${this.props.ContentScaling()}) translate(${this.X}px, ${this.Y}px) rotate(${random(-1, 1) * this.props.jitterRotation}deg)`; }
-    get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : NumCast(this.layoutDoc.x); }
-    get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : NumCast(this.layoutDoc.y); }
+    get X() { return this._animPos !== undefined ? this._animPos[0] : this.renderScriptDim ? this.renderScriptDim.x : this.props.x !== undefined ? this.props.x : this.dataProvider ? this.dataProvider.x : (this.Document.x || 0); }
+    get Y() { return this._animPos !== undefined ? this._animPos[1] : this.renderScriptDim ? this.renderScriptDim.y : this.props.y !== undefined ? this.props.y : this.dataProvider ? this.dataProvider.y : (this.Document.y || 0); }
     get width() { return this.renderScriptDim ? this.renderScriptDim.width : this.props.width !== undefined ? this.props.width : this.props.dataProvider && this.dataProvider ? this.dataProvider.width : this.layoutDoc[WidthSym](); }
     get height() { return this.renderScriptDim ? this.renderScriptDim.height : this.props.height !== undefined ? this.props.height : this.props.dataProvider && this.dataProvider ? this.dataProvider.height : this.layoutDoc[HeightSym](); }
     @computed get dataProvider() { return this.props.dataProvider && this.props.dataProvider(this.props.Document, this.props.DataDoc) ? this.props.dataProvider(this.props.Document, this.props.DataDoc) : undefined; }
-    @computed get nativeWidth() { return FieldValue(this.Document.nativeWidth, 0); }
-    @computed get nativeHeight() { return FieldValue(this.Document.nativeHeight, 0); }
+    @computed get nativeWidth() { return NumCast(this.layoutDoc.nativeWidth); }
+    @computed get nativeHeight() { return NumCast(this.layoutDoc.nativeHeight); }
 
     @computed get renderScriptDim() {
         if (this.Document.renderScript) {
@@ -92,11 +92,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     clusterColorFunc = (doc: Doc) => this.clusterColor;
 
-    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;
-    }
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
 
     @observable _animPos: number[] | undefined = undefined;
 
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index d375405b9..bb9315ca3 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -77,11 +77,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         }
         return this.props.DataDoc;
     }
-    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;
-    }
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
 
     CreateBindings(): JsxBindings {
         let list = {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 26e2c0fa2..1087c57ad 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -144,8 +144,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     public get ContentDiv() { return this._mainCont.current; }
     @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 nativeWidth() { return this.layoutDoc.nativeWidth || 0; }
+    @computed get nativeHeight() { return this.layoutDoc.nativeHeight || 0; }
     @computed get onClickHandler() { return this.props.onClick ? this.props.onClick : this.Document.onClick; }
 
     @action
@@ -287,8 +287,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
 
-    @undoBatch
-    static makeNativeViewClicked = async (doc: Doc): Promise<void> => swapViews(doc, "layoutNative", "layoutCustom")
+    static makeNativeViewClicked = async (doc: Doc): Promise<void> => {
+        undoBatch(() => swapViews(doc, "layoutNative", "layoutCustom"))();
+    }
 
     static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => {
         const batch = UndoManager.StartBatch("CustomViewClicked");
@@ -580,7 +581,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     // 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);
+        return Document(Doc.Layout(this.props.Document));
     }
 
     // does Document set a layout prop 
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index c5bf28d5b..fd6a475fb 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -37,7 +37,7 @@ export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(
     }
     render() {
         let referenceDoc = (this.props.Document.dragFactory instanceof Doc ? this.props.Document.dragFactory : this.props.Document);
-        let referenceLayout = referenceDoc.layout instanceof Doc ? referenceDoc.layout : referenceDoc;
+        let referenceLayout = Doc.Layout(referenceDoc);
         return <button className="fontIconBox-outerDiv" title={StrCast(this.props.Document.title)} ref={this._ref}
             style={{ background: StrCast(referenceLayout.backgroundColor), boxShadow: this.props.Document.unchecked ? undefined : `4px 4px 12px black` }}>
             <FontAwesomeIcon className="fontIconBox-icon" icon={this.Document.icon as any} color={this._foregroundColor} size="sm" />
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6f4ced7f3..ac10d729a 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -149,9 +149,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     // the document containing the view layout information - will be the Document itself unless the Document has
     // a layout field.  In that case, all layout information comes from there unless overriden by Document
-    @computed get layoutDoc(): Doc {
-        return this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
-    }
+    @computed get layoutDoc(): Doc { return Doc.Layout(this.props.Document); }
 
     linkOnDeselect: Map<string, string> = new Map();
 
@@ -268,7 +266,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 // apply as template when dragging with Meta
             } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") {
                 draggedDoc.isTemplateDoc = true;
-                let newLayout = draggedDoc.layout instanceof Doc ? draggedDoc.layout : draggedDoc;
+                let newLayout = Doc.Layout(draggedDoc);
                 if (typeof (draggedDoc.layout) === "string") {
                     newLayout = Doc.MakeDelegate(draggedDoc);
                     newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 3a9318469..76cf60188 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -68,7 +68,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
 
     public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript): boolean {
         const { script, type, onDelegate } = kvpScript;
-        //const target = onDelegate ? (doc.layout instanceof Doc ? doc.layout : doc) : Doc.GetProto(doc); // bcz: need to be able to set fields on layout templates
+        //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates
         const target = onDelegate ? doc : Doc.GetProto(doc);
         let field: Field;
         if (type === "computed") {
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 69c048ebf..31173a5c1 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -624,7 +624,7 @@ export namespace Doc {
         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;
+            let layoutDelegate = Doc.Layout(fieldTemplate);
             layoutDelegate.layout = layout;
             fieldTemplate.layout = layoutDelegate !== fieldTemplate ? layoutDelegate : layout;
             if (fieldTemplate.backgroundColor !== templateDataDoc.defaultBackgroundColor) fieldTemplate.defaultBackgroundColor = fieldTemplate.backgroundColor;
@@ -633,16 +633,18 @@ export namespace Doc {
         return true;
     }
 
-    export function overlapping(doc: Doc, doc2: Doc, clusterDistance: number) {
+    export function overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
+        let doc2Layout = Doc.Layout(doc2);
+        let doc1Layout = Doc.Layout(doc1);
         var x2 = NumCast(doc2.x) - clusterDistance;
         var y2 = NumCast(doc2.y) - clusterDistance;
-        var w2 = NumCast(doc2.width) + clusterDistance;
-        var h2 = NumCast(doc2.height) + clusterDistance;
-        var x = NumCast(doc.x) - clusterDistance;
-        var y = NumCast(doc.y) - clusterDistance;
-        var w = NumCast(doc.width) + clusterDistance;
-        var h = NumCast(doc.height) + clusterDistance;
-        return doc.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
+        var w2 = NumCast(doc2Layout.width) + clusterDistance;
+        var h2 = NumCast(doc2Layout.height) + clusterDistance;
+        var x = NumCast(doc1.x) - clusterDistance;
+        var y = NumCast(doc1.y) - clusterDistance;
+        var w = NumCast(doc1Layout.width) + clusterDistance;
+        var h = NumCast(doc1Layout.height) + clusterDistance;
+        return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
     }
 
     export function isBrushedHighlightedDegree(doc: Doc) {
@@ -663,6 +665,10 @@ export namespace Doc {
         @observable _user_doc: Doc = undefined!;
         @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
     }
+
+    // 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.
+    export function Layout(doc: Doc) { return doc.layout instanceof Doc ? doc.layout : doc; }
     const manager = new DocData();
     export function UserDoc(): Doc { return manager._user_doc; }
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
-- 
cgit v1.2.3-70-g09d2


From d8f12dade0d0b336e4328fa1f00a1a538588d34e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sun, 20 Oct 2019 13:34:25 -0400
Subject: initial version of template refactoring

---
 src/client/documents/Documents.ts                  | 10 +--
 src/client/views/DocumentDecorations.tsx           | 26 ++++----
 src/client/views/TemplateMenu.tsx                  |  3 +-
 .../views/collections/CollectionDockingView.tsx    | 23 +++----
 .../views/collections/CollectionSchemaView.tsx     | 13 ++--
 .../views/collections/CollectionStackingView.tsx   | 37 +++++++----
 src/client/views/collections/CollectionSubView.tsx | 11 ++--
 .../views/collections/CollectionTreeView.tsx       | 19 +++---
 .../CollectionFreeFormLayoutEngines.tsx            |  3 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  2 +-
 src/client/views/nodes/DocumentContentsView.tsx    | 11 ++--
 src/client/views/nodes/DocumentView.tsx            | 77 +++++++---------------
 src/client/views/nodes/FormattedTextBox.tsx        |  3 +-
 src/client/views/nodes/ImageBox.tsx                | 52 ++++++++-------
 src/client/views/nodes/PDFBox.tsx                  | 10 +--
 src/client/views/nodes/VideoBox.tsx                | 35 +++++-----
 src/client/views/nodes/WebBox.tsx                  | 11 ++--
 src/client/views/pdf/PDFViewer.tsx                 | 40 +++++------
 .../views/presentationview/PresElementBox.tsx      |  9 +--
 src/client/views/search/SearchBox.tsx              |  2 +-
 src/client/views/search/SearchItem.tsx             |  2 +-
 src/new_fields/Doc.ts                              | 37 +++++------
 .../authentication/models/current_user_utils.ts    |  2 +-
 24 files changed, 211 insertions(+), 231 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 32fda1954..fbf45816a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -300,7 +300,7 @@ export namespace Docs {
      */
     export namespace Create {
 
-        const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "dropAction", "forceActive"];
+        const delegateKeys = ["x", "y", "width", "height", "panX", "panY", "nativeWidth", "nativeHeight", "dropAction", "forceActive", "fitWidth"];
 
         /**
          * This function receives the relevant document prototype and uses
@@ -369,11 +369,11 @@ export namespace Docs {
             requestImageSize(target)
                 .then((size: any) => {
                     let aspect = size.height / size.width;
-                    if (!inst.proto!.nativeWidth) {
-                        inst.proto!.nativeWidth = size.width;
+                    if (!inst.nativeWidth) {
+                        inst.nativeWidth = size.width;
                     }
-                    inst.proto!.nativeHeight = Number(inst.proto!.nativeWidth!) * aspect;
-                    inst.proto!.height = NumCast(inst.proto!.width) * aspect;
+                    inst.nativeHeight = NumCast(inst.nativeWidth) * aspect;
+                    inst.height = NumCast(inst.width) * aspect;
                 })
                 .catch((err: any) => console.log(err));
             // }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 07af4799b..252f90d46 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -19,7 +19,7 @@ import { CollectionView } from "./collections/CollectionView";
 import { DocumentButtonBar } from './DocumentButtonBar';
 import './DocumentDecorations.scss';
 import { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
-import { DocumentView, swapViews } from "./nodes/DocumentView";
+import { DocumentView } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
 import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { IconBox } from "./nodes/IconBox";
@@ -107,10 +107,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     fieldTemplate.title = metaKey;
                     Doc.MakeMetadataFieldTemplate(fieldTemplate, Doc.GetProto(docTemplate));
                     if (text.startsWith(">>")) {
-                        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}"}`);
+                        Doc.GetProto(docTemplate).layout = StrCast(fieldTemplateView.props.Document.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${metaKey}"}`);
                     }
                 }
             }
@@ -471,8 +468,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             if (dX !== 0 || dY !== 0 || dW !== 0 || dH !== 0) {
                 let doc = PositionDocument(element.props.Document);
                 let layoutDoc = PositionDocument(Doc.Layout(element.props.Document));
-                let nwidth = doc.nativeWidth || 0;
-                let nheight = doc.nativeHeight || 0;
+                let nwidth = layoutDoc.nativeWidth || 0;
+                let nheight = layoutDoc.nativeHeight || 0;
                 let width = (layoutDoc.width || 0);
                 let height = (layoutDoc.height || (nheight / nwidth * width));
                 let scale = element.props.ScreenToLocalTransform().Scale * element.props.ContentScaling();
@@ -480,21 +477,20 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 let actualdH = Math.max(height + (dH * scale), 20);
                 doc.x = (doc.x || 0) + dX * (actualdW - width);
                 doc.y = (doc.y || 0) + dY * (actualdH - height);
-                let proto = doc.isTemplateField ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
                 let fixedAspect = e.ctrlKey || (!layoutDoc.ignoreAspect && nwidth && nheight);
                 if (fixedAspect && e.ctrlKey && layoutDoc.ignoreAspect) {
                     layoutDoc.ignoreAspect = false;
-                    proto.nativeWidth = nwidth = layoutDoc.width || 0;
-                    proto.nativeHeight = nheight = layoutDoc.height || 0;
+                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
                 }
                 if (fixedAspect && (!nwidth || !nheight)) {
-                    proto.nativeWidth = nwidth = layoutDoc.width || 0;
-                    proto.nativeHeight = nheight = layoutDoc.height || 0;
+                    layoutDoc.nativeWidth = nwidth = layoutDoc.width || 0;
+                    layoutDoc.nativeHeight = nheight = layoutDoc.height || 0;
                 }
                 if (nwidth > 0 && nheight > 0 && !layoutDoc.ignoreAspect) {
                     if (Math.abs(dW) > Math.abs(dH)) {
                         if (!fixedAspect) {
-                            Doc.SetInPlace(doc, "nativeWidth", actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0), true);
+                            layoutDoc.nativeWidth = actualdW / (layoutDoc.width || 1) * (layoutDoc.nativeWidth || 0);
                         }
                         layoutDoc.width = actualdW;
                         if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.height = nheight / nwidth * layoutDoc.width;
@@ -502,7 +498,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     }
                     else {
                         if (!fixedAspect) {
-                            Doc.SetInPlace(doc, "nativeHeight", actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0), true);
+                            layoutDoc.nativeHeight = actualdH / (layoutDoc.height || 1) * (doc.nativeHeight || 0);
                         }
                         layoutDoc.height = actualdH;
                         if (fixedAspect && !layoutDoc.fitWidth) layoutDoc.width = nwidth / nheight * layoutDoc.height;
@@ -511,7 +507,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 } else {
                     dW && (layoutDoc.width = actualdW);
                     dH && (layoutDoc.height = actualdH);
-                    dH && layoutDoc.autoHeight && Doc.SetInPlace(layoutDoc, "autoHeight", false, true);
+                    dH && layoutDoc.autoHeight && (layoutDoc.autoHeight = false);
                 }
             }
         });
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index d76b033f0..96265385e 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -9,6 +9,7 @@ import { DocumentView } from "./nodes/DocumentView";
 import { Template, Templates } from "./Templates";
 import React = require("react");
 import { Doc } from "../../new_fields/Doc";
+import { StrCast } from "../../new_fields/Types";
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -128,7 +129,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
         this.props.templates.forEach((checked, template) =>
             templateMenu.push(<TemplateToggle key={template.Name} template={template} checked={checked} toggle={this.toggleTemplate} />));
         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={"custom"} name={"Custom"} checked={StrCast(this.props.docs[0].Document.layoutKey, "layout") !== "layout"} toggle={this.toggleCustom} />);
         templateMenu.push(<OtherToggle key={"chrome"} name={"Chrome"} checked={layout.chromeStatus !== "disabled"} toggle={this.toggleChrome} />);
         return (
             <div className="templating-menu" >
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 1f78c8c97..42d372f4a 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -615,19 +615,20 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
     }
 
-    panelWidth = () => this._document && this._document.maxWidth ? Math.min(Math.max(NumCast(this._document.width), NumCast(this._document.nativeWidth)), this._panelWidth) : this._panelWidth;
+    get layoutDoc() { return this._document && Doc.Layout(this._document);}
+    panelWidth = () => this.layoutDoc && this.layoutDoc.maxWidth ? Math.min(Math.max(NumCast(this.layoutDoc.width), NumCast(this.layoutDoc.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;
+    nativeWidth = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeWidth) || this._panelWidth : 0;
+    nativeHeight = () => !this.layoutDoc!.ignoreAspect && !this.layoutDoc!.fitWidth ? NumCast(this.layoutDoc!.nativeHeight) || this._panelHeight : 0;
 
     contentScaling = () => {
-        if (this._document!.type === DocumentType.PDF) {
-            if ((this._document && this._document.fitWidth) ||
-                this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) {
-                return this._panelWidth / NumCast(this._document!.nativeWidth);
+        if (this.layoutDoc!.type === DocumentType.PDF) {
+            if ((this.layoutDoc && this.layoutDoc.fitWidth) ||
+                this._panelHeight / NumCast(this.layoutDoc!.nativeHeight) > this._panelWidth / NumCast(this.layoutDoc!.nativeWidth)) {
+                return this._panelWidth / NumCast(this.layoutDoc!.nativeWidth);
             } else {
-                return this._panelHeight / NumCast(this._document!.nativeHeight);
+                return this._panelHeight / NumCast(this.layoutDoc!.nativeHeight);
             }
         }
         const nativeH = this.nativeHeight();
@@ -645,7 +646,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
         return Transform.Identity();
     }
-    get previewPanelCenteringOffset() { return this.nativeWidth() && !this._document!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
+    get previewPanelCenteringOffset() { return this.nativeWidth() && !this.layoutDoc!.ignoreAspect ? (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2 : 0; }
 
     addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
         SelectionManager.DeselectAll();
@@ -690,11 +691,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     render() {
-        return (!this._isActive || !this._document) ? (null) :
+        return (!this._isActive || !this.layoutDoc) ? (null) :
             (<div className="collectionDockingView-content" ref={ref => this._mainCont = ref}
                 style={{
                     transform: `translate(${this.previewPanelCenteringOffset}px, 0px)`,
-                    height: this._document && this._document.fitWidth ? undefined : "100%"
+                    height: this.layoutDoc && this.layoutDoc.fitWidth ? undefined : "100%"
                 }}>
                 {this.docView}
             </div >);
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 3218f630a..23e070750 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -922,8 +922,9 @@ interface CollectionSchemaPreviewProps {
 export class CollectionSchemaPreview extends React.Component<CollectionSchemaPreviewProps>{
     private dropDisposer?: DragManager.DragDropDisposer;
     _mainCont?: HTMLDivElement;
-    private get nativeWidth() { return NumCast(this.props.Document!.nativeWidth, this.props.PanelWidth()); }
-    private get nativeHeight() { return NumCast(this.props.Document!.nativeHeight, this.props.PanelHeight()); }
+    private get layoutDoc() { return this.props.Document && Doc.Layout(this.props.Document); }
+    private get nativeWidth() { return NumCast(this.layoutDoc!.nativeWidth, this.props.PanelWidth()); }
+    private get nativeHeight() { return NumCast(this.layoutDoc!.nativeHeight, this.props.PanelHeight()); }
     private contentScaling = () => {
         let wscale = this.props.PanelWidth() / (this.nativeWidth ? this.nativeWidth : this.props.PanelWidth());
         if (wscale * this.nativeHeight > this.props.PanelHeight()) {
@@ -947,10 +948,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
         if (de.data instanceof DragManager.DocumentDragData) {
             this.props.childDocs && this.props.childDocs.map(otherdoc => {
                 let target = Doc.GetProto(otherdoc);
-                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]);
+                target.layout = ComputedField.MakeFunction("this.image_data[0]");
+                target.layoutCustom = Doc.MakeDelegate(de.data.draggedDocuments[0]);
             });
             e.stopPropagation();
         }
@@ -968,7 +967,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
         let br = StrCast(this.props.Document!.borderRounding);
         if (br.endsWith("%")) {
             let percent = Number(br.substr(0, br.length - 1)) / 100;
-            let nativeDim = Math.min(NumCast(this.props.Document!.nativeWidth), NumCast(this.props.Document!.nativeHeight));
+            let nativeDim = Math.min(NumCast(this.layoutDoc!.nativeWidth), NumCast(this.layoutDoc!.nativeHeight));
             let minDim = percent * (nativeDim ? nativeDim : Math.min(this.PanelWidth(), this.PanelHeight()));
             return minDim;
         }
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 1a578f4fc..21c266b7e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -57,15 +57,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         this._docXfs.length = 0;
         return docs.map((d, i) => {
             let pair = Doc.GetLayoutDataDocPair(this.props.Document, this.props.DataDoc, this.props.fieldKey, d);
-            let width = () => Math.min(d.nativeWidth && !d.ignoreAspect && !this.props.Document.fillColumn ? d[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
-            let height = () => this.getDocHeight(pair.layout);
+            let layoutDoc = pair.layout ? Doc.Layout(pair.layout) : d;
+            let width = () => Math.min(layoutDoc.nativeWidth && !layoutDoc.ignoreAspect && !this.props.Document.fillColumn ? layoutDoc[WidthSym]() : Number.MAX_VALUE, this.columnWidth / this.numGroupColumns);
+            let height = () => this.getDocHeight(layoutDoc);
             let dref = React.createRef<HTMLDivElement>();
-            let dxf = () => this.getDocTransform(pair.layout!, dref.current!);
+            let dxf = () => this.getDocTransform(layoutDoc, dref.current!);
             this._docXfs.push({ dxf: dxf, width: width, height: height });
             let rowSpan = Math.ceil((height() + this.gridGap) / this.gridGap);
             let style = this.isStackingView ? { width: width(), margin: "auto", marginTop: i === 0 ? 0 : this.gridGap, height: height() } : { gridRowEnd: `span ${rowSpan}` };
             return <div className={`collectionStackingView-${this.isStackingView ? "columnDoc" : "masonryDoc"}`} key={d[Id]} ref={dref} style={style} >
-                {pair.layout instanceof Doc && this.getDisplayDoc(pair.layout, pair.data, dxf, width)}
+                {this.getDisplayDoc(pair.layout || d, pair.data, dxf, width)}
             </div>;
         });
     }
@@ -111,8 +112,15 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             if (this.props.Document.autoHeight) {
                 let sectionsList = Array.from(this.Sections.size ? this.Sections.values() : [this.filteredChildren]);
                 if (this.isStackingView) {
-                    return this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => Math.max(maxHght,
-                        (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap), this.yMargin)), 0);
+                    let res = this.props.ContentScaling() * sectionsList.reduce((maxHght, s) => {
+                        let r1 = Math.max(maxHght,
+                            (this.Sections.size ? 50 : 0) + s.reduce((height, d, i) => {
+                                let val = height + this.childDocHeight(d) + (i === s.length - 1 ? this.yMargin : this.gridGap);
+                                return val;
+                            }, this.yMargin));
+                        return r1;
+                    }, 0);
+                    return res;
                 } else {
                     let sum = Array.from(this._heightMap.values()).reduce((acc: number, curr: number) => acc += curr, 0);
                     return this.props.ContentScaling() * (sum + (this.Sections.size ? 85 : -15));
@@ -122,7 +130,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         },
             (hgt: number) => {
                 let doc = hgt === -1 ? undefined : this.props.DataDoc && this.props.DataDoc.layout === this.layoutDoc ? this.props.DataDoc : this.layoutDoc;
-                doc && (doc.height = hgt);
+                doc && (Doc.Layout(doc).height = hgt);
             },
             { fireImmediately: true }
         );
@@ -155,11 +163,12 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @computed get onChildClickHandler() { return 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);
+    getDisplayDoc(doc: Doc, dataDoc: Doc | undefined, dxf: () => Transform, width: () => number) {
+        let layoutDoc = Doc.Layout(doc);
+        let height = () => this.getDocHeight(doc);
         let finalDxf = () => dxf().scale(this.columnWidth / layoutDoc[WidthSym]());
         return <CollectionSchemaPreview
-            Document={layoutDoc}
+            Document={doc}
             DataDocument={dataDoc}
             showOverlays={this.overlays}
             renderDepth={this.props.renderDepth}
@@ -186,16 +195,16 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     getDocHeight(d?: Doc) {
         if (!d) return 0;
         let layoutDoc = Doc.Layout(d);
-        let nw = NumCast(d.nativeWidth);
-        let nh = NumCast(d.nativeHeight);
+        let nw = NumCast(layoutDoc.nativeWidth);
+        let nh = NumCast(layoutDoc.nativeHeight);
         let wid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1);
         if (!layoutDoc.ignoreAspect && !layoutDoc.fitWidth && nw && nh) {
             let aspect = nw && nh ? nh / nw : 1;
             if (!(d.nativeWidth && !layoutDoc.ignoreAspect && this.props.Document.fillColumn)) wid = Math.min(layoutDoc[WidthSym](), wid);
             return wid * aspect;
         }
-        return layoutDoc.fitWidth ? !d.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
-            Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(d.nativeHeight)) / NumCast(d.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
+        return layoutDoc.fitWidth ? !layoutDoc.nativeHeight ? this.props.PanelHeight() - 2 * this.yMargin :
+            Math.min(wid * NumCast(layoutDoc.scrollHeight, NumCast(layoutDoc.nativeHeight)) / NumCast(layoutDoc.nativeWidth, 1), this.props.PanelHeight() - 2 * this.yMargin) : layoutDoc[HeightSym]();
     }
 
     columnDividerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index f76447157..0e8c46d40 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -61,11 +61,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             this._childLayoutDisposer = reaction(() => [this.childDocs, Cast(this.props.Document.childLayout, Doc)],
                 async (args) => {
                     if (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)));
+                        this.childDocs.map(async doc => !Doc.AreProtosEqual(args[1] as Doc, (await doc).layout as Doc) && Doc.ApplyTemplateTo(args[1] as Doc, (await doc), "layoutFromParent"));
+                    }
+                    else if (!(args[1] instanceof Promise)) {
+                        this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layoutKey === "layoutFromParent" && (doc.layoutKey = "layout"));
                     }
-                    // else if (!(args[1] instanceof Promise)) {
-                    //     this.childDocs.filter(d => !d.isTemplateField).map(async doc => doc.layout = undefined);
-                    // }
                 });
 
         }
@@ -134,8 +134,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)
-                    );
+                        Doc.ApplyTemplateTo(de.data.draggedDocuments[0], doc, "layoutFromParent"));
                     e.stopPropagation();
                     return true;
                 }
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 2fbe8527e..77736b7d0 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -249,23 +249,22 @@ class TreeView extends React.Component<TreeViewProps> {
         let finalXf = this.props.ScreenToLocalTransform().translate(offset[0], offset[1]);
         return finalXf;
     }
+    get layoutDoc() { return Doc.Layout(this.props.document); }
     docWidth = () => {
-        let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
-        let layoutDoc = Doc.Layout(this.props.document);
-        if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
-        return NumCast(this.props.document.nativeWidth) ? Math.min(layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
+        let aspect = NumCast(this.layoutDoc.nativeHeight) / NumCast(this.layoutDoc.nativeWidth);
+        if (aspect) return Math.min(this.layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT / aspect, this.props.panelWidth() - 20));
+        return NumCast(this.layoutDoc.nativeWidth) ? Math.min(this.layoutDoc[WidthSym](), this.props.panelWidth() - 20) : this.props.panelWidth() - 20;
     }
     docHeight = () => {
         let bounds = this.boundsOfCollectionDocument;
         return Math.min(this.MAX_EMBED_HEIGHT, (() => {
-            let aspect = NumCast(this.props.document.nativeHeight) / NumCast(this.props.document.nativeWidth);
-            let layoutDoc = Doc.Layout(this.props.document);
+            let aspect = NumCast(this.layoutDoc.nativeHeight) / NumCast(this.layoutDoc.nativeWidth);
             if (aspect) return this.docWidth() * aspect;
             if (bounds) return this.docWidth() * (bounds.b - bounds.y) / (bounds.r - bounds.x);
-            return layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
-                Math.min(this.docWidth() * NumCast(layoutDoc.scrollHeight, NumCast(this.props.document.nativeHeight)) / NumCast(this.props.document.nativeWidth,
+            return this.layoutDoc.fitWidth ? (!this.props.document.nativeHeight ? NumCast(this.props.containingCollection.height) :
+                Math.min(this.docWidth() * NumCast(this.layoutDoc.scrollHeight, NumCast(this.layoutDoc.nativeHeight)) / NumCast(this.layoutDoc.nativeWidth,
                     NumCast(this.props.containingCollection.height)))) :
-                NumCast(layoutDoc.height) ? NumCast(layoutDoc.height) : 50;
+                NumCast(this.layoutDoc.height) ? NumCast(this.layoutDoc.height) : 50;
         })());
     }
 
@@ -484,7 +483,7 @@ class TreeView extends React.Component<TreeViewProps> {
             };
             const childLayout = Doc.Layout(pair.layout);
             let rowHeight = () => {
-                let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
+                let aspect = NumCast(childLayout.nativeWidth, 0) / NumCast(childLayout.nativeHeight, 0);
                 return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym]();
             };
             return <TreeView
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 886692172..48d330674 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -63,11 +63,12 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
             fontSize: NumCast(pivotDoc.pivotFontSize, 10)
         });
         for (const doc of val) {
+            let layoutDoc = Doc.Layout(doc);
             docMap.set(doc, {
                 x: x + xCount * pivotAxisWidth * 1.25,
                 y: -y,
                 width: pivotAxisWidth,
-                height: doc.nativeWidth ? (NumCast(doc.nativeHeight) / NumCast(doc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
+                height: layoutDoc.nativeWidth ? (NumCast(layoutDoc.nativeHeight) / NumCast(layoutDoc.nativeWidth)) * pivotAxisWidth : pivotAxisWidth
             });
             xCount++;
             if (xCount >= numCols) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index eff73b14e..aa1106f13 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -63,10 +63,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private _hitCluster = false;
     @observable _clusterSets: (Doc[])[] = [];
 
+    get layoutDoc() { return PanZoomDocument(Doc.Layout(this.props.Document)); }
     @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
     @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; }
     @computed get contentBounds() { return aggregateBounds(this.elements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!)); }
-    @computed get nativeWidth() { return this.fitToContent ? 0 : this.Document.nativeWidth || 0; }
+    @computed get nativeWidth() { return this.layoutDoc.fitToContent ? 0 : this.layoutDoc.nativeWidth || 0; }
     @computed get nativeHeight() { return this.fitToContent ? 0 : this.Document.nativeHeight || 0; }
     private 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')
     private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
@@ -448,6 +449,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             ...this.props,
             DataDoc: childData,
             Document: childLayout,
+            layoutKey: undefined,
             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,
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 2243a44d5..e3ca02fa4 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -77,7 +77,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     borderRounding = () => {
         let ruleRounding = this.props.ruleProvider ? StrCast(this.props.ruleProvider["ruleRounding_" + this.Document.heading]) : undefined;
-        let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined;
+        let ld = this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] instanceof Doc ? this.layoutDoc[StrCast(this.layoutDoc.layoutKey, "layout")] as Doc : undefined;
         let br = StrCast((ld || this.props.Document).borderRounding);
         br = !br && ruleRounding ? ruleRounding : br;
         if (br.endsWith("%")) {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index bb9315ca3..7aed416f4 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -2,7 +2,7 @@ import { computed } from "mobx";
 import { observer } from "mobx-react";
 import { Doc } from "../../../new_fields/Doc";
 import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast } from "../../../new_fields/Types";
+import { Cast, StrCast } from "../../../new_fields/Types";
 import { OmitKeys, Without } from "../../../Utils";
 import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
 import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
@@ -69,10 +69,9 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     }
 
     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 template 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.
+        if (this.props.DataDoc === undefined && (Doc.LayoutField(this.props.Document) instanceof Doc || this.props.Document instanceof Promise)) {
+            // if there is no dataDoc (ie, we're not rendering a template layout), but this document has a layout document (not a layout string), 
+            // then we render the layout document as a template and use this document as the data context for the template layout.
             return this.props.Document;
         }
         return this.props.DataDoc;
@@ -83,7 +82,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         let list = {
             ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit,
             Document: this.layoutDoc,
-            DataDoc: this.dataDoc
+            DataDoc: this.dataDoc,
         };
         return { props: list };
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1087c57ad..2d13eeae4 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -191,9 +191,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             let preventDefault = true;
             if (this._doubleTap && this.props.renderDepth && (!this.onClickHandler || !this.onClickHandler.script)) { // disable double-click to show full screen for things that have an on click behavior since clicking them twice can be misinterpreted as a double click
                 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");
+                if (StrCast(fullScreenAlias.layoutKey) !== "layoutCustom" && fullScreenAlias["layoutCustom"] !== undefined) {
+                    fullScreenAlias.layoutKey = "layoutCustom";
                 }
                 this.props.addDocTab(fullScreenAlias, undefined, "inTab");
                 SelectionManager.DeselectAll();
@@ -287,16 +286,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     deleteClicked = (): void => { SelectionManager.DeselectAll(); this.props.removeDocument && this.props.removeDocument(this.props.Document); }
 
-    static makeNativeViewClicked = async (doc: Doc): Promise<void> => {
-        undoBatch(() => swapViews(doc, "layoutNative", "layoutCustom"))();
+    static makeNativeViewClicked = (doc: Doc) => {
+        undoBatch(() => doc.layoutKey = "layout")();
     }
 
-    static makeCustomViewClicked = async (doc: Doc, dataDoc: Opt<Doc>) => {
+    static makeCustomViewClicked = (doc: Doc, dataDoc: Opt<Doc>) => {
         const batch = UndoManager.StartBatch("CustomViewClicked");
         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, };
@@ -326,10 +322,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             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");
+            Doc.ApplyTemplateTo(docTemplate, dataDoc || doc, "layoutCustom", undefined);
         } else {
-            await swapViews(doc, "layoutCustom", "layoutNative");
+            doc.layoutKey = "layoutCustom";
         }
         batch.end();
     }
@@ -356,7 +351,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             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);
+            Doc.ApplyTemplateTo(de.data.draggedDocuments[0], this.props.Document, "layoutCustom");
             e.stopPropagation();
         }
         if (de.data instanceof DragManager.LinkDragData) {
@@ -383,12 +378,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     freezeNativeDimensions = (): void => {
-        let proto = this.Document.isTemplateDoc ? 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();
+        this.layoutDoc.autoHeight = this.layoutDoc.autoHeight = false;
+        this.layoutDoc.ignoreAspect = !this.layoutDoc.ignoreAspect;
+        if (!this.layoutDoc.ignoreAspect && !this.layoutDoc.nativeWidth) {
+            this.layoutDoc.nativeWidth = this.props.PanelWidth();
+            this.layoutDoc.nativeHeight = this.props.PanelHeight();
         }
     }
 
@@ -505,7 +499,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         layoutItems.push({ description: "Zoom to Document", event: () => this.props.focus(this.props.Document, true), icon: "search" });
         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) {
+        } else {
             layoutItems.push({ description: "Use Native Layout", event: () => DocumentView.makeNativeViewClicked(this.props.Document), icon: "concierge-bell" });
         }
         !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
@@ -599,7 +593,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (showTitle ? 25 : 0) + 1;
     }
 
-    childScaling = () => (this.props.Document.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
+    childScaling = () => (this.layoutDoc.fitWidth ? this.props.PanelWidth() / this.nativeWidth : this.props.ContentScaling());
     @computed get contents() {
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
@@ -646,8 +640,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             this.props.backgroundColor(this.Document) || StrCast(this.layoutDoc.backgroundColor) :
             ruleColor && !colorSet ? ruleColor : StrCast(this.layoutDoc.backgroundColor) || this.props.backgroundColor(this.Document);
 
-        const nativeWidth = this.props.Document.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.Document.ignoreAspect ? `${this.nativeWidth}px` : "100%";
-        const nativeHeight = this.props.Document.fitWidth ? this.props.PanelHeight() - 2 : this.Document.ignoreAspect ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
+        const nativeWidth = this.layoutDoc.fitWidth ? this.props.PanelWidth() - 2 : this.nativeWidth > 0 && !this.layoutDoc.ignoreAspect ? `${this.nativeWidth}px` : "100%";
+        const nativeHeight = this.layoutDoc.fitWidth ? this.props.PanelHeight() - 2 : 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");
@@ -686,7 +680,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
         const highlightColors = ["transparent", "maroon", "maroon", "yellow", "magenta", "cyan", "orange"];
         const highlightStyles = ["solid", "dashed", "solid", "solid", "solid", "solid", "solid", "solid"];
-        let highlighting = fullDegree && this.props.Document.type !== DocumentType.FONTICON && this.props.Document.viewType !== CollectionViewType.Linear;
+        let highlighting = fullDegree && this.layoutDoc.type !== DocumentType.FONTICON && this.layoutDoc.viewType !== CollectionViewType.Linear;
         return (
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
@@ -696,10 +690,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     color: StrCast(this.Document.color),
                     outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
                     border: highlighting && borderRounding ? `${highlightStyles[fullDegree]} ${highlightColors[fullDegree]} ${localScale}px` : undefined,
-                    background: this.props.Document.type === DocumentType.FONTICON || this.props.Document.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
+                    background: this.layoutDoc.type === DocumentType.FONTICON || this.layoutDoc.viewType === CollectionViewType.Linear ? undefined : backgroundColor,
                     width: animwidth,
                     height: animheight,
-                    transform: `scale(${this.props.Document.fitWidth ? 1 : this.props.ContentScaling()})`,
+                    transform: `scale(${this.layoutDoc.fitWidth ? 1 : this.props.ContentScaling()})`,
                     opacity: this.Document.opacity
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
@@ -707,7 +701,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             >
                 {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) =>
                     //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) :
-                    <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.props.Document.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
+                    <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
                         <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} />
                     </div>)}
                 {!showTitle && !showCaption ?
@@ -733,33 +727,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 }
 
-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.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.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");
+    doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout";
 });
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index ac10d729a..f866e6f7e 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -271,7 +271,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     newLayout = Doc.MakeDelegate(draggedDoc);
                     newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
                 }
-                this.props.Document.layout = newLayout;
+                this.props.Document.layoutCustom = newLayout;
+                this.props.Document.layoutKey = "layoutCustom";
                 e.stopPropagation();
                 // embed document when dragging with a userDropAction or an embedDoc flag set
             } else if (de.data.userDropAction || de.data.embedDoc) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 2fc4c04e6..dc80af268 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -2,7 +2,7 @@ import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEye } from '@fortawesome/free-regular-svg-icons';
 import { faAsterisk, faFileAudio, faImage, faPaintBrush } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable, runInAction, trace } from 'mobx';
 import { observer } from "mobx-react";
 import Lightbox from 'react-image-lightbox';
 import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
@@ -13,7 +13,7 @@ import { ComputedField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
 import { AudioField, ImageField } from '../../../new_fields/URLField';
 import { RouteStore } from '../../../server/RouteStore';
-import { Utils, returnOne, emptyFunction } from '../../../Utils';
+import { Utils, returnOne, emptyFunction, OmitKeys } from '../../../Utils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
@@ -159,7 +159,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 const files = await res.json();
                 const url = Utils.prepend(files[0].path);
                 // upload to server with known URL 
-                let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", x: NumCast(self.props.Document.x), y: NumCast(self.props.Document.y), width: 200, height: 32 });
+                let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
                 audioDoc.treeViewExpandedView = "layout";
                 let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));
                 if (audioAnnos === undefined) {
@@ -178,18 +178,18 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         });
     }
 
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
     @undoBatch
     rotate = action(() => {
-        let proto = Doc.GetProto(this.props.Document);
-        let nw = this.props.Document.nativeWidth;
-        let nh = this.props.Document.nativeHeight;
-        let w = this.props.Document.width;
-        let h = this.props.Document.height;
-        proto.rotation = (NumCast(this.props.Document.rotation) + 90) % 360;
-        proto.nativeWidth = nh;
-        proto.nativeHeight = nw;
-        this.props.Document.width = h;
-        this.props.Document.height = w;
+        let nw = this.layoutDoc.nativeWidth;
+        let nh = this.layoutDoc.nativeHeight;
+        let w = this.layoutDoc.width;
+        let h = this.layoutDoc.height;
+        this.layoutDoc.rotation = (NumCast(this.layoutDoc.rotation) + 90) % 360;
+        this.layoutDoc.nativeWidth = nh;
+        this.layoutDoc.nativeHeight = nw;
+        this.layoutDoc.width = h;
+        this.layoutDoc.height = w;
     });
 
     specificContextMenu = (e: React.MouseEvent): void => {
@@ -251,7 +251,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     dots(paths: string[]) {
-        let nativeWidth = FieldValue(this.Document.nativeWidth, 1);
+        let nativeWidth = NumCast(this.layoutDoc.nativeWidth, 1);
         let dist = Math.min(nativeWidth / paths.length, 40);
         let left = (nativeWidth - paths.length * dist) / 2;
         return paths.map((p, i) =>
@@ -379,16 +379,17 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         return (null);
     }
 
-    @computed
-    get content() {
+    content(layoutDoc: Doc) {
+        if (!layoutDoc) return (null);
+        console.log("REDOING IMAGE CONTENT");
         // let transform = this.props.ScreenToLocalTransform().inverse();
         let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
         // var [sptX, sptY] = transform.transformPoint(0, 0);
         // let [bptX, bptY] = transform.transformPoint(pw, this.props.PanelHeight());
         // let w = bptX - sptX;
 
-        let nativeWidth = FieldValue(this.Document.nativeWidth, pw);
-        let nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+        let nativeWidth = NumCast(layoutDoc.nativeWidth, pw);
+        let nativeHeight = NumCast(layoutDoc.nativeHeight, 0);
         let paths: string[] = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
@@ -401,14 +402,14 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         if (field instanceof ImageField) paths = [this.choosePath(field.url)];
         paths.push(...altpaths);
         // }
-        let interactive = this.active() ? "-interactive" : "";// InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
-        let rotation = NumCast(this.dataDoc.rotation, 0);
-        let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
+        let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
+        let rotation = NumCast(layoutDoc.rotation, 0);
+        let aspect = (rotation % 180) ? layoutDoc[HeightSym]() / layoutDoc[WidthSym]() : 1;
         let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
-        let srcpath = paths[Math.min(paths.length - 1, this.Document.curPage || 0)];
+        let srcpath = paths[Math.min(paths.length - 1, NumCast(layoutDoc.curPage))];
         let fadepath = paths[Math.min(paths.length - 1, 1)];
 
-        if (!this.props.Document.ignoreAspect && !this.props.leaveNativeSize) this.resize(srcpath, this.props.Document);
+        (!layoutDoc.ignoreAspect && !this.props.leaveNativeSize) && this.resize(srcpath, layoutDoc);
 
         return (
             <div className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
@@ -448,7 +449,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     render() {
-        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+        trace();
+        if (!Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey)) return (null);
         return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
@@ -468,7 +470,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 renderDepth={this.props.renderDepth + 1}
                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}
                 chromeCollapsed={true}>
-                {() => [this.content]}
+                {() => [this.content(this.layoutDoc)]}
             </CollectionFreeFormView>
         </div >);
     }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index f31128356..f3ea8328b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -4,7 +4,7 @@ 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 { Opt, WidthSym } from "../../../new_fields/Doc";
+import { Opt, WidthSym, Doc } from "../../../new_fields/Doc";
 import { makeInterface } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
 import { Cast } from "../../../new_fields/Types";
@@ -65,11 +65,13 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
             }, { fireImmediately: true });
     }
 
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
+
     loaded = (nw: number, nh: number, np: number) => {
         this.dataDoc.numPages = np;
-        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));
+        this.layoutDoc.nativeWidth = nw * 96 / 72;
+        this.layoutDoc.nativeHeight = nh * 96 / 72;
+        !this.layoutDoc.fitWidth && !this.layoutDoc.ignoreAspect && (this.layoutDoc.height = this.layoutDoc[WidthSym]() * (nh / nw));
     }
 
     public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 19968e6e1..4c01279ae 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -55,14 +55,15 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         return this._videoRef;
     }
 
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
     videoLoad = () => {
         let aspect = this.player!.videoWidth / this.player!.videoHeight;
-        var nativeWidth = FieldValue(this.Document.nativeWidth, 0);
-        var nativeHeight = FieldValue(this.Document.nativeHeight, 0);
+        var nativeWidth = NumCast(this.layoutDoc.nativeWidth, 0);
+        var nativeHeight = NumCast(this.layoutDoc.nativeHeight, 0);
         if (!nativeWidth || !nativeHeight) {
-            if (!this.Document.nativeWidth) this.Document.nativeWidth = this.player!.videoWidth;
-            this.Document.nativeHeight = this.Document.nativeWidth / aspect;
-            this.Document.height = FieldValue(this.Document.width, 0) / aspect;
+            if (!this.layoutDoc.nativeWidth) this.layoutDoc.nativeWidth = this.player!.videoWidth;
+            this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / aspect;
+            this.layoutDoc.height = NumCast(this.layoutDoc.width, 0) / aspect;
         }
         if (!this.Document.duration) this.Document.duration = this.player!.duration;
     }
@@ -107,7 +108,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         let height = this.Document.height || 0;
         var canvas = document.createElement('canvas');
         canvas.width = 640;
-        canvas.height = 640 * (this.Document.nativeHeight || 0) / (this.Document.nativeWidth || 1);
+        canvas.height = 640 * NumCast (this.layoutDoc.nativeHeight) / NumCast(this.layoutDoc.nativeWidth,1);
         var ctx = canvas.getContext('2d');//draw image to canvas. scale to target dimensions
         if (ctx) {
             ctx.rect(0, 0, canvas.width, canvas.height);
@@ -153,12 +154,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
         if (this.youtubeVideoId) {
             let youtubeaspect = 400 / 315;
-            var nativeWidth = (this.Document.nativeWidth || 0);
-            var nativeHeight = (this.Document.nativeHeight || 0);
+            var nativeWidth = NumCast(this.layoutDoc.nativeWidth);
+            var nativeHeight =  NumCast(this.layoutDoc.nativeHeight);
             if (!nativeWidth || !nativeHeight) {
-                if (!this.Document.nativeWidth) this.Document.nativeWidth = 600;
-                this.Document.nativeHeight = this.Document.nativeWidth / youtubeaspect;
-                this.Document.height = (this.Document.width || 0) / youtubeaspect;
+                if (!this.layoutDoc.nativeWidth) this.layoutDoc.nativeWidth = 600;
+                this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
+                this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect;
             }
         }
     }
@@ -264,7 +265,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     }
     private get uIButtons() {
         let scaling = Math.min(1.8, this.props.ScreenToLocalTransform().Scale);
-        let curTime = NumCast(this.props.Document.currentTimecode);
+        let curTime = NumCast(this.layoutDoc.currentTimecode);
         return ([<div className="videoBox-time" key="time" onPointerDown={this.onResetDown} style={{ transform: `scale(${scaling})` }}>
             <span>{"" + Math.round(curTime)}</span>
             <span style={{ fontSize: 8 }}>{" " + Math.round((curTime - Math.trunc(curTime)) * 100)}</span>
@@ -312,7 +313,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     @action
     onResetMove = (e: PointerEvent) => {
         this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
-        this.Seek(Math.max(0, NumCast(this.props.Document.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333));
+        this.Seek(Math.max(0, NumCast(this.layoutDoc.currentTimecode, 0) + Math.sign(e.movementX) * 0.0333));
         e.stopImmediatePropagation();
     }
     @action
@@ -320,10 +321,10 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         document.removeEventListener("pointermove", this.onResetMove, true);
         document.removeEventListener("pointerup", this.onResetUp, true);
         InkingControl.Instance.switchTool(InkTool.None);
-        this._isResetClick < 10 && (this.props.Document.currentTimecode = 0);
+        this._isResetClick < 10 && (this.layoutDoc.currentTimecode = 0);
     }
     @computed get fieldExtensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
+    @computed get dataDoc() { return this.props.DataDoc && this.layoutDoc.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
@@ -331,14 +332,14 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         let style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
         let start = untracked(() => Math.round(this.Document.currentTimecode || 0));
         return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
-            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={(this.Document.nativeWidth || 640)} height={(this.Document.nativeHeight || 390)}
+            onLoad={this.youtubeIframeLoaded} className={`${style}`} width={NumCast(this.layoutDoc.nativeWidth, 640)} height={NumCast(this.layoutDoc.nativeHeigh, 390)}
             src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=1&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._showControls ? 1 : 0}`} />;
     }
 
     @action.bound
     addDocumentWithTimestamp(doc: Doc): boolean {
         Doc.GetProto(doc).annotationOn = this.props.Document;
-        var curTime = NumCast(this.props.Document.currentTimecode, -1);
+        var curTime = NumCast(this.layoutDoc.currentTimecode, -1);
         curTime !== -1 && (doc.displayTimecode = curTime);
         return Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, doc);
     }
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 7c7f9fb83..bb58912b5 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -34,17 +34,18 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
     @observable private collapsed: boolean = true;
     @observable private url: string = "";
 
+    get layoutDoc() {return Doc.Layout(this.props.Document); }
     componentWillMount() {
 
         let field = Cast(this.props.Document[this.props.fieldKey], WebField);
         if (field && field.url.href.indexOf("youtube") !== -1) {
             let youtubeaspect = 400 / 315;
-            var nativeWidth = NumCast(this.props.Document.nativeWidth, 0);
-            var nativeHeight = NumCast(this.props.Document.nativeHeight, 0);
+            var nativeWidth = NumCast(this.layoutDoc.nativeWidth);
+            var nativeHeight = NumCast(this.layoutDoc.nativeHeight);
             if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) {
-                if (!nativeWidth) this.props.Document.nativeWidth = 600;
-                this.props.Document.nativeHeight = NumCast(this.props.Document.nativeWidth) / youtubeaspect;
-                this.props.Document.height = NumCast(this.props.Document.width) / youtubeaspect;
+                if (!nativeWidth) this.layoutDoc.nativeWidth = 600;
+                this.layoutDoc.nativeHeight = NumCast(this.layoutDoc.nativeWidth) / youtubeaspect;
+                this.layoutDoc.height = NumCast(this.layoutDoc.width) / youtubeaspect;
             }
         }
 
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 6e5f1a981..51f7829e8 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -113,7 +113,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     componentDidMount = async () => {
         // change the address to be the file address of the PNG version of each page
         // 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`)));
+        this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${NumCast(this.layoutDoc.curPage, 1)}.PNG`)));
         runInAction(() => this._showWaiting = this._showCover = true);
         this.props.startupLive && this.setupPdfJsViewer();
         this._searchReactionDisposer = reaction(() => StrCast(this.props.Document.search_string), searchString => {
@@ -131,14 +131,14 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             () => (SelectionManager.SelectedDocuments().length === 1) && this.setupPdfJsViewer(),
             { fireImmediately: true });
         this._reactionDisposer = reaction(
-            () => this.props.Document.scrollY,
+            () => this.layoutDoc.scrollY,
             (scrollY) => {
                 if (scrollY !== undefined) {
                     if (this._showCover || this._showWaiting) {
                         this.setupPdfJsViewer();
                     }
-                    this._mainCont.current && smoothScroll(1000, this._mainCont.current, NumCast(this.props.Document.scrollY) || 0);
-                    this.props.Document.scrollY = undefined;
+                    this._mainCont.current && smoothScroll(1000, this._mainCont.current, NumCast(this.layoutDoc.scrollY) || 0);
+                    this.layoutDoc.scrollY = undefined;
                 }
             },
             { fireImmediately: true }
@@ -179,7 +179,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                     i === this.props.pdf.numPages - 1 && this.props.loaded((page.view[page.rotate === 0 || page.rotate === 180 ? 2 : 3] - page.view[page.rotate === 0 || page.rotate === 180 ? 0 : 1]),
                         (page.view[page.rotate === 0 || page.rotate === 180 ? 3 : 2] - page.view[page.rotate === 0 || page.rotate === 180 ? 1 : 0]), i);
                 }))));
-            Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
+                this.layoutDoc.scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
         }
     }
 
@@ -197,7 +197,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
             { fireImmediately: true });
 
         this._filterReactionDisposer = reaction(
-            () => ({ scriptField: Cast(this.props.Document.filterScript, ScriptField), annos: this._annotations.slice() }),
+            () => ({ scriptField: Cast(this.layoutDoc.filterScript, ScriptField), annos: this._annotations.slice() }),
             action(({ scriptField, annos }: { scriptField: FieldResult<ScriptField>, annos: Doc[] }) => {
                 let oldScript = this._script.originalScript;
                 this._script = scriptField && scriptField.script.compiled ? scriptField.script : CompileScript("return true") as CompiledScript;
@@ -224,7 +224,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         document.addEventListener("copy", this.copy);
         document.addEventListener("pagesinit", action(() => {
             this._pdfViewer.currentScaleValue = this._zoomed = 1;
-            this.gotoPage(NumCast(this.props.Document.curPage, 1));
+            this.gotoPage(NumCast(this.layoutDoc.curPage, 1));
         }));
         document.addEventListener("pagerendered", action(() => this._showCover = this._showWaiting = false));
         var pdfLinkService = new PDFJSViewer.PDFLinkService();
@@ -335,7 +335,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @action
     onScroll = (e: React.UIEvent<HTMLElement>) => {
         this._scrollTop = this._mainCont.current!.scrollTop;
-        this._pdfViewer && (this.props.Document.curPage = this._pdfViewer.currentPageNumber);
+        this._pdfViewer && (this.layoutDoc.curPage = this._pdfViewer.currentPageNumber);
     }
 
     // get the page index that the vertical offset passed in is on
@@ -403,7 +403,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         this._downX = e.clientX;
         this._downY = e.clientY;
         addStyleSheetRule(PDFViewer._annotationStyle, "pdfAnnotation", { "pointer-events": "none" });
-        if (NumCast(this.props.Document.scale, 1) !== 1) return;
+        if (NumCast(this.layoutDoc.scale, 1) !== 1) return;
         if ((e.button !== 0 || e.altKey) && this.active()) {
             this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
         }
@@ -568,16 +568,18 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         }
     }
 
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
+
     createSnippet = (marquee: { left: number, top: number, width: number, height: number }): void => {
         let view = Doc.MakeAlias(this.props.Document);
         let data = Doc.MakeDelegate(Doc.GetProto(this.props.Document));
         data.title = StrCast(data.title) + "_snippet";
         view.proto = data;
         view.nativeHeight = marquee.height;
-        view.height = (this.props.Document[WidthSym]() / NumCast(this.props.Document.nativeWidth)) * marquee.height;
-        view.nativeWidth = this.props.Document.nativeWidth;
+        view.height = (this.layoutDoc[WidthSym]() / NumCast(this.layoutDoc.nativeWidth)) * marquee.height;
+        view.nativeWidth = this.layoutDoc.nativeWidth;
         view.startY = marquee.top;
-        view.width = this.props.Document[WidthSym]();
+        view.width = this.layoutDoc[WidthSym]();
         DragManager.StartDocumentDrag([], new DragManager.DocumentDragData([view]), 0, 0);
     }
 
@@ -598,12 +600,12 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     getCoverImage = () => {
         if (!this.props.Document[HeightSym]() || !this.props.Document.nativeHeight) {
             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;
+                this.layoutDoc.height = this.layoutDoc[WidthSym]() * this._coverPath.height / this._coverPath.width;
+                this.layoutDoc.nativeHeight = nativeWidth * this._coverPath.height / this._coverPath.width;
             }).bind(this), 0);
         }
-        let nativeWidth = NumCast(this.props.Document.nativeWidth);
-        let nativeHeight = NumCast(this.props.Document.nativeHeight);
+        let nativeWidth = NumCast(this.layoutDoc.nativeWidth);
+        let nativeHeight = NumCast(this.layoutDoc.nativeHeight);
         return <img key={this._coverPath.path} src={this._coverPath.path} onError={action(() => this._coverPath.path = "http://www.cs.brown.edu/~bcz/face.gif")} onLoad={action(() => this._showWaiting = false)}
             style={{ position: "absolute", display: "inline-block", top: 0, left: 0, width: `${nativeWidth}px`, height: `${nativeHeight}px` }} />;
     }
@@ -619,14 +621,14 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     }
 
     @computed get annotationLayer() {
-        return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.props.Document.nativeHeight) }} ref={this._annotationLayer}>
+        return <div className="pdfViewer-annotationLayer" style={{ height: NumCast(this.layoutDoc.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 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))}
-                    PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.props.Document.nativeWidth)}
+                    PanelHeight={() => NumCast(this.layoutDoc.scrollHeight, NumCast(this.layoutDoc.nativeHeight))}
+                    PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : NumCast(this.layoutDoc.nativeWidth)}
                     VisibleHeight={this.visibleHeight}
                     focus={this.props.focus}
                     isSelected={this.props.isSelected}
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index daf000dc7..0824808eb 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -151,6 +151,7 @@ export class PresElementBox extends React.Component<FieldViewProps> {
      */
     ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
 
+    get layoutDoc() { return Doc.Layout(this.props.Document); }
     /**
      * The function that is responsible for rendering the a preview or not for this
      * presentation element.
@@ -160,12 +161,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
             return (null);
         }
 
-        let propDocWidth = NumCast(this.props.Document.nativeWidth);
-        let propDocHeight = NumCast(this.props.Document.nativeHeight);
-        let scale = () => 175 / NumCast(this.props.Document.nativeWidth, 175);
+        let propDocWidth = NumCast(this.layoutDoc.nativeWidth);
+        let propDocHeight = NumCast(this.layoutDoc.nativeHeight);
+        let scale = () => 175 / NumCast(this.layoutDoc.nativeWidth, 175);
         return (
             <div className="presElementBox-embedded" style={{
-                height: propDocHeight === 0 ? NumCast(this.props.Document.height) - NumCast(this.props.Document.collapsedHeight) : propDocHeight * scale(),
+                height: propDocHeight === 0 ? NumCast(this.layoutDoc.height) - NumCast(this.layoutDoc.collapsedHeight) : propDocHeight * scale(),
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <CollectionSchemaPreview
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index b728ffaa9..899a35f48 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -211,7 +211,7 @@ export class SearchBox extends React.Component {
         });
         let x = 0;
         let y = 0;
-        for (const doc of docs) {
+        for (const doc of docs.map(d => Doc.Layout(d))) {
             doc.x = x;
             doc.y = y;
             const size = 200;
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index b8cff16f2..60307065a 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -150,7 +150,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
         if (!this._useIcons) {
             let returnXDimension = () => this._useIcons ? 50 : Number(SEARCH_THUMBNAIL_SIZE);
             let returnYDimension = () => this._displayDim;
-            let scale = () => returnXDimension() / NumCast(this.props.doc.nativeWidth, returnXDimension());
+            let scale = () => returnXDimension() / NumCast(Doc.Layout(this.props.doc).nativeWidth, returnXDimension());
             const docview = <div
                 onPointerDown={action(() => {
                     this._useIcons = !this._useIcons;
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 31173a5c1..68196de45 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -424,12 +424,16 @@ export namespace Doc {
     export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
         let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
         if (docExtensionForField === undefined) {
-            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);
+            if (immediate) {
+                CreateDocumentExtensionForField(doc, fieldKey);
+                return true;
+            }
+            else {
+                setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+                return false;
+            }
         }
+        return true;
     }
     export function MakeTitled(title: string) {
         let doc = new Doc();
@@ -453,7 +457,7 @@ export namespace Doc {
     // for individual layout properties to be overridden in the expanded layout.
     //
     export function WillExpandTemplateLayout(layoutDoc: Doc, dataDoc?: Doc) {
-        return BoolCast(layoutDoc.isTemplateField) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc.layout instanceof Doc);
+        return BoolCast(layoutDoc.isTemplateField) && dataDoc && layoutDoc !== dataDoc && !(layoutDoc[StrCast(layoutDoc.layoutKey, "layout")] instanceof Doc);
     }
 
     //
@@ -564,13 +568,13 @@ export namespace Doc {
     let _applyCount: number = 0;
     export function ApplyTemplate(templateDoc: Doc) {
         if (templateDoc) {
-            let applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), templateDoc.title + "(..." + _applyCount++ + ")");
+            let applied = ApplyTemplateTo(templateDoc, Doc.MakeDelegate(new Doc()), "layoutCustom", templateDoc.title + "(..." + _applyCount++ + ")");
             applied && (Doc.GetProto(applied).layout = applied.layout);
             return applied;
         }
         return undefined;
     }
-    export function ApplyTemplateTo(templateDoc: Doc, target: Doc, titleTarget: string | undefined = undefined) {
+    export function ApplyTemplateTo(templateDoc: Doc, target: Doc, targetKey: string, titleTarget: string | undefined = undefined) {
         if (!templateDoc) {
             target.layout = undefined;
             target.nativeWidth = undefined;
@@ -580,21 +584,14 @@ export namespace Doc {
             return;
         }
 
-        let layoutCustom = Doc.MakeTitled("layoutCustom");
         let layoutCustomLayout = Doc.MakeDelegate(templateDoc);
 
         titleTarget && (Doc.GetProto(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.layout = layoutCustomLayout;
 
-        target.layoutNative = Cast(templateDoc.layoutNative, Doc) as Doc;
-        target.layoutCustom = layoutCustom;
+        Doc.GetProto(target)[targetKey] = layoutCustomLayout;
+        target.layoutKey = targetKey;
         return target;
     }
 
@@ -666,9 +663,9 @@ export namespace Doc {
         @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
     }
 
-    // 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.
-    export function Layout(doc: Doc) { return doc.layout instanceof Doc ? doc.layout : doc; }
+    // returns the active layout document for 'doc'.  
+    export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? doc[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
+    export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
     const manager = new DocData();
     export function UserDoc(): Doc { return manager._user_doc; }
     export function SetUserDoc(doc: Doc) { manager._user_doc = doc; }
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 9c41c7651..532f527cc 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -71,7 +71,7 @@ export class CurrentUserUtils {
         // setup a masonry view of all he creators
         const dragCreators = Docs.Create.MasonryDocument(CurrentUserUtils.setupCreatorButtons(doc), {
             width: 500, autoHeight: true, columnWidth: 35, ignoreClick: true, lockedPosition: true, chromeStatus: "disabled", title: "buttons",
-            dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 0
+            dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), yMargin: 5
         });
         // setup a color picker
         const color = Docs.Create.ColorDocument({
-- 
cgit v1.2.3-70-g09d2


From 4d02c9b581a22da777232124f2b1a96f8e342285 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 21 Oct 2019 15:45:51 -0400
Subject: initial simplification to fieldExt

---
 src/client/views/DocComponent.tsx                  | 20 +++---
 .../views/collections/CollectionBaseView.tsx       |  4 +-
 .../views/collections/CollectionSchemaView.tsx     |  4 +-
 .../views/collections/CollectionStackingView.tsx   |  2 +-
 .../CollectionStackingViewFieldColumn.tsx          |  4 +-
 src/client/views/collections/CollectionSubView.tsx |  3 +-
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 .../views/collections/CollectionViewChromes.tsx    | 72 ++--------------------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 13 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/nodes/AudioBox.tsx                |  1 -
 src/client/views/nodes/ColorBox.tsx                | 19 +-----
 src/client/views/nodes/DocumentView.tsx            |  2 +
 src/client/views/nodes/FormattedTextBox.tsx        |  6 --
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 src/client/views/nodes/VideoBox.tsx                |  5 +-
 src/client/views/nodes/WebBox.tsx                  |  2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  3 +-
 src/new_fields/Doc.ts                              | 69 +++++++++------------
 21 files changed, 74 insertions(+), 167 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index ff149a9ac..1f9bdaac4 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -10,6 +10,8 @@ import { InkTool } from '../../new_fields/InkField';
 ///  DocComponents returns a generic base class for React views of document fields that are not interactive
 interface DocComponentProps {
     Document: Doc;
+    DataDoc?: Doc;
+    fieldKey: string;
 }
 export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
@@ -18,6 +20,8 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
         get Document(): T {
             return schemaCtor(this.props.Document);
         }
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
     }
     return Component;
 }
@@ -26,6 +30,8 @@ export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: D
 ///  DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox)
 interface DocStaticProps {
     Document: Doc;
+    DataDoc?: Doc;
+    fieldKey: string;
     isSelected: () => boolean;
     renderDepth: number;
 }
@@ -36,6 +42,8 @@ export function DocStaticComponent<P extends DocStaticProps, T>(schemaCtor: (doc
         get Document(): T {
             return schemaCtor(this.props.Document);
         }
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
         active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
     }
     return Component;
@@ -54,12 +62,15 @@ interface DocAnnotatableProps {
 }
 export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
+        _isChildActive = false;
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
         @computed
         get Document(): T {
             return schemaCtor(this.props.Document);
         }
-        _isChildActive = false;
+        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
@@ -67,23 +78,18 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
             let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
             return index !== -1 && value.splice(index, 1) ? true : false;
         }
-
-        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
-
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
         // if the moved document is already in this overlay collection nothing needs to be done.
         // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. 
         @action.bound
         moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
             return Doc.AreProtosEqual(this.props.Document, targetCollection) ? true : this.removeDocument(doc) ? addDocument(doc) : false;
         }
-
         @action.bound
         addDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = this.props.Document;
             return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc);
         }
+
         whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
         active = () => ((InkingControl.Instance.selectedTool === InkTool.None && !this.props.Document.isBackground) &&
             (this.props.Document.forceActive || this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0) ? true : false)
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 1ade44250..fa543cc01 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -97,9 +97,11 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
 
     @action.bound
     addDocument(doc: Doc): boolean {
-        let targetDataDoc = this.props.Document;
+        let targetDataDoc = Doc.GetProto(this.props.Document);
         let targetField = this.props.fieldKey;
         Doc.AddDocToList(targetDataDoc, targetField, doc);
+        let extension = Doc.fieldExtensionDoc(targetDataDoc, targetField);
+        extension && (extension.lastModified = new DateField(new Date(Date.now())));
         Doc.GetProto(doc).lastOpened = new DateField;
         return true;
     }
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 23e070750..04bc550ea 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -158,6 +158,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
             <CollectionSchemaPreview
                 Document={layoutDoc}
                 DataDocument={this.previewDocument !== this.props.DataDoc ? this.props.DataDoc : undefined}
+                fieldKey={this.props.fieldKey}
                 childDocs={this.childDocs}
                 renderDepth={this.props.renderDepth}
                 ruleProvider={this.props.Document.isRuleProvider && layoutDoc && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
@@ -226,7 +227,6 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
     }
 
     render() {
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         return (
             <div className="collectionSchemaView-container" style={{ height: "100%", marginTop: "0", }}>
                 <div className="collectionSchemaView-tableContainer" onPointerDown={this.onPointerDown} onWheel={this.onWheel} onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createTarget}>
@@ -898,6 +898,7 @@ interface CollectionSchemaPreviewProps {
     childDocs?: Doc[];
     renderDepth: number;
     fitToBox?: boolean;
+    fieldKey: string;
     PanelWidth: () => number;
     PanelHeight: () => number;
     ruleProvider: Doc | undefined;
@@ -993,6 +994,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                     <DocumentView {...this.props}
                         DataDoc={this.props.DataDocument}
                         Document={this.props.Document}
+                        fieldKey={this.props.fieldKey}
                         fitToBox={this.props.fitToBox}
                         onClick={this.props.onClick}
                         ruleProvider={this.props.ruleProvider}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 1bb2d2f46..5fb0c0f9f 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -170,6 +170,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         return <CollectionSchemaPreview
             Document={doc}
             DataDocument={dataDoc}
+            fieldKey={this.props.fieldKey}
             showOverlays={this.overlays}
             renderDepth={this.props.renderDepth}
             ruleProvider={this.props.Document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider}
@@ -389,7 +390,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
             SetValue: this.addGroup,
             contents: "+ ADD A GROUP"
         };
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         let sections = [[undefined, this.filteredChildren] as [SchemaHeaderField | undefined, Doc[]]];
         if (this.sectionFilter) {
             let entries = Array.from(this.Sections.entries());
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index 7e54b0f29..fec3d90b9 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -2,7 +2,7 @@ import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPalette } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, trace } from "mobx";
+import { action, observable, trace, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, WidthSym } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
@@ -204,7 +204,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             document.removeEventListener("pointerup", this.pointerUp);
             document.addEventListener("pointerup", this.pointerUp);
         }
-        this._createAliasSelected = false;
+        runInAction(() => this._createAliasSelected = false);
     }
 
     renderColorPicker = () => {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 8bd67b880..43147ed20 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -33,6 +33,7 @@ export interface CollectionViewProps extends FieldViewProps {
     VisibleHeight?: () => number;
     chromeCollapsed: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
+    fieldKey: string;
 }
 
 export interface SubCollectionViewProps extends CollectionViewProps {
@@ -77,7 +78,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         // 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.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt)[this.props.fieldExt || this.props.fieldKey];
+            return this.props.fieldExt ? this.extensionDoc[this.props.fieldExt] : this.dataDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs() {
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index c1b7f7e48..2cad41acb 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -317,6 +317,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 <CollectionSchemaPreview
                     Document={layoutDoc}
                     DataDocument={this.templateDataDoc}
+                    fieldKey={this.fieldKey}
                     renderDepth={this.props.renderDepth}
                     showOverlays={this.noOverlays}
                     ruleProvider={this.props.document.isRuleProvider && layoutDoc.type !== DocumentType.TEXT ? this.props.document : this.props.ruleProvider}
@@ -556,7 +557,6 @@ export class CollectionTreeView extends CollectionSubView(Document) {
     }
 
     render() {
-        Doc.UpdateDocumentExtensionForField(this.props.DataDoc ? this.props.DataDoc : this.props.Document, this.props.fieldKey);
         let dropAction = StrCast(this.props.Document.dropAction) as dropActionType;
         let addDoc = (doc: Doc, relativeTo?: Doc, before?: boolean) => Doc.AddDocToList(this.props.Document, this.props.fieldKey, doc, relativeTo, before, false, false, false);
         let moveDoc = (d: Doc, target: Doc, addDoc: (doc: Doc) => boolean) => this.props.moveDocument(d, target, addDoc);
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 4ca6df034..74a388425 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -149,7 +149,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
                 {this.SubView}
             </CollectionBaseView>
 
-            {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField)!.url!.href))}
+            {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField)!.url.href))}
         </>
         );
     }
diff --git a/src/client/views/collections/CollectionViewChromes.tsx b/src/client/views/collections/CollectionViewChromes.tsx
index a5b7f0181..dd5e630e4 100644
--- a/src/client/views/collections/CollectionViewChromes.tsx
+++ b/src/client/views/collections/CollectionViewChromes.tsx
@@ -480,12 +480,7 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView
 
     getKeySuggestions = async (value: string): Promise<string[]> => {
         value = value.toLowerCase();
-        let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
-            = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
-        if (typeof docs === "function") {
-            docs = docs();
-        }
-        docs = await docs;
+        let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
         if (docs instanceof Doc) {
             return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
         } else {
@@ -591,19 +586,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
         if (textwrappedRows.length) {
             this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>([]);
         } else {
-            let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
-                = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
-            if (typeof docs === "function") {
-                docs = docs();
-            }
-            docs = await docs;
-            if (docs instanceof Doc) {
-                let allRows = [docs[Id]];
-                this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
-            } else {
-                let allRows = docs.map(doc => doc[Id]);
-                this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
-            }
+            let docs = DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldKey]);
+            let allRows = docs instanceof Doc ? [docs[Id]] : docs.map(doc => doc[Id]);
+            this.props.CollectionView.props.Document.textwrappedSchemaRows = new List<string>(allRows);
         }
     }
 
@@ -638,63 +623,14 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewCh
 
 @observer
 export class CollectionTreeViewChrome extends React.Component<CollectionViewChromeProps> {
-    @observable private _currentKey: string = "";
-    @observable private suggestions: string[] = [];
 
     @computed private get descending() { return Cast(this.props.CollectionView.props.Document.sortAscending, "boolean", null); }
-    @computed get sectionFilter() { return StrCast(this.props.CollectionView.props.Document.sectionFilter); }
-
-    getKeySuggestions = async (value: string): Promise<string[]> => {
-        value = value.toLowerCase();
-        let docs: Doc | Doc[] | Promise<Doc> | Promise<Doc[]> | (() => DocLike)
-            = () => DocListCast(this.props.CollectionView.props.Document[this.props.CollectionView.props.fieldExt ? this.props.CollectionView.props.fieldExt : this.props.CollectionView.props.fieldKey]);
-        if (typeof docs === "function") {
-            docs = docs();
-        }
-        docs = await docs;
-        if (docs instanceof Doc) {
-            return Object.keys(docs).filter(key => key.toLowerCase().startsWith(value));
-        } else {
-            const keys = new Set<string>();
-            docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key)));
-            return Array.from(keys).filter(key => key.toLowerCase().startsWith(value));
-        }
-    }
-
-    @action
-    onKeyChange = (e: React.ChangeEvent, { newValue }: { newValue: string }) => {
-        this._currentKey = newValue;
-    }
-
-    getSuggestionValue = (suggestion: string) => suggestion;
-
-    renderSuggestion = (suggestion: string) => {
-        return <p>{suggestion}</p>;
-    }
-
-    onSuggestionFetch = async ({ value }: { value: string }) => {
-        const sugg = await this.getKeySuggestions(value);
-        runInAction(() => {
-            this.suggestions = sugg;
-        });
-    }
-
-    @action
-    onSuggestionClear = () => {
-        this.suggestions = [];
-    }
-
-    setValue = (value: string) => {
-        this.props.CollectionView.props.Document.sectionFilter = value;
-        return true;
-    }
 
     @action toggleSort = () => {
         if (this.props.CollectionView.props.Document.sortAscending) this.props.CollectionView.props.Document.sortAscending = undefined;
         else if (this.props.CollectionView.props.Document.sortAscending === undefined) this.props.CollectionView.props.Document.sortAscending = false;
         else this.props.CollectionView.props.Document.sortAscending = true;
     }
-    @action resetValue = () => { this._currentKey = this.sectionFilter; };
 
     render() {
         return (
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 430ba582b..33d6b1358 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -113,10 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
     }
 
-    @computed get fieldExtensionDoc() {
-        return Doc.fieldExtensionDoc(this.props.DataDoc || this.props.Document, this.props.fieldKey);
-    }
-
     @action
     onDrop = (e: React.DragEvent): Promise<void> => {
         var pt = this.getTransform().transformPoint(e.pageX, e.pageY);
@@ -309,7 +305,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     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);
+                let ink = Cast(this.extensionDoc.ink, InkField);
                 if (ink && ink.inkData) {
                     ink.inkData.forEach((value: StrokeData, key: string) => {
                         let bounds = InkingCanvas.StrokeRect(value);
@@ -605,9 +601,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     analyzeStrokes = async () => {
-        let data = Cast(this.fieldExtensionDoc.ink, InkField);
+        let data = Cast(this.extensionDoc.ink, InkField);
         if (data) {
-            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.fieldExtensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
+            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
         }
     }
 
@@ -680,7 +676,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
         // if isAnnotationOverlay 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);
         return (
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
@@ -690,7 +685,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
-                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.fieldExtensionDoc} inkFieldKey={"ink"} >
+                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
                             {this.childViews}
                         </InkingCanvas>
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 743055875..4ff70daba 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -247,12 +247,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
 
     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);
+        return Cast(this.props.container.extensionDoc.ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
         let cprops = this.props.container.props;
-        Doc.fieldExtensionDoc(cprops.Document, cprops.fieldKey).ink = value;
+        this.props.container.extensionDoc.ink = value;
     }
 
     @undoBatch
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 689d44a2f..3e5deb55b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,7 +7,6 @@ import { AudioField } from "../../../new_fields/URLField";
 import { DocStaticComponent } from "../DocComponent";
 import { makeInterface } from "../../../new_fields/Schema";
 import { documentSchema } from "./DocumentView";
-import { InkingControl } from "../InkingControl";
 
 type AudioDocument = makeInterface<[typeof documentSchema]>;
 const AudioDocument = makeInterface(documentSchema);
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 30554ea36..fdcedb3a5 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -11,7 +11,6 @@ import { trace, reaction, observable, action, IReactionDisposer } from "mobx";
 import { SelectionManager } from "../../util/SelectionManager";
 import { StrCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { Doc } from "../../../new_fields/Doc";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
@@ -19,8 +18,11 @@ const ColorDocument = makeInterface(documentSchema);
 @observer
 export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) {
     public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+
     _selectedDisposer: IReactionDisposer | undefined;
     _penDisposer: IReactionDisposer | undefined;
+    @observable _startupColor = "black";
+
     componentDidMount() {
         this._selectedDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             action(() => this._startupColor = SelectionManager.SelectedDocuments().length ? StrCast(SelectionManager.SelectedDocuments()[0].Document.backgroundColor, "black") : "black"),
@@ -28,27 +30,12 @@ export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(
         this._penDisposer = reaction(() => CurrentUserUtils.ActivePen,
             action(() => this._startupColor = CurrentUserUtils.ActivePen ? StrCast(CurrentUserUtils.ActivePen.backgroundColor, "black") : "black"),
             { fireImmediately: true });
-
-        // compare to this reaction that used to be in Selection Manager
-        // reaction(() => manager.SelectedDocuments, sel => {
-        //     let targetColor = "#FFFFFF";
-        //     if (sel.length > 0) {
-        //         let firstView = sel[0];
-        //         let doc = firstView.props.Document;
-        //         let targetDoc = doc.isTemplateField ? doc : Doc.GetProto(doc);
-        //         let stored = StrCast(targetDoc.backgroundColor);
-        //         stored.length > 0 && (targetColor = stored);
-        //     }
-        //     InkingControl.Instance.updateSelectedColor(targetColor);
-        // }, { fireImmediately: true });
     }
     componentWillUnmount() {
         this._penDisposer && this._penDisposer();
         this._selectedDisposer && this._selectedDisposer();
     }
 
-    @observable _startupColor = "black";
-
     render() {
         return <div className={`colorBox-container${this.active() ? "-interactive" : ""}`}
             onPointerDown={e => e.button === 0 && !e.ctrlKey && e.stopPropagation()}>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a41a37e66..089ec77ba 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -67,6 +67,7 @@ library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCom
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView>;
     ContainingCollectionDoc: Opt<Doc>;
+    fieldKey: string;
     Document: Doc;
     DataDoc?: Doc;
     fitToBox?: boolean;
@@ -598,6 +599,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
             Document={this.props.Document}
+            fieldKey={this.props.fieldKey}
             fitToBox={this.props.fitToBox}
             addDocument={this.props.addDocument}
             removeDocument={this.props.removeDocument}
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f866e6f7e..020442bf3 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -44,7 +44,6 @@ import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { siteVerification } from 'googleapis/build/src/apis/siteVerification';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -143,10 +142,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
 
-    @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-
-    @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc) : Doc.GetProto(this.props.Document); }
-
     // the document containing the view layout information - will be the Document itself unless the Document has
     // a layout field.  In that case, all layout information comes from there unless overriden by Document
     @computed get layoutDoc(): Doc { return Doc.Layout(this.props.Document); }
@@ -1028,7 +1023,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
         let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground
             ? "none" : "all";
-        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         if (this.props.isSelected()) {
             FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
         }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 426b9cbb4..d80e222c2 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -326,11 +326,11 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     render() {
-        if (!Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey)) return (null);
         return (<div className={"imageBox-container"} onContextMenu={this.specificContextMenu}>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
+                fieldExt={"annotations"}
                 isAnnotationOverlay={true}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5795d6f12..78858731f 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -229,7 +229,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
                     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} focus={this.props.focus}
-                    pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
+                    pinToPres={this.props.pinToPres} addDocument={this.addDocument}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                     isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
                     fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 20383bab1..5e8154233 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -322,7 +322,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         InkingControl.Instance.switchTool(InkTool.None);
         this._isResetClick < 10 && (this.Document.currentTimecode = 0);
     }
-    @computed get fieldExtensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
     @computed get dataDoc() { return this.props.DataDoc && this.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
     @computed get youtubeContent() {
@@ -337,14 +336,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
 
     @action.bound
     addDocumentWithTimestamp(doc: Doc): boolean {
-        Doc.GetProto(doc).annotationOn = this.props.Document;
         var curTime = (this.Document.currentTimecode || -1);
         curTime !== -1 && (doc.displayTimecode = curTime);
-        return Doc.AddDocToList(this.fieldExtensionDoc, this.props.fieldExt, doc);
+        return this.addDocument(doc);
     }
 
     render() {
-        if (!Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey)) return (null);
         return (<div className={"videoBox-container"} onContextMenu={this.specificContextMenu}>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d5a1f4e73..907e71627 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -195,11 +195,11 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             </>);
     }
     render() {
-        Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
         return (<div className={"imageBox-container"} >
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
+                fieldExt={"annotations"}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index f3aaca471..361e58c65 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -46,7 +46,6 @@ interface IViewerProps {
     pdf: Pdfjs.PDFDocumentProxy;
     url: string;
     fieldKey: string;
-    fieldExt: string;
     Document: Doc;
     DataDoc?: Doc;
     ContainingCollectionView: Opt<CollectionView>;
@@ -550,7 +549,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     highlight = (color: string) => {
         // creates annotation documents for current highlights
         let annotationDoc = this.makeAnnotationDocument(color);
-        annotationDoc && Doc.AddDocToList(this.props.extensionDoc, this.props.fieldExt, annotationDoc);
+        annotationDoc && this.props.addDocument && this.props.addDocument(annotationDoc);
         return annotationDoc;
     }
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 71b64d7c3..f60a2e720 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -398,44 +398,6 @@ export namespace Doc {
         return bounds;
     }
 
-    //
-    // 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 fieldExtensionDoc(doc: Doc, fieldKey: string, fieldExt: string = "yes") {
-        return fieldExt && doc[fieldKey + "_ext"] instanceof Doc ? doc[fieldKey + "_ext"] as Doc : doc;
-    }
-
-    export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-        let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
-        docExtensionForField.title = fieldKey + ".ext";
-        docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
-        docExtensionForField.type = DocumentType.EXTENSION;
-        let proto: Doc | undefined = doc;
-        while (proto && !Doc.IsPrototype(proto) && proto.proto) {
-            proto = proto.proto;
-        }
-        (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
-        return docExtensionForField;
-    }
-
-    export function UpdateDocumentExtensionForField(doc: Doc, fieldKey: string, immediate: boolean = false) {
-        let docExtensionForField = doc[fieldKey + "_ext"] as Doc;
-        if (docExtensionForField === undefined) {
-            if (immediate) {
-                CreateDocumentExtensionForField(doc, fieldKey);
-                return true;
-            }
-            else {
-                setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
-                return false;
-            }
-        }
-        return true;
-    }
     export function MakeTitled(title: string) {
         let doc = new Doc();
         doc.title = title;
@@ -490,13 +452,38 @@ export namespace Doc {
         let layoutDoc: Doc | undefined = childDocLayout;
         let resolvedDataDoc = !doc.isTemplateField && dataDoc !== doc && dataDoc ? Doc.GetDataDoc(dataDoc) : undefined;
         if (resolvedDataDoc && Doc.WillExpandTemplateLayout(childDocLayout, resolvedDataDoc)) {
-            Doc.UpdateDocumentExtensionForField(resolvedDataDoc, fieldKey);
-            let fieldExtensionDoc = Doc.fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)), "dummy");
-            layoutDoc = Doc.expandTemplateLayout(childDocLayout, fieldExtensionDoc !== resolvedDataDoc ? fieldExtensionDoc : undefined);
+            let extensionDoc = fieldExtensionDoc(resolvedDataDoc, StrCast(childDocLayout.templateField, StrCast(childDocLayout.title)));
+            layoutDoc = Doc.expandTemplateLayout(childDocLayout, extensionDoc !== resolvedDataDoc ? extensionDoc : undefined);
         } else layoutDoc = childDocLayout;
         return { layout: layoutDoc, data: resolvedDataDoc };
     }
 
+    //
+    // 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 fieldExtensionDoc(doc: Doc, fieldKey: string) {
+        let extension = doc[fieldKey + "_ext"] as Doc;
+        (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
+        return extension ? extension : doc;
+    }
+
+    export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
+        let docExtensionForField = new Doc(doc[Id] + fieldKey, true);
+        docExtensionForField.title = fieldKey + ".ext";
+        docExtensionForField.extendsDoc = doc; // this is used by search to map field matches on the extension doc back to the document it extends.
+        docExtensionForField.type = DocumentType.EXTENSION;
+        let proto: Doc | undefined = doc;
+        while (proto && !Doc.IsPrototype(proto) && proto.proto) {
+            proto = proto.proto;
+        }
+        (proto ? proto : doc)[fieldKey + "_ext"] = new PrefetchProxy(docExtensionForField);
+        return docExtensionForField;
+    }
+
     export function Overwrite(doc: Doc, overwrite: Doc, copyProto: boolean = false): Doc {
         Object.keys(doc).forEach(key => {
             const field = ProxyField.WithoutProxy(() => doc[key]);
-- 
cgit v1.2.3-70-g09d2


From 8efea66fd5723becf36dd6e3b2a95435d8528748 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 21 Oct 2019 16:16:59 -0400
Subject: got rid of fieldExt from layoutstring. set directly by annotated
 fields

---
 src/client/documents/Documents.ts                             | 11 +++++------
 src/client/views/CollectionLinearView.tsx                     |  7 +------
 src/client/views/DocComponent.tsx                             | 11 ++++++-----
 src/client/views/MainView.tsx                                 |  1 -
 src/client/views/collections/CollectionSchemaCells.tsx        |  1 -
 src/client/views/collections/CollectionSubView.tsx            |  3 ++-
 src/client/views/collections/CollectionView.tsx               |  2 +-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx |  7 ++++---
 src/client/views/nodes/DocuLinkBox.tsx                        |  2 +-
 src/client/views/nodes/DocumentView.tsx                       |  2 +-
 src/client/views/nodes/FieldView.tsx                          |  5 ++---
 src/client/views/nodes/FormattedTextBox.tsx                   |  7 +++++--
 src/client/views/nodes/ImageBox.tsx                           |  6 +++---
 src/client/views/nodes/KeyValuePair.tsx                       |  1 -
 src/client/views/nodes/PDFBox.tsx                             |  4 ++--
 src/client/views/nodes/VideoBox.tsx                           |  5 +++--
 src/client/views/nodes/WebBox.tsx                             |  6 +++---
 src/client/views/pdf/PDFViewer.tsx                            |  3 ++-
 src/new_fields/Doc.ts                                         |  2 +-
 19 files changed, 42 insertions(+), 44 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fd5bea264..937d3c058 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -130,7 +130,6 @@ export namespace Docs {
         type TemplateMap = Map<DocumentType, PrototypeTemplate>;
         type PrototypeMap = Map<DocumentType, Doc>;
         const data = "data";
-        const anno = "annotations";
 
         const TemplateMap: TemplateMap = new Map([
             [DocumentType.TEXT, {
@@ -150,11 +149,11 @@ export namespace Docs {
                 options: { nativeWidth: 220, nativeHeight: 300 }
             }],
             [DocumentType.IMG, {
-                layout: { view: ImageBox, ext: anno },
+                layout: { view: ImageBox },
                 options: {}
             }],
             [DocumentType.WEB, {
-                layout: { view: WebBox, ext: anno },
+                layout: { view: WebBox },
                 options: { height: 300 }
             }],
             [DocumentType.COL, {
@@ -166,7 +165,7 @@ export namespace Docs {
                 options: { height: 150 }
             }],
             [DocumentType.VID, {
-                layout: { view: VideoBox, ext: anno },
+                layout: { view: VideoBox },
                 options: { currentTimecode: 0 },
             }],
             [DocumentType.AUDIO, {
@@ -174,7 +173,7 @@ export namespace Docs {
                 options: { height: 32 }
             }],
             [DocumentType.PDF, {
-                layout: { view: PDFBox, ext: anno },
+                layout: { view: PDFBox },
                 options: { nativeWidth: 1200, curPage: 1 }
             }],
             [DocumentType.ICON, {
@@ -286,7 +285,7 @@ export namespace Docs {
             // synthesize the default options, the type and title from computed values and
             // whatever options pertain to this specific prototype
             let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
-            options.layout = layout.view.LayoutString(layout.ext);
+            options.layout = layout.view.LayoutString();
             return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: options.layout });
         }
 
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index e8ef20899..c6602b9cb 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -1,21 +1,16 @@
 import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, HeightSym, WidthSym, DocListCast } from '../../new_fields/Doc';
-import { ObjectField } from '../../new_fields/ObjectField';
+import { Doc, HeightSym, WidthSym } from '../../new_fields/Doc';
 import { makeInterface } from '../../new_fields/Schema';
-import { ScriptField } from '../../new_fields/ScriptField';
 import { BoolCast, NumCast, StrCast } from '../../new_fields/Types';
 import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
-import { Docs } from '../documents/Documents';
 import { DragManager } from '../util/DragManager';
 import { Transform } from '../util/Transform';
 import "./CollectionLinearView.scss";
 import { CollectionViewType } from './collections/CollectionBaseView';
 import { CollectionSubView } from './collections/CollectionSubView';
 import { documentSchema, DocumentView } from './nodes/DocumentView';
-import { translate } from 'googleapis/build/src/apis/translate';
-import { DocumentType } from '../documents/DocumentTypes';
 
 
 type LinearDocument = makeInterface<[typeof documentSchema,]>;
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 1f9bdaac4..2f93d9584 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -55,14 +55,14 @@ interface DocAnnotatableProps {
     Document: Doc;
     DataDoc?: Doc;
     fieldKey: string;
-    fieldExt: string;
     whenActiveChanged: (isActive: boolean) => void;
     isSelected: () => boolean;
     renderDepth: number;
 }
-export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
+export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T, fieldExt: string) {
     class Component extends React.Component<P> {
         _isChildActive = false;
+        _fieldExt = fieldExt;
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
         @computed
         get Document(): T {
@@ -70,13 +70,14 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         }
         @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+        @computed get fieldExt() { return this._fieldExt; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
-            let value = Cast(this.extensionDoc[this.props.fieldExt], listSpec(Doc), []);
+            let value = this.extensionDoc && Cast(this.extensionDoc[this._fieldExt], listSpec(Doc), []);
             let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
-            return index !== -1 && value.splice(index, 1) ? true : false;
+            return index !== -1 && value && value.splice(index, 1) ? true : false;
         }
         // if the moved document is already in this overlay collection nothing needs to be done.
         // otherwise, if the document can be removed from where it was, it will then be added to this document's overlay collection. 
@@ -87,7 +88,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @action.bound
         addDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = this.props.Document;
-            return Doc.AddDocToList(this.extensionDoc, this.props.fieldExt, doc);
+            return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this._fieldExt, doc) ? true : false;
         }
 
         whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 4c2b6f262..68efcb000 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -478,7 +478,6 @@ export class MainView extends React.Component {
                     Document={CurrentUserUtils.UserDocument.expandingButtons}
                     DataDoc={undefined}
                     fieldKey={"data"}
-                    fieldExt={""}
                     select={emptyFunction}
                     chromeCollapsed={true}
                     active={returnFalse}
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 79c032723..54a36f691 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -144,7 +144,6 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             Document: this.props.rowProps.original,
             DataDoc: this.props.rowProps.original,
             fieldKey: this.props.rowProps.column.id as string,
-            fieldExt: "",
             ruleProvider: undefined,
             ContainingCollectionView: this.props.CollectionView,
             ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 43147ed20..7f16fe9e0 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -41,6 +41,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
     ruleProvider: Doc | undefined;
     children?: never | (() => JSX.Element[]) | React.ReactNode;
     isAnnotationOverlay?: boolean;
+    fieldExt: string;
 }
 
 export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@@ -78,7 +79,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         // 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 this.props.fieldExt ? this.extensionDoc[this.props.fieldExt] : this.dataDoc[this.props.fieldKey];
+            return this.props.fieldExt ? (this.extensionDoc ? this.extensionDoc[this.props.fieldExt] : undefined) : this.dataDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs() {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 74a388425..e425ea66a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -32,7 +32,7 @@ library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faF
 @observer
 export class CollectionView extends React.Component<FieldViewProps> {
 
-    public static LayoutString(fieldStr: string = "data", fieldExt: string = "") { return FieldView.LayoutString(CollectionView, fieldStr, fieldExt); }
+    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
 
     private _reactionDisposer: IReactionDisposer | undefined;
     @observable private _isLightboxOpen = false;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 33d6b1358..2e534b6b2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -685,9 +685,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
-                        <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
-                            {this.childViews}
-                        </InkingCanvas>
+                        {!this.extensionDoc ? (null) :
+                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.props.Document} AnnotationDocument={this.extensionDoc} inkFieldKey={"ink"} >
+                                {this.childViews}
+                            </InkingCanvas>}
                         <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index 3294a5aa2..22e44948a 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -18,7 +18,7 @@ const DocLinkDocument = makeInterface(documentSchema);
 
 @observer
 export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(DocLinkDocument) {
-    public static LayoutString(fieldKey: string, fieldExt?: string) { return FieldView.LayoutString(DocuLinkBox, fieldKey, fieldExt); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DocuLinkBox, fieldKey); }
     _downx = 0;
     _downy = 0;
     @observable _x = 0;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 089ec77ba..9e36f0811 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -660,7 +660,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 <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}
+                    hideOnLeave={true} fieldKey={showCaption}
                 />
             </div>);
         const titleView = (!showTitle ? (null) :
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 3b9627efc..7d69b5b51 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -24,7 +24,6 @@ import { ScriptField } from "../../../new_fields/ScriptField";
 //
 export interface FieldViewProps {
     fieldKey: string;
-    fieldExt: string;
     fitToBox?: boolean;
     ContainingCollectionView: Opt<CollectionView>;
     ContainingCollectionDoc: Opt<Doc>;
@@ -53,8 +52,8 @@ export interface FieldViewProps {
 
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldType: { name: string }, fieldStr: string = "data", fieldExt: string = "") {
-        return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} fieldExt={"${fieldExt}"} />`;
+    public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
+        return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`;
         //"<ImageBox {...props} />"
     }
 
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 020442bf3..b12ca60c9 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -727,8 +727,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 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);
+                            const extension = Doc.fieldExtensionDoc(pdfDoc, "data");
+                            if (extension) {
+                                let targetAnnotations = await DocListCastAsync(extension.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({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link");
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d80e222c2..540c1bae8 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -54,8 +54,8 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
 const ImageDocument = makeInterface(pageSchema, documentSchema);
 
 @observer
-export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
-    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(ImageBox, "data", fieldExt); }
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument, "annotations") {
+    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(ImageBox, fieldKey); }
     private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _audioState = 0;
@@ -330,7 +330,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={"annotations"}
+                fieldExt={this.fieldExt}
                 isAnnotationOverlay={true}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 67d33845d..225565964 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -60,7 +60,6 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             ContainingCollectionDoc: undefined,
             ruleProvider: undefined,
             fieldKey: this.props.keyName,
-            fieldExt: "",
             isSelected: returnFalse,
             select: emptyFunction,
             renderDepth: 1,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 78858731f..dab602a29 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -26,8 +26,8 @@ type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, t
 const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
 
 @observer
-export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
-    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument, "annotations") {
+    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(PDFBox, fieldKey); }
     private _keyValue: string = "";
     private _valueValue: string = "";
     private _scriptValue: string = "";
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 5e8154233..64871ef41 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -35,7 +35,7 @@ const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
 library.add(faVideo);
 
 @observer
-export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
+export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument, "annotations") {
     static _youtubeIframeCounter: number = 0;
     private _reactionDisposer?: IReactionDisposer;
     private _youtubeReactionDisposer?: IReactionDisposer;
@@ -49,7 +49,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     @observable _fullScreen = false;
     @observable _playing = false;
     @observable static _showControls: boolean;
-    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(VideoBox, "data", fieldExt); }
+    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(VideoBox, fieldKey); }
 
     public get player(): HTMLVideoElement | null {
         return this._videoRef;
@@ -346,6 +346,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
+                fieldExt={this.fieldExt}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 907e71627..2184325a9 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -28,9 +28,9 @@ type WebDocument = makeInterface<[typeof documentSchema]>;
 const WebDocument = makeInterface(documentSchema);
 
 @observer
-export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
+export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument, "annotations") {
 
-    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(WebBox, "data", fieldExt); }
+    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(WebBox, fieldKey); }
     @observable private collapsed: boolean = true;
     @observable private url: string = "";
 
@@ -199,7 +199,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={"annotations"}
+                fieldExt={this.fieldExt}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 361e58c65..5806dec83 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -73,7 +73,7 @@ interface IViewerProps {
  * Handles rendering and virtualization of the pdf
  */
 @observer
-export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) {
+export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument, "annotations") {
     static _annotationStyle: any = addStyleSheet();
     @observable private _pageSizes: { width: number, height: number }[] = [];
     @observable private _annotations: Doc[] = [];
@@ -632,6 +632,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
                 <Annotation {...this.props} focus={this.props.focus} anno={anno} key={`${anno[Id]}-annotation`} />)}
             <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
                 <CollectionFreeFormView {...this.props}
+                    fieldExt={this.fieldExt}
                     setPreviewCursor={this.setPreviewCursor}
                     PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)}
                     PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index f60a2e720..c6f654c33 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -468,7 +468,7 @@ export namespace Doc {
     export function fieldExtensionDoc(doc: Doc, fieldKey: string) {
         let extension = doc[fieldKey + "_ext"] as Doc;
         (extension === undefined) && setTimeout(() => CreateDocumentExtensionForField(doc, fieldKey), 0);
-        return extension ? extension : doc;
+        return extension ? extension : undefined;
     }
 
     export function CreateDocumentExtensionForField(doc: Doc, fieldKey: string) {
-- 
cgit v1.2.3-70-g09d2


From a27db333f528f7381fda591094aebba684a80639 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 21 Oct 2019 16:38:33 -0400
Subject: more fixes after removed fieldExt

---
 src/client/views/CollectionLinearView.tsx          |  1 +
 src/client/views/MainView.tsx                      |  4 ++++
 .../views/collections/CollectionBaseView.tsx       |  2 +-
 src/client/views/collections/CollectionView.tsx    | 18 +++++++--------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++++----
 .../collections/collectionFreeForm/MarqueeView.tsx |  5 ++--
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 src/client/views/nodes/ImageBox.tsx                | 27 ++++++++++++----------
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 9 files changed, 39 insertions(+), 31 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index c6602b9cb..4e03b8c95 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -75,6 +75,7 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                             <DocumentView
                                 Document={pair.layout}
                                 DataDoc={pair.data}
+                                fieldKey={this.props.fieldKey}
                                 addDocument={this.props.addDocument}
                                 moveDocument={this.props.moveDocument}
                                 addDocTab={this.props.addDocTab}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 68efcb000..d4b92a110 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -270,6 +270,7 @@ export class MainView extends React.Component {
                         <DocumentView Document={mainContainer}
                             DataDoc={undefined}
                             addDocument={undefined}
+                            fieldKey={"data"}
                             addDocTab={this.addDocTabFunc}
                             pinToPres={emptyFunction}
                             onClick={undefined}
@@ -359,6 +360,7 @@ export class MainView extends React.Component {
                 <DocumentView
                     Document={sidebarButtonsDoc}
                     DataDoc={undefined}
+                    fieldKey={"data"}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
                     pinToPres={emptyFunction}
@@ -385,6 +387,7 @@ export class MainView extends React.Component {
                 <DocumentView
                     Document={sidebarContent}
                     DataDoc={undefined}
+                    fieldKey={"data"}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
                     pinToPres={emptyFunction}
@@ -478,6 +481,7 @@ export class MainView extends React.Component {
                     Document={CurrentUserUtils.UserDocument.expandingButtons}
                     DataDoc={undefined}
                     fieldKey={"data"}
+                    fieldExt={""}
                     select={emptyFunction}
                     chromeCollapsed={true}
                     active={returnFalse}
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index fa543cc01..9f3d33e6f 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -100,7 +100,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         let targetDataDoc = Doc.GetProto(this.props.Document);
         let targetField = this.props.fieldKey;
         Doc.AddDocToList(targetDataDoc, targetField, doc);
-        let extension = Doc.fieldExtensionDoc(targetDataDoc, targetField);
+        let extension = Doc.fieldExtensionDoc(targetDataDoc, targetField);  // set metadata about the field being rendered (ie, the set of documents) on an extension field for that field
         extension && (extension.lastModified = new DateField(new Date(Date.now())));
         Doc.GetProto(doc).lastOpened = new DateField;
         return true;
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index e425ea66a..f3b0b17f8 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -63,18 +63,18 @@ export class CollectionView extends React.Component<FieldViewProps> {
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         let props = { ...this.props, ...renderProps };
         switch (type) {
-            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
             // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
-            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
-            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
-            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
-            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
-            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
-            case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />); }
+            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
+            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
+            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
+            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
+            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
+            case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
             case CollectionViewType.Freeform:
             default:
                 this.props.Document.freeformLayoutEngine = undefined;
-                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} />);
+                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
         }
         return (null);
     }
@@ -149,7 +149,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
                 {this.SubView}
             </CollectionBaseView>
 
-            {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField)!.url.href))}
+            {this.lightbox(DocListCast(this.props.Document[this.props.fieldKey]).filter(d => d.type === DocumentType.IMG).map(d => Cast(d.data, ImageField) ? Cast(d.data, ImageField)!.url.href : ""))}
         </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2e534b6b2..cc89dc2d4 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -305,7 +305,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     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.extensionDoc.ink, InkField);
+                let ink = this.extensionDoc && Cast(this.extensionDoc.ink, InkField);
                 if (ink && ink.inkData) {
                     ink.inkData.forEach((value: StrokeData, key: string) => {
                         let bounds = InkingCanvas.StrokeRect(value);
@@ -601,9 +601,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     }
 
     analyzeStrokes = async () => {
-        let data = Cast(this.extensionDoc.ink, InkField);
-        if (data) {
-            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
+        const extensionDoc = this.extensionDoc;
+        let data = extensionDoc && Cast(extensionDoc.ink, InkField);
+        if (data && extensionDoc) {
+            CognitiveServices.Inking.Appliers.ConcatenateHandwriting(extensionDoc, ["inkAnalysis", "handwriting"], data.inkData);
         }
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 4ff70daba..3eaad3ad7 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -246,13 +246,12 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     }
 
     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(this.props.container.extensionDoc.ink, InkField);
+        return this.props.container.extensionDoc && Cast(this.props.container.extensionDoc.ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
         let cprops = this.props.container.props;
-        this.props.container.extensionDoc.ink = value;
+        this.props.container.extensionDoc && (this.props.container.extensionDoc.ink = value);
     }
 
     @undoBatch
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index b12ca60c9..995fcee17 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -518,7 +518,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         this._textReactionDisposer = reaction(
             () => this.extensionDoc,
             () => {
-                if (this.dataDoc.text || this.dataDoc.lastModified) {
+                if (this.extensionDoc && (this.dataDoc.text || this.dataDoc.lastModified)) {
                     this.extensionDoc.text = this.dataDoc.text;
                     this.extensionDoc.lastModified = DateCast(this.dataDoc.lastModified)[Copy]();
                     this.dataDoc.text = undefined;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 540c1bae8..4d623a04f 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -75,7 +75,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 e.stopPropagation();
             }
             de.mods === "MetaKey" && de.data.droppedDocuments.forEach(action((drop: Doc) => {
-                Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
+                this.extensionDoc && Doc.AddDocToList(Doc.GetProto(this.extensionDoc), "Alternates", drop);
                 e.stopPropagation();
             }));
         }
@@ -85,7 +85,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         let gumStream: any;
         let recorder: any;
         let self = this;
-        navigator.mediaDevices.getUserMedia({
+        const extensionDoc = this.extensionDoc;
+        extensionDoc && navigator.mediaDevices.getUserMedia({
             audio: true
         }).then(function (stream) {
             gumStream = stream;
@@ -102,9 +103,9 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 // upload to server with known URL 
                 let audioDoc = Docs.Create.AudioDocument(url, { title: "audio test", width: 200, height: 32 });
                 audioDoc.treeViewExpandedView = "layout";
-                let audioAnnos = Cast(self.extensionDoc.audioAnnotations, listSpec(Doc));
+                let audioAnnos = Cast(extensionDoc.audioAnnotations, listSpec(Doc));
                 if (audioAnnos === undefined) {
-                    self.extensionDoc.audioAnnotations = new List([audioDoc]);
+                    extensionDoc.audioAnnotations = new List([audioDoc]);
                 } else {
                     audioAnnos.push(audioDoc);
                 }
@@ -155,7 +156,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             results.reduce((face: CognitiveServices.Image.Face, faceDocs: List<Doc>) => faceDocs.push(Docs.Get.DocumentHierarchyFromJson(face, `Face: ${face.faceId}`)!), new List<Doc>());
             return faceDocs;
         };
-        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
+        this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["faces"], this.url, Service.Face, converter);
     }
 
     generateMetadata = (threshold: Confidence = Confidence.Excellent) => {
@@ -167,12 +168,12 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                 let sanitized = tag.name.replace(" ", "_");
                 tagDoc[sanitized] = ComputedField.MakeFunction(`(${tag.confidence} >= this.confidence) ? ${tag.confidence} : "${ComputedField.undefined}"`);
             });
-            this.extensionDoc.generatedTags = tagsList;
+            this.extensionDoc && (this.extensionDoc.generatedTags = tagsList);
             tagDoc.title = "Generated Tags Doc";
             tagDoc.confidence = threshold;
             return tagDoc;
         };
-        this.url && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
+        this.url && this.extensionDoc && CognitiveServices.Image.Appliers.ProcessImage(this.extensionDoc, ["generatedTagsDoc"], this.url, Service.ComputerVision, converter);
     }
 
     @computed private get url() {
@@ -230,8 +231,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     @action
     onPointerEnter = () => {
         let self = this;
-        let audioAnnos = DocListCast(this.extensionDoc.audioAnnotations);
-        if (audioAnnos.length && this._audioState === 0) {
+        let audioAnnos = this.extensionDoc && DocListCast(this.extensionDoc.audioAnnotations);
+        if (audioAnnos && audioAnnos.length && this._audioState === 0) {
             let anno = audioAnnos[Math.floor(Math.random() * audioAnnos.length)];
             anno.data instanceof AudioField && new Howl({
                 src: [anno.data.url.href],
@@ -264,6 +265,8 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     @computed get content() {
+        const extensionDoc = this.extensionDoc;
+        if (!extensionDoc) return (null);
         // let transform = this.props.ScreenToLocalTransform().inverse();
         let pw = typeof this.props.PanelWidth === "function" ? this.props.PanelWidth() : typeof this.props.PanelWidth === "number" ? (this.props.PanelWidth as any) as number : 50;
         // var [sptX, sptY] = transform.transformPoint(0, 0);
@@ -275,7 +278,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         let paths = [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
         // this._curSuffix = "";
         // if (w > 20) {
-        let alts = DocListCast(this.extensionDoc.Alternates);
+        let alts = DocListCast(extensionDoc.Alternates);
         let altpaths = alts.filter(doc => doc.data instanceof ImageField).map(doc => this.choosePath((doc.data as ImageField).url));
         let field = this.dataDoc[this.props.fieldKey];
         // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
@@ -318,10 +321,10 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
                     style={{ height: `calc(${.1 * nativeHeight / nativeWidth * 100}%)` }}
                 >
                     <FontAwesomeIcon className="imageBox-audioFont"
-                        style={{ color: [DocListCast(this.extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
+                        style={{ color: [DocListCast(extensionDoc.audioAnnotations).length ? "blue" : "gray", "green", "red"][this._audioState] }} icon={faFileAudio} size="sm" />
                 </div>
                 {this.considerGooglePhotosLink()}
-                <FaceRectangles document={this.extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
+                <FaceRectangles document={extensionDoc} color={"#0000FF"} backgroundColor={"#0000FF"} />
             </div>);
     }
 
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index dab602a29..b1110daf4 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -207,7 +207,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf;
         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) ?
+        return (!this.extensionDoc || 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>
-- 
cgit v1.2.3-70-g09d2


From 891b9706ddabc0a73ea6b25dc504297d6efb90fe Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 21 Oct 2019 22:03:02 -0400
Subject: big cleanup of layoutStrings, fieldExt, fieldKey, etc

---
 src/client/apis/youtube/YoutubeBox.tsx             |   2 +-
 src/client/documents/Documents.ts                  |  55 ++++----
 src/client/northstar/dash-nodes/HistogramBox.tsx   |   2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   2 +-
 src/client/views/CollectionLinearView.tsx          |   4 +-
 src/client/views/DocComponent.tsx                  |  45 +++----
 src/client/views/DocumentDecorations.tsx           |  13 +-
 src/client/views/MainView.tsx                      |   5 +-
 .../views/collections/CollectionSchemaView.tsx     |   1 -
 .../views/collections/CollectionStackingView.tsx   |   2 -
 src/client/views/collections/CollectionSubView.tsx |   9 +-
 src/client/views/collections/CollectionView.tsx    |  18 +--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  15 +--
 .../collections/collectionFreeForm/MarqueeView.tsx |  24 ++--
 src/client/views/linking/LinkFollowBox.tsx         |   2 +-
 src/client/views/nodes/AudioBox.tsx                |   8 +-
 src/client/views/nodes/ButtonBox.tsx               |  20 +--
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  18 +--
 src/client/views/nodes/ColorBox.tsx                |  10 +-
 src/client/views/nodes/DocuLinkBox.tsx             |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  71 ++--------
 src/client/views/nodes/FieldView.tsx               |   5 +-
 src/client/views/nodes/FontIconBox.tsx             |   2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  27 ++--
 src/client/views/nodes/IconBox.tsx                 |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  18 +--
 src/client/views/nodes/KeyValueBox.tsx             |   5 +-
 src/client/views/nodes/PDFBox.tsx                  |   8 +-
 src/client/views/nodes/PresBox.tsx                 |   2 +-
 src/client/views/nodes/QueryBox.tsx                |   2 +-
 src/client/views/nodes/VideoBox.tsx                |  40 +++---
 src/client/views/nodes/WebBox.tsx                  |   9 +-
 src/client/views/pdf/PDFViewer.tsx                 |  17 ++-
 .../views/presentationview/PresElementBox.tsx      | 148 ++++++++++-----------
 src/new_fields/Doc.ts                              |   3 +-
 src/new_fields/documentSchemas.ts                  |  51 +++++++
 36 files changed, 319 insertions(+), 348 deletions(-)
 create mode 100644 src/new_fields/documentSchemas.ts

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/apis/youtube/YoutubeBox.tsx b/src/client/apis/youtube/YoutubeBox.tsx
index d73988bb8..bed812852 100644
--- a/src/client/apis/youtube/YoutubeBox.tsx
+++ b/src/client/apis/youtube/YoutubeBox.tsx
@@ -40,7 +40,7 @@ export class YoutubeBox extends React.Component<FieldViewProps> {
     @observable curVideoTemplates: VideoTemplate[] = [];
 
 
-    public static LayoutString() { return FieldView.LayoutString(YoutubeBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(YoutubeBox, fieldKey); }
 
     /**
      * When component mounts, last search's results are laoded in based on the back up stored
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 937d3c058..a400e68a3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -84,7 +84,8 @@ export interface DocumentOptions {
     columnWidth?: number;
     fontSize?: number;
     curPage?: number;
-    currentTimecode?: number;
+    currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)
+    displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
     documentText?: string;
     borderRounding?: string;
     boxShadow?: string;
@@ -119,11 +120,11 @@ export namespace Docs {
 
     export namespace Prototypes {
 
-        type LayoutSource = { LayoutString: (ext?: string) => string };
+        type LayoutSource = { LayoutString: (key: string) => string };
         type PrototypeTemplate = {
             layout: {
                 view: LayoutSource,
-                ext?: string, // optional extension field for layout source
+                dataField: string
             },
             options?: Partial<DocumentOptions>
         };
@@ -133,80 +134,80 @@ export namespace Docs {
 
         const TemplateMap: TemplateMap = new Map([
             [DocumentType.TEXT, {
-                layout: { view: FormattedTextBox },
+                layout: { view: FormattedTextBox, dataField: data },
                 options: { height: 150, backgroundColor: "#f1efeb", defaultBackgroundColor: "#f1efeb" }
             }],
             [DocumentType.HIST, {
-                layout: { view: HistogramBox },
+                layout: { view: HistogramBox, dataField: data },
                 options: { height: 300, backgroundColor: "black" }
             }],
             [DocumentType.QUERY, {
-                layout: { view: QueryBox },
+                layout: { view: QueryBox, dataField: data },
                 options: { width: 400 }
             }],
             [DocumentType.COLOR, {
-                layout: { view: ColorBox },
+                layout: { view: ColorBox, dataField: data },
                 options: { nativeWidth: 220, nativeHeight: 300 }
             }],
             [DocumentType.IMG, {
-                layout: { view: ImageBox },
+                layout: { view: ImageBox, dataField: data },
                 options: {}
             }],
             [DocumentType.WEB, {
-                layout: { view: WebBox },
+                layout: { view: WebBox, dataField: data },
                 options: { height: 300 }
             }],
             [DocumentType.COL, {
-                layout: { view: CollectionView },
+                layout: { view: CollectionView, dataField: data },
                 options: { panX: 0, panY: 0, scale: 1, width: 500, height: 500 }
             }],
             [DocumentType.KVP, {
-                layout: { view: KeyValueBox },
+                layout: { view: KeyValueBox, dataField: data },
                 options: { height: 150 }
             }],
             [DocumentType.VID, {
-                layout: { view: VideoBox },
+                layout: { view: VideoBox, dataField: data },
                 options: { currentTimecode: 0 },
             }],
             [DocumentType.AUDIO, {
-                layout: { view: AudioBox },
+                layout: { view: AudioBox, dataField: data },
                 options: { height: 32 }
             }],
             [DocumentType.PDF, {
-                layout: { view: PDFBox },
+                layout: { view: PDFBox, dataField: data },
                 options: { nativeWidth: 1200, curPage: 1 }
             }],
             [DocumentType.ICON, {
-                layout: { view: IconBox },
+                layout: { view: IconBox, dataField: data },
                 options: { width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) },
             }],
             [DocumentType.IMPORT, {
-                layout: { view: DirectoryImportBox },
+                layout: { view: DirectoryImportBox, dataField: data },
                 options: { height: 150 }
             }],
             [DocumentType.LINKDOC, {
                 data: new List<Doc>(),
-                layout: { view: EmptyBox },
+                layout: { view: EmptyBox, dataField: data },
             }],
             [DocumentType.YOUTUBE, {
-                layout: { view: YoutubeBox }
+                layout: { view: YoutubeBox, dataField: data }
             }],
             [DocumentType.BUTTON, {
-                layout: { view: ButtonBox },
+                layout: { view: ButtonBox, dataField: data },
             }],
             [DocumentType.PRES, {
-                layout: { view: PresBox },
+                layout: { view: PresBox, dataField: data },
                 options: {}
             }],
             [DocumentType.FONTICON, {
-                layout: { view: FontIconBox },
+                layout: { view: FontIconBox, dataField: data },
                 options: { width: 40, height: 40, borderRounding: "100%" },
             }],
             [DocumentType.LINKFOLLOW, {
-                layout: { view: LinkFollowBox }
+                layout: { view: LinkFollowBox, dataField: data }
             }],
             [DocumentType.PRESELEMENT, {
-                layout: { view: PresElementBox }
+                layout: { view: PresElementBox, dataField: data }
             }],
         ]);
 
@@ -285,7 +286,7 @@ export namespace Docs {
             // synthesize the default options, the type and title from computed values and
             // whatever options pertain to this specific prototype
             let options = { title, type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
-            options.layout = layout.view.LayoutString();
+            options.layout = layout.view.LayoutString(layout.dataField);
             return Doc.assign(new Doc(prototypeId, true), { ...options, baseLayout: options.layout });
         }
 
@@ -701,12 +702,12 @@ export namespace DocUtils {
             linkDocProto.linkDescription = description;
 
             linkDocProto.anchor1 = source.doc;
-            linkDocProto.anchor1Context = source.ctx;
-            linkDocProto.anchor1Timecode = source.doc.currentTimecode;
-            linkDocProto.anchor1Groups = new List<Doc>([]);
             linkDocProto.anchor2 = target.doc;
+            linkDocProto.anchor1Context = source.ctx;
             linkDocProto.anchor2Context = target.ctx;
+            linkDocProto.anchor1Groups = new List<Doc>([]);
             linkDocProto.anchor2Groups = new List<Doc>([]);
+            linkDocProto.anchor1Timecode = source.doc.currentTimecode;
             linkDocProto.anchor2Timecode = target.doc.currentTimecode;
             linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
             linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
diff --git a/src/client/northstar/dash-nodes/HistogramBox.tsx b/src/client/northstar/dash-nodes/HistogramBox.tsx
index b81eafbee..854135648 100644
--- a/src/client/northstar/dash-nodes/HistogramBox.tsx
+++ b/src/client/northstar/dash-nodes/HistogramBox.tsx
@@ -24,7 +24,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 
 @observer
 export class HistogramBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(HistogramBox, fieldStr); }
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(HistogramBox, fieldStr); }
     private _dropXRef = React.createRef<HTMLDivElement>();
     private _dropYRef = React.createRef<HTMLDivElement>();
     private _dropXDisposer?: DragManager.DragDropDisposer;
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d74b51993..f27d05487 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -50,7 +50,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     @observable private uploading = false;
     @observable private removeHover = false;
 
-    public static LayoutString() { return FieldView.LayoutString(DirectoryImportBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
 
     constructor(props: FieldViewProps) {
         super(props);
diff --git a/src/client/views/CollectionLinearView.tsx b/src/client/views/CollectionLinearView.tsx
index 4e03b8c95..1f28ef35d 100644
--- a/src/client/views/CollectionLinearView.tsx
+++ b/src/client/views/CollectionLinearView.tsx
@@ -10,7 +10,8 @@ import { Transform } from '../util/Transform';
 import "./CollectionLinearView.scss";
 import { CollectionViewType } from './collections/CollectionBaseView';
 import { CollectionSubView } from './collections/CollectionSubView';
-import { documentSchema, DocumentView } from './nodes/DocumentView';
+import { DocumentView } from './nodes/DocumentView';
+import { documentSchema } from '../../new_fields/documentSchemas';
 
 
 type LinearDocument = makeInterface<[typeof documentSchema,]>;
@@ -75,7 +76,6 @@ export class CollectionLinearView extends CollectionSubView(LinearDocument) {
                             <DocumentView
                                 Document={pair.layout}
                                 DataDoc={pair.data}
-                                fieldKey={this.props.fieldKey}
                                 addDocument={this.props.addDocument}
                                 moveDocument={this.props.moveDocument}
                                 addDocTab={this.props.addDocTab}
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 2f93d9584..ae4b7cf3a 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -1,48 +1,40 @@
 import * as React from 'react';
 import { Doc } from '../../new_fields/Doc';
 import { computed, action } from 'mobx';
-import { Cast, BoolCast } from '../../new_fields/Types';
+import { Cast } from '../../new_fields/Types';
 import { listSpec } from '../../new_fields/Schema';
 import { InkingControl } from './InkingControl';
 import { InkTool } from '../../new_fields/InkField';
+import { PositionDocument } from '../../new_fields/documentSchemas';
 
 
-///  DocComponents returns a generic base class for React views of document fields that are not interactive
+///  DocComponent returns a generic React base class used by views that don't have any data extensions (e.g.,CollectionFreeFormDocumentView, DocumentView, ButtonBox)
 interface DocComponentProps {
     Document: Doc;
-    DataDoc?: Doc;
-    fieldKey: string;
 }
 export function DocComponent<P extends DocComponentProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed
-        get Document(): T {
-            return schemaCtor(this.props.Document);
-        }
-        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
-        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+        @computed get Document(): T { return schemaCtor(this.props.Document); }
+        @computed get layoutDoc() { return PositionDocument(Doc.Layout(this.props.Document)); }
     }
     return Component;
 }
 
-
-///  DocStaticProps return a base class for React views of document fields that are interactive only when selected (e.g. ColorBox)
-interface DocStaticProps {
+///  DocStaticProps return a base class for React document views that have data extensions but aren't annotatable (e.g. AudioBox, FormattedTextBox)
+interface DocExtendableProps {
     Document: Doc;
     DataDoc?: Doc;
     fieldKey: string;
     isSelected: () => boolean;
     renderDepth: number;
 }
-export function DocStaticComponent<P extends DocStaticProps, T>(schemaCtor: (doc: Doc) => T) {
+export function DocExtendableComponent<P extends DocExtendableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed
-        get Document(): T {
-            return schemaCtor(this.props.Document);
-        }
-        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get Document(): T { return schemaCtor(this.props.Document); }
+        @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
+        @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
         active = () => !this.props.Document.isBackground && (this.props.Document.forceActive || this.props.isSelected() || this.props.renderDepth === 0);//  && !InkingControl.Instance.selectedTool;  // bcz: inking state shouldn't affect static tools 
     }
@@ -59,23 +51,20 @@ interface DocAnnotatableProps {
     isSelected: () => boolean;
     renderDepth: number;
 }
-export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T, fieldExt: string) {
+export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schemaCtor: (doc: Doc) => T) {
     class Component extends React.Component<P> {
         _isChildActive = false;
-        _fieldExt = fieldExt;
         //TODO This might be pretty inefficient if doc isn't observed, because computed doesn't cache then
-        @computed
-        get Document(): T {
-            return schemaCtor(this.props.Document);
-        }
+        @computed get Document(): T { return schemaCtor(this.props.Document); }
+        @computed get layoutDoc() { return Doc.Layout(this.props.Document); }
         @computed get dataDoc() { return (this.props.DataDoc && this.props.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document)) as Doc; }
         @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
-        @computed get fieldExt() { return this._fieldExt; }
+        @computed get annotationsKey() { return "annotations"; }
 
         @action.bound
         removeDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = undefined;
-            let value = this.extensionDoc && Cast(this.extensionDoc[this._fieldExt], listSpec(Doc), []);
+            let value = this.extensionDoc && Cast(this.extensionDoc[this.annotationsKey], listSpec(Doc), []);
             let index = value ? Doc.IndexOf(doc, value.map(d => d as Doc), true) : -1;
             return index !== -1 && value && value.splice(index, 1) ? true : false;
         }
@@ -88,7 +77,7 @@ export function DocAnnotatableComponent<P extends DocAnnotatableProps, T>(schema
         @action.bound
         addDocument(doc: Doc): boolean {
             Doc.GetProto(doc).annotationOn = this.props.Document;
-            return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this._fieldExt, doc) ? true : false;
+            return this.extensionDoc && Doc.AddDocToList(this.extensionDoc, this.annotationsKey, doc) ? true : false;
         }
 
         whenActiveChanged = (isActive: boolean) => this.props.whenActiveChanged(this._isChildActive = isActive);
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 252f90d46..b46caf3ea 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -4,27 +4,26 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, observable, reaction, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCastAsync } from "../../new_fields/Doc";
+import { PositionDocument } from '../../new_fields/documentSchemas';
 import { List } from "../../new_fields/List";
 import { ObjectField } from '../../new_fields/ObjectField';
-import { BoolCast, Cast, NumCast, StrCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { emptyFunction, Utils } from "../../Utils";
+import { Utils } from "../../Utils";
 import { Docs, DocUtils } from "../documents/Documents";
 import { DocumentManager } from "../util/DocumentManager";
 import { DragManager } from "../util/DragManager";
 import { SelectionManager } from "../util/SelectionManager";
+import { TooltipTextMenu } from '../util/TooltipTextMenu';
 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 { PositionDocument } from './nodes/CollectionFreeFormDocumentView';
 import { DocumentView } from "./nodes/DocumentView";
 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;
@@ -281,7 +280,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             let selectedDocs = SelectionManager.SelectedDocuments().map(sd => sd);
 
             if (selectedDocs.length > 1) {
-                this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString());
+                this._iconDoc = this._iconDoc ? this._iconDoc : this.createIcon(SelectionManager.SelectedDocuments(), CollectionView.LayoutString(""));
                 this.moveIconDoc(this._iconDoc);
             } else {
                 this.getIconDoc(selectedDocs[0]).then(icon => icon && this.moveIconDoc(this._iconDoc = icon));
@@ -339,7 +338,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let iconDoc: Doc | undefined = await Cast(doc.minimizedDoc, Doc);
 
         if (!iconDoc || !DocumentManager.Instance.getDocumentView(iconDoc)) {
-            const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView));
+            const layout = StrCast(doc.layout, FieldView.LayoutString(DocumentView, ""));
             iconDoc = this.createIcon([docView], layout);
         }
         return iconDoc;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d4b92a110..55a61f098 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -270,7 +270,6 @@ export class MainView extends React.Component {
                         <DocumentView Document={mainContainer}
                             DataDoc={undefined}
                             addDocument={undefined}
-                            fieldKey={"data"}
                             addDocTab={this.addDocTabFunc}
                             pinToPres={emptyFunction}
                             onClick={undefined}
@@ -360,7 +359,6 @@ export class MainView extends React.Component {
                 <DocumentView
                     Document={sidebarButtonsDoc}
                     DataDoc={undefined}
-                    fieldKey={"data"}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
                     pinToPres={emptyFunction}
@@ -387,7 +385,6 @@ export class MainView extends React.Component {
                 <DocumentView
                     Document={sidebarContent}
                     DataDoc={undefined}
-                    fieldKey={"data"}
                     addDocument={undefined}
                     addDocTab={this.addDocTabFunc}
                     pinToPres={emptyFunction}
@@ -481,7 +478,7 @@ export class MainView extends React.Component {
                     Document={CurrentUserUtils.UserDocument.expandingButtons}
                     DataDoc={undefined}
                     fieldKey={"data"}
-                    fieldExt={""}
+                    annotationsKey={""}
                     select={emptyFunction}
                     chromeCollapsed={true}
                     active={returnFalse}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 04bc550ea..34f642f80 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -994,7 +994,6 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                     <DocumentView {...this.props}
                         DataDoc={this.props.DataDocument}
                         Document={this.props.Document}
-                        fieldKey={this.props.fieldKey}
                         fitToBox={this.props.fitToBox}
                         onClick={this.props.onClick}
                         ruleProvider={this.props.ruleProvider}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 5fb0c0f9f..7af56500e 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -75,8 +75,6 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
         this._heightMap.set(key, sectionHeight);
     }
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
-
     get Sections() {
         if (!this.sectionFilter || this.sectionHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>();
 
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7f16fe9e0..55365de8c 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -41,7 +41,7 @@ export interface SubCollectionViewProps extends CollectionViewProps {
     ruleProvider: Doc | undefined;
     children?: never | (() => JSX.Element[]) | React.ReactNode;
     isAnnotationOverlay?: boolean;
-    fieldExt: string;
+    annotationsKey: string;
 }
 
 export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
@@ -74,12 +74,15 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             this._childLayoutDisposer && this._childLayoutDisposer();
         }
 
+        @computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplateField ? Doc.GetProto(this.props.DataDoc!) : Doc.GetProto(this.props.Document); }
+        @computed get extensionDoc() { return Doc.fieldExtensionDoc(this.dataDoc, this.props.fieldKey); }
+
         // The data field for rendering 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.
+        // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
-            return this.props.fieldExt ? (this.extensionDoc ? this.extensionDoc[this.props.fieldExt] : undefined) : this.dataDoc[this.props.fieldKey];
+            return this.props.annotationsKey ? (this.extensionDoc ? this.extensionDoc[this.props.annotationsKey] : undefined) : this.dataDoc[this.props.fieldKey];
         }
 
         get childLayoutPairs() {
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index f3b0b17f8..f8eb28ade 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -32,7 +32,7 @@ library.add(faTh, faTree, faSquare, faProjectDiagram, faSignature, faThList, faF
 @observer
 export class CollectionView extends React.Component<FieldViewProps> {
 
-    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(CollectionView, fieldStr); }
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(CollectionView, fieldStr); }
 
     private _reactionDisposer: IReactionDisposer | undefined;
     @observable private _isLightboxOpen = false;
@@ -63,18 +63,18 @@ export class CollectionView extends React.Component<FieldViewProps> {
     private SubViewHelper = (type: CollectionViewType, renderProps: CollectionRenderProps) => {
         let props = { ...this.props, ...renderProps };
         switch (type) {
-            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
+            case CollectionViewType.Schema: return (<CollectionSchemaView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
             // currently cant think of a reason for collection docking view to have a chrome. mind may change if we ever have nested docking views -syip
-            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
-            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
-            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
-            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
-            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
-            case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />); }
+            case CollectionViewType.Docking: return (<CollectionDockingView chromeCollapsed={true} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
+            case CollectionViewType.Tree: return (<CollectionTreeView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
+            case CollectionViewType.Stacking: { this.props.Document.singleColumn = true; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
+            case CollectionViewType.Masonry: { this.props.Document.singleColumn = false; return (<CollectionStackingView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
+            case CollectionViewType.Pivot: { this.props.Document.freeformLayoutEngine = "pivot"; return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
+            case CollectionViewType.Linear: { return (<CollectionLinearView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />); }
             case CollectionViewType.Freeform:
             default:
                 this.props.Document.freeformLayoutEngine = undefined;
-                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} fieldExt={""} />);
+                return (<CollectionFreeFormView chromeCollapsed={this._collapsed} key="collview" {...props} ChromeHeight={this.chromeHeight} CollectionView={this} annotationsKey={""} />);
         }
         return (null);
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index cc89dc2d4..0419bc3fa 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -25,8 +25,8 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss"
 import { ContextMenu } from "../../ContextMenu";
 import { ContextMenuProps } from "../../ContextMenuItem";
 import { InkingCanvas } from "../../InkingCanvas";
-import { CollectionFreeFormDocumentView, positionSchema } from "../../nodes/CollectionFreeFormDocumentView";
-import { documentSchema, DocumentViewProps } from "../../nodes/DocumentView";
+import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
+import { DocumentViewProps } from "../../nodes/DocumentView";
 import { FormattedTextBox } from "../../nodes/FormattedTextBox";
 import { pageSchema } from "../../nodes/ImageBox";
 import { CollectionSubView } from "../CollectionSubView";
@@ -35,6 +35,7 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
 import "./CollectionFreeFormView.scss";
 import { MarqueeView } from "./MarqueeView";
 import React = require("react");
+import { documentSchema, positionSchema } from "../../DocComponent";
 
 library.add(faEye as any, faTable, faPaintBrush, faExpandArrowsAlt, faCompressArrowsAlt, faCompass, faUpload, faBraille, faChalkboard, faFileUpload);
 
@@ -677,13 +678,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         this.Document.fitH = this.contentBounds && (this.contentBounds.b - this.contentBounds.y);
         // if isAnnotationOverlay 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
-        return (
+        return !this.extensionDoc ? (null) :
             <div className={"collectionfreeformview-container"} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? (this.props.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight() }}
                 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} setPreviewCursor={this.props.setPreviewCursor}
-                    getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
+                <MarqueeView {...this.props} extensionDoc={this.extensionDoc} activeDocuments={this.getActiveDocuments} selectDocuments={this.selectDocuments} addDocument={this.addDocument}
+                    addLiveTextDocument={this.addLiveTextBox} getContainerTransform={this.getContainerTransform} getTransform={this.getTransform} isAnnotationOverlay={this.isAnnotationOverlay}>
                     <CollectionFreeFormViewPannableContents centeringShiftX={this.centeringShiftX} centeringShiftY={this.centeringShiftY}
                         easing={this.easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
                         {!this.extensionDoc ? (null) :
@@ -694,8 +694,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     </CollectionFreeFormViewPannableContents>
                 </MarqueeView>
                 {this.overlayViews}
-            </div>
-        );
+            </div>;
     }
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3eaad3ad7..e0bf4a2f0 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,23 +19,24 @@ import { CollectionViewType } from "../CollectionBaseView";
 import { CollectionFreeFormView } from "./CollectionFreeFormView";
 import "./MarqueeView.scss";
 import React = require("react");
+import { SubCollectionViewProps } from "../CollectionSubView";
 
 interface MarqueeViewProps {
     getContainerTransform: () => Transform;
     getTransform: () => Transform;
-    container: CollectionFreeFormView;
     addDocument: (doc: Doc) => boolean;
     activeDocuments: () => Doc[];
     selectDocuments: (docs: Doc[]) => void;
     removeDocument: (doc: Doc) => boolean;
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
+    extensionDoc: Doc;
     isAnnotationOverlay?: boolean;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean) => void) => void;
 }
 
 @observer
-export class MarqueeView extends React.Component<MarqueeViewProps>
+export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps>
 {
     private _mainCont = React.createRef<HTMLDivElement>();
     @observable _lastX: number = 0;
@@ -187,13 +188,13 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
 
     @action
     onPointerUp = (e: PointerEvent): void => {
-        if (!this.props.container.props.active()) this.props.selectDocuments([this.props.container.props.Document]);
+        if (!this.props.active()) this.props.selectDocuments([this.props.Document]);
         if (this._visible) {
             let mselect = this.marqueeSelect();
             if (!e.shiftKey) {
-                SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
+                SelectionManager.DeselectAll(mselect.length ? undefined : this.props.Document);
             }
-            this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
+            this.props.selectDocuments(mselect.length ? mselect : [this.props.Document]);
         }
         this.cleanupInteractions(true);
 
@@ -246,12 +247,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     }
 
     get ink() { // ink will be stored on the extension doc for the field (fieldKey) where the container's data is stored.
-        return this.props.container.extensionDoc && Cast(this.props.container.extensionDoc.ink, InkField);
+        return this.props.extensionDoc && Cast(this.props.extensionDoc.ink, InkField);
     }
 
     set ink(value: InkField | undefined) {
-        let cprops = this.props.container.props;
-        this.props.container.extensionDoc && (this.props.container.extensionDoc.ink = value);
+        this.props.extensionDoc && (this.props.extensionDoc.ink = value);
     }
 
     @undoBatch
@@ -290,11 +290,11 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
             }
             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 colorPalette = Cast(this.props.Document.colorPalette, listSpec("string"));
+            if (!colorPalette) this.props.Document.colorPalette = new List<string>(defaultPalette);
+            let palette = Array.from(Cast(this.props.Document.colorPalette, listSpec("string")) as string[]);
             let usedPaletted = new Map<string, number>();
-            [...this.props.activeDocuments(), this.props.container.props.Document].map(child => {
+            [...this.props.activeDocuments(), this.props.Document].map(child => {
                 let bg = StrCast(Doc.Layout(child).backgroundColor);
                 if (palette.indexOf(bg) !== -1) {
                     palette.splice(palette.indexOf(bg), 1);
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 32ebe7c61..ef194624a 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -37,7 +37,7 @@ enum FollowOptions {
 @observer
 export class LinkFollowBox extends React.Component<FieldViewProps> {
 
-    public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkFollowBox, fieldKey); }
     public static Instance: LinkFollowBox | undefined;
     @observable static linkDoc: Doc | undefined = undefined;
     @observable static destinationDoc: Doc | undefined = undefined;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 3e5deb55b..4c1c3a465 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -4,18 +4,18 @@ import { observer } from "mobx-react";
 import "./AudioBox.scss";
 import { Cast } from "../../../new_fields/Types";
 import { AudioField } from "../../../new_fields/URLField";
-import { DocStaticComponent } from "../DocComponent";
+import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface } from "../../../new_fields/Schema";
-import { documentSchema } from "./DocumentView";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 type AudioDocument = makeInterface<[typeof documentSchema]>;
 const AudioDocument = makeInterface(documentSchema);
 const defaultField: AudioField = new AudioField(new URL("http://techslides.com/demos/samples/sample.mp3"));
 
 @observer
-export class AudioBox extends DocStaticComponent<FieldViewProps, AudioDocument>(AudioDocument) {
+export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocument>(AudioDocument) {
 
-    public static LayoutString() { return FieldView.LayoutString(AudioBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(AudioBox, fieldKey); }
     _ref = React.createRef<HTMLAudioElement>();
 
     componentDidMount() {
diff --git a/src/client/views/nodes/ButtonBox.tsx b/src/client/views/nodes/ButtonBox.tsx
index b4d33fb0f..1531d825b 100644
--- a/src/client/views/nodes/ButtonBox.tsx
+++ b/src/client/views/nodes/ButtonBox.tsx
@@ -3,11 +3,11 @@ 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, DocListCast } from '../../../new_fields/Doc';
+import { Doc, 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';
-import { BoolCast, StrCast, Cast } from '../../../new_fields/Types';
+import { BoolCast, StrCast, Cast, FieldValue } from '../../../new_fields/Types';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { DocComponent } from '../DocComponent';
@@ -15,26 +15,28 @@ import './ButtonBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
+import { documentSchema } from '../../../new_fields/documentSchemas';
 
 
 library.add(faEdit as any);
 
 const ButtonSchema = createSchema({
     onClick: ScriptField,
+    buttonParams: listSpec("string"),
     text: "string"
 });
 
-type ButtonDocument = makeInterface<[typeof ButtonSchema]>;
-const ButtonDocument = makeInterface(ButtonSchema);
+type ButtonDocument = makeInterface<[typeof ButtonSchema, typeof documentSchema]>;
+const ButtonDocument = makeInterface(ButtonSchema, documentSchema);
 
 @observer
 export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(ButtonDocument) {
-    public static LayoutString() { return FieldView.LayoutString(ButtonBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ButtonBox, fieldKey); }
     private dropDisposer?: DragManager.DragDropDisposer;
 
     @computed get dataDoc() {
         return this.props.DataDoc &&
-            (BoolCast(this.props.Document.isTemplateField) || BoolCast(this.props.DataDoc.isTemplateField) ||
+            (this.Document.isTemplateField || BoolCast(this.props.DataDoc.isTemplateField) ||
                 this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document);
     }
 
@@ -52,7 +54,7 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
         let funcs: ContextMenuProps[] = [];
         funcs.push({
             description: "Clear Script Params", event: () => {
-                let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+                let params = FieldValue(this.Document.buttonParams);
                 params && params.map(p => this.props.Document[p] = undefined);
             }, icon: "trash"
         });
@@ -70,13 +72,13 @@ export class ButtonBox extends DocComponent<FieldViewProps, ButtonDocument>(Butt
     }
     // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
     render() {
-        let params = Cast(this.props.Document.buttonParams, listSpec("string"));
+        let params = this.Document.buttonParams;
         let missingParams = params && params.filter(p => this.props.Document[p] === undefined);
         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}
                 style={{ boxShadow: this.Document.opacity === 0 ? undefined : StrCast(this.Document.boxShadow, "") }}>
-                <div className="buttonBox-mainButton" style={{ background: StrCast(this.props.Document.backgroundColor), color: StrCast(this.props.Document.color, "black") }} >
+                <div className="buttonBox-mainButton" style={{ background: this.Document.backgroundColor || "", color: this.Document.color || "black" }} >
                     <div className="buttonBox-mainButtonCenter">
                         {(this.Document.text || this.Document.title)}
                     </div>
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index e3ca02fa4..58cb831f8 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -2,14 +2,15 @@ import { random } from "animejs";
 import { computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, HeightSym, WidthSym } from "../../../new_fields/Doc";
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
 import { percent2frac } from "../../../Utils";
 import { Transform } from "../../util/Transform";
 import { DocComponent } from "../DocComponent";
 import "./CollectionFreeFormDocumentView.scss";
-import { documentSchema, DocumentView, DocumentViewProps } from "./DocumentView";
+import { DocumentView, DocumentViewProps } from "./DocumentView";
 import React = require("react");
+import { PositionDocument } from "../../../new_fields/documentSchemas";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
@@ -20,15 +21,6 @@ export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     jitterRotation: number;
     transition?: string;
 }
-export const positionSchema = createSchema({
-    zIndex: "number",
-    x: "number",
-    y: "number",
-    z: "number",
-});
-
-export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
-export const PositionDocument = makeInterface(documentSchema, positionSchema);
 
 @observer
 export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps, PositionDocument>(PositionDocument) {
@@ -92,8 +84,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     clusterColorFunc = (doc: Doc) => this.clusterColor;
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
-
     @observable _animPos: number[] | undefined = undefined;
 
     finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index fdcedb3a5..fda6d64f4 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -4,20 +4,20 @@ import { SketchPicker } from 'react-color';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ColorBox.scss";
 import { InkingControl } from "../InkingControl";
-import { DocStaticComponent } from "../DocComponent";
-import { documentSchema } from "./DocumentView";
+import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface } from "../../../new_fields/Schema";
-import { trace, reaction, observable, action, IReactionDisposer } from "mobx";
+import { reaction, observable, action, IReactionDisposer } from "mobx";
 import { SelectionManager } from "../../util/SelectionManager";
 import { StrCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 type ColorDocument = makeInterface<[typeof documentSchema]>;
 const ColorDocument = makeInterface(documentSchema);
 
 @observer
-export class ColorBox extends DocStaticComponent<FieldViewProps, ColorDocument>(ColorDocument) {
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
+export class ColorBox extends DocExtendableComponent<FieldViewProps, ColorDocument>(ColorDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ColorBox, fieldKey); }
 
     _selectedDisposer: IReactionDisposer | undefined;
     _penDisposer: IReactionDisposer | undefined;
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index 22e44948a..7119b0db0 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -7,11 +7,11 @@ import { Utils } from '../../../Utils';
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragLinksAsDocuments } from "../../util/DragManager";
 import { DocComponent } from "../DocComponent";
-import { documentSchema } from "./DocumentView";
 import "./DocuLinkBox.scss";
 import { FieldView, FieldViewProps } from "./FieldView";
 import React = require("react");
 import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 type DocLinkSchema = makeInterface<[typeof documentSchema]>;
 const DocLinkDocument = makeInterface(documentSchema);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9e36f0811..f741dae9a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,29 +1,37 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import * as fa from '@fortawesome/free-solid-svg-icons';
-import { action, computed, 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, Opt } from "../../../new_fields/Doc";
+import { Document } from '../../../new_fields/documentSchemas';
 import { Id } from '../../../new_fields/FieldSymbols';
-import { createSchema, listSpec, makeInterface } from "../../../new_fields/Schema";
+import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, NumCast, PromiseValue, StrCast, FieldValue } from "../../../new_fields/Types";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { ImageField } from '../../../new_fields/URLField';
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { emptyFunction, returnTrue, Utils, returnTransparent, returnOne } from "../../../Utils";
+import { emptyFunction, returnTransparent, returnTrue, Utils } from "../../../Utils";
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from "../../documents/Documents";
+import { DocumentType } from '../../documents/DocumentTypes';
 import { ClientUtils } from '../../util/ClientUtils';
 import { DictationManager } from '../../util/DictationManager';
 import { DocumentManager } from "../../util/DocumentManager";
 import { DragManager, dropActionType } from "../../util/DragManager";
 import { LinkManager } from '../../util/LinkManager';
+import { Scripting } from '../../util/Scripting';
 import { SelectionManager } from "../../util/SelectionManager";
+import SharingManager from '../../util/SharingManager';
 import { Transform } from "../../util/Transform";
 import { undoBatch, UndoManager } from "../../util/UndoManager";
+import { CollectionViewType } from '../collections/CollectionBaseView';
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionView } from "../collections/CollectionView";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
+import { DictationOverlay } from '../DictationOverlay';
 import { DocComponent } from "../DocComponent";
 import { EditableView } from '../EditableView';
 import { OverlayView } from '../OverlayView';
@@ -33,13 +41,6 @@ import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
-import { DocumentType } from '../../documents/DocumentTypes';
-import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
-import { ImageField } from '../../../new_fields/URLField';
-import SharingManager from '../../util/SharingManager';
-import { Scripting } from '../../util/Scripting';
-import { DictationOverlay } from '../DictationOverlay';
-import { CollectionViewType } from '../collections/CollectionBaseView';
 
 library.add(fa.faEdit);
 library.add(fa.faTrash);
@@ -67,7 +68,6 @@ library.add(fa.faLaptopCode, fa.faMale, fa.faCopy, fa.faHandPointRight, fa.faCom
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView>;
     ContainingCollectionDoc: Opt<Doc>;
-    fieldKey: string;
     Document: Doc;
     DataDoc?: Doc;
     fitToBox?: boolean;
@@ -96,41 +96,6 @@ export interface DocumentViewProps {
     layoutKey?: string;
 }
 
-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
-    //links: listSpec(Doc),       // computed (readonly) list of links associated with this document
-    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
-    removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
-    onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
-    onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
-    dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
-    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
-    isTemplateField: "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
-    inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
-    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) 
-});
-
-
-type Document = makeInterface<[typeof documentSchema]>;
-const Document = makeInterface(documentSchema);
 
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps, Document>(Document) {
@@ -572,13 +537,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
     }
 
-
-    // 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(Doc.Layout(this.props.Document));
-    }
-
     // 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.
@@ -599,7 +557,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         return (<DocumentContentsView ContainingCollectionView={this.props.ContainingCollectionView}
             ContainingCollectionDoc={this.props.ContainingCollectionDoc}
             Document={this.props.Document}
-            fieldKey={this.props.fieldKey}
             fitToBox={this.props.fitToBox}
             addDocument={this.props.addDocument}
             removeDocument={this.props.removeDocument}
@@ -729,6 +686,4 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 }
 
-Scripting.addGlobal(function toggleDetail(doc: any) {
-    doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout";
-});
\ No newline at end of file
+Scripting.addGlobal(function toggleDetail(doc: any) { doc.layoutKey = StrCast(doc.layoutKey, "layout") === "layout" ? "layoutCustom" : "layout"; });
\ No newline at end of file
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 7d69b5b51..5108954bb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -52,9 +52,8 @@ export interface FieldViewProps {
 
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
-        return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`;
-        //"<ImageBox {...props} />"
+    public static LayoutString(fieldType: { name: string }, fieldStr: string) {
+        return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"}/>`;  //e.g., "<ImageBox {...props} fieldKey={"dada} />"
     }
 
     @computed
diff --git a/src/client/views/nodes/FontIconBox.tsx b/src/client/views/nodes/FontIconBox.tsx
index fd6a475fb..ae9273639 100644
--- a/src/client/views/nodes/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox.tsx
@@ -17,7 +17,7 @@ type FontIconDocument = makeInterface<[typeof FontIconSchema]>;
 const FontIconDocument = makeInterface(FontIconSchema);
 @observer
 export class FontIconBox extends DocComponent<FieldViewProps, FontIconDocument>(FontIconDocument) {
-    public static LayoutString() { return FieldView.LayoutString(FontIconBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(FontIconBox, fieldKey); }
     @observable _foregroundColor = "white";
     _ref: React.RefObject<HTMLButtonElement> = React.createRef();
     _backgroundReaction: IReactionDisposer | undefined;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 995fcee17..5c2d39d98 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -33,7 +33,7 @@ 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 { DocExtendableComponent } from "../DocComponent";
 import { DocumentButtonBar } from '../DocumentButtonBar';
 import { DocumentDecorations } from '../DocumentDecorations';
 import { InkingControl } from "../InkingControl";
@@ -44,6 +44,7 @@ import React = require("react");
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { documentSchema } from '../../../new_fields/documentSchemas';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -62,16 +63,14 @@ const richTextSchema = createSchema({
 
 export const GoogleRef = "googleDocId";
 
-type RichTextDocument = makeInterface<[typeof richTextSchema]>;
-const RichTextDocument = makeInterface(richTextSchema);
+type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>;
+const RichTextDocument = makeInterface(richTextSchema, documentSchema);
 
 type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void;
 
 @observer
-export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
-    public static LayoutString(fieldStr: string = "data") {
-        return FieldView.LayoutString(FormattedTextBox, fieldStr);
-    }
+export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
     public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
     public static Instance: FormattedTextBox;
     private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
@@ -135,17 +134,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         return true;
     }
 
-    constructor(props: FieldViewProps) {
+    constructor(props: any) {
         super(props);
         FormattedTextBox.Instance = this;
     }
 
     public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
 
-    // the document containing the view layout information - will be the Document itself unless the Document has
-    // a layout field.  In that case, all layout information comes from there unless overriden by Document
-    @computed get layoutDoc(): Doc { return Doc.Layout(this.props.Document); }
-
     linkOnDeselect: Map<string, string> = new Map();
 
     doLinkOnDeselect() {
@@ -266,8 +261,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     newLayout = Doc.MakeDelegate(draggedDoc);
                     newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`);
                 }
-                this.props.Document.layoutCustom = newLayout;
-                this.props.Document.layoutKey = "layoutCustom";
+                this.Document.layoutCustom = newLayout;
+                this.Document.layoutKey = "layoutCustom";
                 e.stopPropagation();
                 // embed document when dragging with a userDropAction or an embedDoc flag set
             } else if (de.data.userDropAction || de.data.embedDoc) {
@@ -1015,7 +1010,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0;
         if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 &&
             getComputedStyle(this._ref.current!.parentElement!).top === "0px") {  // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
-            let nh = this.props.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
+            let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
             let dh = NumCast(this.layoutDoc.height, 0);
             this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
             this.dataDoc.nativeHeight = nh ? scrollHeight : undefined;
@@ -1052,7 +1047,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                 onPointerEnter={action(() => this._entered = true)}
                 onPointerLeave={action(() => this._entered = false)}
             >
-                <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 className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} />
 
                 <div className="formattedTextBox-dictation"
                     onClick={e => {
diff --git a/src/client/views/nodes/IconBox.tsx b/src/client/views/nodes/IconBox.tsx
index 4971f61b7..60f547b1e 100644
--- a/src/client/views/nodes/IconBox.tsx
+++ b/src/client/views/nodes/IconBox.tsx
@@ -24,7 +24,7 @@ library.add(faFilm, faTag, faTextHeight);
 
 @observer
 export class IconBox extends React.Component<FieldViewProps> {
-    public static LayoutString() { return FieldView.LayoutString(IconBox); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(IconBox, fieldKey); }
 
     @observable _panelWidth: number = 0;
     @observable _panelHeight: number = 0;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4d623a04f..4c2f8e22d 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -20,12 +20,12 @@ import { ContextMenu } from "../../views/ContextMenu";
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocAnnotatableComponent } from '../DocComponent';
 import { InkingControl } from '../InkingControl';
-import { documentSchema } from './DocumentView';
 import FaceRectangles from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import "./ImageBox.scss";
 import React = require("react");
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
+import { documentSchema } from '../../../new_fields/documentSchemas';
 var requestImageSize = require('../../util/request-image-size');
 var path = require('path');
 const { Howl } = require('howler');
@@ -38,7 +38,9 @@ library.add(faFileAudio, faAsterisk);
 export const pageSchema = createSchema({
     curPage: "number",
     fitWidth: "boolean",
-    rotation: "number"
+    rotation: "number",
+    googlePhotosUrl: "string",
+    googlePhotosTags: "string"
 });
 
 interface Window {
@@ -54,8 +56,8 @@ type ImageDocument = makeInterface<[typeof pageSchema, typeof documentSchema]>;
 const ImageDocument = makeInterface(pageSchema, documentSchema);
 
 @observer
-export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument, "annotations") {
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(ImageBox, fieldKey); }
+export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocument>(ImageDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
     private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
     private _dropDisposer?: DragManager.DragDropDisposer;
     @observable private _audioState = 0;
@@ -251,7 +253,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     audioDown = () => this.recordAudioAnnotation();
 
     considerGooglePhotosLink = () => {
-        const remoteUrl = StrCast(this.props.Document.googlePhotosUrl);
+        const remoteUrl = this.Document.googlePhotosUrl;
         return !remoteUrl ? (null) : (<img
             id={"google-photos"}
             src={"/assets/google_photos.png"}
@@ -260,7 +262,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
     }
 
     considerGooglePhotosTags = () => {
-        const tags = StrCast(this.props.Document.googlePhotosTags);
+        const tags = this.Document.googlePhotosTags;
         return !tags ? (null) : (<img id={"google-tags"} src={"/assets/google_tags.png"} />);
     }
 
@@ -287,7 +289,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
         if (field instanceof ImageField) paths = [this.choosePath(field.url)];
         paths.push(...altpaths);
         // }
-        let interactive = InkingControl.Instance.selectedTool || this.props.Document.isBackground ? "" : "-interactive";
+        let interactive = InkingControl.Instance.selectedTool || this.Document.isBackground ? "" : "-interactive";
         let rotation = NumCast(this.Document.rotation, 0);
         let aspect = (rotation % 180) ? this.Document[HeightSym]() / this.Document[WidthSym]() : 1;
         let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
@@ -333,7 +335,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={this.fieldExt}
+                annotationsKey={this.annotationsKey}
                 isAnnotationOverlay={true}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 2b46e9dff..35e9e4862 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -25,11 +25,12 @@ export type KVPScript = {
 
 @observer
 export class KeyValueBox extends React.Component<FieldViewProps> {
+    public static LayoutString(fieldStr: string) { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+
     private _mainCont = React.createRef<HTMLDivElement>();
     private _keyHeader = React.createRef<HTMLTableHeaderCellElement>();
-    @observable private rows: KeyValuePair[] = [];
 
-    public static LayoutString(fieldStr: string = "data") { return FieldView.LayoutString(KeyValueBox, fieldStr); }
+    @observable private rows: KeyValuePair[] = [];
     @observable private _keyInput: string = "";
     @observable private _valueInput: string = "";
     @computed get splitPercentage() { return NumCast(this.props.Document.schemaSplitPercentage, 50); }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b1110daf4..396a5356a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -16,18 +16,18 @@ import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { DocAnnotatableComponent } from "../DocComponent";
 import { PDFViewer } from "../pdf/PDFViewer";
-import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
+import { documentSchema } from '../../../new_fields/documentSchemas';
 
 type PdfDocument = makeInterface<[typeof documentSchema, typeof panZoomSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
 
 @observer
-export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument, "annotations") {
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(PDFBox, fieldKey); }
+export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>(PdfDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PDFBox, fieldKey); }
     private _keyValue: string = "";
     private _valueValue: string = "";
     private _scriptValue: string = "";
@@ -232,7 +232,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
                     pinToPres={this.props.pinToPres} addDocument={this.addDocument}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                     isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
-                    fieldKey={this.props.fieldKey} extensionDoc={this.extensionDoc} startupLive={this._initialScale < 2.5 ? true : false} />
+                    fieldKey={this.props.fieldKey} 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 15fafb022..7ec5d0471 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -31,7 +31,7 @@ library.add(faEdit);
 
 @observer
 export class PresBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresBox, fieldKey); }
     _docListChangedReaction: IReactionDisposer | undefined;
     componentDidMount() {
         this._docListChangedReaction = reaction(() => {
diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx
index ced597b59..99b5810fc 100644
--- a/src/client/views/nodes/QueryBox.tsx
+++ b/src/client/views/nodes/QueryBox.tsx
@@ -18,7 +18,7 @@ library.add(faEdit);
 
 @observer
 export class QueryBox extends React.Component<FieldViewProps> {
-    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); }
     _docListChangedReaction: IReactionDisposer | undefined;
     componentDidMount() {
     }
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 64871ef41..48a699e58 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -1,33 +1,32 @@
 import React = require("react");
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked, trace } from "mobx";
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faVideo } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from "mobx";
 import { observer } from "mobx-react";
 import * as rp from 'request-promise';
+import { Doc } from "../../../new_fields/Doc";
 import { InkTool } from "../../../new_fields/InkField";
-import { makeInterface, createSchema, listSpec } from "../../../new_fields/Schema";
-import { Cast, FieldValue, NumCast, BoolCast, StrCast } from "../../../new_fields/Types";
+import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { ScriptField } from "../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../new_fields/Types";
 import { VideoField } from "../../../new_fields/URLField";
 import { RouteStore } from "../../../server/RouteStore";
-import { Utils, emptyFunction, returnOne } from "../../../Utils";
+import { emptyFunction, returnOne, Utils } from "../../../Utils";
 import { Docs, DocUtils } from "../../documents/Documents";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { ContextMenu } from "../ContextMenu";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { DocAnnotatableComponent } from "../DocComponent";
 import { DocumentDecorations } from "../DocumentDecorations";
 import { InkingControl } from "../InkingControl";
-import { documentSchema } from "./DocumentView";
 import { FieldView, FieldViewProps } from './FieldView';
 import "./VideoBox.scss";
-import { library } from "@fortawesome/fontawesome-svg-core";
-import { faVideo } from "@fortawesome/free-solid-svg-icons";
-import { Doc } from "../../../new_fields/Doc";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { positionSchema } from "./CollectionFreeFormDocumentView";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { documentSchema, positionSchema } from "../../../new_fields/documentSchemas";
 var path = require('path');
 
 export const timeSchema = createSchema({
-    currentTimecode: "number",
+    currentTimecode: "number",  // the current time of a video or other linear, time-based document.  Note, should really get set on an extension field, but that's more complicated when it needs to be set since the extension doc needs to be found first
 });
 type VideoDocument = makeInterface<[typeof documentSchema, typeof positionSchema, typeof timeSchema]>;
 const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
@@ -35,7 +34,7 @@ const VideoDocument = makeInterface(documentSchema, positionSchema, timeSchema);
 library.add(faVideo);
 
 @observer
-export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument, "annotations") {
+export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocument>(VideoDocument) {
     static _youtubeIframeCounter: number = 0;
     private _reactionDisposer?: IReactionDisposer;
     private _youtubeReactionDisposer?: IReactionDisposer;
@@ -49,7 +48,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
     @observable _fullScreen = false;
     @observable _playing = false;
     @observable static _showControls: boolean;
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(VideoBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(VideoBox, fieldKey); }
 
     public get player(): HTMLVideoElement | null {
         return this._videoRef;
@@ -282,24 +281,20 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         ]]);
     }
 
-    @action
-    onPlayDown = () => this._playing ? this.Pause() : this.Play()
+    onPlayDown = () => this._playing ? this.Pause() : this.Play();
 
-    @action
     onFullDown = (e: React.PointerEvent) => {
         this.FullScreen();
         e.stopPropagation();
         e.preventDefault();
     }
 
-    @action
     onSnapshot = (e: React.PointerEvent) => {
         this.Snapshot();
         e.stopPropagation();
         e.preventDefault();
     }
 
-    @action
     onResetDown = (e: React.PointerEvent) => {
         this.Pause();
         e.stopPropagation();
@@ -309,12 +304,12 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         InkingControl.Instance.switchTool(InkTool.Eraser);
     }
 
-    @action
     onResetMove = (e: PointerEvent) => {
         this._isResetClick += Math.abs(e.movementX) + Math.abs(e.movementY);
         this.Seek(Math.max(0, (this.Document.currentTimecode || 0) + Math.sign(e.movementX) * 0.0333));
         e.stopImmediatePropagation();
     }
+
     @action
     onResetUp = (e: PointerEvent) => {
         document.removeEventListener("pointermove", this.onResetMove, true);
@@ -322,7 +317,6 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
         InkingControl.Instance.switchTool(InkTool.None);
         this._isResetClick < 10 && (this.Document.currentTimecode = 0);
     }
-    @computed get dataDoc() { return this.props.DataDoc && this.Document.isTemplateField ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
 
     @computed get youtubeContent() {
         this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
@@ -346,7 +340,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={this.fieldExt}
+                annotationsKey={this.annotationsKey}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 2184325a9..5af743859 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -19,8 +19,8 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from "./KeyValueBox";
 import "./WebBox.scss";
 import React = require("react");
-import { documentSchema } from "./DocumentView";
 import { DocAnnotatableComponent } from "../DocComponent";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 
 library.add(faStickyNote);
 
@@ -28,13 +28,12 @@ type WebDocument = makeInterface<[typeof documentSchema]>;
 const WebDocument = makeInterface(documentSchema);
 
 @observer
-export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument, "annotations") {
+export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>(WebDocument) {
 
-    public static LayoutString(fieldKey: string = "data") { return FieldView.LayoutString(WebBox, fieldKey); }
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(WebBox, fieldKey); }
     @observable private collapsed: boolean = true;
     @observable private url: string = "";
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
     componentWillMount() {
 
         let field = Cast(this.props.Document[this.props.fieldKey], WebField);
@@ -199,7 +198,7 @@ export class WebBox extends DocAnnotatableComponent<FieldViewProps, WebDocument>
             <CollectionFreeFormView {...this.props}
                 PanelHeight={this.props.PanelHeight}
                 PanelWidth={this.props.PanelWidth}
-                fieldExt={this.fieldExt}
+                annotationsKey={this.annotationsKey}
                 focus={this.props.focus}
                 isSelected={this.props.isSelected}
                 isAnnotationOverlay={true}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5806dec83..51dc6e8d6 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -24,8 +24,8 @@ import { CollectionFreeFormView } from "../collections/collectionFreeForm/Collec
 import { SelectionManager } from "../../util/SelectionManager";
 import { undoBatch } from "../../util/UndoManager";
 import { DocAnnotatableComponent } from "../DocComponent";
-import { documentSchema } from "../nodes/DocumentView";
 import { DocumentType } from "../../documents/DocumentTypes";
+import { documentSchema } from "../../../new_fields/documentSchemas";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -49,7 +49,6 @@ interface IViewerProps {
     Document: Doc;
     DataDoc?: Doc;
     ContainingCollectionView: Opt<CollectionView>;
-    extensionDoc: Doc;
     PanelWidth: () => number;
     PanelHeight: () => number;
     ContentScaling: () => number;
@@ -73,7 +72,7 @@ interface IViewerProps {
  * Handles rendering and virtualization of the pdf
  */
 @observer
-export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument, "annotations") {
+export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument>(PdfDocument) {
     static _annotationStyle: any = addStyleSheet();
     @observable private _pageSizes: { width: number, height: number }[] = [];
     @observable private _annotations: Doc[] = [];
@@ -109,8 +108,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     private _coverPath: any;
 
     @computed get allAnnotations() {
-        return DocListCast(this.props.extensionDoc.annotations).filter(
-            anno => this._script.run({ this: anno }, console.log, true).result);
+        return this.extensionDoc ? DocListCast(this.extensionDoc.annotations).filter(
+            anno => this._script.run({ this: anno }, console.log, true).result) : [];
     }
 
     @computed get nonDocAnnotations() {
@@ -200,7 +199,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         await this.initialLoad();
 
         this._annotationReactionDisposer = reaction(
-            () => this.props.extensionDoc && DocListCast(this.props.extensionDoc.annotations),
+            () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
             annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
             { fireImmediately: true });
 
@@ -629,10 +628,10 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     @computed get annotationLayer() {
         return <div className="pdfViewer-annotationLayer" style={{ height: (this.Document.nativeHeight || 0) }} 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`} />)}
+                <Annotation {...this.props} focus={this.props.focus} extensionDoc={this.extensionDoc!} anno={anno} key={`${anno[Id]}-annotation`} />)}
             <div className="pdfViewer-overlay" id="overlay" style={{ transform: `scale(${this._zoomed})` }}>
                 <CollectionFreeFormView {...this.props}
-                    fieldExt={this.fieldExt}
+                    annotationsKey={this.annotationsKey}
                     setPreviewCursor={this.setPreviewCursor}
                     PanelHeight={() => (this.Document.scrollHeight || this.Document.nativeHeight || 0)}
                     PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : (this.Document.nativeWidth || 0)}
@@ -673,7 +672,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
     marqueeing = () => this._marqueeing;
     visibleHeight = () => this.props.PanelHeight() / this.props.ContentScaling() * 72 / 96;
     render() {
-        return (<div className={"pdfViewer-viewer" + (this._zoomed !== 1 ? "-zoomed" : "")}
+        return !this.extensionDoc ? (null) : (<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}
diff --git a/src/client/views/presentationview/PresElementBox.tsx b/src/client/views/presentationview/PresElementBox.tsx
index 0824808eb..5f758f496 100644
--- a/src/client/views/presentationview/PresElementBox.tsx
+++ b/src/client/views/presentationview/PresElementBox.tsx
@@ -5,18 +5,19 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { documentSchema } from '../../../new_fields/documentSchemas';
 import { Id } from "../../../new_fields/FieldSymbols";
-import { BoolCast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnOne } from "../../../Utils";
+import { createSchema, makeInterface } from '../../../new_fields/Schema';
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { emptyFunction, returnFalse } from "../../../Utils";
 import { DocumentType } from "../../documents/DocumentTypes";
 import { Transform } from "../../util/Transform";
 import { CollectionViewType } from '../collections/CollectionBaseView';
-import { DocumentView } from "../nodes/DocumentView";
+import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
+import { DocComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import "./PresElementBox.scss";
 import React = require("react");
-import { CollectionSchemaPreview } from '../collections/CollectionSchemaView';
-
 
 library.add(faArrowUp);
 library.add(fileSolid);
@@ -24,34 +25,34 @@ library.add(faLocationArrow);
 library.add(fileRegular as any);
 library.add(faSearch);
 library.add(faArrowDown);
+
+export const presSchema = createSchema({
+    presentationTargetDoc: Doc,
+    presBox: Doc,
+    presBoxKey: "string",
+    showButton: "boolean",
+    navButton: "boolean",
+    hideTillShownButton: "boolean",
+    fadeButton: "boolean",
+    hideAfterButton: "boolean",
+    groupButton: "boolean",
+    embedOpen: "boolean"
+});
+
+type PresDocument = makeInterface<[typeof presSchema, typeof documentSchema]>;
+const PresDocument = makeInterface(presSchema, documentSchema);
 /**
  * This class models the view a document added to presentation will have in the presentation.
  * It involves some functionality for its buttons and options.
  */
 @observer
-export class PresElementBox extends React.Component<FieldViewProps> {
+export class PresElementBox extends DocComponent<FieldViewProps, PresDocument>(PresDocument) {
+    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(PresElementBox, fieldKey); }
 
-    public static LayoutString() { return FieldView.LayoutString(PresElementBox); }
-
-    @computed get myIndex() { return DocListCast(this.presentationDoc[this.presentationFieldKey]).indexOf(this.props.Document); }
-    @computed get presentationDoc() { return this.props.Document.presBox as Doc; }
-    @computed get presentationFieldKey() { return StrCast(this.props.Document.presBoxKey); }
+    @computed get indexInPres() { return DocListCast(this.presentationDoc[this.Document.presBoxKey || ""]).indexOf(this.props.Document); }
+    @computed get presentationDoc() { return Cast(this.Document.presBox, Doc) as Doc; }
+    @computed get targetDoc() { return this.Document.presentationTargetDoc as Doc }
     @computed get currentIndex() { return NumCast(this.presentationDoc.selectedDoc); }
-    @computed get showButton() { return BoolCast(this.props.Document.showButton); }
-    @computed get navButton() { return BoolCast(this.props.Document.navButton); }
-    @computed get hideTillShownButton() { return BoolCast(this.props.Document.hideTillShownButton); }
-    @computed get fadeButton() { return BoolCast(this.props.Document.fadeButton); }
-    @computed get hideAfterButton() { return BoolCast(this.props.Document.hideAfterButton); }
-    @computed get groupButton() { return BoolCast(this.props.Document.groupButton); }
-    @computed get embedOpen() { return BoolCast(this.props.Document.embedOpen); }
-
-    set embedOpen(value: boolean) { this.props.Document.embedOpen = value; }
-    set showButton(val: boolean) { this.props.Document.showButton = val; }
-    set navButton(val: boolean) { this.props.Document.navButton = val; }
-    set hideTillShownButton(val: boolean) { this.props.Document.hideTillShownButton = val; }
-    set fadeButton(val: boolean) { this.props.Document.fadeButton = val; }
-    set hideAfterButton(val: boolean) { this.props.Document.hideAfterButton = val; }
-    set groupButton(val: boolean) { this.props.Document.groupButton = val; }
 
     /**
      * The function that is called on click to turn Hiding document till press option on/off.
@@ -60,14 +61,14 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.hideTillShownButton = !this.hideTillShownButton;
-        if (!this.hideTillShownButton) {
-            if (this.myIndex >= this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+        this.Document.hideTillShownButton = !this.Document.hideTillShownButton;
+        if (!this.Document.hideTillShownButton) {
+            if (this.indexInPres >= this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 1;
             }
         } else {
-            if (this.presentationDoc.presStatus && this.myIndex > this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 0;
+            if (this.presentationDoc.presStatus && this.indexInPres > this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 0;
             }
         }
     }
@@ -80,15 +81,15 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onHideDocumentAfterPresentedClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.hideAfterButton = !this.hideAfterButton;
-        if (!this.hideAfterButton) {
-            if (this.myIndex <= this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+        this.Document.hideAfterButton = !this.Document.hideAfterButton;
+        if (!this.Document.hideAfterButton) {
+            if (this.indexInPres <= this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 1;
             }
         } else {
-            if (this.fadeButton) this.fadeButton = false;
-            if (this.presentationDoc.presStatus && this.myIndex < this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 0;
+            if (this.Document.fadeButton) this.Document.fadeButton = false;
+            if (this.presentationDoc.presStatus && this.indexInPres < this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 0;
             }
         }
     }
@@ -101,15 +102,15 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onFadeDocumentAfterPresentedClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.fadeButton = !this.fadeButton;
-        if (!this.fadeButton) {
-            if (this.myIndex <= this.currentIndex) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 1;
+        this.Document.fadeButton = !this.Document.fadeButton;
+        if (!this.Document.fadeButton) {
+            if (this.indexInPres <= this.currentIndex && this.targetDoc) {
+                this.targetDoc.opacity = 1;
             }
         } else {
-            this.hideAfterButton = false;
-            if (this.presentationDoc.presStatus && (this.myIndex < this.currentIndex)) {
-                (this.props.Document.presentationTargetDoc as Doc).opacity = 0.5;
+            this.Document.hideAfterButton = false;
+            if (this.presentationDoc.presStatus && (this.indexInPres < this.currentIndex) && this.targetDoc) {
+                this.targetDoc.opacity = 0.5;
             }
         }
     }
@@ -120,10 +121,10 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     @action
     onNavigateDocumentClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-        this.navButton = !this.navButton;
-        if (this.navButton) {
-            this.showButton = false;
-            if (this.currentIndex === this.myIndex) {
+        this.Document.navButton = !this.Document.navButton;
+        if (this.Document.navButton) {
+            this.Document.showButton = false;
+            if (this.currentIndex === this.indexInPres) {
                 this.props.focus(this.props.Document);
             }
         }
@@ -136,12 +137,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     onZoomDocumentClick = (e: React.MouseEvent) => {
         e.stopPropagation();
 
-        this.showButton = !this.showButton;
-        if (!this.showButton) {
+        this.Document.showButton = !this.Document.showButton;
+        if (!this.Document.showButton) {
             this.props.Document.viewScale = 1;
         } else {
-            this.navButton = false;
-            if (this.currentIndex === this.myIndex) {
+            this.Document.navButton = false;
+            if (this.currentIndex === this.indexInPres) {
                 this.props.focus(this.props.Document);
             }
         }
@@ -151,13 +152,12 @@ export class PresElementBox extends React.Component<FieldViewProps> {
      */
     ScreenToLocalListTransform = (xCord: number, yCord: number) => [xCord, yCord];
 
-    get layoutDoc() { return Doc.Layout(this.props.Document); }
     /**
      * The function that is responsible for rendering the a preview or not for this
      * presentation element.
      */
     renderEmbeddedInline = () => {
-        if (!this.embedOpen || !(this.props.Document.presentationTargetDoc instanceof Doc)) {
+        if (!this.Document.embedOpen || !this.targetDoc) {
             return (null);
         }
 
@@ -170,8 +170,9 @@ export class PresElementBox extends React.Component<FieldViewProps> {
                 width: propDocWidth === 0 ? "auto" : propDocWidth * scale(),
             }}>
                 <CollectionSchemaPreview
-                    fitToBox={StrCast(this.props.Document.presentationTargetDoc.type).indexOf(DocumentType.COL) !== -1}
-                    Document={this.props.Document.presentationTargetDoc}
+                    fitToBox={StrCast(this.targetDoc.type).indexOf(DocumentType.COL) !== -1}
+                    Document={this.targetDoc}
+                    fieldKey={this.props.fieldKey}
                     addDocument={returnFalse}
                     removeDocument={returnFalse}
                     ruleProvider={undefined}
@@ -193,30 +194,27 @@ export class PresElementBox extends React.Component<FieldViewProps> {
     }
 
     render() {
-        let p = this.props;
-
         let treecontainer = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.viewType === CollectionViewType.Tree;
-        let className = "presElementBox-item" + (this.currentIndex === this.myIndex ? " presElementBox-selected" : "");
+        let className = "presElementBox-item" + (this.currentIndex === this.indexInPres ? " presElementBox-selected" : "");
         let pbi = "presElementBox-interaction";
         return (
-            <div className={className} key={p.Document[Id] + this.myIndex}
-                style={{ outlineWidth: Doc.IsBrushed(p.Document.presentationTargetDoc as Doc) ? `1px` : "0px", }}
-                onClick={e => { p.focus(p.Document); e.stopPropagation(); }}>
+            <div className={className} key={this.props.Document[Id] + this.indexInPres}
+                style={{ outlineWidth: Doc.IsBrushed(this.targetDoc) ? `1px` : "0px", }}
+                onClick={e => { this.props.focus(this.props.Document); e.stopPropagation(); }}>
                 {treecontainer ? (null) : <>
                     <strong className="presElementBox-name">
-                        {`${this.myIndex + 1}. ${p.Document.title}`}
+                        {`${this.indexInPres + 1}. ${this.Document.title}`}
                     </strong>
-                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(p.Document)}>X</button>
+                    <button className="presElementBox-closeIcon" onPointerDown={e => e.stopPropagation()} onClick={e => this.props.removeDocument && this.props.removeDocument(this.props.Document)}>X</button>
                     <br />
-                </>
-                }
-                <button title="Zoom" className={pbi + (this.showButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
-                <button title="Navigate" className={pbi + (this.navButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
-                <button title="Hide Before" className={pbi + (this.hideTillShownButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
-                <button title="Fade After" className={pbi + (this.fadeButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Hide After" className={pbi + (this.hideAfterButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
-                <button title="Group With Up" className={pbi + (this.groupButton ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.groupButton = !this.groupButton; })}><FontAwesomeIcon icon={"arrow-up"} /></button>
-                <button title="Expand Inline" className={pbi + (this.embedOpen ? "-selected" : "")} onPointerDown={(e) => e.stopPropagation()} onClick={action((e: any) => { e.stopPropagation(); this.embedOpen = !this.embedOpen; })}><FontAwesomeIcon icon={"arrow-down"} /></button>
+                </>}
+                <button title="Zoom" className={pbi + (this.Document.showButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
+                <button title="Navigate" className={pbi + (this.Document.navButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
+                <button title="Hide Before" className={pbi + (this.Document.hideTillShownButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
+                <button title="Fade After" className={pbi + (this.Document.fadeButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+                <button title="Hide After" className={pbi + (this.Document.hideAfterButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
+                <button title="Group With Up" className={pbi + (this.Document.groupButton ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.groupButton = !this.Document.groupButton; }}><FontAwesomeIcon icon={"arrow-up"} /></button>
+                <button title="Expand Inline" className={pbi + (this.Document.embedOpen ? "-selected" : "")} onPointerDown={e => e.stopPropagation()} onClick={e => { e.stopPropagation(); this.Document.embedOpen = !this.Document.embedOpen; }}><FontAwesomeIcon icon={"arrow-down"} /></button>
 
                 <br style={{ lineHeight: 0.1 }} />
                 {this.renderEmbeddedInline()}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index c6f654c33..08cb66d5f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -645,7 +645,8 @@ export namespace Doc {
         @observable BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
     }
 
-    // returns the active layout document for 'doc'.  
+    // 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  
     export function Layout(doc: Doc) { return Doc.LayoutField(doc) instanceof Doc ? doc[StrCast(doc.layoutKey, "layout")] as Doc : doc; }
     export function LayoutField(doc: Doc) { return doc[StrCast(doc.layoutKey, "layout")]; }
     const manager = new DocData();
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
new file mode 100644
index 000000000..8c3b62067
--- /dev/null
+++ b/src/new_fields/documentSchemas.ts
@@ -0,0 +1,51 @@
+import { makeInterface, createSchema, listSpec } from "./Schema";
+import { ScriptField } from "./ScriptField";
+import { Doc } from "./Doc";
+
+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
+    layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
+    layoutCustom: Doc,          // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
+    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",           // "
+    color: "string",            // foreground color of document
+    backgroundColor: "string",  // background color of document
+    opacity: "number",          // opacity of document
+    //links: listSpec(Doc),       // computed (readonly) list of links associated with this document
+    dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
+    removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
+    onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
+    onDragStart: ScriptField,   // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
+    dragFactory: Doc,           // the document that serves as the "template" for the onDragStart script.  ie, to drag out copies of the dragFactory document.
+    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
+    isTemplateField: "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
+    inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
+    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 const positionSchema = createSchema({
+    zIndex: "number",
+    x: "number",
+    y: "number",
+    z: "number",
+});
+
+export type Document = makeInterface<[typeof documentSchema]>;
+export const Document = makeInterface(documentSchema);
+
+export type PositionDocument = makeInterface<[typeof documentSchema, typeof positionSchema]>;
+export const PositionDocument = makeInterface(documentSchema, positionSchema);
-- 
cgit v1.2.3-70-g09d2


From 3c8d5d0a53d03d570fd57789ecf43121eb814b0f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 23 Oct 2019 10:39:53 -0400
Subject: several fixes to audio to better support generic timecode linking

---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/views/nodes/AudioBox.tsx                | 63 +++++++++++++---------
 src/client/views/nodes/DocumentView.tsx            | 18 +++----
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 src/new_fields/documentSchemas.ts                  | 14 +++--
 .../authentication/models/current_user_utils.ts    | 14 ++---
 6 files changed, 67 insertions(+), 48 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5ae4ca82a..cad417d33 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -84,7 +84,7 @@ export interface DocumentOptions {
     columnWidth?: number;
     fontSize?: number;
     curPage?: number;
-    currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)
+    currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video)  value is in seconds
     displayTimecode?: number; // the time that a document should be displayed (e.g., time an annotation should be displayed on a video)
     documentText?: string;
     borderRounding?: string;
@@ -104,7 +104,7 @@ export interface DocumentOptions {
     gridGap?: number; // gap between items in masonry view
     xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts
     yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts
-    panel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
+    sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script
     targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script
     dropConverter?: ScriptField; // script to run when documents are dropped on this Document.
     // [key: string]: Opt<Field>;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 62ec683da..135874400 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -2,7 +2,7 @@ import React = require("react");
 import { FieldViewProps, FieldView } from './FieldView';
 import { observer } from "mobx-react";
 import "./AudioBox.scss";
-import { Cast, DateCast } from "../../../new_fields/Types";
+import { Cast, DateCast, NumCast } from "../../../new_fields/Types";
 import { AudioField, nullAudio } from "../../../new_fields/URLField";
 import { DocExtendableComponent } from "../DocComponent";
 import { makeInterface, createSchema } from "../../../new_fields/Schema";
@@ -41,33 +41,33 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _scrubbingDisposer: IReactionDisposer | undefined;
     _ele: HTMLAudioElement | null = null;
     _recorder: any;
+    _recordStart = 0;
     _lastUpdate = 0;
 
-    @observable private _audioState = 0;
+    @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
     @observable public static ScrubTime = 0;
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
-        runInAction(() => this._audioState = this.path ? 2 : 0);
+        runInAction(() => this._audioState = this.path ? "recorded" : "unrecorded");
         this._linkPlayDisposer = reaction(() => this.layoutDoc.scrollToLinkID,
             scrollLinkId => {
-                scrollLinkId && DocListCast(this.dataDoc.links).map(l => {
-                    const la1 = l.anchor1 as Doc;
-                    const la2 = l.anchor2 as Doc;
-                    if (l[Id] === scrollLinkId && la1 && la2) {
-                        let doc = Doc.AreProtosEqual(la1, this.dataDoc) ? la2 : la1;
-                        let seek = DateCast(la1.creationTime);
-                        setTimeout(() => this.playFrom(seek.date.getTime()), 250);
-                    }
+                scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
+                    let la1 = l.anchor1 as Doc;
+                    let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
+                    setTimeout(() => this.playFrom(linkTime), 250);
                 });
-                scrollLinkId && (this.layoutDoc.scrollLinkID = undefined);
+                scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
             }, { fireImmediately: true });
         this._reactionDisposer = reaction(() => SelectionManager.SelectedDocuments(),
             selected => {
                 let sel = selected.length ? selected[0].props.Document : undefined;
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
-        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, time => this.Document.playOnSelect && this.playFrom(time));
+        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, timeInMillisecondsFrom1970 => {
+            let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
+            start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
+        });
     }
 
     updateHighlights = () => {
@@ -76,14 +76,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         const start = extensionDoc && DateCast(extensionDoc.recordingStart);
         if (htmlEle && !htmlEle.paused && start) {
             setTimeout(this.updateHighlights, 30);
+            this.Document.currentTimecode = htmlEle.currentTime;
             DocListCast(this.dataDoc.links).map(l => {
                 let la1 = l.anchor1 as Doc;
+                let linkTime = NumCast(l.anchor2Timecode);
                 if (Doc.AreProtosEqual(la1, this.dataDoc)) {
                     la1 = l.anchor2 as Doc;
+                    linkTime = NumCast(l.anchor1Timecode);
                 }
-                let date = DateCast(la1.creationDate);
-                let offset = (date!.date.getTime() - start.date.getTime()) / 1000;
-                if (offset > this._lastUpdate && offset < htmlEle.currentTime) {
+                if (linkTime > this._lastUpdate && linkTime < htmlEle.currentTime) {
                     Doc.linkFollowHighlight(la1);
                 }
             });
@@ -91,16 +92,15 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         }
     }
 
-    playFrom = (seek: number) => {
+    playFrom = (seekTimeInSeconds: number) => {
         const extensionDoc = this.extensionDoc;
         let start = extensionDoc && DateCast(extensionDoc.recordingStart);
         if (this._ele && start) {
-            if (seek) {
-                let delta = (seek - start.date.getTime()) / 1000;
-                if (start && delta > 0 && delta < this._ele.duration) {
-                    this._ele.currentTime = delta;
+            if (seekTimeInSeconds) {
+                if (seekTimeInSeconds >= 0 && seekTimeInSeconds <= this._ele.duration) {
+                    this._ele.currentTime = seekTimeInSeconds;
                     this._ele.play();
-                    this._lastUpdate = delta;
+                    this._lastUpdate = seekTimeInSeconds;
                     setTimeout(this.updateHighlights, 0);
                 } else {
                     this._ele.pause();
@@ -117,6 +117,14 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
         this._scrubbingDisposer && this._scrubbingDisposer();
     }
 
+
+    updateRecordTime = () => {
+        if (this._audioState === "recording") {
+            setTimeout(this.updateRecordTime, 30);
+            this.Document.currentTimecode = (new Date().getTime() - this._recordStart) / 1000;
+        }
+    }
+
     recordAudioAnnotation = () => {
         let gumStream: any;
         let self = this;
@@ -140,7 +148,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 // upload to server with known URL 
                 self.props.Document[self.props.fieldKey] = new AudioField(url);
             };
-            runInAction(() => self._audioState = 1);
+            runInAction(() => self._audioState = "recording");
+            self._recordStart = new Date().getTime();
+            setTimeout(self.updateRecordTime, 0);
             self._recorder.start();
             setTimeout(() => {
                 self.stopRecording();
@@ -158,7 +168,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     stopRecording = action(() => {
         this._recorder.stop();
-        this._audioState = 2;
+        this._audioState = "recorded";
         let ind = AudioBox.ActiveRecordings.indexOf(this.props.Document);
         ind !== -1 && (AudioBox.ActiveRecordings.splice(ind, 1));
     });
@@ -174,6 +184,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     setRef = (e: HTMLAudioElement | null) => {
         e && e.addEventListener("play", this.playClick as any);
+        e && e.addEventListener("timeupdate", () => this.props.Document.currentTimecode = e!.currentTime);
         this._ele = e;
     }
 
@@ -197,8 +208,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
             <div className={`audiobox-container`} onContextMenu={this.specificContextMenu} onClick={!this.path ? this.recordClick : undefined}>
                 <div className="audiobox-handle"></div>
                 {!this.path ?
-                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: ["black", "red", "blue"][this._audioState] }}>
-                        {this._audioState === 1 ? "STOP" : "RECORD"}
+                    <button className={`audiobox-record${interactive}`} style={{ backgroundColor: this._audioState === "recording" ? "red" : "black" }}>
+                        {this._audioState === "recording" ? "STOP" : "RECORD"}
                     </button> :
                     this.audio
                 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 67c85e158..c82fd0f0f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -165,7 +165,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 Doc.UnBrushDoc(this.props.Document);
             } else if (this.onClickHandler && this.onClickHandler.script) {
                 this.onClickHandler.script.run({ this: this.Document.isTemplateField && this.props.DataDoc ? this.props.DataDoc : this.props.Document }, console.log);
-            } else if (this.props.Document.type === DocumentType.BUTTON) {
+            } else if (this.Document.type === DocumentType.BUTTON) {
                 ScriptBox.EditButtonScript("On Button Clicked ...", this.props.Document, "onClick", e.clientX, e.clientY);
             } else if (this.Document.isButton) {
                 SelectionManager.SelectDoc(this, e.ctrlKey); // don't think this should happen if a button action is actually triggered.
@@ -179,8 +179,8 @@ 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 maximizedDocs = await DocListCastAsync(this.Document.maximizedDocs);
+        let summarizedDocs = await DocListCastAsync(this.Document.summarizedDocs);
         let linkDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
         let expandedDocs: Doc[] = [];
         expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -333,9 +333,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 = StrCast(this.props.Document.layout);
+            let oldLayout = this.Document.layout || "";
             let layout = text.replace("{layout}", oldLayout);
-            this.props.Document.layout = layout;
+            this.Document.layout = layout;
             e.stopPropagation();
             e.preventDefault();
         }
@@ -355,7 +355,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @undoBatch
     @action
     makeIntoPortal = async () => {
-        let anchors = await Promise.all(DocListCast(this.props.Document.links).map(async (d: Doc) => Cast(d.anchor2, Doc)));
+        let anchors = await Promise.all(DocListCast(this.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 => {
@@ -590,7 +590,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     render() {
         if (!this.props.Document) return (null);
-        let animDims = this.props.Document.animateToDimensions ? Array.from(Cast(this.props.Document.animateToDimensions, listSpec("number"))!) : undefined;
+        const animDims = this.Document.animateToDimensions ? Array.from(this.Document.animateToDimensions) : undefined;
         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");
@@ -644,7 +644,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
                 style={{
-                    transition: this.props.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
+                    transition: this.Document.isAnimating !== undefined ? ".5s linear" : StrCast(this.Document.transition),
                     pointerEvents: this.Document.isBackground && !this.isSelected() ? "none" : "all",
                     color: StrCast(this.Document.color),
                     outline: highlighting && !borderRounding ? `${highlightColors[fullDegree]} ${highlightStyles[fullDegree]} ${localScale}px` : "solid 0px",
@@ -658,7 +658,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={() => Doc.BrushDoc(this.props.Document)} onPointerLeave={() => Doc.UnBrushDoc(this.props.Document)}
             >
-                {this.props.Document.links && DocListCast(this.props.Document.links).map((d, i) =>
+                {this.Document.links && DocListCast(this.Document.links).map((d, i) =>
                     //this.linkEndpointDoc(d).type === DocumentType.PDFANNO ? (null) :
                     <div style={{ pointerEvents: "none", position: "absolute", transformOrigin: "top left", width: "100%", height: "100%", transform: `scale(${this.layoutDoc.fitWidth ? 1 : 1 / this.props.ContentScaling()})` }}>
                         <DocumentView {...this.props} backgroundColor={returnTransparent} Document={d} layoutKey={this.linkEndpoint(d)} />
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5c2d39d98..5588bb4c9 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -605,7 +605,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                         setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
                         setTimeout(() => this.unhighlightSearchTerms(), 2000);
                     }
-                    this.layoutDoc.scrollToLinkID = undefined;
+                    Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
                 }
 
             },
diff --git a/src/new_fields/documentSchemas.ts b/src/new_fields/documentSchemas.ts
index 8c3b62067..e2730914f 100644
--- a/src/new_fields/documentSchemas.ts
+++ b/src/new_fields/documentSchemas.ts
@@ -1,9 +1,10 @@
 import { makeInterface, createSchema, listSpec } from "./Schema";
 import { ScriptField } from "./ScriptField";
 import { Doc } from "./Doc";
+import { DateField } from "./DateField";
 
 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
+    layout: "string",           // this is the native layout string for the document.  templates can be added using other fields and setting layoutKey below (see layoutCustom as an example)
     layoutKey: "string",        // holds the field key for the field that actually holds the current lyoat
     layoutCustom: Doc,          // used to hold a custom layout (there's nothing special about this field .. any field could hold a custom layout that can be selected by setting 'layoutKey')
     title: "string",            // document title (can be on either data document or layout)
@@ -14,7 +15,8 @@ export const documentSchema = createSchema({
     color: "string",            // foreground color of document
     backgroundColor: "string",  // background color of document
     opacity: "number",          // opacity of document
-    //links: listSpec(Doc),       // computed (readonly) list of links associated with this document
+    creationDate: DateField,     // when the document was created
+    links: listSpec(Doc),       // computed (readonly) list of links associated with this document
     dropAction: "string",       // override specifying what should happen when this document is dropped (can be "alias" or "copy")
     removeDropProperties: listSpec("string"), // properties that should be removed from the alias/copy/etc of this document when it is dropped
     onClick: ScriptField,       // script to run when document is clicked (can be overriden by an onClick prop)
@@ -25,6 +27,9 @@ export const documentSchema = createSchema({
     isTemplateField: "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
+    currentTimecode: "number",   // current play back time of a temporal document (video / audio)
+    summarizedDocs: listSpec(Doc), // documents that are summarized by this document (and which will typically be opened by clicking this document)
+    maximizedDocs: listSpec(Doc), // documents to maximize when clicking this document (generally this document will be an icon)
     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
     inOverlay: "boolean",       // whether the document is rendered in an OverlayView which handles selection/dragging differently
@@ -33,8 +38,11 @@ export const documentSchema = createSchema({
     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)      
+    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) 
+    isAnimating: "boolean",     // whether the document is in the midst of animating between two layouts (used by icons to de/iconify documents).
+    animateToDimensions: listSpec("number"), // layout information about the target rectangle a document is animating towards 
+    scrollToLinkID: "string",   // id of link being traversed. allows this doc to scroll/highlight/etc its link anchor. scrollToLinkID should be set to undefined by this doc after it sets up its scroll,etc.
 });
 
 export const positionSchema = createSchema({
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 95ebe3cb6..044ec746b 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -83,10 +83,10 @@ export class CurrentUserUtils {
 
         return Docs.Create.ButtonDocument({
             width: 35, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Create", targetContainer: sidebarContainer,
-            panel: Docs.Create.StackingDocument([dragCreators, color], {
+            sourcePanel: Docs.Create.StackingDocument([dragCreators, color], {
                 width: 500, height: 800, chromeStatus: "disabled", title: "creator stack"
             }),
-            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel"),
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
         });
     }
 
@@ -108,11 +108,11 @@ export class CurrentUserUtils {
 
         return Docs.Create.ButtonDocument({
             width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Library",
-            panel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
+            sourcePanel: Docs.Create.TreeDocument([doc.workspaces as Doc, doc.documents as Doc, doc.recentlyClosed as Doc], {
                 title: "Library", xMargin: 5, yMargin: 5, gridGap: 5, forceActive: true, dropAction: "alias", lockedPosition: true
             }),
             targetContainer: sidebarContainer,
-            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
         });
     }
 
@@ -120,11 +120,11 @@ export class CurrentUserUtils {
     static setupSearchPanel(sidebarContainer: Doc) {
         return Docs.Create.ButtonDocument({
             width: 50, height: 35, borderRounding: "50%", boxShadow: "2px 2px 1px", title: "Search",
-            panel: Docs.Create.QueryDocument({
+            sourcePanel: Docs.Create.QueryDocument({
                 title: "search stack", ignoreClick: true
             }),
             targetContainer: sidebarContainer,
-            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.panel")
+            onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
         });
     }
 
@@ -190,7 +190,7 @@ export class CurrentUserUtils {
             stackingDoc && PromiseValue(Cast(stackingDoc.data, listSpec(Doc))).then(sidebarButtons => {
                 sidebarButtons && sidebarButtons.map((sidebarBtn, i) => {
                     sidebarBtn && PromiseValue(Cast(sidebarBtn, Doc)).then(async btn => {
-                        btn && btn.panel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
+                        btn && btn.sourcePanel && btn.targetContainer && i === 1 && (btn.onClick as ScriptField).script.run({ this: btn });
                     });
                 });
             });
-- 
cgit v1.2.3-70-g09d2


From 86e985ef3a96d9849799e436d2158fef0b2b7f94 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 23 Oct 2019 11:45:08 -0400
Subject: added scrubbing from text notes

---
 src/client/views/InkingStroke.tsx           | 2 +-
 src/client/views/nodes/AudioBox.tsx         | 5 +++--
 src/client/views/nodes/FormattedTextBox.tsx | 3 +++
 3 files changed, 7 insertions(+), 3 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 7bbf71482..332c22512 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -34,7 +34,7 @@ export class InkingStroke extends React.Component<StrokeProps> {
             e.preventDefault();
         }
         if (InkingControl.Instance.selectedTool === InkTool.Scrubber && e.buttons === 1) {
-            runInAction(() => AudioBox.ScrubTime = this.props.creationTime);
+            AudioBox.SetScrubTime(this.props.creationTime);
             e.stopPropagation();
             e.preventDefault();
         }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 135874400..e9a11c9b6 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -45,7 +45,8 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     _lastUpdate = 0;
 
     @observable private _audioState: "unrecorded" | "recording" | "recorded" = "unrecorded";
-    @observable public static ScrubTime = 0;
+    @observable private static _scrubTime = 0;
+    public static SetScrubTime = action((timeInMillisFrom1970: number) => AudioBox._scrubTime = timeInMillisFrom1970);
     public static ActiveRecordings: Doc[] = [];
 
     componentDidMount() {
@@ -64,7 +65,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 let sel = selected.length ? selected[0].props.Document : undefined;
                 this.Document.playOnSelect && sel && !Doc.AreProtosEqual(sel, this.props.Document) && this.playFrom(DateCast(sel.creationTime).date.getTime());
             });
-        this._scrubbingDisposer = reaction(() => AudioBox.ScrubTime, timeInMillisecondsFrom1970 => {
+        this._scrubbingDisposer = reaction(() => AudioBox._scrubTime, timeInMillisecondsFrom1970 => {
             let start = this.extensionDoc && DateCast(this.extensionDoc.recordingStart);
             start && this.playFrom((timeInMillisecondsFrom1970 - start.date.getTime()) / 1000);
         });
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 5588bb4c9..566b698bd 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -45,6 +45,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ContextMenu } from '../ContextMenu';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { documentSchema } from '../../../new_fields/documentSchemas';
+import { AudioBox } from './AudioBox';
 
 library.add(faEdit);
 library.add(faSmile, faTextHeight, faUpload);
@@ -190,6 +191,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                 FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
             }
 
+            let tsel = this._editorView.state.selection.$from;
+            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 5000)));
             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())));
-- 
cgit v1.2.3-70-g09d2


From c97b98c165e318da787361b8fc11382b0b98afa4 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 24 Oct 2019 11:54:03 -0400
Subject: several fixes to audioboxes.

---
 src/client/documents/Documents.ts                  |  3 ++-
 .../collections/CollectionMasonryViewFieldRow.tsx  |  2 +-
 .../views/collections/CollectionStackingView.scss  |  5 ++++
 .../CollectionFreeFormLinkView.scss                |  1 +
 .../CollectionFreeFormLinkView.tsx                 |  3 +++
 src/client/views/nodes/AudioBox.scss               | 28 ++++++++++++++--------
 src/client/views/nodes/AudioBox.tsx                | 19 +++++++--------
 src/client/views/nodes/DocuLinkBox.tsx             |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 9 files changed, 40 insertions(+), 25 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f26594e04..d1fcabc4a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -171,7 +171,7 @@ export namespace Docs {
             }],
             [DocumentType.AUDIO, {
                 layout: { view: AudioBox, dataField: data },
-                options: { height: 35, backgroundColor: "lightGray", borderRounding: "20%" }
+                options: { height: 35, backgroundColor: "lightGray" }
             }],
             [DocumentType.PDF, {
                 layout: { view: PDFBox, dataField: data },
@@ -714,6 +714,7 @@ export namespace DocUtils {
             linkDocProto.anchor2Timecode = target.doc.currentTimecode;
             linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
             linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
+            linkDocProto.borderRounding = "20";
             linkDocProto.width = linkDocProto.height = 0;
             linkDocProto.isBackground = true;
             linkDocProto.isButton = true;
diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
index 4259e948b..0249b90b4 100644
--- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
+++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx
@@ -366,7 +366,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr
                         >
                             {headingView}
                             {collapsed ? (null) :
-                                < div >
+                                < div style={{ position: "relative" }}>
                                     <div key={`${heading}-stack`} className={`collectionStackingView-masonryGrid`}
                                         ref={this._contRef}
                                         style={{
diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss
index 3f4b678c7..1ff2275d4 100644
--- a/src/client/views/collections/CollectionStackingView.scss
+++ b/src/client/views/collections/CollectionStackingView.scss
@@ -21,6 +21,11 @@
     overflow-y: auto;
     flex-wrap: wrap;
     transition: top .5s;
+    >div {
+        height: 100%;
+        position: relative;
+        display: inline-block;
+    }
 
     .collectionSchemaView-previewDoc {
         height: 100%;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 1f1bca2f2..75af11537 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -3,6 +3,7 @@
     opacity: 0.8;
     pointer-events: all;
     stroke-width: 3px;
+    transition: opacity 0.5s ease-in;
 }
 .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 962fe2a1c..837413842 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -17,10 +17,12 @@ export interface CollectionFreeFormLinkViewProps {
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
     @observable _alive: number = 0;
+    @observable _opacity: number = 1;
     @action
     componentDidMount() {
         this._alive = 1;
         setTimeout(this.rerender, 50);
+        setTimeout(action(() => this._opacity = 0.05), 50);
     }
     @action
     componentWillUnmount() {
@@ -42,6 +44,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         let pt1 = Utils.getNearestPointInPerimeter(a.left, a.top, a.width, a.height, b.left + b.width / 2, b.top + b.height / 2);
         let pt2 = Utils.getNearestPointInPerimeter(b.left, b.top, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
         return (<line key="linkLine" className="collectionfreeformlinkview-linkLine"
+            style={{ opacity: this._opacity }}
             x1={`${pt1[0]}`} y1={`${pt1[1]}`}
             x2={`${pt2[0]}`} y2={`${pt2[1]}`} />);
     }
diff --git a/src/client/views/nodes/AudioBox.scss b/src/client/views/nodes/AudioBox.scss
index ccbf0d75f..3b19a6dba 100644
--- a/src/client/views/nodes/AudioBox.scss
+++ b/src/client/views/nodes/AudioBox.scss
@@ -44,22 +44,27 @@
             width:100%;
             height: 80%;
             position: relative;
+            padding-right: 5px;
             display: flex;
             .audiobox-playhead {
                 position: relative;
                 margin-top: auto;
                 margin-bottom: auto;
                 width: 25px;
+                padding: 2px;
             }
             .audiobox-timeline {
                 position:relative;
                 height:100%;
                 width:100%;
+                background: white;
+                border: gray solid 1px;
+                border-radius: 3px;
                 .audiobox-current {
                     width: 1px;
                     height:100%;
                     background-color: red;
-                    position: absolute;;
+                    position: absolute;
                 }
                 .audiobox-linker, .audiobox-linker-mini {
                     position:absolute;
@@ -72,10 +77,11 @@
                     background-color: transparent;
                     box-shadow: black 2px 2px 1px;
                     .docuLinkBox-cont {
-                        width:15px !important;
-                        height:15px !important;
-                        left: calc(100% - 15px) !important;
-                        top: calc(100% - 15px) !important;
+                        position: relative !important;
+                        height: 100%  !important;
+                        width: 100% !important;
+                        left:unset !important;
+                        top:unset !important;
                     }
                 }
                 .audiobox-linker-mini {
@@ -86,10 +92,11 @@
                     margin-left: -1;
                     margin-top: -2;
                     .docuLinkBox-cont {
-                        width:8px !important;
-                        height:8px !important;
-                        left: calc(100% - 8px) !important;
-                        top: calc(100% - 8px) !important;
+                        position: relative !important;
+                        height: 100%  !important;
+                        width: 100% !important;
+                        left:unset !important;
+                        top:unset !important;
                     }
                 }
                 .audiobox-linker:hover, .audiobox-linker-mini:hover {
@@ -98,7 +105,8 @@
                 .audiobox-marker-container, .audiobox-marker-minicontainer {
                     position:absolute;
                     width:10px;
-                    height:100%;
+                    height:90%;
+                    top:2.5%;
                     background:gray;
                     border-radius: 5px;
                     box-shadow: black 2px 2px 1px;
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index cc1c63d44..bd1caaf1b 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -12,12 +12,11 @@ import { RouteStore } from "../../../server/RouteStore";
 import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
 import { DateField } from "../../../new_fields/DateField";
 import { SelectionManager } from "../../util/SelectionManager";
-import { Doc, DocListCast, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { ContextMenuProps } from "../ContextMenuItem";
 import { ContextMenu } from "../ContextMenu";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { DocumentManager } from "../../util/DocumentManager";
 import { DocumentView } from "./DocumentView";
 
 interface Window {
@@ -59,7 +58,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                 scrollLinkId && DocListCast(this.dataDoc.links).filter(l => l[Id] === scrollLinkId).map(l => {
                     let la1 = l.anchor1 as Doc;
                     let linkTime = Doc.AreProtosEqual(la1, this.dataDoc) ? NumCast(l.anchor1Timecode) : NumCast(l.anchor2Timecode);
-                    setTimeout(() => this.playFrom(linkTime), 250);
+                    setTimeout(() => { this.playFrom(linkTime); Doc.linkFollowHighlight(l); }, 250);
                 });
                 scrollLinkId && Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
             }, { fireImmediately: true });
@@ -75,11 +74,9 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     timecodeChanged = () => {
-        const extensionDoc = this.extensionDoc;
         const htmlEle = this._ele;
-        const start = extensionDoc && DateCast(extensionDoc.recordingStart);
-        if (start && htmlEle) {
-            htmlEle && htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
+        if (this._audioState === "recorded" && htmlEle) {
+            htmlEle.duration && htmlEle.duration !== Infinity && runInAction(() => this.dataDoc.duration = htmlEle.duration);
             DocListCast(this.dataDoc.links).map(l => {
                 let la1 = l.anchor1 as Doc;
                 let linkTime = NumCast(l.anchor2Timecode);
@@ -195,7 +192,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
     }
 
     setRef = (e: HTMLAudioElement | null) => {
-        e && e.addEventListener("timeupdate", action(() => this.Document.currentTimecode = e!.currentTime));
+        e && e.addEventListener("timeupdate", this.timecodeChanged);
         this._ele = e;
     }
 
@@ -225,13 +222,13 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
                     </button> :
                     <div className="audiobox-controls">
                         <div className="audiobox-player" onClick={this.onPlay}>
-                            <FontAwesomeIcon className="audiobox-playhead" icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} />
-                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon className="audiobox-playhead" icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                            <div className="audiobox-playhead"> <FontAwesomeIcon style={{ width: "100%" }} icon={this._playing ? "pause" : "play"} size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
+                            <div className="audiobox-playhead" onClick={this.onStop}><FontAwesomeIcon style={{ width: "100%" }} icon="stop" size={this.props.PanelHeight() < 36 ? "1x" : "2x"} /></div>
                             <div className="audiobox-timeline" onClick={e => e.stopPropagation()}
                                 onPointerDown={e => {
                                     if (e.button === 0 && !e.ctrlKey) {
                                         let rect = (e.target as any).getBoundingClientRect();
-                                        this._ele!.currentTime = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
+                                        this._ele!.currentTime = this.Document.currentTimecode = (e.clientX - rect.x) / rect.width * NumCast(this.dataDoc.duration);
                                         this.pause();
                                         e.stopPropagation();
                                     }
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index f2ab6fcd8..8f68effe8 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -73,7 +73,7 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         let c = StrCast(this.props.Document.backgroundColor, "lightblue");
         return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.Document[this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"]! as Doc).title)}
             ref={this._ref} style={{
-                background: c, width: "25px", left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
+                background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
                 transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})`
             }} />;
     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 566b698bd..cf55a10ba 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -192,7 +192,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
             }
 
             let tsel = this._editorView.state.selection.$from;
-            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 5000)));
+            tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
             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())));
-- 
cgit v1.2.3-70-g09d2


From cb7078190a06ea19cb443626b9023b93c1930e3a Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Mon, 4 Nov 2019 11:35:50 -0500
Subject: getting back in the saddle: small changes -audio play icons, link
 titling, dictation.

---
 src/client/views/nodes/AudioBox.tsx         | 1 +
 src/client/views/nodes/DocuLinkBox.tsx      | 5 ++++-
 src/client/views/nodes/FormattedTextBox.tsx | 2 +-
 3 files changed, 6 insertions(+), 2 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 268255b46..d652a11d5 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -193,6 +193,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
 
     setRef = (e: HTMLAudioElement | null) => {
         e && e.addEventListener("timeupdate", this.timecodeChanged);
+        e && e.addEventListener("ended", this.pause);
         this._ele = e;
     }
 
diff --git a/src/client/views/nodes/DocuLinkBox.tsx b/src/client/views/nodes/DocuLinkBox.tsx
index f4c7d43aa..d73407903 100644
--- a/src/client/views/nodes/DocuLinkBox.tsx
+++ b/src/client/views/nodes/DocuLinkBox.tsx
@@ -72,7 +72,10 @@ export class DocuLinkBox extends DocComponent<FieldViewProps, DocLinkSchema>(Doc
         let y = NumCast(this.props.Document[this.props.fieldKey + "_y"], 100);
         let x = NumCast(this.props.Document[this.props.fieldKey + "_x"], 100);
         let c = StrCast(this.props.Document.backgroundColor, "lightblue");
-        return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={StrCast((this.props.Document[this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1"]! as Doc).title)}
+        let anchor = this.props.fieldKey === "anchor1" ? "anchor2" : "anchor1";
+        let timecode = this.props.Document[anchor + "Timecode"];
+        let targetTitle = StrCast((this.props.Document[anchor]! as Doc).title) + (timecode !== undefined ? ":" + timecode : "");
+        return <div className="docuLinkBox-cont" onPointerDown={this.onPointerDown} onClick={this.onClick} title={targetTitle}
             ref={this._ref} style={{
                 background: c, left: `calc(${x}% - 12.5px)`, top: `calc(${y}% - 12.5px)`,
                 transform: `scale(${hasAnchor ? 0.333 : 1 / this.props.ContentScaling()})`
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index cf55a10ba..037c92a62 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1059,7 +1059,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                         e.stopPropagation();
                     }} >
                     <FontAwesomeIcon className="formattedTExtBox-audioFont"
-                        style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"file-audio"} size="sm" />
+                        style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"microphone"} size="sm" />
                 </div>
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From d220b23867050fb6565fe639c3be28080f589aa2 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 11 Nov 2019 18:39:23 -0500
Subject: made font size and font style text marks be parametrized to simplify
 api.

---
 src/client/util/RichTextSchema.tsx          | 154 ++--------------
 src/client/util/TooltipTextMenu.tsx         | 277 +++++++++++++++++-----------
 src/client/views/nodes/FormattedTextBox.tsx |  15 +-
 3 files changed, 190 insertions(+), 256 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a224c57eb..a5e0ca720 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -483,72 +483,26 @@ 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: {
-        parseDOM: [{ style: 'font-family: "Times New Roman", Times, serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: "Times New Roman", Times, serif;'
-        }]
-    },
-
-    arial: {
-        parseDOM: [{ style: 'font-family: Arial, Helvetica, sans-serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: Arial, Helvetica, sans-serif;'
-        }]
-    },
-
-    georgia: {
-        parseDOM: [{ style: 'font-family: Georgia, serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: Georgia, serif;'
-        }]
-    },
-
-    comicSans: {
-        parseDOM: [{ style: 'font-family: "Comic Sans MS", cursive, sans-serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: "Comic Sans MS", cursive, sans-serif;'
-        }]
-    },
-
-    tahoma: {
-        parseDOM: [{ style: 'font-family: Tahoma, Geneva, sans-serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: Tahoma, Geneva, sans-serif;'
-        }]
-    },
-
-    impact: {
-        parseDOM: [{ style: 'font-family: Impact, Charcoal, sans-serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: Impact, Charcoal, sans-serif;'
-        }]
-    },
-
-    crimson: {
-        parseDOM: [{ style: 'font-family: "Crimson Text", sans-serif;' }],
-        toDOM: () => ['span', {
-            style: 'font-family: "Crimson Text", sans-serif;'
+    pFontFamily: {
+        attrs: {
+            family: { default: "Crimson Text" },
+        },
+        parseDOM: [{
+            tag: "span", getAttrs(dom: any) {
+                let cstyle = getComputedStyle(dom);
+                if (cstyle.font) {
+                    if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
+                    if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
+                    if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
+                    if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
+                    if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
+                    if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
+                }
+            }
+        }],
+        toDOM: (node) => ['span', {
+            style: `font-family: "${node.attrs.family}";`
         }]
     },
 
@@ -574,76 +528,6 @@ export const marks: { [index: string]: MarkSpec } = {
             style: `font-size: ${node.attrs.fontSize}px;`
         }]
     },
-
-    p10: {
-        parseDOM: [{ style: 'font-size: 10px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 10px;'
-        }]
-    },
-
-    p12: {
-        parseDOM: [{ style: 'font-size: 12px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 12px;'
-        }]
-    },
-
-    p14: {
-        parseDOM: [{ style: 'font-size: 14px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 14px;'
-        }]
-    },
-
-    p16: {
-        parseDOM: [{ style: 'font-size: 16px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 16px;'
-        }]
-    },
-
-    p18: {
-        parseDOM: [{ style: 'font-size: 18px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 18px;'
-        }]
-    },
-
-    p20: {
-        parseDOM: [{ style: 'font-size: 20px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 20px;'
-        }]
-    },
-
-    p24: {
-        parseDOM: [{ style: 'font-size: 24px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 24px;'
-        }]
-    },
-
-    p32: {
-        parseDOM: [{ style: 'font-size: 32px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 32px;'
-        }]
-    },
-
-    p48: {
-        parseDOM: [{ style: 'font-size: 48px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 48px;'
-        }]
-    },
-
-    p72: {
-        parseDOM: [{ style: 'font-size: 72px;' }],
-        toDOM: () => ['span', {
-            style: 'font-size: 72px;'
-        }]
-    },
 };
 
 export class ImageResizeView {
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 5cbf8b761..9ce7acec8 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -29,11 +29,9 @@ 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 fontSizeToNum: Map<MarkType, number>;
-    private fontStylesToName: Map<MarkType, string>;
+    private fontSizes: Mark[] = [];
+    private fontStyles: Mark[] = [];
     private listTypeToIcon: Map<NodeType | any, string>;
     //private link: HTMLAnchorElement;
     private wrapper: HTMLDivElement;
@@ -135,46 +133,32 @@ export class TooltipTextMenu {
 
 
         //list of font styles
-        this.fontStylesToName = new Map();
-        this.fontStylesToName.set(schema.marks.timesNewRoman, "Times New Roman");
-        this.fontStylesToName.set(schema.marks.arial, "Arial");
-        this.fontStylesToName.set(schema.marks.georgia, "Georgia");
-        this.fontStylesToName.set(schema.marks.comicSans, "Comic Sans MS");
-        this.fontStylesToName.set(schema.marks.tahoma, "Tahoma");
-        this.fontStylesToName.set(schema.marks.impact, "Impact");
-        this.fontStylesToName.set(schema.marks.crimson, "Crimson Text");
-        this.fontStyles = Array.from(this.fontStylesToName.keys());
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Times New Roman" }));
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Arial" }));
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Georgia" }));
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Comic Sans MS" }));
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Tahoma" }));
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Impact" }));
+        this.fontStyles.push(schema.marks.pFontFamily.create({ family: "Crimson Text" }));
 
         //font sizes
-        this.fontSizeToNum = new Map();
-        this.fontSizeToNum.set(schema.marks.p10, 10);
-        this.fontSizeToNum.set(schema.marks.p12, 12);
-        this.fontSizeToNum.set(schema.marks.p14, 14);
-        this.fontSizeToNum.set(schema.marks.p16, 16);
-        this.fontSizeToNum.set(schema.marks.p18, 18);
-        this.fontSizeToNum.set(schema.marks.p20, 20);
-        this.fontSizeToNum.set(schema.marks.p24, 24);
-        this.fontSizeToNum.set(schema.marks.p32, 32);
-        this.fontSizeToNum.set(schema.marks.p48, 48);
-        this.fontSizeToNum.set(schema.marks.p72, 72);
-        this.fontSizeToNum.set(schema.marks.pFontSize, 10);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 12);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 14);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 16);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 18);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 20);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 24);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 32);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 48);
-        // this.fontSizeToNum.set(schema.marks.pFontSize, 72);
-        this.fontSizes = Array.from(this.fontSizeToNum.keys());
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 10 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 12 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 14 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 16 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 18 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 20 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 24 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 32 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 48 }));
+        this.fontSizes.push(schema.marks.pFontSize.create({ fontSize: 72 }));
 
         //list types
         this.listTypeToIcon = new Map();
         //this.listTypeToIcon.set(schema.nodes.bullet_list, ":");
         this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "bullet" }), ":");
-        this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1");
-        this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A");
+        this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "decimal" }), "1.1)");
+        this.listTypeToIcon.set(schema.nodes.ordered_list.create({ mapStyle: "multi" }), "1.A)");
         // this.listTypeToIcon.set(schema.nodes.bullet_list, "⬜");
         this.listTypes = Array.from(this.listTypeToIcon.keys());
 
@@ -195,16 +179,13 @@ export class TooltipTextMenu {
 
     //label of dropdown will change to given label
     updateFontSizeDropdown(label: string) {
-        //filtering function - might be unecessary
-        let cut = (arr: MenuItem[]) => arr.filter(x => x);
-
         //font SIZES
         let fontSizeBtns: MenuItem[] = [];
-        this.fontSizeToNum.forEach((number, mark) => {
-            fontSizeBtns.push(this.dropdownMarkBtn(String(number), "color: black; width: 50px;", mark, this.view, this.changeToMarkInGroup, this.fontSizes));
+        this.fontSizes.forEach(mark => {
+            fontSizeBtns.push(this.dropdownFontSizeBtn(String(mark.attrs.fontSize), "color: black; width: 50px;", mark, this.view, this.changeToFontSize));
         });
 
-        let newfontSizeDom = (new Dropdown(cut(fontSizeBtns), {
+        let newfontSizeDom = (new Dropdown(fontSizeBtns, {
             label: label,
             css: "color:black; min-width: 60px; padding-left: 5px; margin-right: 0;"
         }) as MenuItem).render(this.view).dom;
@@ -215,20 +196,15 @@ export class TooltipTextMenu {
         this.fontSizeDom = newfontSizeDom;
     }
 
-    // Make the DIV element draggable
-
     //label of dropdown will change to given label
     updateFontStyleDropdown(label: string) {
-        //filtering function - might be unecessary
-        let cut = (arr: MenuItem[]) => arr.filter(x => x);
-
         //font STYLES
         let fontBtns: MenuItem[] = [];
-        this.fontStylesToName.forEach((name, mark) => {
-            fontBtns.push(this.dropdownMarkBtn(name, "color: black; font-family: " + name + ", sans-serif; width: 125px;", mark, this.view, this.changeToMarkInGroup, this.fontStyles));
+        this.fontStyles.forEach((mark) => {
+            fontBtns.push(this.dropdownFontFamilyBtn(mark.attrs.family, "color: black; font-family: " + mark.attrs.family + ", sans-serif; width: 125px;", mark, this.view, this.changeToFontFamily));
         });
 
-        let newfontStyleDom = (new Dropdown(cut(fontBtns), {
+        let newfontStyleDom = (new Dropdown(fontBtns, {
             label: label,
             css: "color:black; width: 125px; margin-left: -3px; padding-left: 2px;"
         }) as MenuItem).render(this.view).dom;
@@ -468,8 +444,7 @@ export class TooltipTextMenu {
         this.tooltip.appendChild(listTypeBtn);
         return listTypeBtn;
     }
-
-    //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected text
+    //for a specific grouping of marks (passed in), remove all and apply the passed-in one to the selected textchangeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
     changeToMarkInGroup = (markType: MarkType | undefined, view: EditorView, fontMarks: MarkType[]) => {
         let { $cursor, ranges } = view.state.selection as TextSelection;
         let state = view.state;
@@ -498,29 +473,6 @@ export class TooltipTextMenu {
         });
 
         if (markType) {
-            // fontsize
-            if (markType.name[0] === 'p') {
-                let size = this.fontSizeToNum.get(markType);
-                if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
-                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); }
-                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
             if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
                 let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
@@ -531,6 +483,90 @@ export class TooltipTextMenu {
         }
     }
 
+    changeToFontFamily = (mark: Mark, view: EditorView) => {
+        let { $cursor, ranges } = view.state.selection as TextSelection;
+        let state = view.state;
+        let dispatch = view.dispatch;
+
+        //remove all other active font marks
+        if ($cursor) {
+            if (view.state.schema.marks.pFontFamily.isInSet(state.storedMarks || $cursor.marks())) {
+                dispatch(state.tr.removeStoredMark(view.state.schema.marks.pFontFamily));
+            }
+        } else {
+            let has = false;
+            for (let i = 0; !has && i < ranges.length; i++) {
+                let { $from, $to } = ranges[i];
+                has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontFamily);
+            }
+            for (let i of ranges) {
+                if (has) {
+                    toggleMark(view.state.schema.marks.pFontFamily)(view.state, view.dispatch, view);
+                }
+            }
+        }
+
+        let fontName = mark.attrs.family;
+        if (fontName) { this.updateFontStyleDropdown(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
+        if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
+            let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+                { ...(view.state.selection as NodeSelection).node.attrs, setFontFamily: fontName }), view.state.schema);
+            view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
+        }
+        else view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, view.state.schema.marks.pFontFamily.create({ family: fontName })));
+        view.state.storedMarks = [...(view.state.storedMarks || []), view.state.schema.marks.pFontFamily.create({ family: fontName })];
+    }
+
+    changeToFontSize = (mark: Mark, view: EditorView) => {
+        let { $cursor, ranges } = view.state.selection as TextSelection;
+        let state = view.state;
+        let dispatch = view.dispatch;
+
+        //remove all other active font marks
+        if ($cursor) {
+            if (view.state.schema.marks.pFontSize.isInSet(state.storedMarks || $cursor.marks())) {
+                dispatch(state.tr.removeStoredMark(view.state.schema.marks.pFontSize));
+            }
+        } else {
+            let has = false;
+            for (let i = 0; !has && i < ranges.length; i++) {
+                let { $from, $to } = ranges[i];
+                has = state.doc.rangeHasMark($from.pos, $to.pos, view.state.schema.marks.pFontSize);
+            }
+            for (let i of ranges) {
+                if (has) {
+                    toggleMark(view.state.schema.marks.pFontSize)(view.state, view.dispatch, view);
+                }
+            }
+        }
+
+        let size = mark.attrs.fontSize;
+        if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
+        if (this.editorProps) {
+            let ruleProvider = this.editorProps.ruleProvider;
+            let heading = NumCast(this.editorProps.Document.heading);
+            if (ruleProvider && heading) {
+                ruleProvider["ruleSize_" + heading] = size;
+            }
+        }
+        //actually apply font
+        if ((view.state.selection as any).node && (view.state.selection as any).node.type === view.state.schema.nodes.ordered_list) {
+            let status = updateBullets(view.state.tr.setNodeMarkup(view.state.selection.from, (view.state.selection as any).node.type,
+                { ...(view.state.selection as NodeSelection).node.attrs, setFontSize: size }), view.state.schema);
+            view.dispatch(status.setSelection(new NodeSelection(status.doc.resolve(view.state.selection.from))));
+        }
+        else view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, view.state.schema.marks.pFontSize.create({ fontSize: size })));
+        view.state.storedMarks = [...(view.state.storedMarks || []), view.state.schema.marks.pFontSize.create({ fontSize: size })];
+    }
+
     //remove all node typeand apply the passed-in one to the selected text
     changeToNodeType = (nodeType: NodeType | undefined) => {
         //remove oldif (nodeType) { //add new
@@ -558,7 +594,7 @@ export class TooltipTextMenu {
 
     //makes a button for the drop down FOR MARKS
     //css is the style you want applied to the button
-    dropdownMarkBtn(label: string, css: string, markType: MarkType, view: EditorView, changeToMarkInGroup: (markType: MarkType<any>, view: EditorView, groupMarks: MarkType[]) => any, groupMarks: MarkType[]) {
+    dropdownFontFamilyBtn(label: string, css: string, mark: Mark, view: EditorView, changeFontFamily: (mark: Mark<any>, view: EditorView) => any) {
         return new MenuItem({
             title: "",
             label: label,
@@ -567,7 +603,22 @@ export class TooltipTextMenu {
             css: css,
             enable() { return true; },
             run() {
-                changeToMarkInGroup(markType, view, groupMarks);
+                changeFontFamily(mark, view);
+            }
+        });
+    }
+    //makes a button for the drop down FOR MARKS
+    //css is the style you want applied to the button
+    dropdownFontSizeBtn(label: string, css: string, mark: Mark, view: EditorView, changeFontSize: (markType: Mark<any>, view: EditorView) => any) {
+        return new MenuItem({
+            title: "",
+            label: label,
+            execEvent: "",
+            class: "menuicon",
+            css: css,
+            enable() { return true; },
+            run() {
+                changeFontSize(mark, view);
             }
         });
     }
@@ -649,7 +700,9 @@ export class TooltipTextMenu {
                         this.view.dispatch(this.view.state.tr.removeMark(from, to));
                         Array.from(this._brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
                             const markType = mark.type;
-                            this.changeToMarkInGroup(markType, this.view, []);
+                            if (mark.type === schema.marks.pFontFamily) this.changeToFontFamily(mark, this.view);
+                            else if (mark.type === schema.marks.pFontSize) this.changeToFontSize(mark, this.view);
+                            else this.changeToMarkInGroup(markType, this.view, []);
                         });
                     }
                 }
@@ -868,50 +921,23 @@ export class TooltipTextMenu {
         //UPDATE LIST ITEM DROPDOWN
 
         //UPDATE FONT STYLE DROPDOWN
-        let activeStyles = this.activeMarksOnSelection(this.fontStyles);
+        let activeStyles = this.activeFontFamilyOnSelection();
         if (activeStyles !== undefined) {
-            // activeStyles.forEach((markType) => {
-            //     this._activeMarks.push(this.view.state.schema.mark(markType));
-            // });
             if (activeStyles.length === 1) {
-                // if we want to update something somewhere with active font name
-                let fontName = this.fontStylesToName.get(activeStyles[0]);
-                if (fontName) { this.updateFontStyleDropdown(fontName); }
-            } else if (activeStyles.length === 0) {
-                //crimson on default
-                this.updateFontStyleDropdown("Crimson Text");
-            } else {
-                this.updateFontStyleDropdown("Various");
-            }
+                activeStyles[0] && this.updateFontStyleDropdown(activeStyles[0]);
+            } else this.updateFontStyleDropdown(activeStyles.length ? "various" : "default");
         }
 
         //UPDATE FONT SIZE DROPDOWN
-        let activeSizes = this.activeMarksOnSelection(this.fontSizes);
+        let activeSizes = this.activeFontSizeOnSelection();
         if (activeSizes !== undefined) {
             if (activeSizes.length === 1) { //if there's only one active font size
-                // activeSizes.forEach((markType) => {
-                //     this._activeMarks.push(this.view.state.schema.mark(markType));
-                // });
-                let size = this.fontSizeToNum.get(activeSizes[0]);
-                if (size) { this.updateFontSizeDropdown(String(size) + " pt"); }
-            } else if (activeSizes.length === 0) {
-                //should be 14 on default  
-                this.updateFontSizeDropdown("14 pt");
-            } else { //multiple font sizes selected
-                this.updateFontSizeDropdown("Various");
-            }
+                activeSizes[0] && this.updateFontSizeDropdown(String(activeSizes[0]) + " pt");
+            } else this.updateFontSizeDropdown(activeSizes.length ? "various" : "default");
         }
 
         this.update_mark_doms();
     }
-
-    public mark_key_pressed(marks: Mark<any>[]) {
-        if (this.view.state.selection.empty) {
-            if (marks) this._activeMarks = marks;
-            this.update_mark_doms();
-        }
-    }
-
     update_mark_doms() {
         this.reset_mark_doms();
         let foundlink = false;
@@ -946,6 +972,30 @@ export class TooltipTextMenu {
 
     }
 
+    //finds fontSize at start of selection
+    activeFontSizeOnSelection() {
+        //current selection
+        let state = this.view.state;
+        let activeSizes: number[] = [];
+        const pos = this.view.state.selection.$from;
+        const ref_node: ProsNode = this.reference_node(pos);
+        if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+            ref_node.marks.forEach(m => m.type === state.schema.marks.pFontSize && activeSizes.push(m.attrs.fontSize));
+        }
+        return activeSizes;
+    }
+    //finds fontSize at start of selection
+    activeFontFamilyOnSelection() {
+        //current selection
+        let state = this.view.state;
+        let activeFamilies: string[] = [];
+        const pos = this.view.state.selection.$from;
+        const ref_node: ProsNode = this.reference_node(pos);
+        if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+            ref_node.marks.forEach(m => m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family));
+        }
+        return activeFamilies;
+    }
     //finds all active marks on selection in given group
     activeMarksOnSelection(markGroup: MarkType[]) {
         //current selection
@@ -980,6 +1030,9 @@ export class TooltipTextMenu {
                 this._activeMarks = ref_node.marks;
                 activeMarks = markGroup.filter(mark_type => {
                     if (dispatch) {
+                        if (mark_type === state.schema.marks.pFontSize) {
+                            return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+                        }
                         let mark = state.schema.mark(mark_type);
                         return ref_node.marks.includes(mark);
                     }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 037c92a62..abf26826c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -93,8 +93,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
     private _pushReactionDisposer: Opt<IReactionDisposer>;
     private dropDisposer?: DragManager.DragDropDisposer;
 
-    @observable private _fontSize = 13;
-    @observable private _fontFamily = "Arial";
+    @observable private _ruleFontSize = 0;
+    @observable private _ruleFontFamily = "Arial";
     @observable private _fontAlign = "";
     @observable private _entered = false;
     public static SelectOnLoad = "";
@@ -187,9 +187,6 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
             }
             const state = this._editorView.state.apply(tx);
             this._editorView.updateState(state);
-            if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
-                FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
-            }
 
             let tsel = this._editorView.state.selection.$from;
             tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 5000 - 1000)));
@@ -552,8 +549,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
             return undefined;
         },
             action((rules: any) => {
-                this._fontFamily = rules ? rules.font : "Arial";
-                this._fontSize = rules ? rules.size : NumCast(this.layoutDoc.fontSize, 13);
+                this._ruleFontFamily = rules ? rules.font : "Arial";
+                this._ruleFontSize = rules ? rules.size : 0;
                 rules && setTimeout(() => {
                     const view = this._editorView!;
                     if (this._proseRef) {
@@ -1035,8 +1032,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                     opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                     color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
                     pointerEvents: interactive,
-                    fontSize: this._fontSize,
-                    fontFamily: this._fontFamily,
+                    fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13),
+                    fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"),
                 }}
                 onContextMenu={this.specificContextMenu}
                 onKeyDown={this.onKeyPress}
-- 
cgit v1.2.3-70-g09d2


From d7150995d62c498ab8435de986b90d98bdca020c Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 12 Nov 2019 15:07:03 -0500
Subject: a bunch of cleanup and bug fixes to text links and doculink buttons

---
 src/client/documents/Documents.ts                  |  5 +-
 src/client/util/LinkManager.ts                     |  2 -
 src/client/util/RichTextSchema.tsx                 |  4 +-
 src/client/util/TooltipTextMenu.tsx                | 23 +++---
 src/client/views/DocumentDecorations.tsx           | 10 ++-
 src/client/views/GlobalKeyHandler.ts               |  2 +-
 src/client/views/Main.tsx                          | 24 -------
 src/client/views/MainView.scss                     |  2 +-
 src/client/views/MainView.tsx                      |  8 +--
 src/client/views/linking/LinkMenuItem.tsx          |  1 -
 src/client/views/nodes/FormattedTextBox.tsx        |  9 ++-
 src/client/views/nodes/FormattedTextBoxComment.tsx | 83 ++++++++++++----------
 src/client/views/nodes/PDFBox.scss                 |  2 +
 src/client/views/nodes/PDFBox.tsx                  | 76 +++++++++++---------
 src/client/views/pdf/Annotation.tsx                |  6 +-
 src/client/views/pdf/PDFViewer.tsx                 | 14 +---
 src/new_fields/Doc.ts                              |  2 +-
 .../authentication/models/current_user_utils.ts    |  1 -
 18 files changed, 127 insertions(+), 147 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d1fcabc4a..ba9f87025 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -257,6 +257,9 @@ export namespace Docs {
             return PrototypeMap.get(type)!;
         }
 
+        /**
+         * A collection of all links in the database.  Ideally, this would be a search, but for now all links are cached here.
+         */
         export function MainLinkDocument() {
             return Prototypes.get(DocumentType.LINKDOC);
         }
@@ -703,6 +706,7 @@ export namespace DocUtils {
 
             linkDocProto.title = title === "" ? source.doc.title + " to " + target.doc.title : title;
             linkDocProto.linkDescription = description;
+            linkDocProto.isPrototype = true;
 
             linkDocProto.anchor1 = source.doc;
             linkDocProto.anchor2 = target.doc;
@@ -714,7 +718,6 @@ export namespace DocUtils {
             linkDocProto.anchor2Timecode = target.doc.currentTimecode;
             linkDocProto.layoutKey1 = DocuLinkBox.LayoutString("anchor1");
             linkDocProto.layoutKey2 = DocuLinkBox.LayoutString("anchor2");
-            linkDocProto.borderRounding = "20";
             linkDocProto.width = linkDocProto.height = 0;
             linkDocProto.isBackground = true;
             linkDocProto.isButton = true;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index ee2f2dadc..eedc4967d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -34,8 +34,6 @@ export class LinkManager {
     // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
     // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type 
     public get LinkManagerDoc(): Doc | undefined {
-        // return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
-
         return Docs.Prototypes.MainLinkDocument();
     }
 
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index a5e0ca720..76b8aeaa1 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -305,8 +305,8 @@ export const marks: { [index: string]: MarkSpec } = {
         }],
         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];
+                ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
+                ["a", { ...node.attrs, title: `${node.attrs.title}` }, 0];
         }
     },
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 9ce7acec8..38471a955 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -17,7 +17,7 @@ import { DragManager } from "./DragManager";
 import { LinkManager } from "./LinkManager";
 import { schema } from "./RichTextSchema";
 import "./TooltipTextMenu.scss";
-import { Cast, NumCast } from '../../new_fields/Types';
+import { Cast, NumCast, StrCast } from '../../new_fields/Types';
 import { updateBullets } from './ProsemirrorExampleTransfer';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 const { toggleMark, setBlockType } = require("prosemirror-commands");
@@ -284,7 +284,7 @@ export class TooltipTextMenu {
                                     if (proto && docView) {
                                         proto.sourceContext = docView.props.ContainingCollectionDoc;
                                     }
-                                    let text = this.makeLink(linkDoc, ctrlKey ? "onRight" : "inTab");
+                                    let text = this.makeLink(linkDoc, StrCast(linkDoc.anchor2.title), 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
                                     }
@@ -374,25 +374,20 @@ export class TooltipTextMenu {
     //     let link = state.schema.mark(state.schema.marks.link, { href: target, location: location });
     // }
 
-    makeLink = (targetDoc: Doc, location: string): string => {
-        let target = Utils.prepend("/doc/" + targetDoc[Id]);
+    makeLink = (targetDoc: Doc, title: string, location: string): string => {
+        let link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + targetDoc[Id]), title: title, location: location });
+        this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
+            addMark(this.view.state.selection.from, this.view.state.selection.to, link));
         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: 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;
-        link = node && node.marks.find(m => m.type.name === "link");
-        if (node) {
-            if (node.text) {
-                return node.text;
-            }
+        if (node && node.text) {
+            return node.text;
         }
         return "";
     }
 
     deleteLink = () => {
         let node = this.view.state.selection.$from.nodeAfter;
-        let link = node && node.marks.find(m => m.type.name === "link");
+        let link = node && node.marks.find(m => m.type === this.view.state.schema.marks.link);
         let href = link!.attrs.href;
         if (href) {
             if (href.indexOf(Utils.prepend("/doc/")) === 0) {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 0336440d5..55c211d1d 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -24,6 +24,7 @@ import { DocumentView } from "./nodes/DocumentView";
 import { FieldView } from "./nodes/FieldView";
 import { IconBox } from "./nodes/IconBox";
 import React = require("react");
+import { DocumentType } from '../documents/DocumentTypes';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -112,7 +113,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             }
             else {
                 if (SelectionManager.SelectedDocuments().length > 0) {
-                    SelectionManager.SelectedDocuments()[0].props.Document.customTitle = true;
+                    SelectionManager.SelectedDocuments()[0].props.Document.customTitle = !this._title.startsWith("-");
                     let field = SelectionManager.SelectedDocuments()[0].props.Document[this._fieldKey];
                     if (typeof field === "number") {
                         SelectionManager.SelectedDocuments().forEach(d => {
@@ -174,6 +175,13 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
             var [sptX, sptY] = transform.transformPoint(0, 0);
             let [bptX, bptY] = transform.transformPoint(documentView.props.PanelWidth(), documentView.props.PanelHeight());
+            if (documentView.props.Document.type === DocumentType.LINK) {
+                let rect = documentView.ContentDiv!.getElementsByClassName("docuLinkBox-cont")[0].getBoundingClientRect();
+                sptX = rect.left;
+                sptY = rect.top;
+                bptX = rect.right;
+                bptY = rect.bottom;
+            }
             return {
                 x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y),
                 r: Math.max(bptX, bounds.r), b: Math.max(bptY, bounds.b)
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 9ca9fc163..8f397e331 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -165,7 +165,7 @@ export default class KeyManager {
                     }
                 }
                 break;
-            case "c":
+            case "t":
                 PromiseValue(Cast(CurrentUserUtils.UserDocument.Create, Doc)).then(pv => pv && (pv.onClick as ScriptField).script.run({ this: pv }));
                 if (MainView.Instance.flyoutWidth === 240) {
                     MainView.Instance.flyoutWidth = 0;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index a91a2b69e..b21eb9c8f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -3,34 +3,11 @@ import { Docs } from "../documents/Documents";
 import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 import * as ReactDOM from 'react-dom';
 import * as React from 'react';
-import { Cast } from "../../new_fields/Types";
-import { Doc, DocListCastAsync } from "../../new_fields/Doc";
-import { List } from "../../new_fields/List";
 import { DocServer } from "../DocServer";
 import { AssignAllExtensions } from "../../extensions/General/Extensions";
 
 AssignAllExtensions();
 
-let swapDocs = async () => {
-    let oldDoc = await Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc);
-    // Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>();
-    if (oldDoc) {
-        let links = await DocListCastAsync(oldDoc.allLinks);
-        // if (links && DocListCast(links)) {
-        if (links && links.length) {
-            let data = await DocListCastAsync(Docs.Prototypes.MainLinkDocument().allLinks);
-            if (data) {
-                data.push(...links.filter(i => data!.indexOf(i) === -1));
-                Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(data.filter((i, idx) => data!.indexOf(i) === idx));
-            }
-            else {
-                Docs.Prototypes.MainLinkDocument().allLinks = new List<Doc>(links);
-            }
-        }
-        CurrentUserUtils.UserDocument.linkManagerDoc = undefined;
-    }
-};
-
 (async () => {
     const info = await CurrentUserUtils.loadCurrentUser();
     DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
@@ -38,7 +15,6 @@ let swapDocs = async () => {
     if (info.id !== "__guest__") {
         // a guest will not have an id registered
         await CurrentUserUtils.loadUserDocument(info);
-        await swapDocs();
     }
     document.getElementById('root')!.addEventListener('wheel', event => {
         if (event.ctrlKey) {
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index 21b135c49..a858a73c7 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -13,7 +13,7 @@
     left: 250px;
 }
 
-.mainView-container {
+#mainView-container {
     width: 100%;
     height: 100%;
     position: absolute;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 773da05df..83dbb433b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -37,7 +37,6 @@ import { OverlayView } from './OverlayView';
 import PDFMenu from './pdf/PDFMenu';
 import { PreviewCursor } from './PreviewCursor';
 import { Scripting } from '../util/Scripting';
-import { LinkManager } from '../util/LinkManager';
 import { AudioBox } from './nodes/AudioBox';
 
 @observer
@@ -197,11 +196,6 @@ export class MainView extends React.Component {
         var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
         let mainDoc = Docs.Create.DockDocument([freeformDoc], JSON.stringify(dockingLayout), {}, id);
         if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) {
-            if (!this.userDoc.linkManagerDoc) {
-                let linkManagerDoc = new Doc();
-                linkManagerDoc.allLinks = new List<Doc>([]);
-                this.userDoc.linkManagerDoc = linkManagerDoc;
-            }
             Doc.AddDocToList(workspaces, "data", mainDoc);
             mainDoc.title = `Workspace ${DocListCast(workspaces.data).length}`;
         }
@@ -504,7 +498,7 @@ export class MainView extends React.Component {
     }
 
     render() {
-        return (<div className="mainView-container">
+        return (<div id="mainView-container">
             <DictationOverlay />
             <SharingManager />
             <GoogleAuthenticationManager />
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index a6ee9c2c6..238660de3 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -8,7 +8,6 @@ import { Cast, StrCast } from '../../../new_fields/Types';
 import { DragLinkAsDocument } from '../../util/DragManager';
 import { LinkManager } from '../../util/LinkManager';
 import { ContextMenu } from '../ContextMenu';
-import { MainView } from '../MainView';
 import { LinkFollowBox } from './LinkFollowBox';
 import './LinkMenu.scss';
 import React = require("react");
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index abf26826c..a0f8523a2 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -842,6 +842,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
         this._editorView && this._editorView.destroy();
     }
     onPointerDown = (e: React.PointerEvent): void => {
+        FormattedTextBoxComment.textBox = this;
         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) {
@@ -1018,11 +1019,13 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
     }
 
     render() {
+        trace();
         let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
-        let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground
-            ? "none" : "all";
+        let interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
         if (this.props.isSelected()) {
             FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+        } else if (FormattedTextBoxComment.textBox === this) {
+            FormattedTextBoxComment.Hide();
         }
         return (
             <div className={`formattedTextBox-cont`} ref={this._ref}
@@ -1031,7 +1034,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
                     background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
                     opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1,
                     color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit",
-                    pointerEvents: interactive,
+                    pointerEvents: interactive ? "none" : "all",
                     fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13),
                     fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"),
                 }}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index bde278be3..2a7467108 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -6,8 +6,9 @@ import { Doc } from "../../../new_fields/Doc";
 import { schema } from "../../util/RichTextSchema";
 import { DocServer } from "../../DocServer";
 import { Utils } from "../../../Utils";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
 import { FormattedTextBox } from "./FormattedTextBox";
+import { DocUtils } from "../../documents/Documents";
 
 export let formattedTextBoxCommentPlugin = new Plugin({
     view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -61,6 +62,7 @@ export class FormattedTextBoxComment {
             FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
             FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
             FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+            FormattedTextBoxComment.tooltip.style.maxWidth = "350px";
             FormattedTextBoxComment.tooltip.appendChild(input);
             FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
                 let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
@@ -91,62 +93,67 @@ export class FormattedTextBoxComment {
         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;
+            lastState.selection.eq(state.selection)) {
+            return;
+        }
 
-        if (!FormattedTextBoxComment.textBox || !FormattedTextBoxComment.textBox.props || !FormattedTextBoxComment.textBox.props.isSelected()) return;
+        const textBox = FormattedTextBoxComment.textBox;
+        if (!textBox || !textBox.props) {
+            return;
+        }
         let set = "none";
-        if (FormattedTextBoxComment.textBox && state.selection.$from) {
-            let nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
+        let nbef = 0;
+        // this section checks to see if the insertion point is over text entered by a different user.  If so, it sets ths comment text to indicate the user and the modification date
+        if (state.selection.$from) {
+            nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
             let naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
-            const spos = state.selection.$from.pos - nbef;
-            const epos = state.selection.$from.pos + naft;
-            let child = state.selection.$from.nodeBefore;
-            let mark = child && findOtherUserMark(child.marks);
             let noselection = view.state.selection.$from === view.state.selection.$to;
+            let child: any = null;
+            state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
+            let mark = child && findOtherUserMark(child.marks);
             if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
-                FormattedTextBoxComment.SetState(this, mark.attrs.opened, spos, epos, mark);
+                FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, mark.attrs.opened, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
             }
-            if (mark && child && nbef && naft) {
-                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);
-                // The box in which the tooltip is positioned, to use as base
-                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);
-                FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
-                FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+            if (mark && child && ((nbef && naft) || !noselection)) {
+                FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " date=" + (new Date(mark.attrs.modified * 5000)).toDateString();
                 set = "";
             }
         }
+        // this checks if the selection is a hyperlink.  If so, it displays the target doc's text for internal links, and the url of the target for external links. 
         if (set === "none" && state.selection.$from) {
-            FormattedTextBoxComment.textBox = undefined;
-            let nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
+            nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
             let naft = findEndOfMark(state.selection.$from, view, findLinkMark);
-            let child = state.selection.$from.nodeBefore;
+            let child: any = null;
+            state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
             let mark = child && findLinkMark(child.marks);
             if (mark && child && nbef && naft) {
-                FormattedTextBoxComment.tooltipText.textContent = "link : " + (mark.attrs.title || mark.attrs.href);
+                FormattedTextBoxComment.tooltipText.textContent = "external => " + 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) && (FormattedTextBoxComment.tooltipText.textContent = "link :" + StrCast((linkDoc as Doc).title)));
+                    docTarget && DocServer.GetRefField(docTarget).then(linkDoc => {
+                        if (linkDoc instanceof Doc) {
+                            let target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.props.Document) ? Cast(linkDoc.anchor2, Doc) : Cast(linkDoc.anchor1, Doc));
+                            let ext = (target && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field.  probably need an 'overviewLayout' and then just display the target Document ....
+                            let text = ext && StrCast(ext.text);
+                            ext && (FormattedTextBoxComment.tooltipText.textContent = "=> " + (text || StrCast(ext.title)));
+                        }
+                    });
                 }
-                // 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);
-                // The box in which the tooltip is positioned, to use as base
-                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);
-                FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
-                FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
                 set = "";
             }
         }
+        if (set !== "none") {
+            // 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);
+            // The box in which the tooltip is positioned, to use as base
+            let box = (document.getElementById("mainView-container") 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);
+            FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+            FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+        }
         FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
     }
 
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 963205206..2d92c9581 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -48,6 +48,7 @@
     }
     .pdfViewer-text {
         .textLayer {
+            will-change: transform;
             span {
                 user-select: none;
             }
@@ -59,6 +60,7 @@
     pointer-events: all;
     .pdfViewer-text {
         .textLayer {
+            will-change: transform;
             span {
                 user-select: text;
             }
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 3baa6eb09..8e0515f8a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,5 +1,5 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, observable, runInAction, reaction, IReactionDisposer, trace } from 'mobx';
+import { action, observable, runInAction, reaction, IReactionDisposer, trace, untracked, computed } from 'mobx';
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
@@ -32,7 +32,7 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     private _valueValue: string = "";
     private _scriptValue: string = "";
     private _searchString: string = "";
-    private _initialScale: number | undefined;  // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not.  Getting bigger after startup won't make it automatically be live.
+    private _initialScale: number = 0;  // the initial scale of the PDF when first rendered which determines whether the document will be live on startup or not.  Getting bigger after startup won't make it automatically be live.
     private _everActive = false; // has this box ever had its contents activated -- if so, stop drawing the overlay title
     private _pdfViewer: PDFViewer | undefined;
     private _searchRef = React.createRef<HTMLInputElement>();
@@ -46,6 +46,11 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
     @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
     @observable private _pageControls = false;
 
+    constructor(props: any) {
+        super(props);
+        this._initialScale = this.props.ScreenToLocalTransform().Scale;
+    }
+
     componentWillUnmount() {
         this._selectReactionDisposer && this._selectReactionDisposer();
     }
@@ -190,39 +195,46 @@ export class PDFBox extends DocAnnotatableComponent<FieldViewProps, PdfDocument>
         ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
     }
 
-    render() {
+    @computed get renderTitleBox() {
+        let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
+        return <div className="pdfBox-title-outer" >
+            <div className={classname} >
+                <strong className="pdfBox-title" >{` ${this.props.Document.title}`}</strong>
+            </div>
+        </div>;
+    }
+
+    @computed get renderPdfView() {
         trace();
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         let classname = "pdfBox-cont" + (this.active() ? "-interactive" : "");
-        let noPdf = !(pdfUrl instanceof PdfField) || !this._pdf;
-        if (this._initialScale === undefined) this._initialScale = this.props.ScreenToLocalTransform().Scale;
+        return <div className={classname} style={{
+            width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
+            height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
+            transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})`
+        }} onContextMenu={this.specificContextMenu} onPointerDown={e => {
+            let hit = document.elementFromPoint(e.clientX, e.clientY);
+            if (hit && hit.localName === "span" && this.props.isSelected()) {  // drag selecting text stops propagation
+                e.button === 0 && e.stopPropagation();
+            }
+        }}>
+            <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded}
+                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} focus={this.props.focus}
+                pinToPres={this.props.pinToPres} addDocument={this.addDocument}
+                ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
+                isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
+                fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
+            {this.settingsPanel()}
+        </div>;
+    }
+
+    render() {
+        const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField, null);
         if (this.props.isSelected() || this.props.Document.scrollY !== undefined) this._everActive = true;
-        return (!this.extensionDoc || 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>
-                </div>
-            </div> :
-            <div className={classname} style={{
-                width: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
-                height: this.props.Document.fitWidth ? `${100 / this.props.ContentScaling()}%` : undefined,
-                transform: `scale(${this.props.Document.fitWidth ? this.props.ContentScaling() : 1})`
-            }} onContextMenu={this.specificContextMenu} onPointerDown={e => {
-                let hit = document.elementFromPoint(e.clientX, e.clientY);
-                if (hit && hit.localName === "span" && this.props.isSelected()) {  // drag selecting text stops propagation
-                    e.button === 0 && e.stopPropagation();
-                }
-            }}>
-                <PDFViewer {...this.props} pdf={this._pdf!} url={pdfUrl!.url.pathname} active={this.props.active} loaded={this.loaded}
-                    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} focus={this.props.focus}
-                    pinToPres={this.props.pinToPres} addDocument={this.addDocument}
-                    ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
-                    isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
-                    fieldKey={this.props.fieldKey} startupLive={this._initialScale < 2.5 ? true : false} />
-                {this.settingsPanel()}
-            </div>);
+        return !pdfUrl || !this._pdf || !this.extensionDoc || (!this._everActive && this.props.ScreenToLocalTransform().Scale > 2.5) ?
+            this.renderTitleBox : this.renderPdfView;
     }
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 2d8f47666..936af9ab8 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -52,11 +52,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
 
         this._brushDisposer = reaction(
             () => FieldValue(Cast(this.props.document.group, Doc)) && Doc.isBrushedHighlightedDegree(FieldValue(Cast(this.props.document.group, Doc))!),
-            (brushed) => {
-                if (brushed !== undefined) {
-                    runInAction(() => this._brushed = brushed !== 0);
-                }
-            }
+            brushed => brushed !== undefined && runInAction(() => this._brushed = brushed !== 0)
         );
     }
 
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 060ba8613..0cb671156 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -199,7 +199,7 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
 
         this._annotationReactionDisposer = reaction(
             () => this.extensionDoc && DocListCast(this.extensionDoc.annotations),
-            annotations => annotations && annotations.length && this.renderAnnotations(annotations, true),
+            annotations => annotations && annotations.length && (this._annotations = annotations),
             { fireImmediately: true });
 
         this._filterReactionDisposer = reaction(
@@ -297,18 +297,6 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
         this.Index = -1;
         return mainAnnoDoc;
     }
-
-    @action
-    renderAnnotations = (annotations: Doc[], removeOldAnnotations: boolean): void => {
-        if (removeOldAnnotations) {
-            this._annotations = annotations;
-        }
-        else {
-            this._annotations.push(...annotations);
-            this._annotations = new Array<Doc>(...this._annotations);
-        }
-    }
-
     @action
     prevAnnotation = () => {
         this.Index = Math.max(this.Index - 1, 0);
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 6aad4a6be..3bf1129b5 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -669,8 +669,8 @@ export namespace Doc {
         return doc;
     }
 
-    export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
 
+    export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { return Doc.AreProtosEqual(anchorDoc, Cast(linkDoc.anchor1, Doc) as Doc) ? "layoutKey1" : "layoutKey2"; }
     export function linkFollowUnhighlight() {
         Doc.UnhighlightAll();
         document.removeEventListener("pointerdown", linkFollowUnhighlight);
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 56ea5bfe1..833e44bf6 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -15,7 +15,6 @@ import { RouteStore } from "../../RouteStore";
 import { InkingControl } from "../../../client/views/InkingControl";
 import { DragManager } from "../../../client/util/DragManager";
 import { nullAudio } from "../../../new_fields/URLField";
-import { LinkManager } from "../../../client/util/LinkManager";
 
 export class CurrentUserUtils {
     private static curr_id: string;
-- 
cgit v1.2.3-70-g09d2


From ecbe527575bab2cb5f1ced278039ec0a6fc50809 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 12 Nov 2019 16:04:05 -0500
Subject: switched text hyper link following to go through pop-up comment box
 now.

---
 src/client/views/nodes/FormattedTextBox.tsx        | 64 +++++++++++-----------
 src/client/views/nodes/FormattedTextBoxComment.tsx | 15 ++++-
 2 files changed, 45 insertions(+), 34 deletions(-)

(limited to 'src/client/views/nodes/FormattedTextBox.tsx')

diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index a0f8523a2..24d6f2509 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -884,38 +884,38 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F
     onClick = (e: React.MouseEvent): void => {
         if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
         (e.nativeEvent as any).formattedHandled = true;
-        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 location: string;
-            if ((e.target as any).attributes.location) {
-                location = (e.target as any).attributes.location.value;
-            }
-            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);
-                if (link && !(link.attrs.docref && link.attrs.title)) {  // bcz: getting hacky.  this indicates that we clicked on a PDF excerpt quotation.  In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
-                    href = link && link.attrs.href;
-                    location = link && link.attrs.location;
-                }
-            }
-            if (href) {
-                if (href.indexOf(Utils.prepend("/doc/")) === 0) {
-                    let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
-                    if (linkClicked) {
-                        DocServer.GetRefField(linkClicked).then(async linkDoc => {
-                            (linkDoc instanceof Doc) &&
-                                DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false);
-                        });
-                    }
-                } else {
-                    let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
-                    this.props.addDocument && this.props.addDocument(webDoc);
-                }
-                e.stopPropagation();
-                e.preventDefault();
-            }
-        }
+        // 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 location: string;
+        //     if ((e.target as any).attributes.location) {
+        //         location = (e.target as any).attributes.location.value;
+        //     }
+        //     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);
+        //         if (link && !(link.attrs.docref && link.attrs.title)) {  // bcz: getting hacky.  this indicates that we clicked on a PDF excerpt quotation.  In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
+        //             href = link && link.attrs.href;
+        //             location = link && link.attrs.location;
+        //         }
+        //     }
+        //     if (href) {
+        //         if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+        //             let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+        //             if (linkClicked) {
+        //                 DocServer.GetRefField(linkClicked).then(async linkDoc => {
+        //                     (linkDoc instanceof Doc) &&
+        //                         DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false);
+        //                 });
+        //             }
+        //         } else {
+        //             let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
+        //             this.props.addDocument && this.props.addDocument(webDoc);
+        //         }
+        //         e.stopPropagation();
+        //         e.preventDefault();
+        //     }
+        // }
 
         this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
         if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500);
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 2a7467108..f35ca502f 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -9,6 +9,9 @@ import { Utils } from "../../../Utils";
 import { StrCast, Cast, FieldValue } from "../../../new_fields/Types";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { DocUtils } from "../../documents/Documents";
+import { isBuffer } from "util";
+import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentType } from "../../documents/DocumentTypes";
 
 export let formattedTextBoxCommentPlugin = new Plugin({
     view(editorView) { return new FormattedTextBoxComment(editorView); }
@@ -52,6 +55,7 @@ export class FormattedTextBoxComment {
     static mark: Mark;
     static opened: boolean;
     static textBox: FormattedTextBox | undefined;
+    static linkDoc: Doc | undefined;
     constructor(view: any) {
         if (!FormattedTextBoxComment.tooltip) {
             const root = document.getElementById("root");
@@ -66,10 +70,15 @@ export class FormattedTextBoxComment {
             FormattedTextBoxComment.tooltip.appendChild(input);
             FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
                 let keep = e.target && (e.target as any).type === "checkbox" ? true : false;
+                if (FormattedTextBoxComment.linkDoc && !keep && FormattedTextBoxComment.textBox) {
+                    DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, FormattedTextBoxComment.textBox.props.Document,
+                        (doc: Doc, maxLocation: string) => FormattedTextBoxComment.textBox!.props.addDocTab(doc, undefined, e.ctrlKey ? "inTab" : "onRight"));
+                }
                 FormattedTextBoxComment.opened = keep || !FormattedTextBoxComment.opened;
                 FormattedTextBoxComment.textBox && FormattedTextBoxComment.start !== undefined && FormattedTextBoxComment.textBox.setAnnotation(
                     FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark,
                     FormattedTextBoxComment.opened, keep);
+                e.stopPropagation();
             };
             root && root.appendChild(FormattedTextBoxComment.tooltip);
         }
@@ -96,6 +105,7 @@ export class FormattedTextBoxComment {
             lastState.selection.eq(state.selection)) {
             return;
         }
+        FormattedTextBoxComment.linkDoc = undefined;
 
         const textBox = FormattedTextBoxComment.textBox;
         if (!textBox || !textBox.props) {
@@ -132,10 +142,11 @@ export class FormattedTextBoxComment {
                     let docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
                     docTarget && DocServer.GetRefField(docTarget).then(linkDoc => {
                         if (linkDoc instanceof Doc) {
+                            FormattedTextBoxComment.linkDoc = linkDoc;
                             let target = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.props.Document) ? Cast(linkDoc.anchor2, Doc) : Cast(linkDoc.anchor1, Doc));
-                            let ext = (target && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field.  probably need an 'overviewLayout' and then just display the target Document ....
+                            let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field.  probably need an 'overviewLayout' and then just display the target Document ....
                             let text = ext && StrCast(ext.text);
-                            ext && (FormattedTextBoxComment.tooltipText.textContent = "=> " + (text || StrCast(ext.title)));
+                            ext && (FormattedTextBoxComment.tooltipText.textContent = (target && target.type === DocumentType.PDFANNO ? "Quoted from " : "") + "=> " + (text || StrCast(ext.title)));
                         }
                     });
                 }
-- 
cgit v1.2.3-70-g09d2

