From 5c9f40006aa157c58ec40828ebd4845c16daa8af Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Wed, 21 Aug 2019 15:08:39 -0400
Subject: start of making link follow

---
 src/client/views/MainView.tsx | 11 +++++++++++
 1 file changed, 11 insertions(+)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f28844009..f3c8a176c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -40,6 +40,7 @@ import { PreviewCursor } from './PreviewCursor';
 import { FilterBox } from './search/FilterBox';
 import PresModeMenu from './presentationview/PresentationModeMenu';
 import { PresBox } from './nodes/PresBox';
+import { LinkFollowBox } from './linking/LinkFollowBox';
 
 @observer
 export class MainView extends React.Component {
@@ -55,6 +56,8 @@ export class MainView extends React.Component {
 
     public overlayTimeout: NodeJS.Timeout | undefined;
 
+    @observable private _linkFollowBox = false;
+
     public initiateDictationFade = () => {
         let duration = DictationManager.Commands.dictationFadeDuration;
         this.overlayTimeout = setTimeout(() => {
@@ -498,6 +501,12 @@ export class MainView extends React.Component {
         this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
     }
 
+    @action
+    toggleLinkFollowBox = () => {
+        console.log("toggling link editor")
+        this._linkFollowBox = !this._linkFollowBox;
+    }
+
     /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
     @computed
     get miscButtons() {
@@ -506,6 +515,7 @@ export class MainView extends React.Component {
         return [
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
             <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
+                <button onClick={this.toggleLinkFollowBox}>Open Link Editor</button>
                 <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
         ];
 
@@ -564,6 +574,7 @@ export class MainView extends React.Component {
                 <PDFMenu />
                 <MainOverlayTextBox firstinstance={true} />
                 <OverlayView />
+                {/* <LinkFollowBox /> */}
             </div >
         );
     }
-- 
cgit v1.2.3-70-g09d2


From 1fb290bcc1c46214cfd553f31c1282d2694530ea Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Thu, 22 Aug 2019 19:42:54 -0400
Subject: making the box come up

---
 src/client/documents/Documents.ts           |  4 ++
 src/client/views/MainView.tsx               |  2 +
 src/client/views/linking/LinkFollowBox.scss |  2 +-
 src/client/views/linking/LinkFollowBox.tsx  | 87 ++++++++++++++++++-----------
 src/client/views/linking/LinkMenuItem.tsx   | 12 +++-
 5 files changed, 73 insertions(+), 34 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index cd612aaa9..b7c8f4c12 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -446,6 +446,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.DRAGBOX), undefined, { ...(options || {}) });
         }
 
+        export function LinkFollowBoxDocument(options?: DocumentOptions) {
+            return InstanceFromProto(Prototypes.get(DocumentType.LINKFOLLOW), undefined, { ...(options || {}) });
+        }
+
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f3c8a176c..79c44ae72 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -443,6 +443,7 @@ export class MainView extends React.Component {
         let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
 
         let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
+        let addLinkFollowBox = action(() => Docs.Create.LinkFollowBoxDocument({ width: 200, height: 500, title: "Link Follow Document" }));
         let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
         let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
         let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
@@ -458,6 +459,7 @@ export class MainView extends React.Component {
             [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
             [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
             [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
+            [React.createRef<HTMLDivElement>(), "caret-up", "Open Link Follow Box", addLinkFollowBox],
             [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
             //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
         ];
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
index c764b002f..522191792 100644
--- a/src/client/views/linking/LinkFollowBox.scss
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -2,5 +2,5 @@
 
 .linkFollowBox-main {
     position: absolute;
-    background: $main-accent;
+    background: red;
 }
\ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 7fc4449d3..247b67776 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -9,13 +9,22 @@ import { CollectionViewType } from "../collections/CollectionBaseView";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { SelectionManager } from "../../util/SelectionManager";
 import { DocumentManager } from "../../util/DocumentManager";
+import { DocumentView } from "../nodes/DocumentView";
+import "./LinkFollowBox.scss";
+
+export type LinkParamOptions = {
+    container: Doc;
+    context: Doc;
+    sourceDoc: Doc;
+    shoudldZoom: boolean;
+    linkDoc: Doc;
+};
 
 @observer
 export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
     public static Instance: LinkFollowBox;
-    //set this to be the default link behavior, can be any of the above
 
     unhighlight = () => {
         Doc.UnhighlightAll();
@@ -31,19 +40,32 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         }, 10000);
     }
 
+    // DONE
+    @undoBatch
+    openFullScreen = (destinationDoc: Doc) => {
+        let view: DocumentView | null = DocumentManager.Instance.getDocumentView(destinationDoc)
+        view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+    }
+
+    // should container be a doc or documentview or what? This one needs work and is more long term
+    @undoBatch
+    openInContainer = (destinationDoc: Doc, options: { container: Doc }) => {
+
+    }
+
     // NOT TESTED
     // col = collection the doc is in
     // target = the document to center on
     @undoBatch
-    openLinkColRight = (destinationDoc: Doc, col: Doc) => {
-        col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
-        if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+    openLinkColRight = (destinationDoc: Doc, options: { context: Doc }) => {
+        options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
+        if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
             const newPanX = NumCast(destinationDoc.x) + NumCast(destinationDoc.width) / NumCast(destinationDoc.zoomBasis, 1) / 2;
             const newPanY = NumCast(destinationDoc.y) + NumCast(destinationDoc.height) / NumCast(destinationDoc.zoomBasis, 1) / 2;
-            col.panX = newPanX;
-            col.panY = newPanY;
+            options.context.panX = newPanX;
+            options.context.panY = newPanY;
         }
-        CollectionDockingView.Instance.AddRightSplit(col, undefined);
+        CollectionDockingView.Instance.AddRightSplit(options.context, undefined);
     }
 
     // DONE
@@ -60,39 +82,39 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     // this is the standard "follow link" (jump to document)
     // taken from follow link
     @undoBatch
-    jumpToLink = async (destinationDoc: Doc, shouldZoom: boolean, linkDoc: Doc) => {
+    jumpToLink = async (destinationDoc: Doc, options: { shouldZoom: boolean, linkDoc: Doc }) => {
         //there is an issue right now so this will be false automatically
-        shouldZoom = false;
+        options.shouldZoom = false;
         this.highlightDoc(destinationDoc);
         let jumpToDoc = destinationDoc;
         let pdfDoc = FieldValue(Cast(destinationDoc, Doc));
         if (pdfDoc) {
             jumpToDoc = pdfDoc;
         }
-        let proto = Doc.GetProto(linkDoc);
+        let proto = Doc.GetProto(options.linkDoc);
         let targetContext = await Cast(proto.targetContext, Doc);
         let sourceContext = await Cast(proto.sourceContext, Doc);
 
         let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
 
-        if (destinationDoc === linkDoc.anchor2 && targetContext) {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
+        if (destinationDoc === options.linkDoc.anchor2 && targetContext) {
+            DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
         }
-        else if (destinationDoc === linkDoc.anchor1 && sourceContext) {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
+        else if (destinationDoc === options.linkDoc.anchor1 && sourceContext) {
+            DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, document => dockingFunc(sourceContext!));
         }
         else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined, NumCast((destinationDoc === linkDoc.anchor2 ? linkDoc.anchor2Page : linkDoc.anchor1Page)));
+            DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, undefined, undefined, NumCast((destinationDoc === options.linkDoc.anchor2 ? options.linkDoc.anchor2Page : options.linkDoc.anchor1Page)));
 
         }
         else {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc);
+            DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, dockingFunc);
         }
     }
 
     // DONE
     // opens link in new tab (not in a collection)
-    // this opens it full screen, do we need a separate full screen option?
+    // this opens it full screen in new tab
     @undoBatch
     openLinkTab = (destinationDoc: Doc) => {
         this.highlightDoc(destinationDoc);
@@ -106,32 +128,32 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     // col = collection the doc is in
     // target = the document to center on
     @undoBatch
-    openLinkColTab = (destinationDoc: Doc, col: Doc) => {
+    openLinkColTab = (destinationDoc: Doc, options: { context: Doc }) => {
         this.highlightDoc(destinationDoc);
-        col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
-        if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+        options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
+        if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
             const newPanX = NumCast(destinationDoc.x) + NumCast(destinationDoc.width) / NumCast(destinationDoc.zoomBasis, 1) / 2;
             const newPanY = NumCast(destinationDoc.y) + NumCast(destinationDoc.height) / NumCast(destinationDoc.zoomBasis, 1) / 2;
-            col.panX = newPanX;
-            col.panY = newPanY;
+            options.context.panX = newPanX;
+            options.context.panY = newPanY;
         }
         // CollectionDockingView.Instance.AddRightSplit(col, undefined);
-        this.props.addDocTab(col, undefined, "inTab");
+        this.props.addDocTab(options.context, undefined, "inTab");
         SelectionManager.DeselectAll();
     }
 
     // DONE
     // this will open a link next to the source doc
     @undoBatch
-    openLinkInPlace = (destinationDoc: Doc, sourceDoc: Doc) => {
+    openLinkInPlace = (destinationDoc: Doc, options: { sourceDoc: Doc }) => {
         this.highlightDoc(destinationDoc);
 
         let alias = Doc.MakeAlias(destinationDoc);
-        let y = NumCast(sourceDoc.y);
-        let x = NumCast(sourceDoc.x);
+        let y = NumCast(options.sourceDoc.y);
+        let x = NumCast(options.sourceDoc.x);
 
-        let width = NumCast(sourceDoc.width);
-        let height = NumCast(sourceDoc.height);
+        let width = NumCast(options.sourceDoc.width);
+        let height = NumCast(options.sourceDoc.height);
 
         alias.x = x + width + 30;
         alias.y = y;
@@ -139,18 +161,19 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         alias.height = height;
 
         SelectionManager.SelectedDocuments().map(dv => {
-            if (dv.props.Document === sourceDoc) {
+            if (dv.props.Document === options.sourceDoc) {
                 dv.props.addDocument && dv.props.addDocument(alias, false);
             }
         });
     }
 
-    private defaultLinkBehavior: any = this.openLinkInPlace;
-    private currentLinkBehavior: any = this.defaultLinkBehavior;
+    //set this to be the default link behavior, can be any of the above
+    private defaultLinkBehavior: (destinationDoc: Doc, options?: any) => void = this.openLinkInPlace;
+    private currentLinkBehavior: (destinationDoc: Doc, options?: any) => void = this.defaultLinkBehavior;
 
     render() {
         return (
-            <div className="linkFollowBox-main">
+            <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
 
             </div>
         );
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 41723030d..60f27c664 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -15,6 +15,7 @@ import { CollectionDockingView } from '../collections/CollectionDockingView';
 import { SelectionManager } from '../../util/SelectionManager';
 import { CollectionViewType } from '../collections/CollectionBaseView';
 import { DocumentView } from '../nodes/DocumentView';
+import { SearchUtil } from '../../util/SearchUtil';
 library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
 
 
@@ -63,6 +64,13 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         CollectionDockingView.Instance.AddRightSplit(col, undefined);
     }
 
+    // DONE
+    @undoBatch
+    openFullScreen = () => {
+        let view: DocumentView | null = DocumentManager.Instance.getDocumentView(this.props.destinationDoc);
+        view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+    }
+
     // DONE
     // this opens the linked doc in a right split, NOT in its collection
     @undoBatch
@@ -161,10 +169,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                 dv.props.addDocument && dv.props.addDocument(alias, false);
             }
         });
+
+        this.jumpToLink(false);
     }
 
     //set this to be the default link behavior, can be any of the above
-    private defaultLinkBehavior: any = this.openLinkInPlace;
+    private defaultLinkBehavior: any = this.openLinkTab;
 
     onEdit = (e: React.PointerEvent): void => {
         e.stopPropagation();
-- 
cgit v1.2.3-70-g09d2


From ed7f9a9cd3255f7b774268cfda35685ddacfe2e9 Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Fri, 23 Aug 2019 16:38:27 -0400
Subject: basic menu and setting docs working

---
 src/client/documents/Documents.ts           |   1 +
 src/client/views/MainView.tsx               |   6 +-
 src/client/views/linking/LinkFollowBox.scss |  77 +++++++++-
 src/client/views/linking/LinkFollowBox.tsx  | 213 +++++++++++++++++++++++-----
 src/client/views/linking/LinkMenuItem.tsx   |   5 +-
 5 files changed, 260 insertions(+), 42 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b7c8f4c12..e903d1e06 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -54,6 +54,7 @@ var path = require('path');
 export interface DocumentOptions {
     x?: number;
     y?: number;
+    z?: number;
     type?: string;
     width?: number;
     height?: number;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 79c44ae72..82e3c706a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-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 } from '@fortawesome/free-solid-svg-icons';
+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 } 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';
@@ -443,7 +443,7 @@ export class MainView extends React.Component {
         let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
 
         let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
-        let addLinkFollowBox = action(() => Docs.Create.LinkFollowBoxDocument({ width: 200, height: 500, title: "Link Follow Document" }));
+        let addLinkFollowBox = action(() => Docs.Create.LinkFollowBoxDocument({ width: 500, height: 350, title: "Link Follower" }));
         let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
         let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
         let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
@@ -459,7 +459,7 @@ export class MainView extends React.Component {
             [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
             [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
             [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
-            [React.createRef<HTMLDivElement>(), "caret-up", "Open Link Follow Box", addLinkFollowBox],
+            [React.createRef<HTMLDivElement>(), "link", "Open Link Follow Box", addLinkFollowBox],
             [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
             //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
         ];
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
index 522191792..aedbfdea4 100644
--- a/src/client/views/linking/LinkFollowBox.scss
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -2,5 +2,80 @@
 
 .linkFollowBox-main {
     position: absolute;
-    background: red;
+    background: whitesmoke;
+    color: grey;
+    border-radius: 15px;
+    box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
+    border: solid #BBBBBBBB 5px;
+    pointer-events: all;
+    // overflow: hidden;
+
+    .linkFollowBox-header {
+        height: 30px;
+        text-align: center;
+        text-transform: uppercase;
+        line-height: 30px;
+        letter-spacing: 2px;
+        font-size: 16px;
+    }
+
+    .linkFollowBox-footer {
+        height: 50px;
+        text-align: center;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+
+        button {
+            background-color: $darker-alt-accent;
+            // height: 30px;
+            width: 30%;
+            // font-size: 18px;
+        }
+    }
+
+    .linkFollowBox-content {
+        display: grid;
+        grid-template-columns: 1fr 1fr 1fr;
+        grid-column-gap: 5px;
+        margin-left: 5px;
+        margin-right: 5px;
+
+        .linkFollowBox-item {
+            background-color: $light-color;
+            width: 100%;
+            height: 100%;
+
+            .linkFollowBox-itemContent {
+                padding: 5px;
+                font-size: 12px;
+                // line-height: 40px;
+                overflow: scroll;
+
+
+                input[type=radio] {
+                    border: 0px;
+                    // width: 100%;
+                    // height: 20px;
+                    // width: 20px;
+                    margin-right: 5px;
+                }
+            }
+
+            .title {
+                display: flex;
+                justify-content: center;
+                align-items: center;
+                text-transform: uppercase;
+                color: $light-color;
+                background-color: $lighter-alt-accent;
+                width: 100%;
+                height: 30px;
+                border-bottom: solid $darker-alt-accent 5px;
+                font-size: 12px;
+                text-align: center;
+            }
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 247b67776..8cd26bdec 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -1,10 +1,10 @@
-import { observable, computed, action, trace } from "mobx";
+import { observable, computed, action, trace, ObservableMap } from "mobx";
 import React = require("react");
 import { observer } from "mobx-react";
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
 import { Doc } from "../../../new_fields/Doc";
 import { undoBatch } from "../../util/UndoManager";
-import { NumCast, FieldValue, Cast } from "../../../new_fields/Types";
+import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
 import { CollectionViewType } from "../collections/CollectionBaseView";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { SelectionManager } from "../../util/SelectionManager";
@@ -12,19 +12,41 @@ import { DocumentManager } from "../../util/DocumentManager";
 import { DocumentView } from "../nodes/DocumentView";
 import "./LinkFollowBox.scss";
 
-export type LinkParamOptions = {
-    container: Doc;
-    context: Doc;
-    sourceDoc: Doc;
-    shoudldZoom: boolean;
-    linkDoc: Doc;
-};
+enum FollowModes {
+    OPENTAB = "Open in Tab",
+    OPENRIGHT = "Open in Right Split",
+    OPENFULL = "Open Full Screen",
+    PAN = "Pan to Document",
+    INPLACE = "Open In Place"
+}
+
+enum FollowOptions {
+    ZOOM = "zoom",
+    NOZOOM = "no zoom",
+}
 
 @observer
 export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     public static LayoutString() { return FieldView.LayoutString(LinkFollowBox); }
     public static Instance: LinkFollowBox;
+    @observable static linkDoc: Doc | undefined = undefined;
+    @observable static destinationDoc: Doc | undefined = undefined;
+    @observable static sourceDoc: Doc | undefined = undefined;
+    @observable selectedMode: string = "";
+    @observable selectedOption: string = "";
+
+    constructor(props: FieldViewProps) {
+        super(props);
+        LinkFollowBox.Instance = this;
+    }
+
+    @action
+    setLinkDocs = (linkDoc: Doc, source: Doc, dest: Doc) => {
+        LinkFollowBox.linkDoc = linkDoc;
+        LinkFollowBox.sourceDoc = source;
+        LinkFollowBox.destinationDoc = dest;
+    }
 
     unhighlight = () => {
         Doc.UnhighlightAll();
@@ -40,11 +62,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         }, 10000);
     }
 
-    // DONE
     @undoBatch
     openFullScreen = (destinationDoc: Doc) => {
-        let view: DocumentView | null = DocumentManager.Instance.getDocumentView(destinationDoc)
+        let view: DocumentView | null = DocumentManager.Instance.getDocumentView(destinationDoc);
         view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+        SelectionManager.DeselectAll();
     }
 
     // should container be a doc or documentview or what? This one needs work and is more long term
@@ -54,8 +76,6 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     }
 
     // NOT TESTED
-    // col = collection the doc is in
-    // target = the document to center on
     @undoBatch
     openLinkColRight = (destinationDoc: Doc, options: { context: Doc }) => {
         options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
@@ -66,26 +86,22 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             options.context.panY = newPanY;
         }
         CollectionDockingView.Instance.AddRightSplit(options.context, undefined);
+
+        this.highlightDoc(destinationDoc);
+        SelectionManager.DeselectAll();
     }
 
-    // DONE
-    // this opens the linked doc in a right split, NOT in its collection
     @undoBatch
     openLinkRight = (destinationDoc: Doc) => {
-        this.highlightDoc(destinationDoc);
         let alias = Doc.MakeAlias(destinationDoc);
         CollectionDockingView.Instance.AddRightSplit(alias, undefined);
+        this.highlightDoc(destinationDoc);
         SelectionManager.DeselectAll();
+
     }
 
-    // DONE
-    // this is the standard "follow link" (jump to document)
-    // taken from follow link
     @undoBatch
     jumpToLink = async (destinationDoc: Doc, options: { shouldZoom: boolean, linkDoc: Doc }) => {
-        //there is an issue right now so this will be false automatically
-        options.shouldZoom = false;
-        this.highlightDoc(destinationDoc);
         let jumpToDoc = destinationDoc;
         let pdfDoc = FieldValue(Cast(destinationDoc, Doc));
         if (pdfDoc) {
@@ -110,26 +126,23 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         else {
             DocumentManager.Instance.jumpToDocument(jumpToDoc, options.shouldZoom, false, dockingFunc);
         }
+
+        this.highlightDoc(destinationDoc);
+        SelectionManager.DeselectAll();
     }
 
-    // DONE
-    // opens link in new tab (not in a collection)
-    // this opens it full screen in new tab
     @undoBatch
     openLinkTab = (destinationDoc: Doc) => {
-        this.highlightDoc(destinationDoc);
         let fullScreenAlias = Doc.MakeAlias(destinationDoc);
         this.props.addDocTab(fullScreenAlias, undefined, "inTab");
+
+        this.highlightDoc(destinationDoc);
         SelectionManager.DeselectAll();
     }
 
     // NOT TESTED
-    // opens link in new tab in collection
-    // col = collection the doc is in
-    // target = the document to center on
     @undoBatch
     openLinkColTab = (destinationDoc: Doc, options: { context: Doc }) => {
-        this.highlightDoc(destinationDoc);
         options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
         if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
             const newPanX = NumCast(destinationDoc.x) + NumCast(destinationDoc.width) / NumCast(destinationDoc.zoomBasis, 1) / 2;
@@ -137,16 +150,14 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             options.context.panX = newPanX;
             options.context.panY = newPanY;
         }
-        // CollectionDockingView.Instance.AddRightSplit(col, undefined);
         this.props.addDocTab(options.context, undefined, "inTab");
+
+        this.highlightDoc(destinationDoc);
         SelectionManager.DeselectAll();
     }
 
-    // DONE
-    // this will open a link next to the source doc
     @undoBatch
-    openLinkInPlace = (destinationDoc: Doc, options: { sourceDoc: Doc }) => {
-        this.highlightDoc(destinationDoc);
+    openLinkInPlace = (destinationDoc: Doc, options: { sourceDoc: Doc, linkDoc: Doc }) => {
 
         let alias = Doc.MakeAlias(destinationDoc);
         let y = NumCast(options.sourceDoc.y);
@@ -165,16 +176,146 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 dv.props.addDocument && dv.props.addDocument(alias, false);
             }
         });
+
+        this.jumpToLink(destinationDoc, { shouldZoom: false, linkDoc: options.linkDoc });
+
+        this.highlightDoc(destinationDoc);
+        SelectionManager.DeselectAll();
     }
 
     //set this to be the default link behavior, can be any of the above
     private defaultLinkBehavior: (destinationDoc: Doc, options?: any) => void = this.openLinkInPlace;
-    private currentLinkBehavior: (destinationDoc: Doc, options?: any) => void = this.defaultLinkBehavior;
+    // private currentLinkBehavior: (destinationDoc: Doc, options?: any) => void = this.defaultLinkBehavior;
+
+    @computed
+    get LinkFollowTitle(): string {
+        if (LinkFollowBox.linkDoc) {
+            return StrCast(LinkFollowBox.linkDoc.title);
+        }
+        return "No Link Selected";
+    }
+
+    @action
+    currentLinkBehavior = () => {
+        if (this.selectedMode === FollowModes.INPLACE) {
+
+        }
+        else if (this.selectedMode === FollowModes.OPENFULL) {
+
+        }
+        else if (this.selectedMode === FollowModes.OPENRIGHT) {
+
+        }
+        else if (this.selectedMode === FollowModes.OPENTAB) {
+
+        }
+        else if (this.selectedMode === FollowModes.INPLACE) {
+
+        }
+        else if (this.selectedMode === FollowModes.PAN) {
+
+        }
+        else return;
+    }
+
+    @action
+    handleModeChange = (e: React.ChangeEvent) => {
+        let target = e.target as HTMLInputElement;
+        this.selectedMode = target.value;
+    }
+
+    @action
+    handleOptionChange = (e: React.ChangeEvent) => {
+        let target = e.target as HTMLInputElement;
+        this.selectedOption = target.value;
+    }
+
+    @computed
+    get availableModes() {
+        return (
+            <div>
+                <label><input
+                    type="radio"
+                    name="mode"
+                    value={FollowModes.OPENRIGHT}
+                    checked={this.selectedMode === FollowModes.OPENRIGHT}
+                    onChange={this.handleModeChange} />
+                    {FollowModes.OPENRIGHT}
+                </label><br />
+                <label><input
+                    type="radio"
+                    name="mode"
+                    value={FollowModes.OPENTAB}
+                    checked={this.selectedMode === FollowModes.OPENTAB}
+                    onChange={this.handleModeChange} />
+                    {FollowModes.OPENTAB}
+                </label><br />
+                <label><input
+                    type="radio"
+                    name="mode"
+                    value={FollowModes.OPENFULL}
+                    checked={this.selectedMode === FollowModes.OPENFULL}
+                    onChange={this.handleModeChange} />
+                    {FollowModes.OPENFULL}
+                </label><br />
+                <label><input
+                    type="radio"
+                    name="mode"
+                    value={FollowModes.PAN}
+                    checked={this.selectedMode === FollowModes.PAN}
+                    onChange={this.handleModeChange} />
+                    {FollowModes.PAN}
+                </label><br />
+                <label><input
+                    type="radio"
+                    name="mode"
+                    value={FollowModes.INPLACE}
+                    checked={this.selectedMode === FollowModes.INPLACE}
+                    onChange={this.handleModeChange} />
+                    {FollowModes.INPLACE}
+                </label><br />
+            </div>
+        );
+    }
+
+    @computed
+    get contexts() {
+        return (
+            <div>
+
+            </div>
+        );
+    }
 
     render() {
         return (
             <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
+                <div className="linkFollowBox-header">{this.LinkFollowTitle}</div>
+                <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 90 }}>
+                    <div className="linkFollowBox-item">
+                        <div className="linkFollowBox-item title">Mode</div>
+                        <div className="linkFollowBox-itemContent">{this.availableModes}</div>
+                    </div>
+                    <div className="linkFollowBox-item">
+                        <div className="linkFollowBox-item title">Context</div>
+                        <div className="linkFollowBox-itemContent">
+
+                        </div>
+                    </div>
+                    <div className="linkFollowBox-item">
+                        <div className="linkFollowBox-item title">Options</div>
+                        <div className="linkFollowBox-itemContent">
 
+                        </div>
+                    </div>
+                </div>
+                <div className="linkFollowBox-footer">
+                    <button
+                        onClick={this.currentLinkBehavior}
+                        disabled={(LinkFollowBox.linkDoc) ? false : true}>
+                        Follow Link
+                    </button>
+                </div>
             </div>
         );
     }
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 60f27c664..406429ebf 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -16,6 +16,7 @@ import { SelectionManager } from '../../util/SelectionManager';
 import { CollectionViewType } from '../collections/CollectionBaseView';
 import { DocumentView } from '../nodes/DocumentView';
 import { SearchUtil } from '../../util/SearchUtil';
+import { LinkFollowBox } from './LinkFollowBox';
 library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
 
 
@@ -174,7 +175,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     }
 
     //set this to be the default link behavior, can be any of the above
-    private defaultLinkBehavior: any = this.openLinkTab;
+    // private defaultLinkBehavior: any = LinkFollowBox.computeLinkDocs(this.props.linkDoc);
 
     onEdit = (e: React.PointerEvent): void => {
         e.stopPropagation();
@@ -242,7 +243,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                             {/* Original */}
                             {/* <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */}
                             {/* New */}
-                            <div title="Follow link" className="button" onPointerDown={this.defaultLinkBehavior}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+                            <div title="Follow link" className="button" onClick={() => LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc)}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                         </div>
                     </div>
                     {this._showMore ? this.renderMetadata() : <></>}
-- 
cgit v1.2.3-70-g09d2


From d527b7ee793a9cbed963963b263a8490d74c797f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 24 Aug 2019 13:15:29 -0400
Subject: can set links in google docs

---
 .../apis/google_docs/GoogleApiClientUtils.ts       | 17 ++++++++++---
 src/client/views/MainView.tsx                      | 28 ++++++++++++++++++++++
 2 files changed, 42 insertions(+), 3 deletions(-)

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

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 798886def..61df69d5c 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -190,6 +190,18 @@ export namespace GoogleApiClientUtils {
             });
         };
 
+        export const setStyle = async (options: UpdateOptions) => {
+            let replies: any = await update({
+                documentId: options.documentId,
+                requests: options.requests
+            });
+            if ("errors" in replies) {
+                console.log("Write operation failed:");
+                console.log(replies.errors.map((error: any) => error.message));
+            }
+            return replies;
+        };
+
         export const write = async (options: WriteOptions): Promise<UpdateResult> => {
             const requests: docs_v1.Schema$Request[] = [];
             const identifier = await Utils.initialize(options.reference);
@@ -226,10 +238,9 @@ export namespace GoogleApiClientUtils {
                 return undefined;
             }
             let replies: any = await update({ documentId: identifier, requests });
-            let errors = "errors";
-            if (errors in replies) {
+            if ("errors" in replies) {
                 console.log("Write operation failed:");
-                console.log(replies[errors].map((error: any) => error.message));
+                console.log(replies.errors.map((error: any) => error.message));
             }
             return replies;
         };
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a02214deb..b35b2d331 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -40,6 +40,8 @@ import { PreviewCursor } from './PreviewCursor';
 import { FilterBox } from './search/FilterBox';
 import PresModeMenu from './presentationview/PresentationModeMenu';
 import { PresBox } from './nodes/PresBox';
+import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils';
+import { docs_v1 } from 'googleapis';
 
 @observer
 export class MainView extends React.Component {
@@ -120,6 +122,32 @@ export class MainView extends React.Component {
     componentWillMount() {
         var tag = document.createElement('script');
 
+        let requests: docs_v1.Schema$Request[] =
+            [{
+                updateTextStyle: {
+                    fields: "*",
+                    range: {
+                        startIndex: 1,
+                        endIndex: 15
+                    },
+                    textStyle: {
+                        bold: true,
+                        link: { url: window.location.href },
+                        foregroundColor: {
+                            color: {
+                                rgbColor: {
+                                    red: 1.0,
+                                    green: 0.0,
+                                    blue: 0.0
+                                }
+                            }
+                        }
+                    }
+                }
+            }];
+        let documentId = "1xBwN4akVePW_Zp8wbiq0WNjlzGAE2PyNVvwzFbUyv3I";
+        GoogleApiClientUtils.Docs.setStyle({ documentId, requests });
+
         tag.src = "https://www.youtube.com/iframe_api";
         var firstScriptTag = document.getElementsByTagName('script')[0];
         firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
-- 
cgit v1.2.3-70-g09d2


From 045d889f138861c5b22ec671e24aa553eaa4f441 Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Mon, 26 Aug 2019 18:10:14 -0400
Subject: ALMOST EVERYTHING DONE

---
 src/client/views/MainView.tsx               |   3 +-
 src/client/views/linking/LinkFollowBox.scss |   8 +-
 src/client/views/linking/LinkFollowBox.tsx  | 255 +++++++++++++++++-----------
 3 files changed, 160 insertions(+), 106 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7520dff8c..aae67ffb2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -443,7 +443,7 @@ export class MainView extends React.Component {
         let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
 
         let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
-        let addLinkFollowBox = action(() => Docs.Create.LinkFollowBoxDocument({ width: 500, height: 350, title: "Link Follower" }));
+        let addLinkFollowBox = action(() => Docs.Create.LinkFollowBoxDocument({ width: 500, height: 370, title: "Link Follower" }));
         let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
         let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
         let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
@@ -576,7 +576,6 @@ export class MainView extends React.Component {
                 <PDFMenu />
                 <MainOverlayTextBox firstinstance={true} />
                 <OverlayView />
-                {/* <LinkFollowBox /> */}
             </div >
         );
     }
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
index aedbfdea4..7fad9ae93 100644
--- a/src/client/views/linking/LinkFollowBox.scss
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -11,14 +11,18 @@
     // overflow: hidden;
 
     .linkFollowBox-header {
-        height: 30px;
+        height: 50px;
         text-align: center;
         text-transform: uppercase;
-        line-height: 30px;
+        // line-height: 30px;
         letter-spacing: 2px;
         font-size: 16px;
     }
 
+    .direction-indicator {
+        font-size: 12px;
+    }
+
     .linkFollowBox-footer {
         height: 50px;
         text-align: center;
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index bf742cb81..213022bc8 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -14,6 +14,8 @@ import "./LinkFollowBox.scss";
 import { SearchUtil } from "../../util/SearchUtil";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { listSpec } from "../../../new_fields/Schema";
+import { DocServer } from "../../DocServer";
+import { RefField } from "../../../new_fields/RefField";
 
 enum FollowModes {
     OPENTAB = "Open in Tab",
@@ -37,13 +39,15 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @observable static destinationDoc: Doc | undefined = undefined;
     @observable static sourceDoc: Doc | undefined = undefined;
     @observable selectedMode: string = "";
-    @observable selectedContext: any = undefined;
+    @observable selectedContext: Doc | undefined = undefined;
+    @observable selectedContextAliases: Doc[] | undefined = undefined;
     @observable selectedOption: string = "";
     @observable selectedContextString: string = "";
     @observable sourceView: DocumentView | undefined = undefined;
     @observable canPan: boolean = false;
     @observable shouldUseOnlyParentContext = false;
-    _panDisposer?: IReactionDisposer;
+    _contextDisposer?: IReactionDisposer;
+    collectionTypes: string[];
 
     @observable private _docs: { col: Doc, target: Doc }[] = [];
     @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
@@ -51,33 +55,49 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     constructor(props: FieldViewProps) {
         super(props);
         LinkFollowBox.Instance = this;
+
+        this.collectionTypes = ["Invalid", "Freeform", "Schema", "Docking", "Tree", "Stacking", "Masonry"];
     }
 
     componentDidMount = () => {
         this.resetVars();
 
-        this._panDisposer = reaction(
-            () => LinkFollowBox.destinationDoc,
+        this._contextDisposer = reaction(
+            () => this.selectedContextString,
             async () => {
-                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 === 1) {
-                        //this means its in a freeform collection
-                        let docs = Cast(colDoc.data, listSpec(Doc), []);
-                        let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
-
-                        aliases.forEach(alias => {
-                            if (docs.filter(doc => doc === alias).length > 0) { runInAction(() => { this.canPan = true; }); }
-                        });
+                let ref = await DocServer.GetRefField(this.selectedContextString);
+                runInAction(() => {
+                    if (ref instanceof Doc) {
+                        this.selectedContext = ref;
                     }
+                });
+                if (this.selectedContext instanceof Doc) {
+                    let aliases = await SearchUtil.GetViewsOfDocument(this.selectedContext);
+                    runInAction(() => { this.selectedContextAliases = aliases; });
                 }
             }
         );
     }
 
     componentWillUnmount = () => {
-        this._panDisposer && this._panDisposer();
+        this._contextDisposer && this._contextDisposer();
+    }
+
+    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), []);
+                let aliases = await SearchUtil.GetViewsOfDocument(Doc.GetProto(LinkFollowBox.destinationDoc));
+
+                aliases.forEach(alias => {
+                    if (docs.filter(doc => doc === alias).length > 0) {
+                        runInAction(() => { this.canPan = true; });
+                    }
+                });
+            }
+        }
     }
 
     @action
@@ -124,6 +144,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 this.sourceView = dv;
             }
         });
+
+        this.resetPan();
     }
 
     unhighlight = () => {
@@ -151,15 +173,30 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         }
     }
 
+    @undoBatch
+    openColFullScreen = (options: { context: Doc }) => {
+        if (LinkFollowBox.destinationDoc) {
+            if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
+                const newPanX = NumCast(LinkFollowBox.destinationDoc.x) + NumCast(LinkFollowBox.destinationDoc.width) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2;
+                const newPanY = NumCast(LinkFollowBox.destinationDoc.y) + NumCast(LinkFollowBox.destinationDoc.height) / NumCast(LinkFollowBox.destinationDoc.zoomBasis, 1) / 2;
+                options.context.panX = newPanX;
+                options.context.panY = newPanY;
+            }
+            let view = DocumentManager.Instance.getDocumentView(options.context);
+            view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
+            this.highlightDoc();
+            SelectionManager.DeselectAll();
+        }
+    }
+
     // should container be a doc or documentview or what? This one needs work and is more long term
     @undoBatch
     openInContainer = (options: { container: Doc }) => {
 
     }
 
-    // NOT TESTED
     @undoBatch
-    openLinkColRight = (options: { context: Doc }) => {
+    openLinkColRight = (options: { context: Doc, shouldZoom: boolean }) => {
         if (LinkFollowBox.destinationDoc) {
             options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
             if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
@@ -170,6 +207,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             }
             CollectionDockingView.Instance.AddRightSplit(options.context, undefined);
 
+            if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
+
             this.highlightDoc();
             SelectionManager.DeselectAll();
         }
@@ -231,9 +270,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         }
     }
 
-    // NOT TESTED
     @undoBatch
-    openLinkColTab = (options: { context: Doc }) => {
+    openLinkColTab = (options: { context: Doc, shouldZoom: boolean }) => {
         if (LinkFollowBox.destinationDoc) {
             options.context = Doc.IsPrototype(options.context) ? Doc.MakeDelegate(options.context) : options.context;
             if (NumCast(options.context.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
@@ -243,6 +281,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 options.context.panY = newPanY;
             }
             this.props.addDocTab(options.context, undefined, "inTab");
+            if (options.shouldZoom) this.jumpToLink({ shouldZoom: options.shouldZoom });
 
             this.highlightDoc();
             SelectionManager.DeselectAll();
@@ -254,7 +293,6 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
         if (LinkFollowBox.destinationDoc && LinkFollowBox.sourceDoc) {
             let alias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
-            console.log(alias)
             let y = NumCast(LinkFollowBox.sourceDoc.y);
             let x = NumCast(LinkFollowBox.sourceDoc.x);
 
@@ -279,33 +317,35 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
     //set this to be the default link behavior, can be any of the above
     private defaultLinkBehavior: (options?: any) => void = this.openLinkInPlace;
-    // private currentLinkBehavior: (destinationDoc: Doc, options?: any) => void = this.defaultLinkBehavior;
 
     @action
     currentLinkBehavior = () => {
-        let shouldZoom: boolean | undefined = this.selectedOption === "" ? undefined :
-            this.selectedOption === FollowOptions.NOZOOM ? false : true;
-
-        if (this.selectedMode === FollowModes.INPLACE) {
-            if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom });
-        }
-        else if (this.selectedMode === FollowModes.OPENFULL) {
-            this.openFullScreen();
-        }
-        else if (this.selectedMode === FollowModes.OPENRIGHT) {
-            if (this.selectedContextString === "self") this.openLinkRight();
-            else this.selectedContext && this.openLinkColRight({ context: this.selectedContext });
-        }
-        else if (this.selectedMode === FollowModes.OPENTAB) {
-
-        }
-        else if (this.selectedMode === FollowModes.INPLACE) {
-
-        }
-        else if (this.selectedMode === FollowModes.PAN) {
+        // this.resetPan();
+        if (LinkFollowBox.destinationDoc) {
+            if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM;
+            let shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
+            let notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
 
+            if (this.selectedMode === FollowModes.INPLACE) {
+                if (shouldZoom !== undefined) this.openLinkInPlace({ shouldZoom: shouldZoom });
+            }
+            else if (this.selectedMode === FollowModes.OPENFULL) {
+                if (notOpenInContext) this.openFullScreen();
+                else this.selectedContext && this.openColFullScreen({ context: this.selectedContext });
+            }
+            else if (this.selectedMode === FollowModes.OPENRIGHT) {
+                if (notOpenInContext) this.openLinkRight();
+                else this.selectedContext && this.openLinkColRight({ context: this.selectedContext, shouldZoom: shouldZoom });
+            }
+            else if (this.selectedMode === FollowModes.OPENTAB) {
+                if (notOpenInContext) this.openLinkTab();
+                else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom })
+            }
+            else if (this.selectedMode === FollowModes.PAN) {
+                this.jumpToLink({ shouldZoom: shouldZoom });
+            }
+            else return;
         }
-        else return;
     }
 
     @action
@@ -317,11 +357,6 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
 
         this.shouldUseOnlyParentContext = (this.selectedMode === FollowModes.INPLACE || this.selectedMode === FollowModes.PAN);
 
-        if (this.selectedMode === FollowModes.OPENFULL) {
-            this.selectedContextString = "self";
-            this.selectedContext = LinkFollowBox.destinationDoc;
-        }
-
         if (this.shouldUseOnlyParentContext) {
             if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
                 this.selectedContext = this.sourceView.props.ContainingCollectionView.props.Document;
@@ -339,10 +374,8 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @action
     handleContextChange = (e: React.ChangeEvent) => {
         let target = e.target as HTMLInputElement;
-        let toArr = target.value;
-        let arr = toArr.split(",");
-        this.selectedContextString = arr[0];
-        console.log(this.selectedContextString)
+        this.selectedContextString = target.value;
+        // selectedContext is updated in reaction
         this.selectedOption = "";
     }
 
@@ -351,10 +384,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         if (this.sourceView && this.sourceView.props.ContainingCollectionView) {
             let colView = this.sourceView.props.ContainingCollectionView;
             let colDoc = colView.props.Document;
-            if (colDoc.viewType && colDoc.viewType === 1) {
-                //this means its in a freeform collection
-                return true;
-            }
+            if (colDoc.viewType && colDoc.viewType === CollectionViewType.Freeform) return true;
         }
         return false;
     }
@@ -436,75 +466,96 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                 <label><input
                     type="radio" disabled={true}
                     name="context"
-                    value={["col", this.parentID]}
+                    value={this.parentID}
                     checked={true} />
                     {this.parentName} (Parent Collection)
                 </label>
                 :
                 <div>
                     <label><input
-                        type="radio" disabled={LinkFollowBox.linkDoc && !(this.selectedMode === FollowModes.OPENFULL) ? false : true}
+                        type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
                         name="context"
-                        value={LinkFollowBox.destinationDoc ? ["self", StrCast(LinkFollowBox.destinationDoc[Id])] : "self"}
-                        checked={this.selectedContextString === "self"}
+                        value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"}
+                        checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) : this.selectedContextString === "self"}
                         onChange={this.handleContextChange} />
                         Open Self
                 </label><br />
-                    {this.selectedMode === FollowModes.OPENFULL ? null :
-                        [...this._docs, ...this._otherDocs].map(doc => {
-                            if (doc && doc.target) {
-                                return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}>
-                                    <input
-                                        type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
-                                        name="context"
-                                        value={[StrCast(doc.col.title), StrCast(doc.col[Id])]}
-                                        checked={this.selectedContextString === StrCast(doc.col.title)}
-                                        onChange={this.handleContextChange} />
-                                    {doc.col.title}
-                                </label><br /></div>;
-                            }
-                        })}
+                    {[...this._docs, ...this._otherDocs].map(doc => {
+                        if (doc && doc.target && doc.col.title !== "Recently Closed") {
+                            return <div key={doc.col[Id] + doc.target[Id]}><label key={doc.col[Id] + doc.target[Id]}>
+                                <input
+                                    type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
+                                    name="context"
+                                    value={StrCast(doc.col[Id])}
+                                    checked={this.selectedContextString === StrCast(doc.col[Id])}
+                                    onChange={this.handleContextChange} />
+                                {doc.col.title}
+                            </label><br /></div>;
+                        }
+                    })}
                 </div>
         );
     }
 
+    @computed
+    get shouldShowZoom(): boolean {
+        if (this.selectedMode === FollowModes.OPENFULL) return false;
+        if (this.shouldUseOnlyParentContext) return true;
+        if (LinkFollowBox.destinationDoc ? this.selectedContextString === LinkFollowBox.destinationDoc[Id] : "self") return false;
+
+        let contextMatch: boolean = false;
+        if (this.selectedContextAliases) {
+            this.selectedContextAliases.forEach(alias => {
+                if (alias.viewType === CollectionViewType.Freeform) contextMatch = true;
+            });
+        }
+        if (contextMatch) return true;
+
+        return false;
+    }
+
     @computed
     get availableOptions() {
-        let shouldShowZoom = (this.shouldUseOnlyParentContext || this.selectedContextString !== "self");
-        return (
-            shouldShowZoom ?
-                <div>
-                    <label><input
-                        type="radio"
-                        name="option"
-                        value={FollowOptions.ZOOM}
-                        checked={this.selectedOption === FollowOptions.ZOOM}
-                        onChange={this.handleOptionChange}
-                        disabled={false} />
-                        {FollowOptions.ZOOM}
-                    </label><br />
-                    <label><input
-                        type="radio"
-                        name="option"
-                        value={FollowOptions.NOZOOM}
-                        checked={this.selectedOption === FollowOptions.NOZOOM}
-                        onChange={this.handleOptionChange}
-                        disabled={false} />
-                        {FollowOptions.NOZOOM}
-                    </label><br />
-                </div>
-                :
-                <div>No Available Options</div>
-        );
+        if (LinkFollowBox.destinationDoc) {
+            return (
+                this.shouldShowZoom ?
+                    <div>
+                        <label><input
+                            type="radio"
+                            name="option"
+                            value={FollowOptions.ZOOM}
+                            checked={this.selectedOption === FollowOptions.ZOOM}
+                            onChange={this.handleOptionChange}
+                            disabled={false} />
+                            {FollowOptions.ZOOM}
+                        </label><br />
+                        <label><input
+                            type="radio"
+                            name="option"
+                            value={FollowOptions.NOZOOM}
+                            checked={this.selectedOption === FollowOptions.NOZOOM}
+                            onChange={this.handleOptionChange}
+                            disabled={false} />
+                            {FollowOptions.NOZOOM}
+                        </label><br />
+                    </div>
+                    :
+                    <div>No Available Options</div>
+            );
+        }
+        return null;
     }
 
     render() {
         return (
             <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
                 <div className="linkFollowBox-header">
-                    {LinkFollowBox.linkDoc ? "Link Title :" + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
+                    {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
+                    <div className="linkFollowBox-header direction-indicator">{LinkFollowBox.linkDoc ?
+                        LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
+                            : "" : ""}</div>
                 </div>
-                <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 90 }}>
+                <div className="linkFollowBox-content" style={{ height: NumCast(this.props.Document.height) - 110 }}>
                     <div className="linkFollowBox-item">
                         <div className="linkFollowBox-item title">Mode</div>
                         <div className="linkFollowBox-itemContent">
-- 
cgit v1.2.3-70-g09d2


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

---
 .../apis/google_docs/GoogleApiClientUtils.ts       |  26 ++---
 src/client/documents/Documents.ts                  |   7 +-
 src/client/views/MainView.tsx                      |  26 -----
 src/client/views/nodes/FormattedTextBox.tsx        |  51 ++++----
 src/new_fields/RichTextField.ts                    |  49 --------
 src/new_fields/RichTextUtils.ts                    | 129 +++++++++++++++++++++
 6 files changed, 173 insertions(+), 115 deletions(-)
 create mode 100644 src/new_fields/RichTextUtils.ts

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

diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 61df69d5c..689009254 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -27,11 +27,14 @@ export namespace GoogleApiClientUtils {
 
     export type Identifier = string;
     export type Reference = Identifier | CreateOptions;
-    export type TextContent = string | string[];
+    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 type ReadResult = { title: string, body: string };
 
     export interface CreateOptions {
         service: Service;
@@ -50,7 +53,7 @@ export namespace GoogleApiClientUtils {
 
     export interface WriteOptions {
         mode: WriteMode;
-        content: TextContent;
+        content: Content;
         reference: Reference;
         index?: number; // if excluded, will compute the last index of the document and append the content there
     }
@@ -165,28 +168,24 @@ export namespace GoogleApiClientUtils {
             }
         };
 
-        export const read = async (options: ReadOptions): Promise<ReadResult> => {
+        export const read = async (options: ReadOptions): Promise<Opt<ReadResult>> => {
             return retrieve({ ...options, service: Service.Documents }).then(document => {
-                let result: ReadResult = {};
                 if (document) {
-                    let title = document.title;
+                    let title = document.title!;
                     let body = Utils.extractText(document, options.removeNewlines);
-                    result = { title, body };
+                    return { title, body };
                 }
-                return result;
             });
         };
 
-        export const readLines = async (options: ReadOptions): Promise<ReadLinesResult> => {
+        export const readLines = async (options: ReadOptions): Promise<Opt<ReadLinesResult>> => {
             return retrieve({ ...options, service: Service.Documents }).then(document => {
-                let result: ReadLinesResult = {};
                 if (document) {
                     let title = document.title;
                     let bodyLines = Utils.extractText(document).split("\n");
                     options.removeNewlines && (bodyLines = bodyLines.filter(line => line.length));
-                    result = { title, bodyLines };
+                    return { title, bodyLines };
                 }
-                return result;
             });
         };
 
@@ -227,7 +226,7 @@ export namespace GoogleApiClientUtils {
                 });
                 index = 1;
             }
-            const text = options.content;
+            const text = options.content.text;
             text.length && requests.push({
                 insertText: {
                     text: isArray(text) ? text.join("\n") : text,
@@ -237,6 +236,7 @@ export namespace GoogleApiClientUtils {
             if (!requests.length) {
                 return undefined;
             }
+            requests.push(...options.content.links);
             let replies: any = await update({ documentId: identifier, requests });
             if ("errors" in replies) {
                 console.log("Write operation failed:");
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 47df17329..e40e095d6 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -496,10 +496,13 @@ export namespace Docs {
          * @param title an optional title to give to the highest parent document in the hierarchy
          */
         export function DocumentHierarchyFromJson(input: any, title?: string): Opt<Doc> {
-            if (input === null || ![...primitives, "object"].includes(typeof input)) {
+            if (input === undefined || input === null || ![...primitives, "object"].includes(typeof input)) {
                 return undefined;
             }
-            let parsed: any = typeof input === "string" ? JSONUtils.tryParse(input) : input;
+            let parsed = input;
+            if (typeof input === "string") {
+                parsed = JSONUtils.tryParse(input);
+            }
             let converted: Doc;
             if (typeof parsed === "object" && !(parsed instanceof Array)) {
                 converted = convertObject(parsed, title);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b35b2d331..ab87f0c7b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -122,32 +122,6 @@ export class MainView extends React.Component {
     componentWillMount() {
         var tag = document.createElement('script');
 
-        let requests: docs_v1.Schema$Request[] =
-            [{
-                updateTextStyle: {
-                    fields: "*",
-                    range: {
-                        startIndex: 1,
-                        endIndex: 15
-                    },
-                    textStyle: {
-                        bold: true,
-                        link: { url: window.location.href },
-                        foregroundColor: {
-                            color: {
-                                rgbColor: {
-                                    red: 1.0,
-                                    green: 0.0,
-                                    blue: 0.0
-                                }
-                            }
-                        }
-                    }
-                }
-            }];
-        let documentId = "1xBwN4akVePW_Zp8wbiq0WNjlzGAE2PyNVvwzFbUyv3I";
-        GoogleApiClientUtils.Docs.setStyle({ documentId, requests });
-
         tag.src = "https://www.youtube.com/iframe_api";
         var firstScriptTag = document.getElementsByTagName('script')[0];
         firstScriptTag.parentNode!.insertBefore(tag, firstScriptTag);
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);
         }
     }
 
diff --git a/src/new_fields/RichTextField.ts b/src/new_fields/RichTextField.ts
index 1b52e6f82..d2f76c969 100644
--- a/src/new_fields/RichTextField.ts
+++ b/src/new_fields/RichTextField.ts
@@ -4,11 +4,6 @@ import { Deserializable } from "../client/util/SerializationHelper";
 import { Copy, ToScriptString } from "./FieldSymbols";
 import { scriptingGlobal } from "../client/util/Scripting";
 
-export const ToPlainText = Symbol("PlainText");
-export const FromPlainText = Symbol("PlainText");
-const delimiter = "\n";
-const joiner = "";
-
 @scriptingGlobal
 @Deserializable("RichTextField")
 export class RichTextField extends ObjectField {
@@ -28,48 +23,4 @@ export class RichTextField extends ObjectField {
         return `new RichTextField("${this.Data}")`;
     }
 
-    public static Initialize = (initial: string) => {
-        !initial.length && (initial = " ");
-        let pos = initial.length + 1;
-        return `{"doc":{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"${initial}"}]}]},"selection":{"type":"text","anchor":${pos},"head":${pos}}}`;
-    }
-
-    [ToPlainText]() {
-        // Because we're working with plain text, just concatenate all paragraphs
-        let content = JSON.parse(this.Data).doc.content;
-        let paragraphs = content.filter((item: any) => item.type === "paragraph");
-
-        // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
-        // While this function already exists in state.doc.textBeteen(), it doesn't account for newlines 
-        let blockText = (block: any) => block.text;
-        let concatenateParagraph = (p: any) => (p.content ? p.content.map(blockText).join(joiner) : "") + delimiter;
-
-        // Concatentate paragraphs and string the result together
-        let textParagraphs: string[] = paragraphs.map(concatenateParagraph);
-        let plainText = textParagraphs.join(joiner);
-        return plainText.substring(0, plainText.length - 1);
-    }
-
-    [FromPlainText](plainText: string) {
-        // Remap the text, creating blocks split on newlines
-        let elements = plainText.split(delimiter);
-
-        // Google Docs adds in an extra carriage return automatically, so this counteracts it
-        !elements[elements.length - 1].length && elements.pop();
-
-        // Preserve the current state, but re-write the content to be the blocks
-        let parsed = JSON.parse(this.Data);
-        parsed.doc.content = elements.map(text => {
-            let paragraph: any = { type: "paragraph" };
-            text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
-            return paragraph;
-        });
-
-        // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it
-        parsed.selection = { type: "text", anchor: 1, head: 1 };
-
-        // Export the ProseMirror-compatible state object we've jsut built
-        return JSON.stringify(parsed);
-    }
-
 }
\ No newline at end of file
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
new file mode 100644
index 000000000..b2b1dbaee
--- /dev/null
+++ b/src/new_fields/RichTextUtils.ts
@@ -0,0 +1,129 @@
+import { EditorState } from "prosemirror-state";
+import { Node } from "prosemirror-model";
+import { RichTextField } from "./RichTextField";
+import { docs_v1 } from "googleapis";
+import { GoogleApiClientUtils } from "../client/apis/google_docs/GoogleApiClientUtils";
+
+export namespace RichTextUtils {
+
+    const delimiter = "\n";
+    const joiner = "";
+
+
+    export const Initialize = (initial?: string) => {
+        let content: any[] = [];
+        let state = {
+            doc: {
+                type: "doc",
+                content,
+            },
+            selection: {
+                type: "text",
+                anchor: 0,
+                head: 0
+            }
+        };
+        if (initial && initial.length) {
+            content.push({
+                type: "paragraph",
+                content: {
+                    type: "text",
+                    text: initial
+                }
+            });
+            state.selection.anchor = state.selection.head = initial.length + 1;
+        }
+        return JSON.stringify(state);
+    };
+
+    export const Synthesize = (plainText: string, oldState?: RichTextField) => {
+        return new RichTextField(ToProsemirrorState(plainText, oldState));
+    };
+
+    export const ToPlainText = (state: EditorState) => {
+        // Because we're working with plain text, just concatenate all paragraphs
+        let content = state.doc.content;
+        let paragraphs: Node<any>[] = [];
+        content.forEach(node => node.type.name === "paragraph" && paragraphs.push(node));
+
+        // Functions to flatten ProseMirror paragraph objects (and their components) to plain text
+        // Concatentate paragraphs and string the result together
+        let textParagraphs: string[] = paragraphs.map(paragraph => {
+            let text: string[] = [];
+            paragraph.content.forEach(node => node.text && text.push(node.text));
+            return text.join(joiner) + delimiter;
+        });
+        let plainText = textParagraphs.join(joiner);
+        return plainText.substring(0, plainText.length - 1);
+    };
+
+    export const ToProsemirrorState = (plainText: string, oldState?: RichTextField) => {
+        // Remap the text, creating blocks split on newlines
+        let elements = plainText.split(delimiter);
+
+        // Google Docs adds in an extra carriage return automatically, so this counteracts it
+        !elements[elements.length - 1].length && elements.pop();
+
+        // Preserve the current state, but re-write the content to be the blocks
+        let parsed = JSON.parse(oldState ? oldState.Data : Initialize());
+        parsed.doc.content = elements.map(text => {
+            let paragraph: any = { type: "paragraph" };
+            text.length && (paragraph.content = [{ type: "text", marks: [], text }]); // An empty paragraph gets treated as a line break
+            return paragraph;
+        });
+
+        // If the new content is shorter than the previous content and selection is unchanged, may throw an out of bounds exception, so we reset it
+        parsed.selection = { type: "text", anchor: 1, head: 1 };
+
+        // Export the ProseMirror-compatible state object we've just built
+        return JSON.stringify(parsed);
+    };
+
+    export namespace GoogleDocs {
+
+        export const Convert = (state: EditorState): GoogleApiClientUtils.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 links: docs_v1.Schema$Request[] = [];
+            let position = 1;
+            for (let node of textNodes) {
+                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,
+                    }));
+                }
+                position += length;
+            }
+            return { text, links };
+        };
+
+        interface LinkInformation {
+            startIndex: number;
+            endIndex: number;
+            url: string;
+        }
+        const encode = (information: LinkInformation) => {
+            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 } } }
+                    }
+                }
+            };
+        };
+    }
+
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From b4f79e48d5d30312367e268c2c505f65c5cacdb4 Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Tue, 27 Aug 2019 13:19:20 -0400
Subject: things are progressing

---
 src/client/views/MainView.tsx               | 30 ++++++++++++++++++-----------
 src/client/views/OverlayView.tsx            |  2 +-
 src/client/views/linking/LinkFollowBox.scss |  1 +
 src/client/views/linking/LinkFollowBox.tsx  | 12 +++++++++++-
 4 files changed, 32 insertions(+), 13 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index aae67ffb2..b49274e7e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -12,7 +12,7 @@ import { List } from '../../new_fields/List';
 import { Id } from '../../new_fields/FieldSymbols';
 import { InkTool } from '../../new_fields/InkField';
 import { listSpec } from '../../new_fields/Schema';
-import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types';
+import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
 import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
@@ -41,6 +41,7 @@ import { FilterBox } from './search/FilterBox';
 import PresModeMenu from './presentationview/PresentationModeMenu';
 import { PresBox } from './nodes/PresBox';
 import { LinkFollowBox } from './linking/LinkFollowBox';
+import { DocumentManager } from '../util/DocumentManager';
 
 @observer
 export class MainView extends React.Component {
@@ -436,14 +437,27 @@ export class MainView extends React.Component {
         }
     }
 
+    toggleLinkFollowBox = () => {
+        if (LinkFollowBox.Instance) {
+            console.log("is instance!")
+            // if (LinkFollowBox.Instance.props && LinkFollowBox.Instance.props.removeDocument) LinkFollowBox.Instance.props.removeDocument(LinkFollowBox.Instance.props.Document);
+        }
+        else {
+            let overlayView = document.getElementById("overlayView");
+            let overlayWidth = 0;
+            if (overlayView) overlayWidth = overlayView.offsetWidth;
+            let x: number = overlayWidth - 500;
+            let doc = Docs.Create.LinkFollowBoxDocument({ x: x, y: 20, width: 500, height: 370, title: "Link Follower" });
+            Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", doc);
+        }
+    }
+
     @observable private _colorPickerDisplay = false;
     /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
     nodesMenu() {
-
         let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg";
 
         let addColNode = action(() => Docs.Create.FreeformDocument([], { width: this.pwidth * .7, height: this.pheight, title: "a freeform collection" }));
-        let addLinkFollowBox = action(() => Docs.Create.LinkFollowBoxDocument({ width: 500, height: 370, title: "Link Follower" }));
         let addPresNode = action(() => Doc.UserDoc().curPresentation = Docs.Create.PresDocument(new List<Doc>(), { width: 200, height: 500, title: "a presentation trail" }));
         let addWebNode = action(() => Docs.Create.WebDocument("https://en.wikipedia.org/wiki/Hedgehog", { width: 300, height: 300, title: "New Webpage" }));
         let addDragboxNode = action(() => Docs.Create.DragboxDocument({ width: 40, height: 40, title: "drag collection" }));
@@ -459,7 +473,7 @@ export class MainView extends React.Component {
             [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
             [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
             [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
-            [React.createRef<HTMLDivElement>(), "link", "Open Link Follow Box", addLinkFollowBox],
+            // [React.createRef<HTMLDivElement>(), "link", "Open Link Follow Box", addLinkFollowBox],
             [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
             //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
         ];
@@ -481,6 +495,7 @@ export class MainView extends React.Component {
                                 <FontAwesomeIcon icon={btn[1]} size="sm" />
                             </button>
                         </div></li>)}
+                    <li key="linkFollow"><button className="add-button round-button" title="Open Link Follower" onClick={this.toggleLinkFollowBox}><FontAwesomeIcon icon="link" size="sm" /></button></li>
                     <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
                         <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
                             <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
@@ -503,12 +518,6 @@ export class MainView extends React.Component {
         this._colorPickerDisplay = close ? false : !this._colorPickerDisplay;
     }
 
-    @action
-    toggleLinkFollowBox = () => {
-        console.log("toggling link editor")
-        this._linkFollowBox = !this._linkFollowBox;
-    }
-
     /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
     @computed
     get miscButtons() {
@@ -517,7 +526,6 @@ export class MainView extends React.Component {
         return [
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
             <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                <button onClick={this.toggleLinkFollowBox}>Open Link Editor</button>
                 <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
         ];
 
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index a60dc591c..8d80ad1f1 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -140,7 +140,7 @@ export class OverlayView extends React.Component {
 
     render() {
         return (
-            <div className="overlayView">
+            <div className="overlayView" id="overlayView">
                 {this._elements}
                 {CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => (
                     <CollectionFreeFormDocumentView key={d[Id]}
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
index 7fad9ae93..a34285727 100644
--- a/src/client/views/linking/LinkFollowBox.scss
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -8,6 +8,7 @@
     box-shadow: $intermediate-color 0.2vw 0.2vw 0.4vw;
     border: solid #BBBBBBBB 5px;
     pointer-events: all;
+    // transform-origin: top right;
     // overflow: hidden;
 
     .linkFollowBox-header {
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 213022bc8..cf44ae649 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -16,6 +16,7 @@ import { Id } from "../../../new_fields/FieldSymbols";
 import { listSpec } from "../../../new_fields/Schema";
 import { DocServer } from "../../DocServer";
 import { RefField } from "../../../new_fields/RefField";
+import { Docs } from "../../documents/Documents";
 
 enum FollowModes {
     OPENTAB = "Open in Tab",
@@ -59,6 +60,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         this.collectionTypes = ["Invalid", "Freeform", "Schema", "Docking", "Tree", "Stacking", "Masonry"];
     }
 
+    @computed
+    get getDoc() {
+        return this.props.Document;
+    }
+
     componentDidMount = () => {
         this.resetVars();
 
@@ -322,6 +328,10 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     currentLinkBehavior = () => {
         // this.resetPan();
         if (LinkFollowBox.destinationDoc) {
+            if (this.selectedContextString === "") {
+                this.selectedContextString = "self";
+                this.selectedContext = LinkFollowBox.destinationDoc;
+            }
             if (this.selectedOption === "") this.selectedOption = FollowOptions.NOZOOM;
             let shouldZoom: boolean = this.selectedOption === FollowOptions.NOZOOM ? false : true;
             let notOpenInContext: boolean = this.selectedContextString === "self" || this.selectedContextString === LinkFollowBox.destinationDoc[Id];
@@ -476,7 +486,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                         type="radio" disabled={LinkFollowBox.linkDoc ? false : true}
                         name="context"
                         value={LinkFollowBox.destinationDoc ? StrCast(LinkFollowBox.destinationDoc[Id]) : "self"}
-                        checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) : this.selectedContextString === "self"}
+                        checked={LinkFollowBox.destinationDoc ? this.selectedContextString === StrCast(LinkFollowBox.destinationDoc[Id]) || this.selectedContextString === "self" : true}
                         onChange={this.handleContextChange} />
                         Open Self
                 </label><br />
-- 
cgit v1.2.3-70-g09d2


From 9c116867bb73b7d84654d9e56688ebd8a982462e Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Tue, 27 Aug 2019 17:45:20 -0400
Subject: about to make big changes

---
 src/client/views/MainView.tsx               |  19 ++-
 src/client/views/linking/LinkFollowBox.scss |  23 +++
 src/client/views/linking/LinkFollowBox.tsx  |  28 +++-
 src/client/views/linking/LinkMenuItem.tsx   | 214 ++++------------------------
 4 files changed, 77 insertions(+), 207 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b49274e7e..d580925d6 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -57,8 +57,6 @@ export class MainView extends React.Component {
 
     public overlayTimeout: NodeJS.Timeout | undefined;
 
-    @observable private _linkFollowBox = false;
-
     public initiateDictationFade = () => {
         let duration = DictationManager.Commands.dictationFadeDuration;
         this.overlayTimeout = setTimeout(() => {
@@ -437,17 +435,16 @@ export class MainView extends React.Component {
         }
     }
 
-    toggleLinkFollowBox = () => {
+    toggleLinkFollowBox = (shouldClose: boolean) => {
         if (LinkFollowBox.Instance) {
-            console.log("is instance!")
-            // if (LinkFollowBox.Instance.props && LinkFollowBox.Instance.props.removeDocument) LinkFollowBox.Instance.props.removeDocument(LinkFollowBox.Instance.props.Document);
+            let dvs = DocumentManager.Instance.getDocumentViews(LinkFollowBox.Instance.props.Document);
+            // if it already exisits, close it
+            if (dvs.length > 0 && shouldClose) LinkFollowBox.Instance.close();
+            // open it if not
+            else Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", LinkFollowBox.Instance.props.Document);
         }
         else {
-            let overlayView = document.getElementById("overlayView");
-            let overlayWidth = 0;
-            if (overlayView) overlayWidth = overlayView.offsetWidth;
-            let x: number = overlayWidth - 500;
-            let doc = Docs.Create.LinkFollowBoxDocument({ x: x, y: 20, width: 500, height: 370, title: "Link Follower" });
+            let doc = Docs.Create.LinkFollowBoxDocument({ x: this.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
             Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", doc);
         }
     }
@@ -495,7 +492,7 @@ export class MainView extends React.Component {
                                 <FontAwesomeIcon icon={btn[1]} size="sm" />
                             </button>
                         </div></li>)}
-                    <li key="linkFollow"><button className="add-button round-button" title="Open Link Follower" onClick={this.toggleLinkFollowBox}><FontAwesomeIcon icon="link" size="sm" /></button></li>
+                    <li key="linkFollow"><button className="add-button round-button" title="Open Link Follower" onClick={() => this.toggleLinkFollowBox(true)}><FontAwesomeIcon icon="link" size="sm" /></button></li>
                     <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
                         <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
                             <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
diff --git a/src/client/views/linking/LinkFollowBox.scss b/src/client/views/linking/LinkFollowBox.scss
index a34285727..e425bde69 100644
--- a/src/client/views/linking/LinkFollowBox.scss
+++ b/src/client/views/linking/LinkFollowBox.scss
@@ -18,12 +18,35 @@
         // line-height: 30px;
         letter-spacing: 2px;
         font-size: 16px;
+        width: 100%;
     }
 
     .direction-indicator {
         font-size: 12px;
     }
 
+    .closeDocument {
+        position: relative;
+        // z-index: inherit;
+        max-width: 30px;
+        top: -20px;
+        // left: 230px;
+        left: 460px;
+        color: $darker-alt-accent // right: 5px;
+    }
+
+    .closeDocument:hover {
+        color: $main-accent;
+    }
+
+    .topHeader {
+        // display: grid;
+        // grid-template-columns: 90% 10%;
+        width: 100%;
+        // flex-direction: row;
+        height: 25px;
+    }
+
     .linkFollowBox-footer {
         height: 50px;
         text-align: center;
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index cf44ae649..588f48017 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -2,7 +2,7 @@ import { observable, computed, action, trace, ObservableMap, runInAction, reacti
 import React = require("react");
 import { observer } from "mobx-react";
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
-import { Doc } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync } from "../../../new_fields/Doc";
 import { undoBatch } from "../../util/UndoManager";
 import { NumCast, FieldValue, Cast, StrCast } from "../../../new_fields/Types";
 import { CollectionViewType } from "../collections/CollectionBaseView";
@@ -17,6 +17,9 @@ import { listSpec } from "../../../new_fields/Schema";
 import { DocServer } from "../../DocServer";
 import { RefField } from "../../../new_fields/RefField";
 import { Docs } from "../../documents/Documents";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faTimes } from '@fortawesome/free-solid-svg-icons';
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 
 enum FollowModes {
     OPENTAB = "Open in Tab",
@@ -48,7 +51,6 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     @observable canPan: boolean = false;
     @observable shouldUseOnlyParentContext = false;
     _contextDisposer?: IReactionDisposer;
-    collectionTypes: string[];
 
     @observable private _docs: { col: Doc, target: Doc }[] = [];
     @observable private _otherDocs: { col: Doc, target: Doc }[] = [];
@@ -56,8 +58,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     constructor(props: FieldViewProps) {
         super(props);
         LinkFollowBox.Instance = this;
-
-        this.collectionTypes = ["Invalid", "Freeform", "Schema", "Docking", "Tree", "Stacking", "Masonry"];
+        this.props.Document.isBackground = true;
     }
 
     @computed
@@ -322,7 +323,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
     }
 
     //set this to be the default link behavior, can be any of the above
-    private defaultLinkBehavior: (options?: any) => void = this.openLinkInPlace;
+    public defaultLinkBehavior: (options?: any) => void = this.openLinkRight;
 
     @action
     currentLinkBehavior = () => {
@@ -556,12 +557,20 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         return null;
     }
 
+    async close() {
+        let res = await DocListCastAsync((CurrentUserUtils.UserDocument.overlays as Doc).data);
+        if (res) res.splice(res.indexOf(LinkFollowBox.Instance.props.Document), 1);
+    }
+
     render() {
         return (
             <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
                 <div className="linkFollowBox-header">
-                    {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
-                    <div className="linkFollowBox-header direction-indicator">{LinkFollowBox.linkDoc ?
+                    <div className="topHeader">
+                        {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
+                        <div onClick={this.close} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div>
+                    </div>
+                    <div className=" direction-indicator">{LinkFollowBox.linkDoc ?
                         LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
                             : "" : ""}</div>
                 </div>
@@ -586,6 +595,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
                     </div>
                 </div>
                 <div className="linkFollowBox-footer">
+                    <button
+                        onClick={this.resetVars}>
+                        Clear Link
+                    </button>
+                    <div style={{ width: 20 }}></div>
                     <button
                         onClick={this.currentLinkBehavior}
                         disabled={(LinkFollowBox.linkDoc) ? false : true}>
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index d2dc3f7ff..0dda948c4 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -17,6 +17,9 @@ import { CollectionViewType } from '../collections/CollectionBaseView';
 import { DocumentView } from '../nodes/DocumentView';
 import { SearchUtil } from '../../util/SearchUtil';
 import { LinkFollowBox } from './LinkFollowBox';
+import { ContextMenu } from '../ContextMenu';
+import { MainView } from '../MainView';
+import { Docs } from '../../documents/Documents';
 library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
 
 
@@ -35,148 +38,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     @observable private _showMore: boolean = false;
     @action toggleShowMore() { this._showMore = !this._showMore; }
 
-
-    unhighlight = () => {
-        Doc.UnhighlightAll();
-        document.removeEventListener("pointerdown", this.unhighlight);
-    }
-
-    @action
-    highlightDoc = () => {
-        document.removeEventListener("pointerdown", this.unhighlight);
-        Doc.HighlightDoc(this.props.destinationDoc);
-        window.setTimeout(() => {
-            document.addEventListener("pointerdown", this.unhighlight);
-        }, 10000);
-    }
-
-    // NOT TESTED
-    // col = collection the doc is in
-    // target = the document to center on
-    @undoBatch
-    openLinkColRight = (col: Doc) => {
-        col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
-        if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-            const newPanX = NumCast(this.props.destinationDoc.x) + NumCast(this.props.destinationDoc.width) / NumCast(this.props.destinationDoc.zoomBasis, 1) / 2;
-            const newPanY = NumCast(this.props.destinationDoc.y) + NumCast(this.props.destinationDoc.height) / NumCast(this.props.destinationDoc.zoomBasis, 1) / 2;
-            col.panX = newPanX;
-            col.panY = newPanY;
-        }
-        CollectionDockingView.Instance.AddRightSplit(col, undefined);
-    }
-
-    // DONE
-    @undoBatch
-    openFullScreen = () => {
-        let view: DocumentView | null = DocumentManager.Instance.getDocumentView(this.props.destinationDoc);
-        view && CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(view);
-    }
-
-    // DONE
-    // this opens the linked doc in a right split, NOT in its collection
-    @undoBatch
-    openLinkRight = () => {
-        this.highlightDoc();
-        let alias = Doc.MakeAlias(this.props.destinationDoc);
-        CollectionDockingView.Instance.AddRightSplit(alias, undefined);
-        SelectionManager.DeselectAll();
-    }
-
-    // DONE
-    // this is the standard "follow link" (jump to document)
-    // taken from follow link
-    @undoBatch
-    jumpToLink = async (shouldZoom: boolean) => {
-        //there is an issue right now so this will be false automatically
-        shouldZoom = false;
-        this.highlightDoc();
-        let jumpToDoc = this.props.destinationDoc;
-        let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
-        if (pdfDoc) {
-            jumpToDoc = pdfDoc;
-        }
-        let proto = Doc.GetProto(this.props.linkDoc);
-        let targetContext = await Cast(proto.targetContext, Doc);
-        let sourceContext = await Cast(proto.sourceContext, Doc);
-        let self = this;
-
-        let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
-
-        if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, async document => dockingFunc(document), undefined, targetContext);
-        }
-        else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, document => dockingFunc(sourceContext!));
-        }
-        else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page)));
-
-        }
-        else {
-            DocumentManager.Instance.jumpToDocument(jumpToDoc, shouldZoom, false, dockingFunc);
-        }
-    }
-
-    // DONE
-    // opens link in new tab (not in a collection)
-    // this opens it full screen, do we need a separate full screen option?
-    @undoBatch
-    openLinkTab = () => {
-        this.highlightDoc();
-        let fullScreenAlias = Doc.MakeAlias(this.props.destinationDoc);
-        this.props.addDocTab(fullScreenAlias, undefined, "inTab");
-        SelectionManager.DeselectAll();
-    }
-
-    // NOT TESTED
-    // opens link in new tab in collection
-    // col = collection the doc is in
-    // target = the document to center on
-    @undoBatch
-    openLinkColTab = (col: Doc) => {
-        this.highlightDoc();
-        col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col;
-        if (NumCast(col.viewType, CollectionViewType.Invalid) === CollectionViewType.Freeform) {
-            const newPanX = NumCast(this.props.destinationDoc.x) + NumCast(this.props.destinationDoc.width) / NumCast(this.props.destinationDoc.zoomBasis, 1) / 2;
-            const newPanY = NumCast(this.props.destinationDoc.y) + NumCast(this.props.destinationDoc.height) / NumCast(this.props.destinationDoc.zoomBasis, 1) / 2;
-            col.panX = newPanX;
-            col.panY = newPanY;
-        }
-        // CollectionDockingView.Instance.AddRightSplit(col, undefined);
-        this.props.addDocTab(col, undefined, "inTab");
-        SelectionManager.DeselectAll();
-    }
-
-    // DONE
-    // this will open a link next to the source doc
-    @undoBatch
-    openLinkInPlace = () => {
-        this.highlightDoc();
-
-        let alias = Doc.MakeAlias(this.props.destinationDoc);
-        let y = NumCast(this.props.sourceDoc.y);
-        let x = NumCast(this.props.sourceDoc.x);
-
-        let width = NumCast(this.props.sourceDoc.width);
-        let height = NumCast(this.props.sourceDoc.height);
-
-        alias.x = x + width + 30;
-        alias.y = y;
-        alias.width = width;
-        alias.height = height;
-
-        SelectionManager.SelectedDocuments().map(dv => {
-            if (dv.props.Document === this.props.sourceDoc) {
-                dv.props.addDocument && dv.props.addDocument(alias, false);
-            }
-        });
-
-        this.jumpToLink(false);
-    }
-
-    //set this to be the default link behavior, can be any of the above
-    private defaultLinkBehavior: any = this.openLinkInPlace;
-
     onEdit = (e: React.PointerEvent): void => {
         e.stopPropagation();
         this.props.showEditor(this.props.linkDoc);
@@ -226,6 +87,26 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         e.stopPropagation();
     }
 
+    onContextMenu = (e: React.MouseEvent) => {
+        e.preventDefault();
+        ContextMenu.Instance.addItem({ description: "Open in Link Follower", event: () => console.log("opening 1!"), icon: "link" });
+        ContextMenu.Instance.addItem({ description: "Follow Default Link", event: () => this.followDefault(), icon: "arrow-right" });
+        ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
+    }
+
+    followDefault = () => {
+        if (!LinkFollowBox.Instance) {
+            Docs.Create.LinkFollowBoxDocument({ x: MainView.Instance.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
+        }
+        LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+        LinkFollowBox.Instance.defaultLinkBehavior();
+    }
+
+    openLinkFollower = () => {
+        LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+        MainView.Instance.toggleLinkFollowBox(false);
+    }
+
     render() {
 
         let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
@@ -240,12 +121,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                             {canExpand ? <div title="Show more" className="button" onPointerDown={() => this.toggleShowMore()}>
                                 <FontAwesomeIcon className="fa-icon" icon={this._showMore ? "chevron-up" : "chevron-down"} size="sm" /></div> : <></>}
                             <div title="Edit link" className="button" onPointerDown={this.onEdit}><FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
-                            {/* Original */}
-                            {/* <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */}
-                            {/* New */}
-                            <div title="Follow link" className="button" onClick={() => LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc)}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
-                            {/* <div title="Follow link" className="button" onClick={this.defaultLinkBehavior}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div> */}
-
+                            <div title="Follow link" className="button" onClick={this.followDefault} onContextMenu={this.onContextMenu}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                         </div>
                     </div>
                     {this._showMore ? this.renderMetadata() : <></>}
@@ -254,44 +130,4 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
             </div >
         );
     }
-}
-
-    // @undoBatch
-    // onFollowLink = async (e: React.PointerEvent): Promise<void> => {
-    //     e.stopPropagation();
-    //     e.persist();
-    //     let jumpToDoc = this.props.destinationDoc;
-    //     let pdfDoc = FieldValue(Cast(this.props.destinationDoc, Doc));
-    //     if (pdfDoc) {
-    //         jumpToDoc = pdfDoc;
-    //     }
-    //     let proto = Doc.GetProto(this.props.linkDoc);
-    //     let targetContext = await Cast(proto.targetContext, Doc);
-    //     let sourceContext = await Cast(proto.sourceContext, Doc);
-    //     let self = this;
-
-
-    //     let dockingFunc = (document: Doc) => { this.props.addDocTab(document, undefined, "inTab"); SelectionManager.DeselectAll(); };
-    //     if (e.ctrlKey) {
-    //         dockingFunc = (document: Doc) => CollectionDockingView.Instance.AddRightSplit(document, undefined);
-    //     }
-
-    //     if (this.props.destinationDoc === self.props.linkDoc.anchor2 && targetContext) {
-    //         DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, async document => dockingFunc(document), undefined, targetContext!);
-    //         console.log("1")
-    //     }
-    //     else if (this.props.destinationDoc === self.props.linkDoc.anchor1 && sourceContext) {
-    //         DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, document => dockingFunc(sourceContext!));
-    //         console.log("2")
-    //     }
-    //     else if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
-    //         DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, undefined, undefined, NumCast((this.props.destinationDoc === self.props.linkDoc.anchor2 ? self.props.linkDoc.anchor2Page : self.props.linkDoc.anchor1Page)));
-    //         console.log("3")
-
-    //     }
-    //     else {
-    //         DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey, false, dockingFunc);
-    //         console.log("4")
-
-    //     }
-    // }
\ No newline at end of file
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From c1adf10630a4fd163c1c9ef11abf9ad090bf29a3 Mon Sep 17 00:00:00 2001
From: monikahedman <monika_hedman@brown.edu>
Date: Tue, 27 Aug 2019 18:29:02 -0400
Subject: haudfhajfjadjad['jad'

---
 src/client/views/MainView.tsx             | 2 +-
 src/client/views/linking/LinkMenuItem.tsx | 8 +++++---
 2 files changed, 6 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d580925d6..20fc1dce8 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -155,6 +155,7 @@ export class MainView extends React.Component {
     constructor(props: Readonly<{}>) {
         super(props);
         MainView.Instance = this;
+
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
         if (window.location.pathname !== RouteStore.home) {
@@ -470,7 +471,6 @@ export class MainView extends React.Component {
             [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
             [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
             [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
-            // [React.createRef<HTMLDivElement>(), "link", "Open Link Follow Box", addLinkFollowBox],
             [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
             //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
         ];
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 0dda948c4..1210d7198 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -8,7 +8,7 @@ import './LinkMenu.scss';
 import React = require("react");
 import { Doc, DocListCastAsync, WidthSym } from '../../../new_fields/Doc';
 import { StrCast, Cast, FieldValue, NumCast } from '../../../new_fields/Types';
-import { observable, action } from 'mobx';
+import { observable, action, computed } from 'mobx';
 import { LinkManager } from '../../util/LinkManager';
 import { DragLinkAsDocument } from '../../util/DragManager';
 import { CollectionDockingView } from '../collections/CollectionDockingView';
@@ -20,6 +20,7 @@ import { LinkFollowBox } from './LinkFollowBox';
 import { ContextMenu } from '../ContextMenu';
 import { MainView } from '../MainView';
 import { Docs } from '../../documents/Documents';
+import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
 library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
 
 
@@ -95,8 +96,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     }
 
     followDefault = () => {
-        if (!LinkFollowBox.Instance) {
-            Docs.Create.LinkFollowBoxDocument({ x: MainView.Instance.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
+        if (LinkFollowBox.Instance === undefined) {
+            let doc = Docs.Create.LinkFollowBoxDocument({ x: MainView.Instance.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
+            Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", doc);
         }
         LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
         LinkFollowBox.Instance.defaultLinkBehavior();
-- 
cgit v1.2.3-70-g09d2


From f6c15087d30f27c15b3b6304af58c93c65536131 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 29 Aug 2019 14:50:22 -0400
Subject: changed linkFollowBox creation approach.  fixed target contexts to
 add link default.

---
 src/client/views/MainView.tsx                      |   8 +-
 src/client/views/OverlayView.tsx                   | 108 +++++++++++----------
 src/client/views/linking/LinkFollowBox.tsx         |  20 ++--
 src/client/views/linking/LinkMenuItem.tsx          |  18 ++--
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   1 -
 src/client/views/nodes/FormattedTextBoxComment.tsx |   8 +-
 .../authentication/models/current_user_utils.ts    |   3 +
 7 files changed, 78 insertions(+), 88 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 1fc8d1397..b64986084 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -438,13 +438,7 @@ export class MainView extends React.Component {
         if (LinkFollowBox.Instance) {
             let dvs = DocumentManager.Instance.getDocumentViews(LinkFollowBox.Instance.props.Document);
             // if it already exisits, close it
-            if (dvs.length > 0 && shouldClose) LinkFollowBox.Instance.close();
-            // open it if not
-            else Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", LinkFollowBox.Instance.props.Document);
-        }
-        else {
-            let doc = Docs.Create.LinkFollowBoxDocument({ x: this.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
-            Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", doc);
+            LinkFollowBox.Instance.props.Document.isMinimized = (dvs.length > 0 && shouldClose);
         }
     }
 
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index a1afe651c..54ab8696e 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,6 +1,6 @@
 import * as React from "react";
 import { observer } from "mobx-react";
-import { observable, action } from "mobx";
+import { observable, action, trace, computed } from "mobx";
 import { Utils, emptyFunction, returnOne, returnTrue, returnEmptyString, returnZero, returnFalse } from "../../Utils";
 
 import './OverlayView.scss';
@@ -140,61 +140,65 @@ export class OverlayView extends React.Component {
         return remove;
     }
 
+    @computed get overlayDocs() {
+        return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
+            let offsetx = 0, offsety = 0;
+            let onPointerMove = action((e: PointerEvent) => {
+                if (e.buttons === 1) {
+                    d.x = e.clientX + offsetx;
+                    d.y = e.clientY + offsety;
+                    e.stopPropagation();
+                    e.preventDefault();
+                }
+            });
+            let onPointerUp = action((e: PointerEvent) => {
+                document.removeEventListener("pointermove", onPointerMove);
+                document.removeEventListener("pointerup", onPointerUp);
+                e.stopPropagation();
+                e.preventDefault();
+            });
+
+            let onPointerDown = (e: React.PointerEvent) => {
+                offsetx = NumCast(d.x) - e.clientX;
+                offsety = NumCast(d.y) - e.clientY;
+                e.stopPropagation();
+                e.preventDefault();
+                document.addEventListener("pointermove", onPointerMove);
+                document.addEventListener("pointerup", onPointerUp);
+            };
+            return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)`, display: d.isMinimized ? "none" : "" }}>
+                <DocumentContentsView
+                    Document={d}
+                    ChromeHeight={returnZero}
+                    isSelected={returnFalse}
+                    select={emptyFunction}
+                    layoutKey={"layout"}
+                    bringToFront={emptyFunction}
+                    addDocument={undefined}
+                    removeDocument={undefined}
+                    ContentScaling={returnOne}
+                    PanelWidth={returnOne}
+                    PanelHeight={returnOne}
+                    ScreenToLocalTransform={Transform.Identity}
+                    renderDepth={1}
+                    parentActive={returnTrue}
+                    whenActiveChanged={emptyFunction}
+                    focus={emptyFunction}
+                    backgroundColor={returnEmptyString}
+                    addDocTab={emptyFunction}
+                    pinToPres={emptyFunction}
+                    ContainingCollectionView={undefined}
+                    zoomToScale={emptyFunction}
+                    getScale={returnOne} />
+            </div>;
+        })
+    }
+
     render() {
         return (
             <div className="overlayView" id="overlayView">
                 {this._elements}
-                {CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
-                    let offsetx = 0, offsety = 0;
-                    let onPointerMove = action((e: PointerEvent) => {
-                        if (e.buttons === 1) {
-                            d.x = e.clientX + offsetx;
-                            d.y = e.clientY + offsety;
-                            e.stopPropagation();
-                            e.preventDefault();
-                        }
-                    });
-                    let onPointerUp = action((e: PointerEvent) => {
-                        document.removeEventListener("pointermove", onPointerMove);
-                        document.removeEventListener("pointerup", onPointerUp);
-                        e.stopPropagation();
-                        e.preventDefault();
-                    });
-
-                    let onPointerDown = (e: React.PointerEvent) => {
-                        offsetx = NumCast(d.x) - e.clientX;
-                        offsety = NumCast(d.y) - e.clientY;
-                        e.stopPropagation();
-                        e.preventDefault();
-                        document.addEventListener("pointermove", onPointerMove);
-                        document.addEventListener("pointerup", onPointerUp);
-                    }
-                    return <div className="overlayView-doc" key={d[Id]} onPointerDown={onPointerDown} style={{ transform: `translate(${d.x}px, ${d.y}px)` }}>
-                        <DocumentContentsView
-                            Document={d}
-                            ChromeHeight={returnZero}
-                            isSelected={returnFalse}
-                            select={emptyFunction}
-                            layoutKey={"layout"}
-                            bringToFront={emptyFunction}
-                            addDocument={undefined}
-                            removeDocument={undefined}
-                            ContentScaling={returnOne}
-                            PanelWidth={returnOne}
-                            PanelHeight={returnOne}
-                            ScreenToLocalTransform={Transform.Identity}
-                            renderDepth={1}
-                            parentActive={returnTrue}
-                            whenActiveChanged={emptyFunction}
-                            focus={emptyFunction}
-                            backgroundColor={returnEmptyString}
-                            addDocTab={emptyFunction}
-                            pinToPres={emptyFunction}
-                            ContainingCollectionView={undefined}
-                            zoomToScale={emptyFunction}
-                            getScale={returnOne} />
-                    </div>
-                })}
+                {this.overlayDocs}
             </div>
         );
     }
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index 07f07f72a..74663f9af 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -1,4 +1,4 @@
-import { observable, computed, action, trace, ObservableMap, runInAction, reaction, IReactionDisposer } from "mobx";
+import { observable, computed, action, runInAction, reaction, IReactionDisposer } from "mobx";
 import React = require("react");
 import { observer } from "mobx-react";
 import { FieldViewProps, FieldView } from "../nodes/FieldView";
@@ -15,8 +15,6 @@ import { SearchUtil } from "../../util/SearchUtil";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { listSpec } from "../../../new_fields/Schema";
 import { DocServer } from "../../DocServer";
-import { RefField } from "../../../new_fields/RefField";
-import { Docs } from "../../documents/Documents";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faTimes } from '@fortawesome/free-solid-svg-icons';
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
@@ -126,9 +124,11 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             const allDocs = await Promise.all(aliases.map(doc => SearchUtil.Search("", true, { fq: `data_l:"${doc[Id]}"` }).then(result => result.docs)));
             allDocs.forEach((docs, index) => docs.forEach(doc => map.set(doc, aliases[index])));
             docs.forEach(doc => map.delete(doc));
-            runInAction(() => {
+            runInAction(async () => {
                 this._docs = docs.filter(doc => !Doc.AreProtosEqual(doc, CollectionDockingView.Instance.props.Document)).map(doc => ({ col: doc, target: dest }));
                 this._otherDocs = Array.from(map.entries()).filter(entry => !Doc.AreProtosEqual(entry[0], CollectionDockingView.Instance.props.Document)).map(([col, target]) => ({ col, target }));
+                let tcontext = LinkFollowBox.linkDoc && (await Cast(LinkFollowBox.linkDoc.targetContext, Doc)) as Doc;
+                runInAction(() => tcontext && this._docs.splice(0, 0, { col: tcontext, target: dest }));
             });
         }
     }
@@ -268,7 +268,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             let fullScreenAlias = Doc.MakeAlias(LinkFollowBox.destinationDoc);
             // THIS IS EMPTY FUNCTION
             this.props.addDocTab(fullScreenAlias, undefined, "inTab");
-            console.log(this.props.addDocTab)
+            console.log(this.props.addDocTab);
 
             this.highlightDoc();
             SelectionManager.DeselectAll();
@@ -348,7 +348,7 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
             }
             else if (this.selectedMode === FollowModes.OPENTAB) {
                 if (notOpenInContext) this.openLinkTab();
-                else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom })
+                else this.selectedContext && this.openLinkColTab({ context: this.selectedContext, shouldZoom: shouldZoom });
             }
             else if (this.selectedMode === FollowModes.PAN) {
                 this.jumpToLink({ shouldZoom: shouldZoom });
@@ -555,19 +555,13 @@ export class LinkFollowBox extends React.Component<FieldViewProps> {
         return null;
     }
 
-    async close() {
-        let res = await DocListCastAsync((CurrentUserUtils.UserDocument.overlays as Doc).data);
-        if (res) res.splice(res.indexOf(LinkFollowBox.Instance!.props.Document), 1);
-        LinkFollowBox.Instance = undefined;
-    }
-
     render() {
         return (
             <div className="linkFollowBox-main" style={{ height: NumCast(this.props.Document.height), width: NumCast(this.props.Document.width) }}>
                 <div className="linkFollowBox-header">
                     <div className="topHeader">
                         {LinkFollowBox.linkDoc ? "Link Title: " + StrCast(LinkFollowBox.linkDoc.title) : "No Link Selected"}
-                        <div onClick={this.close} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div>
+                        <div onClick={() => this.props.Document.isMinimized = true} className="closeDocument"><FontAwesomeIcon icon={faTimes} size="lg" /></div>
                     </div>
                     <div className=" direction-indicator">{LinkFollowBox.linkDoc ?
                         LinkFollowBox.sourceDoc && LinkFollowBox.destinationDoc ? "Source: " + StrCast(LinkFollowBox.sourceDoc.title) + ", Destination: " + StrCast(LinkFollowBox.destinationDoc.title)
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 6c10d4fb6..780f161d3 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -97,24 +97,20 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
     @action.bound
     async followDefault() {
-        if (LinkFollowBox.Instance === undefined) {
-            let doc = await Docs.Create.LinkFollowBoxDocument({ x: MainView.Instance.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
-            await Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", doc);
-        } else {
-            LinkFollowBox.Instance!.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
-            LinkFollowBox.Instance!.defaultLinkBehavior();
+        if (LinkFollowBox.Instance !== undefined) {
+            LinkFollowBox.Instance.props.Document.isMinimized = false;
+            LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
+            LinkFollowBox.Instance.defaultLinkBehavior();
         }
     }
 
     @action.bound
     async openLinkFollower() {
-        if (LinkFollowBox.Instance === undefined) {
-            let doc = await Docs.Create.LinkFollowBoxDocument({ x: MainView.Instance.flyoutWidth, y: 20, width: 500, height: 370, title: "Link Follower" });
-            await Doc.AddDocToList(Cast(CurrentUserUtils.UserDocument.overlays, Doc) as Doc, "data", doc);
-        } else {
+        if (LinkFollowBox.Instance !== undefined) {
+            LinkFollowBox.Instance.props.Document.isMinimized = false;
             MainView.Instance.toggleLinkFollowBox(false);
+            LinkFollowBox.Instance.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
         }
-        LinkFollowBox.Instance!.setLinkDocs(this.props.linkDoc, this.props.sourceDoc, this.props.destinationDoc);
     }
 
     render() {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 7631ecc6c..c9c394960 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -8,7 +8,6 @@ import { DocumentView, DocumentViewProps, positionSchema } from "./DocumentView"
 import "./DocumentView.scss";
 import React = require("react");
 import { Doc } from "../../../new_fields/Doc";
-import { returnEmptyString } from "../../../Utils";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
     x?: number;
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
index 9123f8aed..0287d93a9 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/FormattedTextBoxComment.tsx
@@ -1,12 +1,12 @@
-import { Plugin, EditorState } from "prosemirror-state"
-import './FormattedTextBoxComment.scss'
+import { Plugin, EditorState } from "prosemirror-state";
+import './FormattedTextBoxComment.scss';
 import { ResolvedPos, Mark } from "prosemirror-model";
 import { EditorView } from "prosemirror-view";
 import { Doc } from "../../../new_fields/Doc";
 
 export let selectionSizePlugin = new Plugin({
     view(editorView) { return new SelectionSizeTooltip(editorView); }
-})
+});
 export function findOtherUserMark(marks: Mark[]): Mark | undefined {
     return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
 }
@@ -106,5 +106,5 @@ export class SelectionSizeTooltip {
         }
     }
 
-    destroy() { SelectionSizeTooltip.tooltip.style.display = "none" }
+    destroy() { SelectionSizeTooltip.tooltip.style.display = "none"; }
 }
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 83e45d3ce..9866e22eb 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -79,6 +79,9 @@ export class CurrentUserUtils {
             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;
-- 
cgit v1.2.3-70-g09d2


From 77e08a4362ba8fab4cab361fcb472702c97edf15 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 1 Sep 2019 13:05:54 -0400
Subject: initial commit

---
 package.json                                       |   3 +
 src/client/views/MainView.tsx                      |   4 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |  13 +-
 src/server/apis/google/GooglePhotosUtils.ts        |  12 +
 src/server/authentication/config/passport.ts       |  18 +-
 src/server/credentials/auth.json                   |  12 +
 .../credentials/google_docs_credentials.json       |  12 +-
 src/server/credentials/google_docs_token.json      |   2 +-
 .../credentials/google_photos_credentials.ts       |  35 ++
 src/server/index.ts                                |  27 +
 src/typings/index.d.ts                             | 632 +++++++++++----------
 11 files changed, 447 insertions(+), 323 deletions(-)
 create mode 100644 src/server/apis/google/GooglePhotosUtils.ts
 create mode 100644 src/server/credentials/auth.json
 create mode 100644 src/server/credentials/google_photos_credentials.ts

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

diff --git a/package.json b/package.json
index f98224469..f0f2b467e 100644
--- a/package.json
+++ b/package.json
@@ -81,6 +81,7 @@
         "@types/node": "^10.12.30",
         "@types/nodemailer": "^4.6.6",
         "@types/passport": "^1.0.0",
+        "@types/passport-google-oauth20": "^2.0.2",
         "@types/passport-local": "^1.0.33",
         "@types/pdfjs-dist": "^2.0.0",
         "@types/prosemirror-commands": "^1.0.1",
@@ -141,6 +142,7 @@
         "golden-layout": "^1.5.9",
         "google-auth-library": "^4.2.4",
         "googleapis": "^40.0.0",
+        "googlephotos": "^0.2.1",
         "howler": "^2.1.2",
         "html-to-image": "^0.1.0",
         "i": "^0.3.6",
@@ -166,6 +168,7 @@
         "npm": "^6.10.3",
         "p-limit": "^2.2.0",
         "passport": "^0.4.0",
+        "passport-google-oauth20": "^2.0.0",
         "passport-local": "^1.0.0",
         "pdfjs-dist": "^2.0.943",
         "probe-image-size": "^4.0.0",
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ab87f0c7b..df0718072 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema';
 import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs } from '../documents/Documents';
 import { ClientUtils } from '../util/ClientUtils';
@@ -128,6 +128,8 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
+        PostToServer('/googleDocs/Photos/Test', {});
+
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
             let recent = CurrentUserUtils.UserDocument.recentlyClosed;
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 8785cd974..2fb44d9a2 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -5,6 +5,7 @@ import { OAuth2Client } from "google-auth-library";
 import { Opt } from "../../../new_fields/Doc";
 import { GlobalOptions } from "googleapis-common";
 import { GaxiosResponse } from "gaxios";
+import Photos = require("googlephotos");
 
 /**
  * Server side authentication for Google Api queries.
@@ -20,16 +21,18 @@ export namespace GoogleApiServerUtils {
         'presentations.readonly',
         'drive',
         'drive.file',
+        'photoslibrary',
+        'photoslibrary.sharing'
     ];
 
     export const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
 
     export enum Service {
         Documents = "Documents",
-        Slides = "Slides"
+        Slides = "Slides",
+        Photos = "Photos"
     }
 
-
     export interface CredentialPaths {
         credentials: string;
         token: string;
@@ -50,7 +53,7 @@ export namespace GoogleApiServerUtils {
                     reject(err);
                     return console.log('Error loading client secret file:', err);
                 }
-                return authorize(parseBuffer(credentials), paths.token).then(auth => {
+                return authorize(parseBuffer(credentials), paths.token).then(async auth => {
                     let routed: Opt<Endpoint>;
                     let parameters: EndpointParameters = { auth, version: "v1" };
                     switch (sector) {
@@ -60,6 +63,10 @@ export namespace GoogleApiServerUtils {
                         case Service.Slides:
                             routed = google.slides(parameters).presentations;
                             break;
+                        case Service.Photos:
+                            const photos = new Photos(auth);
+                            let response = await photos.albums.list();
+                            console.log("WE GOT SOMETHING!", response);
                     }
                     resolve(routed);
                 });
diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts
new file mode 100644
index 000000000..c33ad2dd9
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosUtils.ts
@@ -0,0 +1,12 @@
+import request = require('request-promise');
+const key = require("../../credentials/auth.json");
+
+export const PhotosLibraryQuery = async (authToken: any, parameters: any) => {
+    let options = {
+        headers: { 'Content-Type': 'application/json' },
+        json: parameters,
+        auth: { 'bearer': authToken },
+    };
+    const result = await request.post(config.apiEndpoint + '/v1/mediaItems:search', options);
+    return result;
+};
\ No newline at end of file
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index d42741410..97ded8785 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -1,12 +1,14 @@
 import * as passport from 'passport';
 import * as passportLocal from 'passport-local';
-import * as mongodb from 'mongodb';
-import * as _ from "lodash";
+import _ from "lodash";
 import { default as User } from '../models/user_model';
 import { Request, Response, NextFunction } from "express";
 import { RouteStore } from '../../RouteStore';
+import * as GoogleOAuth from "passport-google-oauth20";
+const config = require("../../credentials/google_photos_credentials");
 
 const LocalStrategy = passportLocal.Strategy;
+const GoogleOAuthStrategy = GoogleOAuth.Strategy;
 
 passport.serializeUser<any, any>((user, done) => {
     done(undefined, user.id);
@@ -32,6 +34,18 @@ passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true
     });
 }));
 
+
+passport.use(new GoogleOAuthStrategy(
+    {
+        clientID: config.oAuthClientID,
+        clientSecret: config.oAuthclientSecret,
+        callbackURL: config.oAuthCallbackUrl,
+        // Set the correct profile URL that does not require any additional APIs
+        userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
+    },
+    (token, refreshToken, profile, done) => done(undefined, { profile, token })
+));
+
 export let isAuthenticated = (req: Request, res: Response, next: NextFunction) => {
     if (req.isAuthenticated()) {
         return next();
diff --git a/src/server/credentials/auth.json b/src/server/credentials/auth.json
new file mode 100644
index 000000000..557eca4b6
--- /dev/null
+++ b/src/server/credentials/auth.json
@@ -0,0 +1,12 @@
+{
+    "type": "service_account",
+    "project_id": "brown-dash",
+    "private_key_id": "ddf0473a9ac56956b5818e04a7ee406a64d5b0a6",
+    "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCueRfxic2oL9nr\nnWSLgl7XR/BKikm4p2sib6szaoTO+q6itcJgt2TDleK/7Y4KW/KhvCfhWVet0Hz0\nIDyg4N/gc2yxuDA6/m8DPWU9kDj8VFR7LVFawOKgo1WbgLcC0Qu8qHzAffrlg8si\nhj3vGuoS/YDn/mz0krwFmCfIx+S0lJ9a7FUjJL5C+CIwAEEYiU7xnTW7pVVNXAm/\n/YKD17ToAjREOtlfVVYO7tZ7V5BiW0I0jJvxw+t1pgrZZe7WPBSBJg9KKGIl+mRi\ndtUMR9Hyt3nMKNZIrSm0OkAz82HxfcapRdSB3wkVjoyW63YaTVHKoBOqRElfMtoM\nqu8wbhhNAgMBAAECggEAA41wJ8kg8J8peQMZ/b7gZvzuPy+h0M/J3j2MrG3rY9qA\nrUv1oqoBSXvhuNDhtEN12oYIqtg6m+L+Sas8CMuOC2rWPafM2u/80IGoGtDhtCjp\nv8inBX8ew4YSiL7IxdbTU2/70Es7DVV5u0t6ndsmr88ibYwwPupGR/fhPCpDyssg\n7lFAEpOwnbKG9a7E7axHpXBRSIE54sh+ESyf6MHH/oyKOLhZ0v4PjRDKaKuMDRst\nMOClgNjD/4bzKpfWljuPYemXz1oIBQitBW5aXnCdsmdrmOLDQpz3qOgIo+RRiyki\nvVU5N54L65sj4WisLt1TT45wbhrkQUz+8GmhV5rHwQKBgQDow32/gSb/M0BKFk5y\n+pSeoLkYp/dwfBFWYT6CNBaKePARFVCdr3db8yOEQD4hrmTOU0EP9c4FSLcaa0Xy\n7n2crhhfZWFpIpRMyXhpeKpqdjQFimfBOK6cIdjnWrtJ6Ik3m4E9p7kKThIsInTc\n/TETwAyzFN+J2ADRdrUdCukOwQKBgQC/4+1Rk8a++Jr9Sznx+JH4vj/J2cGsu7uQ\n0nikcOAFO5HzG7+mt9Wv9/MiPtEYwmc7YziDXldKLpshT2m6wrS1uzzOXAnvXFAh\npiCXQsmVA3gmrVd53k+eZfqrZ1n/rL1kCewRS5LX8xIhM28VIkGqkVy4ZEifMotG\nZKSbH0b4jQKBgQCsJ6rh8Uw+hFGQel8be2pgyM8eBV1lvN213ca11oC1ei1U9Ubi\n2dyWDYa/UiSiFLJKSBlfDJaMIfQLfjwGKY6OS9WK+RjLAeBdysVcfPrOMw7W6j9D\nEgFTSVV8CAdt6qdSkZlNWLfrf0LBkdqNeFbMHMdHzLBo63HverUJ/f/SAQKBgHIk\n2t5T0T14FHnnbaiJ/ArC4J7pcVOWuJQFHs5ydk+mh8LdFrvNTsdF7tLIGwlnWpDx\nDITYcYQnBRBjdLkraONRZXI7PY2sk93wPCK+D7scPTSEmCxeGW5XqyyaZea4klAX\nttzy336lkHs/ZSxlHDqiDU2CGdDY+A//fgroKAdhAoGAA5FXfMzTQLGqxg4J2B1z\nFEXNbrqZZFGgKiveUhhZLm4zPiHXtZXvDtSLwGgcO8oGfTfYueTcHb/Eiar7mKv+\n+SqpAqkINJTthIFVIiD39S9jPFUXzBkf5ZJKPLKQArhzEGxen+SD6ZUO058fA94L\n9FblRGlMtr2o5z0NC7H5zaU=\n-----END PRIVATE KEY-----\n",
+    "client_email": "google-photos-api@brown-dash.iam.gserviceaccount.com",
+    "client_id": "112995422877175743408",
+    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+    "token_uri": "https://oauth2.googleapis.com/token",
+    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+    "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/google-photos-api%40brown-dash.iam.gserviceaccount.com"
+}
\ No newline at end of file
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_docs_credentials.json
index 8d097d363..955c5a3c1 100644
--- a/src/server/credentials/google_docs_credentials.json
+++ b/src/server/credentials/google_docs_credentials.json
@@ -1 +1,11 @@
-{"installed":{"client_id":"343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com","project_id":"quickstart-1565056383187","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"w8KIFSc0MQpmUYHed4qEzn8b","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}
\ No newline at end of file
+{
+    "installed": {
+        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
+        "project_id": "quickstart-1565056383187",
+        "auth_uri": "https://accounts.google.com/o/oauth2/auth",
+        "token_uri": "https://oauth2.googleapis.com/token",
+        "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
+        "client_secret": "w8KIFSc0MQpmUYHed4qEzn8b",
+        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
+    }
+}
\ 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 07c02d56c..cea452f08 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GltjB4-x03xFpd2NY2555cxg1xlT_ajqRi78M9osOfdOF2jTIjlPkn_UZL8cUwVP0DPC8rH3vhhg8RpspFe8Vewx92shAO3RPos_uMH0CUqEiCiZlaaB5I3Jq3Mv","refresh_token":"1/teUKUqGKMLjVqs-eed0L8omI02pzSxMUYaxGc2QxBw0","scope":"https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly","token_type":"Bearer","expiry_date":1565654175862}
\ No newline at end of file
+{"access_token":"ya29.Glt2B3lsrpWxZ9DMg1RcTksAFzfR8dVWhf7d7tAvbJ4UbcSVO0Q3aYNGtaMKPtmxR24rH88iQSiKCL8S328TQFEN6LtZgvizymednK5EW0jNCvG6ecdZQ-vwcypR","refresh_token":"1/6X3oGYz4A0p8UW2IgsZ-GqTgQUY43S6Ahsaf_GQhSs8","scope":"https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations 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/photoslibrary","token_type":"Bearer","expiry_date":1567360444627}
\ No newline at end of file
diff --git a/src/server/credentials/google_photos_credentials.ts b/src/server/credentials/google_photos_credentials.ts
new file mode 100644
index 000000000..11c1c766c
--- /dev/null
+++ b/src/server/credentials/google_photos_credentials.ts
@@ -0,0 +1,35 @@
+const config: any = {};
+
+// The OAuth client ID from the Google Developers console.
+config.oAuthClientID = '1005546247619-l40012sl4idpq17b5emielcs1delffog.apps.googleusercontent.com';
+
+// The OAuth client secret from the Google Developers console.
+config.oAuthclientSecret = 'xEUJ0OBvhlCKA6SLt8TvWBs3';
+
+// The callback to use for OAuth requests. This is the URL where the app is
+// running. For testing and running it locally, use 127.0.0.1.
+config.oAuthCallbackUrl = 'http://localhost:1050/auth/google/callback';
+
+// The port where the app should listen for requests.
+config.port = 1050;
+
+// The scopes to request. The app requires the photoslibrary.readonly and
+// plus.me scopes.
+config.scopes = [
+    'https://www.googleapis.com/auth/photoslibrary.readonly',
+    'profile',
+];
+
+// The number of photos to load for search requests.
+config.photosToLoad = 150;
+
+// The page size to use for search requests. 100 is reccommended.
+config.searchPageSize = 100;
+
+// The page size to use for the listing albums request. 50 is reccommended.
+config.albumPageSize = 50;
+
+// The API end point to use. Do not change.
+config.apiEndpoint = 'https://photoslibrary.googleapis.com';
+
+module.exports = config;
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 34a0a19f1..6105dedcc 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -29,6 +29,7 @@ import { RouteStore } from './RouteStore';
 import v4 = require('uuid/v4');
 const app = express();
 const config = require('../../webpack.config');
+const OAuthConfig = require('../server/credentials/google_photos_credentials');
 import { createCanvas, loadImage, Canvas } from "canvas";
 const compiler = webpack(config);
 const port = 1050; // default port to listen
@@ -46,6 +47,7 @@ import { GaxiosResponse } from 'gaxios';
 import { Opt } from '../new_fields/Doc';
 import { docs_v1 } from 'googleapis';
 import { Endpoint } from 'googleapis-common';
+import { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils';
 const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
 const probe = require("probe-image-size");
@@ -196,6 +198,31 @@ const solrURL = "http://localhost:8983/solr/#/dash";
 
 // GETTERS
 
+// app.get('/auth/google', passport.authenticate('google', {
+//     scope: OAuthConfig.scopes,
+//     failureFlash: true,  // Display errors to the user.
+//     session: true,
+// }));
+
+// app.get("/failed", (req, res) => res.send("DIDN'T WORK!"));
+
+// app.get(
+//     '/auth/google/callback',
+//     passport.authenticate(
+//         'google', { failureRedirect: '/failed', failureFlash: true, session: true }),
+//     (req, res) => {
+//         // User has logged in.
+//         console.log('OAUTH: user has logged in 1.');
+//         PhotosLibraryQuery(req.user.token, {});
+//         console.log('OAUTH: user has logged in 2.');
+//         res.redirect('/');
+//     });
+
+// app.get('/GooglePhotos', (req, res) => {
+//     console.log("WORKING ON GOOGLE PHOTOS");
+//     PhotosLibraryQuery(req.user.token, {});
+// });
+
 app.get("/search", async (req, res) => {
     const solrQuery: any = {};
     ["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index 7939ae8be..36d828fdb 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -1,322 +1,324 @@
 /// <reference types="node" />
 
-declare module '@react-pdf/renderer' {
-  import * as React from 'react';
-
-  namespace ReactPDF {
-    interface Style {
-      [property: string]: any;
-    }
-    interface Styles {
-      [key: string]: Style;
-    }
-    type Orientation = 'portrait' | 'landscape';
-
-    interface DocumentProps {
-      title?: string;
-      author?: string;
-      subject?: string;
-      keywords?: string;
-      creator?: string;
-      producer?: string;
-      onRender?: () => any;
-    }
-
-    /**
-     * This component represent the PDF document itself. It must be the root
-     * of your tree element structure, and under no circumstances should it be
-     * used as children of another react-pdf component. In addition, it should
-     * only have childs of type <Page />.
-     */
-    class Document extends React.Component<DocumentProps> { }
-
-    interface NodeProps {
-      style?: Style | Style[];
-      /**
-       * Render component in all wrapped pages.
-       * @see https://react-pdf.org/advanced#fixed-components
-       */
-      fixed?: boolean;
-      /**
-       * Force the wrapping algorithm to start a new page when rendering the
-       * element.
-       * @see https://react-pdf.org/advanced#page-breaks
-       */
-      break?: boolean;
-    }
-
-    interface PageProps extends NodeProps {
-      /**
-       * Enable page wrapping for this page.
-       * @see https://react-pdf.org/components#page-wrapping
-       */
-      wrap?: boolean;
-      debug?: boolean;
-      size?: string | [number, number] | { width: number; height: number };
-      orientation?: Orientation;
-      ruler?: boolean;
-      rulerSteps?: number;
-      verticalRuler?: boolean;
-      verticalRulerSteps?: number;
-      horizontalRuler?: boolean;
-      horizontalRulerSteps?: number;
-      ref?: Page;
-    }
-
-    /**
-     * Represents single page inside the PDF document, or a subset of them if
-     * using the wrapping feature. A <Document /> can contain as many pages as
-     * you want, but ensure not rendering a page inside any component besides
-     * Document.
-     */
-    class Page extends React.Component<PageProps> { }
-
-    interface ViewProps extends NodeProps {
-      /**
-       * Enable/disable page wrapping for element.
-       * @see https://react-pdf.org/components#page-wrapping
-       */
-      wrap?: boolean;
-      debug?: boolean;
-      render?: (props: { pageNumber: number }) => React.ReactNode;
-      children?: React.ReactNode;
-    }
-
-    /**
-     * The most fundamental component for building a UI and is designed to be
-     * nested inside other views and can have 0 to many children.
-     */
-    class View extends React.Component<ViewProps> { }
-
-    interface ImageProps extends NodeProps {
-      debug?: boolean;
-      src: string | { data: Buffer; format: 'png' | 'jpg' };
-      cache?: boolean;
-    }
-
-    /**
-     * A React component for displaying network or local (Node only) JPG or
-     * PNG images, as well as base64 encoded image strings.
-     */
-    class Image extends React.Component<ImageProps> { }
-
-    interface TextProps extends NodeProps {
-      /**
-       * Enable/disable page wrapping for element.
-       * @see https://react-pdf.org/components#page-wrapping
-       */
-      wrap?: boolean;
-      debug?: boolean;
-      render?: (
-        props: { pageNumber: number; totalPages: number },
-      ) => React.ReactNode;
-      children?: React.ReactNode;
-      /**
-       * How much hyphenated breaks should be avoided.
-       */
-      hyphenationCallback?: number;
-    }
-
-    /**
-     * A React component for displaying text. Text supports nesting of other
-     * Text or Link components to create inline styling.
-     */
-    class Text extends React.Component<TextProps> { }
-
-    interface LinkProps extends NodeProps {
-      /**
-       * Enable/disable page wrapping for element.
-       * @see https://react-pdf.org/components#page-wrapping
-       */
-      wrap?: boolean;
-      debug?: boolean;
-      src: string;
-      children?: React.ReactNode;
-    }
+declare module 'googlephotos';
 
-    /**
-     * A React component for displaying an hyperlink. Link’s can be nested
-     * inside a Text component, or being inside any other valid primitive.
-     */
-    class Link extends React.Component<LinkProps> { }
-
-    interface NoteProps extends NodeProps {
-      children: string;
-    }
-
-    class Note extends React.Component<NoteProps> { }
-
-    interface BlobProviderParams {
-      blob: Blob | null;
-      url: string | null;
-      loading: boolean;
-      error: Error | null;
-    }
-    interface BlobProviderProps {
-      document: React.ReactElement<DocumentProps>;
-      children: (params: BlobProviderParams) => React.ReactNode;
-    }
-
-    /**
-     * Easy and declarative way of getting document's blob data without
-     * showing it on screen.
-     * @see https://react-pdf.org/advanced#on-the-fly-rendering
-     * @platform web
-     */
-    class BlobProvider extends React.Component<BlobProviderProps> { }
-
-    interface PDFViewerProps {
-      width?: number;
-      height?: number;
-      style?: Style | Style[];
-      className?: string;
-      children?: React.ReactElement<DocumentProps>;
-    }
-
-    /**
-     * Iframe PDF viewer for client-side generated documents.
-     * @platform web
-     */
-    class PDFViewer extends React.Component<PDFViewerProps> { }
-
-    interface PDFDownloadLinkProps {
-      document: React.ReactElement<DocumentProps>;
-      fileName?: string;
-      style?: Style | Style[];
-      className?: string;
-      children?:
-      | React.ReactNode
-      | ((params: BlobProviderParams) => React.ReactNode);
+declare module '@react-pdf/renderer' {
+    import * as React from 'react';
+
+    namespace ReactPDF {
+        interface Style {
+            [property: string]: any;
+        }
+        interface Styles {
+            [key: string]: Style;
+        }
+        type Orientation = 'portrait' | 'landscape';
+
+        interface DocumentProps {
+            title?: string;
+            author?: string;
+            subject?: string;
+            keywords?: string;
+            creator?: string;
+            producer?: string;
+            onRender?: () => any;
+        }
+
+        /**
+         * This component represent the PDF document itself. It must be the root
+         * of your tree element structure, and under no circumstances should it be
+         * used as children of another react-pdf component. In addition, it should
+         * only have childs of type <Page />.
+         */
+        class Document extends React.Component<DocumentProps> { }
+
+        interface NodeProps {
+            style?: Style | Style[];
+            /**
+             * Render component in all wrapped pages.
+             * @see https://react-pdf.org/advanced#fixed-components
+             */
+            fixed?: boolean;
+            /**
+             * Force the wrapping algorithm to start a new page when rendering the
+             * element.
+             * @see https://react-pdf.org/advanced#page-breaks
+             */
+            break?: boolean;
+        }
+
+        interface PageProps extends NodeProps {
+            /**
+             * Enable page wrapping for this page.
+             * @see https://react-pdf.org/components#page-wrapping
+             */
+            wrap?: boolean;
+            debug?: boolean;
+            size?: string | [number, number] | { width: number; height: number };
+            orientation?: Orientation;
+            ruler?: boolean;
+            rulerSteps?: number;
+            verticalRuler?: boolean;
+            verticalRulerSteps?: number;
+            horizontalRuler?: boolean;
+            horizontalRulerSteps?: number;
+            ref?: Page;
+        }
+
+        /**
+         * Represents single page inside the PDF document, or a subset of them if
+         * using the wrapping feature. A <Document /> can contain as many pages as
+         * you want, but ensure not rendering a page inside any component besides
+         * Document.
+         */
+        class Page extends React.Component<PageProps> { }
+
+        interface ViewProps extends NodeProps {
+            /**
+             * Enable/disable page wrapping for element.
+             * @see https://react-pdf.org/components#page-wrapping
+             */
+            wrap?: boolean;
+            debug?: boolean;
+            render?: (props: { pageNumber: number }) => React.ReactNode;
+            children?: React.ReactNode;
+        }
+
+        /**
+         * The most fundamental component for building a UI and is designed to be
+         * nested inside other views and can have 0 to many children.
+         */
+        class View extends React.Component<ViewProps> { }
+
+        interface ImageProps extends NodeProps {
+            debug?: boolean;
+            src: string | { data: Buffer; format: 'png' | 'jpg' };
+            cache?: boolean;
+        }
+
+        /**
+         * A React component for displaying network or local (Node only) JPG or
+         * PNG images, as well as base64 encoded image strings.
+         */
+        class Image extends React.Component<ImageProps> { }
+
+        interface TextProps extends NodeProps {
+            /**
+             * Enable/disable page wrapping for element.
+             * @see https://react-pdf.org/components#page-wrapping
+             */
+            wrap?: boolean;
+            debug?: boolean;
+            render?: (
+                props: { pageNumber: number; totalPages: number },
+            ) => React.ReactNode;
+            children?: React.ReactNode;
+            /**
+             * How much hyphenated breaks should be avoided.
+             */
+            hyphenationCallback?: number;
+        }
+
+        /**
+         * A React component for displaying text. Text supports nesting of other
+         * Text or Link components to create inline styling.
+         */
+        class Text extends React.Component<TextProps> { }
+
+        interface LinkProps extends NodeProps {
+            /**
+             * Enable/disable page wrapping for element.
+             * @see https://react-pdf.org/components#page-wrapping
+             */
+            wrap?: boolean;
+            debug?: boolean;
+            src: string;
+            children?: React.ReactNode;
+        }
+
+        /**
+         * A React component for displaying an hyperlink. Link’s can be nested
+         * inside a Text component, or being inside any other valid primitive.
+         */
+        class Link extends React.Component<LinkProps> { }
+
+        interface NoteProps extends NodeProps {
+            children: string;
+        }
+
+        class Note extends React.Component<NoteProps> { }
+
+        interface BlobProviderParams {
+            blob: Blob | null;
+            url: string | null;
+            loading: boolean;
+            error: Error | null;
+        }
+        interface BlobProviderProps {
+            document: React.ReactElement<DocumentProps>;
+            children: (params: BlobProviderParams) => React.ReactNode;
+        }
+
+        /**
+         * Easy and declarative way of getting document's blob data without
+         * showing it on screen.
+         * @see https://react-pdf.org/advanced#on-the-fly-rendering
+         * @platform web
+         */
+        class BlobProvider extends React.Component<BlobProviderProps> { }
+
+        interface PDFViewerProps {
+            width?: number;
+            height?: number;
+            style?: Style | Style[];
+            className?: string;
+            children?: React.ReactElement<DocumentProps>;
+        }
+
+        /**
+         * Iframe PDF viewer for client-side generated documents.
+         * @platform web
+         */
+        class PDFViewer extends React.Component<PDFViewerProps> { }
+
+        interface PDFDownloadLinkProps {
+            document: React.ReactElement<DocumentProps>;
+            fileName?: string;
+            style?: Style | Style[];
+            className?: string;
+            children?:
+            | React.ReactNode
+            | ((params: BlobProviderParams) => React.ReactNode);
+        }
+
+        /**
+         * Anchor tag to enable generate and download PDF documents on the fly.
+         * @see https://react-pdf.org/advanced#on-the-fly-rendering
+         * @platform web
+         */
+        class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
+
+        interface EmojiSource {
+            url: string;
+            format: string;
+        }
+        interface RegisteredFont {
+            src: string;
+            loaded: boolean;
+            loading: boolean;
+            data: any;
+            [key: string]: any;
+        }
+        type HyphenationCallback = (
+            words: string[],
+            glyphString: { [key: string]: any },
+        ) => string[];
+
+        const Font: {
+            register: (
+                src: string,
+                options: { family: string;[key: string]: any },
+            ) => void;
+            getEmojiSource: () => EmojiSource;
+            getRegisteredFonts: () => string[];
+            registerEmojiSource: (emojiSource: EmojiSource) => void;
+            registerHyphenationCallback: (
+                hyphenationCallback: HyphenationCallback,
+            ) => void;
+            getHyphenationCallback: () => HyphenationCallback;
+            getFont: (fontFamily: string) => RegisteredFont | undefined;
+            load: (
+                fontFamily: string,
+                document: React.ReactElement<DocumentProps>,
+            ) => Promise<void>;
+            clear: () => void;
+            reset: () => void;
+        };
+
+        const StyleSheet: {
+            hairlineWidth: number;
+            create: <TStyles>(styles: TStyles) => TStyles;
+            resolve: (
+                style: Style,
+                container: {
+                    width: number;
+                    height: number;
+                    orientation: Orientation;
+                },
+            ) => Style;
+            flatten: (...styles: Style[]) => Style;
+            absoluteFillObject: {
+                position: 'absolute';
+                left: 0;
+                right: 0;
+                top: 0;
+                bottom: 0;
+            };
+        };
+
+        const version: any;
+
+        const PDFRenderer: any;
+
+        const createInstance: (
+            element: {
+                type: string;
+                props: { [key: string]: any };
+            },
+            root?: any,
+        ) => any;
+
+        const pdf: (
+            document: React.ReactElement<DocumentProps>,
+        ) => {
+            isDirty: () => boolean;
+            updateContainer: (document: React.ReactElement<any>) => void;
+            toBuffer: () => NodeJS.ReadableStream;
+            toBlob: () => Blob;
+            toString: () => string;
+        };
+
+        const renderToStream: (
+            document: React.ReactElement<DocumentProps>,
+        ) => NodeJS.ReadableStream;
+
+        const renderToFile: (
+            document: React.ReactElement<DocumentProps>,
+            filePath: string,
+            callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
+        ) => Promise<NodeJS.ReadableStream>;
+
+        const render: typeof renderToFile;
     }
 
-    /**
-     * Anchor tag to enable generate and download PDF documents on the fly.
-     * @see https://react-pdf.org/advanced#on-the-fly-rendering
-     * @platform web
-     */
-    class PDFDownloadLink extends React.Component<PDFDownloadLinkProps> { }
-
-    interface EmojiSource {
-      url: string;
-      format: string;
-    }
-    interface RegisteredFont {
-      src: string;
-      loaded: boolean;
-      loading: boolean;
-      data: any;
-      [key: string]: any;
-    }
-    type HyphenationCallback = (
-      words: string[],
-      glyphString: { [key: string]: any },
-    ) => string[];
-
-    const Font: {
-      register: (
-        src: string,
-        options: { family: string;[key: string]: any },
-      ) => void;
-      getEmojiSource: () => EmojiSource;
-      getRegisteredFonts: () => string[];
-      registerEmojiSource: (emojiSource: EmojiSource) => void;
-      registerHyphenationCallback: (
-        hyphenationCallback: HyphenationCallback,
-      ) => void;
-      getHyphenationCallback: () => HyphenationCallback;
-      getFont: (fontFamily: string) => RegisteredFont | undefined;
-      load: (
-        fontFamily: string,
-        document: React.ReactElement<DocumentProps>,
-      ) => Promise<void>;
-      clear: () => void;
-      reset: () => void;
+    const Document: typeof ReactPDF.Document;
+    const Page: typeof ReactPDF.Page;
+    const View: typeof ReactPDF.View;
+    const Image: typeof ReactPDF.Image;
+    const Text: typeof ReactPDF.Text;
+    const Link: typeof ReactPDF.Link;
+    const Note: typeof ReactPDF.Note;
+    const Font: typeof ReactPDF.Font;
+    const StyleSheet: typeof ReactPDF.StyleSheet;
+    const createInstance: typeof ReactPDF.createInstance;
+    const PDFRenderer: typeof ReactPDF.PDFRenderer;
+    const version: typeof ReactPDF.version;
+    const pdf: typeof ReactPDF.pdf;
+
+    export default ReactPDF;
+    export {
+        Document,
+        Page,
+        View,
+        Image,
+        Text,
+        Link,
+        Note,
+        Font,
+        StyleSheet,
+        createInstance,
+        PDFRenderer,
+        version,
+        pdf,
     };
-
-    const StyleSheet: {
-      hairlineWidth: number;
-      create: <TStyles>(styles: TStyles) => TStyles;
-      resolve: (
-        style: Style,
-        container: {
-          width: number;
-          height: number;
-          orientation: Orientation;
-        },
-      ) => Style;
-      flatten: (...styles: Style[]) => Style;
-      absoluteFillObject: {
-        position: 'absolute';
-        left: 0;
-        right: 0;
-        top: 0;
-        bottom: 0;
-      };
-    };
-
-    const version: any;
-
-    const PDFRenderer: any;
-
-    const createInstance: (
-      element: {
-        type: string;
-        props: { [key: string]: any };
-      },
-      root?: any,
-    ) => any;
-
-    const pdf: (
-      document: React.ReactElement<DocumentProps>,
-    ) => {
-      isDirty: () => boolean;
-      updateContainer: (document: React.ReactElement<any>) => void;
-      toBuffer: () => NodeJS.ReadableStream;
-      toBlob: () => Blob;
-      toString: () => string;
-    };
-
-    const renderToStream: (
-      document: React.ReactElement<DocumentProps>,
-    ) => NodeJS.ReadableStream;
-
-    const renderToFile: (
-      document: React.ReactElement<DocumentProps>,
-      filePath: string,
-      callback?: (output: NodeJS.ReadableStream, filePath: string) => any,
-    ) => Promise<NodeJS.ReadableStream>;
-
-    const render: typeof renderToFile;
-  }
-
-  const Document: typeof ReactPDF.Document;
-  const Page: typeof ReactPDF.Page;
-  const View: typeof ReactPDF.View;
-  const Image: typeof ReactPDF.Image;
-  const Text: typeof ReactPDF.Text;
-  const Link: typeof ReactPDF.Link;
-  const Note: typeof ReactPDF.Note;
-  const Font: typeof ReactPDF.Font;
-  const StyleSheet: typeof ReactPDF.StyleSheet;
-  const createInstance: typeof ReactPDF.createInstance;
-  const PDFRenderer: typeof ReactPDF.PDFRenderer;
-  const version: typeof ReactPDF.version;
-  const pdf: typeof ReactPDF.pdf;
-
-  export default ReactPDF;
-  export {
-    Document,
-    Page,
-    View,
-    Image,
-    Text,
-    Link,
-    Note,
-    Font,
-    StyleSheet,
-    createInstance,
-    PDFRenderer,
-    version,
-    pdf,
-  };
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 38176e5ba949b84dc410d29197180121d81e085c Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 3 Sep 2019 12:51:59 -0400
Subject: implemented refresh tokens and create, get, list

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 35 +++++++++
 src/client/views/MainView.tsx                      | 10 ++-
 src/server/RouteStore.ts                           |  3 +-
 src/server/apis/google/GoogleApiServerUtils.ts     | 90 ++++++++++++++--------
 src/server/apis/google/GooglePhotosServerUtils.ts  | 68 ++++++++++++++++
 src/server/apis/google/GooglePhotosUploadUtils.ts  | 42 ++++++++++
 src/server/apis/google/GooglePhotosUtils.ts        | 21 -----
 src/server/apis/google/typings/albums.ts           | 30 ++++----
 src/server/credentials/google_docs_token.json      |  2 +-
 src/server/index.ts                                | 54 ++++++-------
 10 files changed, 252 insertions(+), 103 deletions(-)
 create mode 100644 src/client/apis/google_docs/GooglePhotosClientUtils.ts
 create mode 100644 src/server/apis/google/GooglePhotosServerUtils.ts
 create mode 100644 src/server/apis/google/GooglePhotosUploadUtils.ts
 delete mode 100644 src/server/apis/google/GooglePhotosUtils.ts

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
new file mode 100644
index 000000000..67a282f48
--- /dev/null
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -0,0 +1,35 @@
+import { Album } from "../../../server/apis/google/typings/albums";
+import { PostToServer } from "../../../Utils";
+import { RouteStore } from "../../../server/RouteStore";
+
+export namespace GooglePhotosClientUtils {
+
+    export const Create = async (title: string) => {
+        let parameters = {
+            action: Album.Action.Create,
+            body: { album: { title } }
+        } as Album.Create;
+        return PostToServer(RouteStore.googlePhotos, parameters);
+    };
+
+    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.googlePhotos, parameters);
+    };
+
+    export const Get = async (albumId: string) => {
+        let parameters = {
+            action: Album.Action.Get,
+            albumId
+        } as Album.Get;
+        return PostToServer(RouteStore.googlePhotos, parameters);
+    };
+
+}
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index df0718072..ece475c80 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -42,6 +42,8 @@ import PresModeMenu from './presentationview/PresentationModeMenu';
 import { PresBox } from './nodes/PresBox';
 import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils';
 import { docs_v1 } from 'googleapis';
+import { Album } from '../../server/apis/google/typings/albums';
+import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils';
 
 @observer
 export class MainView extends React.Component {
@@ -128,7 +130,7 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
-        PostToServer('/googleDocs/Photos/Test', {});
+        this.executeGooglePhotosAlbumTestRoutine();
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
@@ -147,6 +149,12 @@ export class MainView extends React.Component {
         }, { fireImmediately: true });
     }
 
+    executeGooglePhotosAlbumTestRoutine = async () => {
+        let title = "This is a generically created album!";
+        console.log(await GooglePhotosClientUtils.Create(title));
+        console.log(await GooglePhotosClientUtils.List({ pageSize: 50 }));
+    }
+
     componentWillUnMount() {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         //close presentation 
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index 014906054..fc5511f98 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -31,6 +31,7 @@ export enum RouteStore {
 
     // APIS
     cognitiveServices = "/cognitiveservices",
-    googleDocs = "/googleDocs"
+    googleDocs = "/googleDocs",
+    googlePhotos = "/googlePhotos"
 
 }
\ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 00e289b00..048ac4b21 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,13 +1,12 @@
-import { google, docs_v1, slides_v1 } from "googleapis";
+import { google } from "googleapis";
 import { createInterface } from "readline";
 import { readFile, writeFile } from "fs";
 import { OAuth2Client, Credentials } from "google-auth-library";
 import { Opt } from "../../../new_fields/Doc";
 import { GlobalOptions } from "googleapis-common";
 import { GaxiosResponse } from "gaxios";
-import { GooglePhotos } from "./GooglePhotosUtils";
-import { Utils } from "../../../Utils";
-import { Album } from "./typings/albums";
+import request = require('request-promise');
+import * as qs from 'query-string';
 
 /**
  * Server side authentication for Google Api queries.
@@ -31,8 +30,7 @@ export namespace GoogleApiServerUtils {
 
     export enum Service {
         Documents = "Documents",
-        Slides = "Slides",
-        Photos = "Photos"
+        Slides = "Slides"
     }
 
     export interface CredentialPaths {
@@ -49,38 +47,31 @@ export namespace GoogleApiServerUtils {
     export type EndpointParameters = GlobalOptions & { version: "v1" };
 
     export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
-        return new Promise<Opt<Endpoint>>((resolve, reject) => {
+        return new Promise<Opt<Endpoint>>(resolve => {
+            RetrieveAuthenticationInformation(paths).then(authentication => {
+                let routed: Opt<Endpoint>;
+                let parameters: EndpointParameters = { auth: authentication.client, version: "v1" };
+                switch (sector) {
+                    case Service.Documents:
+                        routed = google.docs(parameters).documents;
+                        break;
+                    case Service.Slides:
+                        routed = google.slides(parameters).presentations;
+                        break;
+                }
+                resolve(routed);
+            });
+        });
+    };
+
+    export const RetrieveAuthenticationInformation = async (paths: CredentialPaths) => {
+        return new Promise<TokenResult>((resolve, reject) => {
             readFile(paths.credentials, async (err, credentials) => {
                 if (err) {
                     reject(err);
                     return console.log('Error loading client secret file:', err);
                 }
-                authorize(parseBuffer(credentials), paths.token).then(async result => {
-                    let routed: Opt<Endpoint>;
-                    let parameters: EndpointParameters = { auth: result.client, version: "v1" };
-                    switch (sector) {
-                        case Service.Documents:
-                            routed = google.docs(parameters).documents;
-                            break;
-                        case Service.Slides:
-                            routed = google.slides(parameters).presentations;
-                            break;
-                        case Service.Photos:
-                            let token = result.token.access_token;
-                            if (token) {
-                                let create: Album.Create = {
-                                    action: Album.Action.Create,
-                                    body: {
-                                        album: {
-                                            title: "Sam's Bulk Export",
-                                        }
-                                    }
-                                };
-                                console.log(await GooglePhotos.ExecuteQuery(token, create));
-                            }
-                    }
-                    resolve(routed);
-                });
+                authorize(parseBuffer(credentials), paths.token).then(resolve, reject);
             });
         });
     };
@@ -101,13 +92,44 @@ export namespace GoogleApiServerUtils {
                 if (err) {
                     return getNewToken(oAuth2Client, token_path).then(resolve, reject);
                 }
-                let parsed = parseBuffer(token);
+                let parsed: Credentials = parseBuffer(token);
+                if (parsed.expiry_date! < new Date().getTime()) {
+                    return refreshToken(parsed, client_id, client_secret, oAuth2Client, token_path).then(resolve, reject);
+                }
                 oAuth2Client.setCredentials(parsed);
                 resolve({ token: parsed, client: oAuth2Client });
             });
         });
     }
 
+    const refreshEndpoint = "https://oauth2.googleapis.com/token";
+    const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, token_path: string) => {
+        return new Promise<TokenResult>((resolve, reject) => {
+            let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
+            let queryParameters = {
+                refreshToken: credentials.refresh_token,
+                client_id,
+                client_secret,
+                grant_type: "refresh_token"
+            };
+            let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`;
+            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;
+                writeFile(token_path, JSON.stringify(credentials), (err) => {
+                    if (err) {
+                        console.error(err);
+                        reject(err);
+                    }
+                    console.log('Refreshed token stored to', token_path);
+                    oAuth2Client.setCredentials(credentials);
+                    resolve({ token: credentials, client: oAuth2Client });
+                });
+            });
+        });
+    };
+
     /**
      * Get and store new token after prompting for user authorization, and then
      * execute the given callback with the authorized OAuth2 client.
diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts
new file mode 100644
index 000000000..cb5464abc
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosServerUtils.ts
@@ -0,0 +1,68 @@
+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
new file mode 100644
index 000000000..2e1599aaf
--- /dev/null
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -0,0 +1,42 @@
+import request = require('request-promise');
+import { Authorization } from './GooglePhotosServerUtils';
+
+export namespace GooglePhotosUploadUtils {
+
+    interface UploadInformation {
+        title: string;
+        url: URL;
+    }
+
+    const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads";
+
+    export const SubmitUpload = async (parameters: Authorization & UploadInformation) => {
+        let MEDIA_BINARY_DATA = binary(parameters.url.href);
+
+        let options = {
+            headers: {
+                'Content-Type': 'application/octet-stream',
+                Authorization: { 'bearer': parameters.token },
+                'X-Goog-Upload-File-Name': parameters.title,
+                'X-Goog-Upload-Protocol': 'raw'
+            },
+            body: { MEDIA_BINARY_DATA },
+            json: true
+        };
+        const result = await request.post(apiEndpoint, options);
+        return result;
+    };
+
+    const binary = (source: string) => {
+        const image = document.createElement("img");
+        image.src = source;
+        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,/, "");
+    };
+
+}
\ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts
deleted file mode 100644
index 750630626..000000000
--- a/src/server/apis/google/GooglePhotosUtils.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import request = require('request-promise');
-import { Album } from './typings/albums';
-
-const apiEndpoint = "https://photoslibrary.googleapis.com";
-
-export namespace GooglePhotos {
-
-    export type Query = Album.Query;
-
-    export const ExecuteQuery = async (authToken: string, query: GooglePhotos.Query) => {
-        let options = {
-            headers: { 'Content-Type': 'application/json' },
-            auth: { 'bearer': authToken },
-            body: query.body,
-            json: true
-        };
-        const result = await request.post(apiEndpoint + '/v1/albums', options);
-        return result;
-    };
-
-}
diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts
index 1c9b379fe..f3025567d 100644
--- a/src/server/apis/google/typings/albums.ts
+++ b/src/server/apis/google/typings/albums.ts
@@ -1,16 +1,16 @@
 export namespace Album {
 
-    export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare) & { body: any };
+    export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare);
 
     export enum Action {
-        AddEnrichment,
-        BatchAddMediaItems,
-        BatchRemoveMediaItems,
-        Create,
-        Get,
-        List,
-        Share,
-        Unshare
+        AddEnrichment = "addEnrichment",
+        BatchAddMediaItems = "batchAddMediaItems",
+        BatchRemoveMediaItems = "batchRemoveMediaItems",
+        Create = "create",
+        Get = "get",
+        List = "list",
+        Share = "share",
+        Unshare = "unshare"
     }
 
     export interface AddEnrichment {
@@ -52,11 +52,13 @@ export namespace Album {
 
     export interface List {
         action: Action.List;
-        parameters: {
-            pageSize: number,
-            pageToken: string,
-            excludeNonAppCreatedData: boolean
-        };
+        parameters: ListOptions;
+    }
+
+    export interface ListOptions {
+        pageSize: number;
+        pageToken: string;
+        excludeNonAppCreatedData: boolean;
     }
 
     export interface Share {
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 61864512c..39e067c86 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glt3B8HoVEda7Ab5TQMVrfvjPN2fFp4sFHtGoDs3TsBgFfw4G208q90JiFjkmQqwODjJi3sf4NCZd78VZTVL3aI0By7_ElZF7XaCvA0LJnfcAi2gi1P-2-boyjYO","refresh_token":"1/tJOVDbPZlADzd2B8Q2_j7jqignXlRwHsU7LbZkdbDBc","scope":"https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567374969108}
\ No newline at end of file
+{"access_token":"ya29.Glx4B9p8tvFsmsD-AD-D4jygL_YZVCFpPewM9djtfOT3T4S6ROxN5r0WLAKTYVNnXQbUri3Gu_-vIb0NWq9wEy1TdFTLIM8azWD82X5-I5BQq2DSOsYiKugvgVoHLw","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567529401142}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 1f105e9d2..54b954cfb 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -42,11 +42,7 @@ 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 { GaxiosResponse } from 'gaxios';
-// import { Opt } from '../new_fields/Doc';
-// import { docs_v1 } from 'googleapis';
-// import { Endpoint } from 'googleapis-common';
-// import { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils';
+import { GooglePhotos } from './apis/google/GooglePhotosServerUtils';
 const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
 const probe = require("probe-image-size");
@@ -195,32 +191,7 @@ app.get("/version", (req, res) => {
 // SEARCH
 const solrURL = "http://localhost:8983/solr/#/dash";
 
-// GETTERS
-
-// app.get('/auth/google', passport.authenticate('google', {
-//     scope: OAuthConfig.scopes,
-//     failureFlash: true,  // Display errors to the user.
-//     session: true,
-// }));
-
-// app.get("/failed", (req, res) => res.send("DIDN'T WORK!"));
-
-// app.get(
-//     '/auth/google/callback',
-//     passport.authenticate(
-//         'google', { failureRedirect: '/failed', failureFlash: true, session: true }),
-//     (req, res) => {
-//         // User has logged in.
-//         console.log('OAUTH: user has logged in 1.');
-//         PhotosLibraryQuery(req.user.token, {});
-//         console.log('OAUTH: user has logged in 2.');
-//         res.redirect('/');
-//     });
-
-// app.get('/GooglePhotos', (req, res) => {
-//     console.log("WORKING ON GOOGLE PHOTOS");
-//     PhotosLibraryQuery(req.user.token, {});
-// });
+// GETTERSå
 
 app.get("/search", async (req, res) => {
     const solrQuery: any = {};
@@ -853,6 +824,27 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     });
 });
 
+app.post(RouteStore.googlePhotos, (req, res) => {
+    GoogleApiServerUtils.RetrieveAuthenticationInformation({ credentials, token }).then(authentication => {
+        let validated = authentication.token.access_token;
+        if (!validated) {
+            res.send("Error: unable to authenticate Google Photos API request.");
+            return;
+        }
+        GooglePhotos.ExecuteQuery({ token: validated, 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}`);
+            });
+    });
+});
+
 const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
     "number": "_n",
     "string": "_t",
-- 
cgit v1.2.3-70-g09d2


From 0724d62e2a6797c640e79b46b678bd1d3917147f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 3 Sep 2019 13:18:23 -0400
Subject: fixed refresh token issue

---
 src/client/views/MainView.tsx                  | 8 --------
 src/server/apis/google/GoogleApiServerUtils.ts | 2 +-
 src/server/credentials/google_docs_token.json  | 2 +-
 3 files changed, 2 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ece475c80..3ee62ae86 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -130,8 +130,6 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
-        this.executeGooglePhotosAlbumTestRoutine();
-
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
             let recent = CurrentUserUtils.UserDocument.recentlyClosed;
@@ -149,12 +147,6 @@ export class MainView extends React.Component {
         }, { fireImmediately: true });
     }
 
-    executeGooglePhotosAlbumTestRoutine = async () => {
-        let title = "This is a generically created album!";
-        console.log(await GooglePhotosClientUtils.Create(title));
-        console.log(await GooglePhotosClientUtils.List({ pageSize: 50 }));
-    }
-
     componentWillUnMount() {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         //close presentation 
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 048ac4b21..edfd89de4 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -116,7 +116,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;
+                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/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 39e067c86..c83d82607 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx4B9p8tvFsmsD-AD-D4jygL_YZVCFpPewM9djtfOT3T4S6ROxN5r0WLAKTYVNnXQbUri3Gu_-vIb0NWq9wEy1TdFTLIM8azWD82X5-I5BQq2DSOsYiKugvgVoHLw","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567529401142}
\ No newline at end of file
+{"access_token":"ya29.Glx4B4EQ5Q7WNw1hPrcqbZHssp0l1BtzszwNgmKGd783VBP9G3hLMe1AkbQw_Dl0amzgFeO29R4WP0ca-5U_Rf1tjsdtio6XjBGpJrY-S5wK7t71raKkwRPZFITw2Q","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567531020435}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 7e87aa4b7e0125482c87ab61f4a0de14e774558d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 3 Sep 2019 16:23:42 -0400
Subject: working on media uploading

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 22 ++++++++--
 src/client/views/MainView.tsx                      |  6 +++
 src/server/RouteStore.ts                           |  3 +-
 src/server/apis/google/GoogleApiServerUtils.ts     | 13 +++++-
 src/server/apis/google/GooglePhotosUploadUtils.ts  | 20 ++-------
 src/server/credentials/google_docs_token.json      |  2 +-
 src/server/index.ts                                | 50 ++++++++++++++--------
 7 files changed, 73 insertions(+), 43 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 67a282f48..0864ebdb1 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,6 +1,7 @@
 import { Album } from "../../../server/apis/google/typings/albums";
 import { PostToServer } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
+import { ImageField } from "../../../new_fields/URLField";
 
 export namespace GooglePhotosClientUtils {
 
@@ -9,7 +10,7 @@ export namespace GooglePhotosClientUtils {
             action: Album.Action.Create,
             body: { album: { title } }
         } as Album.Create;
-        return PostToServer(RouteStore.googlePhotos, parameters);
+        return PostToServer(RouteStore.googlePhotosQuery, parameters);
     };
 
     export const List = async (options?: Partial<Album.ListOptions>) => {
@@ -21,7 +22,7 @@ export namespace GooglePhotosClientUtils {
                 excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false,
             } as Album.ListOptions
         } as Album.List;
-        return PostToServer(RouteStore.googlePhotos, parameters);
+        return PostToServer(RouteStore.googlePhotosQuery, parameters);
     };
 
     export const Get = async (albumId: string) => {
@@ -29,7 +30,22 @@ export namespace GooglePhotosClientUtils {
             action: Album.Action.Get,
             albumId
         } as Album.Get;
-        return PostToServer(RouteStore.googlePhotos, parameters);
+        return PostToServer(RouteStore.googlePhotosQuery, parameters);
+    };
+
+    export const toDataURL = (field: ImageField | undefined) => {
+        if (!field) {
+            return undefined;
+        }
+        const image = document.createElement("img");
+        image.src = field.url.href;
+        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,/, "");
     };
 
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3ee62ae86..590addaa3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -44,6 +44,7 @@ import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils';
 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';
 
 @observer
 export class MainView extends React.Component {
@@ -130,6 +131,11 @@ 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);
+
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
             let recent = CurrentUserUtils.UserDocument.recentlyClosed;
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index fc5511f98..3b3fd9b4a 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -32,6 +32,7 @@ export enum RouteStore {
     // APIS
     cognitiveServices = "/cognitiveservices",
     googleDocs = "/googleDocs",
-    googlePhotos = "/googlePhotos"
+    googlePhotosQuery = "/googlePhotosQuery",
+    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 edfd89de4..2c9085ebb 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -48,7 +48,7 @@ export namespace GoogleApiServerUtils {
 
     export const GetEndpoint = async (sector: string, paths: CredentialPaths) => {
         return new Promise<Opt<Endpoint>>(resolve => {
-            RetrieveAuthenticationInformation(paths).then(authentication => {
+            RetrieveCredentials(paths).then(authentication => {
                 let routed: Opt<Endpoint>;
                 let parameters: EndpointParameters = { auth: authentication.client, version: "v1" };
                 switch (sector) {
@@ -64,7 +64,7 @@ export namespace GoogleApiServerUtils {
         });
     };
 
-    export const RetrieveAuthenticationInformation = async (paths: CredentialPaths) => {
+    export const RetrieveCredentials = async (paths: CredentialPaths) => {
         return new Promise<TokenResult>((resolve, reject) => {
             readFile(paths.credentials, async (err, credentials) => {
                 if (err) {
@@ -76,6 +76,15 @@ export namespace GoogleApiServerUtils {
         });
     };
 
+    export const RetrieveAccessToken = async (paths: CredentialPaths) => {
+        return new Promise<string>((resolve, reject) => {
+            RetrieveCredentials(paths).then(
+                credentials => resolve(credentials.token.access_token!),
+                error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
+            );
+        });
+    };
+
     type TokenResult = { token: Credentials, client: OAuth2Client };
     /**
      * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 2e1599aaf..b358f9698 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -5,38 +5,24 @@ export namespace GooglePhotosUploadUtils {
 
     interface UploadInformation {
         title: string;
-        url: URL;
+        MEDIA_BINARY_DATA: string;
     }
 
     const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads";
 
     export const SubmitUpload = async (parameters: Authorization & UploadInformation) => {
-        let MEDIA_BINARY_DATA = binary(parameters.url.href);
-
         let options = {
             headers: {
                 'Content-Type': 'application/octet-stream',
-                Authorization: { 'bearer': parameters.token },
+                auth: { 'bearer': parameters.token },
                 'X-Goog-Upload-File-Name': parameters.title,
                 'X-Goog-Upload-Protocol': 'raw'
             },
-            body: { MEDIA_BINARY_DATA },
+            body: { MEDIA_BINARY_DATA: parameters.MEDIA_BINARY_DATA },
             json: true
         };
         const result = await request.post(apiEndpoint, options);
         return result;
     };
 
-    const binary = (source: string) => {
-        const image = document.createElement("img");
-        image.src = source;
-        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,/, "");
-    };
-
 }
\ 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 c83d82607..e4be9ff60 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx4B4EQ5Q7WNw1hPrcqbZHssp0l1BtzszwNgmKGd783VBP9G3hLMe1AkbQw_Dl0amzgFeO29R4WP0ca-5U_Rf1tjsdtio6XjBGpJrY-S5wK7t71raKkwRPZFITw2Q","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567531020435}
\ No newline at end of file
+{"access_token":"ya29.Glx4B9gmH9fKJex9Je-k1KBNWEsJBPgQqoEWHhwk0TtTemCxONoyVMO38idAE7Vy9vdjcosjl0H4swnnH1s2QjTwZaspzKPeQr8Oh4sHkCJ-MbNd6naMZBdy1pccjQ","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567545365417}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 54b954cfb..3e85b1ce0 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -43,6 +43,7 @@ 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');
 const probe = require("probe-image-size");
@@ -824,25 +825,36 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     });
 });
 
-app.post(RouteStore.googlePhotos, (req, res) => {
-    GoogleApiServerUtils.RetrieveAuthenticationInformation({ credentials, token }).then(authentication => {
-        let validated = authentication.token.access_token;
-        if (!validated) {
-            res.send("Error: unable to authenticate Google Photos API request.");
-            return;
-        }
-        GooglePhotos.ExecuteQuery({ token: validated, 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}`);
-            });
-    });
+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.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 suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
-- 
cgit v1.2.3-70-g09d2


From 0404047f5e747608b33474fa1c883a489cd6b403 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Tue, 3 Sep 2019 19:29:49 -0400
Subject: fixed data url

---
 src/client/apis/google_docs/GooglePhotosClientUtils.ts | 2 ++
 src/client/views/MainView.tsx                          | 2 +-
 2 files changed, 3 insertions(+), 1 deletion(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 0864ebdb1..b95cc98c9 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -39,6 +39,8 @@ export namespace GooglePhotosClientUtils {
         }
         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;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f01083fbb..c1c95fc88 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -134,7 +134,7 @@ export class MainView extends React.Component {
         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);
+        // PostToServer(RouteStore.googlePhotosMediaUpload, parameters).then(console.log);
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
-- 
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/MainView.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 d94509864920b2bbe7f4af8837f3af3f721b7dad Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 7 Sep 2019 13:19:10 -0400
Subject: implemented via context menu

---
 src/client/apis/google_docs/GooglePhotosClientUtils.ts |  4 ++--
 src/client/views/MainView.tsx                          |  2 +-
 src/client/views/nodes/DocumentView.tsx                |  5 +++++
 src/server/apis/google/GooglePhotosUploadUtils.ts      | 16 ++++------------
 src/server/credentials/google_docs_token.json          |  2 +-
 src/server/index.ts                                    |  8 ++++----
 tsconfig.json                                          |  2 +-
 7 files changed, 18 insertions(+), 21 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 2b72800a9..924362c03 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -13,8 +13,8 @@ export namespace GooglePhotosClientUtils {
     export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text()));
 
     export interface MediaInput {
+        url: string;
         description: string;
-        source: string;
     }
 
     export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => {
@@ -29,7 +29,7 @@ export namespace GooglePhotosClientUtils {
                 return undefined;
             }
             media.push({
-                source: data.url.href,
+                url: data.url.href,
                 description,
             } as MediaInput);
         });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 6d366216e..7fe35494d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -130,7 +130,7 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
-        this.executeGooglePhotosRoutine();
+        // this.executeGooglePhotosRoutine();
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b60730a6b..b8a034efc 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -41,6 +41,8 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from './FormattedTextBox';
 import React = require("react");
 import { DocumentType } from '../../documents/DocumentTypes';
+import { GooglePhotosClientUtils } 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?
 
 library.add(fa.faTrash);
@@ -588,6 +590,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         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" });
         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.UploadMedia([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" });
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 3b513aaf1..13db1df03 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -14,8 +14,8 @@ export namespace GooglePhotosUploadUtils {
     }
 
     export interface MediaInput {
+        url: string;
         description: string;
-        source: string;
     }
 
     export interface DownloadInformation {
@@ -40,21 +40,13 @@ export namespace GooglePhotosUploadUtils {
         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 });
-        }
+    export const DispatchGooglePhotosUpload = async (url: string) => {
+        const body = await request(url, { encoding: null });
         const parameters = {
             method: 'POST',
             headers: {
                 ...headers('octet-stream'),
-                'X-Goog-Upload-File-Name': filename,
+                'X-Goog-Upload-File-Name': path.basename(url),
                 'X-Goog-Upload-Protocol': 'raw'
             },
             uri: prepend('uploads'),
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index f3c8cf82a..e67c4b5ba 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"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
+{"access_token":"ya29.Glx8B81Wqa67aMtB6AwlIUcLO4k0bnsICbtkXJUkqXWPIZgnSw0SnCG0jiFAmwLGPg8ca-Qk3R0SqWt4JlgwfrzuOqt90I0P8tHH2x_4RXfgisVBg4Muf8Gz59AEkA","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":1567878663996}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index fab00a02d..99d8a02d4 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -808,9 +808,9 @@ 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], { credentialsPath, tokenPath }).then(endpoint => {
+    let sector: GoogleApiServerUtils.Service = req.params.sector;
+    let action: GoogleApiServerUtils.Action = req.params.action;
+    GoogleApiServerUtils.GetEndpoint(sector, { credentialsPath, tokenPath }).then(endpoint => {
         let handler = EndpointHandlerMap.get(action);
         if (endpoint && handler) {
             let execute = handler(endpoint, req.body).then(
@@ -833,7 +833,7 @@ 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);
+        const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
         return !uploadToken ? undefined : {
             description: element.description,
             simpleMediaItem: { uploadToken }
diff --git a/tsconfig.json b/tsconfig.json
index 9ea91ec49..75541abca 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,7 @@
         "lib": [
             "dom",
             "es2015"
-        ],
+        ]
     },
     // "exclude": [
     //   "node_modules",
-- 
cgit v1.2.3-70-g09d2


From 32cd51e2bcc0a8cf498c0b31a5ead60802f672de Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 7 Sep 2019 17:08:49 -0400
Subject: working on import from google photos

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 123 +++++++++++++++++++--
 src/client/views/MainView.tsx                      |   4 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/server/RouteStore.ts                           |   3 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  |  14 +--
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  16 ++-
 7 files changed, 140 insertions(+), 24 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 924362c03..e8daf3dd4 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -6,9 +6,42 @@ import { Doc, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import requestImageSize = require('../../util/request-image-size');
 import Photos = require('googlephotos');
+import { RichTextField } from "../../../new_fields/RichTextField";
+import { RichTextUtils } from "../../../new_fields/RichTextUtils";
+import { EditorState } from "prosemirror-state";
+import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
 
 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'
+    }
+
+    export enum MediaType {
+        ALL_MEDIA = 'ALL_MEDIA',
+        PHOTO = 'PHOTO',
+        VIDEO = 'VIDEO'
+    }
+
     export type AlbumReference = { id: string } | { title: string };
     export const endpoint = () => fetch(Utils.prepend(RouteStore.googlePhotosAccessToken)).then(async response => new Photos(await response.text()));
 
@@ -17,26 +50,96 @@ export namespace GooglePhotosClientUtils {
         description: string;
     }
 
-    export const UploadMedia = async (sources: Doc[], album?: AlbumReference) => {
+    export const UploadImageDocuments = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
         if (album && "title" in album) {
-            album = (await endpoint()).albums.create(album.title);
+            album = await (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({
+            data && media.push({
                 url: data.url.href,
-                description,
-            } as MediaInput);
+                description: parseDescription(document, descriptionKey),
+            });
         });
         if (media.length) {
             return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
         }
-        return undefined;
+    };
+
+    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;
+    };
+
+    export interface DateRange {
+        after: Date;
+        before: Date;
+    }
+    export interface SearchOptions {
+        pageSize: number;
+        included: ContentCategories[];
+        excluded: ContentCategories[];
+        date: Opt<Date | DateRange>;
+        includeArchivedMedia: boolean;
+        type: MediaType;
+    }
+
+    const DefaultSearchOptions: SearchOptions = {
+        pageSize: 20,
+        included: [],
+        excluded: [],
+        date: undefined,
+        includeArchivedMedia: true,
+        type: MediaType.ALL_MEDIA
+    };
+
+    export interface SearchResponse {
+        mediaItems: any[];
+        nextPageToken: string;
+    }
+
+    export const Search = async (requested: Opt<Partial<SearchOptions>>) => {
+        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<any>((resolve, reject) => {
+            photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => {
+                if (!response) {
+                    return reject();
+                }
+                let filenames = await PostToServer(RouteStore.googlePhotosMediaDownload, response);
+                console.log(filenames);
+                resolve(filenames);
+            });
+        });
     };
 
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 7fe35494d..ee58c684a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -131,6 +131,8 @@ export class MainView extends React.Component {
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
         // this.executeGooglePhotosRoutine();
+        const imageTag = GooglePhotosClientUtils.ContentCategories;
+        GooglePhotosClientUtils.Search({ included: [imageTag.ANIMALS] });
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
@@ -155,7 +157,7 @@ export class MainView extends React.Component {
         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 }));
+        console.log(await GooglePhotosClientUtils.UploadImageDocuments([doc], { id: albumId }));
     }
 
     componentWillUnMount() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b8a034efc..4033ffd9c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -591,7 +591,7 @@ 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.UploadMedia([this.props.Document]), icon: "caret-square-right" });
+            cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImageDocuments([this.props.Document]), icon: "caret-square-right" });
         }
         let existingMake = ContextMenu.Instance.findByDescription("Make...");
         let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index b221b71bc..f65e6134c 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -33,6 +33,7 @@ export enum RouteStore {
     cognitiveServices = "/cognitiveservices",
     googleDocs = "/googleDocs",
     googlePhotosAccessToken = "/googlePhotosAccessToken",
-    googlePhotosMediaUpload = "/googlePhotosMediaUpload"
+    googlePhotosMediaUpload = "/googlePhotosMediaUpload",
+    googlePhotosMediaDownload = "/googlePhotosMediaDownload"
 
 }
\ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 13db1df03..032bc2a2d 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -20,6 +20,7 @@ export namespace GooglePhotosUploadUtils {
 
     export interface DownloadInformation {
         mediaPath: string;
+        fileName: string;
         contentType?: string;
         contentSize?: string;
     }
@@ -77,15 +78,9 @@ export namespace GooglePhotosUploadUtils {
 
     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;
-            }
-
+        export const Download = async (url: string, filename?: string): Promise<Opt<DownloadInformation>> => {
+            const resolved = filename || `upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
+            const mediaPath = Paths.uploadDirectory + resolved;
             return new Promise<DownloadInformation>((resolve, reject) => {
                 request.head(url, (error, res) => {
                     if (error) {
@@ -95,6 +90,7 @@ export namespace GooglePhotosUploadUtils {
                         mediaPath,
                         contentType: res.headers['content-type'],
                         contentSize: res.headers['content-length'],
+                        fileName: resolved
                     };
                     request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information));
                 });
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index e67c4b5ba..88838e18a 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx8B81Wqa67aMtB6AwlIUcLO4k0bnsICbtkXJUkqXWPIZgnSw0SnCG0jiFAmwLGPg8ca-Qk3R0SqWt4JlgwfrzuOqt90I0P8tHH2x_4RXfgisVBg4Muf8Gz59AEkA","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":1567878663996}
\ No newline at end of file
+{"access_token":"ya29.Glx8B266dydsOIEYhedUZYQ8sIsR9utSSxCBUex0O85zYrujZCSTbjVhrXF3Y4q41mLFghLwspgW-1w6zqnGnMtkZhuDGpBGArIwLZsJDyhUugEu3xvh7gY78WfePA","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":1567890805451}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 99d8a02d4..aadadb11a 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -113,7 +113,7 @@ function addSecureRoute(method: Method,
 ) {
     let abstracted = (req: express.Request, res: express.Response) => {
         if (req.user) {
-            handler(req.user as any, res, req);
+            handler(req.user, res, req);
         } else {
             req.session!.target = req.originalUrl;
             onRejection(res, req);
@@ -848,6 +848,20 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
     );
 });
 
+app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
+    const contents = req.body;
+    if (!contents) {
+        return res.send(undefined);
+    }
+    await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath });
+    let bundles: GooglePhotosUploadUtils.DownloadInformation[] = [];
+    await Promise.all(contents.mediaItems.forEach(async (item: any) => {
+        const information = await GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, item.filename);
+        information && bundles.push(information);
+    }));
+    res.send(bundles);
+});
+
 const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
     "number": "_n",
     "string": "_t",
-- 
cgit v1.2.3-70-g09d2


From c24f5f29dff8dd22f1d4029a2722ee4d1a725aad Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 7 Sep 2019 23:15:32 -0400
Subject: collection of search

---
 src/client/apis/google_docs/GooglePhotosClientUtils.ts | 14 +++++++-------
 src/client/views/MainView.tsx                          |  7 ++++---
 src/server/apis/google/GooglePhotosUploadUtils.ts      | 10 +++++-----
 src/server/credentials/google_docs_token.json          |  2 +-
 src/server/index.ts                                    | 10 ++++------
 5 files changed, 21 insertions(+), 22 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index e8daf3dd4..bb5d23971 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -10,6 +10,7 @@ import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from "../../../new_fields/RichTextUtils";
 import { EditorState } from "prosemirror-state";
 import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
+import { Docs } from "../../documents/Documents";
 
 export namespace GooglePhotosClientUtils {
 
@@ -130,14 +131,13 @@ export namespace GooglePhotosClientUtils {
 
         filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA));
 
-        return new Promise<any>((resolve, reject) => {
+        return new Promise<Doc>(resolve => {
             photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => {
-                if (!response) {
-                    return reject();
-                }
-                let filenames = await PostToServer(RouteStore.googlePhotosMediaDownload, response);
-                console.log(filenames);
-                resolve(filenames);
+                response && resolve(Docs.Create.StackingDocument((await PostToServer(RouteStore.googlePhotosMediaDownload, response)).map((download: any) => {
+                    let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileName}`));
+                    document.contentSize = download.contentSize;
+                    return document;
+                }), { width: 500, height: 500 }));
             });
         });
     };
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ee58c684a..b72df3715 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -131,8 +131,6 @@ export class MainView extends React.Component {
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
         // this.executeGooglePhotosRoutine();
-        const imageTag = GooglePhotosClientUtils.ContentCategories;
-        GooglePhotosClientUtils.Search({ included: [imageTag.ANIMALS] });
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
@@ -472,12 +470,15 @@ export class MainView extends React.Component {
         // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
         // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
 
-        let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc][] = [
+        let googlePhotosSearch = () => GooglePhotosClientUtils.Search({ included: [GooglePhotosClientUtils.ContentCategories.ANIMALS] });
+
+        let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [
             [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
             [React.createRef<HTMLDivElement>(), "tv", "Add Presentation Trail", addPresNode],
             [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
             [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
             [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
+            [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch],
             [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
             //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
         ];
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 032bc2a2d..9b3e68761 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -78,8 +78,8 @@ export namespace GooglePhotosUploadUtils {
 
     export namespace IOUtils {
 
-        export const Download = async (url: string, filename?: string): Promise<Opt<DownloadInformation>> => {
-            const resolved = filename || `upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
+        export const Download = async (url: string, filename?: string, prefix = ""): Promise<Opt<DownloadInformation>> => {
+            const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
             const mediaPath = Paths.uploadDirectory + resolved;
             return new Promise<DownloadInformation>((resolve, reject) => {
                 request.head(url, (error, res) => {
@@ -87,10 +87,10 @@ export namespace GooglePhotosUploadUtils {
                         return reject(error);
                     }
                     const information: DownloadInformation = {
-                        mediaPath,
-                        contentType: res.headers['content-type'],
+                        fileName: resolved,
                         contentSize: res.headers['content-length'],
-                        fileName: resolved
+                        contentType: res.headers['content-type'],
+                        mediaPath
                     };
                     request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information));
                 });
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 88838e18a..a1c23ea35 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx8B266dydsOIEYhedUZYQ8sIsR9utSSxCBUex0O85zYrujZCSTbjVhrXF3Y4q41mLFghLwspgW-1w6zqnGnMtkZhuDGpBGArIwLZsJDyhUugEu3xvh7gY78WfePA","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":1567890805451}
\ No newline at end of file
+{"access_token":"ya29.Glx9B3Fumh3qHpgasQvHNNrwNXtmTVWJR9XckFsnUjOswDOO91ccF3FhD4ko7Z-3rvxEljpP1Qj5BgNq305pt-pgIquoLPWYiaEtinHNF7IXGPz4s4raqJWEJPJxow","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":1567913435149}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index aadadb11a..49010e7e2 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -848,18 +848,16 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
     );
 });
 
+const prefix = "google_photos_";
 app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
     const contents = req.body;
     if (!contents) {
         return res.send(undefined);
     }
     await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath });
-    let bundles: GooglePhotosUploadUtils.DownloadInformation[] = [];
-    await Promise.all(contents.mediaItems.forEach(async (item: any) => {
-        const information = await GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, item.filename);
-        information && bundles.push(information);
-    }));
-    res.send(bundles);
+    res.send(await Promise.all(contents.mediaItems.map((item: any) =>
+        GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, undefined, prefix)))
+    );
 });
 
 const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
-- 
cgit v1.2.3-70-g09d2


From f18e2265e5d468f1cbf6e82dd5f01d5f5216b851 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 8 Sep 2019 04:16:04 -0400
Subject: factored out collection creation, sharp() resizers and image
 management

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  27 +++--
 src/client/documents/Documents.ts                  |   9 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  | 119 +++++++++++++++------
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  27 +++--
 6 files changed, 131 insertions(+), 55 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index bb5d23971..5f5b39b14 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,16 +1,16 @@
 import { PostToServer, Utils } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { ImageField } from "../../../new_fields/URLField";
-import { StrCast, Cast } from "../../../new_fields/Types";
+import { 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');
 import { RichTextField } from "../../../new_fields/RichTextField";
 import { RichTextUtils } from "../../../new_fields/RichTextUtils";
 import { EditorState } from "prosemirror-state";
 import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
-import { Docs } from "../../documents/Documents";
+import { Docs, DocumentOptions } from "../../documents/Documents";
+import { type } from "os";
 
 export namespace GooglePhotosClientUtils {
 
@@ -98,7 +98,7 @@ export namespace GooglePhotosClientUtils {
         excluded: [],
         date: undefined,
         includeArchivedMedia: true,
-        type: MediaType.ALL_MEDIA
+        type: MediaType.ALL_MEDIA,
     };
 
     export interface SearchResponse {
@@ -106,7 +106,18 @@ export namespace GooglePhotosClientUtils {
         nextPageToken: string;
     }
 
-    export const Search = async (requested: Opt<Partial<SearchOptions>>) => {
+    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 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);
@@ -133,11 +144,7 @@ export namespace GooglePhotosClientUtils {
 
         return new Promise<Doc>(resolve => {
             photos.mediaItems.search(filters, options.pageSize || 20).then(async (response: SearchResponse) => {
-                response && resolve(Docs.Create.StackingDocument((await PostToServer(RouteStore.googlePhotosMediaDownload, response)).map((download: any) => {
-                    let document = Docs.Create.ImageDocument(Utils.prepend(`/files/${download.fileName}`));
-                    document.contentSize = download.contentSize;
-                    return document;
-                }), { width: 500, height: 500 }));
+                response && resolve(await PostToServer(RouteStore.googlePhotosMediaDownload, response));
             });
         });
     };
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4b7f1eeb6..9bac57d16 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -21,7 +21,7 @@ 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 } from "../../Utils";
+import { OmitKeys, JSONUtils, Utils } from "../../Utils";
 import { ImageField, VideoField, AudioField, PdfField, WebField, YoutubeField } from "../../new_fields/URLField";
 import { HtmlField } from "../../new_fields/HtmlField";
 import { List } from "../../new_fields/List";
@@ -332,7 +332,12 @@ export namespace Docs {
         export function ImageDocument(url: string, options: DocumentOptions = {}) {
             let imgField = new ImageField(new URL(url));
             let inst = InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: path.basename(url), ...options });
-            requestImageSize(imgField.url.href)
+            let target = imgField.url.href;
+            if (new RegExp(window.location.origin).test(target)) {
+                let extension = path.extname(target);
+                target = `${target.substring(0, target.length - extension.length)}_o${extension}`;
+            }
+            requestImageSize(target)
                 .then((size: any) => {
                     let aspect = size.height / size.width;
                     if (!inst.proto!.nativeWidth) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b72df3715..326c13424 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -470,7 +470,7 @@ export class MainView extends React.Component {
         // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
         // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
 
-        let googlePhotosSearch = () => GooglePhotosClientUtils.Search({ included: [GooglePhotosClientUtils.ContentCategories.ANIMALS] });
+        let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] });
 
         let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [
             [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 9b3e68761..5ac3eaef7 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -4,6 +4,9 @@ import * as fs from 'fs';
 import { Utils } from '../../../Utils';
 import * as path from 'path';
 import { Opt } from '../../../new_fields/Doc';
+import * as sharp from 'sharp';
+
+const uploadDirectory = path.join(__dirname, "../../public/files/");
 
 export namespace GooglePhotosUploadUtils {
 
@@ -18,13 +21,6 @@ export namespace GooglePhotosUploadUtils {
         description: string;
     }
 
-    export interface DownloadInformation {
-        mediaPath: string;
-        fileName: string;
-        contentType?: string;
-        contentSize?: string;
-    }
-
     const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`;
     const headers = (type: string) => ({
         'Content-Type': `application/${type}`,
@@ -76,35 +72,92 @@ export namespace GooglePhotosUploadUtils {
         });
     };
 
-    export namespace IOUtils {
+}
 
-        export const Download = async (url: string, filename?: string, prefix = ""): Promise<Opt<DownloadInformation>> => {
-            const resolved = filename || `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
-            const mediaPath = Paths.uploadDirectory + resolved;
-            return new Promise<DownloadInformation>((resolve, reject) => {
-                request.head(url, (error, res) => {
-                    if (error) {
-                        return reject(error);
-                    }
-                    const information: DownloadInformation = {
-                        fileName: resolved,
-                        contentSize: res.headers['content-length'],
-                        contentType: res.headers['content-type'],
-                        mediaPath
-                    };
-                    request(url).pipe(fs.createWriteStream(mediaPath)).on('close', () => resolve(information));
-                });
-            });
-        };
+export namespace DownloadUtils {
 
-        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 interface Size {
+        width: number;
+        suffix: string;
+    }
 
-        export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
+    export const Sizes: { [size: string]: Size } = {
+        SMALL: { width: 100, suffix: "_s" },
+        MEDIUM: { width: 400, suffix: "_m" },
+        LARGE: { width: 900, suffix: "_l" },
+    };
+
+    const png = ".png";
+    const pngs = [".png", ".PNG"];
+    const jpg = [".jpg", ".JPG", ".jpeg", ".JPEG"];
+    const size = "content-length";
+    const type = "content-type";
+
+    export interface DownloadInformation {
+        mediaPaths: string[];
+        fileNames: { [key: string]: string };
+        contentSize?: string;
+        contentType?: string;
     }
 
+    const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${path.extname(url).toLowerCase()}`;
+    const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
+
+    export const Download = async (url: string, filename?: string, prefix = ""): Promise<Opt<DownloadInformation>> => {
+        const resolved = filename ? sanitize(filename) : generate(prefix, url);
+        const extension = path.extname(url) || path.extname(resolved) || png;
+        return new Promise<DownloadInformation>((resolve, reject) => {
+            request.head(url, async (error, res) => {
+                if (error) {
+                    return reject(error);
+                }
+                const information: DownloadInformation = {
+                    fileNames: { clean: resolved },
+                    contentSize: res.headers[size],
+                    contentType: res.headers[type],
+                    mediaPaths: []
+                };
+                const resizers = [
+                    { resizer: sharp().rotate(), suffix: "_o" },
+                    ...Object.values(Sizes).map(size => ({
+                        resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
+                        suffix: size.suffix
+                    }))
+                ];
+                let validated = true;
+                if (pngs.includes(extension)) {
+                    resizers.forEach(element => element.resizer = element.resizer.png());
+                } else if (jpg.includes(extension)) {
+                    resizers.forEach(element => element.resizer = element.resizer.jpeg());
+                } else {
+                    validated = false;
+                }
+                if (validated) {
+                    for (let resizer of resizers) {
+                        const suffix = resizer.suffix;
+                        let mediaPath: string;
+                        await new Promise<void>(resolve => {
+                            const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
+                            information.mediaPaths.push(mediaPath = uploadDirectory + filename);
+                            information.fileNames[suffix] = filename;
+                            request(url)
+                                .pipe(resizer.resizer)
+                                .pipe(fs.createWriteStream(mediaPath))
+                                .on('close', resolve);
+                        });
+                    }
+                    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/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index a1c23ea35..4f911f7e0 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx9B3Fumh3qHpgasQvHNNrwNXtmTVWJR9XckFsnUjOswDOO91ccF3FhD4ko7Z-3rvxEljpP1Qj5BgNq305pt-pgIquoLPWYiaEtinHNF7IXGPz4s4raqJWEJPJxow","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":1567913435149}
\ No newline at end of file
+{"access_token":"ya29.Glx9B3_l5JbKNtvzx378Nsz917bP-OTKf6VZzc2K8QDBm-Y0j_-c8v7bL8LCEM3wF8d7JauF-5Z4Uq4v7wPwUQUlDO1uPyoHeSF6iz98xkgJr9OW4KzJo2Ij722gpQ","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":1567931641928}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 49010e7e2..013345a76 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -42,7 +42,7 @@ 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 { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
+import { GooglePhotosUploadUtils, DownloadUtils } from './apis/google/GooglePhotosUploadUtils';
 const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
 const probe = require("probe-image-size");
@@ -154,6 +154,11 @@ app.get("/buxton", (req, res) => {
     command_line('python scraper.py', cwd).then(onResolved, tryPython3);
 });
 
+const STATUS = {
+    OK: 200,
+    BAD_REQUEST: 400
+};
+
 const command_line = (command: string, fromDirectory?: string) => {
     return new Promise<string>((resolve, reject) => {
         let options: ExecOptions = {};
@@ -848,16 +853,22 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
     );
 });
 
+interface MediaItem {
+    baseUrl: string;
+    filename: string;
+}
 const prefix = "google_photos_";
+
 app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
-    const contents = req.body;
-    if (!contents) {
-        return res.send(undefined);
+    const contents: { mediaItems: MediaItem[] } = req.body;
+    if (contents) {
+        const downloads = contents.mediaItems.map(item =>
+            DownloadUtils.Download(item.baseUrl, item.filename, prefix)
+        );
+        res.status(STATUS.OK).send(await Promise.all(downloads));
+        return;
     }
-    await GooglePhotosUploadUtils.initialize({ uploadDirectory, credentialsPath, tokenPath });
-    res.send(await Promise.all(contents.mediaItems.map((item: any) =>
-        GooglePhotosUploadUtils.IOUtils.Download(item.baseUrl, undefined, prefix)))
-    );
+    res.status(STATUS.BAD_REQUEST).send();
 });
 
 const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
-- 
cgit v1.2.3-70-g09d2


From b24c475d8cd36af860fc374b0c5621b0d096be1d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Mon, 9 Sep 2019 20:47:34 -0400
Subject: nearly finished transferring images between text notes and google
 docs

---
 package.json                                       |   2 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  23 ++--
 .../util/Import & Export/DirectoryImportBox.tsx    |   2 +-
 src/client/views/MainView.tsx                      |   6 +-
 src/client/views/collections/CollectionSubView.tsx |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/new_fields/RichTextUtils.ts                    |  67 ++++++++++-
 src/server/RouteStore.ts                           |   3 +-
 src/server/apis/google/GoogleApiServerUtils.ts     |   2 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  |   5 +
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  20 +++-
 src/server/updateSearch.ts                         | 123 ---------------------
 13 files changed, 112 insertions(+), 147 deletions(-)
 delete mode 100644 src/server/updateSearch.ts

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

diff --git a/package.json b/package.json
index f56e34ce0..f0f2b467e 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 5f5b39b14..fddcf3aa5 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -51,17 +51,26 @@ export namespace GooglePhotosClientUtils {
         description: string;
     }
 
-    export const UploadImageDocuments = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
+    export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => {
         if (album && "title" in album) {
             album = await (await endpoint()).albums.create(album.title);
         }
         const media: MediaInput[] = [];
-        sources.forEach(document => {
-            const data = Cast(Doc.GetProto(document).data, ImageField);
-            data && media.push({
-                url: data.url.href,
-                description: parseDescription(document, descriptionKey),
-            });
+        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 });
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 7693a388f..a19fd39b7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -117,7 +117,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`);
         }));
 
-        await GooglePhotosClientUtils.UploadImageDocuments(docs, { title: directory });
+        await GooglePhotosClientUtils.UploadImages(docs, { title: directory });
         console.log("Finished upload!");
 
         for (let i = 0; i < docs.length; i++) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 326c13424..0c0ed9072 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -155,7 +155,7 @@ export class MainView extends React.Component {
         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.UploadImageDocuments([doc], { id: albumId }));
+        console.log(await GooglePhotosClientUtils.UploadImages([doc], { id: albumId }));
     }
 
     componentWillUnMount() {
@@ -470,7 +470,7 @@ export class MainView extends React.Component {
         // let youtubeurl = "https://www.youtube.com/embed/TqcApsGRzWw";
         // let addYoutubeSearcher = action(() => Docs.Create.YoutubeDocument(youtubeurl, { width: 600, height: 600, title: "youtube search" }));
 
-        let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] });
+        // let googlePhotosSearch = () => GooglePhotosClientUtils.CollectionFromSearch(Docs.Create.MasonryDocument, { included: [GooglePhotosClientUtils.ContentCategories.LANDSCAPES] });
 
         let btns: [React.RefObject<HTMLDivElement>, IconName, string, () => Doc | Promise<Doc>][] = [
             [React.createRef<HTMLDivElement>(), "object-group", "Add Collection", addColNode],
@@ -478,7 +478,7 @@ export class MainView extends React.Component {
             [React.createRef<HTMLDivElement>(), "globe-asia", "Add Website", addWebNode],
             [React.createRef<HTMLDivElement>(), "bolt", "Add Button", addButtonDocument],
             [React.createRef<HTMLDivElement>(), "file", "Add Document Dragger", addDragboxNode],
-            [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch],
+            // [React.createRef<HTMLDivElement>(), "object-group", "Test Google Photos Search", googlePhotosSearch],
             [React.createRef<HTMLDivElement>(), "cloud-upload-alt", "Import Directory", addImportCollectionNode], //remove at some point in favor of addImportCollectionNode
             //[React.createRef<HTMLDivElement>(), "play", "Add Youtube Searcher", addYoutubeSearcher],
         ];
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 99e5ab7b3..5fc4f36a7 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -253,7 +253,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                     }).then(async (res: Response) => {
                         (await res.json()).map(action((file: any) => {
                             let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
-                            let path = Utils.prepend(file);
+                            let path = Utils.prepend(file.path);
                             Docs.Get.DocumentFromType(type, path, full).then(doc => doc && this.props.addDocument(doc));
                         }));
                     });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4033ffd9c..cb9346a8b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -591,7 +591,7 @@ 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.UploadImageDocuments([this.props.Document]), icon: "caret-square-right" });
+            cm.addItem({ description: "Export to Google Photos", event: () => GooglePhotosClientUtils.UploadImages([this.props.Document]), icon: "caret-square-right" });
         }
         let existingMake = ContextMenu.Instance.findByDescription("Make...");
         let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 0aba50c0d..500b93676 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -7,6 +7,11 @@ import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
 import { Opt } from "./Doc";
 import * as Color from "color";
 import { sinkListItem } from "prosemirror-schema-list";
+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";
 
 export namespace RichTextUtils {
 
@@ -86,19 +91,36 @@ export namespace RichTextUtils {
     export namespace GoogleDocs {
 
         export const Export = (state: EditorState): GoogleApiClientUtils.Docs.Content => {
-            let textNodes: Node<any>[] = [];
+            let nodes: { [type: string]: Node<any>[] } = {
+                text: [],
+                image: []
+            };
             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);
+            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 = ExtractImages(nodes.image);
             return {
                 text,
-                requests: [...linkRequests]
+                requests: [...linkRequests, ...imageRequests]
             };
         };
 
         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>> => {
             const document = await GoogleApiClientUtils.Docs.retrieve({ documentId });
             if (!document) {
@@ -109,6 +131,17 @@ export namespace RichTextUtils {
             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[] = [];
@@ -151,6 +184,12 @@ 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 })));
+            }
+
             return { title, text, state };
         };
 
@@ -252,6 +291,26 @@ export namespace RichTextUtils {
             return links;
         };
 
+        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 result = (await GooglePhotosClientUtils.UploadImages([uri])).newMediaItemResults;
+                images.push({
+                    insertInlineImage: {
+                        uri: result[0].mediaItem.productUrl,
+                        objectSize: { width: { magnitude: parseFloat(attrs.width.replace("px", "")), unit: "PT" } },
+                        location: { index: position + length }
+                    }
+                });
+                position += length;
+            }
+            return images;
+        };
+
         const Encode = (information: LinkInformation) => {
             return {
                 updateTextStyle: {
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index f65e6134c..ee9cd8a0e 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -34,6 +34,7 @@ export enum RouteStore {
     googleDocs = "/googleDocs",
     googlePhotosAccessToken = "/googlePhotosAccessToken",
     googlePhotosMediaUpload = "/googlePhotosMediaUpload",
-    googlePhotosMediaDownload = "/googlePhotosMediaDownload"
+    googlePhotosMediaDownload = "/googlePhotosMediaDownload",
+    googleDocsGet = "/googleDocsGet"
 
 }
\ No newline at end of file
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index ac8023ce1..e0bd8a800 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -42,7 +42,7 @@ export namespace GoogleApiServerUtils {
 
     export type ApiResponse = Promise<GaxiosResponse>;
     export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse;
-    export type ApiHandler = (parameters: any) => ApiResponse;
+    export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse;
     export type Action = "create" | "retrieve" | "update";
 
     export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler };
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 35f986250..d1f1f81bd 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -151,6 +151,11 @@ export namespace DownloadUtils {
                         .on('close', resolve)
                         .on('error', reject);
                 });
+                if (!isLocal) {
+                    await new Promise<void>(resolve => {
+                        stream(url).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+                    });
+                }
             }
             resolve(information);
         });
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index fabc18cfd..5c142fba1 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.Glx-BwgWcpQUukTNyuUqvSAYrDyxDNUhCLtrFDJAViROvicm0DrcRvCn4OaQdn2m2IZQYcG-19cvQYoOC3UJCtWXLRvKZzQCbZZSykpxYu_lflUyEnIGZOIHMbbEjA","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":1568008211814}
\ No newline at end of file
+{"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
diff --git a/src/server/index.ts b/src/server/index.ts
index baef94a59..8469770d5 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -38,6 +38,7 @@ import flash = require('connect-flash');
 import { Search } from './Search';
 import _ = require('lodash');
 import * as Archiver from 'archiver';
+import * as request_promise from 'request-promise';
 var AdmZip = require('adm-zip');
 import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
 import { Response } from 'express-serve-static-core';
@@ -576,9 +577,9 @@ app.post(
         form.parse(req, async (_err, _fields, files) => {
             let results: FileResponse[] = [];
             for (const key in files) {
-                const { name, type, path: location } = files[key];
+                const { type, path: location, name } = files[key];
                 const filename = path.basename(location);
-                await UploadUtils.UploadImage(uploadDirectory + filename, path.basename(name));
+                await UploadUtils.UploadImage(uploadDirectory + filename, filename);
                 results.push({ name, type, path: `/files/${filename}` });
                 console.log(path.basename(name));
             }
@@ -790,10 +791,23 @@ const tokenPath = path.join(__dirname, "./credentials/google_docs_token.json");
 
 const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
     ["create", (api, params) => api.create(params)],
-    ["retrieve", (api, params) => api.get(params)],
+    ["retrieve", (api, params) => api.get(params, { params: "fields=inlineObjects" })],
     ["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;
diff --git a/src/server/updateSearch.ts b/src/server/updateSearch.ts
deleted file mode 100644
index 906b795f1..000000000
--- a/src/server/updateSearch.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-import { Database } from "./database";
-import { Cursor } from "mongodb";
-import { Search } from "./Search";
-import pLimit from 'p-limit';
-
-const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
-    "number": "_n",
-    "string": "_t",
-    "boolean": "_b",
-    // "image": ["_t", "url"],
-    "video": ["_t", "url"],
-    "pdf": ["_t", "url"],
-    "audio": ["_t", "url"],
-    "web": ["_t", "url"],
-    "date": ["_d", value => new Date(value.date).toISOString()],
-    "proxy": ["_i", "fieldId"],
-    "list": ["_l", list => {
-        const results = [];
-        for (const value of list.fields) {
-            const term = ToSearchTerm(value);
-            if (term) {
-                results.push(term.value);
-            }
-        }
-        return results.length ? results : null;
-    }]
-};
-
-function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
-    if (val === null || val === undefined) {
-        return;
-    }
-    const type = val.__type || typeof val;
-    let suffix = suffixMap[type];
-    if (!suffix) {
-        return;
-    }
-
-    if (Array.isArray(suffix)) {
-        const accessor = suffix[1];
-        if (typeof accessor === "function") {
-            val = accessor(val);
-        } else {
-            val = val[accessor];
-        }
-        suffix = suffix[0];
-    }
-
-    return { suffix, value: val };
-}
-
-function getSuffix(value: string | [string, any]): string {
-    return typeof value === "string" ? value : value[0];
-}
-
-const limit = pLimit(5);
-async function update() {
-    // await new Promise(res => setTimeout(res, 5));
-    console.log("update");
-    await Search.Instance.clear();
-    const cursor = await Database.Instance.query({});
-    console.log("Cleared");
-    const updates: any[] = [];
-    let numDocs = 0;
-    function updateDoc(doc: any) {
-        numDocs++;
-        if ((numDocs % 50) === 0) {
-            console.log("updateDoc " + numDocs);
-        }
-        // console.log("doc " + numDocs);
-        if (doc.__type !== "Doc") {
-            return;
-        }
-        const fields = doc.fields;
-        if (!fields) {
-            return;
-        }
-        const update: any = { id: doc._id };
-        let dynfield = false;
-        for (const key in fields) {
-            const value = fields[key];
-            const term = ToSearchTerm(value);
-            if (term !== undefined) {
-                let { suffix, value } = term;
-                update[key + suffix] = value;
-                dynfield = true;
-            }
-        }
-        if (dynfield) {
-            updates.push(update);
-            // console.log(updates.length);
-        }
-    }
-    await cursor.forEach(updateDoc);
-    console.log(`Updating ${updates.length} documents`);
-    const result = await Search.Instance.updateDocuments(updates);
-    try {
-        console.log(JSON.parse(result).responseHeader.status);
-    } catch {
-        console.log("Error:");
-        // console.log(updates[i]);
-        console.log(result);
-        console.log("\n");
-    }
-    // for (let i = 0; i < updates.length; i++) {
-    //     console.log(i);
-    //     const result = await Search.Instance.updateDocument(updates[i]);
-    // try {
-    //     console.log(JSON.parse(result).responseHeader.status);
-    // } catch {
-    //     console.log("Error:");
-    //     console.log(updates[i]);
-    //     console.log(result);
-    //     console.log("\n");
-    // }
-    // }
-    // await Promise.all(updates.map(update => {
-    //     return limit(() => Search.Instance.updateDocument(update));
-    // }));
-    cursor.close();
-}
-
-update();
\ No newline at end of file
-- 
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/MainView.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 5a0794a74ce62612435133907395482f494747f4 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 11 Sep 2019 12:19:30 -0400
Subject: now support auto tagging

---
 .../apis/google_docs/GooglePhotosClientUtils.ts    | 126 ++++++++++++++-------
 .../util/Import & Export/DirectoryImportBox.tsx    |   7 +-
 src/client/views/MainView.tsx                      |  19 ++--
 src/client/views/nodes/DocumentView.tsx            |   3 +-
 src/new_fields/RichTextUtils.ts                    |   2 +-
 .../apis/google/CustomizedWrapper/filters.js       |  46 ++++++++
 src/server/apis/google/GooglePhotosUploadUtils.ts  |  34 ++++--
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |   2 +-
 9 files changed, 168 insertions(+), 73 deletions(-)
 create mode 100644 src/server/apis/google/CustomizedWrapper/filters.js

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index b1de24d1a..a28b183d1 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -10,7 +10,10 @@ import { RichTextUtils } from "../../../new_fields/RichTextUtils";
 import { EditorState } from "prosemirror-state";
 import { FormattedTextBox } from "../../views/nodes/FormattedTextBox";
 import { Docs, DocumentOptions } from "../../documents/Documents";
-import { MediaItemCreationResult, NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
+import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
+import { AssertionError } from "assert";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
 
 export namespace GooglePhotos {
 
@@ -53,7 +56,14 @@ export namespace GooglePhotos {
         PERFORMANCES: 'PERFORMANCES',
         WHITEBOARDS: 'WHITEBOARDS',
         SCREENSHOTS: 'SCREENSHOTS',
-        UTILITY: 'UTILITY'
+        UTILITY: 'UTILITY',
+        ARTS: 'ARTS',
+        CRAFTS: 'CRAFTS',
+        FASHION: 'FASHION',
+        HOUSES: 'HOUSES',
+        GARDENS: 'GARDENS',
+        FLOWERS: 'FLOWERS',
+        HOLIDAYS: 'HOLIDAYS'
     };
 
     export namespace Export {
@@ -63,7 +73,15 @@ export namespace GooglePhotos {
             mediaItems: MediaItem[];
         }
 
-        export const CollectionToAlbum = async (collection: Doc, title?: string, descriptionKey?: string): Promise<Opt<AlbumCreationResult>> => {
+        export interface AlbumCreationOptions {
+            collection: Doc;
+            title?: string;
+            descriptionKey?: string;
+            tag?: boolean;
+        }
+
+        export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => {
+            const { collection, title, descriptionKey, tag } = options;
             const dataDocument = Doc.GetProto(collection);
             const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
             if (!images || !images.length) {
@@ -71,9 +89,24 @@ export namespace GooglePhotos {
             }
             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);
+            const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey);
+            if (newMediaItemResults) {
+                const mediaItems = newMediaItemResults.map(item => item.mediaItem);
+                if (mediaItems.length !== images.length) {
+                    throw new AssertionError({ actual: mediaItems.length, expected: images.length });
+                }
+                const idMapping = new Doc;
+                for (let i = 0; i < images.length; i++) {
+                    const image = images[i];
+                    const mediaItem = mediaItems[i];
+                    image.googlePhotosId = mediaItem.id;
+                    image.googlePhotosUrl = mediaItem.baseUrl || mediaItem.productUrl;
+                    idMapping[mediaItem.id] = image;
+                }
+                collection.googlePhotosIdMapping = idMapping;
+                if (tag) {
+                    await Query.AppendImageMetadata(collection);
+                }
                 return { albumId: id, mediaItems };
             }
         };
@@ -101,21 +134,32 @@ export namespace GooglePhotos {
 
     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);
+        export const AppendImageMetadata = async (collection: Doc) => {
+            const idMapping = await Cast(collection.googlePhotosIdMapping, Doc);
+            if (!idMapping) {
+                throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!");
+            }
+            const images = await DocListCastAsync(collection.data);
+            images && images.forEach(image => image.googlePhotosTags = new List());
+            const values = Object.values(ContentCategories);
+            for (let value of values) {
+                console.log("Searching for ", value);
+                const results = await Search({ included: [value] });
+                if (results.mediaItems) {
+                    console.log(`${results.mediaItems.length} found!`);
+                    const ids = results.mediaItems.map(item => item.id);
+                    for (let id of ids) {
+                        const image = await Cast(idMapping[id], Doc);
+                        if (image) {
+                            const tags = Cast(image.googlePhotosTags, listSpec("string"))!;
+                            if (!tags.includes(value)) {
+                                tags.push(value);
+                                console.log(`${value}: ${id}`);
+                            }
+                        }
                     }
                 }
-                //...
-                included = excluded = [];
+                console.log();
             }
         };
 
@@ -125,20 +169,22 @@ export namespace GooglePhotos {
         }
 
         const DefaultSearchOptions: SearchOptions = {
-            pageSize: 20,
+            pageSize: 50,
             included: [],
             excluded: [],
             date: undefined,
             includeArchivedMedia: true,
+            excludeNonAppCreatedData: false,
             type: MediaType.ALL_MEDIA,
         };
 
         export interface SearchOptions {
             pageSize: number;
-            included: ContentCategories[];
-            excluded: ContentCategories[];
+            included: string[];
+            excluded: string[];
             date: Opt<Date | DateRange>;
             includeArchivedMedia: boolean;
+            excludeNonAppCreatedData: boolean;
             type: MediaType;
         }
 
@@ -173,7 +219,7 @@ export namespace GooglePhotos {
             filters.setMediaTypeFilter(new photos.MediaTypeFilter(options.type || MediaType.ALL_MEDIA));
 
             return new Promise<SearchResponse>(resolve => {
-                photos.mediaItems.search(filters, options.pageSize || 20).then(resolve);
+                photos.mediaItems.search(filters, options.pageSize || 100).then(resolve);
             });
         };
 
@@ -183,7 +229,7 @@ export namespace GooglePhotos {
 
     }
 
-    export namespace Create {
+    namespace Create {
 
         export const Album = async (title: string) => {
             return (await endpoint()).albums.create(title);
@@ -211,40 +257,34 @@ export namespace GooglePhotos {
             return uploads;
         };
 
-        export const UploadThenFetch = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption") => {
-            const result = await UploadImages(sources, album, descriptionKey);
-            if (!result) {
+        export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
+            const newMediaItems = await UploadImages(sources, album, descriptionKey);
+            if (!newMediaItems) {
                 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)));
+            const baseUrls: string[] = await Promise.all(newMediaItems.map(item => {
+                return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
             }));
             return baseUrls;
         };
 
-        export const UploadImages = async (sources: (Doc | string)[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<MediaItemCreationResult>> => {
+        export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<NewMediaItemResult[]>> => {
             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();
+                const data = Cast(Doc.GetProto(source).data, ImageField);
+                if (!data) {
+                    return;
                 }
+                const url = data.url.href;
+                const description = parseDescription(source, descriptionKey);
                 media.push({ url, description });
             });
             if (media.length) {
-                return PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+                const uploads: NewMediaItemResult[] = await PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+                return uploads;
             }
         };
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index d58c02ce5..348f216a5 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -74,13 +74,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
         runInAction(() => this.uploading = true);
 
-        let promises: Promise<void>[] = [];
         let docs: Doc[] = [];
 
         let files = e.target.files;
         if (!files || files.length === 0) return;
 
-        let directory = (files.item(0) as any).webkitRelativePath.split("/", 1);
+        let directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
 
         let validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
@@ -117,8 +116,6 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             console.log(`(${this.quota - this.remaining}/${this.quota}) ${upload.name}`);
         }));
 
-        await GooglePhotos.Transactions.UploadImages(docs, { title: directory });
-
         for (let i = 0; i < docs.length; i++) {
             let doc = docs[i];
             doc.size = sizes[i];
@@ -142,11 +139,11 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         let parent = this.props.ContainingCollectionView;
         if (parent) {
             let importContainer = Docs.Create.StackingDocument(docs, options);
+            await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
             importContainer.singleColumn = false;
             Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
             !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
             DocumentManager.Instance.jumpToDocument(importContainer, true);
-
         }
 
         runInAction(() => {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8d10a91ce..28edf181b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -130,7 +130,7 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
-        // this.executeGooglePhotosRoutine();
+        this.executeGooglePhotosRoutine();
 
         reaction(() => {
             let workspaces = CurrentUserUtils.UserDocument.workspaces;
@@ -149,14 +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 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 }));
-    // }
+    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 }));
+        GooglePhotos.Query.Search({ included: [GooglePhotos.ContentCategories.ANIMALS] }).then(console.log);
+    }
 
     componentWillUnMount() {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a51f783ad..a38f42751 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -594,7 +594,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             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" });
+            cm.addItem({ description: "Export to Google Photos Album", event: () => GooglePhotos.Export.CollectionToAlbum({ collection: this.props.Document }).then(console.log), icon: "caret-square-right" });
+            cm.addItem({ description: "Tag Child Images via Google Photos", event: () => GooglePhotos.Query.AppendImageMetadata(this.props.Document), icon: "caret-square-right" });
         }
         let existingMake = ContextMenu.Instance.findByDescription("Make...");
         let makes: ContextMenuProps[] = existingMake && "subitems" in existingMake ? existingMake.subitems : [];
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 27737782b..6afe4ddfd 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -298,7 +298,7 @@ export namespace RichTextUtils {
                 const length = node.nodeSize;
                 const attrs = node.attrs;
                 const uri = attrs.src;
-                const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([uri]);
+                const baseUrls = await GooglePhotos.Transactions.UploadThenFetch([Docs.Create.ImageDocument(uri)]);
                 if (!baseUrls) {
                     continue;
                 }
diff --git a/src/server/apis/google/CustomizedWrapper/filters.js b/src/server/apis/google/CustomizedWrapper/filters.js
new file mode 100644
index 000000000..576a90b75
--- /dev/null
+++ b/src/server/apis/google/CustomizedWrapper/filters.js
@@ -0,0 +1,46 @@
+'use strict';
+
+const DateFilter = require('../common/date_filter');
+const MediaTypeFilter = require('./media_type_filter');
+const ContentFilter = require('./content_filter');
+
+class Filters {
+    constructor(includeArchivedMedia = false) {
+        this.includeArchivedMedia = includeArchivedMedia;
+    }
+
+    setDateFilter(dateFilter) {
+        this.dateFilter = dateFilter;
+        return this;
+    }
+
+    setContentFilter(contentFilter) {
+        this.contentFilter = contentFilter;
+        return this;
+    }
+
+    setMediaTypeFilter(mediaTypeFilter) {
+        this.mediaTypeFilter = mediaTypeFilter;
+        return this;
+    }
+
+    setIncludeArchivedMedia(includeArchivedMedia) {
+        this.includeArchivedMedia = includeArchivedMedia;
+        return this;
+    }
+
+    toJSON() {
+        return {
+            dateFilter: this.dateFilter instanceof DateFilter ? this.dateFilter.toJSON() : this.dateFilter,
+            mediaTypeFilter: this.mediaTypeFilter instanceof MediaTypeFilter ?
+                this.mediaTypeFilter.toJSON() :
+                this.mediaTypeFilter,
+            contentFilter: this.contentFilter instanceof ContentFilter ?
+                this.contentFilter.toJSON() :
+                this.contentFilter,
+            includeArchivedMedia: this.includeArchivedMedia
+        };
+    }
+}
+
+module.exports = Filters;
\ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 447ed23ac..1a8adc836 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -5,7 +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';
+import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
 
 const uploadDirectory = path.join(__dirname, "../../public/files/");
 
@@ -55,24 +55,34 @@ export namespace GooglePhotosUploadUtils {
 
 
 
-    export const CreateMediaItems = (newMediaItems: any[], album?: { id: string }): Promise<MediaItemCreationResult> => {
-        return new Promise<MediaItemCreationResult>((resolve, reject) => {
+    export const CreateMediaItems = async (newMediaItems: any[], album?: { id: string }): Promise<MediaItemCreationResult> => {
+        const quota = newMediaItems.length;
+        let handled = 0;
+        const newMediaItemResults: NewMediaItemResult[] = [];
+        while (handled < quota) {
+            const cap = Math.min(newMediaItems.length, handled + 50);
+            const batch = newMediaItems.slice(handled, cap);
+            console.log(batch.length);
             const parameters = {
                 method: 'POST',
                 headers: headers('json'),
                 uri: prepend('mediaItems:batchCreate'),
-                body: { newMediaItems } as any,
+                body: { newMediaItems: batch } as any,
                 json: true
             };
             album && (parameters.body.albumId = album.id);
-            request(parameters, (error, _response, body) => {
-                if (error) {
-                    reject(error);
-                } else {
-                    resolve(body);
-                }
-            });
-        });
+            newMediaItemResults.push(...(await new Promise<MediaItemCreationResult>((resolve, reject) => {
+                request(parameters, (error, _response, body) => {
+                    if (error) {
+                        reject(error);
+                    } else {
+                        resolve(body);
+                    }
+                });
+            })).newMediaItemResults);
+            handled = cap;
+        }
+        return { newMediaItemResults };
     };
 
 }
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 22d57d744..0c06f68b7 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"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
+{"access_token":"ya29.GlyAB7VxfbK7fwV9-lqu9NZ1-p73aC8KaEXAYGHFOIIgAhx40CCUgS07vy485y7O0x9RwK-7FL6P547SscD5bVlTlJkclP-9uupKxDaeez7Tc7o2pJwt6bgJlbbw7w","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":1568220636395}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 2c3e76c55..507463841 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -831,7 +831,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
         return _error(res, tokenError);
     }
     GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
-        mediaItems => _success(res, mediaItems),
+        result => _success(res, result.newMediaItemResults),
         error => _error(res, mediaError, error)
     );
 });
-- 
cgit v1.2.3-70-g09d2


From cbb016dd4bec4ce1367314717adf85640ae51c93 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Thu, 12 Sep 2019 05:21:29 -0400
Subject: sharing workflow supported

---
 .vscode/launch.json                                |   5 +-
 src/client/DocServer.ts                            |  10 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |   3 +-
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/DictationManager.ts                |  46 +-
 src/client/util/History.ts                         |   6 +-
 src/client/util/SharingManager.scss                | 136 +++
 src/client/util/SharingManager.tsx                 | 293 ++++++
 src/client/views/GlobalKeyHandler.ts               |   2 +
 src/client/views/InkingCanvas.scss                 |   2 +-
 src/client/views/Main.scss                         |  40 -
 src/client/views/Main.tsx                          |  16 +-
 src/client/views/MainView.tsx                      | 272 ++++--
 src/client/views/MainViewModal.scss                |  25 +
 src/client/views/MainViewModal.tsx                 |  44 +
 src/client/views/OverlayView.tsx                   |   3 +
 src/client/views/ScriptingRepl.scss                |   1 +
 .../views/collections/CollectionDockingView.tsx    |  33 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  18 +-
 src/client/views/nodes/DocumentView.scss           |  15 +
 src/client/views/nodes/DocumentView.tsx            |  43 +-
 .../views/presentationview/PresentationView.tsx    | 993 +++++++++++++++++++++
 src/server/Message.ts                              |   7 +-
 src/server/apis/google/GooglePhotosUploadUtils.ts  |   2 -
 .../authentication/models/current_user_utils.ts    |   8 +-
 src/server/credentials/google_docs_token.json      |   2 +-
 src/server/index.ts                                |  64 +-
 27 files changed, 1835 insertions(+), 255 deletions(-)
 create mode 100644 src/client/util/SharingManager.scss
 create mode 100644 src/client/util/SharingManager.tsx
 create mode 100644 src/client/views/MainViewModal.scss
 create mode 100644 src/client/views/MainViewModal.tsx
 create mode 100644 src/client/views/presentationview/PresentationView.tsx

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

diff --git a/.vscode/launch.json b/.vscode/launch.json
index d2c18d6f1..e1c5c6f94 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -3,14 +3,13 @@
     // Hover to view descriptions of existing attributes.
     // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
     "version": "0.2.0",
-    "configurations": [
-        {
+    "configurations": [{
             "type": "chrome",
             "request": "launch",
             "name": "Launch Chrome against localhost",
             "sourceMaps": true,
             "breakOnLoad": true,
-            "url": "http://localhost:1050/login",
+            "url": "http://localhost:1050/logout",
             "webRoot": "${workspaceFolder}",
             "runtimeArgs": [
                 "--experimental-modules"
diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts
index 2cec1046b..4dea4f11c 100644
--- a/src/client/DocServer.ts
+++ b/src/client/DocServer.ts
@@ -144,7 +144,7 @@ export namespace DocServer {
      * the server if the document has not been cached.
      * @param id the id of the requested document
      */
-    const _GetRefFieldImpl = (id: string): Promise<Opt<RefField>> => {
+    const _GetRefFieldImpl = (id: string, mongoCollection?: string): Promise<Opt<RefField>> => {
         // an initial pass through the cache to determine whether the document needs to be fetched,
         // is already in the process of being fetched or already exists in the
         // cache
@@ -155,7 +155,7 @@ export namespace DocServer {
             // synchronously, we emit a single callback to the server requesting the serialized (i.e. represented by a string)
             // field for the given ids. This returns a promise, which, when resolved, indicates the the JSON serialized version of
             // the field has been returned from the server
-            const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, id);
+            const getSerializedField = Utils.EmitCallback(_socket, MessageStore.GetRefField, { id, mongoCollection });
 
             // when the serialized RefField has been received, go head and begin deserializing it into an object.
             // Here, once deserialized, we also invoke .proto to 'load' the document's prototype, which ensures that all
@@ -188,10 +188,10 @@ export namespace DocServer {
         }
     };
 
-    let _GetRefField: (id: string) => Promise<Opt<RefField>> = errorFunc;
+    let _GetRefField: (id: string, mongoCollection?: string) => Promise<Opt<RefField>> = errorFunc;
 
-    export function GetRefField(id: string): Promise<Opt<RefField>> {
-        return _GetRefField(id);
+    export function GetRefField(id: string, mongoCollection = "newDocuments"): Promise<Opt<RefField>> {
+        return _GetRefField(id, mongoCollection);
     }
 
     export async function getYoutubeChannels() {
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 700c0401a..b308cc9be 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -108,6 +108,7 @@ export namespace GooglePhotos {
                     await Query.TagChildImages(collection);
                 }
                 collection.albumId = id;
+                Transactions.AddTextEnrichment(collection, `Find me at ${Utils.prepend(`/doc/${collection[Id]}?sharing=true`)}`);
                 return { albumId: id, mediaItems };
             }
         };
@@ -313,7 +314,7 @@ export namespace GooglePhotos {
         };
 
         const parseDescription = (document: Doc, descriptionKey: string) => {
-            let description: string = Utils.prepend("/doc/" + document[Id]);
+            let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`);
             const target = document[descriptionKey];
             if (typeof target === "string") {
                 description = target;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 5dd945c16..cfed2bf14 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -158,7 +158,6 @@ export namespace Docs {
             [DocumentType.LINKDOC, {
                 data: new List<Doc>(),
                 layout: { view: EmptyBox },
-                options: {}
             }],
             [DocumentType.YOUTUBE, {
                 layout: { view: YoutubeBox }
diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index fb3c15cea..0711effe6 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -3,7 +3,7 @@ import { DocumentView } from "../views/nodes/DocumentView";
 import { UndoManager } from "./UndoManager";
 import * as interpreter from "words-to-numbers";
 import { DocumentType } from "../documents/DocumentTypes";
-import { Doc } from "../../new_fields/Doc";
+import { Doc, Opt } from "../../new_fields/Doc";
 import { List } from "../../new_fields/List";
 import { Docs } from "../documents/Documents";
 import { CollectionViewType } from "../views/collections/CollectionBaseView";
@@ -40,12 +40,26 @@ export namespace DictationManager {
             webkitSpeechRecognition: any;
         }
     }
-    const { webkitSpeechRecognition }: CORE.IWindow = window as CORE.IWindow;
+    const { webkitSpeechRecognition }: CORE.IWindow = window as any as CORE.IWindow;
     export const placeholder = "Listening...";
 
     export namespace Controls {
 
         export const Infringed = "unable to process: dictation manager still involved in previous session";
+        const browser = (() => {
+            let identifier = navigator.userAgent.toLowerCase();
+            if (identifier.indexOf("safari") >= 0) {
+                return "Safari";
+            }
+            if (identifier.indexOf("chrome") >= 0) {
+                return "Chrome";
+            }
+            if (identifier.indexOf("firefox") >= 0) {
+                return "Firefox";
+            }
+            return "Unidentified Browser";
+        })();
+        const unsupported = `listening is not supported in ${browser}`;
         const intraSession = ". ";
         const interSession = " ... ";
 
@@ -55,8 +69,7 @@ export namespace DictationManager {
         let current: string | undefined = undefined;
         let sessionResults: string[] = [];
 
-        const recognizer: SpeechRecognition = new webkitSpeechRecognition() || new SpeechRecognition();
-        recognizer.onstart = () => console.log("initiating speech recognition session...");
+        const recognizer: Opt<SpeechRecognition> = webkitSpeechRecognition ? new webkitSpeechRecognition() : undefined;
 
         export type InterimResultHandler = (results: string) => any;
         export type ContinuityArgs = { indefinite: boolean } | false;
@@ -109,6 +122,10 @@ export namespace DictationManager {
         };
 
         const listenImpl = (options?: Partial<ListeningOptions>) => {
+            if (!recognizer) {
+                console.log(unsupported);
+                return unsupported;
+            }
             if (isListening) {
                 return Infringed;
             }
@@ -121,6 +138,7 @@ export namespace DictationManager {
             let intra = options && options.delimiters ? options.delimiters.intra : undefined;
             let inter = options && options.delimiters ? options.delimiters.inter : undefined;
 
+            recognizer.onstart = () => console.log("initiating speech recognition session...");
             recognizer.interimResults = handler !== undefined;
             recognizer.continuous = continuous === undefined ? false : continuous !== false;
             recognizer.lang = language === undefined ? "en-US" : language;
@@ -167,14 +185,20 @@ export namespace DictationManager {
                     } else {
                         resolve(current);
                     }
-                    reset();
+                    current = undefined;
+                    sessionResults = [];
+                    isListening = false;
+                    isManuallyStopped = false;
+                    recognizer.onresult = null;
+                    recognizer.onerror = null;
+                    recognizer.onend = null;
                 };
 
             });
         };
 
         export const stop = (salvageSession = true) => {
-            if (!isListening) {
+            if (!isListening || !recognizer) {
                 return;
             }
             isManuallyStopped = true;
@@ -197,16 +221,6 @@ export namespace DictationManager {
             return transcripts.join(delimiter || intraSession);
         };
 
-        const reset = () => {
-            current = undefined;
-            sessionResults = [];
-            isListening = false;
-            isManuallyStopped = false;
-            recognizer.onresult = null;
-            recognizer.onerror = null;
-            recognizer.onend = null;
-        };
-
     }
 
     export namespace Commands {
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index e9ff21b22..c72ae05de 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -16,8 +16,10 @@ export namespace HistoryUtil {
         initializers?: {
             [docId: string]: DocInitializerList;
         };
+        safe?: boolean;
         readonly?: boolean;
         nro?: boolean;
+        sharing?: boolean;
     }
 
     export type ParsedUrl = DocUrl;
@@ -141,7 +143,7 @@ export namespace HistoryUtil {
         };
     }
 
-    addParser("doc", {}, { readonly: true, initializers: true, nro: true }, (pathname, opts, current) => {
+    addParser("doc", {}, { readonly: true, initializers: true, nro: true, sharing: true }, (pathname, opts, current) => {
         if (pathname.length !== 2) return undefined;
 
         current.initializers = current.initializers || {};
@@ -156,7 +158,7 @@ export namespace HistoryUtil {
     export function parseUrl(location: Location | URL): ParsedUrl | undefined {
         const pathname = location.pathname.substring(1);
         const search = location.search;
-        const opts = qs.parse(search, { sort: false });
+        const opts = search.length ? qs.parse(search, { sort: false }) : {};
         let pathnameSplit = pathname.split("/");
 
         const type = pathnameSplit[0];
diff --git a/src/client/util/SharingManager.scss b/src/client/util/SharingManager.scss
new file mode 100644
index 000000000..9a4c5db30
--- /dev/null
+++ b/src/client/util/SharingManager.scss
@@ -0,0 +1,136 @@
+.sharing-interface {
+    display: flex;
+    flex-direction: column;
+
+    p {
+        font-size: 20px;
+        text-align: left;
+        font-style: italic;
+        padding: 0;
+        margin: 0 0 20px 0;
+    }
+
+    .hr-substitute {
+        border: solid black 0.5px;
+        margin-top: 20px;
+    }
+
+    .people-with-container {
+        display: flex;
+        height: 25px;
+
+        .people-with {
+            font-size: 14px;
+            margin: 0;
+            padding-top: 3px;
+            font-style: normal;
+        }
+
+        .people-with-select {
+            width: 126px;
+            outline: none;
+        }
+    }
+
+    .share-individual {
+        margin-top: 20px;
+        margin-bottom: 20px;
+    }
+
+    .users-list {
+        font-style: italic;
+        background: white;
+        border: 1px solid black;
+        padding-left: 10px;
+        padding-right: 10px;
+        max-height: 200px;
+        overflow: scroll;
+        height: -webkit-fill-available;
+        text-align: left;
+        display: flex;
+        align-content: center;
+        align-items: center;
+        text-align: center;
+        justify-content: center;
+        color: red;
+    }
+
+    .container {
+        display: block;
+        position: relative;
+        margin-top: 10px;
+        margin-bottom: 10px;
+        font-size: 22px;
+        -webkit-user-select: none;
+        -moz-user-select: none;
+        -ms-user-select: none;
+        user-select: none;
+        width: 700px;
+        min-width: 700px;
+        max-width: 700px;
+        text-align: left;
+        font-style: normal;
+        font-size: 15;
+        font-weight: normal;
+        padding: 0;
+
+        .padding {
+            padding: 0 0 0 20px;
+            color: black;
+        }
+
+        .permissions-dropdown {
+            outline: none;
+        }
+    }
+
+    .no-users {
+        margin-top: 20px;
+    }
+
+    .link-container {
+        display: flex;
+        flex-direction: row;
+        margin-bottom: 10px;
+        margin-left: auto;
+        margin-right: auto;
+
+        .link-box,
+        .copy {
+            padding: 10px;
+            border-radius: 10px;
+            padding: 10px;
+            border: solid black 1px;
+        }
+
+        .link-box {
+            background: white;
+            color: blue;
+            text-decoration: underline;
+        }
+
+        .copy {
+            margin-left: 20px;
+            cursor: alias;
+            border-radius: 50%;
+            width: 42px;
+            height: 42px;
+            transition: 1.5s all ease;
+            padding-top: 12px;
+        }
+    }
+
+    .close-button {
+        border-radius: 5px;
+        margin-top: 20px;
+        padding: 10px 0;
+        background: aliceblue;
+        transition: 0.5s ease all;
+        border: 1px solid;
+        border-color: aliceblue;
+    }
+
+    .close-button:hover {
+        border-color: black;
+    }
+}
\ No newline at end of file
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
new file mode 100644
index 000000000..72a4b4141
--- /dev/null
+++ b/src/client/util/SharingManager.tsx
@@ -0,0 +1,293 @@
+import { observable, runInAction, action, autorun } from "mobx";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Doc, Opt } from "../../new_fields/Doc";
+import { DocServer } from "../DocServer";
+import { Cast, StrCast } from "../../new_fields/Types";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+import { RouteStore } from "../../server/RouteStore";
+import * as RequestPromise from "request-promise";
+import { Utils } from "../../Utils";
+import "./SharingManager.scss";
+import { Id } from "../../new_fields/FieldSymbols";
+import { observer } from "mobx-react";
+import { MainView } from "../views/MainView";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import * as fa from '@fortawesome/free-solid-svg-icons';
+import { DocumentView } from "../views/nodes/DocumentView";
+import { SelectionManager } from "./SelectionManager";
+import { DocumentManager } from "./DocumentManager";
+import { CollectionVideoView } from "../views/collections/CollectionVideoView";
+import { CollectionPDFView } from "../views/collections/CollectionPDFView";
+import { CollectionView } from "../views/collections/CollectionView";
+
+library.add(fa.faCopy);
+
+export interface User {
+    email: string;
+    userDocumentId: string;
+}
+
+export enum SharingPermissions {
+    None = "Not Shared",
+    View = "Can View",
+    Comment = "Can Comment",
+    Edit = "Can Edit"
+}
+
+const ColorMapping = new Map<string, string>([
+    [SharingPermissions.None, "red"],
+    [SharingPermissions.View, "maroon"],
+    [SharingPermissions.Comment, "blue"],
+    [SharingPermissions.Edit, "green"]
+]);
+
+const SharingKey = "sharingPermissions";
+const PublicKey = "publicLinkPermissions";
+const DefaultColor = "black";
+
+@observer
+export default class SharingManager extends React.Component<{}> {
+    public static Instance: SharingManager;
+    @observable private isOpen = false;
+    @observable private users: User[] = [];
+    @observable private targetDoc: Doc | undefined;
+    @observable private targetDocView: DocumentView | undefined;
+    @observable private copied = false;
+    @observable private dialogueBoxOpacity = 1;
+    @observable private overlayOpacity = 0.4;
+
+    private get linkVisible() {
+        return this.sharingDoc ? this.sharingDoc[PublicKey] !== SharingPermissions.None : false;
+    }
+
+    public open = (target: DocumentView) => {
+        SelectionManager.DeselectAll();
+        this.populateUsers().then(action(() => {
+            this.targetDocView = target;
+            this.targetDoc = target.props.Document;
+            MainView.Instance.hasActiveModal = true;
+            this.isOpen = true;
+            if (!this.sharingDoc) {
+                this.sharingDoc = new Doc;
+            }
+        }));
+    }
+
+    public close = action(() => {
+        this.isOpen = false;
+        setTimeout(action(() => {
+            this.copied = false;
+            MainView.Instance.hasActiveModal = false;
+            this.targetDoc = undefined;
+        }), 500);
+    });
+
+    private get sharingDoc() {
+        return this.targetDoc ? Cast(this.targetDoc[SharingKey], Doc) as Doc : undefined;
+    }
+
+    private set sharingDoc(value: Doc | undefined) {
+        this.targetDoc && (this.targetDoc[SharingKey] = value);
+    }
+
+    constructor(props: {}) {
+        super(props);
+        SharingManager.Instance = this;
+    }
+
+    populateUsers = async () => {
+        let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
+        runInAction(() => {
+            this.users = (JSON.parse(userList) as User[]).filter(({ email }) => email !== CurrentUserUtils.email);
+        });
+    }
+
+    setInternalSharing = async (user: User, state: string) => {
+        if (!this.sharingDoc) {
+            console.log("SHARING ABORTED!");
+            return;
+        }
+        let sharingDoc = await this.sharingDoc;
+        sharingDoc[user.userDocumentId] = state;
+        const userDocument = await DocServer.GetRefField(user.userDocumentId);
+        if (!(userDocument instanceof Doc)) {
+            console.log(`Couldn't get user document of user ${user.email}`);
+            return;
+        }
+        let target = this.targetDoc;
+        if (!target) {
+            console.log("SharingManager trying to share an undefined document!!");
+            return;
+        }
+        const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
+        if (notifDoc instanceof Doc) {
+            const data = await Cast(notifDoc.data, listSpec(Doc));
+            if (!data) {
+                console.log("UNABLE TO ACCESS NOTIFICATION DATA");
+                return;
+            }
+            console.log(`Attempting to set permissions to ${state} for the document ${target[Id]}`);
+            if (state !== SharingPermissions.None) {
+                const sharedDoc = Doc.MakeAlias(target);
+                if (data) {
+                    data.push(sharedDoc);
+                } else {
+                    notifDoc.data = new List([sharedDoc]);
+                }
+            } else {
+                let dataDocs = (await Promise.all(data.map(doc => doc))).map(doc => Doc.GetProto(doc));
+                if (dataDocs.includes(target)) {
+                    console.log("Searching in ", dataDocs, "for", target);
+                    dataDocs.splice(dataDocs.indexOf(target), 1);
+                    console.log("SUCCESSFULLY UNSHARED DOC");
+                } else {
+                    console.log("DIDN'T THINK WE HAD IT, SO NOT SUCCESSFULLY UNSHARED");
+                }
+            }
+        }
+    }
+
+    private setExternalSharing = (state: string) => {
+        let sharingDoc = this.sharingDoc;
+        if (!sharingDoc) {
+            return;
+        }
+        sharingDoc[PublicKey] = state;
+    }
+
+    private get sharingUrl() {
+        if (!this.targetDoc) {
+            return undefined;
+        }
+        let baseUrl = Utils.prepend("/doc/" + this.targetDoc[Id]);
+        return `${baseUrl}?sharing=true`;
+    }
+
+    copy = action(() => {
+        if (this.sharingUrl) {
+            Utils.CopyText(this.sharingUrl);
+            this.copied = true;
+        }
+    });
+
+    private get sharingOptions() {
+        return Object.values(SharingPermissions).map(permission => {
+            return (
+                <option key={permission} value={permission}>
+                    {permission}
+                </option>
+            );
+        });
+    }
+
+    private focusOn = (contents: string) => {
+        let title = this.targetDoc ? StrCast(this.targetDoc.title) : "";
+        return (
+            <span
+                title={title}
+                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);
+                    }
+                }}
+                onPointerEnter={action(() => {
+                    if (this.targetDoc) {
+                        Doc.BrushDoc(this.targetDoc);
+                        this.dialogueBoxOpacity = 0.1;
+                        this.overlayOpacity = 0.1;
+                    }
+                })}
+                onPointerLeave={action(() => {
+                    this.targetDoc && Doc.UnBrushDoc(this.targetDoc);
+                    this.dialogueBoxOpacity = 1;
+                    this.overlayOpacity = 0.4;
+                })}
+            >
+                {contents}
+            </span>
+        );
+    }
+
+    private get sharingInterface() {
+        return (
+            <div className={"sharing-interface"}>
+                <p className={"share-link"}>Manage the public link to {this.focusOn("this document...")}</p>
+                {!this.linkVisible ? (null) :
+                    <div className={"link-container"}>
+                        <div className={"link-box"} onClick={this.copy}>{this.sharingUrl}</div>
+                        <div
+                            title={"Copy link to clipboard"}
+                            className={"copy"}
+                            style={{ backgroundColor: this.copied ? "lawngreen" : "gainsboro" }}
+                            onClick={this.copy}
+                        >
+                            <FontAwesomeIcon icon={fa.faCopy} />
+                        </div>
+                    </div>
+                }
+                <div className={"people-with-container"}>
+                    {!this.linkVisible ? (null) : <p className={"people-with"}>People with this link</p>}
+                    <select
+                        className={"people-with-select"}
+                        value={this.sharingDoc ? StrCast(this.sharingDoc[PublicKey], SharingPermissions.None) : SharingPermissions.None}
+                        style={{
+                            marginLeft: this.linkVisible ? 10 : 0,
+                            color: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[PublicKey], SharingPermissions.None)) : DefaultColor,
+                            borderColor: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[PublicKey], SharingPermissions.None)) : DefaultColor
+                        }}
+                        onChange={e => this.setExternalSharing(e.currentTarget.value)}
+                    >
+                        {this.sharingOptions}
+                    </select>
+                </div>
+                <div className={"hr-substitute"} />
+                <p className={"share-individual"}>Privately share {this.focusOn("this document")} with an individual...</p>
+                <div className={"users-list"} style={{ display: this.users.length ? "block" : "flex" }}>
+                    {!this.users.length ? "There are no other users in your database." :
+                        this.users.map(user => {
+                            return (
+                                <div
+                                    key={user.email}
+                                    className={"container"}
+                                >
+                                    <select
+                                        className={"permissions-dropdown"}
+                                        value={this.sharingDoc ? StrCast(this.sharingDoc[user.userDocumentId], SharingPermissions.None) : SharingPermissions.None}
+                                        style={{
+                                            color: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[user.userDocumentId], SharingPermissions.None)) : DefaultColor,
+                                            borderColor: this.sharingDoc ? ColorMapping.get(StrCast(this.sharingDoc[user.userDocumentId], SharingPermissions.None)) : DefaultColor
+                                        }}
+                                        onChange={e => this.setInternalSharing(user, e.currentTarget.value)}
+                                    >
+                                        {this.sharingOptions}
+
+                                    </select>
+                                    <span className={"padding"}>{user.email}</span>
+                                </div>
+                            );
+                        })
+                    }
+                </div>
+                <div className={"close-button"} onClick={this.close}>Done</div>
+            </div>
+        );
+    }
+
+    render() {
+        return (
+            <MainViewModal
+                contents={this.sharingInterface}
+                isDisplayed={this.isOpen}
+                interactive={true}
+                dialogueBoxDisplayedOpacity={this.dialogueBoxOpacity}
+                overlayDisplayedOpacity={this.overlayOpacity}
+            />
+        );
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d0464bd5f..0255ab78a 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -6,6 +6,7 @@ import { DragManager } from "../util/DragManager";
 import { action, runInAction } from "mobx";
 import { Doc } from "../../new_fields/Doc";
 import { DictationManager } from "../util/DictationManager";
+import SharingManager from "../util/SharingManager";
 
 const modifiers = ["control", "meta", "shift", "alt"];
 type KeyHandler = (keycode: string, e: KeyboardEvent) => KeyControlInfo | Promise<KeyControlInfo>;
@@ -72,6 +73,7 @@ export default class KeyManager {
                 main.toggleColorPicker(true);
                 SelectionManager.DeselectAll();
                 DictationManager.Controls.stop();
+                SharingManager.Instance.close();
                 break;
             case "delete":
             case "backspace":
diff --git a/src/client/views/InkingCanvas.scss b/src/client/views/InkingCanvas.scss
index 5437b26d6..1365974dd 100644
--- a/src/client/views/InkingCanvas.scss
+++ b/src/client/views/InkingCanvas.scss
@@ -34,7 +34,7 @@
 
 .inkingCanvas-noSelect {
     pointer-events: none;
-    cursor: "arrow";
+    cursor: "crosshair";
 }
 
 .inkingCanvas-paths-ink,
diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index bc0975c86..04249506a 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -268,44 +268,4 @@ ul#add-options-list {
     height: 25%;
     position: relative;
     display: flex;
-}
-
-.dictation-prompt {
-    position: absolute;
-    z-index: 1000;
-    text-align: center;
-    justify-content: center;
-    align-self: center;
-    align-content: center;
-    padding: 20px;
-    background: gainsboro;
-    border-radius: 10px;
-    border: 3px solid black;
-    box-shadow: #00000044 5px 5px 10px;
-    transform: translate(-50%, -50%);
-    top: 50%;
-    font-style: italic;
-    left: 50%;
-    transition: 0.5s all ease;
-    pointer-events: none;
-}
-
-.dictation-prompt-overlay {
-    width: 100%;
-    height: 100%;
-    position: absolute;
-    z-index: 999;
-    transition: 0.5s all ease;
-    pointer-events: none;
-}
-
-.webpage-input {
-    display: none;
-    height: 60px;
-    width: 600px;
-    position: absolute;
-
-    .url-input {
-        width: 80%;
-    }
 }
\ No newline at end of file
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index e35ba18e4..b623cab4e 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -52,12 +52,16 @@ let swapDocs = async () => {
     const info = await CurrentUserUtils.loadCurrentUser();
     DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
     await Docs.Prototypes.initialize();
-    await CurrentUserUtils.loadUserDocument(info);
-    // updates old user documents to prevent chrome on tree view.
-    (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled";
-    (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled";
-    (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled";
-    await swapDocs();
+    if (info.id !== "__guest__") {
+        // a guest will not have an id registered
+        await CurrentUserUtils.loadUserDocument(info);
+        // updates old user documents to prevent chrome on tree view.
+        (await Cast(CurrentUserUtils.UserDocument.workspaces, Doc))!.chromeStatus = "disabled";
+        (await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))!.chromeStatus = "disabled";
+        (await Cast(CurrentUserUtils.UserDocument.sidebar, Doc))!.chromeStatus = "disabled";
+        CurrentUserUtils.UserDocument.chromeStatus = "disabled";
+        await swapDocs();
+    }
     document.getElementById('root')!.addEventListener('wheel', event => {
         if (event.ctrlKey) {
             event.preventDefault();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 28edf181b..85bf0344b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -7,8 +7,8 @@ import "normalize.css";
 import * as React from 'react';
 import { SketchPicker } from 'react-color';
 import Measure from 'react-measure';
-import { Doc, DocListCast, Opt, HeightSym } from '../../new_fields/Doc';
 import { List } from '../../new_fields/List';
+import { Doc, DocListCast, Opt, HeightSym, FieldResult, Field } from '../../new_fields/Doc';
 import { Id } from '../../new_fields/FieldSymbols';
 import { InkTool } from '../../new_fields/InkField';
 import { listSpec } from '../../new_fields/Schema';
@@ -17,14 +17,14 @@ import { CurrentUserUtils } from '../../server/authentication/models/current_use
 import { RouteStore } from '../../server/RouteStore';
 import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils';
 import { DocServer } from '../DocServer';
-import { Docs } from '../documents/Documents';
 import { ClientUtils } from '../util/ClientUtils';
 import { DictationManager } from '../util/DictationManager';
 import { SetupDrag } from '../util/DragManager';
-import { HistoryUtil } from '../util/History';
 import { Transform } from '../util/Transform';
 import { UndoManager, undoBatch } from '../util/UndoManager';
-import { CollectionBaseView } from './collections/CollectionBaseView';
+import { Docs, DocumentOptions } from '../documents/Documents';
+import { HistoryUtil } from '../util/History';
+import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionTreeView } from './collections/CollectionTreeView';
 import { ContextMenu } from './ContextMenu';
@@ -44,6 +44,9 @@ import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../new_fields/URLField';
 import { LinkFollowBox } from './linking/LinkFollowBox';
 import { DocumentManager } from '../util/DocumentManager';
+import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
+import MainViewModal from './MainViewModal';
+import SharingManager from '../util/SharingManager';
 
 @observer
 export class MainView extends React.Component {
@@ -57,6 +60,8 @@ export class MainView extends React.Component {
     @observable private dictationDisplayState = false;
     @observable private dictationListeningState: DictationManager.Controls.ListeningUIStatus = false;
 
+    public hasActiveModal = false;
+
     public overlayTimeout: NodeJS.Timeout | undefined;
 
     public initiateDictationFade = () => {
@@ -64,10 +69,17 @@ export class MainView extends React.Component {
         this.overlayTimeout = setTimeout(() => {
             this.dictationOverlayVisible = false;
             this.dictationSuccess = undefined;
+            this.hasActiveModal = false;
             setTimeout(() => this.dictatedPhrase = DictationManager.placeholder, 500);
         }, duration);
     }
 
+    private urlState: HistoryUtil.DocUrl;
+
+    @computed private get userDoc() {
+        return CurrentUserUtils.UserDocument;
+    }
+
     public cancelDictationFade = () => {
         if (this.overlayTimeout) {
             clearTimeout(this.overlayTimeout);
@@ -76,7 +88,7 @@ export class MainView extends React.Component {
     }
 
     @computed private get mainContainer(): Opt<Doc> {
-        return FieldValue(Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc));
+        return this.userDoc ? FieldValue(Cast(this.userDoc.activeWorkspace, Doc)) : CurrentUserUtils.GuestWorkspace;
     }
     @computed get mainFreeform(): Opt<Doc> {
         let docs = DocListCast(this.mainContainer!.data);
@@ -85,7 +97,10 @@ export class MainView extends React.Component {
     public isPointerDown = false;
     private set mainContainer(doc: Opt<Doc>) {
         if (doc) {
-            CurrentUserUtils.UserDocument.activeWorkspace = doc;
+            if (!("presentationView" in doc)) {
+                doc.presentationView = new List<Doc>([Docs.Create.TreeDocument([], { title: "Presentation" })]);
+            }
+            this.userDoc ? (this.userDoc.activeWorkspace = doc) : (CurrentUserUtils.GuestWorkspace = doc);
         }
     }
 
@@ -130,23 +145,23 @@ export class MainView extends React.Component {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         window.addEventListener("keydown", KeyManager.Instance.handle);
 
-        this.executeGooglePhotosRoutine();
-
-        reaction(() => {
-            let workspaces = CurrentUserUtils.UserDocument.workspaces;
-            let recent = CurrentUserUtils.UserDocument.recentlyClosed;
-            if (!(recent instanceof Doc)) return 0;
-            if (!(workspaces instanceof Doc)) return 0;
-            let workspacesDoc = workspaces;
-            let recentDoc = recent;
-            let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + CurrentUserUtils.UserDocument[HeightSym]() * 0.00001;
-            return libraryHeight;
-        }, (libraryHeight: number) => {
-            if (libraryHeight && Math.abs(CurrentUserUtils.UserDocument[HeightSym]() - libraryHeight) > 5) {
-                CurrentUserUtils.UserDocument.height = libraryHeight;
-            }
-            (Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc) as Doc).allowClear = true;
-        }, { fireImmediately: true });
+        if (this.userDoc) {
+            reaction(() => {
+                let workspaces = this.userDoc.workspaces;
+                let recent = this.userDoc.recentlyClosed;
+                if (!(recent instanceof Doc)) return 0;
+                if (!(workspaces instanceof Doc)) return 0;
+                let workspacesDoc = workspaces;
+                let recentDoc = recent;
+                let libraryHeight = this.getPHeight() - workspacesDoc[HeightSym]() - recentDoc[HeightSym]() - 20 + this.userDoc[HeightSym]() * 0.00001;
+                return libraryHeight;
+            }, (libraryHeight: number) => {
+                if (libraryHeight && Math.abs(this.userDoc[HeightSym]() - libraryHeight) > 5) {
+                    this.userDoc.height = libraryHeight;
+                }
+                (Cast(this.userDoc.recentlyClosed, Doc) as Doc).allowClear = true;
+            }, { fireImmediately: true });
+        }
     }
 
     executeGooglePhotosRoutine = async () => {
@@ -169,7 +184,7 @@ export class MainView extends React.Component {
     constructor(props: Readonly<{}>) {
         super(props);
         MainView.Instance = this;
-
+        this.urlState = HistoryUtil.parseUrl(window.location) || {} as any;
         // causes errors to be generated when modifying an observable outside of an action
         configure({ enforceActions: "observed" });
         if (window.location.pathname !== RouteStore.home) {
@@ -178,6 +193,12 @@ export class MainView extends React.Component {
                 let type = pathname[0];
                 if (type === "doc") {
                     CurrentUserUtils.MainDocId = pathname[1];
+                    if (!this.userDoc) {
+                        runInAction(() => this.flyoutWidth = 0);
+                        DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action(field => {
+                            field instanceof Doc && (CurrentUserUtils.GuestTarget = field);
+                        }));
+                    }
                 }
             }
         }
@@ -234,68 +255,109 @@ export class MainView extends React.Component {
 
     initAuthenticationRouters = async () => {
         // Load the user's active workspace, or create a new one if initial session after signup
-        if (!CurrentUserUtils.MainDocId) {
-            const doc = await Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc);
-            if (doc) {
+        let received = CurrentUserUtils.MainDocId;
+        if (received && !this.userDoc) {
+            reaction(
+                () => CurrentUserUtils.GuestTarget,
+                target => target && this.createNewWorkspace(),
+                { fireImmediately: true }
+            );
+        } else {
+            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) {
+                                    const target = Doc.MakeAlias(field);
+                                    const artificialParent = Docs.Create.FreeformDocument([target], { title: `View of ${StrCast(field.title)}` });
+                                    CollectionDockingView.Instance.AddRightSplit(artificialParent, undefined);
+                                    DocumentManager.Instance.jumpToDocument(target, true, undefined, undefined, undefined, artificialParent);
+                                }
+                            });
+                        }
+                    },
+                );
+            }
+            let doc: Opt<Doc>;
+            if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
                 this.openWorkspace(doc);
             } else {
                 this.createNewWorkspace();
             }
-        } else {
-            DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
-                field instanceof Doc ? this.openWorkspace(field) :
-                    this.createNewWorkspace(CurrentUserUtils.MainDocId));
         }
     }
 
-
     @action
     createNewWorkspace = async (id?: string) => {
-        let workspaces = Cast(CurrentUserUtils.UserDocument.workspaces, Doc);
-        if (!(workspaces instanceof Doc)) return;
-        const list = Cast((CurrentUserUtils.UserDocument.workspaces as Doc).data, listSpec(Doc));
-        if (list) {
-            let freeformDoc = Docs.Create.FreeformDocument([], { x: 0, y: 400, width: this.pwidth * .7, height: this.pheight, title: `WS collection ${list.length + 1}` });
-            var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
-            let mainDoc = Docs.Create.DockDocument([CurrentUserUtils.UserDocument, freeformDoc], JSON.stringify(dockingLayout), { title: `Workspace ${list.length + 1}` }, id);
-            if (!CurrentUserUtils.UserDocument.linkManagerDoc) {
-                let linkManagerDoc = new Doc();
-                linkManagerDoc.allLinks = new List<Doc>([]);
-                CurrentUserUtils.UserDocument.linkManagerDoc = linkManagerDoc;
+        let freeformOptions: DocumentOptions = {
+            x: 0,
+            y: 400,
+            width: this.pwidth * .7,
+            height: this.pheight,
+            title: CurrentUserUtils.GuestTarget ? `Guest View of ${StrCast(CurrentUserUtils.GuestTarget.title)}` : "My Blank Collection"
+        };
+        let workspaces: FieldResult<Doc>;
+        let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
+        var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc, freeformDoc, 600)] }] };
+        let mainDoc = Docs.Create.DockDocument([this.userDoc, freeformDoc], JSON.stringify(dockingLayout), {}, id);
+        if (this.userDoc && ((workspaces = Cast(this.userDoc.workspaces, Doc)) instanceof Doc)) {
+            const list = Cast((workspaces).data, listSpec(Doc));
+            if (list) {
+                if (!this.userDoc.linkManagerDoc) {
+                    let linkManagerDoc = new Doc();
+                    linkManagerDoc.allLinks = new List<Doc>([]);
+                    this.userDoc.linkManagerDoc = linkManagerDoc;
+                }
+                list.push(mainDoc);
+                mainDoc.title = `Workspace ${list.length}`;
             }
-            list.push(mainDoc);
-            // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
-            setTimeout(() => {
-                this.openWorkspace(mainDoc);
-                // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" });
-                // mainDoc.optionalRightCollection = pendingDocument;
-            }, 0);
         }
+        // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
+        setTimeout(() => {
+            this.openWorkspace(mainDoc);
+            // let pendingDocument = Docs.StackingDocument([], { title: "New Mobile Uploads" });
+            // mainDoc.optionalRightCollection = pendingDocument;
+        }, 0);
     }
 
     @action
     openWorkspace = async (doc: Doc, fromHistory = false) => {
         CurrentUserUtils.MainDocId = doc[Id];
         this.mainContainer = doc;
-        const state = HistoryUtil.parseUrl(window.location) || {} as any;
-        fromHistory || HistoryUtil.pushState({ type: "doc", docId: doc[Id], readonly: state.readonly, nro: state.nro });
-        if (state.readonly === true || state.readonly === null) {
+        let state = this.urlState;
+        if (state.sharing === true && !this.userDoc) {
             DocServer.Control.makeReadOnly();
-        } else if (state.safe) {
-            if (!state.nro) {
+        } else {
+            fromHistory || HistoryUtil.pushState({
+                type: "doc",
+                docId: doc[Id],
+                readonly: state.readonly,
+                nro: state.nro,
+                sharing: false,
+            });
+            if (state.readonly === true || state.readonly === null) {
+                DocServer.Control.makeReadOnly();
+            } else if (state.safe) {
+                if (!state.nro) {
+                    DocServer.Control.makeReadOnly();
+                }
+                CollectionBaseView.SetSafeMode(true);
+            } else if (state.nro || state.nro === null || state.readonly === false) {
+            } else if (BoolCast(doc.readOnly)) {
                 DocServer.Control.makeReadOnly();
+            } else {
+                DocServer.Control.makeEditable();
             }
-            CollectionBaseView.SetSafeMode(true);
-        } else if (state.nro || state.nro === null || state.readonly === false) {
-        } else if (BoolCast(doc.readOnly)) {
-            DocServer.Control.makeReadOnly();
-        } else {
-            DocServer.Control.makeEditable();
         }
-        const col = await Cast(CurrentUserUtils.UserDocument.optionalRightCollection, Doc);
+        let col: Opt<Doc>;
         // if there is a pending doc, and it has new data, show it (syip: we use a timeout to prevent collection docking view from being uninitialized)
         setTimeout(async () => {
-            if (col) {
+            if (this.userDoc && (col = await Cast(this.userDoc.optionalRightCollection, Doc))) {
                 const l = Cast(col.data, listSpec(Doc));
                 if (l) {
                     runInAction(() => CollectionTreeView.NotifsCol = col);
@@ -389,11 +451,12 @@ export class MainView extends React.Component {
     }
     @computed
     get flyout() {
-        let sidebar = CurrentUserUtils.UserDocument.sidebar;
-        if (!(sidebar instanceof Doc)) return (null);
-        let sidebarDoc = sidebar;
+        let sidebar: FieldResult<Field>;
+        if (!this.userDoc || !((sidebar = this.userDoc.sidebar) instanceof Doc)) {
+            return (null);
+        }
         return <DocumentView
-            Document={sidebarDoc}
+            Document={sidebar}
             DataDoc={undefined}
             addDocument={undefined}
             addDocTab={this.addDocTabFunc}
@@ -417,9 +480,14 @@ export class MainView extends React.Component {
     }
     @computed
     get mainContent() {
-        let sidebar = CurrentUserUtils.UserDocument.sidebar;
-        if (!(sidebar instanceof Doc)) return (null);
-        return <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}>
+        if (!this.userDoc) {
+            return <div>{this.dockingContent}</div>;
+        }
+        let sidebar = this.userDoc.sidebar;
+        if (!(sidebar instanceof Doc)) {
+            return (null);
+        }
+        return <div>
             <div className="mainView-libraryHandle"
                 style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
                 onPointerDown={this.onPointerDown}>
@@ -448,14 +516,22 @@ export class MainView extends React.Component {
         }
     }
 
-    toggleLinkFollowBox = (shouldClose: boolean) => {
-        if (LinkFollowBox.Instance) {
-            let dvs = DocumentManager.Instance.getDocumentViews(LinkFollowBox.Instance.props.Document);
-            // if it already exisits, close it
-            LinkFollowBox.Instance.props.Document.isMinimized = (dvs.length > 0 && shouldClose);
-        }
+    setWriteMode = (mode: DocServer.WriteMode) => {
+        console.log(DocServer.WriteMode[mode]);
+        const mode1 = mode;
+        const mode2 = mode === DocServer.WriteMode.Default ? mode : DocServer.WriteMode.Playground;
+        DocServer.setFieldWriteMode("x", mode1);
+        DocServer.setFieldWriteMode("y", mode1);
+        DocServer.setFieldWriteMode("width", mode1);
+        DocServer.setFieldWriteMode("height", mode1);
+
+        DocServer.setFieldWriteMode("panX", mode2);
+        DocServer.setFieldWriteMode("panY", mode2);
+        DocServer.setFieldWriteMode("scale", mode2);
+        DocServer.setFieldWriteMode("viewType", mode2);
     }
 
+
     @observable private _colorPickerDisplay = false;
     /* for the expandable add nodes menu. Not included with the miscbuttons because once it expands it expands the whole div with it, making canvas interactions limited. */
     nodesMenu() {
@@ -501,7 +577,13 @@ export class MainView extends React.Component {
                                 <FontAwesomeIcon icon={btn[1]} size="sm" />
                             </button>
                         </div></li>)}
-                    <li key="linkFollow"><button className="add-button round-button" title="Open Link Follower" onClick={() => this.toggleLinkFollowBox(true)}><FontAwesomeIcon icon="link" size="sm" /></button></li>
+                    <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
+                    {ClientUtils.RELEASE ? [] : [
+                        <li key="test"><button className="add-button round-button" title="Default" onClick={() => this.setWriteMode(DocServer.WriteMode.Default)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
+                        <li key="test1"><button className="add-button round-button" title="Playground" onClick={() => this.setWriteMode(DocServer.WriteMode.Playground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
+                        <li key="test2"><button className="add-button round-button" title="Live Playground" onClick={() => this.setWriteMode(DocServer.WriteMode.LivePlayground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
+                        <li key="test3"><button className="add-button round-button" title="Live Readonly" onClick={() => this.setWriteMode(DocServer.WriteMode.LiveReadonly)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
+                    ]}
                     <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
                         <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
                             <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
@@ -543,28 +625,25 @@ export class MainView extends React.Component {
         this.isSearchVisible = !this.isSearchVisible;
     }
 
-    private get dictationOverlay() {
-        let display = this.dictationOverlayVisible;
+    @computed private get dictationOverlay() {
         let success = this.dictationSuccess;
         let result = this.isListening && !this.isListening.interim ? DictationManager.placeholder : `"${this.dictatedPhrase}"`;
+        let dialogueBoxStyle = {
+            background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
+            borderColor: this.isListening ? "red" : "black",
+            fontStyle: "italic"
+        };
+        let overlayStyle = {
+            backgroundColor: this.isListening ? "red" : "darkslategrey"
+        };
         return (
-            <div>
-                <div
-                    className={"dictation-prompt"}
-                    style={{
-                        opacity: display ? 1 : 0,
-                        background: success === undefined ? "gainsboro" : success ? "lawngreen" : "red",
-                        borderColor: this.isListening ? "red" : "black",
-                    }}
-                >{result}</div>
-                <div
-                    className={"dictation-prompt-overlay"}
-                    style={{
-                        opacity: display ? 0.4 : 0,
-                        backgroundColor: this.isListening ? "red" : "darkslategrey"
-                    }}
-                />
-            </div>
+            <MainViewModal
+                contents={result}
+                isDisplayed={this.dictationOverlayVisible}
+                interactive={false}
+                dialogueBoxStyle={dialogueBoxStyle}
+                overlayStyle={overlayStyle}
+            />
         );
     }
 
@@ -580,6 +659,7 @@ export class MainView extends React.Component {
         return (
             <div id="main-div">
                 {this.dictationOverlay}
+                <SharingManager />
                 <DocumentDecorations />
                 {this.mainContent}
                 {this.miniPresentation}
diff --git a/src/client/views/MainViewModal.scss b/src/client/views/MainViewModal.scss
new file mode 100644
index 000000000..f5a9ee76c
--- /dev/null
+++ b/src/client/views/MainViewModal.scss
@@ -0,0 +1,25 @@
+.dialogue-box {
+    position: absolute;
+    z-index: 1000;
+    text-align: center;
+    justify-content: center;
+    align-self: center;
+    align-content: center;
+    padding: 20px;
+    background: gainsboro;
+    border-radius: 10px;
+    border: 3px solid black;
+    box-shadow: #00000044 5px 5px 10px;
+    transform: translate(-50%, -50%);
+    top: 50%;
+    left: 50%;
+    transition: 0.5s all ease;
+}
+
+.overlay {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    z-index: 999;
+    transition: 0.5s all ease;
+}
\ No newline at end of file
diff --git a/src/client/views/MainViewModal.tsx b/src/client/views/MainViewModal.tsx
new file mode 100644
index 000000000..b7fdd6168
--- /dev/null
+++ b/src/client/views/MainViewModal.tsx
@@ -0,0 +1,44 @@
+import * as React from 'react';
+import "./MainViewModal.scss";
+
+export interface MainViewOverlayProps {
+    isDisplayed: boolean;
+    interactive: boolean;
+    contents: string | JSX.Element;
+    dialogueBoxStyle?: React.CSSProperties;
+    overlayStyle?: React.CSSProperties;
+    dialogueBoxDisplayedOpacity?: number;
+    overlayDisplayedOpacity?: number;
+}
+
+export default class MainViewModal extends React.Component<MainViewOverlayProps> {
+
+    render() {
+        let p = this.props;
+        let dialogueOpacity = p.dialogueBoxDisplayedOpacity || 1;
+        let overlayOpacity = p.overlayDisplayedOpacity || 0.4;
+        return (
+            <div style={{ pointerEvents: p.isDisplayed ? p.interactive ? "all" : "none" : "none" }}>
+                <div
+                    className={"dialogue-box"}
+                    style={{
+                        backgroundColor: "gainsboro",
+                        borderColor: "black",
+                        ...(p.dialogueBoxStyle || {}),
+                        opacity: p.isDisplayed ? dialogueOpacity : 0
+                    }}
+                >{p.contents}</div>
+                <div
+                    className={"overlay"}
+                    style={{
+                        backgroundColor: "gainsboro",
+                        ...(p.overlayStyle || {}),
+                        opacity: p.isDisplayed ? overlayOpacity : 0
+                    }}
+                />
+            </div>
+        );
+    }
+
+
+}
\ No newline at end of file
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index da4b71e5c..7a1fac4c8 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -141,6 +141,9 @@ export class OverlayView extends React.Component {
     }
 
     @computed get overlayDocs() {
+        if (!CurrentUserUtils.UserDocument) {
+            return (null);
+        }
         return CurrentUserUtils.UserDocument.overlays instanceof Doc && DocListCast(CurrentUserUtils.UserDocument.overlays.data).map(d => {
             let offsetx = 0, offsety = 0;
             let onPointerMove = action((e: PointerEvent) => {
diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss
index f1ef64193..778e9c445 100644
--- a/src/client/views/ScriptingRepl.scss
+++ b/src/client/views/ScriptingRepl.scss
@@ -21,6 +21,7 @@
 .scriptingRepl-commandsContainer {
     flex: 1 1 auto;
     overflow-y: scroll;
+    height: 30px;
 }
 
 .documentIcon-outerDiv {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index fb8b0c41b..f184a3944 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,6 +1,6 @@
 import 'golden-layout/src/css/goldenlayout-base.css';
 import 'golden-layout/src/css/goldenlayout-dark-theme.css';
-import { action, Lambda, observable, reaction, trace, computed } from "mobx";
+import { action, Lambda, observable, reaction, trace, computed, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import * as ReactDOM from 'react-dom';
 import Measure from "react-measure";
@@ -35,7 +35,7 @@ library.add(faFile);
 
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
-    public static Instance: CollectionDockingView;
+    @observable public static Instance: CollectionDockingView;
     public static makeDocumentConfig(document: Doc, dataDoc: Doc | undefined, width?: number) {
         return {
             type: 'react-component',
@@ -50,7 +50,11 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         };
     }
 
-    private _goldenLayout: any = null;
+    @computed public get initialized() {
+        return this._goldenLayout !== null;
+    }
+
+    @observable private _goldenLayout: any = null;
     private _containerRef = React.createRef<HTMLDivElement>();
     private _flush: boolean = false;
     private _ignoreStateChange = "";
@@ -59,7 +63,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     constructor(props: SubCollectionViewProps) {
         super(props);
-        if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this;
+        if (props.addDocTab === emptyFunction) {
+            runInAction(() => CollectionDockingView.Instance = this);
+        }
         //Why is this here?
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
@@ -245,7 +251,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         var config = StrCast(this.props.Document.dockingConfig);
         if (config) {
             if (!this._goldenLayout) {
-                this._goldenLayout = new GoldenLayout(JSON.parse(config));
+                runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config)));
             }
             else {
                 if (config === JSON.stringify(this._goldenLayout.toConfig())) {
@@ -258,7 +264,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     this._goldenLayout.unbind('stackCreated', this.stackCreated);
                 } catch (e) { }
                 this._goldenLayout.destroy();
-                this._goldenLayout = new GoldenLayout(JSON.parse(config));
+                runInAction(() => this._goldenLayout = new GoldenLayout(JSON.parse(config)));
             }
             this._goldenLayout.on('itemDropped', this.itemDropped);
             this._goldenLayout.on('tabCreated', this.tabCreated);
@@ -286,7 +292,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                         // Because this is in a set timeout, if this component unmounts right after mounting,
                         // we will leak a GoldenLayout, because we try to destroy it before we ever create it
                         setTimeout(() => this.setupGoldenLayout(), 1);
-                        DocListCast((CurrentUserUtils.UserDocument.workspaces as Doc).data).map(d => d.workspaceBrush = false);
+                        let userDoc = CurrentUserUtils.UserDocument;
+                        userDoc && DocListCast((userDoc.workspaces as Doc).data).map(d => d.workspaceBrush = false);
                         this.props.Document.workspaceBrush = true;
                     }
                     this._ignoreStateChange = "";
@@ -306,7 +313,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
         }
         if (this._goldenLayout) this._goldenLayout.destroy();
-        this._goldenLayout = null;
+        runInAction(() => this._goldenLayout = null);
         window.removeEventListener('resize', this.onResize);
 
         if (this.reactionDisposer) {
@@ -438,8 +445,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     let theDoc = doc;
                     CollectionDockingView.Instance._removedDocs.push(theDoc);
 
-                    const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
-                    if (recent) {
+                    let userDoc = CurrentUserUtils.UserDocument;
+                    let recent: Doc | undefined;
+                    if (userDoc && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) {
                         Doc.AddDocToList(recent, "data", doc, undefined, true, true);
                     }
                     SelectionManager.DeselectAll();
@@ -471,12 +479,13 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             .off('click') //unbind the current click handler
             .click(action(async function () {
                 //if (confirm('really close this?')) {
-                const recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc);
+
                 stack.remove();
                 stack.contentItems.forEach(async (contentItem: any) => {
                     let doc = await DocServer.GetRefField(contentItem.config.props.documentId);
                     if (doc instanceof Doc) {
-                        if (recent) {
+                        let recent: Doc | undefined;
+                        if (CurrentUserUtils.UserDocument && (recent = await Cast(CurrentUserUtils.UserDocument.recentlyClosed, Doc))) {
                             Doc.AddDocToList(recent, "data", doc, undefined, true, true);
                         }
                         let theDoc = doc;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3377775db..85002e40f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -947,18 +947,22 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         layoutItems.push({ description: "Jitter Rotation", event: action(() => this.props.Document.jitterRotation = 10), icon: "paint-brush" });
 
         let noteItems: ContextMenuProps[] = [];
-        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" })
+        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" });
         ContextMenu.Instance.addItem({ description: "Freeform Options ...", subitems: layoutItems, icon: "eye" });
     }
 
     createText = (noteStyle: number) => {
         let pt = this.getTransform().transformPoint(ContextMenu.Instance.pageX, ContextMenu.Instance.pageY);
-        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);
+        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 = () => [
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 7c72fb6e6..a91683c94 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -27,6 +27,21 @@
     overflow-y: scroll;
     height: calc(100% - 20px);
   }
+
+  .documentView-overlays {
+    border-radius: inherit;
+    position: absolute; 
+    display: inline-block; 
+    width: 100%; 
+    height: 100%; 
+    pointer-events: none;
+    .documentView-textOverlay {
+        border-radius: inherit;
+        width: 100%;
+        display: inline-block; 
+        position: absolute;
+    }
+  }
 }
 .documentView-node-topmost {
     background: white;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2a8030c0c..a85ff3d04 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -43,6 +43,8 @@ import React = require("react");
 import { DocumentType } from '../../documents/DocumentTypes';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { ImageField } from '../../../new_fields/URLField';
+import { IDisposable } from '../../northstar/utils/IDisposable';
+import SharingManager from '../../util/SharingManager';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(fa.faTrash);
@@ -673,10 +675,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
         !existing && cm.addItem({ description: "Layout...", subitems: layoutItems, icon: "compass" });
         if (!ClientUtils.RELEASE) {
-            let copies: ContextMenuProps[] = [];
-            copies.push({ description: "Copy URL", event: () => Utils.CopyText(Utils.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
-            copies.push({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
-            cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
+            // let copies: ContextMenuProps[] = [];
+            cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
+            // cm.addItem({ description: "Copy...", subitems: copies, icon: "copy" });
         }
         let existingAnalyze = ContextMenu.Instance.findByDescription("Analyzers...");
         let analyzers: ContextMenuProps[] = existingAnalyze && "subitems" in existingAnalyze ? existingAnalyze.subitems : [];
@@ -700,34 +701,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         });
 
         cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
-        type User = { email: string, userDocumentId: string };
-        let usersMenu: ContextMenuProps[] = [];
-        try {
-            let stuff = await rp.get(Utils.prepend(RouteStore.getUsers));
-            const users: User[] = JSON.parse(stuff);
-            usersMenu = users.filter(({ email }) => email !== Doc.CurrentUserEmail).map(({ email, userDocumentId }) => ({
-                description: email, event: async () => {
-                    const userDocument = await Cast(DocServer.GetRefField(userDocumentId), Doc);
-                    if (!userDocument) {
-                        throw new Error(`Couldn't get user document of user ${email}`);
-                    }
-                    const notifDoc = await Cast(userDocument.optionalRightCollection, Doc);
-                    if (notifDoc instanceof Doc) {
-                        const data = await Cast(notifDoc.data, listSpec(Doc));
-                        const sharedDoc = Doc.MakeAlias(this.props.Document);
-                        if (data) {
-                            data.push(sharedDoc);
-                        } else {
-                            notifDoc.data = new List([sharedDoc]);
-                        }
-                    }
-                }, icon: "male"
-            }));
-        } catch {
-
-        }
         runInAction(() => {
-            cm.addItem({ description: "Share...", subitems: usersMenu, icon: "share" });
             if (!ClientUtils.RELEASE) {
                 let setWriteMode = (mode: DocServer.WriteMode) => {
                     DocServer.AclsMode = mode;
@@ -751,6 +725,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 cm.addItem({ description: "Collaboration ACLs...", subitems: aclsMenu, icon: "share" });
                 cm.addItem({ description: "Undo Debug Test", event: () => UndoManager.TraceOpenBatches(), icon: "exclamation" });
             }
+        });
+        runInAction(() => {
+            cm.addItem({
+                description: "Share",
+                event: () => SharingManager.Instance.open(this),
+                icon: "external-link-alt"
+            });
 
             if (!this.topMost) {
                 // DocumentViews should stop propagation of this event
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
new file mode 100644
index 000000000..54e789a0a
--- /dev/null
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -0,0 +1,993 @@
+import { observer } from "mobx-react";
+import React = require("react");
+import { observable, action, runInAction, reaction, autorun } from "mobx";
+import "./PresentationView.scss";
+import { DocumentManager } from "../../util/DocumentManager";
+import { Utils } from "../../../Utils";
+import { Doc, DocListCast, DocListCastAsync, WidthSym } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+import { Cast, NumCast, FieldValue, PromiseValue, StrCast, BoolCast } from "../../../new_fields/Types";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { List } from "../../../new_fields/List";
+import PresentationElement, { buttonIndex } from "./PresentationElement";
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faArrowRight, faArrowLeft, faPlay, faStop, faPlus, faTimes, faMinus, faEdit, faEye } from '@fortawesome/free-solid-svg-icons';
+import { Docs } from "../../documents/Documents";
+import { undoBatch, UndoManager } from "../../util/UndoManager";
+import PresentationViewList from "./PresentationList";
+import PresModeMenu from "./PresentationModeMenu";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+
+library.add(faArrowLeft);
+library.add(faArrowRight);
+library.add(faPlay);
+library.add(faStop);
+library.add(faPlus);
+library.add(faTimes);
+library.add(faMinus);
+library.add(faEdit);
+library.add(faEye);
+
+
+export interface PresViewProps {
+    Documents: List<Doc>;
+}
+
+const expandedWidth = 400;
+const presMinWidth = 300;
+
+@observer
+export class PresentationView extends React.Component<PresViewProps>  {
+    public static Instance: PresentationView;
+
+    //Mapping from presentation ids to a list of doc that represent a group
+    @observable groupMappings: Map<String, Doc[]> = new Map();
+    //mapping from docs to their rendered component
+    @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
+    //variable that holds all the docs in the presentation
+    @observable childrenDocs: Doc[] = [];
+    //variable to hold if presentation is started
+    @observable presStatus: boolean = false;
+    //back-up so that presentation stays the way it's when refreshed
+    @observable presGroupBackUp: Doc = new Doc();
+    @observable presButtonBackUp: Doc = new Doc();
+
+    //Keeping track of the doc for the current presentation
+    @observable curPresentation: Doc = new Doc();
+    //Mapping of guids to presentations.
+    @observable presentationsMapping: Map<String, Doc> = new Map();
+    //Mapping of presentations to guid, so that select option values can be given.
+    @observable presentationsKeyMapping: Map<Doc, String> = new Map();
+    //Variable to keep track of guid of the current presentation
+    @observable currentSelectedPresValue: string | undefined;
+    //A flag to keep track if title input is open, which is used in rendering.
+    @observable PresTitleInputOpen: boolean = false;
+    //Variable that holds reference to title input, so that new presentations get titles assigned.
+    @observable titleInputElement: HTMLInputElement | undefined;
+    @observable PresTitleChangeOpen: boolean = false;
+    @observable presMode: boolean = false;
+
+
+    @observable opacity = 1;
+    @observable persistOpacity = true;
+    @observable labelOpacity = 0;
+
+    //initilize class variables
+    constructor(props: PresViewProps) {
+        super(props);
+        PresentationView.Instance = this;
+    }
+
+    @action
+    toggle = (forcedValue: boolean | undefined) => {
+        if (forcedValue !== undefined) {
+            this.curPresentation.width = forcedValue ? expandedWidth : 0;
+        } else {
+            this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
+        }
+    }
+
+    //The first lifecycle function that gets called to set up the current presentation.
+    async componentWillMount() {
+
+        this.props.Documents.forEach(async (doc, index: number) => {
+
+            //For each presentation received from mainContainer, a mapping is created.
+            let curDoc: Doc = await doc;
+            let newGuid = Utils.GenerateGuid();
+            this.presentationsKeyMapping.set(curDoc, newGuid);
+            this.presentationsMapping.set(newGuid, curDoc);
+
+            //The Presentation at first index gets set as default start presentation
+            if (index === 0) {
+                runInAction(() => this.currentSelectedPresValue = newGuid);
+                runInAction(() => this.curPresentation = curDoc);
+            }
+        });
+    }
+
+    //Second lifecycle function that gets called when component mounts. It makes sure to
+    //get the back-up information from previous session for the current presentation.
+    async componentDidMount() {
+        let docAtZero = await this.props.Documents[0];
+        runInAction(() => this.curPresentation = docAtZero);
+        this.curPresentation.width = 0;
+        this.setPresentationBackUps();
+    }
+
+
+    /**
+     * The function that retrieves the backUps for the current Presentation if present,
+     * otherwise initializes.
+     */
+    setPresentationBackUps = async () => {
+        //getting both backUp documents
+
+        let castedGroupBackUp = Cast(this.curPresentation.presGroupBackUp, Doc);
+        let castedButtonBackUp = Cast(this.curPresentation.presButtonBackUp, Doc);
+        //if instantiated before 
+        if (castedGroupBackUp instanceof Promise) {
+            castedGroupBackUp.then(doc => {
+                let toAssign = doc ? doc : new Doc();
+                this.curPresentation.presGroupBackUp = toAssign;
+                runInAction(() => this.presGroupBackUp = toAssign);
+                if (doc) {
+                    if (toAssign[Id] === doc[Id]) {
+                        this.retrieveGroupMappings();
+                    }
+                }
+            });
+
+            //if never instantiated a store doc yet
+        } else if (castedGroupBackUp instanceof Doc) {
+            let castedDoc: Doc = await castedGroupBackUp;
+            runInAction(() => this.presGroupBackUp = castedDoc);
+            this.retrieveGroupMappings();
+        } else {
+            runInAction(() => {
+                let toAssign = new Doc();
+                this.presGroupBackUp = toAssign;
+                this.curPresentation.presGroupBackUp = toAssign;
+
+            });
+
+        }
+        //if instantiated before 
+        if (castedButtonBackUp instanceof Promise) {
+            castedButtonBackUp.then(doc => {
+                let toAssign = doc ? doc : new Doc();
+                this.curPresentation.presButtonBackUp = toAssign;
+                runInAction(() => this.presButtonBackUp = toAssign);
+            });
+
+            //if never instantiated a store doc yet
+        } else if (castedButtonBackUp instanceof Doc) {
+            let castedDoc: Doc = await castedButtonBackUp;
+            runInAction(() => this.presButtonBackUp = castedDoc);
+
+        } else {
+            runInAction(() => {
+                let toAssign = new Doc();
+                this.presButtonBackUp = toAssign;
+                this.curPresentation.presButtonBackUp = toAssign;
+            });
+
+        }
+
+
+        //storing the presentation status,ie. whether it was stopped or playing
+        let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
+        runInAction(() => this.presStatus = presStatusBackUp);
+    }
+
+    /**
+     * This is the function that is called to retrieve the groups that have been stored and
+     * push them to the groupMappings.
+     */
+    retrieveGroupMappings = async () => {
+        let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+        if (castedGroupDocs !== undefined) {
+            castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+                let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+                let castedKey = StrCast(groupDoc.presentIdStore, null);
+                if (castedGrouping) {
+                    castedGrouping.forEach((doc: Doc) => {
+                        doc.presentId = castedKey;
+                    });
+                }
+                if (castedGrouping !== undefined && castedKey !== undefined) {
+                    this.groupMappings.set(castedKey, castedGrouping);
+                }
+            });
+        }
+    }
+
+    //observable means render is re-called every time variable is changed
+    @observable
+    collapsed: boolean = false;
+    closePresentation = action(() => this.curPresentation.width = 0);
+    next = async () => {
+        const current = NumCast(this.curPresentation.selectedDoc);
+        //asking to get document at current index
+        let docAtCurrentNext = await this.getDocAtIndex(current + 1);
+        if (docAtCurrentNext === undefined) {
+            return;
+        }
+        //asking for it's presentation id
+        let curNextPresId = StrCast(docAtCurrentNext.presentId);
+        let nextSelected = current + 1;
+
+        //if curDoc is in a group, selection slides until last one, if not it's next one
+        if (this.groupMappings.has(curNextPresId)) {
+            let currentsArray = this.groupMappings.get(StrCast(docAtCurrentNext.presentId))!;
+            nextSelected = current + currentsArray.length - currentsArray.indexOf(docAtCurrentNext);
+
+            //end of grup so go beyond
+            if (nextSelected === current) nextSelected = current + 1;
+        }
+
+        this.gotoDocument(nextSelected, current);
+
+    }
+    back = async () => {
+        const current = NumCast(this.curPresentation.selectedDoc);
+        //requesting for the doc at current index
+        let docAtCurrent = await this.getDocAtIndex(current);
+        if (docAtCurrent === undefined) {
+            return;
+        }
+
+        //asking for its presentation id.
+        let curPresId = StrCast(docAtCurrent.presentId);
+        let prevSelected = current - 1;
+        let zoomOut: boolean = false;
+
+        //checking if this presentation id is mapped to a group, if so chosing the first element in group
+        if (this.groupMappings.has(curPresId)) {
+            let currentsArray = this.groupMappings.get(StrCast(docAtCurrent.presentId))!;
+            prevSelected = current - currentsArray.length + (currentsArray.length - currentsArray.indexOf(docAtCurrent)) - 1;
+            //end of grup so go beyond
+            if (prevSelected === current) prevSelected = current - 1;
+
+            //checking if any of the group members had used zooming in
+            currentsArray.forEach((doc: Doc) => {
+                //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
+                if (this.presElementsMappings.get(doc)!.selected[buttonIndex.Show]) {
+                    zoomOut = true;
+                    return;
+                }
+            });
+
+        }
+
+        // if a group set that flag to zero or a single element
+        //If so making sure to zoom out, which goes back to state before zooming action
+        if (current > 0) {
+            if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.selected[buttonIndex.Show]) {
+                let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
+                let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
+                if (prevScale !== undefined) {
+                    if (prevScale !== curScale) {
+                        DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+                    }
+                }
+            }
+        }
+        this.gotoDocument(prevSelected, current);
+
+    }
+
+    /**
+     * This is the method that checks for the actions that need to be performed
+     * after the document has been presented, which involves 3 button options:
+     * Hide Until Presented, Hide After Presented, Fade After Presented
+     */
+    showAfterPresented = (index: number) => {
+        this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+            let selectedButtons: boolean[] = presElem.selected;
+            //the order of cases is aligned based on priority
+            if (selectedButtons[buttonIndex.HideTillPressed]) {
+                if (this.childrenDocs.indexOf(key) <= index) {
+                    key.opacity = 1;
+                }
+            }
+            if (selectedButtons[buttonIndex.HideAfter]) {
+                if (this.childrenDocs.indexOf(key) < index) {
+                    key.opacity = 0;
+                }
+            }
+            if (selectedButtons[buttonIndex.FadeAfter]) {
+                if (this.childrenDocs.indexOf(key) < index) {
+                    key.opacity = 0.5;
+                }
+            }
+        });
+    }
+
+    /**
+     * This is the method that checks for the actions that need to be performed
+     * before the document has been presented, which involves 3 button options:
+     * Hide Until Presented, Hide After Presented, Fade After Presented
+     */
+    hideIfNotPresented = (index: number) => {
+        this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+            let selectedButtons: boolean[] = presElem.selected;
+
+            //the order of cases is aligned based on priority
+
+            if (selectedButtons[buttonIndex.HideAfter]) {
+                if (this.childrenDocs.indexOf(key) >= index) {
+                    key.opacity = 1;
+                }
+            }
+            if (selectedButtons[buttonIndex.FadeAfter]) {
+                if (this.childrenDocs.indexOf(key) >= index) {
+                    key.opacity = 1;
+                }
+            }
+            if (selectedButtons[buttonIndex.HideTillPressed]) {
+                if (this.childrenDocs.indexOf(key) > index) {
+                    key.opacity = 0;
+                }
+            }
+        });
+    }
+
+    /**
+     * This method makes sure that cursor navigates to the element that
+     * has the option open and last in the group. If not in the group, and it has
+     * te option open, navigates to that element.
+     */
+    navigateToElement = async (curDoc: Doc, fromDoc: number) => {
+        let docToJump: Doc = curDoc;
+        let curDocPresId = StrCast(curDoc.presentId, null);
+        let willZoom: boolean = false;
+
+        //checking if in group
+        if (curDocPresId !== undefined) {
+            if (this.groupMappings.has(curDocPresId)) {
+                let currentDocGroup = this.groupMappings.get(curDocPresId)!;
+                currentDocGroup.forEach((doc: Doc, index: number) => {
+                    let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected;
+                    if (selectedButtons[buttonIndex.Navigate]) {
+                        docToJump = doc;
+                        willZoom = false;
+                    }
+                    if (selectedButtons[buttonIndex.Show]) {
+                        docToJump = doc;
+                        willZoom = true;
+                    }
+                });
+            }
+
+        }
+        //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 curDocButtons = this.presElementsMappings.get(curDoc)!.selected;
+            if (curDocButtons[buttonIndex.Navigate]) {
+                this.jumpToTabOrRight(curDocButtons, curDoc);
+            } else if (curDocButtons[buttonIndex.Show]) {
+                let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+                if (curDocButtons[buttonIndex.OpenRight]) {
+                    //awaiting jump so that new scale can be found, since jumping is async
+                    await DocumentManager.Instance.jumpToDocument(curDoc, true);
+                } else {
+                    await DocumentManager.Instance.jumpToDocument(curDoc, true, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
+                }
+
+                let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+                curDoc.viewScale = newScale;
+
+                //saving the scale user was on before zooming in
+                if (curScale !== 1) {
+                    this.childrenDocs[fromDoc].viewScale = curScale;
+                }
+
+            }
+            return;
+        }
+        let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+        let curDocButtons = this.presElementsMappings.get(docToJump)!.selected;
+
+
+        if (curDocButtons[buttonIndex.OpenRight]) {
+            //awaiting jump so that new scale can be found, since jumping is async
+            await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+        } else {
+            await DocumentManager.Instance.jumpToDocument(docToJump, willZoom, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
+        }
+        let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+        curDoc.viewScale = newScale;
+        //saving the scale that user was on
+        if (curScale !== 1) {
+            this.childrenDocs[fromDoc].viewScale = curScale;
+        }
+
+    }
+
+    /**
+     * This function  checks if right option is clicked on a presentation element, if not it does open it as a tab
+     * with help of CollectionDockingView.
+     */
+    jumpToTabOrRight = (curDocButtons: boolean[], curDoc: Doc) => {
+        if (curDocButtons[buttonIndex.OpenRight]) {
+            DocumentManager.Instance.jumpToDocument(curDoc, false);
+        } else {
+            DocumentManager.Instance.jumpToDocument(curDoc, false, undefined, doc => CollectionDockingView.Instance.AddTab(undefined, doc, undefined));
+        }
+    }
+
+    /**
+     * Async function that supposedly return the doc that is located at given index.
+     */
+    getDocAtIndex = async (index: number) => {
+        const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+        if (!list) {
+            return undefined;
+        }
+        if (index < 0 || index >= list.length) {
+            return undefined;
+        }
+
+        this.curPresentation.selectedDoc = index;
+        //awaiting async call to finish to get Doc instance
+        const doc = await list[index];
+        return doc;
+    }
+
+    /**
+     * The function that removes a doc from a presentation. It also makes sure to
+     * do necessary updates to backUps and mappings stored locally.
+     */
+    @action
+    public RemoveDoc = async (index: number) => {
+        const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+        if (value) {
+            let removedDoc = await value.splice(index, 1)[0];
+
+            //removing the Presentation Element stored for it
+            this.presElementsMappings.delete(removedDoc);
+
+            let removedDocPresentId = StrCast(removedDoc.presentId);
+
+            //Removing it from local mapping of the groups
+            if (this.groupMappings.has(removedDocPresentId)) {
+                let removedDocsGroup = this.groupMappings.get(removedDocPresentId);
+                if (removedDocsGroup) {
+                    removedDocsGroup.splice(removedDocsGroup.indexOf(removedDoc), 1);
+                    if (removedDocsGroup.length === 0) {
+                        this.groupMappings.delete(removedDocPresentId);
+                    }
+                }
+            }
+
+
+            let castedList = Cast(this.presButtonBackUp.selectedButtonDocs, listSpec(Doc));
+            if (castedList) {
+                for (let doc of castedList) {
+                    let curDoc = await doc;
+                    let curDocId = StrCast(curDoc.docId);
+                    if (curDocId === removedDoc[Id]) {
+                        castedList.splice(castedList.indexOf(curDoc), 1);
+                        break;
+
+                    }
+                }
+            }
+
+            //removing it from the backup of groups
+            let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+            if (castedGroupDocs) {
+                castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+                    let castedKey = StrCast(groupDoc.presentIdStore, null);
+                    if (castedKey === removedDocPresentId) {
+                        let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+                        if (castedGrouping) {
+                            castedGrouping.splice(castedGrouping.indexOf(removedDoc), 1);
+                            if (castedGrouping.length === 0) {
+                                castedGroupDocs!.splice(castedGroupDocs!.indexOf(groupDoc), 1);
+                            }
+                        }
+                    }
+
+                });
+
+            }
+
+
+        }
+    }
+
+    /**
+     * An alternative remove method that removes a doc from presentation by its actual
+     * reference.
+     */
+    public removeDocByRef = (doc: Doc) => {
+        let indexOfDoc = this.childrenDocs.indexOf(doc);
+        const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+        if (value) {
+            value.splice(indexOfDoc, 1)[0];
+        }
+        if (indexOfDoc !== - 1) {
+            return true;
+        }
+        return false;
+    }
+
+    //The function that is called when a document is clicked or reached through next or back.
+    //it'll also execute the necessary actions if presentation is playing.
+    @action
+    public gotoDocument = async (index: number, fromDoc: number) => {
+        const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+        if (!list) {
+            return;
+        }
+        if (index < 0 || index >= list.length) {
+            return;
+        }
+        this.curPresentation.selectedDoc = index;
+
+        if (!this.presStatus) {
+            this.presStatus = true;
+            this.startPresentation(index);
+        }
+
+        const doc = await list[index];
+        if (this.presStatus) {
+            this.navigateToElement(doc, fromDoc);
+            this.hideIfNotPresented(index);
+            this.showAfterPresented(index);
+        }
+
+    }
+
+    //Function that is called to resetGroupIds, so that documents get new groupIds at
+    //first load, when presentation is changed.
+    resetGroupIds = async () => {
+        let castedGroupDocs = await DocListCastAsync(this.presGroupBackUp.groupDocs);
+        if (castedGroupDocs !== undefined) {
+            castedGroupDocs.forEach(async (groupDoc: Doc, index: number) => {
+                let castedGrouping = await DocListCastAsync(groupDoc.grouping);
+                if (castedGrouping) {
+                    castedGrouping.forEach((doc: Doc) => {
+                        doc.presentId = Utils.GenerateGuid();
+                    });
+                }
+            });
+        }
+        runInAction(() => this.groupMappings = new Map());
+    }
+
+    /**
+     * Adds a document to the presentation view
+     **/
+    @undoBatch
+    @action
+    public PinDoc(doc: Doc) {
+        //add this new doc to props.Document
+        const data = Cast(this.curPresentation.data, listSpec(Doc));
+        if (data) {
+            data.push(doc);
+        } else {
+            this.curPresentation.data = new List([doc]);
+        }
+
+        this.toggle(true);
+    }
+
+    //Function that sets the store of the children docs.
+    @action
+    setChildrenDocs = (docList: Doc[]) => {
+        this.childrenDocs = docList;
+    }
+
+    //The function that is called to render the play or pause button depending on
+    //if presentation is running or not.
+    renderPlayPauseButton = () => {
+        if (this.presStatus) {
+            return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
+        } else {
+            return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
+        }
+    }
+
+    //The function that starts or resets presentaton functionally, depending on status flag.
+    @action
+    startOrResetPres = async () => {
+        if (this.presStatus) {
+            this.resetPresentation();
+        } else {
+            this.presStatus = true;
+            let startIndex = await this.findStartDocument();
+            this.startPresentation(startIndex);
+            const current = NumCast(this.curPresentation.selectedDoc);
+            this.gotoDocument(startIndex, current);
+        }
+        this.curPresentation.presStatus = this.presStatus;
+    }
+
+    /**
+     * This method is called to find the start document of presentation. So
+     * that when user presses on play, the correct presentation element will be
+     * selected.
+     */
+    findStartDocument = async () => {
+        let docAtZero = await this.getDocAtIndex(0);
+        if (docAtZero === undefined) {
+            return 0;
+        }
+        let docAtZeroPresId = StrCast(docAtZero.presentId);
+
+        if (this.groupMappings.has(docAtZeroPresId)) {
+            let group = this.groupMappings.get(docAtZeroPresId)!;
+            let lastDoc = group[group.length - 1];
+            return this.childrenDocs.indexOf(lastDoc);
+        } else {
+            return 0;
+        }
+    }
+
+    //The function that resets the presentation by removing every action done by it. It also
+    //stops the presentaton.
+    @action
+    resetPresentation = () => {
+        this.childrenDocs.forEach((doc: Doc) => {
+            doc.opacity = 1;
+            doc.viewScale = 1;
+        });
+        this.curPresentation.selectedDoc = 0;
+        this.presStatus = false;
+        this.curPresentation.presStatus = this.presStatus;
+        if (this.childrenDocs.length === 0) {
+            return;
+        }
+        DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
+    }
+
+
+    //The function that starts the presentation, also checking if actions should be applied
+    //directly at start.
+    startPresentation = (startIndex: number) => {
+        let selectedButtons: boolean[];
+        this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
+            selectedButtons = component.selected;
+            if (selectedButtons[buttonIndex.HideTillPressed]) {
+                if (this.childrenDocs.indexOf(doc) > startIndex) {
+                    doc.opacity = 0;
+                }
+
+            }
+            if (selectedButtons[buttonIndex.HideAfter]) {
+                if (this.childrenDocs.indexOf(doc) < startIndex) {
+                    doc.opacity = 0;
+                }
+            }
+            if (selectedButtons[buttonIndex.FadeAfter]) {
+                if (this.childrenDocs.indexOf(doc) < startIndex) {
+                    doc.opacity = 0.5;
+                }
+            }
+
+        });
+
+    }
+
+    /**
+     * The function that is called to add a new presentation to the presentationView.
+     * It sets up te mappings and local copies of it. Resets the groupings and presentation.
+     * Makes the new presentation current selected, and retrieve the back-Ups if present.
+     */
+    @action
+    addNewPresentation = (presTitle: string) => {
+        //creating a new presentation doc
+        let newPresentationDoc = Docs.Create.TreeDocument([], { title: presTitle });
+        this.props.Documents.push(newPresentationDoc);
+
+        //setting that new doc as current
+        this.curPresentation = newPresentationDoc;
+
+        //storing the doc in local copies for easier access
+        let newGuid = Utils.GenerateGuid();
+        this.presentationsMapping.set(newGuid, newPresentationDoc);
+        this.presentationsKeyMapping.set(newPresentationDoc, newGuid);
+
+        //resetting the previous presentation's actions so that new presentation can be loaded.
+        this.resetGroupIds();
+        this.resetPresentation();
+        this.presElementsMappings = new Map();
+        this.currentSelectedPresValue = newGuid;
+        this.setPresentationBackUps();
+
+    }
+
+    /**
+     * The function that is called to change the current selected presentation.
+     * Changes the presentation, also resetting groupings and presentation in process.
+     * Plus retrieving the backUps for the newly selected presentation.
+     */
+    @action
+    getSelectedPresentation = (e: React.ChangeEvent<HTMLSelectElement>) => {
+        //get the guid of the selected presentation
+        let selectedGuid = e.target.value;
+        //set that as current presentation
+        this.curPresentation = this.presentationsMapping.get(selectedGuid)!;
+
+        //reset current Presentations local things so that new one can be loaded
+        this.resetGroupIds();
+        this.resetPresentation();
+        this.presElementsMappings = new Map();
+        this.currentSelectedPresValue = selectedGuid;
+        this.setPresentationBackUps();
+
+
+    }
+
+    /**
+     * The function that is called to render either select for presentations, or title inputting.
+     */
+    renderSelectOrPresSelection = () => {
+        let presentationList = DocListCast(this.props.Documents);
+        if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
+            return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
+        } else {
+            return <select value={this.currentSelectedPresValue} id="pres_selector" className="presentationView-title" onChange={this.getSelectedPresentation}>
+                {presentationList.map((doc: Doc, index: number) => {
+                    let mappedGuid = this.presentationsKeyMapping.get(doc);
+                    let docGuid: string = mappedGuid ? mappedGuid.toString() : "";
+                    return <option key={docGuid} value={docGuid}>{StrCast(doc.title)}</option>;
+                })}
+            </select>;
+        }
+    }
+
+    /**
+     * The function that is called on enter press of title input. It gives the
+     * new presentation the title user entered. If nothing is entered, gives a default title.
+     */
+    @action
+    submitPresentationTitle = (e: React.KeyboardEvent) => {
+        if (e.keyCode === 13) {
+            let presTitle = this.titleInputElement!.value;
+            this.titleInputElement!.value = "";
+            if (this.PresTitleInputOpen) {
+                if (presTitle === "") {
+                    presTitle = "Presentation";
+                }
+                this.PresTitleInputOpen = false;
+                this.addNewPresentation(presTitle);
+            } else if (this.PresTitleChangeOpen) {
+                this.PresTitleChangeOpen = false;
+                this.changePresentationTitle(presTitle);
+            }
+        }
+    }
+
+    /**
+     * The function that is called to remove a presentation from all its copies, and the main Container's
+     * list. Sets up the next presentation as current.
+     */
+    @action
+    removePresentation = async () => {
+        if (this.presentationsMapping.size !== 1) {
+            let presentationList = Cast(this.props.Documents, listSpec(Doc));
+            let batch = UndoManager.StartBatch("presRemoval");
+
+            //getting the presentation that will be removed
+            let removedDoc = this.presentationsMapping.get(this.currentSelectedPresValue!);
+            //that presentation is removed
+            presentationList!.splice(presentationList!.indexOf(removedDoc!), 1);
+
+            //its mappings are removed from local copies
+            this.presentationsKeyMapping.delete(removedDoc!);
+            this.presentationsMapping.delete(this.currentSelectedPresValue!);
+
+            //the next presentation is set as current
+            let remainingPresentations = this.presentationsMapping.values();
+            let nextDoc = remainingPresentations.next().value;
+            this.curPresentation = nextDoc;
+
+
+            //Storing these for being able to undo changes
+            let curGuid = this.currentSelectedPresValue!;
+            let curPresStatus = this.presStatus;
+
+            //resetting the groups and presentation actions so that next presentation gets loaded
+            this.resetGroupIds();
+            this.resetPresentation();
+            this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
+            this.setPresentationBackUps();
+
+            //Storing for undo
+            let currentGroups = this.groupMappings;
+            let curPresElemMapping = this.presElementsMappings;
+
+            //Event to undo actions that are not related to doc directly, aka. local things
+            UndoManager.AddEvent({
+                undo: action(() => {
+                    this.curPresentation = removedDoc!;
+                    this.presentationsMapping.set(curGuid, removedDoc!);
+                    this.presentationsKeyMapping.set(removedDoc!, curGuid);
+                    this.currentSelectedPresValue = curGuid;
+
+                    this.presStatus = curPresStatus;
+                    this.groupMappings = currentGroups;
+                    this.presElementsMappings = curPresElemMapping;
+                    this.setPresentationBackUps();
+
+                }),
+                redo: action(() => {
+                    this.curPresentation = nextDoc;
+                    this.presStatus = false;
+                    this.presentationsKeyMapping.delete(removedDoc!);
+                    this.presentationsMapping.delete(curGuid);
+                    this.currentSelectedPresValue = this.presentationsKeyMapping.get(nextDoc)!.toString();
+                    this.setPresentationBackUps();
+
+                }),
+            });
+
+            batch.end();
+        }
+    }
+
+    /**
+     * The function that is called to change title of presentation to what user entered.
+     */
+    @undoBatch
+    changePresentationTitle = (newTitle: string) => {
+        if (newTitle === "") {
+            return;
+        }
+        this.curPresentation.title = newTitle;
+    }
+
+    /**
+     * On pointer down element that is catched on resizer of te
+     * presentation view. Sets up the event listeners to change the size with
+     * mouse move.
+     */
+    _downsize = 0;
+    onPointerDown = (e: React.PointerEvent) => {
+        this._downsize = e.clientX;
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        document.addEventListener("pointermove", this.onPointerMove);
+        document.addEventListener("pointerup", this.onPointerUp);
+        e.stopPropagation();
+        e.preventDefault();
+    }
+    /**
+     * Changes the size of the presentation view, with mouse move.
+     * Minimum size is set to 300, so that every button is visible.
+     */
+    @action
+    onPointerMove = (e: PointerEvent) => {
+
+        this.curPresentation.width = Math.max(window.innerWidth - e.clientX, presMinWidth);
+    }
+
+    /**
+     * The method that is called on pointer up event. It checks if the button is just
+     * clicked so that presentation view will be closed. The way it's done is to check 
+     * for minimal pixel change like 4, and accept it as it's just a click on top of the dragger.
+     */
+    @action
+    onPointerUp = (e: PointerEvent) => {
+        if (Math.abs(e.clientX - this._downsize) < 4) {
+            let presWidth = NumCast(this.curPresentation.width);
+            if (presWidth - presMinWidth !== 0) {
+                this.curPresentation.width = 0;
+            }
+            if (presWidth === 0) {
+                this.curPresentation.width = presMinWidth;
+            }
+        }
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+    }
+
+    /**
+     * This function is a setter that opens up the 
+     * presentation mode, by setting it's render flag
+     * to true. It also closes the presentation view.
+     */
+    @action
+    openPresMode = () => {
+        if (!this.presMode) {
+            this.curPresentation.width = 0;
+            this.presMode = true;
+        }
+    }
+
+    /**
+     * This function closes the presentation mode by setting its
+     * render flag to false. It also opens up the presentation view.
+     * By setting it to it's minimum size.
+     */
+    @action
+    closePresMode = () => {
+        if (this.presMode) {
+            this.presMode = false;
+            this.curPresentation.width = presMinWidth;
+        }
+
+    }
+
+    /**
+     * Function that is called to render the presentation mode, depending on its flag.
+     */
+    renderPresMode = () => {
+        if (this.presMode) {
+            return <PresModeMenu next={this.next} back={this.back} startOrResetPres={this.startOrResetPres} presStatus={this.presStatus} closePresMode={this.closePresMode} />;
+        } else {
+            return (null);
+        }
+
+    }
+
+    render() {
+
+        let width = NumCast(this.curPresentation.width);
+
+        return (
+            <div>
+                <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))} style={{ width: width, overflowY: "scroll", overflowX: "hidden", opacity: this.opacity, transition: "0.7s opacity ease" }}>
+                    <div className="presentationView-heading">
+                        {this.renderSelectOrPresSelection()}
+                        <button title="Close Presentation" className='presentation-icon' onClick={this.closePresentation}><FontAwesomeIcon icon={"times"} /></button>
+                        <button title="Open Presentation Mode" className="presentation-icon" style={{ marginRight: 10 }} onClick={this.openPresMode}><FontAwesomeIcon icon={"eye"} /></button>
+                        <button title="Add Presentation" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+                            runInAction(() => { if (this.PresTitleChangeOpen) { this.PresTitleChangeOpen = false; } });
+                            runInAction(() => this.PresTitleInputOpen ? this.PresTitleInputOpen = false : this.PresTitleInputOpen = true);
+                        }}><FontAwesomeIcon icon={"plus"} /></button>
+                        <button title="Remove Presentation" className='presentation-icon' style={{ marginRight: 10 }} onClick={this.removePresentation}><FontAwesomeIcon icon={"minus"} /></button>
+                        <button title="Change Presentation Title" className="presentation-icon" style={{ marginRight: 10 }} onClick={() => {
+                            runInAction(() => { if (this.PresTitleInputOpen) { this.PresTitleInputOpen = false; } });
+                            runInAction(() => this.PresTitleChangeOpen ? this.PresTitleChangeOpen = false : this.PresTitleChangeOpen = true);
+                        }}><FontAwesomeIcon icon={"edit"} /></button>
+                    </div>
+                    <div className="presentation-buttons">
+                        <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+                        {this.renderPlayPauseButton()}
+                        <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+                    </div>
+
+                    <PresentationViewList
+                        mainDocument={this.curPresentation}
+                        deleteDocument={this.RemoveDoc}
+                        gotoDocument={this.gotoDocument}
+                        groupMappings={this.groupMappings}
+                        PresElementsMappings={this.presElementsMappings}
+                        setChildrenDocs={this.setChildrenDocs}
+                        presStatus={this.presStatus}
+                        presButtonBackUp={this.presButtonBackUp}
+                        presGroupBackUp={this.presGroupBackUp}
+                        removeDocByRef={this.removeDocByRef}
+                        clearElemMap={() => this.presElementsMappings.clear()}
+                    />
+                    <input
+                        type="checkbox"
+                        onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
+                            this.persistOpacity = e.target.checked;
+                            this.opacity = this.persistOpacity ? 1 : 0.4;
+                        })}
+                        checked={this.persistOpacity}
+                        style={{ position: "absolute", bottom: 5, left: 5 }}
+                        onPointerEnter={action(() => this.labelOpacity = 1)}
+                        onPointerLeave={action(() => this.labelOpacity = 0)}
+                    />
+                    <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
+                </div>
+                <div className="mainView-libraryHandle"
+                    style={{ cursor: "ew-resize", right: `${width - 10}px`, backgroundColor: "white", opacity: this.opacity, transition: "0.7s opacity ease" }}
+                    onPointerDown={this.onPointerDown}>
+                    <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
+                </div>
+                {this.renderPresMode()}
+
+            </div>
+        );
+    }
+}
diff --git a/src/server/Message.ts b/src/server/Message.ts
index 4ec390ade..a5679797f 100644
--- a/src/server/Message.ts
+++ b/src/server/Message.ts
@@ -23,6 +23,7 @@ export interface Transferable {
     readonly id: string;
     readonly type: Types;
     readonly data?: any;
+    readonly mongoCollection?: string;
 }
 
 export enum YoutubeQueryTypes {
@@ -43,6 +44,10 @@ export interface Diff extends Reference {
     readonly diff: any;
 }
 
+export interface SourceSpecified extends Reference {
+    readonly mongoCollection?: string;
+}
+
 export namespace MessageStore {
     export const Foo = new Message<string>("Foo");
     export const Bar = new Message<string>("Bar");
@@ -52,7 +57,7 @@ export namespace MessageStore {
     export const GetDocument = new Message<string>("Get Document");
     export const DeleteAll = new Message<any>("Delete All");
 
-    export const GetRefField = new Message<string>("Get Ref Field");
+    export const GetRefField = new Message<SourceSpecified>("Get Ref Field");
     export const GetRefFields = new Message<string[]>("Get Ref Fields");
     export const UpdateField = new Message<Diff>("Update Ref Field");
     export const CreateField = new Message<Reference>("Create Ref Field");
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index c2656cc1c..0215c533f 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -58,8 +58,6 @@ export namespace GooglePhotosUploadUtils {
         }));
     };
 
-
-
     export const CreateMediaItems = async (newMediaItems: any[], album?: { id: string }): Promise<MediaItemCreationResult> => {
         const quota = newMediaItems.length;
         let handled = 0;
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index af5774ebe..050a71eb4 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -2,7 +2,6 @@ import { action, computed, observable, runInAction } from "mobx";
 import * as rp from 'request-promise';
 import { DocServer } from "../../../client/DocServer";
 import { Docs } from "../../../client/documents/Documents";
-import { Gateway, NorthstarSettings } from "../../../client/northstar/manager/Gateway";
 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";
@@ -24,6 +23,9 @@ export class CurrentUserUtils {
     public static get MainDocId() { return this.mainDocId; }
     public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
 
+    @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;
@@ -59,7 +61,7 @@ export class CurrentUserUtils {
             noteTypes.excludeFromLibrary = true;
             doc.noteTypes = noteTypes;
         }
-        PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(vals => DocListCast(vals)));
+        PromiseValue(Cast(doc.noteTypes, Doc)).then(noteTypes => noteTypes && PromiseValue(noteTypes.data).then(DocListCast));
         if (doc.recentlyClosed === undefined) {
             const recentlyClosed = Docs.Create.TreeDocument([], { title: "Recently Closed", height: 75 });
             recentlyClosed.excludeFromLibrary = true;
@@ -112,7 +114,7 @@ export class CurrentUserUtils {
         this.curr_id = id;
         Doc.CurrentUserEmail = email;
         await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
-            if (id) {
+            if (id && id !== "guest") {
                 return DocServer.GetRefField(id).then(async field => {
                     if (field instanceof Doc) {
                         await this.updateUserDocument(field);
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index fec1625f5..31763c2cf 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GlyBB1MlCG7GL2pYFleLp9uUJoN6s0_PFBDLUIhyrKAY4kkVo7vbuaW_zmkJs1Fym0f7NVpaYvFsBK2dbN6Qn5P8bWNW2NsHNNGcwbyGIS8H52GUlyCsawNt6PTnOw","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":1568274162450}
\ No newline at end of file
+{"access_token":"ya29.GlyBB9YYhy7l9LZ9yDpItKvLpibt59SpmBQUMo_sX-3d4eN8W-9teuc_7Ca4YiOboy_gHTdcwaR1ArnpQEqZlzOsfNmV6dXZsldgxin3bVuDn1q4sCWvz01yuZduIA","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":1568281677559}
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 101a4f63f..62c3df8de 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -21,10 +21,10 @@ import * as wdm from 'webpack-dev-middleware';
 import * as whm from 'webpack-hot-middleware';
 import { Utils } from '../Utils';
 import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller';
-import { DashUserModel } from './authentication/models/user_model';
+import User, { DashUserModel } from './authentication/models/user_model';
 import { Client } from './Client';
 import { Database } from './database';
-import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput, SourceSpecified } from "./Message";
 import { RouteStore } from './RouteStore';
 import v4 = require('uuid/v4');
 const app = express();
@@ -36,9 +36,7 @@ const serverPort = 4321;
 import expressFlash = require('express-flash');
 import flash = require('connect-flash');
 import { Search } from './Search';
-import _ = require('lodash');
 import * as Archiver from 'archiver';
-import * as request_promise from 'request-promise';
 var AdmZip = require('adm-zip');
 import * as YoutubeApi from "./apis/youtube/youtubeApiSample";
 import { Response } from 'express-serve-static-core';
@@ -47,6 +45,7 @@ import { GooglePhotosUploadUtils, DownloadUtils as UploadUtils } from './apis/go
 const MongoStore = require('connect-mongo')(session);
 const mongoose = require('mongoose');
 const probe = require("probe-image-size");
+import * as qs from 'query-string';
 
 const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
 let youtubeApiKey: string;
@@ -113,7 +112,9 @@ function addSecureRoute(method: Method,
     ...subscribers: string[]
 ) {
     let abstracted = (req: express.Request, res: express.Response) => {
-        if (req.user) {
+        let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true";
+        sharing = sharing && req.originalUrl.startsWith("/doc/");
+        if (req.user || sharing) {
             handler(req.user, res, req);
         } else {
             req.session!.target = req.originalUrl;
@@ -507,21 +508,20 @@ addSecureRoute(
         res.sendFile(path.join(__dirname, '../../deploy/' + filename));
     },
     undefined,
-    RouteStore.home,
-    RouteStore.openDocumentWithId
+    RouteStore.home, RouteStore.openDocumentWithId
 );
 
 addSecureRoute(
     Method.GET,
-    (user, res) => res.send(user.userDocumentId || ""),
-    undefined,
+    (user, res) => res.send(user.userDocumentId),
+    (res) => res.send(undefined),
     RouteStore.getUserDocumentId,
 );
 
 addSecureRoute(
     Method.GET,
-    (user, res) => res.send(JSON.stringify({ id: user.id, email: user.email })),
-    undefined,
+    (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); },
+    (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })),
     RouteStore.getCurrUser
 );
 
@@ -666,21 +666,31 @@ app.use(RouteStore.corsProxy, (req, res) => {
     }).pipe(res);
 });
 
-app.get(RouteStore.delete, (req, res) => {
-    if (release) {
-        res.send("no");
-        return;
-    }
-    deleteFields().then(() => res.redirect(RouteStore.home));
-});
+addSecureRoute(
+    Method.GET,
+    (user, res, req) => {
+        if (release) {
+            res.send("no");
+            return;
+        }
+        deleteFields().then(() => res.redirect(RouteStore.home));
+    },
+    undefined,
+    RouteStore.delete
+);
 
-app.get(RouteStore.deleteAll, (req, res) => {
-    if (release) {
-        res.send("no");
-        return;
-    }
-    deleteAll().then(() => res.redirect(RouteStore.home));
-});
+addSecureRoute(
+    Method.GET,
+    (user, res, req) => {
+        if (release) {
+            res.send("no");
+            return;
+        }
+        deleteAll().then(() => res.redirect(RouteStore.home));
+    },
+    undefined,
+    RouteStore.deleteAll
+);
 
 app.use(wdm(compiler, { publicPath: config.output.publicPath }));
 
@@ -766,8 +776,8 @@ function setField(socket: Socket, newValue: Transferable) {
     }
 }
 
-function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
-    Database.Instance.getDocument(id, callback, "newDocuments");
+function GetRefField([args, callback]: [SourceSpecified, (result?: Transferable) => void]) {
+    Database.Instance.getDocument(args.id, callback, args.mongoCollection || "newDocuments");
 }
 
 function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
-- 
cgit v1.2.3-70-g09d2


From cb04cae3e5b7d4ae3fb2e59afe866d95320aab14 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Thu, 12 Sep 2019 14:38:09 -0400
Subject: now support custom view

---
 src/client/apis/google_docs/GooglePhotosClientUtils.ts | 2 +-
 src/client/views/MainView.tsx                          | 6 ++----
 src/server/credentials/google_docs_token.json          | 2 +-
 3 files changed, 4 insertions(+), 6 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 63cbc8867..3dac1d65c 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -306,7 +306,7 @@ export namespace GooglePhotos {
                     return;
                 }
                 const url = data.url.href;
-                const description = parseDescription(source, descriptionKey);
+                const description = parseDescription(Doc.MakeAlias(source), descriptionKey);
                 media.push({ url, description });
             });
             if (media.length) {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 85bf0344b..f7b66cae3 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -273,10 +273,8 @@ export class MainView extends React.Component {
                         if (initialized && received) {
                             DocServer.GetRefField(received).then(field => {
                                 if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
-                                    const target = Doc.MakeAlias(field);
-                                    const artificialParent = Docs.Create.FreeformDocument([target], { title: `View of ${StrCast(field.title)}` });
-                                    CollectionDockingView.Instance.AddRightSplit(artificialParent, undefined);
-                                    DocumentManager.Instance.jumpToDocument(target, true, undefined, undefined, undefined, artificialParent);
+                                    CollectionDockingView.Instance.AddRightSplit(field, undefined);
+                                    DocumentManager.Instance.jumpToDocument(field, true, undefined, undefined, undefined, undefined);
                                 }
                             });
                         }
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index bb313f136..5b0b5ab5d 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.GlyBB9xs77CFscdtWApHKMcsd6eS9NW3tO0FEvZlfO87HTl7zc1nIVhvtB7MLxadXvxVg4VUAvl6eFjVFsbdmA7TmURhIygYsZbds87ybMuLH5W68mRAVd3HDYyCzg","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":1568310697477}
\ No newline at end of file
+{"access_token":"ya29.GlyBB-8WTaj3RgOZt5lYaTgidUCgFXHwwtO1ZOYfo9gYq_YuAGQfVC-uRDJ36fIIEgi9F_TWgp8rda2MEXK4KCtTyeeG6Q8-03pdxEdCMdcgf01cmZbheErDY3iLEQ","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":1568316273289}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 233893698083cbcfcf39ddad8b57049aeb1ba842 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 13 Sep 2019 14:18:55 -0400
Subject: refactored how ruleProvider's work.   overloaded custom template  for
 creating metadata fields

---
 src/client/util/RichTextRules.ts                   |  8 +++----
 src/client/util/TooltipTextMenu.tsx                |  4 ++--
 src/client/views/DocumentDecorations.tsx           | 11 ++-------
 src/client/views/InkingControl.tsx                 |  5 ++--
 src/client/views/MainOverlayTextBox.tsx            |  2 +-
 src/client/views/MainView.tsx                      |  2 ++
 src/client/views/TemplateMenu.tsx                  | 16 ++++++-------
 .../views/collections/CollectionDockingView.tsx    |  1 +
 .../views/collections/CollectionSchemaView.tsx     |  1 +
 src/client/views/collections/CollectionSubView.tsx |  1 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 27 +++++++++++-----------
 .../collections/collectionFreeForm/MarqueeView.tsx |  2 --
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 13 +++++++----
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/FormattedTextBox.tsx        |  2 +-
 .../views/presentationview/PresentationElement.tsx |  1 +
 src/client/views/search/SearchItem.tsx             |  1 +
 src/new_fields/Doc.ts                              |  2 +-
 19 files changed, 51 insertions(+), 51 deletions(-)

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

diff --git a/src/client/util/RichTextRules.ts b/src/client/util/RichTextRules.ts
index c0c62463a..c727eec73 100644
--- a/src/client/util/RichTextRules.ts
+++ b/src/client/util/RichTextRules.ts
@@ -61,7 +61,7 @@ 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) {
                     (Cast(FormattedTextBox.InputBoxOverlay!.props.Document, Doc) as Doc).heading = Number(match[1]);
@@ -74,7 +74,7 @@ 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] = "center";
@@ -88,7 +88,7 @@ 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";
@@ -100,7 +100,7 @@ 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] = "right";
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index c376b6f86..84d045e6f 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -496,7 +496,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 +505,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;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 6d63e8f73..ebdf2a749 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -365,14 +365,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                     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!));
-                    }
+                    topDocView.props.Document.subBulletDocs = new List<Doc>(docViews.filter(v => v !== topDocView).map(v => v.props.Document.proto!));
                 }
             }
             this._removeIcon = false;
@@ -439,7 +432,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         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 = cv && cv.props.ruleProvider;
             let heading = NumCast(dv.props.Document.heading);
             ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
             usingRule = usingRule || (ruleProvider && heading ? true : false);
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 867735c0b..86d0fc0be 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -71,9 +71,8 @@ export class InkingControl extends React.Component {
                     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 && (Doc.GetProto(ruleProvider ? ruleProvider : cv.props.Document)["ruleColor_" + NumCast(view.props.Document.heading)] = Utils.toRGBAstr(color.rgb));
+                    let ruleProvider = cv && (Cast(cv.props.ruleProvider, Doc) as Doc);
+                    cv && (Doc.GetProto(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);
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index c3a2cb214..71fb2707d 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -72,7 +72,6 @@ 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;
@@ -143,6 +142,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                                 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}
                                 ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b64986084..2cec1c052 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -322,6 +322,7 @@ export class MainView extends React.Component {
                             addDocTab={emptyFunction}
                             pinToPres={emptyFunction}
                             onClick={undefined}
+                            ruleProvider={undefined}
                             removeDocument={undefined}
                             ScreenToLocalTransform={Transform.Identity}
                             ContentScaling={returnOne}
@@ -385,6 +386,7 @@ export class MainView extends React.Component {
             addDocTab={this.addDocTabFunc}
             pinToPres={emptyFunction}
             removeDocument={undefined}
+            ruleProvider={undefined}
             onClick={undefined}
             ScreenToLocalTransform={Transform.Identity}
             ContentScaling={returnOne}
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 0ef1a137d..060191e29 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -51,16 +51,16 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     @observable private _hidden: boolean = true;
     dragRef = React.createRef<HTMLUListElement>();
 
-    constructor(props: TemplateMenuProps) {
-        super(props);
-    }
-
     toggleCustom = (e: React.MouseEvent): void => {
         this.props.docs.map(dv => {
-            if (dv.Document.type !== DocumentType.COL && dv.Document.type !== DocumentType.TEMPLATE) {
-                dv.makeCustomViewClicked();
-            } else if (dv.Document.nativeLayout) {
-                dv.makeNativeViewClicked();
+            if (dv.props.ContainingCollectionView && dv.props.ContainingCollectionView.props.DataDoc) {
+                Doc.MakeMetadataFieldTemplate(dv.props.Document, dv.props.ContainingCollectionView.props.DataDoc)
+            } else {
+                if (dv.Document.type !== DocumentType.COL && dv.Document.type !== DocumentType.TEMPLATE) {
+                    dv.makeCustomViewClicked();
+                } else if (dv.Document.nativeLayout) {
+                    dv.makeNativeViewClicked();
+                }
             }
         });
     }
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index fb8b0c41b..166fa0811 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -631,6 +631,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}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 9d83aa6c1..dca1d7c1d 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -995,6 +995,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                         Document={this.props.Document}
                         fitToBox={this.props.fitToBox}
                         onClick={this.props.onClick}
+                        ruleProvider={undefined}
                         showOverlays={this.props.showOverlays}
                         addDocument={this.props.addDocument}
                         removeDocument={this.props.removeDocument}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 99e5ab7b3..270916522 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -34,6 +34,7 @@ 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) {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 9a8ae3535..4a3e5039a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -257,18 +257,14 @@ 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
-        let heading = this.childDocs.reduce((maxHeading, doc) => NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading, 0);
-        heading = heading === 0 || this.childDocs.length === 0 ? 1 : heading === 1 ? 2 : 0;
+        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 ? 1 : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
-        }
-        newBox.heading = heading;
-
-        if (Cast(this.props.Document.ruleProvider, Doc) as Doc) {
-            newBox.ruleProvider = Doc.GetProto(Cast(this.props.Document.ruleProvider, Doc) as Doc);
+            heading = !sorted.length ? Math.max(1, maxHeading) : NumCast(sorted[sorted.length - 1].heading) === 1 ? 2 : NumCast(sorted[sorted.length - 1].heading);
         }
+        !this.props.Document.isRuleProvider && (newBox.heading = heading);
         this.addDocument(newBox, false);
     }
     private addDocument = (newBox: Doc, allowDuplicates: boolean) => {
@@ -698,6 +694,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             addDocument: this.props.addDocument,
             removeDocument: this.props.removeDocument,
             moveDocument: this.props.moveDocument,
+            ruleProvider: this.props.Document.isRuleProvider && childLayout.type !== DocumentType.TEXT ? this.props.Document : this.props.ruleProvider,
             onClick: this.props.onClick,
             ScreenToLocalTransform: childLayout.z ? this.getTransformOverlay : this.getTransform,
             renderDepth: this.props.renderDepth + 1,
@@ -723,6 +720,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,
@@ -817,6 +815,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     if (pair.layout && !(pair.data instanceof Promise)) {
                         prev.push({
                             ele: <CollectionFreeFormDocumentView key={doc[Id]}
+                                ruleProvider={this.props.Document.isRuleProvider ? this.props.Document : this.props.ruleProvider}
                                 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)} />,
@@ -873,6 +872,11 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         }, "arrange contents");
     }
 
+    autoFormat = () => {
+        this.props.Document.isRuleProvider = !this.props.Document.isRuleProvider;
+        this.childDocs.map(child => child.heading = undefined);
+    }
+
     analyzeStrokes = async () => {
         let data = Cast(this.fieldExtensionDoc[this.inkKey], InkField);
         if (!data) {
@@ -900,11 +904,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             },
             icon: !this.props.Document.useClusters ? "braille" : "braille"
         });
-        layoutItems.push({
-            description: `${this.props.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`,
-            event: () => this.props.Document.isRuleProvider = !this.props.Document.isRuleProvider,
-            icon: !this.props.Document.useClusters ? "chalkboard" : "chalkboard"
-        });
+        layoutItems.push({ description: `${this.props.Document.isRuleProvider ? "Stop Auto Format" : "Auto Format"}`, event: this.autoFormat, icon: !this.props.Document.isRuleProvider ? "chalkboard" : "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" });
@@ -1034,7 +1034,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 4308497a1..e46e8cb88 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -329,7 +329,6 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 selected = [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;
@@ -356,7 +355,6 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
                 this.props.addLiveTextDocument(summary);
             }
             else {
-                newCollection.ruleProvider = this.props.container.props.Document.isRuleProvider ? this.props.container.props.Document : this.props.container.props.Document.ruleProvider;
                 this.props.addDocument(newCollection, false);
                 this.props.selectDocuments([newCollection]);
             }
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 082e5c5e3..4872a7aa1 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -74,7 +74,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     }
 
     borderRounding = () => {
-        let ruleProvider = this.props.Document.ruleProvider as Doc;
+        let ruleProvider = this.props.ruleProvider;
         let ruleRounding = ruleProvider ? StrCast(Doc.GetProto(ruleProvider)["ruleRounding_" + NumCast(this.props.Document.heading)]) : undefined;
         let br = StrCast(this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout.borderRounding : this.props.Document.borderRounding);
         br = !br && ruleRounding ? ruleRounding : br;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0816cb813..cc04c5a9f 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,6 +1,6 @@
 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, IReactionDisposer, reaction, 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";
@@ -9,12 +9,13 @@ 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";
 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";
@@ -35,12 +36,10 @@ 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';
 import React = require("react");
-import { DocumentType } from '../../documents/DocumentTypes';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(fa.faTrash);
@@ -89,6 +88,7 @@ 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;
@@ -470,6 +470,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     this.props.Document.type === DocumentType.VID ? Docs.Create.VideoDocument("http://www.cs.brown.edu", options) :
                         Docs.Create.ImageDocument("http://www.cs.brown.edu", options);
 
+                fieldTemplate.backgroundColor = StrCast(this.props.Document.backgroundColor);
+                fieldTemplate.heading = 1;
+
                 let docTemplate = Docs.Create.FreeformDocument([fieldTemplate], { title: StrCast(this.Document.title) + "layout", width: NumCast(this.props.Document.width) + 20, height: Math.max(100, NumCast(this.props.Document.height) + 45) });
                 let proto = Doc.GetProto(docTemplate);
                 Doc.MakeMetadataFieldTemplate(fieldTemplate, proto);
@@ -800,7 +803,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
 
     render() {
-        let ruleProvider = this.props.Document.ruleProvider as Doc;
+        let ruleProvider = this.props.ruleProvider;
         let ruleColor = ruleProvider ? StrCast(Doc.GetProto(ruleProvider)["ruleColor_" + NumCast(this.props.Document.heading)]) : undefined;
         let ruleRounding = ruleProvider ? StrCast(Doc.GetProto(ruleProvider)["ruleRounding_" + NumCast(this.props.Document.heading)]) : undefined;
         let colorSet = this.layoutDoc.backgroundColor !== this.layoutDoc.defaultBackgroundColor;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index d9774303b..943d181d6 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,6 +30,7 @@ export interface FieldViewProps {
     leaveNativeSize?: boolean;
     fitToBox?: boolean;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+    ruleProvider: Doc | undefined;
     Document: Doc;
     DataDoc?: Doc;
     onClick?: ScriptField;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index d39291743..a0dc054cf 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -458,7 +458,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
 
         this._rulesReactionDisposer = reaction(() => {
-            let ruleProvider = Cast(this.props.Document.ruleProvider, Doc);
+            let ruleProvider = this.props.ruleProvider;
             let heading = NumCast(this.props.Document.heading);
             if (ruleProvider instanceof Doc) {
                 return {
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 80aa25f48..7be44faf6 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}
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index c56d093fa..cd3dd912c 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -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}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1a3d689bb..5b22a62a1 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -560,7 +560,7 @@ export namespace Doc {
 
     export function MakeMetadataFieldTemplate(fieldTemplate: Doc, templateDataDoc: Doc) {
         // move data doc fields to layout doc as needed (nativeWidth/nativeHeight, data, ??)
-        let metadataFieldName = StrCast(fieldTemplate.title);
+        let metadataFieldName = StrCast(fieldTemplate.title).replace(/^-/, "");
         let backgroundLayout = StrCast(fieldTemplate.backgroundLayout);
         let fieldLayoutDoc = fieldTemplate;
         if (fieldTemplate.layout instanceof Doc) {
-- 
cgit v1.2.3-70-g09d2


From e2e642dfb3d71ea37c4d521d93ab16f166cc63cf Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sat, 14 Sep 2019 05:06:59 -0400
Subject: update routine

---
 src/client/northstar/utils/Extensions.ts           |  8 +--
 .../util/Import & Export/DirectoryImportBox.scss   |  6 ++
 .../util/Import & Export/DirectoryImportBox.tsx    | 70 ++++++++++++++++------
 src/client/util/UtilExtensions.ts                  | 20 +++++--
 src/client/views/MainView.tsx                      |  6 --
 src/server/credentials/google_docs_token.json      |  2 +-
 6 files changed, 78 insertions(+), 34 deletions(-)
 create mode 100644 src/client/util/Import & Export/DirectoryImportBox.scss

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

diff --git a/src/client/northstar/utils/Extensions.ts b/src/client/northstar/utils/Extensions.ts
index 00c1e113c..f1fddf6c8 100644
--- a/src/client/northstar/utils/Extensions.ts
+++ b/src/client/northstar/utils/Extensions.ts
@@ -20,10 +20,10 @@ String.prototype.Truncate = function (length: number, replacement: string): Stri
     return target;
 };
 
-type BatchConverterSync<I, O> = (batch: I[]) => O[];
-type BatchHandlerSync<I> = (batch: I[]) => void;
-type BatchConverterAsync<I, O> = (batch: I[]) => Promise<O[]>;
-type BatchHandlerAsync<I> = (batch: I[]) => Promise<void>;
+type BatchConverterSync<I, O> = (batch: I[], isFullBatch: boolean) => O[];
+type BatchHandlerSync<I> = (batch: I[], isFullBatch: boolean) => void;
+type BatchConverterAsync<I, O> = (batch: I[], isFullBatch: boolean) => Promise<O[]>;
+type BatchHandlerAsync<I> = (batch: I[], isFullBatch: boolean) => Promise<void>;
 type BatchConverter<I, O> = BatchConverterSync<I, O> | BatchConverterAsync<I, O>;
 type BatchHandler<I> = BatchHandlerSync<I> | BatchHandlerAsync<I>;
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.scss b/src/client/util/Import & Export/DirectoryImportBox.scss
new file mode 100644
index 000000000..d33cb524b
--- /dev/null
+++ b/src/client/util/Import & Export/DirectoryImportBox.scss	
@@ -0,0 +1,6 @@
+.phase {
+    position: absolute;
+    top: 15px;
+    left: 15px;
+    font-style: italic;
+}
\ 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 93ab5cb3b..7634d8234 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,9 +1,8 @@
 import "fs";
 import React = require("react");
-import { Doc, Opt, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { DocServer } from "../../DocServer";
+import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
 import { RouteStore } from "../../../server/RouteStore";
-import { action, observable, autorun, runInAction, computed } from "mobx";
+import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
 import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
 import Measure, { ContentRect } from "react-measure";
 import { library } from '@fortawesome/fontawesome-svg-core';
@@ -20,6 +19,7 @@ import { Cast, BoolCast, NumCast } from "../../../new_fields/Types";
 import { listSpec } from "../../../new_fields/Schema";
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
+import "./DirectoryImportBox.scss";
 
 const unsupported = ["text/html", "text/plain"];
 interface FileResponse {
@@ -34,6 +34,8 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     @observable private top = 0;
     @observable private left = 0;
     private dimensions = 50;
+    @observable private phase = "";
+    private disposer: Opt<IReactionDisposer>;
 
     @observable private entries: ImportMetadataEntry[] = [];
 
@@ -73,7 +75,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     }
 
     handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
-        runInAction(() => this.uploading = true);
+        runInAction(() => {
+            this.uploading = true;
+            this.phase = "Initializing download...";
+        });
 
         let docs: Doc[] = [];
 
@@ -108,8 +113,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             return (await fetch(Utils.prepend(RouteStore.upload), parameters)).json();
         };
 
+        runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
+
         const uploads = await validated.convertInBatchesAsync<FileResponse>(15, uploadLocally);
 
+        runInAction(() => this.phase = `Creating documents from uploads...`);
+
         await Promise.all(uploads.map(async upload => {
             const type = upload.type;
             const path = Utils.prepend(upload.path);
@@ -151,8 +160,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 const headers = ["title", "size"].map(key => new SchemaHeaderField(key));
                 importContainer = Docs.Create.SchemaDocument(headers, docs, options);
             }
-            await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
+            runInAction(() => this.phase = 'External: uploading files to Google Photos...');
             importContainer.singleColumn = false;
+            await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
+            runInAction(() => this.phase = 'All files uploaded to Google Photos...');
             Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
             !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
             DocumentManager.Instance.jumpToDocument(importContainer, true);
@@ -168,6 +179,14 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
     componentDidMount() {
         this.selector.current!.setAttribute("directory", "");
         this.selector.current!.setAttribute("webkitdirectory", "");
+        this.disposer = reaction(
+            () => this.completed,
+            completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)
+        );
+    }
+
+    componentWillUnmount() {
+        this.disposer && this.disposer();
     }
 
     @action
@@ -218,10 +237,38 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
         percent = percent.split(".")[0];
         percent = percent.startsWith("100") ? "99" : percent;
         let marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
+        const message = <span className={"phase"}>{this.phase}</span>;
+        const centerPiece = this.phase.includes("Google Photos") ?
+            <img src={"/assets/google_photos.png"} style={{
+                transition: "0.4s opacity ease",
+                width: 30,
+                height: 30,
+                opacity: uploading ? 1 : 0,
+                pointerEvents: "none",
+                position: "absolute",
+                left: 12,
+                top: this.top + 10,
+                fontSize: 18,
+                color: "white",
+                marginLeft: this.left + marginOffset
+            }} />
+            : <div
+                style={{
+                    transition: "0.4s opacity ease",
+                    opacity: uploading ? 1 : 0,
+                    pointerEvents: "none",
+                    position: "absolute",
+                    left: 10,
+                    top: this.top + 12.3,
+                    fontSize: 18,
+                    color: "white",
+                    marginLeft: this.left + marginOffset
+                }}>{percent}%</div>;
         return (
             <Measure offset onResize={this.preserveCentering}>
                 {({ measureRef }) =>
                     <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+                        {message}
                         <input
                             id={"selector"}
                             ref={this.selector}
@@ -294,18 +341,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                                 opacity: showRemoveLabel ? 1 : 0,
                                 transition: "0.4s opacity ease"
                             }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
-                        <div
-                            style={{
-                                transition: "0.4s opacity ease",
-                                opacity: uploading ? 1 : 0,
-                                pointerEvents: "none",
-                                position: "absolute",
-                                left: 10,
-                                top: this.top + 12.3,
-                                fontSize: 18,
-                                color: "white",
-                                marginLeft: this.left + marginOffset
-                            }}>{percent}%</div>
+                        {centerPiece}
                         <div
                             style={{
                                 position: "absolute",
diff --git a/src/client/util/UtilExtensions.ts b/src/client/util/UtilExtensions.ts
index eca10c3b1..0b325bc51 100644
--- a/src/client/util/UtilExtensions.ts
+++ b/src/client/util/UtilExtensions.ts
@@ -12,7 +12,8 @@ module.exports.Batch = function <T>(batchSize: number): T[][] {
 module.exports.ExecuteBatches = function <I, O>(batchSize: number, handler: BatchHandlerSync<I>): void {
     if (this.length) {
         for (let batch of this.batch(batchSize)) {
-            handler(batch);
+            const isFullBatch = batch.length === batchSize;
+            handler(batch, isFullBatch);
         }
     }
 };
@@ -23,7 +24,8 @@ module.exports.ConvertInBatches = function <I, O>(batchSize: number, handler: Ba
     }
     let collector: O[] = [];
     for (let batch of this.batch(batchSize)) {
-        collector.push(...handler(batch));
+        const isFullBatch = batch.length === batchSize;
+        collector.push(...handler(batch, isFullBatch));
     }
     return collector;
 };
@@ -31,7 +33,8 @@ module.exports.ConvertInBatches = function <I, O>(batchSize: number, handler: Ba
 module.exports.ExecuteInBatchesAsync = async function <I>(batchSize: number, handler: BatchHandler<I>): Promise<void> {
     if (this.length) {
         for (let batch of this.batch(batchSize)) {
-            await handler(batch);
+            const isFullBatch = batch.length === batchSize;
+            await handler(batch, isFullBatch);
         }
     }
 };
@@ -42,7 +45,8 @@ module.exports.ConvertInBatchesAsync = async function <I, O>(batchSize: number,
     }
     let collector: O[] = [];
     for (let batch of this.batch(batchSize)) {
-        collector.push(...(await handler(batch)));
+        const isFullBatch = batch.length === batchSize;
+        collector.push(...(await handler(batch, isFullBatch)));
     }
     return collector;
 };
@@ -59,7 +63,9 @@ module.exports.ExecuteInBatchesAtInterval = async function <I>(batchSize: number
             const next = iterator.next();
             await new Promise<void>(resolve => {
                 setTimeout(async () => {
-                    await handler(next.value);
+                    const batch = next.value;
+                    const isFullBatch = batch.length === batchSize;
+                    await handler(batch, isFullBatch);
                     resolve();
                 }, interval * 1000);
             });
@@ -84,7 +90,9 @@ module.exports.ConvertInBatchesAtInterval = async function <I, O>(batchSize: num
             const next = iterator.next();
             await new Promise<void>(resolve => {
                 setTimeout(async () => {
-                    collector.push(...(await handler(next.value)));
+                    const batch = next.value;
+                    const isFullBatch = batch.length === batchSize;
+                    collector.push(...(await handler(batch, isFullBatch)));
                     resolve();
                 }, interval * 1000);
             });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f7b66cae3..d1e0733a7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -576,12 +576,6 @@ export class MainView extends React.Component {
                             </button>
                         </div></li>)}
                     <li key="undoTest"><button className="add-button round-button" title="Click if undo isn't working" onClick={() => UndoManager.TraceOpenBatches()}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
-                    {ClientUtils.RELEASE ? [] : [
-                        <li key="test"><button className="add-button round-button" title="Default" onClick={() => this.setWriteMode(DocServer.WriteMode.Default)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
-                        <li key="test1"><button className="add-button round-button" title="Playground" onClick={() => this.setWriteMode(DocServer.WriteMode.Playground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
-                        <li key="test2"><button className="add-button round-button" title="Live Playground" onClick={() => this.setWriteMode(DocServer.WriteMode.LivePlayground)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>,
-                        <li key="test3"><button className="add-button round-button" title="Live Readonly" onClick={() => this.setWriteMode(DocServer.WriteMode.LiveReadonly)}><FontAwesomeIcon icon="exclamation" size="sm" /></button></li>
-                    ]}
                     <li key="color"><button className="add-button round-button" title="Select Color" style={{ zIndex: 1000 }} onClick={() => this.toggleColorPicker()}><div className="toolbar-color-button" style={{ backgroundColor: InkingControl.Instance.selectedColor }} >
                         <div className="toolbar-color-picker" onClick={this.onColorClick} style={this._colorPickerDisplay ? { color: "black", display: "block" } : { color: "black", display: "none" }}>
                             <SketchPicker color={InkingControl.Instance.selectedColor} onChange={InkingControl.Instance.switchColor} />
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index a5197b5f3..5d7fe5455 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"access_token":"ya29.ImCDByMCQwL1GEi2BT-yGcoD7Y1Bn-UY-N-Nguu5mijnhOkahApylC6HAJJ8mbkoGdFxywbn-yIkJkr6xQZkZMa2TW29ekjl_791aEvnr_ZUn2O-xJ7r909Rm1FuOhcGdwQ","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":1568448563751}
\ No newline at end of file
+{"access_token":"ya29.ImCDB3rM9R_0Sg7kblgjz0CJvz43FxDGnbC97xBSDhV20QyzQLrom5uJB-eEOOwrvRbP3e4LiumtI_ghx-6a0q9ydDsctF0-0Npohmo5cWFtwpcph7bmbBoPmJIJvQrjQ1Q","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":1568452654650}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 14da721a8cf5362d50e946a61617c25be2149828 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 15 Sep 2019 13:56:56 -0400
Subject: drag drop of album

---
 .../apis/google_docs/GooglePhotosClientUtils.ts       | 19 ++++++++++++++++---
 src/client/views/MainView.tsx                         |  2 +-
 src/client/views/collections/CollectionSubView.tsx    |  8 +++++++-
 src/server/credentials/google_docs_token.json         |  2 +-
 4 files changed, 25 insertions(+), 6 deletions(-)

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

diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index f3f652ce1..dfc4a6ddf 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -121,7 +121,7 @@ export namespace GooglePhotos {
         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 response = await Query.ContentSearch(requested);
             let uploads = await Transactions.WriteMediaItemsToServer(response);
             const children = uploads.map((upload: Transactions.UploadInformation) => {
                 let document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean));
@@ -149,7 +149,7 @@ export namespace GooglePhotos {
             const values = Object.values(ContentCategories);
             for (let value of values) {
                 if (value !== ContentCategories.NONE) {
-                    const results = await Search({ included: [value] });
+                    const results = await ContentSearch({ included: [value] });
                     if (results.mediaItems) {
                         const ids = results.mediaItems.map(item => item.id);
                         for (let id of ids) {
@@ -208,7 +208,20 @@ export namespace GooglePhotos {
             nextPageToken: string;
         }
 
-        export const Search = async (requested: Opt<Partial<SearchOptions>>): Promise<SearchResponse> => {
+        export const AlbumSearch = async (albumId: string, pageSize = 100): Promise<MediaItem[]> => {
+            const photos = await endpoint();
+            let mediaItems: MediaItem[] = [];
+            let nextPageTokenStored: Opt<string> = undefined;
+            let found = 0;
+            do {
+                const { mediaItems, nextPageToken } = (await photos.search(albumId, pageSize, nextPageTokenStored)) as any;
+                mediaItems.push(...mediaItems);
+                nextPageTokenStored = nextPageToken;
+            } while (found);
+            return mediaItems;
+        };
+
+        export const ContentSearch = 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);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d1e0733a7..2111dba0a 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -171,7 +171,7 @@ export class MainView extends React.Component {
         // 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 }));
-        GooglePhotos.Query.Search({ included: [GooglePhotos.ContentCategories.ANIMALS] }).then(console.log);
+        GooglePhotos.Query.ContentSearch({ included: [GooglePhotos.ContentCategories.ANIMALS] }).then(console.log);
     }
 
     componentWillUnMount() {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5fc4f36a7..aafd9460e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,6 +22,7 @@ import { CollectionPDFView } from "./CollectionPDFView";
 import { CollectionVideoView } from "./CollectionVideoView";
 import { CollectionView } from "./CollectionView";
 import React = require("react");
+import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 
 export interface CollectionViewProps extends FieldViewProps {
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
@@ -143,7 +144,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
 
         @undoBatch
         @action
-        protected onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
+        protected async onDrop(e: React.DragEvent, options: DocumentOptions, completed?: () => void) {
             if (e.ctrlKey) {
                 e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl
                 return;
@@ -218,6 +219,11 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
                 this.props.addDocument(newBox);
                 return;
             }
+            if ((matches = /(https:\/\/)?photos\.google\.com\/(u\/3\/)?album\/([^\\]+)/g.exec(text)) !== null) {
+                const albumId = matches[3];
+                const mediaItems = await GooglePhotos.Query.AlbumSearch(albumId);
+                console.log(mediaItems);
+            }
             let batch = UndoManager.StartBatch("collection view drop");
             let promises: Promise<void>[] = [];
             // tslint:disable-next-line:prefer-for-of
diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json
index 265c07c69..4f2fb0f9d 100644
--- a/src/server/credentials/google_docs_token.json
+++ b/src/server/credentials/google_docs_token.json
@@ -1 +1 @@
-{"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
+{"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
-- 
cgit v1.2.3-70-g09d2


From be5456616a7813572b9fdc9637d4009e7283a46a Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 19 Sep 2019 00:11:23 -0400
Subject: added ContainingCollectionDoc to props that get passed around.

---
 src/client/documents/Documents.ts                  |   2 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |   5 +-
 src/client/util/TooltipTextMenu.tsx                |   4 +-
 src/client/views/DocumentDecorations.tsx           |  11 +-
 src/client/views/InkingControl.tsx                 |  20 +--
 src/client/views/MainOverlayTextBox.tsx            |   6 +-
 src/client/views/MainView.tsx                      |   2 +
 src/client/views/OverlayView.tsx                   |   1 +
 .../views/collections/CollectionDockingView.tsx    |   1 +
 .../views/collections/CollectionSchemaCells.tsx    |   1 +
 .../CollectionSchemaMovableTableHOC.tsx            |   8 +-
 .../views/collections/CollectionSchemaView.tsx     |   3 +
 .../CollectionFreeFormLinksView.tsx                |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   2 +
 src/client/views/linking/LinkFollowBox.tsx         |  30 ++--
 src/client/views/nodes/DocumentView.tsx            | 164 ++++++++++-----------
 src/client/views/nodes/FieldView.tsx               |   1 +
 src/client/views/nodes/KeyValuePair.tsx            |   1 +
 src/client/views/nodes/PDFBox.tsx                  |   2 +-
 .../views/presentationview/PresentationElement.tsx |   1 +
 src/client/views/search/FilterBox.tsx              |   6 +-
 src/client/views/search/SearchItem.tsx             |   1 +
 22 files changed, 136 insertions(+), 142 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 99707db19..2fa0d2dcb 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -640,7 +640,7 @@ export namespace DocUtils {
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", sourceContext?: Doc, id?: string, anchored1?: boolean) {
         if (LinkManager.Instance.doesLinkExist(source, target)) return undefined;
         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);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 75b0b52a7..dc6a0cb7a 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -134,11 +134,10 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
             x: NumCast(doc.x),
             y: NumCast(doc.y) + offset
         };
-        let parent = this.props.ContainingCollectionView;
-        if (parent) {
+        if (this.props.ContainingCollectionDoc) {
             let importContainer = Docs.Create.StackingDocument(docs, options);
             importContainer.singleColumn = false;
-            Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+            Doc.AddDocToList(Doc.GetProto(this.props.ContainingCollectionDoc), "data", importContainer);
             !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
             DocumentManager.Instance.jumpToDocument(importContainer, true);
 
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index 5764af282..aa096dd64 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -310,8 +310,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");
                             }),
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index b730f88a4..ad38d16aa 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -152,8 +152,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 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);
                     if (metaKey !== containerView.props.fieldKey && containerView.props.DataDoc) {
                         const fd = fieldTemplate.data;
@@ -431,8 +431,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         dist = dist < 3 ? 0 : dist;
         let usingRule = false;
         SelectionManager.SelectedDocuments().map(dv => {
-            let cv = dv.props.ContainingCollectionView;
-            let ruleProvider = cv && cv.props.ruleProvider;
+            let ruleProvider = dv.props.ruleProvider;
             let heading = NumCast(dv.props.Document.heading);
             ruleProvider && heading && (Doc.GetProto(ruleProvider)["ruleRounding_" + heading] = `${Math.min(100, dist)}%`);
             usingRule = usingRule || (ruleProvider && heading ? true : false);
@@ -502,7 +501,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
             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 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");
@@ -847,7 +846,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
 
         let templates: Map<Template, boolean> = new Map();
         Array.from(Object.values(Templates.TemplateList)).map(template =>
-            templates.set(template, SelectionManager.SelectedDocuments().reduce((checked, doc) => checked || (doc.props.Document["show" + template.Name] ? true : false), false)));
+            templates.set(template, SelectionManager.SelectedDocuments().reduce((checked, doc) => checked || (doc.props.Document["show" + template.Name] ? true : false), false as boolean)));
 
         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;
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 57dad5e6b..a10df0e75 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -52,16 +52,16 @@ export class InkingControl extends React.Component {
                 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);
                 let matchedColor = this._selectedColor;
-                const cv = view.props.ContainingCollectionView;
-                let ruleProvider: Doc | undefined;
-                if (cv) {
-                    if (!cv.props.Document.colorPalette) {
-                        let defaultPalette = ["rg14,229,239)", "rgb(255,246,209)", "rgb(255,188,156)", "rgb(247,220,96)", "rgb(122,176,238)",
+                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(cv.props.Document.colorPalette, listSpec("string"));
-                        if (!colorPalette) cv.props.Document.colorPalette = new List<string>(defaultPalette);
+                        let colorPalette = Cast(cvd.colorPalette, listSpec("string"));
+                        if (!colorPalette) cvd.colorPalette = new List<string>(defaultPalette);
                     }
-                    let cp = Cast(cv.props.Document.colorPalette, listSpec("string")) as string[];
+                    let cp = Cast(cvd.colorPalette, listSpec("string")) as string[];
                     let closest = 0;
                     let dist = 10000000;
                     let ccol = Utils.fromRGBAstr(StrCast(targetDoc.backgroundColor));
@@ -74,9 +74,9 @@ export class InkingControl extends React.Component {
                         }
                     }
                     cp[closest] = "rgba(" + color.rgb.r + "," + color.rgb.g + "," + color.rgb.b + "," + color.rgb.a + ")";
-                    cv.props.Document.colorPalette = new List(cp);
+                    cvd.colorPalette = new List(cp);
                     matchedColor = cp[closest];
-                    ruleProvider = (view.props.Document.heading && cv && cv.props.ruleProvider) ? cv.props.ruleProvider : undefined;
+                    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);
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 8e72d236c..ac1b437af 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -142,9 +142,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                                 DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
                                 onClick={undefined}
                                 ruleProvider={this._textBox ? this._textBox.props.ruleProvider : undefined}
-                                ChromeHeight={this.ChromeHeight}
-                                isSelected={returnTrue} select={emptyFunction} renderDepth={0}
-                                ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue} ContentScaling={returnOne}
+                                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 2cec1c052..8b0caf9a6 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -335,6 +335,7 @@ export class MainView extends React.Component {
                             whenActiveChanged={emptyFunction}
                             bringToFront={emptyFunction}
                             ContainingCollectionView={undefined}
+                            ContainingCollectionDoc={undefined}
                             zoomToScale={emptyFunction}
                             getScale={returnOne}
                         />}
@@ -399,6 +400,7 @@ export class MainView extends React.Component {
             whenActiveChanged={emptyFunction}
             bringToFront={emptyFunction}
             ContainingCollectionView={undefined}
+            ContainingCollectionDoc={undefined}
             zoomToScale={emptyFunction}
             getScale={returnOne}>
         </DocumentView>;
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 15faea3cd..8124ce653 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -189,6 +189,7 @@ export class OverlayView extends React.Component {
                     addDocTab={emptyFunction}
                     pinToPres={emptyFunction}
                     ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
                     zoomToScale={emptyFunction}
                     getScale={returnOne} />
             </div>;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 55ba3a314..cab085d9b 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -639,6 +639,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 3452e8702..059967a7d 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -151,6 +151,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             fieldExt: "",
             ruleProvider: undefined,
             ContainingCollectionView: this.props.CollectionView,
+            ContainingCollectionDoc: this.props.CollectionView.props.Document,
             isSelected: returnFalse,
             select: emptyFunction,
             renderDepth: this.props.renderDepth + 1,
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 4d2b8e1c1..a742054d0 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -196,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}
@@ -247,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;
@@ -1004,6 +1006,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                         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/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 c9e78cee6..ad77e0428 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -659,6 +659,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             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,
@@ -685,6 +686,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,
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index b1c6c367f..d7cb2585e 100644
--- a/src/client/views/linking/LinkFollowBox.tsx
+++ b/src/client/views/linking/LinkFollowBox.tsx
@@ -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 => {
@@ -371,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));
             }
         }
     }
@@ -394,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;
@@ -457,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/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index d4e396752..39bba2eb2 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 | CollectionPDFView | CollectionVideoView>;
+    ContainingCollectionDoc: Opt<Doc>;
     Document: Doc;
     DataDoc?: Doc;
     fitToBox?: boolean;
@@ -208,88 +209,83 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     onClick = async (e: React.MouseEvent) => {
-        if (e.nativeEvent.cancelBubble) return; // || SelectionManager.IsSelected(this)) -- bcz: 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.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 });
+            } 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);
-            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");
+    }
+
+    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 (!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();
-            SelectionManager.SelectDoc(this, e.ctrlKey);
-            if (this.Document.isButton || this.Document.type === DocumentType.BUTTON) {
-                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();
-                    let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace");
-                    let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
-                    if (altKey || ctrlKey) {
-                        maxLocation = this.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 && !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);
+            let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace");
+            let getDispDoc = (target: Doc) => Object.getOwnPropertyNames(target).indexOf("isPrototype") === -1 ? target : Doc.MakeDelegate(target);
+            if (altKey || ctrlKey) {
+                maxLocation = this.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 && !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);
                 }
-                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.ContainingCollectionView && this.props.ContainingCollectionView.props.Document) ? 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); return true; }),
-                            linkedFwdPage[altKey ? 1 : 0], targetContext);
-                    }
+            }
+            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) && !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); return true; }),
+                    linkedFwdPage[altKey ? 1 : 0], targetContext);
             }
         }
     }
@@ -387,25 +383,21 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             let targetDoc = this.props.Document;
             let annotations = await DocListCastAsync(sourceDoc.annotations);
             sourceDoc.linkedToDoc = true;
-            de.data.targetContext = this.props.ContainingCollectionView!.props.Document;
+            de.data.targetContext = this.props.ContainingCollectionDoc;
             targetDoc.targetContext = de.data.targetContext;
             annotations && annotations.forEach(anno => anno.target = targetDoc);
 
-            DocUtils.MakeLink(sourceDoc, targetDoc, this.props.ContainingCollectionView!.props.Document, `Link from ${StrCast(sourceDoc.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);
             e.stopPropagation();
         }
         if (de.data instanceof DragManager.LinkDragData) {
-            let sourceDoc = de.data.linkSourceDocument;
-            let destDoc = this.props.Document;
-
             e.stopPropagation();
             // 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.droppedDocuments.push(destDoc);
-            de.data.linkDocument = DocUtils.MakeLink(sourceDoc, destDoc, this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document);
+            de.data.linkDocument = DocUtils.MakeLink(de.data.linkSourceDocument, this.props.Document, this.props.ContainingCollectionDoc);
         }
     }
 
@@ -436,7 +428,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) => await Cast(d.anchor2, Doc)));
+        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 => {
@@ -512,7 +504,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         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");
             }
         });
@@ -637,7 +629,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         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.Document.backgroundColor !== this.Document.defaultBackgroundColor;
-        const clusterCol = this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document.clusterOverridesDefaultBackground;
+        const clusterCol = this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.clusterOverridesDefaultBackground;
         const backgroundColor = this.Document.isBackground || (clusterCol && !colorSet) ?
             this.props.backgroundColor(this.Document) || StrCast(this.Document.backgroundColor) :
             ruleColor && !colorSet ? ruleColor : StrCast(this.Document.backgroundColor) || this.props.backgroundColor(this.Document);
@@ -753,7 +745,7 @@ export async function swapViews(doc: Doc, newLayoutField: string, oldLayoutField
         doc.type = newLayoutExt.type;
         doc.layout = await newLayoutExt.layout;
     }
-};
+}
 
 Scripting.addGlobal(function toggleDetail(doc: any) {
     let native = typeof doc.layout === "string";
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 943d181d6..faf11e9be 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,6 +30,7 @@ export interface FieldViewProps {
     leaveNativeSize?: boolean;
     fitToBox?: boolean;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
+    ContainingCollectionDoc: Opt<Doc>;
     ruleProvider: Doc | undefined;
     Document: Doc;
     DataDoc?: Doc;
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 7e0f3735d..c7e0f51d7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -55,6 +55,7 @@ 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: "",
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 64b84ba55..c8534ed0d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -31,7 +31,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     @observable private _alt = false;
     @observable private _pdf: Opt<Pdfjs.PDFDocumentProxy>;
 
-    @computed get containingCollectionDocument() { return this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document; }
+    @computed get containingCollectionDocument() { return this.props.ContainingCollectionDoc; }
     @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); }
 
 
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 7be44faf6..126d62c52 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -366,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/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 510672788..b2e886d95 100644
--- a/src/client/views/search/SearchItem.tsx
+++ b/src/client/views/search/SearchItem.tsx
@@ -180,6 +180,7 @@ export class SearchItem extends React.Component<SearchItemProps> {
                     zoomToScale={emptyFunction}
                     getScale={returnOne}
                     ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
                     ContentScaling={scale}
                 />
             </div>;
-- 
cgit v1.2.3-70-g09d2


From 2cdca63ac039a7c66a9c93acb35fe51467269e64 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 19 Sep 2019 11:29:11 -0400
Subject: switche calls to COllectionDockingView... to addDocTabs.  fixed
 following links to navigate/restore properly.

---
 src/client/util/DocumentManager.ts                 | 15 ++--
 src/client/util/TooltipTextMenu.tsx                |  8 +-
 src/client/views/GlobalKeyHandler.ts               |  4 +-
 src/client/views/MainOverlayTextBox.tsx            |  4 +-
 src/client/views/MainView.tsx                      | 10 ++-
 src/client/views/OverlayView.tsx                   |  2 +-
 .../views/collections/CollectionBaseView.tsx       |  4 +-
 .../views/collections/CollectionDockingView.tsx    | 85 ++++++++++++----------
 .../views/collections/CollectionSchemaCells.tsx    |  2 +-
 .../views/collections/CollectionSchemaView.tsx     |  4 +-
 .../views/collections/CollectionTreeView.tsx       |  8 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 17 +++--
 src/client/views/linking/LinkFollowBox.tsx         | 16 ++--
 src/client/views/linking/LinkMenu.tsx              |  2 +-
 src/client/views/linking/LinkMenuGroup.tsx         | 24 +++---
 src/client/views/linking/LinkMenuItem.tsx          |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 24 ++----
 src/client/views/nodes/FieldView.tsx               |  2 +-
 src/client/views/nodes/KeyValueBox.tsx             |  2 +-
 src/client/views/nodes/KeyValuePair.tsx            |  7 +-
 src/client/views/pdf/Annotation.tsx                |  8 +-
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 src/client/views/search/SearchItem.tsx             |  4 +-
 23 files changed, 127 insertions(+), 129 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ec731da84..65ab32539 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 { emptyFunction } from '../../Utils';
 
 
 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);
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index aa096dd64..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");
                             }
                         }));
                     }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index ef5d76ce8..2fa03e969 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -141,7 +141,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) {
@@ -149,7 +149,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) {
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index ac1b437af..f32fb584a 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -119,9 +119,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
     }
 
     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);
-        }
+        this._textBox && this._textBox.props.addDocTab(doc, dataDoc, location);
     }
     render() {
         this.TextDoc; this.TextDataDoc;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8b0caf9a6..097040b6c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -288,6 +288,7 @@ export class MainView extends React.Component {
                 }
             }
         }, 100);
+        return true;
     }
 
     onDrop = (e: React.DragEvent<HTMLDivElement>) => {
@@ -319,7 +320,7 @@ export class MainView extends React.Component {
                         <DocumentView Document={mainCont}
                             DataDoc={undefined}
                             addDocument={undefined}
-                            addDocTab={emptyFunction}
+                            addDocTab={this.addDocTabFunc}
                             pinToPres={emptyFunction}
                             onClick={undefined}
                             ruleProvider={undefined}
@@ -368,11 +369,14 @@ 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
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 8124ce653..45e0a3562 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -186,7 +186,7 @@ export class OverlayView extends React.Component {
                     whenActiveChanged={emptyFunction}
                     focus={emptyFunction}
                     backgroundColor={returnEmptyString}
-                    addDocTab={emptyFunction}
+                    addDocTab={returnFalse}
                     pinToPres={emptyFunction}
                     ContainingCollectionView={undefined}
                     ContainingCollectionDoc={undefined}
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index fdb2f0dc9..0399371ff 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -103,7 +103,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));
@@ -126,7 +125,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));
 
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index cab085d9b..277fa0066 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 } from "mobx";
+import { action, computed, Lambda, observable, reaction } 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';
 library.add(faFile);
 
 @observer
@@ -59,7 +58,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     constructor(props: SubCollectionViewProps) {
         super(props);
-        if (props.addDocTab === emptyFunction) CollectionDockingView.Instance = this;
+        !CollectionDockingView.Instance && (CollectionDockingView.Instance = this);
         //Why is this here?
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
@@ -121,21 +120,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;
                         }
@@ -146,7 +149,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             });
         }
         if (retVal) {
-            this.stateChanged();
+            instance.stateChanged();
         }
         return retVal;
     }
@@ -173,8 +176,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     //
     @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);
         }
@@ -183,15 +188,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);
@@ -206,9 +211,8 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             // newContentItem.config.height = 10;
         }
         newContentItem.callDownwards('_$init');
-        this.layoutChanged();
-
-        return newContentItem;
+        instance.layoutChanged();
+        return true;
     }
 
     @undoBatch
@@ -238,6 +242,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             stack.addChild(docContentConfig, undefined);
         }
         this.layoutChanged();
+        return true;
     }
 
     setupGoldenLayout() {
@@ -416,7 +421,10 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                             hideSource: false
                         });
                     }}><FontAwesomeIcon icon="file" size="lg" /></span>, dragSpan);
-                ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={doc => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv);
+                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, upDiv];
                 tab.element.append(dragSpan);
                 tab.element.append(upDiv);
@@ -604,15 +612,16 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
     get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 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() {
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 059967a7d..0306d415c 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;
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index a742054d0..7bd2a1971 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -256,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;
@@ -915,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;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index b4026a810..08d87c7b2 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -40,7 +40,7 @@ export interface TreeViewProps {
     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;
@@ -414,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 },
@@ -562,8 +562,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");
         }
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ad77e0428..7383c5551 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -508,14 +508,13 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     });
                 }
 
-                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;
+                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;
@@ -613,8 +612,10 @@ 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;
diff --git a/src/client/views/linking/LinkFollowBox.tsx b/src/client/views/linking/LinkFollowBox.tsx
index d7cb2585e..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";
@@ -195,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;
     }
 
@@ -211,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 });
 
@@ -224,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();
         }
@@ -244,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);
@@ -271,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();
@@ -288,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();
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 d711ac284..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;
 
 }
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/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4a37457c0..50691fd38 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -86,7 +86,7 @@ export interface DocumentViewProps {
     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;
@@ -242,23 +242,13 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (expandedDocs.length) {
             SelectionManager.DeselectAll();
             let maxLocation = StrCast(this.Document.maximizeLocation, "inPlace");
-            if (altKey || ctrlKey) {
-                maxLocation = this.Document.maximizeLocation = (ctrlKey ? maxLocation : (maxLocation === "inPlace" ? "inTab" : "inPlace"));
-                if (maxLocation === "inPlace") {
-                    let hadView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
-                    let wasMinimized = !hadView && expandedDocs.reduce((min, d) => !min && !d.isMinimized, false);
-                    expandedDocs.forEach(maxDoc => maxDoc.isMinimized = false);
-                    let hasView = expandedDocs.length === 1 && DocumentManager.Instance.getDocumentView(expandedDocs[0], this.props.ContainingCollectionView);
-                    !hasView && expandedDocs.forEach(async maxDoc => this.props.addDocument && this.props.addDocument(maxDoc, false));
-                    expandedDocs.forEach(maxDoc => maxDoc.isMinimized = wasMinimized);
-                }
-            }
-            if (maxLocation !== "inPlace" && CollectionDockingView.Instance) {
-                expandedDocs.forEach(maxDoc =>
-                    (!CollectionDockingView.Instance.CloseRightSplit(maxDoc) && this.props.addDocTab(maxDoc, undefined, maxLocation)));
-            } else {
+            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);
                 this.collapseTargetsToPoint(scrpt, expandedDocs);
+            } else {
+                expandedDocs.forEach(maxDoc => (!this.props.addDocTab(maxDoc, undefined, "close") && this.props.addDocTab(maxDoc, undefined, maxLocation)));
             }
         }
         else if (linkedDocs.length) {
@@ -277,7 +267,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 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); return true; }),
+                    doc => this.props.focus(this.props.Document, true, 1, () => this.props.addDocTab(doc, undefined, maxLocation)),
                     linkedFwdPage[altKey ? 1 : 0], targetContext);
             }
         }
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index faf11e9be..49fc2263d 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -39,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/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index ee70942de..8fcd44f46 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -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);
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index c7e0f51d7..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);
         }
     }
@@ -68,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/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 34e3b0931..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;
 }
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c508935f2..19ef713c2 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -35,7 +35,7 @@ interface IViewerProps {
     scrollTo: (y: number) => void;
     active: () => boolean;
     setPanY?: (n: number) => void;
-    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc | undefined, where: string) => boolean;
     pinToPres: (document: Doc) => void;
     addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
 }
diff --git a/src/client/views/search/SearchItem.tsx b/src/client/views/search/SearchItem.tsx
index b2e886d95..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 (
-- 
cgit v1.2.3-70-g09d2


From 75cf84dd29e7b397e829a137a7c9845134d05984 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 20 Sep 2019 16:51:18 -0400
Subject: switched PDF curPage to a compute field.   fixed full screen views of
 native dimensioned documents.

---
 src/client/views/MainView.tsx                          |  5 +++--
 src/client/views/collections/CollectionDockingView.tsx |  2 +-
 src/client/views/nodes/PDFBox.tsx                      | 11 ++++++-----
 3 files changed, 10 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 097040b6c..003919866 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -228,9 +228,10 @@ export class MainView extends React.Component {
                 this.createNewWorkspace();
             }
         } else {
-            DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field =>
+            DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => {
                 field instanceof Doc ? this.openWorkspace(field) :
-                    this.createNewWorkspace(CurrentUserUtils.MainDocId));
+                    this.createNewWorkspace(CurrentUserUtils.MainDocId)
+            });
         }
     }
 
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 5ace1048d..8fcba99e3 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -616,7 +616,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
         return Transform.Identity();
     }
-    get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth()) / 2 : 0; }
+    get previewPanelCenteringOffset() { return this.nativeWidth() && !BoolCast(this._document!.ignoreAspect) ? (this._panelWidth - this.nativeWidth() / this.ScreenToLocalTransform().Scale) / 2 : 0; }
 
     addDocTab = (doc: Doc, dataDoc: Opt<Doc>, location: string) => {
         if (doc.dockingConfig) {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 37bf6dbb7..ffec9f29b 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -6,7 +6,7 @@ import "pdfjs-dist/web/pdf_viewer.css";
 import 'react-image-lightbox/style.css';
 import { Doc, WidthSym, Opt } from "../../../new_fields/Doc";
 import { makeInterface } from "../../../new_fields/Schema";
-import { ScriptField } from '../../../new_fields/ScriptField';
+import { ScriptField, ComputedField } from '../../../new_fields/ScriptField';
 import { BoolCast, Cast, NumCast } from "../../../new_fields/Types";
 import { PdfField } from "../../../new_fields/URLField";
 import { KeyCodes } from '../../northstar/utils/KeyCodes';
@@ -18,6 +18,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
+import { Scripting } from '../../util/Scripting';
 
 type PdfDocument = makeInterface<[typeof documentSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(documentSchema, pageSchema);
@@ -47,6 +48,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     componentDidMount() {
         this.props.setPdfBox && this.props.setPdfBox(this);
 
+        this.props.Document.pdfDoc = this.dataDoc;
+        this.props.Document.curPage = ComputedField.MakeFunction("Math.floor(Number(this.panY) / Number(this.pdfDoc.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));
@@ -70,7 +74,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
         let cp = Math.ceil(NumCast(this.props.Document.panY) / NumCast(this.dataDoc.nativeHeight)) + 1;
         cp = cp - 1;
         if (cp > 0) {
-            this.props.Document.curPage = cp;
             this.props.Document.panY = (cp - 1) * NumCast(this.dataDoc.nativeHeight);
         }
     }
@@ -78,7 +81,6 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     @action
     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);
         }
     }
@@ -87,7 +89,6 @@ 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);
         }
     }
@@ -193,7 +194,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                 onScroll={this.onScroll}
                 style={{ marginTop: `${this.props.ContainingCollectionDoc ? NumCast(this.props.ContainingCollectionDoc.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%" }} />
+                do<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}
-- 
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/MainView.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 f4b628c2a6810c1af51508685f12287a300d6e6f Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 25 Sep 2019 22:26:54 -0400
Subject: all pdf annotations work?

---
 src/client/documents/Documents.ts                  |   7 +-
 src/client/views/DocumentButtonBar.tsx             |   5 +-
 src/client/views/MainView.tsx                      |   7 +-
 src/client/views/TemplateMenu.tsx                  |   4 +-
 .../views/collections/CollectionDockingView.tsx    |   6 +-
 .../views/collections/CollectionSchemaCells.tsx    |   6 +-
 .../views/collections/CollectionSchemaView.tsx     |   4 +-
 .../views/collections/CollectionStackingView.tsx   |   8 +-
 .../CollectionStackingViewFieldColumn.tsx          |   6 +-
 src/client/views/collections/CollectionSubView.tsx |   5 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  18 +--
 .../collections/collectionFreeForm/MarqueeView.tsx |  13 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   6 +-
 src/client/views/nodes/DocumentView.tsx            |   3 +-
 src/client/views/nodes/FormattedTextBox.tsx        |   6 +-
 src/client/views/nodes/PDFBox.scss                 |   3 +
 src/client/views/nodes/PDFBox.tsx                  |  20 ++--
 src/client/views/pdf/PDFViewer.scss                |   5 +-
 src/client/views/pdf/PDFViewer.tsx                 | 132 +++++++++++++--------
 20 files changed, 159 insertions(+), 109 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ea7a3a8b6..392dca373 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -95,12 +95,13 @@ export namespace Docs {
 
     export namespace Prototypes {
 
-        type LayoutSource = { LayoutString: () => string };
+        type LayoutSource = { LayoutString: (ext?: string) => string };
         type CollectionLayoutSource = { LayoutString: (fieldStr: string, fieldExt?: string) => string };
         type CollectionViewType = [CollectionLayoutSource, string, string?];
         type PrototypeTemplate = {
             layout: {
                 view: LayoutSource,
+                ext?: string, // optional extension field for layout source
                 collectionView?: CollectionViewType
             },
             options?: Partial<DocumentOptions>
@@ -144,7 +145,7 @@ export namespace Docs {
                 options: { height: 32 }
             }],
             [DocumentType.PDF, {
-                layout: { view: PDFBox },
+                layout: { view: PDFBox, ext: anno },
                 options: { nativeWidth: 1200, curPage: 1 }
             }],
             [DocumentType.ICON, {
@@ -254,7 +255,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: title, type: type, baseProto: true, ...defaultOptions, ...(template.options || {}) };
-            let primary = layout.view.LayoutString();
+            let primary = layout.view.LayoutString(layout.ext);
             let collectionView = layout.collectionView;
             if (collectionView) {
                 options.layout = collectionView[0].LayoutString(collectionView[1], collectionView[2]);
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index b482e3298..9ca54f738 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -23,6 +23,7 @@ import React = require("react");
 import { DocumentView } from './nodes/DocumentView';
 import { ParentDocSelector } from './collections/ParentDocumentSelector';
 import { CollectionDockingView } from './collections/CollectionDockingView';
+import { DocumentDecorations } from './DocumentDecorations';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -225,7 +226,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
         return (
             <div className={"linkButtonWrapper"}>
                 <div title={`${published ? "Push" : "Publish"} to Google Docs`} className="linkButton-linker" onClick={() => {
-                    DocumentDecorations.hasPushedHack = false;
+                    DocumentButtonBar.hasPushedHack = false;
                     this.targetDoc[Pushes] = NumCast(this.targetDoc[Pushes]) + 1;
                 }}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon={icon} size={published ? "sm" : "xs"} />
@@ -259,7 +260,7 @@ export class DocumentButtonBar extends React.Component<{ views: DocumentView[],
                             window.open(`https://docs.google.com/document/d/${dataDoc[GoogleRef]}/edit`);
                         } else {
                             this.clearPullColor();
-                            DocumentDecorations.hasPulledHack = false;
+                            DocumentButtonBar.hasPulledHack = false;
                             this.targetDoc[Pulls] = NumCast(this.targetDoc[Pulls]) + 1;
                             dataDoc.unchanged && runInAction(() => this.isAnimatingFetch = true);
                         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 003919866..244b217ed 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -230,7 +230,7 @@ export class MainView extends React.Component {
         } else {
             DocServer.GetRefField(CurrentUserUtils.MainDocId).then(field => {
                 field instanceof Doc ? this.openWorkspace(field) :
-                    this.createNewWorkspace(CurrentUserUtils.MainDocId)
+                    this.createNewWorkspace(CurrentUserUtils.MainDocId);
             });
         }
     }
@@ -371,8 +371,9 @@ export class MainView extends React.Component {
     }
     flyoutWidthFunc = () => this.flyoutWidth;
     addDocTabFunc = (doc: Doc, data: Opt<Doc>, where: string) => {
-        if (where === "close")
+        if (where === "close") {
             return CollectionDockingView.CloseRightSplit(doc);
+        }
         if (doc.dockingConfig) {
             this.openWorkspace(doc);
             return true;
@@ -564,7 +565,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, "onRight"); });
         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/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index e4ef8313d..9e5e62e03 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 as Doc : dv.Document;
+            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 as Doc : this.props.docs[0].Document;
+        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} />));
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e5d652648..2d9faee6b 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -603,9 +603,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
 
     contentScaling = () => {
         if (this._document!.type === DocumentType.PDF) {
-            if (this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth))
+            if (this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) {
                 return this._panelWidth / NumCast(this._document!.nativeWidth);
-            else return this._panelHeight / NumCast(this._document!.nativeHeight);
+            } else {
+                return this._panelHeight / NumCast(this._document!.nativeHeight);
+            }
         }
         const nativeH = this.nativeHeight();
         const nativeW = this.nativeWidth();
diff --git a/src/client/views/collections/CollectionSchemaCells.tsx b/src/client/views/collections/CollectionSchemaCells.tsx
index 4dac27e60..179e44266 100644
--- a/src/client/views/collections/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/CollectionSchemaCells.tsx
@@ -34,7 +34,7 @@ export interface CellProps {
     row: number;
     col: number;
     rowProps: CellInfo;
-    CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+    CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     ContainingCollection: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     Document: Doc;
     fieldKey: string;
@@ -151,7 +151,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             fieldExt: "",
             ruleProvider: undefined,
             ContainingCollectionView: this.props.CollectionView,
-            ContainingCollectionDoc: this.props.CollectionView.props.Document,
+            ContainingCollectionDoc: this.props.CollectionView && this.props.CollectionView.props.Document,
             isSelected: returnFalse,
             select: emptyFunction,
             renderDepth: this.props.renderDepth + 1,
@@ -301,7 +301,7 @@ export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
     render() {
         let reference = React.createRef<HTMLDivElement>();
         let onItemDown = (e: React.PointerEvent) => {
-            (!this.props.CollectionView.props.isSelected() ? undefined :
+            (!this.props.CollectionView || !this.props.CollectionView.props.isSelected() ? undefined :
                 SetupDrag(reference, () => this._document, this.props.moveDocument, this.props.Document.schemaDoc ? "copy" : undefined)(e));
         };
         return (
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 7bd2a1971..8d931f812 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -246,7 +246,7 @@ export interface SchemaTableProps {
     PanelHeight: () => number;
     PanelWidth: () => number;
     childDocs?: Doc[];
-    CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+    CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     ContainingCollectionDoc: Opt<Doc>;
     fieldKey: string;
@@ -804,7 +804,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
         csv.substring(0, csv.length - 1);
         let dbName = StrCast(this.props.Document.title);
         let res = await Gateway.Instance.PostSchema(csv, dbName);
-        if (self.props.CollectionView.props.addDocument) {
+        if (self.props.CollectionView && self.props.CollectionView.props.addDocument) {
             let schemaDoc = await Docs.Create.DBDocument("https://www.cs.brown.edu/" + dbName, { title: dbName }, { dbDoc: self.props.Document });
             if (schemaDoc) {
                 //self.props.CollectionView.props.addDocument(schemaDoc, false);
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ccf131797..597f3f745 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -42,7 +42,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     @computed get gridGap() { return NumCast(this.props.Document.gridGap, 10); }
     @computed get isStackingView() { return BoolCast(this.props.Document.singleColumn, true); }
     @computed get numGroupColumns() { return this.isStackingView ? Math.max(1, this.Sections.size + (this.showAddAGroup ? 1 : 0)) : 1; }
-    @computed get showAddAGroup() { return (this.sectionFilter && (this.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.CollectionView.props.Document.chromeStatus !== 'disabled')); }
+    @computed get showAddAGroup() { return (this.sectionFilter && this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled')); }
     @computed get columnWidth() {
         return Math.min(this.props.PanelWidth() / (this.props as any).ContentScaling() - 2 * this.xMargin,
             this.isStackingView ? Number.MAX_VALUE : NumCast(this.props.Document.columnWidth, 250));
@@ -347,7 +347,7 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
     }
 
     onToggle = (checked: Boolean) => {
-        this.props.CollectionView.props.Document.chromeStatus = checked ? "collapsed" : "view-mode";
+        this.props.ContainingCollectionDoc && (this.props.ContainingCollectionDoc.chromeStatus = checked ? "collapsed" : "view-mode");
     }
 
     onContextMenu = (e: React.MouseEvent): void => {
@@ -391,10 +391,10 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                         style={{ width: this.columnWidth / this.numGroupColumns - 10, marginTop: 10 }}>
                         <EditableView {...editableViewProps} />
                     </div>}
-                {this.props.CollectionView.props.Document.chromeStatus !== 'disabled' ? <Switch
+                {this.props.ContainingCollectionDoc && this.props.ContainingCollectionDoc.chromeStatus !== 'disabled' ? <Switch
                     onChange={this.onToggle}
                     onClick={this.onToggle}
-                    defaultChecked={this.props.CollectionView.props.Document.chromeStatus !== 'view-mode'}
+                    defaultChecked={this.props.ContainingCollectionDoc.chromeStatus !== 'view-mode'}
                     checkedChildren="edit"
                     unCheckedChildren="view"
                 /> : null}
diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
index b3b7b40dd..240adf428 100644
--- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
+++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx
@@ -266,7 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                 style={{
                     width: (style.columnWidth) /
                         ((uniqueHeadings.length +
-                            ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
+                            ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)
                 }}>
                 {/* the default bucket (no key value) has a tooltip that describes what it is.
                     Further, it does not have a color and cannot be deleted. */}
@@ -297,7 +297,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
             </div> : (null);
         for (let i = 0; i < cols; i++) templatecols += `${style.columnWidth / style.numGroupColumns}px `;
         return (
-            <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
+            <div className="collectionStackingViewFieldColumn" key={heading} style={{ width: `${100 / ((uniqueHeadings.length + ((this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ? 1 : 0)) || 1)}%`, background: this._background }}
                 ref={this.createColumnDropRef} onPointerEnter={this.pointerEntered} onPointerLeave={this.pointerLeave}>
                 {headingView}
                 <div key={`${heading}-stack`} className={`collectionStackingView-masonry${singleColumn ? "Single" : "Grid"}`}
@@ -315,7 +315,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC
                     {this.children(this.props.docList)}
                     {singleColumn ? (null) : this.props.parent.columnDragger}
                 </div>
-                {(this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'view-mode' && this.props.parent.props.CollectionView.props.Document.chromeStatus !== 'disabled') ?
+                {(this.props.parent.props.ContainingCollectionDoc && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'view-mode' && this.props.parent.props.ContainingCollectionDoc.chromeStatus !== 'disabled') ?
                     <div key={`${heading}-add-document`} className="collectionStackingView-addDocumentButton"
                         style={{ width: style.columnWidth / style.numGroupColumns }}>
                         <EditableView {...newEditableViewProps} />
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 774e6b1b9..ce80526b2 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,7 +1,7 @@
 import { action, computed, IReactionDisposer, reaction } from "mobx";
 import * as rp from 'request-promise';
 import CursorField from "../../../new_fields/CursorField";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
@@ -30,10 +30,11 @@ export interface CollectionViewProps extends FieldViewProps {
     PanelWidth: () => number;
     PanelHeight: () => number;
     chromeCollapsed: boolean;
+    setPreviewCursor?: (func: (x: number, y: number) => void) => void;
 }
 
 export interface SubCollectionViewProps extends CollectionViewProps {
-    CollectionView: CollectionView | CollectionPDFView | CollectionVideoView;
+    CollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     ruleProvider: Doc | undefined;
 }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 21855b168..6135f3e45 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -90,9 +90,7 @@ export function computePivotLayout(pivotDoc: Doc, childDocs: Doc[], childPairs:
         layoutPoolData.set(pair, { transition: "transform 1s", ...pos });
     });
     return { map: layoutPoolData, elements: viewDefsToJSX(groupNames) };
-};
-
-
+}
 
 export function AddCustomFreeFormLayout(doc: Doc, dataKey: string): () => void {
     return () => {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 45c021c5f..075914e29 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -50,7 +50,7 @@ export const panZoomSchema = createSchema({
     useClusters: "boolean",
     isRuleProvider: "boolean",
     fitToBox: "boolean",
-    panTransformType: "string"
+    panTransformType: "string",
 });
 
 type PanZoomDocument = makeInterface<[typeof panZoomSchema, typeof documentSchema, typeof positionSchema, typeof pageSchema]>;
@@ -61,6 +61,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private _lastX: number = 0;
     private _lastY: number = 0;
     private _clusterDistance: number = 75;
+    private _hitCluster = false;
     @observable _clusterSets: (Doc[])[] = [];
 
     @computed get fitToContent() { return (this.props.fitToBox || this.Document.fitToBox) && !this.isAnnotationOverlay; }
@@ -265,7 +266,6 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         return clusterColor;
     }
 
-    _hitCluster = false;
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         this._hitCluster = this.props.Document.useClusters ? this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)) !== -1 : false;
@@ -286,7 +286,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @action
     onPointerMove = (e: PointerEvent): void => {
-        if (!e.cancelBubble && this.props.layoutKey) {
+        if (!e.cancelBubble && !this.isAnnotationOverlay) {
             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();
@@ -451,7 +451,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             PanelHeight: childLayout[HeightSym],
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
-            ContainingCollectionDoc: this.props.CollectionView.props.Document,
+            ContainingCollectionDoc: this.props.ContainingCollectionDoc,
             focus: this.focusDocument,
             backgroundColor: this.getClusterColor,
             parentActive: this.props.active,
@@ -478,7 +478,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             PanelHeight: layoutDoc[HeightSym],
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
-            ContainingCollectionDoc: this.props.CollectionView.props.Document,
+            ContainingCollectionDoc: this.props.ContainingCollectionDoc,
             focus: this.focusDocument,
             backgroundColor: returnEmptyString,
             parentActive: this.props.active,
@@ -699,10 +699,10 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         // 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"} style={{ height: this.props.layoutKey ? "100%" : this.props.PanelHeight() }} ref={this.createDropTarget} onWheel={this.onPointerWheel}
+            <div className={"collectionfreeformview-container"} style={{ pointerEvents: SelectionManager.GetIsDragging() ? "all" : undefined, height: this.isAnnotationOverlay ? "100%" : this.props.PanelHeight() }} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 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}
+                    addDocument={this.addDocument} removeDocument={this.props.removeDocument} addLiveTextDocument={this.addLiveTextBox} setPreviewCursor={this.props.setPreviewCursor}
                     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}>
@@ -725,7 +725,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
     render() {
         return <DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
-            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />
+            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />;
     }
 }
 
@@ -734,7 +734,7 @@ class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps
     render() {
         return !this.props.Document.backgroundLayout ? (null) :
             (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
-                renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />)
+                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 c85c3e55b..689a55ec4 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -31,6 +31,7 @@ interface MarqueeViewProps {
     addLiveTextDocument: (doc: Doc) => void;
     isSelected: () => boolean;
     isAnnotationOverlay: boolean;
+    setPreviewCursor?: (func: (x: number, y: number) => void) => void;
 }
 
 @observer
@@ -44,6 +45,10 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
     @observable _visible: boolean = false;
     _commandExecuted = false;
 
+    componentDidMount() {
+        this.props.setPreviewCursor && this.props.setPreviewCursor(this.setPreviewCursor);
+    }
+
     @action
     cleanupInteractions = (all: boolean = false) => {
         if (all) {
@@ -203,11 +208,17 @@ export class MarqueeView extends React.Component<MarqueeViewProps>
         }
     }
 
+    setPreviewCursor = (x: number, y: number) => {
+        this._downX = x;
+        this._downY = y;
+        PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
+    }
+
     @action
     onClick = (e: React.MouseEvent): void => {
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD &&
             Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
-            PreviewCursor.Show(e.clientX, e.clientY, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument);
+            this.setPreviewCursor(e.clientX, e.clientY);
             // let the DocumentView stopPropagation of this event when it selects this document
         } else {  // why do we get a click event when the cursor have moved a big distance?
             // let's cut it off here so no one else has to deal with it.
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index bcb26b4c4..cd183a984 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -12,7 +12,7 @@ import { Doc, WidthSym, HeightSym } from "../../../new_fields/Doc";
 import { random } from "animejs";
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined
+    dataProvider?: (doc: Doc, dataDoc?: Doc) => { x: number, y: number, width: number, height: number, z: number, transition?: string } | undefined;
     x?: number;
     y?: number;
     width?: number;
@@ -99,8 +99,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     @observable _animPos: number[] | undefined = undefined;
 
-    finalPanelWidh = () => { return this.dataProvider ? this.dataProvider.width : this.panelWidth(); }
-    finalPanelHeight = () => { return this.dataProvider ? this.dataProvider.height : this.panelHeight(); }
+    finalPanelWidh = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
+    finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
 
     render() {
         trace();
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e89fddd25..759c064b4 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -264,7 +264,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (e.cancelBubble && this.active) {
             document.removeEventListener("pointermove", this.onPointerMove);
         }
-        else if (!e.cancelBubble && this.active) {
+        else if (!e.cancelBubble && (SelectionManager.IsSelected(this) ||
+            this.props.parentActive())) {
             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);
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 47b64e260..923dd1544 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -153,7 +153,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                         }
                     });
                     return { frag: Fragment.fromArray(nodes), start: start };
-                }
+                };
                 let findLinkNode = (node: Node, editor: EditorView) => {
                     if (!node.isText) {
                         const content = findLinkFrag(node.content, editor);
@@ -162,7 +162,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     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;
 
@@ -748,7 +748,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     let ref = editorView.domAtPos(editorView.state.selection.from);
                     let refNode = ref.node as any;
                     while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement;
-                    let r1 = refNode && (refNode as any).getBoundingClientRect();
+                    let r1 = refNode && refNode.getBoundingClientRect();
                     let r3 = self._ref.current!.getBoundingClientRect();
                     r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale);
                     return true;
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index b7ff84d4a..2147292d6 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -16,6 +16,9 @@
             user-select: none; 
         }
     }
+    .collectionFreeFormView-none {
+        pointer-events: none;
+    }
 }
 
 .pdfBox-cont-interactive {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index e00635408..69e438d4f 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, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, untracked } from 'mobx';
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
@@ -25,7 +25,7 @@ const PdfDocument = makeInterface(documentSchema, panZoomSchema, pageSchema);
 
 @observer
 export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocument) {
-    public static LayoutString() { return FieldView.LayoutString(PDFBox); }
+    public static LayoutString(fieldExt?: string) { return FieldView.LayoutString(PDFBox, "data", fieldExt); }
     private _reactionDisposer?: IReactionDisposer;
     private _keyValue: string = "";
     private _valueValue: string = "";
@@ -73,24 +73,24 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     }
 
     public GetPage() {
-        return this._pdfViewer!._pdfViewer.currentPageNumber;
+        return this._pdfViewer!.pdfViewer.currentPageNumber;
     }
 
     @action
     public BackPage() {
-        this._pdfViewer!._pdfViewer.scrollPageIntoView({ pageNumber: Math.max(1, this.GetPage() - 1) });
+        this._pdfViewer!.pdfViewer.scrollPageIntoView({ pageNumber: Math.max(1, this.GetPage() - 1) });
         this.props.Document.curPage = this.GetPage();
     }
 
     @action
     public GotoPage = (p: number) => {
-        this._pdfViewer!._pdfViewer.scrollPageIntoView(p);
+        this._pdfViewer!.pdfViewer.scrollPageIntoView(p);
         this.props.Document.curPage = this.GetPage();
     }
 
     @action
     public ForwardPage() {
-        this._pdfViewer!._pdfViewer.scrollPageIntoView({ pageNumber: Math.min(this._pdfViewer!._pdfViewer.pagesCount, this.GetPage() + 1) });
+        this._pdfViewer!.pdfViewer.scrollPageIntoView({ pageNumber: Math.min(this._pdfViewer!.pdfViewer.pagesCount, this.GetPage() + 1) });
         this.props.Document.curPage = this.GetPage();
     }
 
@@ -153,7 +153,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                     <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-down"} size="sm" />
                 </button>
                 <button className="pdfBox-overlayButton-iconCont" key="back" title="Page Back"
-                    onPointerDown={(e) => { e.stopPropagation() }}
+                    onPointerDown={(e) => e.stopPropagation()}
                     onClick={() => this.BackPage()}
                     style={{ left: 20, top: 5, height: "30px", position: "absolute", pointerEvents: "all", display: this.props.active() ? "flex" : "none" }}>
                     <FontAwesomeIcon style={{ color: "white" }} icon={"arrow-left"} size="sm" />
@@ -211,7 +211,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
 
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
-        let classname = "pdfBox-cont" + (this.props.active() && !InkingControl.Instance.selectedTool && !this._alt ? "-interactive" : "");
+        let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.props.isSelected() ? "" : "-interactive");
         return (!(pdfUrl instanceof PdfField) || !this._pdf ?
             <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
             <div className={classname} onWheel={(e: React.WheelEvent) => e.stopPropagation()} onPointerDown={(e: React.PointerEvent) => {
@@ -221,11 +221,13 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                 }
             }}>
                 <PDFViewer {...this.props} pdf={this._pdf} url={pdfUrl.url.pathname} active={this.props.active} scrollTo={this.scrollTo} loaded={this.loaded}
-                    setPdfViewer={this.setPdfViewer}
+                    setPdfViewer={this.setPdfViewer} ContainingCollectionView={this.props.ContainingCollectionView}
+                    renderDepth={this.props.renderDepth}
                     Document={this.props.Document} DataDoc={this.dataDoc}
                     addDocTab={this.props.addDocTab} GoToPage={this.GotoPage}
                     pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
                     ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                    isSelected={this.props.isSelected}
                     fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} />
                 {this.settingsPanel()}
             </div>);
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index a561be94d..4ec2118ee 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -18,9 +18,8 @@
     .page {
         position: relative;
     }
-
-    .pdfViewer-text {
-        transform-origin: top left;
+    .collectionfreeformview-container {
+        pointer-events: none;
     }
 
     .pdfViewer-dragAnnotationBox {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index ea5e00d73..783495e5a 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -1,29 +1,29 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
 import { observer } from "mobx-react";
 import * as Pdfjs from "pdfjs-dist";
 import "pdfjs-dist/web/pdf_viewer.css";
 import { Dictionary } from "typescript-collections";
-import { Doc, DocListCast, FieldResult, WidthSym } from "../../../new_fields/Doc";
+import { Doc, DocListCast, FieldResult, WidthSym, Opt } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
-import { BoolCast, Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { numberRange } from "../../../Utils";
+import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
+import { numberRange, emptyFunction, returnOne } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { Docs, DocUtils } from "../../documents/Documents";
-import { KeyCodes } from "../../northstar/utils/KeyCodes";
 import { DragManager } from "../../util/DragManager";
 import { CompiledScript, CompileScript } from "../../util/Scripting";
+import { Transform } from "../../util/Transform";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import Annotation from "./Annotation";
 import PDFMenu from "./PDFMenu";
 import "./PDFViewer.scss";
 import React = require("react");
-import requestPromise = require("request-promise");
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
+import { CollectionPDFView } from "../collections/CollectionPDFView";
+import { CollectionVideoView } from "../collections/CollectionVideoView";
 import { CollectionView } from "../collections/CollectionView";
-import { listSpec } from "../../../new_fields/Schema";
-import { Transform } from "../../util/Transform";
+import { SelectionManager } from "../../util/SelectionManager";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 
 interface IViewerProps {
@@ -33,6 +33,9 @@ interface IViewerProps {
     DataDoc?: Doc;
     fieldExtensionDoc: Doc;
     fieldKey: string;
+    fieldExt: string;
+    renderDepth: number;
+    isSelected: () => boolean;
     loaded: (nw: number, nh: number, np: number) => void;
     scrollTo: (y: number) => void;
     active: () => boolean;
@@ -42,7 +45,7 @@ interface IViewerProps {
     addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
     setPdfViewer: (view: PDFViewer) => void;
     ScreenToLocalTransform: () => Transform;
-
+    ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
 }
 
 /**
@@ -60,18 +63,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @observable private _marqueeWidth: number = 0;
     @observable private _marqueeHeight: number = 0;
 
+    public pdfViewer: any;
+    private _isChildActive = false;
+    private _setPreviewCursor: undefined | ((x: number, y: number) => void);
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _reactionDisposer?: IReactionDisposer;
     private _annotationReactionDisposer?: IReactionDisposer;
     private _filterReactionDisposer?: IReactionDisposer;
     private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
-    public _pdfViewer: any;
     private _selectionText: string = "";
     private _marquee: React.RefObject<HTMLDivElement> = React.createRef();
     private _marqueeing: boolean = false;
-    private _startY: number = 0;
     private _startX: number = 0;
+    private _startY: number = 0;
+    private _downX: number = 0;
+    private _downY: number = 0;
 
     @computed get allAnnotations() {
         return DocListCast(this.props.fieldExtensionDoc.annotations).filter(
@@ -157,22 +164,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     setupPdfJsViewer = () => {
-        document.addEventListener("pagesinit", () => this._pdfViewer.currentScaleValue = 1);
+        document.addEventListener("pagesinit", () => this.pdfViewer.currentScaleValue = 1);
         document.addEventListener("pagerendered", () => console.log("rendered"));
         var pdfLinkService = new PDFJSViewer.PDFLinkService();
         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: "svg"
         });
-        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
@@ -195,7 +202,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             annoDocs.push(annoDoc);
             annoDoc.isButton = true;
             anno.remove();
-            this.props.addDocument && this.props.addDocument(annoDoc, false);
+            // this.props.addDocument && this.props.addDocument(annoDoc, false);
             mainAnnoDoc = annoDoc;
             mainAnnoDocProto.y = annoDoc.y;
             mainAnnoDocProto = Doc.GetProto(annoDoc);
@@ -305,8 +312,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     search = (searchString: string) => {
-        if (this._pdfViewer._pageViewsReady) {
-            this._pdfViewer.findController.executeCommand('findagain', {
+        if (this.pdfViewer._pageViewsReady) {
+            this.pdfViewer.findController.executeCommand('findagain', {
                 caseSensitive: false,
                 findPrevious: undefined,
                 highlightAll: true,
@@ -316,7 +323,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: undefined,
                     highlightAll: true,
@@ -333,7 +340,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
     onPointerDown = (e: React.PointerEvent): void => {
         // if alt+left click, drag and annotate
         if (NumCast(this.props.Document.scale, 1) !== 1) return;
-        if (!e.altKey && e.button === 0) {
+        this._marqueeing = false;
+        if (!e.altKey && e.button === 0 && this.active()) {
             PDFMenu.Instance.StartDrag = this.startDrag;
             PDFMenu.Instance.Highlight = this.highlight;
             PDFMenu.Instance.Snippet = this.createSnippet;
@@ -346,6 +354,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 }
             }
             else {
+                this._downX = e.clientX;
+                this._downY = e.clientY;
                 // set marquee x and y positions to the spatially transformed position
                 if (this._mainCont.current) {
                     let boundingRect = this._mainCont.current.getBoundingClientRect();
@@ -356,15 +366,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 this._marquee.current && (this._marquee.current.style.opacity = "0.2");
                 this.receiveAnnotations([], -1);
             }
-            document.removeEventListener("pointermove", this.onSelectStart);
-            document.addEventListener("pointermove", this.onSelectStart);
+            document.removeEventListener("pointermove", this.onSelectMove);
+            document.addEventListener("pointermove", this.onSelectMove);
             document.removeEventListener("pointerup", this.onSelectEnd);
             document.addEventListener("pointerup", this.onSelectEnd);
         }
     }
 
     @action
-    onSelectStart = (e: PointerEvent): void => {
+    onSelectMove = (e: PointerEvent): void => {
         if (this._marqueeing && this._mainCont.current) {
             // transform positions and find the width and height to set the marquee to
             let boundingRect = this._mainCont.current.getBoundingClientRect();
@@ -419,7 +429,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @action
     onSelectEnd = (e: PointerEvent): void => {
         if (this._marqueeing) {
-            this._marqueeing = false;
             if (this._marqueeWidth > 10 || this._marqueeHeight > 10) {
                 if (this._marquee.current) { // make a copy of the marquee
                     let copy = document.createElement("div");
@@ -444,23 +453,23 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
             this._marqueeHeight = this._marqueeWidth = 0;
         }
-        // else {
-        //     let sel = window.getSelection();
-        //     if (sel && sel.type === "Range") {
-        //         let selRange = sel.getRangeAt(0);
-        //         this.createTextAnnotation(sel, selRange);
-        //         PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
-        //     }
-        // }
-
-        // if (PDFMenu.Instance.Highlighting) {
-        //     this.highlight(undefined, "goldenrod");
-        // }
-        // else {
-        //     PDFMenu.Instance.StartDrag = this.startDrag;
-        //     PDFMenu.Instance.Highlight = this.highlight;
-        // }
-        document.removeEventListener("pointermove", this.onSelectStart);
+        else {
+            let sel = window.getSelection();
+            if (sel && sel.type === "Range") {
+                let selRange = sel.getRangeAt(0);
+                this.createTextAnnotation(sel, selRange);
+                PDFMenu.Instance.jumpTo(e.clientX, e.clientY);
+            }
+        }
+
+        if (PDFMenu.Instance.Highlighting) {
+            this.highlight(undefined, "goldenrod");
+        }
+        else {
+            PDFMenu.Instance.StartDrag = this.startDrag;
+            PDFMenu.Instance.Highlight = this.highlight;
+        }
+        document.removeEventListener("pointermove", this.onSelectMove);
         document.removeEventListener("pointerup", this.onSelectEnd);
     }
 
@@ -468,7 +477,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     highlight = (targetDoc: Doc | undefined, color: string) => {
         // creates annotation documents for current highlights
         let annotationDoc = this.makeAnnotationDocument(targetDoc, color, false);
-        Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", annotationDoc);
+        Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, annotationDoc);
         return annotationDoc;
     }
 
@@ -529,7 +538,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     removeDocument(doc: Doc): boolean {
         //TODO This won't create the field if it doesn't already exist
         let targetDataDoc = this.props.fieldExtensionDoc;
-        let targetField = "annotations";
+        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);
@@ -539,8 +548,22 @@ export class PDFViewer extends React.Component<IViewerProps> {
     scrollXf = () => {
         return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, this._mainCont.current.scrollTop) : this.props.ScreenToLocalTransform();
     }
+    setPreviewCursor = (func?: (x: number, y: number) => void) => {
+        this._setPreviewCursor = func;
+    }
+    onClick = (e: React.MouseEvent) => {
+        this._setPreviewCursor && this._marqueeing && Math.abs(e.clientX - this._downX) < 3 && Math.abs(e.clientY - this._downY) < 3 &&
+            this._setPreviewCursor(e.clientX, e.clientY);
+    }
+    whenActiveChanged = (isActive: boolean) => {
+        this._isChildActive = isActive;
+        //this.props.whenActiveChanged(isActive); // bcz: is this needed here?
+    }
+    active = () => {
+        return this.props.isSelected() || this._isChildActive || this.props.renderDepth === 0;
+    }
     render() {
-        return (<div className="pdfViewer-viewer" onPointerDown={this.onPointerDown} onWheel={(e) => e.stopPropagation()} ref={this._mainCont}>
+        return (<div className="pdfViewer-viewer" onPointerDown={this.onPointerDown} onWheel={(e) => e.stopPropagation()} onClick={this.onClick} ref={this._mainCont}>
             <div className="pdfViewer-text" ref={this._viewer} />
             <div className="pdfViewer-dragAnnotationBox" ref={this._marquee}
                 style={{
@@ -554,17 +577,24 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     <Annotation {...this.props} anno={anno} key={`${anno[Id]}-annotation`} />)}
             </div>
             <CollectionFreeFormView {...this.props}
-                fieldExt="annotations"
+                setPreviewCursor={this.setPreviewCursor}
                 PanelHeight={() => this._pageSizes.length && this._pageSizes[0] ? this.props.pdf.numPages * this._pageSizes[0].height : 300}
+                PanelWidth={() => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : 300}
+                focus={emptyFunction}
+                isSelected={this.props.isSelected}
+                select={emptyFunction}
+                active={this.active}
+                ContentScaling={returnOne}
+                whenActiveChanged={this.whenActiveChanged}
                 removeDocument={this.removeDocument}
                 moveDocument={this.moveDocument}
-                addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.AddDocToList(this.props.fieldExtensionDoc, "annotations", doc); return true; }}
+                addDocument={(doc: Doc, allow: boolean | undefined) => { Doc.AddDocToList(this.props.fieldExtensionDoc, this.props.fieldExt, doc); return true; }}
                 CollectionView={this.props.ContainingCollectionView}
                 ScreenToLocalTransform={this.scrollXf}
-                ruleProvider={this.props.ruleProvider}
-                chromeCollapsed={true}
-                layoutKey={undefined}
-                backgroundLayout={undefined} >
+                ruleProvider={undefined}
+                renderDepth={this.props.renderDepth + 1}
+                ContainingCollectionDoc={this.props.ContainingCollectionView && this.props.ContainingCollectionView.props.Document}
+                chromeCollapsed={true}>
             </CollectionFreeFormView>
         </div >);
     }
-- 
cgit v1.2.3-70-g09d2


From e95387732e1fbff49ec035c3bec4b03324d814c8 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Thu, 26 Sep 2019 18:11:10 -0400
Subject: beginning to read from and write to database

---
 src/Utils.ts                                       | 92 ++++++++++------------
 src/client/Network.ts                              | 25 ++++++
 .../apis/google_docs/GoogleApiClientUtils.ts       |  2 +-
 .../apis/google_docs/GooglePhotosClientUtils.ts    |  3 +-
 .../util/Import & Export/DirectoryImportBox.tsx    |  4 +-
 src/client/views/MainView.tsx                      |  2 +-
 src/new_fields/RichTextUtils.ts                    |  4 +-
 src/server/apis/google/GoogleApiServerUtils.ts     | 51 +++++-------
 src/server/database.ts                             | 34 +++++---
 src/server/index.ts                                | 12 ++-
 10 files changed, 126 insertions(+), 103 deletions(-)
 create mode 100644 src/client/Network.ts

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 5f06b5cec..ae8371f15 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -3,22 +3,20 @@ import v5 = require("uuid/v5");
 import { Socket } from 'socket.io';
 import { Message } from './server/Message';
 import { RouteStore } from './server/RouteStore';
-import requestPromise = require('request-promise');
-import { CurrentUserUtils } from './server/authentication/models/current_user_utils';
 
-export class Utils {
+export namespace Utils {
 
-    public static DRAG_THRESHOLD = 4;
+    export const DRAG_THRESHOLD = 4;
 
-    public static GenerateGuid(): string {
+    export function GenerateGuid(): string {
         return v4();
     }
 
-    public static GenerateDeterministicGuid(seed: string): string {
+    export function GenerateDeterministicGuid(seed: string): string {
         return v5(seed, v5.URL);
     }
 
-    public static GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
+    export function GetScreenTransform(ele?: HTMLElement): { scale: number, translateX: number, translateY: number } {
         if (!ele) {
             return { scale: 1, translateX: 1, translateY: 1 };
         }
@@ -35,23 +33,23 @@ export class Utils {
      * requested extension
      * @param extension the specified sub-path to append to the window origin
      */
-    public static prepend(extension: string): string {
+    export function prepend(extension: string): string {
         return window.location.origin + extension;
     }
 
-    public static fileUrl(filename: string): string {
-        return this.prepend(`/files/${filename}`);
+    export function fileUrl(filename: string): string {
+        return prepend(`/files/${filename}`);
     }
 
-    public static shareUrl(documentId: string): string {
-        return this.prepend(`/doc/${documentId}?sharing=true`);
+    export function shareUrl(documentId: string): string {
+        return prepend(`/doc/${documentId}?sharing=true`);
     }
 
-    public static CorsProxy(url: string): string {
-        return this.prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+    export function CorsProxy(url: string): string {
+        return prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
     }
 
-    public static CopyText(text: string) {
+    export function CopyText(text: string) {
         var textArea = document.createElement("textarea");
         textArea.value = text;
         document.body.appendChild(textArea);
@@ -63,7 +61,7 @@ export class Utils {
         document.body.removeChild(textArea);
     }
 
-    public static fromRGBAstr(rgba: string) {
+    export function fromRGBAstr(rgba: string) {
         let rm = rgba.match(/rgb[a]?\(([0-9]+)/);
         let r = rm ? Number(rm[1]) : 0;
         let gm = rgba.match(/rgb[a]?\([0-9]+,([0-9]+)/);
@@ -74,11 +72,12 @@ export class Utils {
         let a = am ? Number(am[1]) : 0;
         return { r: r, g: g, b: b, a: a };
     }
-    public static toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
+
+    export function toRGBAstr(col: { r: number, g: number, b: number, a?: number }) {
         return "rgba(" + col.r + "," + col.g + "," + col.b + (col.a !== undefined ? "," + col.a : "") + ")";
     }
 
-    public static HSLtoRGB(h: number, s: number, l: number) {
+    export function HSLtoRGB(h: number, s: number, l: number) {
         // Must be fractions of 1
         // s /= 100;
         // l /= 100;
@@ -108,7 +107,7 @@ export class Utils {
         return { r: r, g: g, b: b };
     }
 
-    public static RGBToHSL(r: number, g: number, b: number) {
+    export function RGBToHSL(r: number, g: number, b: number) {
         // Make r, g, and b fractions of 1
         r /= 255;
         g /= 255;
@@ -150,7 +149,7 @@ export class Utils {
     }
 
 
-    public static GetClipboardText(): string {
+    export function GetClipboardText(): string {
         var textArea = document.createElement("textarea");
         document.body.appendChild(textArea);
         textArea.focus();
@@ -163,51 +162,53 @@ export class Utils {
         return val;
     }
 
-    public static loggingEnabled: Boolean = false;
-    public static logFilter: number | undefined = undefined;
-    private static log(prefix: string, messageName: string, message: any, receiving: boolean) {
-        if (!this.loggingEnabled) {
+    export const loggingEnabled: Boolean = false;
+    export const logFilter: number | undefined = undefined;
+
+    function log(prefix: string, messageName: string, message: any, receiving: boolean) {
+        if (!loggingEnabled) {
             return;
         }
         message = message || {};
-        if (this.logFilter !== undefined && this.logFilter !== message.type) {
+        if (logFilter !== undefined && logFilter !== message.type) {
             return;
         }
         let idString = (message.id || "").padStart(36, ' ');
         prefix = prefix.padEnd(16, ' ');
         console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)}`);
     }
-    private static loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
+
+    function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
         return (args: any) => {
-            this.log(prefix, messageName, args, true);
+            log(prefix, messageName, args, true);
             func(args);
         };
     }
 
-    public static Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
-        this.log("Emit", message.Name, args, false);
+    export function Emit<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T) {
+        log("Emit", message.Name, args, false);
         socket.emit(message.Message, args);
     }
 
-    public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
-    public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
-    public static EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
-        this.log("Emit", message.Name, args, false);
+    export function EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T): Promise<any>;
+    export function EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn: (args: any) => any): void;
+    export function EmitCallback<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, args: T, fn?: (args: any) => any): void | Promise<any> {
+        log("Emit", message.Name, args, false);
         if (fn) {
-            socket.emit(message.Message, args, this.loggingCallback('Receiving', fn, message.Name));
+            socket.emit(message.Message, args, loggingCallback('Receiving', fn, message.Name));
         } else {
-            return new Promise<any>(res => socket.emit(message.Message, args, this.loggingCallback('Receiving', res, message.Name)));
+            return new Promise<any>(res => socket.emit(message.Message, args, loggingCallback('Receiving', res, message.Name)));
         }
     }
 
-    public static AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
-        socket.on(message.Message, this.loggingCallback('Incoming', handler, message.Name));
+    export function AddServerHandler<T>(socket: Socket | SocketIOClient.Socket, message: Message<T>, handler: (args: T) => any) {
+        socket.on(message.Message, loggingCallback('Incoming', handler, message.Name));
     }
 
-    public static AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
+    export function AddServerHandlerCallback<T>(socket: Socket, message: Message<T>, handler: (args: [T, (res: any) => any]) => any) {
         socket.on(message.Message, (arg: T, fn: (res: any) => any) => {
-            this.log('S receiving', message.Name, arg, true);
-            handler([arg, this.loggingCallback('S sending', fn, message.Name)]);
+            log('S receiving', message.Name, arg, true);
+            handler([arg, loggingCallback('S sending', fn, message.Name)]);
         });
     }
 }
@@ -291,15 +292,4 @@ export namespace JSONUtils {
         return results;
     }
 
-}
-
-export function PostToServer(relativeRoute: string, body?: any) {
-    body = { userId: CurrentUserUtils.id, ...body };
-    let options = {
-        method: "POST",
-        uri: Utils.prepend(relativeRoute),
-        json: true,
-        body
-    };
-    return requestPromise.post(options);
 }
\ No newline at end of file
diff --git a/src/client/Network.ts b/src/client/Network.ts
new file mode 100644
index 000000000..cb46105f8
--- /dev/null
+++ b/src/client/Network.ts
@@ -0,0 +1,25 @@
+import { Utils } from "../Utils";
+import { CurrentUserUtils } from "../server/authentication/models/current_user_utils";
+import requestPromise = require('request-promise');
+
+export async function PostToServer(relativeRoute: string, body?: any) {
+    let options = {
+        uri: Utils.prepend(relativeRoute),
+        method: "POST",
+        headers: { userId: CurrentUserUtils.id },
+        body,
+        json: true
+    };
+    return requestPromise.post(options);
+}
+
+export async function PostFormDataToServer(relativeRoute: string, formData: FormData) {
+    const parameters = {
+        method: 'POST',
+        headers: { userId: CurrentUserUtils.id },
+        body: formData,
+    };
+    const response = await fetch(relativeRoute, parameters);
+    const text = await response.json();
+    return text;
+}
\ No newline at end of file
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 2c84741db..0f0f81891 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,9 +1,9 @@
 import { docs_v1, slides_v1 } from "googleapis";
-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 { PostToServer } from "../../Network";
 
 export const Pulls = "googleDocsPullCount";
 export const Pushes = "googleDocsPushCount";
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index c45a49f1a..b1b29210a 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,4 +1,4 @@
-import { PostToServer, Utils } from "../../../Utils";
+import { Utils } from "../../../Utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { ImageField } from "../../../new_fields/URLField";
 import { Cast, StrCast } from "../../../new_fields/Types";
@@ -14,6 +14,7 @@ import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/Share
 import { AssertionError } from "assert";
 import { DocumentView } from "../../views/nodes/DocumentView";
 import { DocumentManager } from "../../util/DocumentManager";
+import { PostToServer } from "../../Network";
 
 export namespace GooglePhotos {
 
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 6670f685e..d0291aec4 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -21,6 +21,7 @@ import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
 import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
 import "./DirectoryImportBox.scss";
 import BatchedArray from "array-batcher";
+import { PostFormDataToServer } from "../../Network";
 
 const unsupported = ["text/html", "text/plain"];
 interface FileResponse {
@@ -106,7 +107,6 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
 
         const uploads = await BatchedArray.from(validated, { batchSize: 15 }).batchedMapAsync(async batch => {
             const formData = new FormData();
-            const parameters = { method: 'POST', body: formData };
 
             batch.forEach(file => {
                 sizes.push(file.size);
@@ -114,7 +114,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
                 formData.append(Utils.GenerateGuid(), file);
             });
 
-            const responses = await (await fetch(RouteStore.upload, parameters)).json();
+            const responses = await PostFormDataToServer(RouteStore.upload, formData);
             runInAction(() => this.completed += batch.length);
             return responses as FileResponse[];
         });
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index b1f753635..296574a04 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -15,7 +15,7 @@ import { listSpec } from '../../new_fields/Schema';
 import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString, PostToServer } from '../../Utils';
+import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { ClientUtils } from '../util/ClientUtils';
 import { DictationManager } from '../util/DictationManager';
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index 02079e92c..f3208ce41 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -7,17 +7,17 @@ import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
 import { Opt, Doc } from "./Doc";
 import Color = require('color');
 import { sinkListItem } from "prosemirror-schema-list";
-import { Utils, PostToServer } from "../Utils";
+import { Utils } from "../Utils";
 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, StrCast } from "./Types";
 import { Id } from "./FieldSymbols";
 import { DocumentView } from "../client/views/nodes/DocumentView";
 import { AssertionError } from "assert";
+import { PostToServer } from "../client/Network";
 
 export namespace RichTextUtils {
 
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 684a8081b..665dcf862 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -66,6 +66,15 @@ export namespace GoogleApiServerUtils {
         });
     };
 
+    export const RetrieveAccessToken = (information: CredentialInformation) => {
+        return new Promise<string>((resolve, reject) => {
+            RetrieveCredentials(information).then(
+                credentials => resolve(credentials.token.access_token!),
+                error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
+            );
+        });
+    };
+
     export const RetrieveCredentials = (information: CredentialInformation) => {
         return new Promise<TokenResult>((resolve, reject) => {
             readFile(information.credentialsPath, async (err, credentials) => {
@@ -78,15 +87,6 @@ export namespace GoogleApiServerUtils {
         });
     };
 
-    export const RetrieveAccessToken = (information: CredentialInformation) => {
-        return new Promise<string>((resolve, reject) => {
-            RetrieveCredentials(information).then(
-                credentials => resolve(credentials.token.access_token!),
-                error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
-            );
-        });
-    };
-
     export const RetrievePhotosEndpoint = (paths: CredentialInformation) => {
         return new Promise<any>((resolve, reject) => {
             RetrieveAccessToken(paths).then(
@@ -107,7 +107,7 @@ export namespace GoogleApiServerUtils {
             client_id, client_secret, redirect_uris[0]);
 
         return new Promise<TokenResult>((resolve, reject) => {
-            Database.Auxiliary.FetchGoogleAuthenticationToken(userId).then(token => {
+            Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => {
                 // Check if we have previously stored a token for this userId.
                 if (!token) {
                     return getNewToken(oAuth2Client, userId).then(resolve, reject);
@@ -123,8 +123,8 @@ export namespace GoogleApiServerUtils {
     }
 
     const refreshEndpoint = "https://oauth2.googleapis.com/token";
-    const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, token_path: string) => {
-        return new Promise<TokenResult>((resolve, reject) => {
+    const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, userId: string) => {
+        return new Promise<TokenResult>(resolve => {
             let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
             let queryParameters = {
                 refreshToken: credentials.refresh_token,
@@ -133,19 +133,13 @@ export namespace GoogleApiServerUtils {
                 grant_type: "refresh_token"
             };
             let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`;
-            request.post(url, headerParameters).then(response => {
+            request.post(url, headerParameters).then(async response => {
                 let parsed = JSON.parse(response);
                 credentials.access_token = parsed.access_token;
                 credentials.expiry_date = new Date().getTime() + (parsed.expires_in * 1000);
-                writeFile(token_path, JSON.stringify(credentials), (err) => {
-                    if (err) {
-                        console.error(err);
-                        reject(err);
-                    }
-                    console.log('Refreshed token stored to', token_path);
-                    oAuth2Client.setCredentials(credentials);
-                    resolve({ token: credentials, client: oAuth2Client });
-                });
+                oAuth2Client.setCredentials(credentials);
+                await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, credentials);
+                resolve({ token: credentials, client: oAuth2Client });
             });
         });
     };
@@ -156,7 +150,7 @@ export namespace GoogleApiServerUtils {
      * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
      * @param {getEventsCallback} callback The callback for the authorized client.
      */
-    function getNewToken(oAuth2Client: OAuth2Client, token_path: string) {
+    function getNewToken(oAuth2Client: OAuth2Client, userId: string) {
         return new Promise<TokenResult>((resolve, reject) => {
             const authUrl = oAuth2Client.generateAuthUrl({
                 access_type: 'offline',
@@ -169,20 +163,13 @@ export namespace GoogleApiServerUtils {
             });
             rl.question('Enter the code from that page here: ', (code) => {
                 rl.close();
-                oAuth2Client.getToken(code, (err, token) => {
+                oAuth2Client.getToken(code, async (err, token) => {
                     if (err || !token) {
                         reject(err);
                         return console.error('Error retrieving access token', err);
                     }
                     oAuth2Client.setCredentials(token);
-                    // Store the token to disk for later program executions
-                    writeFile(token_path, JSON.stringify(token), (err) => {
-                        if (err) {
-                            console.error(err);
-                            reject(err);
-                        }
-                        console.log('Token stored to', token_path);
-                    });
+                    await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token);
                     resolve({ token, client: oAuth2Client });
                 });
             });
diff --git a/src/server/database.ts b/src/server/database.ts
index ce29478ad..890ac6b32 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -231,19 +231,37 @@ export namespace Database {
 
         const GoogleAuthentication = "GoogleAuthentication";
 
-        const SanitizedSingletonQuery = async (query: { [key: string]: any }, collection: string) => {
+        const SanitizedCappedQuery = async (query: { [key: string]: any }, collection: string, cap: number) => {
             const cursor = await Instance.query(query, undefined, collection);
-            const existing = (await cursor.toArray())[0];
-            if (existing) {
-                delete existing._id;
-            }
-            return existing;
+            const results = await cursor.toArray();
+            const slice = results.slice(0, Math.min(cap, results.length));
+            return slice.map(result => {
+                delete result._id;
+                return result;
+            });
+        };
+
+        const SanitizedSingletonQuery = async (query: { [key: string]: any }, collection: string) => {
+            const results = await SanitizedCappedQuery(query, collection, 1);
+            return results.length ? results[0] : undefined;
         };
 
         export const QueryUploadHistory = async (contentSize: number): Promise<Opt<DashUploadUtils.UploadInformation>> => {
             return SanitizedSingletonQuery({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
         };
 
+        export namespace GoogleAuthenticationToken {
+
+            export const Fetch = async (userId: string) => {
+                return SanitizedSingletonQuery({ userId }, GoogleAuthentication);
+            };
+
+            export const Write = async (userId: string, token: any) => {
+                return Instance.insert({ userId, ...token }, GoogleAuthentication);
+            };
+
+        }
+
         export const LogUpload = async (information: DashUploadUtils.UploadInformation) => {
             const bundle = {
                 _id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
@@ -258,10 +276,6 @@ export namespace Database {
             return Promise.all(pendingDeletions);
         };
 
-        export const FetchGoogleAuthenticationToken = async (userId: string) => {
-            return SanitizedSingletonQuery({ userId }, GoogleAuthentication);
-        };
-
     }
 
 }
\ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 386ecce4d..2ff63ab78 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -810,7 +810,7 @@ const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerU
 app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service;
     let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action;
-    GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, userId: req.body.userId }).then(endpoint => {
+    GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, userId: req.headers.userId as string }).then(endpoint => {
         let handler = EndpointHandlerMap.get(action);
         if (endpoint && handler) {
             let execute = handler(endpoint, req.body).then(
@@ -824,10 +824,11 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
     });
 });
 
-app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.body.userId }).then(token => res.send(token)));
+app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.headers.userId as string }).then(token => res.send(token)));
 
 const tokenError = "Unable to successfully upload bytes for all images!";
 const mediaError = "Unable to convert all uploaded bytes to media items!";
+const userIdError = "Unable to parse the identification of the user!";
 
 export interface NewMediaItem {
     description: string;
@@ -837,7 +838,12 @@ export interface NewMediaItem {
 }
 
 app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
-    const { userId, media } = req.body;
+    const { media } = req.body;
+    const { userId } = req.headers;
+
+    if (!userId || Array.isArray(userId)) {
+        return _error(res, userIdError);
+    }
 
     await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
 
-- 
cgit v1.2.3-70-g09d2


From 78d9ea53fcf13a42445f2b2cd9b8a76fef0fdfea Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sat, 28 Sep 2019 15:26:27 -0400
Subject: sidebar now flies out

---
 src/client/views/Main.scss    |  7 +++++
 src/client/views/MainView.tsx | 65 ++++++++++++++++++++++++++++++++++++-------
 2 files changed, 62 insertions(+), 10 deletions(-)

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

diff --git a/src/client/views/Main.scss b/src/client/views/Main.scss
index bc0975c86..67bfe460f 100644
--- a/src/client/views/Main.scss
+++ b/src/client/views/Main.scss
@@ -238,6 +238,13 @@ ul#add-options-list {
     flex-direction: column;
 }
 
+.expandFlyoutButton {
+    position: absolute;
+    top: 30px;
+    right: 30px;
+    cursor: pointer;
+}
+
 .mainView-libraryHandle {
     width: 20px;
     height: 40px;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 244b217ed..afc3612f6 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +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 } from '@fortawesome/free-solid-svg-icons';
+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 } 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';
@@ -195,6 +195,7 @@ export class MainView extends React.Component {
         library.add(faArrowUp);
         library.add(faCloudUploadAlt);
         library.add(faBolt);
+        library.add(faChevronRight);
         this.initEventListeners();
         this.initAuthenticationRouters();
     }
@@ -311,12 +312,14 @@ export class MainView extends React.Component {
     }
 
     @observable flyoutWidth: number = 250;
+    @observable flyoutTranslate: boolean = true;
     @computed get dockingContent() {
         let flyoutWidth = this.flyoutWidth;
+        let countWidth = this.flyoutTranslate;
         let mainCont = this.mainContainer;
         return <Measure offset onResize={this.onResize}>
             {({ measureRef }) =>
-                <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${flyoutWidth}px`, transform: `translate(${flyoutWidth}px, 0px)` }} onDrop={this.onDrop}>
+                <div ref={measureRef} id="mainContent-div" style={{ width: `calc(100% - ${countWidth ? flyoutWidth : 0}px`, transform: `translate(${countWidth ? flyoutWidth : 0}px, 0px)` }} onDrop={this.onDrop}>
                     {!mainCont ? (null) :
                         <DocumentView Document={mainCont}
                             DataDoc={undefined}
@@ -356,6 +359,23 @@ export class MainView extends React.Component {
         e.stopPropagation();
         e.preventDefault();
     }
+
+    @action
+    pointerOverDragger = () => {
+        if (this.flyoutWidth === 0) {
+            this.flyoutWidth = 250;
+            this.flyoutTranslate = false;
+        }
+    }
+
+    @action
+    pointerLeaveDragger = () => {
+        if (!this.flyoutTranslate) {
+            this.flyoutWidth = 0;
+            this.flyoutTranslate = true;
+        }
+    }
+
     @action
     onPointerMove = (e: PointerEvent) => {
         this.flyoutWidth = Math.max(e.clientX, 0);
@@ -411,18 +431,34 @@ export class MainView extends React.Component {
             getScale={returnOne}>
         </DocumentView>;
     }
+
     @computed
     get mainContent() {
         let sidebar = CurrentUserUtils.UserDocument.sidebar;
         if (!(sidebar instanceof Doc)) return (null);
         return <div className="mainContent" style={{ width: "100%", height: "100%", position: "absolute" }}>
-            <div className="mainView-libraryHandle"
-                style={{ cursor: "ew-resize", left: `${this.flyoutWidth - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
-                onPointerDown={this.onPointerDown}>
-                <span title="library View Dragger" style={{ width: "100%", height: "100%", position: "absolute" }} />
-            </div>
-            <div className="mainView-libraryFlyout" style={{ width: `${this.flyoutWidth}px`, zIndex: 1 }}>
-                {this.flyout}
+            <div onPointerLeave={this.pointerLeaveDragger}>
+                <div className="mainView-libraryHandle"
+                    style={{ cursor: "ew-resize", left: `${(this.flyoutWidth * (this.flyoutTranslate ? 1 : 0)) - 10}px`, backgroundColor: `${StrCast(sidebar.backgroundColor, "lightGray")}` }}
+                    onPointerDown={this.onPointerDown} onPointerOver={this.pointerOverDragger}>
+                    <span title="library View Dragger" style={{
+                        width: (this.flyoutWidth !== 0 && this.flyoutTranslate) ? "100%" : "5vw",
+                        height: (this.flyoutWidth !== 0 && this.flyoutTranslate) ? "100%" : "30vh",
+                        position: "absolute",
+                        top: (this.flyoutWidth !== 0 && this.flyoutTranslate) ? "" : "-10vh"
+                    }} />
+                </div>
+                <div className="mainView-libraryFlyout" style={{
+                    width: `${this.flyoutWidth}px`,
+                    zIndex: 1,
+                    transformOrigin: this.flyoutTranslate ? "" : "left center",
+                    transition: this.flyoutTranslate ? "" : "width .5s",
+                    transform: `scale(${this.flyoutTranslate ? 1 : 0.8})`,
+                    boxShadow: this.flyoutTranslate ? "" : "rgb(156, 147, 150) 0.2vw 0.2vw 0.8vw"
+                }}>
+                    {this.flyout}
+                    {this.expandButton}
+                </div>
             </div>
             {this.dockingContent}
         </div>;
@@ -478,7 +514,7 @@ export class MainView extends React.Component {
         ];
         if (!ClientUtils.RELEASE) btns.unshift([React.createRef<HTMLDivElement>(), "cat", "Add Cat Image", addImageNode]);
 
-        return < div id="add-nodes-menu" style={{ left: this.flyoutWidth + 20, bottom: 20 }} >
+        return < div id="add-nodes-menu" style={{ left: (this.flyoutTranslate ? this.flyoutWidth : 0) + 20, bottom: 20 }} >
 
             <input type="checkbox" id="add-menu-toggle" ref={this.addMenuToggle} />
             <label htmlFor="add-menu-toggle" style={{ marginTop: 2 }} title="Close Menu"><p>+</p></label>
@@ -569,6 +605,15 @@ export class MainView extends React.Component {
         return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
     }
 
+    @computed get expandButton() {
+        return !this.flyoutTranslate ? (<div className="expandFlyoutButton" title="Re-attach sidebar" onPointerDown={() => {
+            runInAction(() => {
+                this.flyoutWidth = 250;
+                this.flyoutTranslate = true;
+            });
+        }}><FontAwesomeIcon icon="chevron-right" color="grey" size="lg" /></div>) : (null);
+    }
+
     render() {
         return (
             <div id="main-div">
-- 
cgit v1.2.3-70-g09d2


From 2d3deb7291b7d98acf71566a67c4d648a90ef5af Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Sat, 28 Sep 2019 16:00:44 -0400
Subject: search bar filtering updatesA

---
 src/client/views/MainView.tsx          |  3 ++-
 src/client/views/search/SearchBox.scss |  9 +++++++++
 src/client/views/search/SearchBox.tsx  | 11 ++++++++---
 3 files changed, 19 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index afc3612f6..ffbba3a20 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +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 } from '@fortawesome/free-solid-svg-icons';
+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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -196,6 +196,7 @@ export class MainView extends React.Component {
         library.add(faCloudUploadAlt);
         library.add(faBolt);
         library.add(faChevronRight);
+        library.add(faEllipsisV);
         this.initEventListeners();
         this.initAuthenticationRouters();
     }
diff --git a/src/client/views/search/SearchBox.scss b/src/client/views/search/SearchBox.scss
index 5ed33a596..0dd4d3dc5 100644
--- a/src/client/views/search/SearchBox.scss
+++ b/src/client/views/search/SearchBox.scss
@@ -34,6 +34,9 @@
 
         &.searchBox-filter {
             align-self: stretch;
+        }
+
+        &.searchBox-submit {
             margin-left: 2px;
             margin-right: 2px
         }
@@ -45,6 +48,12 @@
     }
 }
 
+.searchBox-quickFilter {
+    width: 500px;
+    margin-left: 25px;
+    margin-top: 10px;
+}
+
 .searchBox-results {
     margin-right: 136px;
     top: 300px;
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 0d50124dd..1b8177842 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -18,6 +18,7 @@ import { FilterBox } from './FilterBox';
 import "./FilterBox.scss";
 import "./SearchBox.scss";
 import { SearchItem } from './SearchItem';
+import { IconBar } from './IconBar';
 
 library.add(faTimes);
 
@@ -231,7 +232,7 @@ export class SearchBox extends React.Component {
     }
 
     @action.bound
-    openSearch(e: React.PointerEvent) {
+    openSearch(e: React.SyntheticEvent) {
         e.stopPropagation();
         this._openNoResults = false;
         FilterBox.Instance.closeFilter();
@@ -337,12 +338,16 @@ export class SearchBox extends React.Component {
                         <FontAwesomeIcon icon="object-group" size="lg" />
                     </span>
                     <input value={this._searchString} onChange={this.onChange} type="text" placeholder="Search..." id="search-input" ref={this.inputRef}
-                        className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter}
+                        className="searchBox-barChild searchBox-input" onPointerDown={this.openSearch} onKeyPress={this.enter} onFocus={this.openSearch}
                         style={{ width: this._searchbarOpen ? "500px" : "100px" }} />
+                    <button className="searchBox-barChild searchBox-filter" title="Advanced Filtering Options" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}><FontAwesomeIcon icon="ellipsis-v" color="white" /></button>
                     <button className="searchBox-barChild searchBox-submit" onClick={this.submitSearch} onPointerDown={FilterBox.Instance.stopProp}>Submit</button>
-                    <button className="searchBox-barChild searchBox-filter" onClick={FilterBox.Instance.openFilter} onPointerDown={FilterBox.Instance.stopProp}>Filter</button>
                     <button className="searchBox-barChild searchBox-close" title={"Close Search Bar"} onPointerDown={MainView.Instance.toggleSearch}><FontAwesomeIcon icon={faTimes} size="lg" /></button>
                 </div>
+                {(this._numTotalResults > 0 || !this._searchbarOpen) ? (null) :
+                    (<div className="searchBox-quickFilter" onPointerDown={this.openSearch}>
+                        <div className="filter-panel"><IconBar /></div>
+                    </div>)}
                 <div className="searchBox-results" onScroll={this.resultsScrolled} style={{
                     display: this._resultsOpen ? "flex" : "none",
                     height: this.resFull ? "560px" : this.resultHeight, overflow: this.resFull ? "auto" : "visible"
-- 
cgit v1.2.3-70-g09d2


From a7fc637fb8a9ecf52e737a1d0e28b3805193b82e Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 28 Sep 2019 23:43:25 -0400
Subject: lots of fixes to pdfs and link following.

---
 src/Utils.ts                                       | 30 +++++++++
 src/client/util/DocumentManager.ts                 |  4 +-
 src/client/views/DocumentDecorations.tsx           |  8 +--
 src/client/views/MainView.tsx                      |  5 +-
 .../views/collections/CollectionDockingView.scss   |  5 +-
 .../views/collections/CollectionDockingView.tsx    | 63 ++++++++----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 51 ++++++++-------
 .../collections/collectionFreeForm/MarqueeView.tsx |  3 -
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  6 +-
 src/client/views/nodes/DocumentView.tsx            |  9 ++-
 src/client/views/nodes/FormattedTextBox.tsx        | 75 +++++++++++-----------
 src/client/views/nodes/ImageBox.tsx                |  1 +
 src/client/views/nodes/PDFBox.tsx                  | 20 ++++--
 src/client/views/pdf/PDFViewer.tsx                 | 45 +++++++------
 src/new_fields/Doc.ts                              |  2 +-
 15 files changed, 184 insertions(+), 143 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 4fac53c7d..2b00a6530 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -307,4 +307,34 @@ export function PostToServer(relativeRoute: string, body: any) {
         body: body
     };
     return requestPromise.post(options);
+}
+
+const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => {
+    let newCurrentTime = currentTime / (duration / 2);
+
+    if (newCurrentTime < 1) {
+        return (change / 2) * newCurrentTime * newCurrentTime + start;
+    }
+
+    newCurrentTime -= 1;
+    return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
+};
+
+export default function smoothScroll(duration: number, element: HTMLElement, to: number) {
+    const start = element.scrollTop;
+    const change = to - start;
+    const startDate = new Date().getTime();
+
+    const animateScroll = () => {
+        const currentDate = new Date().getTime();
+        const currentTime = currentDate - startDate;
+        element.scrollTop = easeInOutQuad(currentTime, start, change, duration);
+
+        if (currentTime < duration) {
+            requestAnimationFrame(animateScroll);
+        } else {
+            element.scrollTop = to;
+        }
+    };
+    animateScroll();
 }
\ No newline at end of file
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index e60ab09bb..c048125c5 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -132,7 +132,7 @@ export class DocumentManager {
         let doc = Doc.GetProto(docDelegate);
         const contextDoc = await Cast(doc.annotationOn, Doc);
         if (contextDoc) {
-            contextDoc.panY = doc.y;
+            contextDoc.scrollY = NumCast(doc.y) - NumCast(contextDoc.height) / 2;
         }
 
         let docView: DocumentView | null;
@@ -178,7 +178,7 @@ export class DocumentManager {
                     (dockFunc || CollectionDockingView.AddRightSplit)(contextDoc, undefined);
                     setTimeout(() => {
                         this.jumpToDocument(docDelegate, willZoom, forceDockFunc, dockFunc, linkPage);
-                    }, 10);
+                    }, 1000);
                 }
             }
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index cacaddcc8..944ae586c 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -515,8 +515,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 doc.x = (doc.x || 0) + dX * (actualdW - width);
                 doc.y = (doc.y || 0) + dY * (actualdH - height);
                 let proto = doc.isTemplate ? doc : Doc.GetProto(element.props.Document); // bcz: 'doc' didn't work here...
-                let fixedAspect = e.ctrlKey || (!BoolCast(doc.ignoreAspect) && nwidth && nheight);
-                if (fixedAspect && e.ctrlKey && BoolCast(doc.ignoreAspect)) {
+                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;
@@ -531,7 +531,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                             Doc.SetInPlace(element.props.Document, "nativeWidth", actualdW / (doc.width || 1) * (doc.nativeWidth || 0), true);
                         }
                         doc.width = actualdW;
-                        if (fixedAspect) doc.height = nheight / nwidth * doc.width;
+                        if (fixedAspect && !doc.fitWidth) doc.height = nheight / nwidth * doc.width;
                         else doc.height = actualdH;
                     }
                     else {
@@ -539,7 +539,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                             Doc.SetInPlace(element.props.Document, "nativeHeight", actualdH / (doc.height || 1) * (doc.nativeHeight || 0), true);
                         }
                         doc.height = actualdH;
-                        if (fixedAspect) doc.width = nwidth / nheight * doc.height;
+                        if (fixedAspect && !doc.fitWidth) doc.width = nwidth / nheight * doc.height;
                         else doc.width = actualdW;
                     }
                 } else {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 244b217ed..0d1546e68 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -505,6 +505,7 @@ export class MainView extends React.Component {
                     <li key="marker"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Highlighter)} title="Highlighter" style={this.selected(InkTool.Highlighter)}><FontAwesomeIcon icon="highlighter" size="lg" /></button></li>
                     <li key="eraser"><button onClick={() => InkingControl.Instance.switchTool(InkTool.Eraser)} title="Eraser" style={this.selected(InkTool.Eraser)}><FontAwesomeIcon icon="eraser" size="lg" /></button></li>
                     <li key="inkControls"><InkingControl /></li>
+                    <li key="logout"><button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></li>
                 </ul>
             </div>
         </div >;
@@ -520,12 +521,8 @@ export class MainView extends React.Component {
     /* @TODO this should really be moved into a moveable toolbar component, but for now let's put it here to meet the deadline */
     @computed
     get miscButtons() {
-        let logoutRef = React.createRef<HTMLDivElement>();
-
         return [
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
-            <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
         ];
 
     }
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index 0e7e0afa7..6f5abd05b 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -1,8 +1,5 @@
 @import "../../views/globalCssVariables.scss";
 
-.collectiondockingview-content {
-    height: 100%;
-}
 .lm_active .messageCounter{
     color:white;
     background: #999999;
@@ -21,7 +18,7 @@
 
 .collectiondockingview-container {
     width: 100%;
-    height: 100%;
+    height:100%;
     border-style: solid;
     border-width: $COLLECTION_BORDER_WIDTH;
     position: absolute;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index b6bc4b4ba..b047e77a8 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -32,6 +32,7 @@ import React = require("react");
 import { ButtonSelector } from './ParentDocumentSelector';
 import { DocumentType } from '../../documents/DocumentTypes';
 library.add(faFile);
+const _global = (window /* browser */ || global /* node */) as any;
 
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -534,12 +535,11 @@ interface DockedFrameProps {
 }
 @observer
 export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
-    _mainCont: HTMLDivElement | undefined = undefined;
+    _mainCont: HTMLDivElement | null = null;
     @observable private _panelWidth = 0;
     @observable private _panelHeight = 0;
     @observable private _document: Opt<Doc>;
     @observable private _dataDoc: Opt<Doc>;
-
     @observable private _isActive: boolean = false;
 
     get _stack(): any {
@@ -577,6 +577,13 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
 
     componentDidMount() {
+        let observer = new _global.ResizeObserver(action((entries: any) => {
+            for (let entry of entries) {
+                this._panelWidth = entry.contentRect.width;
+                this._panelHeight = entry.contentRect.height;
+            }
+        }));
+        observer.observe(this.props.glContainer._element[0]);
         this.props.glContainer.layoutManager.on("activeContentItemChanged", this.onActiveContentItemChanged);
         this.props.glContainer.on("tab", this.onActiveContentItemChanged);
         this.onActiveContentItemChanged();
@@ -595,15 +602,16 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         }
     }
 
-    panelWidth = () => this._document!.ignoreAspect ? this._panelWidth : Math.min(this._panelWidth, Math.max(NumCast(this._document!.width), this.nativeWidth()));
-    panelHeight = () => this._document!.ignoreAspect ? this._panelHeight : Math.min(this._panelHeight, Math.max(NumCast(this._document!.height), this.nativeHeight()));
+    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()));
 
-    nativeWidth = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeWidth) || this._panelWidth : 0;
-    nativeHeight = () => !this._document!.ignoreAspect ? NumCast(this._document!.nativeHeight) || this._panelHeight : 0;
+    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;
 
     contentScaling = () => {
         if (this._document!.type === DocumentType.PDF) {
-            if (this._panelHeight / NumCast(this._document!.nativeHeight) > this._panelWidth / NumCast(this._document!.nativeWidth)) {
+            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);
             } else {
                 return this._panelHeight / NumCast(this._document!.nativeHeight);
@@ -639,13 +647,10 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             return CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
         }
     }
-    @computed get docView() {
-        if (!this._document) {
-            return (null);
-        }
-        let resolvedDataDoc = this._document.layout instanceof Doc ? this._document : this._dataDoc;
-        return <DocumentView key={this._document[Id]}
-            Document={this._document}
+    docView(document: Doc) {
+        let resolvedDataDoc = document.layout instanceof Doc ? document : this._dataDoc;
+        return <DocumentView key={document[Id]}
+            Document={document}
             DataDoc={resolvedDataDoc}
             bringToFront={emptyFunction}
             addDocument={undefined}
@@ -668,28 +673,14 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
             getScale={returnOne} />;
     }
 
-    @computed get content() {
-        return (
-            <div className="collectionDockingView-content" ref={action((ref: HTMLDivElement) => {
-                this._mainCont = ref;
-                if (ref) {
-                    this._panelWidth = Number(getComputedStyle(ref).width!.replace("px", ""));
-                    this._panelHeight = Number(getComputedStyle(ref).height!.replace("px", ""));
-                }
-            })}
-                style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px)` }}>
-                {this.docView}
-            </div >);
-    }
-
     render() {
-        if (!this._isActive || !this._document) return null;
-        let theContent = this.content;
-        return !this._document ? (null) :
-            <Measure offset onResize={action((r: any) => { this._panelWidth = r.offset.width; this._panelHeight = r.offset.height; })}>
-                {({ measureRef }) => <div ref={measureRef}>
-                    {theContent}
-                </div>}
-            </Measure>;
+        return (!this._isActive || !this._document) ? (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%"
+                }}>
+                {this.docView(this._document)}
+            </div >);
     }
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 721732774..4b260d111 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -401,27 +401,34 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             }
         }
         SelectionManager.DeselectAll();
-        const newPanX = NumCast(doc.x) + NumCast(doc.width) / 2;
-        const newPanY = NumCast(doc.y) + NumCast(doc.height) / 2;
-        const newState = HistoryUtil.getState();
-        newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
-        HistoryUtil.pushState(newState);
-
-        let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
-
-        this.setPan(newPanX, newPanY);
-        this.Document.panTransformType = "Ease";
-        this.props.focus(this.props.Document);
-        willZoom && this.setScaleToZoom(doc, scale);
-
-        afterFocus && setTimeout(() => {
-            if (afterFocus && afterFocus()) {
-                this.Document.panX = savedState.px;
-                this.Document.panY = savedState.py;
-                this.Document.scale = savedState.s;
-                this.Document.panTransformType = savedState.pt;
-            }
-        }, 1000);
+        if (this.props.Document.scrollHeight) {
+            let annotOn = Cast(doc.annotationOn, Doc) as Doc;
+            let offset = annotOn && (NumCast(annotOn.height) / 2);
+            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;
+            const newState = HistoryUtil.getState();
+            newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };
+            HistoryUtil.pushState(newState);
+
+            let savedState = { px: this.Document.panX, py: this.Document.panY, s: this.Document.scale, pt: this.Document.panTransformType };
+
+            this.setPan(newPanX, newPanY);
+            this.Document.panTransformType = "Ease";
+            this.props.focus(this.props.Document);
+            willZoom && this.setScaleToZoom(doc, scale);
+
+            afterFocus && setTimeout(() => {
+                if (afterFocus && afterFocus()) {
+                    this.Document.panX = savedState.px;
+                    this.Document.panY = savedState.py;
+                    this.Document.scale = savedState.s;
+                    this.Document.panTransformType = savedState.pt;
+                }
+            }, 1000);
+        }
+
     }
 
     setScaleToZoom = (doc: Doc, scale: number = 0.5) => {
@@ -449,7 +456,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             PanelHeight: childLayout[HeightSym],
             ContentScaling: returnOne,
             ContainingCollectionView: this.props.CollectionView,
-            ContainingCollectionDoc: this.props.ContainingCollectionDoc,
+            ContainingCollectionDoc: this.props.Document,
             focus: this.focusDocument,
             backgroundColor: this.getClusterColor,
             parentActive: this.props.active,
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 44611869e..82193aefa 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -188,16 +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]);
-        // console.log("pointer up!");
         if (this._visible) {
-            // console.log("visible");
             let mselect = this.marqueeSelect();
             if (!e.shiftKey) {
                 SelectionManager.DeselectAll(mselect.length ? undefined : this.props.container.props.Document);
             }
             this.props.selectDocuments(mselect.length ? mselect : [this.props.container.props.Document]);
         }
-        //console.log("invisible");
         this.cleanupInteractions(true);
 
         if (e.altKey) {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index c3d2c9e51..c4fed200f 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 as Doc : undefined;
+        let ld = this.layoutDoc.layout instanceof Doc ? this.layoutDoc.layout : undefined;
         let br = StrCast((ld || this.props.Document).borderRounding);
         br = !br && ruleRounding ? ruleRounding : br;
         if (br.endsWith("%")) {
@@ -100,7 +100,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     @observable _animPos: number[] | undefined = undefined;
 
-    finalPanelWidh = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
+    finalPanelWidth = () => this.dataProvider ? this.dataProvider.width : this.panelWidth();
     finalPanelHeight = () => this.dataProvider ? this.dataProvider.height : this.panelHeight();
 
     render() {
@@ -124,7 +124,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                     ContentScaling={this.contentScaling}
                     ScreenToLocalTransform={this.getTransform}
                     backgroundColor={this.clusterColorFunc}
-                    PanelWidth={this.finalPanelWidh}
+                    PanelWidth={this.finalPanelWidth}
                     PanelHeight={this.finalPanelHeight}
                 />
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6ee88f834..bd702c6a2 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -223,15 +223,18 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         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]];
-            let linkedFwdDocs = first.length ? [first[0].anchor2 as Doc, first[0].anchor1 as Doc] : [expandedDocs[0], expandedDocs[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.some(l => l instanceof Promise)) {
+            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,
@@ -601,7 +604,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             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` : nativeWidth !== "100%" ? nativeWidth : "100%";
+        const nativeHeight = this.Document.ignoreAspect || this.props.Document.fitWidth ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : nativeWidth !== "100%" ? nativeWidth : "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");
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 923dd1544..565fb0505 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -815,6 +815,40 @@ 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();
+        }
+    }
+
+    onPointerUp = (e: React.PointerEvent): void => {
+        FormattedTextBoxComment.textBox = this;
+        if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
+            e.stopPropagation();
+        }
+    }
+
+    @action
+    onFocused = (e: React.FocusEvent): void => {
+        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
+        if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
+            e.stopPropagation();
+        }
+    }
+
+    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;
@@ -829,8 +863,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
             if (node) {
                 let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
-                href = link && link.attrs.href;
-                location = link && link.attrs.location;
+                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) {
@@ -848,7 +884,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                                         return;
                                     }
                                 }
-                                if (targetContext) {
+                                if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
                                     DocumentManager.Instance.jumpToDocument(targetContext, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
                                 } else if (jumpToDoc) {
                                     DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, false, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
@@ -870,39 +906,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             }
 
         }
-        if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
-            e.preventDefault();
-        }
-    }
-
-    onPointerUp = (e: React.PointerEvent): void => {
-        FormattedTextBoxComment.textBox = this;
-        if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
-            e.stopPropagation();
-        }
-    }
-
-    @action
-    onFocused = (e: React.FocusEvent): void => {
-        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
-        if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
-            e.stopPropagation();
-        }
-    }
-
-    onClick = (e: React.MouseEvent): void => {
         // this hackiness handles clicking on the list item bullets to do expand/collapse.  the bullets are ::before pseudo elements so there's no real way to hit test against them.
         if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
             let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 624593245..004f50590 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -38,6 +38,7 @@ library.add(faFileAudio, faAsterisk);
 
 export const pageSchema = createSchema({
     curPage: "number",
+    fitWidth: "boolean"
 });
 
 interface Window {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 0fcbaaa7c..fe71e76fd 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -20,6 +20,9 @@ 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);
@@ -58,7 +61,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
             this.Document.nativeWidth = nw * 96 / 72;
             this.Document.nativeHeight = this.Document.nativeHeight ? nw * 96 / 72 * oldaspect : nh * 96 / 72;
         }
-        this.Document.height = this.Document[WidthSym]() * (nh / nw);
+        !this.Document.fitWidth && !this.Document.ignoreAspect && (this.Document.height = this.Document[WidthSym]() * (nh / nw));
     }
 
     public search(string: string, fwd: boolean) { this._pdfViewer && this._pdfViewer.search(string, fwd); }
@@ -165,14 +168,23 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
             </div>);
     }
 
+    specificContextMenu = (e: React.MouseEvent): void => {
+        const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
+        let funcs: ContextMenuProps[] = [];
+        pdfUrl && funcs.push({ description: "Copy path", event: () => Utils.CopyText(pdfUrl.url.pathname), icon: "expand-arrows-alt" });
+        funcs.push({ description: "Toggle Fit Width " + (this.Document.fitWidth ? "Off" : "On"), event: () => this.Document.fitWidth = !this.Document.fitWidth, icon: "expand-arrows-alt" });
+
+        ContextMenu.Instance.addItem({ description: "Pdf Funcs...", subitems: funcs, icon: "asterisk" });
+    }
+
     render() {
         const pdfUrl = Cast(this.dataDoc[this.props.fieldKey], PdfField);
         let classname = "pdfBox-cont" + (InkingControl.Instance.selectedTool || !this.active ? "" : "-interactive");
         return (!(pdfUrl instanceof PdfField) || !this._pdf ?
             <div>{`pdf, ${this.dataDoc[this.props.fieldKey]}, not found`}</div> :
-            <div className={classname} onPointerDown={(e: React.PointerEvent) => {
+            <div className={classname} onContextMenu={this.specificContextMenu} onPointerDown={(e: React.PointerEvent) => {
                 let hit = document.elementFromPoint(e.clientX, e.clientY);
-                if (hit && hit.localName === "span" && this.props.isSelected()) {
+                if (hit && hit.localName === "span" && this.props.isSelected()) {  // drag selecting text stops propagation
                     e.button === 0 && e.stopPropagation();
                 }
             }}>
@@ -182,7 +194,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                     Document={this.props.Document} DataDoc={this.dataDoc} ContentScaling={this.props.ContentScaling}
                     addDocTab={this.props.addDocTab} GoToPage={this.gotoPage}
                     pinToPres={this.props.pinToPres} addDocument={this.props.addDocument}
-                    ScreenToLocalTransform={this.props.ScreenToLocalTransform}
+                    ScreenToLocalTransform={this.props.ScreenToLocalTransform} select={this.props.select}
                     isSelected={this.props.isSelected} whenActiveChanged={this.whenActiveChanged}
                     fieldKey={this.props.fieldKey} fieldExtensionDoc={this.extensionDoc} />
                 {this.settingsPanel()}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5ad4ffd48..13fd8ea98 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -9,14 +9,12 @@ 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 { emptyFunction, returnOne, Utils } from "../../../Utils";
+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";
 import { Transform } from "../../util/Transform";
-import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
-import Annotation from "./Annotation";
 import PDFMenu from "./PDFMenu";
 import "./PDFViewer.scss";
 import React = require("react");
@@ -24,7 +22,8 @@ import * as rp from "request-promise";
 import { CollectionPDFView } from "../collections/CollectionPDFView";
 import { CollectionVideoView } from "../collections/CollectionVideoView";
 import { CollectionView } from "../collections/CollectionView";
-import { SelectionManager } from "../../util/SelectionManager";
+import Annotation from "./Annotation";
+import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 const PDFJSViewer = require("pdfjs-dist/web/pdf_viewer");
 const pdfjsLib = require("pdfjs-dist");
 
@@ -41,6 +40,7 @@ interface IViewerProps {
     PanelWidth: () => number;
     PanelHeight: () => number;
     ContentScaling: () => number;
+    select: (isCtrlPressed: boolean) => void;
     renderDepth: number;
     isSelected: () => boolean;
     loaded: (nw: number, nh: number, np: number) => void;
@@ -106,11 +106,20 @@ 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.setupPdfJsViewer();
-            this._selectionReactionDisposer && this._selectionReactionDisposer();
-            this._selectionReactionDisposer = undefined;
-        })
+        this._selectionReactionDisposer = reaction(() => this.props.isSelected(), () => this.setupPdfJsViewer());
+        this._reactionDisposer = reaction(
+            () => this.props.Document.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;
+                }
+            },
+            { fireImmediately: true }
+        );
     }
 
     componentWillUnmount = () => {
@@ -153,12 +162,14 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     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);
+            Doc.GetProto(this.props.Document).scrollHeight = this._pageSizes.reduce((size, page) => size + page.height, 0) * 96 / 72;
         }
     }
 
     @action
     setupPdfJsViewer = async () => {
+        this._selectionReactionDisposer && this._selectionReactionDisposer();
+        this._selectionReactionDisposer = undefined;
         this._showWaiting = true;
         this.props.setPdfViewer(this);
         await this.initialLoad();
@@ -180,10 +191,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
             }),
             { fireImmediately: true }
         );
-        this._reactionDisposer = reaction(
-            () => this.props.Document.panY,
-            () => this._mainCont.current && this._mainCont.current.scrollTo({ top: NumCast(this.props.Document.panY) || 0, behavior: "auto" })
-        );
 
         document.removeEventListener("copy", this.copy);
         document.addEventListener("copy", this.copy);
@@ -228,10 +235,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
             annoDocs.push(annoDoc);
             annoDoc.isButton = true;
             anno.remove();
-            // this.props.addDocument && this.props.addDocument(annoDoc, false);
             mainAnnoDoc = annoDoc;
+            mainAnnoDocProto = Doc.GetProto(mainAnnoDoc);
             mainAnnoDocProto.y = annoDoc.y;
-            mainAnnoDocProto = Doc.GetProto(annoDoc);
         } else {
             this._savedAnnotations.forEach((key: number, value: HTMLDivElement[]) => value.map(anno => {
                 let annoDoc = new Doc();
@@ -381,7 +387,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         this._downX = e.clientX;
         this._downY = e.clientY;
         if (NumCast(this.props.Document.scale, 1) !== 1) return;
-        if (e.button !== 0 && this.active()) {
+        if ((e.button !== 0 || e.altKey) && this.active()) {
             this._setPreviewCursor && this._setPreviewCursor(e.clientX, e.clientY, true);
         }
         this._marqueeing = false;
@@ -638,18 +644,15 @@ export class PDFViewer extends React.Component<IViewerProps> {
     }
 
     @computed get annotationLayer() {
-        trace();
         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`} />)}
         </div>;
     }
     @computed get pdfViewerDiv() {
-        trace();
         return <div className="pdfViewer-text" ref={this._viewer} style={{ transformOrigin: "left top" }} />;
     }
     @computed get standinViews() {
-        trace();
         return <>
             {this._showCover ? this.getCoverImage() : (null)}
             {this._showWaiting ? <img className="pdfViewer-waiting" key="waiting" src={"/assets/loading.gif"} /> : (null)}
@@ -710,7 +713,7 @@ class PdfViewerMarquee extends React.Component<PdfViewerMarqueeProps> {
                 width: `${this.props.width()}px`, height: `${this.props.height()}px`,
                 border: `${this.props.width() === 0 ? "" : "2px dashed black"}`
             }}>
-        </div>
+        </div>;
     }
 }
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1b3c8b0b0..79b73aba8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -344,7 +344,7 @@ export namespace Doc {
         let list = Cast(target[key], listSpec(Doc));
         if (list) {
             if (allowDuplicates !== true) {
-                let pind = list.reduce((l, d, i) => d instanceof Doc && Doc.AreProtosEqual(d, doc) ? i : l, -1);
+                let pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
                 if (pind !== -1) {
                     list.splice(pind, 1);
                 }
-- 
cgit v1.2.3-70-g09d2


From 0cf79318e179588ff5855156b2ad0d729958c9e8 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 29 Sep 2019 18:08:06 -0400
Subject: working on sharing / doc opening logic

---
 src/client/views/MainView.tsx                      | 27 ++++++++++++++++++----
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 2 files changed, 24 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index be62de1d6..29e046e8b 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -257,16 +257,18 @@ export class MainView extends React.Component {
         // Load the user's active workspace, or create a new one if initial session after signup
         let received = CurrentUserUtils.MainDocId;
         let doc: Opt<Doc>;
-        if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
-            this.openWorkspace(doc);
-        } else if (received) {
+        if (received) {
             if (!this.userDoc) {
+                // if no one's logged in...
+                console.log("NO One's LOGGED IN!");
                 reaction(
                     () => CurrentUserUtils.GuestTarget,
                     target => target && this.createNewWorkspace(),
                     { fireImmediately: true }
                 );
             } else if (this.urlState.sharing) {
+                // if a logged in user is opening a shared link
+                console.log("SHARING, opening right split after docking view initialized");
                 if (received && this.urlState.sharing) {
                     reaction(
                         () => {
@@ -279,6 +281,14 @@ export class MainView extends React.Component {
                                     if (field instanceof Doc && field.viewType !== CollectionViewType.Docking) {
                                         CollectionDockingView.AddRightSplit(field, undefined);
                                         DocumentManager.Instance.jumpToDocument(field, true, undefined, undefined, undefined, undefined);
+                                        const { nro, readonly } = this.urlState;
+                                        HistoryUtil.pushState({
+                                            type: "doc",
+                                            docId: CurrentUserUtils.MainDocId!,
+                                            readonly,
+                                            nro,
+                                            sharing: false,
+                                        });
                                     }
                                 });
                             }
@@ -286,13 +296,22 @@ export class MainView extends React.Component {
                     );
                 }
             } else {
+                // a logged in user is just opening a document, likely their own workspace
+                console.log("DEFAULT");
                 DocServer.GetRefField(received).then(field => {
+                    console.log(field instanceof Doc ? "Opening" : "Creating");
                     field instanceof Doc ? this.openWorkspace(field) :
                         this.createNewWorkspace(received);
                 });
             }
         } else {
-            this.createNewWorkspace();
+            if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
+                console.log("Opening existing workspace for existing user!");
+                this.openWorkspace(doc);
+            } else {
+                console.log("Creating a new workspace for existing user!");
+                this.createNewWorkspace();
+            }
         }
     }
 
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 230b04a68..6d054b4fc 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -63,7 +63,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
     constructor(props: SubCollectionViewProps) {
         super(props);
-        !CollectionDockingView.Instance && (CollectionDockingView.Instance = this);
+        !CollectionDockingView.Instance && runInAction(() => CollectionDockingView.Instance = this);
         //Why is this here?
         (window as any).React = React;
         (window as any).ReactDOM = ReactDOM;
-- 
cgit v1.2.3-70-g09d2


From 8fb87d09649c269f27b2d475a78b935fc80c637e Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 29 Sep 2019 18:38:04 -0400
Subject: fixed login logic

---
 src/client/views/MainView.tsx | 76 ++++++++++++++-----------------------------
 1 file changed, 25 insertions(+), 51 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 29e046e8b..332e07f1c 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -256,60 +256,34 @@ 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;
-        let doc: Opt<Doc>;
-        if (received) {
-            if (!this.userDoc) {
-                // if no one's logged in...
-                console.log("NO One's LOGGED IN!");
+        if (received && !this.userDoc) {
+            reaction(
+                () => CurrentUserUtils.GuestTarget,
+                target => target && this.createNewWorkspace(),
+                { fireImmediately: true }
+            );
+        } else {
+            if (received && this.urlState.sharing) {
                 reaction(
-                    () => CurrentUserUtils.GuestTarget,
-                    target => target && this.createNewWorkspace(),
-                    { fireImmediately: true }
+                    () => {
+                        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);
+                                }
+                            });
+                        }
+                    },
                 );
-            } else if (this.urlState.sharing) {
-                // if a logged in user is opening a shared link
-                console.log("SHARING, opening right split after docking view initialized");
-                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);
-                                        const { nro, readonly } = this.urlState;
-                                        HistoryUtil.pushState({
-                                            type: "doc",
-                                            docId: CurrentUserUtils.MainDocId!,
-                                            readonly,
-                                            nro,
-                                            sharing: false,
-                                        });
-                                    }
-                                });
-                            }
-                        },
-                    );
-                }
-            } else {
-                // a logged in user is just opening a document, likely their own workspace
-                console.log("DEFAULT");
-                DocServer.GetRefField(received).then(field => {
-                    console.log(field instanceof Doc ? "Opening" : "Creating");
-                    field instanceof Doc ? this.openWorkspace(field) :
-                        this.createNewWorkspace(received);
-                });
             }
-        } else {
+            let doc: Opt<Doc>;
             if (this.userDoc && (doc = await Cast(this.userDoc.activeWorkspace, Doc))) {
-                console.log("Opening existing workspace for existing user!");
                 this.openWorkspace(doc);
             } else {
-                console.log("Creating a new workspace for existing user!");
                 this.createNewWorkspace();
             }
         }
@@ -322,7 +296,7 @@ export class MainView extends React.Component {
             y: 400,
             width: this.pwidth * .7,
             height: this.pheight,
-            title: CurrentUserUtils.GuestTarget ? `Guest View of ${StrCast(CurrentUserUtils.GuestTarget.title)}` : "My Blank Collection"
+            title: "My Blank Collection"
         };
         let workspaces: FieldResult<Doc>;
         let freeformDoc = CurrentUserUtils.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions);
@@ -636,11 +610,11 @@ export class MainView extends React.Component {
     @computed
     get miscButtons() {
         let logoutRef = React.createRef<HTMLDivElement>();
-
+        const prompt = CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out";
         return [
             this.isSearchVisible ? <div className="main-searchDiv" key="search" style={{ top: '34px', right: '1px', position: 'absolute' }} > <FilterBox /> </div> : null,
             <div className="main-buttonDiv" key="logout" style={{ bottom: '0px', right: '1px', position: 'absolute' }} ref={logoutRef}>
-                <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>Log Out</button></div>
+                <button onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>{prompt}</button></div>
         ];
 
     }
-- 
cgit v1.2.3-70-g09d2


From 2b41e6743a4ebfb3b3c30a87633f547f30635ed4 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Sun, 29 Sep 2019 19:05:11 -0400
Subject: final

---
 src/client/views/MainView.tsx                   | 10 ----------
 src/client/views/collections/CollectionView.tsx |  8 --------
 2 files changed, 18 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2b5a2698e..cf09bd2d0 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -164,16 +164,6 @@ export class MainView extends React.Component {
         }
     }
 
-    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 }));
-        GooglePhotos.Query.ContentSearch({ included: [GooglePhotos.ContentCategories.ANIMALS] }).then(console.log);
-    }
-
     componentWillUnMount() {
         window.removeEventListener("keydown", KeyManager.Instance.handle);
         //close presentation 
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 2d74da41a..90fa00202 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -114,14 +114,6 @@ export class CollectionView extends React.Component<FieldViewProps> {
                     break;
                 }
             }
-            subItems.push({
-                description: "Pivot", icon: "copy", event: async () => {
-                    const doc = this.props.Document;
-                    doc.viewType = CollectionViewType.Freeform;
-                    (await DocListCastAsync(doc.data))!.filter(doc => Cast(doc.data, ImageField)).forEach(doc => doc.ignoreAspect = true);
-                    doc.usePivotLayout = true;
-                }
-            });
             !existingVm && ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems, icon: "eye" });
 
             let existing = ContextMenu.Instance.findByDescription("Layout...");
-- 
cgit v1.2.3-70-g09d2


From d49e573d62fb9caee0568b454cb3555775a887ff Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 30 Sep 2019 11:14:52 -0400
Subject: start of presentationview cleanup

---
 src/client/views/MainView.tsx                      |  43 +--
 src/client/views/nodes/PresBox.tsx                 | 410 ++++++++-------------
 .../presentationview/PresentationModeMenu.tsx      |   4 -
 3 files changed, 161 insertions(+), 296 deletions(-)

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

diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index cf09bd2d0..dadff21a7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -1,5 +1,5 @@
 import { IconName, library } from '@fortawesome/fontawesome-svg-core';
-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 } 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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, configure, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
@@ -7,23 +7,24 @@ import "normalize.css";
 import * as React from 'react';
 import { SketchPicker } from 'react-color';
 import Measure from 'react-measure';
-import { List } from '../../new_fields/List';
-import { Doc, DocListCast, Opt, HeightSym, FieldResult, Field } from '../../new_fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, HeightSym, Opt } from '../../new_fields/Doc';
 import { Id } from '../../new_fields/FieldSymbols';
 import { InkTool } from '../../new_fields/InkField';
+import { List } from '../../new_fields/List';
 import { listSpec } from '../../new_fields/Schema';
-import { BoolCast, Cast, FieldValue, StrCast, NumCast } from '../../new_fields/Types';
+import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types';
 import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
 import { RouteStore } from '../../server/RouteStore';
-import { emptyFunction, returnOne, returnTrue, Utils, returnEmptyString } from '../../Utils';
+import { emptyFunction, returnEmptyString, returnOne, returnTrue, Utils } from '../../Utils';
 import { DocServer } from '../DocServer';
+import { Docs, DocumentOptions } from '../documents/Documents';
 import { ClientUtils } from '../util/ClientUtils';
 import { DictationManager } from '../util/DictationManager';
 import { SetupDrag } from '../util/DragManager';
-import { Transform } from '../util/Transform';
-import { UndoManager, undoBatch } from '../util/UndoManager';
-import { Docs, DocumentOptions } from '../documents/Documents';
 import { HistoryUtil } from '../util/History';
+import SharingManager from '../util/SharingManager';
+import { Transform } from '../util/Transform';
+import { UndoManager } from '../util/UndoManager';
 import { CollectionBaseView, CollectionViewType } from './collections/CollectionBaseView';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { CollectionTreeView } from './collections/CollectionTreeView';
@@ -33,20 +34,13 @@ 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 PresModeMenu from './presentationview/PresentationModeMenu';
-import { PresBox } from './nodes/PresBox';
-import { GooglePhotos } from '../apis/google_docs/GooglePhotosClientUtils';
-import { ImageField } from '../../new_fields/URLField';
-import { LinkFollowBox } from './linking/LinkFollowBox';
-import { DocumentManager } from '../util/DocumentManager';
-import { SchemaHeaderField, RandomPastel } from '../../new_fields/SchemaHeaderField';
-import MainViewModal from './MainViewModal';
-import SharingManager from '../util/SharingManager';
 
 @observer
 export class MainView extends React.Component {
@@ -185,9 +179,8 @@ export class MainView extends React.Component {
                     CurrentUserUtils.MainDocId = pathname[1];
                     if (!this.userDoc) {
                         runInAction(() => this.flyoutWidth = 0);
-                        DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action(field => {
-                            field instanceof Doc && (CurrentUserUtils.GuestTarget = field);
-                        }));
+                        DocServer.GetRefField(CurrentUserUtils.MainDocId).then(action((field: Opt<Field>) =>
+                            field instanceof Doc && (CurrentUserUtils.GuestTarget = field)));
                     }
                 }
             }
@@ -634,14 +627,6 @@ export class MainView extends React.Component {
         );
     }
 
-    @computed get miniPresentation() {
-        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, undefined, "onRight"); });
-        return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation.presMode ? (null) : <PresModeMenu next={next} back={back} presStatus={PresBox.CurrentPresentation.presStatus} startOrResetPres={startOrResetPres} closePresMode={closePresMode} > </PresModeMenu>;
-    }
-
     render() {
         return (
             <div id="main-div">
@@ -649,7 +634,7 @@ export class MainView extends React.Component {
                 <SharingManager />
                 <DocumentDecorations />
                 {this.mainContent}
-                {this.miniPresentation}
+                {PresBox.miniPresentation}
                 <PreviewCursor />
                 <ContextMenu />
                 {this.nodesMenu()}
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index 5afd85430..d246da87a 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -2,21 +2,20 @@ import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable, runInAction } from "mobx";
+import { action, observable, runInAction, computed } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { Id } from "../../../new_fields/FieldSymbols";
-import { List } from "../../../new_fields/List";
+import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { listSpec } from "../../../new_fields/Schema";
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from "../../../new_fields/Types";
-import { Utils } from "../../../Utils";
+import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
 import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
+import { ContextMenu } from "../ContextMenu";
 import PresentationElement from "../presentationview/PresentationElement";
 import PresentationViewList from "../presentationview/PresentationList";
 import "../presentationview/PresentationView.scss";
 import { FieldView, FieldViewProps } from './FieldView';
-import { ContextMenu } from "../ContextMenu";
+import PresModeMenu from "../presentationview/PresentationModeMenu";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
 
 library.add(faArrowLeft);
 library.add(faArrowRight);
@@ -27,147 +26,99 @@ library.add(faTimes);
 library.add(faMinus);
 library.add(faEdit);
 
-
-export interface PresViewProps {
-    Documents: List<Doc>;
-}
-
-const expandedWidth = 450;
-
 @observer
-export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
-
-
+export class PresBox extends React.Component<FieldViewProps> {
     public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
 
     public static Instance: PresBox;
-
-    //Keeping track of the doc for the current presentation -- bcz: keeping a list of current presentations shouldn't be needed.  Let users create them, store them, as they see fit.
-    @computed get curPresentation() { return this.props.Document; }
-
     //mapping from docs to their rendered component
-    @observable presElementsMappings: Map<Doc, PresentationElement> = new Map();
+    @observable _presElementsMappings: Map<Doc, PresentationElement> = new Map();
     //variable that holds all the docs in the presentation
-    @observable childrenDocs: Doc[] = [];
-    //variable to hold if presentation is started
-    @observable presStatus: boolean = false;
-    //Mapping of guids to presentations.
-    @observable presentationsMapping: Map<String, Doc> = new Map();
-    //Mapping of presentations to guid, so that select option values can be given.
-    @observable presentationsKeyMapping: Map<Doc, String> = new Map();
-    //Variable to keep track of guid of the current presentation
-    @observable currentSelectedPresValue: string | undefined;
-    //A flag to keep track if title input is open, which is used in rendering.
-    @observable PresTitleInputOpen: boolean = false;
-    //Variable that holds reference to title input, so that new presentations get titles assigned.
-    @observable titleInputElement: HTMLInputElement | undefined;
-    @observable PresTitleChangeOpen: boolean = false;
-
-    @observable opacity = 1;
-    @observable persistOpacity = true;
-    @observable labelOpacity = 0;
-    @observable presMode = false;
+    @observable _childrenDocs: Doc[] = [];
 
+    @observable _opacity = 1;
+    @observable _persistOpacity = true;
+    @observable _labelOpacity = 0;
+    // whether presentation view has been minimized
+    @observable _presMode = false;
     @observable public static CurrentPresentation: PresBox;
 
+    @computed static get miniPresentation() {
+        let next = () => PresBox.CurrentPresentation.next();
+        let back = () => PresBox.CurrentPresentation.back();
+        let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres();
+        let closePresMode = action(() => {
+            PresBox.CurrentPresentation._presMode = false;
+            CollectionDockingView.AddRightSplit(PresBox.CurrentPresentation.props.Document, undefined);
+        });
+        return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation._presMode ? (null) :
+            <PresModeMenu next={next} back={back} presStatus={BoolCast(PresBox.CurrentPresentation.props.Document.presStatus)}
+                startOrResetPres={startOrResetPres} closePresMode={closePresMode} />;
+    }
+
     //initilize class variables
     constructor(props: FieldViewProps) {
         super(props);
         runInAction(() => PresBox.CurrentPresentation = this);
     }
 
-    @action
-    toggle = (forcedValue: boolean | undefined) => {
-        if (forcedValue !== undefined) {
-            this.curPresentation.width = forcedValue ? expandedWidth : 0;
-        } else {
-            this.curPresentation.width = this.curPresentation.width === expandedWidth ? 0 : expandedWidth;
-        }
-    }
-
-    //Second lifecycle function that gets called when component mounts. It makes sure toS
-    //get the back-up information from previous session for the current presentation.
-    async componentDidMount() {
-        this.setPresentationBackUps();
-    }
-
-
-    /**
-     * The function that retrieves the backUps for the current Presentation if present,
-     * otherwise initializes.
-     */
-    setPresentationBackUps = async () => {
-        //storing the presentation status,ie. whether it was stopped or playing
-        let presStatusBackUp = BoolCast(this.curPresentation.presStatus);
-        runInAction(() => this.presStatus = presStatusBackUp);
-    }
-
-    //observable means render is re-called every time variable is changed
-    @observable
-    collapsed: boolean = false;
     next = async () => {
-        const current = NumCast(this.curPresentation.selectedDoc);
+        const current = NumCast(this.props.Document.selectedDoc);
         //asking to get document at current index
         let docAtCurrentNext = await this.getDocAtIndex(current + 1);
-        if (docAtCurrentNext === undefined) {
-            return;
-        }
-        let nextSelected = current + 1;
+        if (docAtCurrentNext !== undefined) {
+            let presDocs = DocListCast(this.props.Document.data);
+            let nextSelected = current + 1;
 
-        let presDocs = DocListCast(this.curPresentation.data);
-        for (; nextSelected < presDocs.length - 1; nextSelected++) {
-            if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
-                break;
+            for (; nextSelected < presDocs.length - 1; nextSelected++) {
+                if (!this._presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+                    break;
+                }
             }
-        }
-
-        this.gotoDocument(nextSelected, current);
 
+            this.gotoDocument(nextSelected, current);
+        }
     }
     back = async () => {
-        const current = NumCast(this.curPresentation.selectedDoc);
+        const current = NumCast(this.props.Document.selectedDoc);
         //requesting for the doc at current index
         let docAtCurrent = await this.getDocAtIndex(current);
-        if (docAtCurrent === undefined) {
-            return;
-        }
+        if (docAtCurrent !== undefined) {
 
-        //asking for its presentation id.
-        let curPresId = StrCast(docAtCurrent.presentId);
-        let prevSelected = current;
-        let zoomOut: boolean = false;
+            //asking for its presentation id.
+            let prevSelected = current;
+            let zoomOut: boolean = false;
 
-        //checking if this presentation id is mapped to a group, if so chosing the first element in group
-        let presDocs = DocListCast(this.curPresentation.data);
-        let currentsArray: Doc[] = [];
-        for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
-            currentsArray.push(presDocs[prevSelected]);
-        }
-        prevSelected = Math.max(0, prevSelected - 1);
-
-        //checking if any of the group members had used zooming in
-        currentsArray.forEach((doc: Doc) => {
-            //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
-            if (this.presElementsMappings.get(doc)!.props.document.showButton) {
-                zoomOut = true;
-                return;
+            //checking if this presentation id is mapped to a group, if so chosing the first element in group
+            let presDocs = DocListCast(this.props.Document.data);
+            let currentsArray: Doc[] = [];
+            for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
+                currentsArray.push(presDocs[prevSelected]);
             }
-        });
-
-
-        // if a group set that flag to zero or a single element
-        //If so making sure to zoom out, which goes back to state before zooming action
-        if (current > 0) {
-            if (zoomOut || this.presElementsMappings.get(docAtCurrent)!.showButton) {
-                let prevScale = NumCast(this.childrenDocs[prevSelected].viewScale, null);
-                let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[current]);
-                if (prevScale !== undefined && prevScale !== curScale) {
-                    DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+            prevSelected = Math.max(0, prevSelected - 1);
+
+            //checking if any of the group members had used zooming in
+            currentsArray.forEach((doc: Doc) => {
+                //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
+                if (this._presElementsMappings.get(doc)!.props.document.showButton) {
+                    zoomOut = true;
+                    return;
+                }
+            });
+
+            // if a group set that flag to zero or a single element
+            //If so making sure to zoom out, which goes back to state before zooming action
+            if (current > 0) {
+                if (zoomOut || this._presElementsMappings.get(docAtCurrent)!.showButton) {
+                    let prevScale = NumCast(this._childrenDocs[prevSelected].viewScale, null);
+                    let curScale = DocumentManager.Instance.getScaleOfDocView(this._childrenDocs[current]);
+                    if (prevScale !== undefined && prevScale !== curScale) {
+                        DocumentManager.Instance.zoomIntoScale(docAtCurrent, prevScale);
+                    }
                 }
             }
+            this.gotoDocument(prevSelected, current);
         }
-        this.gotoDocument(prevSelected, current);
-
     }
 
     /**
@@ -176,21 +127,21 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
      * Hide Until Presented, Hide After Presented, Fade After Presented
      */
     showAfterPresented = (index: number) => {
-        this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+        this._presElementsMappings.forEach((presElem, doc) => {
             //the order of cases is aligned based on priority
             if (presElem.props.document.hideTillShownButton) {
-                if (this.childrenDocs.indexOf(key) <= index) {
-                    key.opacity = 1;
+                if (this._childrenDocs.indexOf(doc) <= index) {
+                    doc.opacity = 1;
                 }
             }
             if (presElem.props.document.hideAfterButton) {
-                if (this.childrenDocs.indexOf(key) < index) {
-                    key.opacity = 0;
+                if (this._childrenDocs.indexOf(doc) < index) {
+                    doc.opacity = 0;
                 }
             }
             if (presElem.props.document.fadeButton) {
-                if (this.childrenDocs.indexOf(key) < index) {
-                    key.opacity = 0.5;
+                if (this._childrenDocs.indexOf(doc) < index) {
+                    doc.opacity = 0.5;
                 }
             }
         });
@@ -202,21 +153,21 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
      * Hide Until Presented, Hide After Presented, Fade After Presented
      */
     hideIfNotPresented = (index: number) => {
-        this.presElementsMappings.forEach((presElem: PresentationElement, key: Doc) => {
+        this._presElementsMappings.forEach((presElem, key) => {
             //the order of cases is aligned based on priority
 
             if (presElem.props.document.hideAfterButton) {
-                if (this.childrenDocs.indexOf(key) >= index) {
+                if (this._childrenDocs.indexOf(key) >= index) {
                     key.opacity = 1;
                 }
             }
             if (presElem.props.document.fadeButton) {
-                if (this.childrenDocs.indexOf(key) >= index) {
+                if (this._childrenDocs.indexOf(key) >= index) {
                     key.opacity = 1;
                 }
             }
             if (presElem.props.document.hideTillShownButton) {
-                if (this.childrenDocs.indexOf(key) > index) {
+                if (this._childrenDocs.indexOf(key) > index) {
                     key.opacity = 0;
                 }
             }
@@ -229,26 +180,25 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
      * te option open, navigates to that element.
      */
     navigateToElement = async (curDoc: Doc, fromDoc: number) => {
-        let docToJump: Doc = curDoc;
-        let willZoom: boolean = false;
-
+        let docToJump = curDoc;
+        let willZoom = false;
 
-        let presDocs = DocListCast(this.curPresentation.data);
+        let presDocs = DocListCast(this.props.Document.data);
         let nextSelected = presDocs.indexOf(curDoc);
         let currentDocGroups: Doc[] = [];
         for (; nextSelected < presDocs.length - 1; nextSelected++) {
-            if (!this.presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+            if (!this._presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
                 break;
             }
             currentDocGroups.push(presDocs[nextSelected]);
         }
 
         currentDocGroups.forEach((doc: Doc, index: number) => {
-            if (this.presElementsMappings.get(doc)!.navButton) {
+            if (this._presElementsMappings.get(doc)!.navButton) {
                 docToJump = doc;
                 willZoom = false;
             }
-            if (this.presElementsMappings.get(doc)!.showButton) {
+            if (this._presElementsMappings.get(doc)!.showButton) {
                 docToJump = doc;
                 willZoom = true;
             }
@@ -257,24 +207,23 @@ export class PresBox extends React.Component<FieldViewProps> { //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
-            if (this.presElementsMappings.get(curDoc)!.navButton) {
+            if (this._presElementsMappings.get(curDoc)!.navButton) {
                 DocumentManager.Instance.jumpToDocument(curDoc, false);
-            } else if (this.presElementsMappings.get(curDoc)!.showButton) {
-                let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+            } else if (this._presElementsMappings.get(curDoc)!.showButton) {
+                let curScale = DocumentManager.Instance.getScaleOfDocView(this._childrenDocs[fromDoc]);
                 //awaiting jump so that new scale can be found, since jumping is async
                 await DocumentManager.Instance.jumpToDocument(curDoc, true);
-                let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
-                curDoc.viewScale = newScale;
+                curDoc.viewScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
 
                 //saving the scale user was on before zooming in
                 if (curScale !== 1) {
-                    this.childrenDocs[fromDoc].viewScale = curScale;
+                    this._childrenDocs[fromDoc].viewScale = curScale;
                 }
 
             }
             return;
         }
-        let curScale = DocumentManager.Instance.getScaleOfDocView(this.childrenDocs[fromDoc]);
+        let curScale = DocumentManager.Instance.getScaleOfDocView(this._childrenDocs[fromDoc]);
 
         //awaiting jump so that new scale can be found, since jumping is async
         await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
@@ -282,7 +231,7 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
         curDoc.viewScale = newScale;
         //saving the scale that user was on
         if (curScale !== 1) {
-            this.childrenDocs[fromDoc].viewScale = curScale;
+            this._childrenDocs[fromDoc].viewScale = curScale;
         }
 
     }
@@ -291,18 +240,13 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
      * Async function that supposedly return the doc that is located at given index.
      */
     getDocAtIndex = async (index: number) => {
-        const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
-        if (!list) {
-            return undefined;
-        }
-        if (index < 0 || index >= list.length) {
-            return undefined;
+        const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
+        if (list && index >= 0 && index < list.length) {
+            this.props.Document.selectedDoc = index;
+            //awaiting async call to finish to get Doc instance
+            return await list[index];
         }
-
-        this.curPresentation.selectedDoc = index;
-        //awaiting async call to finish to get Doc instance
-        const doc = await list[index];
-        return doc;
+        return undefined
     }
 
     /**
@@ -311,23 +255,19 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
      */
     @action
     public RemoveDoc = async (index: number) => {
-        const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+        const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); // don't replace with DocListCast -- we need to modify the document's actual stored list
         if (value) {
-            let removedDoc = await value.splice(index, 1)[0];
-
-            //removing the Presentation Element stored for it
-            this.presElementsMappings.delete(removedDoc);
-
+            //removing the Presentation Element from the document and update mappings
+            this._presElementsMappings.delete(await value.splice(index, 1)[0]);
         }
     }
 
     public removeDocByRef = (doc: Doc) => {
-        let indexOfDoc = this.childrenDocs.indexOf(doc);
-        const value = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
+        let indexOfDoc = this._childrenDocs.indexOf(doc);
+        const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
         if (value) {
             value.splice(indexOfDoc, 1)[0];
         }
-        //this.RemoveDoc(indexOfDoc, true);
         if (indexOfDoc !== - 1) {
             return true;
         }
@@ -339,144 +279,92 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
     @action
     public gotoDocument = async (index: number, fromDoc: number) => {
         Doc.UnBrushAllDocs();
-        const list = FieldValue(Cast(this.curPresentation.data, listSpec(Doc)));
-        if (!list) {
-            return;
-        }
-        if (index < 0 || index >= list.length) {
-            return;
-        }
-        this.curPresentation.selectedDoc = index;
+        const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
+        if (list && index >= 0 && index < list.length) {
+            this.props.Document.selectedDoc = index;
 
-        if (!this.presStatus) {
-            this.presStatus = true;
-            this.startPresentation(index);
-        }
+            if (!this.props.Document.presStatus) {
+                this.props.Document.presStatus = true;
+                this.startPresentation(index);
+            }
 
-        const doc = await list[index];
-        if (this.presStatus) {
-            this.navigateToElement(doc, fromDoc);
-            this.hideIfNotPresented(index);
-            this.showAfterPresented(index);
+            const doc = await list[index];
+            if (this.props.Document.presStatus) {
+                this.navigateToElement(doc, fromDoc);
+                this.hideIfNotPresented(index);
+                this.showAfterPresented(index);
+            }
         }
     }
     //Function that sets the store of the children docs.
     @action
     setChildrenDocs = (docList: Doc[]) => {
-        this.childrenDocs = docList;
+        this._childrenDocs = docList;
     }
 
     //The function that is called to render the play or pause button depending on
     //if presentation is running or not.
     renderPlayPauseButton = () => {
-        if (this.presStatus) {
-            return <button title="Reset Presentation" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
-        } else {
-            return <button title="Start Presentation From Start" className="presentation-button" onClick={this.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
-        }
+        return <button title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} className="presentation-button" onClick={this.startOrResetPres}>
+            <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} />
+        </button>;
     }
 
     //The function that starts or resets presentaton functionally, depending on status flag.
     @action
     startOrResetPres = () => {
-        if (this.presStatus) {
+        if (this.props.Document.presStatus) {
             this.resetPresentation();
         } else {
-            this.presStatus = true;
+            this.props.Document.presStatus = true;
             this.startPresentation(0);
-            const current = NumCast(this.curPresentation.selectedDoc);
-            this.gotoDocument(0, current);
+            this.gotoDocument(0, NumCast(this.props.Document.selectedDoc));
         }
-        this.curPresentation.presStatus = this.presStatus;
     }
 
     //The function that resets the presentation by removing every action done by it. It also
     //stops the presentaton.
     @action
     resetPresentation = () => {
-        this.childrenDocs.forEach((doc: Doc) => {
+        this._childrenDocs.forEach((doc: Doc) => {
             doc.opacity = 1;
             doc.viewScale = 1;
         });
-        this.curPresentation.selectedDoc = 0;
-        this.presStatus = false;
-        this.curPresentation.presStatus = this.presStatus;
-        if (this.childrenDocs.length === 0) {
-            return;
+        this.props.Document.selectedDoc = 0;
+        this.props.Document.presStatus = false;
+        if (this._childrenDocs.length !== 0) {
+            DocumentManager.Instance.zoomIntoScale(this._childrenDocs[0], 1);
         }
-        DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
     }
 
-
     //The function that starts the presentation, also checking if actions should be applied
     //directly at start.
     startPresentation = (startIndex: number) => {
-        this.presElementsMappings.forEach((component: PresentationElement, doc: Doc) => {
+        this._presElementsMappings.forEach((component, doc) => {
             if (component.props.document.hideTillShownButton) {
-                if (this.childrenDocs.indexOf(doc) > startIndex) {
+                if (this._childrenDocs.indexOf(doc) > startIndex) {
                     doc.opacity = 0;
                 }
-
             }
             if (component.props.document.hideAfterButton) {
-                if (this.childrenDocs.indexOf(doc) < startIndex) {
+                if (this._childrenDocs.indexOf(doc) < startIndex) {
                     doc.opacity = 0;
                 }
             }
             if (component.props.document.fadeButton) {
-                if (this.childrenDocs.indexOf(doc) < startIndex) {
+                if (this._childrenDocs.indexOf(doc) < startIndex) {
                     doc.opacity = 0.5;
                 }
             }
-
         });
-
-    }
-
-
-    /**
-     * The function that is called to render either select for presentations, or title inputting.
-     */
-    renderSelectOrPresSelection = () => {
-        if (this.PresTitleInputOpen || this.PresTitleChangeOpen) {
-            return <input ref={(e) => this.titleInputElement = e!} type="text" className="presentationView-title" placeholder="Enter Name!" onKeyDown={this.submitPresentationTitle} />;
-        } else {
-            return (null);
-        }
-    }
-
-    /**
-     * The function that is called on enter press of title input. It gives the
-     * new presentation the title user entered. If nothing is entered, gives a default title.
-     */
-    @action
-    submitPresentationTitle = (e: React.KeyboardEvent) => {
-        if (e.keyCode === 13) {
-            let presTitle = this.titleInputElement!.value;
-            this.titleInputElement!.value = "";
-            if (this.PresTitleChangeOpen) {
-                this.PresTitleChangeOpen = false;
-                this.changePresentationTitle(presTitle);
-            }
-        }
-    }
-    /**
-     * The function that is called to change title of presentation to what user entered.
-     */
-    @undoBatch
-    changePresentationTitle = (newTitle: string) => {
-        if (newTitle === "") {
-            return;
-        }
-        this.curPresentation.title = newTitle;
     }
 
     addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
-        this.presElementsMappings.set(keyDoc, elem);
+        this._presElementsMappings.set(keyDoc, elem);
     }
 
     minimize = undoBatch(action(() => {
-        this.presMode = true;
+        this._presMode = true;
         this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
     }));
 
@@ -485,12 +373,10 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
     }
 
     render() {
-
-        let width = "100%"; //NumCast(this.curPresentation.width)
         return (
-            <div className="presentationView-cont" onPointerEnter={action(() => !this.persistOpacity && (this.opacity = 1))} onContextMenu={this.specificContextMenu}
-                onPointerLeave={action(() => !this.persistOpacity && (this.opacity = 0.4))}
-                style={{ width: width, opacity: this.opacity, }}>
+            <div className="presentationView-cont" onPointerEnter={action(() => !this._persistOpacity && (this._opacity = 1))} onContextMenu={this.specificContextMenu}
+                onPointerLeave={action(() => !this._persistOpacity && (this._opacity = 0.4))}
+                style={{ width: "100%", opacity: this._opacity, }}>
                 <div className="presentation-buttons">
                     <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
                     {this.renderPlayPauseButton()}
@@ -500,28 +386,26 @@ export class PresBox extends React.Component<FieldViewProps> { //FieldViewProps?
                 <input
                     type="checkbox"
                     onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                        this.persistOpacity = e.target.checked;
-                        this.opacity = this.persistOpacity ? 1 : 0.4;
+                        this._persistOpacity = e.target.checked;
+                        this._opacity = this._persistOpacity ? 1 : 0.4;
                     })}
-                    checked={this.persistOpacity}
+                    checked={this._persistOpacity}
                     style={{ position: "absolute", bottom: 5, left: 5 }}
-                    onPointerEnter={action(() => this.labelOpacity = 1)}
-                    onPointerLeave={action(() => this.labelOpacity = 0)}
+                    onPointerEnter={action(() => this._labelOpacity = 1)}
+                    onPointerLeave={action(() => this._labelOpacity = 0)}
                 />
-                <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this.labelOpacity, transition: "0.7s opacity ease" }}>opacity {this.persistOpacity ? "persistent" : "on focus"}</p>
+                <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this._labelOpacity, transition: "0.7s opacity ease" }}>opacity {this._persistOpacity ? "persistent" : "on focus"}</p>
                 <PresentationViewList
-                    mainDocument={this.curPresentation}
+                    mainDocument={this.props.Document}
                     deleteDocument={this.RemoveDoc}
                     gotoDocument={this.gotoDocument}
-                    PresElementsMappings={this.presElementsMappings}
+                    PresElementsMappings={this._presElementsMappings}
                     setChildrenDocs={this.setChildrenDocs}
-                    presStatus={this.presStatus}
+                    presStatus={BoolCast(this.props.Document.presStatus)}
                     removeDocByRef={this.removeDocByRef}
-                    clearElemMap={() => this.presElementsMappings.clear()}
+                    clearElemMap={() => this._presElementsMappings.clear()}
                 />
             </div>
         );
     }
-
-
 }
\ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx
index 0dd2b7731..e4123a1fe 100644
--- a/src/client/views/presentationview/PresentationModeMenu.tsx
+++ b/src/client/views/presentationview/PresentationModeMenu.tsx
@@ -98,8 +98,4 @@ export default class PresModeMenu extends React.Component<PresModeMenuProps> {
             </div >
         );
     }
-
-
-
-
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From d7f515f32de780884774e7bbdcc1cfe78733af45 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 30 Sep 2019 14:01:59 -0400
Subject: a bunch of changes to presentation view ... more coming.

---
 src/client/documents/DocumentTypes.ts              |   2 +-
 src/client/documents/Documents.ts                  |   5 +-
 src/client/views/MainView.tsx                      |   1 -
 src/client/views/nodes/DocumentContentsView.tsx    |  31 ++--
 src/client/views/nodes/PresBox.tsx                 | 165 ++++++++-------------
 .../views/presentationview/PresentationElement.tsx |  19 +--
 .../views/presentationview/PresentationList.tsx    |   9 --
 .../presentationview/PresentationModeMenu.scss     |  30 ----
 .../presentationview/PresentationModeMenu.tsx      | 101 -------------
 src/new_fields/Doc.ts                              |  22 ++-
 10 files changed, 105 insertions(+), 280 deletions(-)
 delete mode 100644 src/client/views/presentationview/PresentationModeMenu.scss
 delete mode 100644 src/client/views/presentationview/PresentationModeMenu.tsx

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

diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 381981e1b..8ec6e9642 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -19,5 +19,5 @@ export enum DocumentType {
     YOUTUBE = "youtube",
     DRAGBOX = "dragbox",
     PRES = "presentation",
-    LINKFOLLOW = "linkfollow",
+    LINKFOLLOW = "linkfollow"
 }
\ No newline at end of file
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d1ec2ac39..01c36182f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -46,8 +46,6 @@ import { ComputedField } from "../../new_fields/ScriptField";
 import { ProxyField } from "../../new_fields/Proxy";
 import { DocumentType } from "./DocumentTypes";
 import { LinkFollowBox } from "../views/linking/LinkFollowBox";
-//import { PresBox } from "../views/nodes/PresBox";
-//import { PresField } from "../../new_fields/PresField";
 var requestImageSize = require('../util/request-image-size');
 var path = require('path');
 
@@ -176,7 +174,7 @@ export namespace Docs {
             }],
             [DocumentType.LINKFOLLOW, {
                 layout: { view: LinkFollowBox }
-            }]
+            }],
         ]);
 
         // All document prototypes are initialized with at least these values
@@ -459,7 +457,6 @@ export namespace Docs {
         export function LinkFollowBoxDocument(options?: DocumentOptions) {
             return InstanceFromProto(Prototypes.get(DocumentType.LINKFOLLOW), undefined, { ...(options || {}) });
         }
-
         export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, viewType: CollectionViewType.Docking, dockingConfig: config }, id);
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index dadff21a7..35d527c91 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -634,7 +634,6 @@ export class MainView extends React.Component {
                 <SharingManager />
                 <DocumentDecorations />
                 {this.mainContent}
-                {PresBox.miniPresentation}
                 <PreviewCursor />
                 <ContextMenu />
                 {this.nodesMenu()}
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3c3cc0d91..d035cbe18 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -1,37 +1,34 @@
-import { computed, trace } from "mobx";
+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 { OmitKeys, Without } from "../../../Utils";
+import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
+import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
 import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
 import { CollectionPDFView } from "../collections/CollectionPDFView";
 import { CollectionSchemaView } from "../collections/CollectionSchemaView";
 import { CollectionVideoView } from "../collections/CollectionVideoView";
 import { CollectionView } from "../collections/CollectionView";
+import { LinkFollowBox } from "../linking/LinkFollowBox";
+import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
 import { AudioBox } from "./AudioBox";
+import { ButtonBox } from "./ButtonBox";
 import { DocumentViewProps } from "./DocumentView";
 import "./DocumentView.scss";
-import { FormattedTextBox } from "./FormattedTextBox";
-import { ImageBox } from "./ImageBox";
 import { DragBox } from "./DragBox";
-import { ButtonBox } from "./ButtonBox";
-import { PresBox } from "./PresBox";
-import { LinkFollowBox } from "../linking/LinkFollowBox";
+import { FieldView, FieldViewProps } from "./FieldView";
+import { FormattedTextBox } from "./FormattedTextBox";
 import { IconBox } from "./IconBox";
+import { ImageBox } from "./ImageBox";
 import { KeyValueBox } from "./KeyValueBox";
 import { PDFBox } from "./PDFBox";
+import { PresBox } from "./PresBox";
 import { VideoBox } from "./VideoBox";
-import { FieldView } from "./FieldView";
 import { WebBox } from "./WebBox";
-import { YoutubeBox } from "./../../apis/youtube/YoutubeBox";
-import { HistogramBox } from "../../northstar/dash-nodes/HistogramBox";
 import React = require("react");
-import { FieldViewProps } from "./FieldView";
-import { Without, OmitKeys } from "../../../Utils";
-import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
-import { List } from "../../../new_fields/List";
-import { Doc } from "../../../new_fields/Doc";
-import DirectoryImportBox from "../../util/Import & Export/DirectoryImportBox";
-import { ScriptField } from "../../../new_fields/ScriptField";
-import { fromPromise } from "mobx-utils";
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 type BindingProps = Without<FieldViewProps, 'fieldKey'>;
diff --git a/src/client/views/nodes/PresBox.tsx b/src/client/views/nodes/PresBox.tsx
index d246da87a..e2dac4fd3 100644
--- a/src/client/views/nodes/PresBox.tsx
+++ b/src/client/views/nodes/PresBox.tsx
@@ -2,20 +2,19 @@ import React = require("react");
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faArrowLeft, faArrowRight, faEdit, faMinus, faPlay, faPlus, faStop, faTimes } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, observable, runInAction, computed } from "mobx";
+import { action, observable, runInAction } from "mobx";
 import { observer } from "mobx-react";
-import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
 import { listSpec } from "../../../new_fields/Schema";
 import { BoolCast, Cast, FieldValue, NumCast } from "../../../new_fields/Types";
+import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { DocumentManager } from "../../util/DocumentManager";
 import { undoBatch } from "../../util/UndoManager";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
 import { ContextMenu } from "../ContextMenu";
-import PresentationElement from "../presentationview/PresentationElement";
 import PresentationViewList from "../presentationview/PresentationList";
 import "../presentationview/PresentationView.scss";
 import { FieldView, FieldViewProps } from './FieldView';
-import PresModeMenu from "../presentationview/PresentationModeMenu";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
 
 library.add(faArrowLeft);
 library.add(faArrowRight);
@@ -31,31 +30,13 @@ export class PresBox extends React.Component<FieldViewProps> {
     public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(PresBox, fieldKey); }
 
     public static Instance: PresBox;
-    //mapping from docs to their rendered component
-    @observable _presElementsMappings: Map<Doc, PresentationElement> = new Map();
     //variable that holds all the docs in the presentation
     @observable _childrenDocs: Doc[] = [];
 
-    @observable _opacity = 1;
-    @observable _persistOpacity = true;
-    @observable _labelOpacity = 0;
     // whether presentation view has been minimized
     @observable _presMode = false;
     @observable public static CurrentPresentation: PresBox;
 
-    @computed static get miniPresentation() {
-        let next = () => PresBox.CurrentPresentation.next();
-        let back = () => PresBox.CurrentPresentation.back();
-        let startOrResetPres = () => PresBox.CurrentPresentation.startOrResetPres();
-        let closePresMode = action(() => {
-            PresBox.CurrentPresentation._presMode = false;
-            CollectionDockingView.AddRightSplit(PresBox.CurrentPresentation.props.Document, undefined);
-        });
-        return !PresBox.CurrentPresentation || !PresBox.CurrentPresentation._presMode ? (null) :
-            <PresModeMenu next={next} back={back} presStatus={BoolCast(PresBox.CurrentPresentation.props.Document.presStatus)}
-                startOrResetPres={startOrResetPres} closePresMode={closePresMode} />;
-    }
-
     //initilize class variables
     constructor(props: FieldViewProps) {
         super(props);
@@ -71,7 +52,7 @@ export class PresBox extends React.Component<FieldViewProps> {
             let nextSelected = current + 1;
 
             for (; nextSelected < presDocs.length - 1; nextSelected++) {
-                if (!this._presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+                if (!presDocs[nextSelected + 1].groupButton) {
                     break;
                 }
             }
@@ -89,18 +70,16 @@ export class PresBox extends React.Component<FieldViewProps> {
             let prevSelected = current;
             let zoomOut: boolean = false;
 
-            //checking if this presentation id is mapped to a group, if so chosing the first element in group
-            let presDocs = DocListCast(this.props.Document.data);
+            let presDocs = await DocListCastAsync(this.props.Document[this.props.fieldKey]);
             let currentsArray: Doc[] = [];
-            for (; prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
+            for (; presDocs && prevSelected > 0 && presDocs[prevSelected].groupButton; prevSelected--) {
                 currentsArray.push(presDocs[prevSelected]);
             }
             prevSelected = Math.max(0, prevSelected - 1);
 
             //checking if any of the group members had used zooming in
             currentsArray.forEach((doc: Doc) => {
-                //let presElem: PresentationElement | undefined = this.presElementsMappings.get(doc);
-                if (this._presElementsMappings.get(doc)!.props.document.showButton) {
+                if (doc.showButton) {
                     zoomOut = true;
                     return;
                 }
@@ -109,7 +88,7 @@ export class PresBox extends React.Component<FieldViewProps> {
             // if a group set that flag to zero or a single element
             //If so making sure to zoom out, which goes back to state before zooming action
             if (current > 0) {
-                if (zoomOut || this._presElementsMappings.get(docAtCurrent)!.showButton) {
+                if (zoomOut || docAtCurrent.showButton) {
                     let prevScale = NumCast(this._childrenDocs[prevSelected].viewScale, null);
                     let curScale = DocumentManager.Instance.getScaleOfDocView(this._childrenDocs[current]);
                     if (prevScale !== undefined && prevScale !== curScale) {
@@ -127,20 +106,20 @@ export class PresBox extends React.Component<FieldViewProps> {
      * Hide Until Presented, Hide After Presented, Fade After Presented
      */
     showAfterPresented = (index: number) => {
-        this._presElementsMappings.forEach((presElem, doc) => {
+        this._childrenDocs.forEach((doc, ind) => {
             //the order of cases is aligned based on priority
-            if (presElem.props.document.hideTillShownButton) {
-                if (this._childrenDocs.indexOf(doc) <= index) {
+            if (doc.hideTillShownButton) {
+                if (ind <= index) {
                     doc.opacity = 1;
                 }
             }
-            if (presElem.props.document.hideAfterButton) {
-                if (this._childrenDocs.indexOf(doc) < index) {
+            if (doc.hideAfterButton) {
+                if (ind < index) {
                     doc.opacity = 0;
                 }
             }
-            if (presElem.props.document.fadeButton) {
-                if (this._childrenDocs.indexOf(doc) < index) {
+            if (doc.fadeButton) {
+                if (ind < index) {
                     doc.opacity = 0.5;
                 }
             }
@@ -153,21 +132,21 @@ export class PresBox extends React.Component<FieldViewProps> {
      * Hide Until Presented, Hide After Presented, Fade After Presented
      */
     hideIfNotPresented = (index: number) => {
-        this._presElementsMappings.forEach((presElem, key) => {
+        this._childrenDocs.forEach((key, ind) => {
             //the order of cases is aligned based on priority
 
-            if (presElem.props.document.hideAfterButton) {
-                if (this._childrenDocs.indexOf(key) >= index) {
+            if (key.hideAfterButton) {
+                if (ind >= index) {
                     key.opacity = 1;
                 }
             }
-            if (presElem.props.document.fadeButton) {
-                if (this._childrenDocs.indexOf(key) >= index) {
+            if (key.fadeButton) {
+                if (ind >= index) {
                     key.opacity = 1;
                 }
             }
-            if (presElem.props.document.hideTillShownButton) {
-                if (this._childrenDocs.indexOf(key) > index) {
+            if (key.hideTillShownButton) {
+                if (ind > index) {
                     key.opacity = 0;
                 }
             }
@@ -187,18 +166,18 @@ export class PresBox extends React.Component<FieldViewProps> {
         let nextSelected = presDocs.indexOf(curDoc);
         let currentDocGroups: Doc[] = [];
         for (; nextSelected < presDocs.length - 1; nextSelected++) {
-            if (!this._presElementsMappings.get(presDocs[nextSelected + 1])!.props.document.groupButton) {
+            if (!presDocs[nextSelected + 1].groupButton) {
                 break;
             }
             currentDocGroups.push(presDocs[nextSelected]);
         }
 
         currentDocGroups.forEach((doc: Doc, index: number) => {
-            if (this._presElementsMappings.get(doc)!.navButton) {
+            if (doc.navButton) {
                 docToJump = doc;
                 willZoom = false;
             }
-            if (this._presElementsMappings.get(doc)!.showButton) {
+            if (doc.showButton) {
                 docToJump = doc;
                 willZoom = true;
             }
@@ -207,9 +186,9 @@ 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
-            if (this._presElementsMappings.get(curDoc)!.navButton) {
+            if (curDoc.navButton) {
                 DocumentManager.Instance.jumpToDocument(curDoc, false);
-            } else if (this._presElementsMappings.get(curDoc)!.showButton) {
+            } else if (curDoc.showButton) {
                 let curScale = DocumentManager.Instance.getScaleOfDocView(this._childrenDocs[fromDoc]);
                 //awaiting jump so that new scale can be found, since jumping is async
                 await DocumentManager.Instance.jumpToDocument(curDoc, true);
@@ -250,15 +229,13 @@ export class PresBox extends React.Component<FieldViewProps> {
     }
 
     /**
-     * The function that removes a doc from a presentation. It also makes sure to
-     * do necessary updates to backUps and mappings stored locally.
+     * The function that removes a doc from a presentation. 
      */
     @action
     public RemoveDoc = async (index: number) => {
         const value = FieldValue(Cast(this.props.Document.data, listSpec(Doc))); // don't replace with DocListCast -- we need to modify the document's actual stored list
         if (value) {
-            //removing the Presentation Element from the document and update mappings
-            this._presElementsMappings.delete(await value.splice(index, 1)[0]);
+            value.splice(index, 1);
         }
     }
 
@@ -302,14 +279,6 @@ export class PresBox extends React.Component<FieldViewProps> {
         this._childrenDocs = docList;
     }
 
-    //The function that is called to render the play or pause button depending on
-    //if presentation is running or not.
-    renderPlayPauseButton = () => {
-        return <button title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} className="presentation-button" onClick={this.startOrResetPres}>
-            <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} />
-        </button>;
-    }
-
     //The function that starts or resets presentaton functionally, depending on status flag.
     @action
     startOrResetPres = () => {
@@ -340,18 +309,18 @@ export class PresBox extends React.Component<FieldViewProps> {
     //The function that starts the presentation, also checking if actions should be applied
     //directly at start.
     startPresentation = (startIndex: number) => {
-        this._presElementsMappings.forEach((component, doc) => {
-            if (component.props.document.hideTillShownButton) {
+        this._childrenDocs.map(doc => {
+            if (doc.hideTillShownButton) {
                 if (this._childrenDocs.indexOf(doc) > startIndex) {
                     doc.opacity = 0;
                 }
             }
-            if (component.props.document.hideAfterButton) {
+            if (doc.hideAfterButton) {
                 if (this._childrenDocs.indexOf(doc) < startIndex) {
                     doc.opacity = 0;
                 }
             }
-            if (component.props.document.fadeButton) {
+            if (doc.fadeButton) {
                 if (this._childrenDocs.indexOf(doc) < startIndex) {
                     doc.opacity = 0.5;
                 }
@@ -359,13 +328,18 @@ export class PresBox extends React.Component<FieldViewProps> {
         });
     }
 
-    addPressElem = (keyDoc: Doc, elem: PresentationElement) => {
-        this._presElementsMappings.set(keyDoc, elem);
-    }
-
-    minimize = undoBatch(action(() => {
-        this._presMode = true;
-        this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
+    toggleMinimize = undoBatch(action((e: React.PointerEvent) => {
+        if (this.props.Document.minimizedView) {
+            this.props.Document.minimizedView = false;
+            Doc.RemoveDocFromList((CurrentUserUtils.UserDocument.overlays as Doc), "data", this.props.Document);
+            CollectionDockingView.AddRightSplit(this.props.Document, this.props.DataDoc);
+        } else {
+            this.props.Document.minimizedView = true;
+            this.props.Document.x = e.clientX + 25;
+            this.props.Document.y = e.clientY - 25;
+            this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "close");
+            Doc.AddDocToList((CurrentUserUtils.UserDocument.overlays as Doc), "data", this.props.Document);
+        }
     }));
 
     specificContextMenu = (e: React.MouseEvent): void => {
@@ -374,37 +348,24 @@ export class PresBox extends React.Component<FieldViewProps> {
 
     render() {
         return (
-            <div className="presentationView-cont" onPointerEnter={action(() => !this._persistOpacity && (this._opacity = 1))} onContextMenu={this.specificContextMenu}
-                onPointerLeave={action(() => !this._persistOpacity && (this._opacity = 0.4))}
-                style={{ width: "100%", opacity: this._opacity, }}>
-                <div className="presentation-buttons">
-                    <button title="Back" className="presentation-button" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
-                    {this.renderPlayPauseButton()}
-                    <button title="Next" className="presentation-button" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
-                    <button title="Minimize" className="presentation-button" onClick={this.minimize}><FontAwesomeIcon icon={"eye"} /></button>
+            <div className="presentationView-cont" onContextMenu={this.specificContextMenu} style={{ width: "100%", minWidth: "200px", height: "100%", minHeight: "50px" }}>
+                <div className="presentation-buttons" style={{ width: "100%" }}>
+                    <button className="presentation-button" title="Back" onClick={this.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
+                    <button className="presentation-button" title={"Reset Presentation" + this.props.Document.presStatus ? "" : " From Start"} onClick={this.startOrResetPres}>
+                        <FontAwesomeIcon icon={this.props.Document.presStatus ? "stop" : "play"} />
+                    </button>
+                    <button className="presentation-button" title="Next" onClick={this.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
+                    <button className="presentation-button" title={this.props.Document.minimizedView ? "Expand" : "Minimize"} onClick={this.toggleMinimize}><FontAwesomeIcon icon={"eye"} /></button>
                 </div>
-                <input
-                    type="checkbox"
-                    onChange={action((e: React.ChangeEvent<HTMLInputElement>) => {
-                        this._persistOpacity = e.target.checked;
-                        this._opacity = this._persistOpacity ? 1 : 0.4;
-                    })}
-                    checked={this._persistOpacity}
-                    style={{ position: "absolute", bottom: 5, left: 5 }}
-                    onPointerEnter={action(() => this._labelOpacity = 1)}
-                    onPointerLeave={action(() => this._labelOpacity = 0)}
-                />
-                <p style={{ position: "absolute", bottom: 1, left: 22, opacity: this._labelOpacity, transition: "0.7s opacity ease" }}>opacity {this._persistOpacity ? "persistent" : "on focus"}</p>
-                <PresentationViewList
-                    mainDocument={this.props.Document}
-                    deleteDocument={this.RemoveDoc}
-                    gotoDocument={this.gotoDocument}
-                    PresElementsMappings={this._presElementsMappings}
-                    setChildrenDocs={this.setChildrenDocs}
-                    presStatus={BoolCast(this.props.Document.presStatus)}
-                    removeDocByRef={this.removeDocByRef}
-                    clearElemMap={() => this._presElementsMappings.clear()}
-                />
+                {this.props.Document.minimizedView ? (null) :
+                    <PresentationViewList
+                        mainDocument={this.props.Document}
+                        deleteDocument={this.RemoveDoc}
+                        gotoDocument={this.gotoDocument}
+                        setChildrenDocs={this.setChildrenDocs}
+                        presStatus={BoolCast(this.props.Document.presStatus)}
+                        removeDocByRef={this.removeDocByRef}
+                    />}
             </div>
         );
     }
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 126d62c52..6e8c28d34 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
-import { faArrowRight, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
+import { faArrowDown, faArrowUp, faFile as fileSolid, faFileDownload, faLocationArrow, faSearch } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { action, computed } from "mobx";
 import { observer } from "mobx-react";
@@ -22,7 +22,7 @@ library.add(fileSolid);
 library.add(faLocationArrow);
 library.add(fileRegular as any);
 library.add(faSearch);
-library.add(faArrowRight);
+library.add(faArrowDown);
 
 interface PresentationElementProps {
     mainDocument: Doc;
@@ -33,7 +33,6 @@ interface PresentationElementProps {
     allListElements: Doc[];
     presStatus: boolean;
     removeDocByRef(doc: Doc): boolean;
-    PresElementsMappings: Map<Doc, PresentationElement>;
 }
 
 /**
@@ -59,14 +58,14 @@ export default class PresentationElement extends React.Component<PresentationEle
     @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 openRightButton() { return BoolCast(this.props.document.openRightButton); }
+    @computed get expandInlineButton() { return BoolCast(this.props.document.expandInlineButton); }
     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; }
-    set openRightButton(val: boolean) { this.props.document.openRightButton = val; }
+    set expandInlineButton(val: boolean) { this.props.document.expandInlineButton = val; }
 
     //Lifecycle function that makes sure that button BackUp is received when mounted.
     async componentDidMount() {
@@ -190,14 +189,12 @@ export default class PresentationElement extends React.Component<PresentationEle
     }
 
     /**
-     * Function that opens up the option to open a element on right when navigated,
-     * instead of openening it as tab as default.
+     * Function that expands the target inline
      */
     @action
-    onRightTabClick = (e: React.MouseEvent) => {
+    onExpandTabClick = (e: React.MouseEvent) => {
         e.stopPropagation();
-
-        this.openRightButton = !this.openRightButton;
+        this.embedInline = !this.embedInline
     }
 
     /**
@@ -416,7 +413,7 @@ export default class PresentationElement extends React.Component<PresentationEle
                 <button title="Fade Document After Presented" className={this.fadeButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
                 <button title="Hide Document After Presented" className={this.hideAfterButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onHideDocumentAfterPresentedClick}><FontAwesomeIcon icon={faFileDownload} /></button>
                 <button title="Group With Up" className={this.groupButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onGroupClick}> <FontAwesomeIcon icon={"arrow-up"} /> </button>
-                <button title="Open Right" className={this.openRightButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onRightTabClick}><FontAwesomeIcon icon={"arrow-right"} /></button>
+                <button title="Expand Inline" className={this.expandInlineButton ? "presentation-interaction-selected" : "presentation-interaction"} onPointerDown={(e) => e.stopPropagation()} onClick={this.onExpandTabClick}><FontAwesomeIcon icon={"arrow-down"} /></button>
 
                 <br />
                 {this.renderEmbeddedInline()}
diff --git a/src/client/views/presentationview/PresentationList.tsx b/src/client/views/presentationview/PresentationList.tsx
index da48a856a..483461e5a 100644
--- a/src/client/views/presentationview/PresentationList.tsx
+++ b/src/client/views/presentationview/PresentationList.tsx
@@ -12,11 +12,9 @@ interface PresListProps {
     mainDocument: Doc;
     deleteDocument(index: number): void;
     gotoDocument(index: number, fromDoc: number): Promise<void>;
-    PresElementsMappings: Map<Doc, PresentationElement>;
     setChildrenDocs: (docList: Doc[]) => void;
     presStatus: boolean;
     removeDocByRef(doc: Doc): boolean;
-    clearElemMap(): void;
 }
 
 
@@ -45,16 +43,10 @@ export default class PresentationViewList extends React.Component<PresListProps>
         const children = DocListCast(this.props.mainDocument.data);
         this.initializeScaleViews(children);
         this.props.setChildrenDocs(children);
-        this.props.clearElemMap();
         return (
             <div className="presentationView-listCont" >
                 {children.map((doc: Doc, index: number) =>
                     <PresentationElement
-                        ref={(e) => {
-                            if (e && e !== null) {
-                                this.props.PresElementsMappings.set(doc, e);
-                            }
-                        }}
                         key={doc[Id]}
                         mainDocument={this.props.mainDocument}
                         document={doc}
@@ -64,7 +56,6 @@ export default class PresentationViewList extends React.Component<PresListProps>
                         allListElements={children}
                         presStatus={this.props.presStatus}
                         removeDocByRef={this.props.removeDocByRef}
-                        PresElementsMappings={this.props.PresElementsMappings}
                     />
                 )}
             </div>
diff --git a/src/client/views/presentationview/PresentationModeMenu.scss b/src/client/views/presentationview/PresentationModeMenu.scss
deleted file mode 100644
index 336f43d20..000000000
--- a/src/client/views/presentationview/PresentationModeMenu.scss
+++ /dev/null
@@ -1,30 +0,0 @@
-.presMenu-cont {
-    position: fixed;
-    z-index: 10000;
-    height: 35px;
-    background: #323232;
-    box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
-    border-radius: 0px 6px 6px 6px;
-    overflow: hidden;
-    display: flex;
-
-    .presMenu-button {
-        background-color: transparent;
-        width: 35px;
-        height: 35px;
-    }
-
-    .presMenu-button:hover {
-        background-color: #121212;
-    }
-
-    .presMenu-dragger {
-        height: 100%;
-        transition: width .2s;
-        background-image: url("https://logodix.com/logo/1020374.png");
-        background-size: 90% 100%;
-        background-repeat: no-repeat;
-        background-position: left center;
-    }
-
-}
\ No newline at end of file
diff --git a/src/client/views/presentationview/PresentationModeMenu.tsx b/src/client/views/presentationview/PresentationModeMenu.tsx
deleted file mode 100644
index e4123a1fe..000000000
--- a/src/client/views/presentationview/PresentationModeMenu.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import React = require("react");
-import { observable, action, runInAction } from "mobx";
-import "./PresentationModeMenu.scss";
-import { observer } from "mobx-react";
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-
-
-export interface PresModeMenuProps {
-    next: () => void;
-    back: () => void;
-    presStatus: boolean;
-    startOrResetPres: () => void;
-    closePresMode: () => void;
-}
-
-/**
- * This class is responsible for modeling of the Presentation Mode Menu. The menu allows
- * user to navigate through presentation elements, and start/stop the presentation.
- */
-@observer
-export default class PresModeMenu extends React.Component<PresModeMenuProps> {
-
-    @observable private _top: number = 20;
-    @observable private _left: number = window.innerWidth - 160;
-    @observable private _opacity: number = 1;
-    @observable private _transition: string = "opacity 0.5s";
-    @observable private _transitionDelay: string = "";
-    private _offsetY: number = 0;
-    private _offsetX: number = 0;
-
-
-    private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
-
-    /**
-     * The function that changes the coordinates of the menu, depending on the
-     * movement of the mouse when it's being dragged.
-     */
-    @action
-    dragging = (e: PointerEvent) => {
-        this._left = e.pageX - this._offsetX;
-        this._top = e.pageY - this._offsetY;
-
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    /**
-     * The function that removes the event listeners that are responsible for
-     * dragging of the menu.
-     */
-    dragEnd = (e: PointerEvent) => {
-        document.removeEventListener("pointermove", this.dragging);
-        document.removeEventListener("pointerup", this.dragEnd);
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    /**
-     * The function that starts the dragging of the presentation mode menu. When
-     * the lines on further right are clicked on.
-     */
-    dragStart = (e: React.PointerEvent) => {
-        document.removeEventListener("pointermove", this.dragging);
-        document.addEventListener("pointermove", this.dragging);
-        document.removeEventListener("pointerup", this.dragEnd);
-        document.addEventListener("pointerup", this.dragEnd);
-
-        this._offsetX = this._mainCont.current!.getBoundingClientRect().width - e.nativeEvent.offsetX;
-        this._offsetY = e.nativeEvent.offsetY;
-
-        e.stopPropagation();
-        e.preventDefault();
-    }
-
-    /**
-     * The function that is responsible for rendering the play or pause button, depending on the
-     * status of the presentation.
-     */
-    renderPlayPauseButton = () => {
-        if (this.props.presStatus) {
-            return <button title="Reset Presentation" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="stop" /></button>;
-        } else {
-            return <button title="Start Presentation From Start" className="presMenu-button" onClick={this.props.startOrResetPres}><FontAwesomeIcon icon="play" /></button>;
-        }
-    }
-
-    render() {
-        return (
-            <div className="presMenu-cont" ref={this._mainCont}
-                style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
-                <button title="Back" className="presMenu-button" onClick={this.props.back}><FontAwesomeIcon icon={"arrow-left"} /></button>
-                {this.renderPlayPauseButton()}
-                <button title="Next" className="presMenu-button" onClick={this.props.next}><FontAwesomeIcon icon={"arrow-right"} /></button>
-                <button className="presMenu-button" title="Close Presentation Menu" onClick={this.props.closePresMode}>
-                    <FontAwesomeIcon icon="times" size="lg" />
-                </button>
-                <div className="presMenu-dragger" onPointerDown={this.dragStart} style={{ width: "20px" }} />
-            </div >
-        );
-    }
-}
\ No newline at end of file
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 79b73aba8..4a03fed08 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -337,11 +337,25 @@ export namespace Doc {
     export function IndexOf(toFind: Doc, list: Doc[]) {
         return list.findIndex(doc => doc === toFind || Doc.AreProtosEqual(doc, toFind));
     }
-    export function AddDocToList(target: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
-        if (target[key] === undefined) {
-            Doc.GetProto(target)[key] = new List<Doc>();
+    export function RemoveDocFromList(listDoc: Doc, key: string, doc: Doc) {
+        if (listDoc[key] === undefined) {
+            Doc.GetProto(listDoc)[key] = new List<Doc>();
         }
-        let list = Cast(target[key], listSpec(Doc));
+        let list = Cast(listDoc[key], listSpec(Doc));
+        if (list) {
+            let ind = list.indexOf(doc);
+            if (ind !== -1) {
+                list.splice(ind, 1);
+                return true;
+            }
+        }
+        return false;
+    }
+    export function AddDocToList(listDoc: Doc, key: string, doc: Doc, relativeTo?: Doc, before?: boolean, first?: boolean, allowDuplicates?: boolean, reversed?: boolean) {
+        if (listDoc[key] === undefined) {
+            Doc.GetProto(listDoc)[key] = new List<Doc>();
+        }
+        let list = Cast(listDoc[key], listSpec(Doc));
         if (list) {
             if (allowDuplicates !== true) {
                 let pind = list.reduce((l, d, i) => d instanceof Doc && d[Id] === doc[Id] ? i : l, -1);
-- 
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/MainView.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 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/MainView.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

