From 464096297d34bb824ed665aa50dfb58f268c1d12 Mon Sep 17 00:00:00 2001
From: madelinegr <mgriswold99@gmail.com>
Date: Tue, 4 Jun 2019 20:01:59 -0400
Subject: BUttons in Presentation CSSed and grouping up insertion done, removal
 almost done

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

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7750b9173..984cac9b9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -23,7 +23,7 @@ import { CollectionVideoView } from "../collections/CollectionVideoView";
 import { CollectionView } from "../collections/CollectionView";
 import { ContextMenu } from "../ContextMenu";
 import { DocComponent } from "../DocComponent";
-import { PresentationView } from "../PresentationView";
+import { PresentationView } from "../presentationview/PresentationView";
 import { Template } from "./../Templates";
 import { DocumentContentsView } from "./DocumentContentsView";
 import "./DocumentView.scss";
-- 
cgit v1.2.3-70-g09d2


From a54e3b99c2c6d4c39cbdd935978b5e78015956ab Mon Sep 17 00:00:00 2001
From: madelinegr <mgriswold99@gmail.com>
Date: Wed, 5 Jun 2019 17:29:43 -0400
Subject: Enums for buttons

---
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 .../views/presentationview/PresentationElement.tsx | 45 +++++++++++++++++-----
 .../views/presentationview/PresentationView.tsx    |  5 +--
 3 files changed, 40 insertions(+), 14 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 984cac9b9..449fa76a7 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -83,6 +83,7 @@ const schema = createSchema({
     nativeWidth: "number",
     nativeHeight: "number",
     backgroundColor: "string",
+    opacity: "number"
 });
 
 export const positionSchema = createSchema({
@@ -464,7 +465,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     background: this.Document.backgroundColor || "",
                     width: nativeWidth,
                     height: nativeHeight,
-                    transform: `scale(${scaling}, ${scaling})`
+                    transform: `scale(${scaling}, ${scaling})`,
+                    opacity: NumCast(this.props.Document.opacity, 1)
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
 
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index 08a663150..48e740c4b 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -19,6 +19,16 @@ interface PresentationElementProps {
 
 }
 
+enum buttonIndex {
+    Show = 0,
+    Navigate = 1,
+    HideTillPressed = 2,
+    FadeAfter = 3,
+    HideAfter = 4,
+    Group = 5,
+
+}
+
 @observer
 export default class PresentationElement extends React.Component<PresentationElementProps> {
 
@@ -84,10 +94,10 @@ export default class PresentationElement extends React.Component<PresentationEle
 
     @action
     changeGroupStatus = () => {
-        if (this.selectedButtons[5]) {
-            this.selectedButtons[5] = false;
+        if (this.selectedButtons[buttonIndex.Group]) {
+            this.selectedButtons[buttonIndex.Group] = false;
         } else {
-            this.selectedButtons[5] = true;
+            this.selectedButtons[buttonIndex.Group] = true;
         }
     }
 
@@ -95,6 +105,21 @@ export default class PresentationElement extends React.Component<PresentationEle
         this.props.groupedMembers.forEach((doc: Doc[], index: number) => console.log("Index: ", index, " size: ", doc.length));
     }
 
+    @action
+    onHideDocumentUntilPressClick = (e: React.MouseEvent) => {
+        e.stopPropagation();
+        if (this.selectedButtons[buttonIndex.HideTillPressed]) {
+            this.selectedButtons[buttonIndex.HideTillPressed] = false;
+
+        } else {
+            this.selectedButtons[buttonIndex.HideTillPressed] = true;
+        }
+    }
+
+    hideDocumentIfNotPressed = () => {
+        this.props.allListElements.forEach((doc: Doc) => doc.opacity = 0);
+    }
+
 
 
     render() {
@@ -125,15 +150,15 @@ export default class PresentationElement extends React.Component<PresentationEle
                 </strong>
                 <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
                 <br></br>
-                <button className={this.selectedButtons[0] ? "presentation-interaction-selected" : "presentation-interaction"}>A</button>
-                <button className={this.selectedButtons[1] ? "presentation-interaction-selected" : "presentation-interaction"}>B</button>
-                <button className={this.selectedButtons[2] ? "presentation-interaction-selected" : "presentation-interaction"}>C</button>
-                <button className={this.selectedButtons[3] ? "presentation-interaction-selected" : "presentation-interaction"}>D</button>
-                <button className={this.selectedButtons[4] ? "presentation-interaction-selected" : "presentation-interaction"}>E</button>
-                <button className={this.selectedButtons[5] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => {
+                <button className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"}>A</button>
+                <button className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"}>B</button>
+                <button className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => { this.onHideDocumentUntilPressClick(e); this.hideDocumentIfNotPressed(); }}>C</button>
+                <button className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"}>D</button>
+                <button className={this.selectedButtons[buttonIndex.HideAfter] ? "presentation-interaction-selected" : "presentation-interaction"}>E</button>
+                <button className={this.selectedButtons[buttonIndex.Group] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={(e) => {
                     e.stopPropagation();
                     this.changeGroupStatus();
-                    this.onGroupClickRec(p.document, p.index, this.selectedButtons[5]);
+                    this.onGroupClickRec(p.document, p.index, this.selectedButtons[buttonIndex.Group]);
                     this.printGroupSizes();
                 }}>F</button>
 
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
index 9baf75ad3..b41149099 100644
--- a/src/client/views/presentationview/PresentationView.tsx
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -55,7 +55,7 @@ class PresentationViewList extends React.Component<PresListProps> {
         console.log("Starting len: ", this.props.groupedMembers.length);
         docList.forEach((doc: Doc, index: number) => {
             if (this.props.groupedMembers.length < index + 2) {
-                this.props.groupedMembers[index] = new Array();
+                this.props.groupedMembers[index] = [];
                 this.props.groupedMembers[index].push(docList[index]);
 
             }
@@ -123,8 +123,7 @@ class PresentationViewList extends React.Component<PresListProps> {
 export class PresentationView extends React.Component<PresViewProps>  {
     public static Instance: PresentationView;
 
-    @observable groupedMembers: [Doc[]] = [[]];
-
+    @observable groupedMembers: Doc[][] = [];
 
     //observable means render is re-called every time variable is changed
     @observable
-- 
cgit v1.2.3-70-g09d2


From 972f76a34e3c1a1aa5f0be59639fbd5763c9c16f Mon Sep 17 00:00:00 2001
From: Sam Wilkins <abdullah_ahmed@brown.edu>
Date: Thu, 6 Jun 2019 17:45:26 -0400
Subject: links get saved to global table

---
 src/client/documents/Documents.ts                  | 30 +++++-----
 src/client/util/DocumentManager.ts                 | 54 ++++++++++++------
 src/client/util/DragManager.ts                     | 19 +++++--
 src/client/views/DocumentDecorations.tsx           |  8 ++-
 .../CollectionFreeFormLinksView.tsx                |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  6 +-
 src/client/views/nodes/LinkBox.tsx                 | 17 +++---
 src/client/views/nodes/LinkManager.tsx             | 51 +++++++++++++++++
 src/client/views/nodes/LinkMenu.scss               | 65 +++++++++++++++++++++-
 src/client/views/nodes/LinkMenu.tsx                | 51 ++++++++++++++---
 10 files changed, 247 insertions(+), 56 deletions(-)
 create mode 100644 src/client/views/nodes/LinkManager.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ab61b915c..87b831663 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,6 +35,7 @@ import { dropActionType } from "../util/DragManager";
 import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
+import { LinkManager } from "../views/nodes/LinkManager";
 var requestImageSize = require('request-image-size');
 var path = require('path');
 
@@ -70,12 +71,12 @@ const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
 
 export namespace DocUtils {
     export function MakeLink(source: Doc, target: Doc) {
-        let protoSrc = source.proto ? source.proto : source;
-        let protoTarg = target.proto ? target.proto : target;
+        // let protoSrc = source.proto ? source.proto : source;
+        // let protoTarg = target.proto ? target.proto : target;
         UndoManager.RunInBatch(() => {
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             //let linkDoc = new Doc;
-            linkDoc.proto!.title = "-link name-";
+            linkDoc.proto!.title = source.proto!.title + " and " + target.proto!.title;
             linkDoc.proto!.linkDescription = "";
             linkDoc.proto!.linkTags = "Default";
 
@@ -84,17 +85,20 @@ export namespace DocUtils {
             linkDoc.proto!.linkedFrom = source;
             linkDoc.proto!.linkedFromPage = source.curPage;
 
-            let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
-            if (!linkedFrom) {
-                protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
-            }
-            linkedFrom.push(linkDoc);
+            // let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
+            // if (!linkedFrom) {
+            //     protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
+            // }
+            // linkedFrom.push(linkDoc);
+
+            // let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
+            // if (!linkedTo) {
+            //     protoSrc.linkedToDocs = linkedTo = new List<Doc>();
+            // }
+            // linkedTo.push(linkDoc);
+
+            LinkManager.Instance.allLinks.push(linkDoc);
 
-            let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
-            if (!linkedTo) {
-                protoSrc.linkedToDocs = linkedTo = new List<Doc>();
-            }
-            linkedTo.push(linkDoc);
             return linkDoc;
         }, "make link");
     }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 65c4b9e4b..712529745 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,6 +9,7 @@ import { CollectionView } from '../views/collections/CollectionView';
 import { CollectionPDFView } from '../views/collections/CollectionPDFView';
 import { CollectionVideoView } from '../views/collections/CollectionVideoView';
 import { Id } from '../../new_fields/FieldSymbols';
+import { LinkManager } from '../views/nodes/LinkManager';
 
 
 export class DocumentManager {
@@ -83,12 +84,16 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
-        return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-            let linksList = DocListCast(dv.props.Document.linkedToDocs);
+        let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+
+
+            let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
             if (linksList && linksList.length) {
                 pairs.push(...linksList.reduce((pairs, link) => {
                     if (link) {
-                        let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
+                        let destination = (link["linkedTo"] === dv.props.Document) ? link["linkedFrom"] : link["linkedTo"];
+                        let linkToDoc = FieldValue(Cast(destination, Doc));
+                        // let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
                         if (linkToDoc) {
                             DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
                                 pairs.push({ a: dv, b: docView1, l: link }));
@@ -97,21 +102,38 @@ export class DocumentManager {
                     return pairs;
                 }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
             }
-            linksList = DocListCast(dv.props.Document.linkedFromDocs);
-            if (linksList && linksList.length) {
-                pairs.push(...linksList.reduce((pairs, link) => {
-                    if (link) {
-                        let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
-                        if (linkFromDoc) {
-                            DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
-                                pairs.push({ a: dv, b: docView1, l: link }));
-                        }
-                    }
-                    return pairs;
-                }, pairs));
-            }
+
+            // let linksList = DocListCast(dv.props.Document.linkedToDocs);
+            // console.log("to links", linksList.length);
+            // if (linksList && linksList.length) {
+            //     pairs.push(...linksList.reduce((pairs, link) => {
+            //         if (link) {
+            //             let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
+            //             if (linkToDoc) {
+            //                 DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
+            //                     pairs.push({ a: dv, b: docView1, l: link }));
+            //             }
+            //         }
+            //         return pairs;
+            //     }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+            // }
+            // linksList = DocListCast(dv.props.Document.linkedFromDocs);
+            // console.log("from links", linksList.length);
+            // if (linksList && linksList.length) {
+            //     pairs.push(...linksList.reduce((pairs, link) => {
+            //         if (link) {
+            //             let linkFromDoc = FieldValue(Cast(link.linkedFrom, Doc));
+            //             if (linkFromDoc) {
+            //                 DocumentManager.Instance.getDocumentViews(linkFromDoc).map(docView1 =>
+            //                     pairs.push({ a: dv, b: docView1, l: link }));
+            //             }
+            //         }
+            //         return pairs;
+            //     }, pairs));
+            // }
             return pairs;
         }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+        return linked;
     }
 
     @undoBatch
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 1e84a0db0..7854cc080 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -4,6 +4,7 @@ import { Cast } from "../../new_fields/Types";
 import { emptyFunction } from "../../Utils";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import * as globalCssVariables from "../views/globalCssVariables.scss";
+import { LinkManager } from "../views/nodes/LinkManager";
 
 export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
@@ -41,14 +42,20 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
 export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: number, sourceDoc: Doc) {
     let srcTarg = sourceDoc.proto;
     let draggedDocs: Doc[] = [];
-    let draggedFromDocs: Doc[] = [];
+    // let draggedFromDocs: Doc[] = [];
     if (srcTarg) {
-        let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
-        let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
-        if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
-        if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
+        // let linkToDocs = await DocListCastAsync(srcTarg.linkedToDocs);
+        // let linkFromDocs = await DocListCastAsync(srcTarg.linkedFromDocs);
+        let linkDocs = LinkManager.Instance.findAllRelatedLinks(srcTarg);
+        if (linkDocs) draggedDocs = linkDocs.map(link => {
+            return LinkManager.Instance.findOppositeAnchor(link, sourceDoc);
+        });
+
+
+        // if (linkToDocs) draggedDocs = linkToDocs.map(linkDoc => Cast(linkDoc.linkedTo, Doc) as Doc);
+        // if (linkFromDocs) draggedFromDocs = linkFromDocs.map(linkDoc => Cast(linkDoc.linkedFrom, Doc) as Doc);
     }
-    draggedDocs.push(...draggedFromDocs);
+    // draggedDocs.push(...draggedFromDocs);
     if (draggedDocs.length) {
         let moddrag: Doc[] = [];
         for (const draggedDoc of draggedDocs) {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index da9b1253e..2aae9bce6 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -28,6 +28,7 @@ import { CollectionView } from "./collections/CollectionView";
 import { DocumentManager } from "../util/DocumentManager";
 import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { FieldView } from "./nodes/FieldView";
+import { LinkManager } from "./nodes/LinkManager";
 
 library.add(faLink);
 
@@ -510,9 +511,10 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let linkButton = null;
         if (SelectionManager.SelectedDocuments().length > 0) {
             let selFirst = SelectionManager.SelectedDocuments()[0];
-            let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
-            let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
-            let linkCount = linkToSize + linkFromSize;
+            // let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
+            // let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
+            // let linkCount = linkToSize + linkFromSize;
+            let linkCount = LinkManager.Instance.findAllRelatedLinks(selFirst.props.Document).length;
             linkButton = (<Flyout
                 anchorPoint={anchorPoints.RIGHT_TOP}
                 content={<LinkMenu docView={selFirst}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index a43c5f241..637674e36 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -110,7 +110,7 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                     }
                     return match || found;
                 }, false)) {
-                    console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]);
+                    // console.log("A" + possiblePair.a[Id] + " B" + possiblePair.b[Id] + " L" + connection.l[Id]);
                     drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
                 }
             });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7750b9173..0baa061ab 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
 import { Doc, DocListCast, HeightSym, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { ObjectField } from "../../../new_fields/ObjectField";
-import { createSchema, makeInterface } from "../../../new_fields/Schema";
+import { createSchema, makeInterface, 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 { emptyFunction, Utils } from "../../../Utils";
@@ -30,6 +30,7 @@ import "./DocumentView.scss";
 import React = require("react");
 import { Id, Copy } from '../../../new_fields/FieldSymbols';
 import { ContextMenuProps } from '../ContextMenuItem';
+import { list, object, createSimpleSchema } from 'serializr';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(faTrash);
@@ -47,10 +48,11 @@ library.add(faFingerprint);
 library.add(faCrosshairs);
 library.add(faDesktop);
 
+
 const linkSchema = createSchema({
     title: "string",
     linkDescription: "string",
-    linkTags: "string",
+    linkTags: listSpec("string"),
     linkedTo: Doc,
     linkedFrom: Doc
 });
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index 68b692aad..ebca1dc69 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -1,5 +1,5 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes } from '@fortawesome/free-solid-svg-icons';
+import { faEdit, faEye, faTimes, faArrowRight } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { observer } from "mobx-react";
 import { DocumentManager } from "../../util/DocumentManager";
@@ -16,6 +16,7 @@ import { action } from 'mobx';
 library.add(faEye);
 library.add(faEdit);
 library.add(faTimes);
+library.add(faArrowRight);
 
 interface Props {
     linkDoc: Doc;
@@ -29,7 +30,7 @@ interface Props {
 export class LinkBox extends React.Component<Props> {
 
     @undoBatch
-    onViewButtonPressed = async (e: React.PointerEvent): Promise<void> => {
+    followLink = async (e: React.PointerEvent): Promise<void> => {
         e.stopPropagation();
         DocumentManager.Instance.jumpToDocument(this.props.pairedDoc, e.altKey);
     }
@@ -62,8 +63,8 @@ export class LinkBox extends React.Component<Props> {
 
         return (
             //<LinkEditor linkBox={this} linkDoc={this.props.linkDoc} />
-            <div className="link-container">
-                <div className="info-container" onPointerDown={this.onViewButtonPressed}>
+            <div className="link-menu-item">
+                <div className="link-menu-item-content">
                     <div className="link-name">
                         <p>{this.props.linkName}</p>
                     </div>
@@ -72,13 +73,15 @@ export class LinkBox extends React.Component<Props> {
                     </div>
                 </div>
 
-                <div className="button-container">
+                <div className="link-menu-item-buttons">
                     {/* <div title="Follow Link" className="button" onPointerDown={this.onViewButtonPressed}>
                         <FontAwesomeIcon className="fa-icon-view" icon="eye" size="sm" /></div> */}
+                    <div title="Follow Link" className="button" onPointerDown={this.followLink}>
+                        <FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                     <div title="Edit Link" className="button" onPointerDown={this.onEditButtonPressed}>
-                        <FontAwesomeIcon className="fa-icon-edit" icon="edit" size="sm" /></div>
+                        <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" /></div>
                     <div title="Delete Link" className="button" onPointerDown={this.onDeleteButtonPressed}>
-                        <FontAwesomeIcon className="fa-icon-delete" icon="times" size="sm" /></div>
+                        <FontAwesomeIcon className="fa-icon" icon="times" size="sm" /></div>
                 </div>
             </div>
         );
diff --git a/src/client/views/nodes/LinkManager.tsx b/src/client/views/nodes/LinkManager.tsx
new file mode 100644
index 000000000..1064eaa22
--- /dev/null
+++ b/src/client/views/nodes/LinkManager.tsx
@@ -0,0 +1,51 @@
+import { observable, computed, action } from "mobx";
+import React = require("react");
+import { SelectionManager } from "../../util/SelectionManager";
+import { observer } from "mobx-react";
+import './LinkEditor.scss';
+import { props } from "bluebird";
+import { DocumentView } from "./DocumentView";
+import { link } from "fs";
+import { StrCast, Cast } from "../../../new_fields/Types";
+import { Doc } from "../../../new_fields/Doc";
+import { listSpec } from "../../../new_fields/Schema";
+
+
+export class LinkManager {
+    private static _instance: LinkManager;
+    public static get Instance(): LinkManager {
+        return this._instance || (this._instance = new this());
+    }
+    private constructor() {
+    }
+
+    @observable
+    public allLinks: Array<Doc> = [];
+
+    public findAllRelatedLinks(source: Doc): Array<Doc> {
+        let related = LinkManager.Instance.allLinks.filter(
+            link => Doc.AreProtosEqual(source, Cast(link.linkedFrom, Doc, new Doc)) || Doc.AreProtosEqual(source, Cast(link.linkedTo, Doc, new Doc)));
+        return related;
+    }
+
+    public findRelatedGroupedLinks(source: Doc): Map<string, Array<Doc>> {
+        let related = this.findAllRelatedLinks(source);
+        let categories = new Map();
+        related.forEach(link => {
+            let group = categories.get(link.linkTags);
+            if (group) group.push(link);
+            else group = [link];
+            categories.set(link.linkTags, group);
+        })
+        return categories;
+    }
+
+    public findOppositeAnchor(link: Doc, source: Doc): Doc {
+        if (Doc.AreProtosEqual(source, Cast(link.linkedFrom, Doc, new Doc))) {
+            return Cast(link.linkedTo, Doc, new Doc);
+        } else {
+            return Cast(link.linkedFrom, Doc, new Doc);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index dedcce6ef..860f31d8a 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -1,3 +1,5 @@
+@import "../globalCssVariables";
+
 #linkMenu-container {
     width: 100%;
     height: auto;
@@ -18,4 +20,65 @@
     width: 100%;
     height: 100px;
     overflow-y: scroll;
-}
\ No newline at end of file
+}
+
+.link-menu-group {
+    .link-menu-item {
+        border-top: 0.5px solid $light-color-secondary; 
+        padding: 6px;
+        position: relative;
+        display: flex;
+
+        .link-menu-item-content {
+            width: 100%;
+        }
+
+        &:last-child {
+            border-bottom: 0.5px solid $light-color-secondary;
+        }
+        &:hover .link-menu-item-buttons {
+            display: flex;
+        }
+        &:hover .link-menu-item-content {
+            width: calc(100% - 72px);
+        }
+    }
+    .link-menu-item-buttons {
+        display: none;
+        position: absolute;
+        top: 50%;
+        right: 0;
+        transform: translateY(-50%);
+
+        .button {
+            width: 20px;
+            height: 20px;
+            margin: 0;
+            margin-right: 6px;
+            border-radius: 50%;
+            cursor: pointer;
+            pointer-events: auto;
+            background-color: $dark-color;
+            color: $light-color;
+            font-size: 65%;
+            transition: transform 0.2s;
+            text-align: center;
+            position: relative;
+
+            .fa-icon {
+                position: absolute;
+                top: 50%;
+                left: 50%;
+                transform: translate(-50%, -50%);
+            }
+
+            &:last-child {
+                margin-right: 0;
+            }
+            &:hover {
+                background: $main-accent;
+            }
+        }
+    }
+}
+
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 3f09d6214..6dc5623d1 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -8,6 +8,7 @@ import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { Id } from "../../../new_fields/FieldSymbols";
+import { LinkManager } from "./LinkManager";
 
 interface Props {
     docView: DocumentView;
@@ -19,26 +20,62 @@ export class LinkMenu extends React.Component<Props> {
 
     @observable private _editingLink?: Doc;
 
-    renderLinkItems(links: Doc[], key: string, type: string) {
+    // renderLinkItems(links: Doc[], key: string, type: string) {
+    //     return links.map(link => {
+    //         let doc = FieldValue(Cast(link[key], Doc));
+    //         if (doc) {
+    //             return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+    //         }
+    //     });
+    // }
+
+    renderLinkGroup(links: Doc[]) {
+        console.log("render link group");
+        let source = this.props.docView.Document;
+        console.log("num links", links.length, typeof links);
         return links.map(link => {
-            let doc = FieldValue(Cast(link[key], Doc));
+            let destination = (link["linkedTo"] === source) ? link["linkedFrom"] : link["linkedTo"];
+            let doc = FieldValue(Cast(destination, Doc));
             if (doc) {
-                return <LinkBox key={doc[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={type} />;
+                console.log(doc[Id] + source[Id], "source is", source[Id]);
+                return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
             }
         });
     }
 
+    renderLinkItems(links: Map<string, Array<Doc>>) {
+        console.log("render link items");
+
+        let linkItems: Array<JSX.Element> = [];
+
+        links.forEach((links, group) => {
+            console.log("category is ", group);
+            linkItems.push(
+                <div key={group} className="link-menu-group">
+                    <p className="link-menu-group-name">{group}:</p>
+                    <div className="link-menu-group-wrapper">
+                        {this.renderLinkGroup(links)}
+                    </div>
+                </div>
+            )
+        });
+
+        return linkItems;
+    }
+
     render() {
         //get list of links from document
-        let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
-        let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
+        // let linkFrom = DocListCast(this.props.docView.props.Document.linkedFromDocs);
+        // let linkTo = DocListCast(this.props.docView.props.Document.linkedToDocs);
+        let related = LinkManager.Instance.findRelatedGroupedLinks(this.props.docView.props.Document);
         if (this._editingLink === undefined) {
             return (
                 <div id="linkMenu-container">
                     {/* <input id="linkMenu-searchBar" type="text" placeholder="Search..."></input> */}
                     <div id="linkMenu-list">
-                        {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
-                        {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")}
+                        {/* {this.renderLinkItems(linkTo, "linkedTo", "Destination: ")}
+                        {this.renderLinkItems(linkFrom, "linkedFrom", "Source: ")} */}
+                        {this.renderLinkItems(related)}
                     </div>
                 </div>
             );
-- 
cgit v1.2.3-70-g09d2


From 70996f3f19d408e819e081ed03bd7ccf0de44503 Mon Sep 17 00:00:00 2001
From: Vellichora <fangrui_tong@brown.edu>
Date: Fri, 7 Jun 2019 17:52:31 -0400
Subject: links can be categorized under a group

---
 src/client/documents/Documents.ts        |  40 +++++++++---
 src/client/util/DocumentManager.ts       |   5 +-
 src/client/util/DragManager.ts           |   2 +-
 src/client/util/LinkManager.ts           |  84 +++++++++++++++++++++++++
 src/client/views/DocumentDecorations.tsx |   2 +-
 src/client/views/nodes/DocumentView.tsx  |   2 +-
 src/client/views/nodes/LinkEditor.tsx    | 103 +++++++++++++++++++++++++------
 src/client/views/nodes/LinkManager.tsx   |  51 ---------------
 src/client/views/nodes/LinkMenu.tsx      |  33 ++++++----
 9 files changed, 227 insertions(+), 95 deletions(-)
 create mode 100644 src/client/util/LinkManager.ts
 delete mode 100644 src/client/views/nodes/LinkManager.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 87b831663..5ec19f987 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,7 +35,7 @@ import { dropActionType } from "../util/DragManager";
 import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
-import { LinkManager } from "../views/nodes/LinkManager";
+import { LinkManager } from "../util/LinkManager";
 var requestImageSize = require('request-image-size');
 var path = require('path');
 
@@ -69,21 +69,45 @@ export interface DocumentOptions {
 }
 const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
 
+// export interface LinkData {
+//     anchor1: Doc;
+//     anchor1Page: number;
+//     anchor1Tags: Array<{ tag: string, name: string, description: string }>;
+//     anchor2: Doc;
+//     anchor2Page: number;
+//     anchor2Tags: Array<{ tag: string, name: string, description: string }>;
+// }
+
+// export interface TagData {
+//     tag: string;
+//     name: string;
+//     description: string;
+// }
+
 export namespace DocUtils {
     export function MakeLink(source: Doc, target: Doc) {
         // let protoSrc = source.proto ? source.proto : source;
         // let protoTarg = target.proto ? target.proto : target;
         UndoManager.RunInBatch(() => {
+            let groupDoc = Docs.TextDocument();
+            groupDoc.proto!.type = "*";
+
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             //let linkDoc = new Doc;
-            linkDoc.proto!.title = source.proto!.title + " and " + target.proto!.title;
-            linkDoc.proto!.linkDescription = "";
-            linkDoc.proto!.linkTags = "Default";
+            // linkDoc.proto!.title = source.proto!.title + " and " + target.proto!.title;
+            // linkDoc.proto!.linkDescription = "";
+            linkDoc.proto!.anchor1 = source;
+            linkDoc.proto!.anchor1Page = source.curPage;
+            linkDoc.proto!.anchor1Groups = new List<Doc>([groupDoc]);
+
+            linkDoc.proto!.anchor2 = target;
+            linkDoc.proto!.anchor2Page = target.curPage;
+            linkDoc.proto!.anchor2Groups = new List<Doc>([groupDoc]);
 
-            linkDoc.proto!.linkedTo = target;
-            linkDoc.proto!.linkedToPage = target.curPage;
-            linkDoc.proto!.linkedFrom = source;
-            linkDoc.proto!.linkedFromPage = source.curPage;
+            // linkDoc.proto!.linkedTo = target;
+            // linkDoc.proto!.linkedToPage = target.curPage;
+            // linkDoc.proto!.linkedFrom = source;
+            // linkDoc.proto!.linkedFromPage = source.curPage;
 
             // let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
             // if (!linkedFrom) {
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 712529745..52f0fe3f6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -9,7 +9,7 @@ import { CollectionView } from '../views/collections/CollectionView';
 import { CollectionPDFView } from '../views/collections/CollectionPDFView';
 import { CollectionVideoView } from '../views/collections/CollectionVideoView';
 import { Id } from '../../new_fields/FieldSymbols';
-import { LinkManager } from '../views/nodes/LinkManager';
+import { LinkManager } from './LinkManager';
 
 
 export class DocumentManager {
@@ -91,7 +91,8 @@ export class DocumentManager {
             if (linksList && linksList.length) {
                 pairs.push(...linksList.reduce((pairs, link) => {
                     if (link) {
-                        let destination = (link["linkedTo"] === dv.props.Document) ? link["linkedFrom"] : link["linkedTo"];
+                        // let destination = (link["linkedTo"] === dv.props.Document) ? link["linkedFrom"] : link["linkedTo"];
+                        let destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
                         let linkToDoc = FieldValue(Cast(destination, Doc));
                         // let linkToDoc = FieldValue(Cast(link.linkedTo, Doc));
                         if (linkToDoc) {
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 7854cc080..809368aff 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -4,7 +4,7 @@ import { Cast } from "../../new_fields/Types";
 import { emptyFunction } from "../../Utils";
 import { CollectionDockingView } from "../views/collections/CollectionDockingView";
 import * as globalCssVariables from "../views/globalCssVariables.scss";
-import { LinkManager } from "../views/nodes/LinkManager";
+import { LinkManager } from "./LinkManager";
 
 export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType) {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
new file mode 100644
index 000000000..a6d649395
--- /dev/null
+++ b/src/client/util/LinkManager.ts
@@ -0,0 +1,84 @@
+import { observable, computed, action } from "mobx";
+import React = require("react");
+import { SelectionManager } from "./SelectionManager";
+import { observer } from "mobx-react";
+import { props } from "bluebird";
+import { DocumentView } from "../views/nodes/DocumentView";
+import { link } from "fs";
+import { StrCast, Cast } from "../../new_fields/Types";
+import { Doc } from "../../new_fields/Doc";
+import { listSpec } from "../../new_fields/Schema";
+import { List } from "../../new_fields/List";
+
+
+export class LinkManager {
+    private static _instance: LinkManager;
+    public static get Instance(): LinkManager {
+        return this._instance || (this._instance = new this());
+    }
+    private constructor() {
+    }
+
+    @observable
+    public allLinks: Array<Doc> = [];
+
+    public findAllRelatedLinks(source: Doc): Array<Doc> {
+        let related = LinkManager.Instance.allLinks.filter(
+            link => Doc.AreProtosEqual(source, Cast(link.anchor1, Doc, new Doc)) || Doc.AreProtosEqual(source, Cast(link.anchor2, Doc, new Doc)));
+        return related;
+    }
+
+    public findRelatedGroupedLinks(source: Doc): Map<string, Array<Doc>> {
+        let related = this.findAllRelatedLinks(source);
+
+        let categories = new Map();
+        related.forEach(link => {
+            // get groups of given doc
+            let groups = (Doc.AreProtosEqual(source, Cast(link.anchor1, Doc, new Doc))) ? Cast(link.anchor1Groups, listSpec(Doc), []) : Cast(link.anchor2Groups, listSpec(Doc), []);
+            if (groups) {
+                if (groups.length > 0) {
+                    groups.forEach(groupDoc => {
+                        if (groupDoc instanceof Doc) {
+                            let groupType = StrCast(groupDoc.proto!.type);
+                            let group = categories.get(groupType); // TODO: clean this up lol
+                            if (group) group.push(link);
+                            else group = [link];
+                            categories.set(groupType, group);
+                        } else {
+                            // promise doc
+                        }
+
+                    })
+                }
+                else {
+                    // if link is in no groups then put it in default group
+                    let group = categories.get("*");
+                    if (group) group.push(link);
+                    else group = [link];
+                    categories.set("*", group);
+                }
+            }
+
+
+            //     let anchor = this.findOppositeAnchor(link, source);
+            //     let group = categories.get(link.linkTags);
+            //     if (group) group.push(link);
+            //     else group = [link];
+            //     categories.set(link.linkTags, group);
+        })
+        return categories;
+    }
+
+    public findOppositeAnchor(link: Doc, source: Doc): Doc {
+        if (Doc.AreProtosEqual(source, Cast(link.anchor1, Doc, new Doc))) {
+            return Cast(link.anchor2, Doc, new Doc);
+        } else {
+            return Cast(link.anchor1, Doc, new Doc);
+        }
+    }
+
+    // public findAnchorTags(link: Doc, source: Doc): Doc[] {
+    //     if (source)
+    // }
+
+}
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 2aae9bce6..eb45c770e 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -28,7 +28,7 @@ import { CollectionView } from "./collections/CollectionView";
 import { DocumentManager } from "../util/DocumentManager";
 import { FormattedTextBox } from "./nodes/FormattedTextBox";
 import { FieldView } from "./nodes/FieldView";
-import { LinkManager } from "./nodes/LinkManager";
+import { LinkManager } from "../util/LinkManager";
 
 library.add(faLink);
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 0baa061ab..fc6974e6c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -52,7 +52,7 @@ library.add(faDesktop);
 const linkSchema = createSchema({
     title: "string",
     linkDescription: "string",
-    linkTags: listSpec("string"),
+    linkTags: "string",
     linkedTo: Doc,
     linkedFrom: Doc
 });
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 71a423338..2ab8c3460 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -6,52 +6,115 @@ import './LinkEditor.scss';
 import { props } from "bluebird";
 import { DocumentView } from "./DocumentView";
 import { link } from "fs";
-import { StrCast } from "../../../new_fields/Types";
+import { StrCast, Cast } from "../../../new_fields/Types";
 import { Doc } from "../../../new_fields/Doc";
+import { List } from "../../../new_fields/List";
+import { listSpec } from "../../../new_fields/Schema";
+import { LinkManager } from "../../util/LinkManager";
 
 interface Props {
+    sourceDoc: Doc;
     linkDoc: Doc;
+    groups: Map<number, Doc>;
     showLinks: () => void;
 }
 
 @observer
 export class LinkEditor extends React.Component<Props> {
 
-    @observable private _nameInput: string = StrCast(this.props.linkDoc.title);
-    @observable private _descriptionInput: string = StrCast(this.props.linkDoc.linkDescription);
+    // @observable private _title: string = StrCast(this.props.linkDoc.title);
+    // @observable private _description: string = StrCast(this.props.linkDoc.linkDescription);
+    // @observable private _tags: Array<string> = Cast(this.props.linkDoc.linkTags, List);
 
+    // @action
+    // onTitleChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+    //     this._title = e.target.value;
+    // }
 
-    onSaveButtonPressed = (e: React.PointerEvent): void => {
-        e.stopPropagation();
+    // @action
+    // onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    //     this._description = e.target.value;
+    // }
+
+    // renderTags() {
+    //     return this._tags.map(tag => {
+    //         if (tag === "") {
+    //             return <input type="text" placeholder="Tag"></input>;
+    //         } else {
+    //             return <input type="text" value={tag}></input>;
+    //         }
+    //     })
+    // }
+
+    // addTag = (): void => {
+    //     this._tags.push("");
+    // }
 
+    @action
+    editGroup(groupId: number, value: string) {
         let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        linkDoc.title = this._nameInput;
-        linkDoc.linkDescription = this._descriptionInput;
+        let groupDoc = this.props.groups.get(groupId);
+        if (groupDoc) {
+            groupDoc.proto!.type = value;
+            if (Doc.AreProtosEqual(this.props.sourceDoc, Cast(linkDoc.anchor1, Doc, new Doc))) {
+                // let groups = Cast(linkDoc.anchor1Groups, listSpec(Doc), []);
+                // groups.push(groupDoc);
+                linkDoc.anchor1Groups = new List<Doc>([groupDoc]);
+
+            } else {
+                linkDoc.anchor2Groups = new List<Doc>([groupDoc]);
+            }
+        }
 
-        this.props.showLinks();
     }
 
+    renderGroup(groupId: number, groupDoc: Doc) {
+        return (
+            <div>
+                <p>type:</p>
+                <input type="text" value={StrCast(groupDoc.proto!.type)} onChange={e => this.editGroup(groupId, e.target.value)}></input>
+            </div>
+        )
+    }
 
+    renderGroups() {
+        let groups: Array<JSX.Element> = [];
+        this.props.groups.forEach((groupDoc, groupId) => {
+            groups.push(
+                <div>
+                    {this.renderGroup(groupId, groupDoc)}
+                </div>
+            )
+        });
+        return groups;
+    }
+
+    onSaveButtonPressed = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+
+        // let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
+        // // linkDoc.title = this._title;
+        // // linkDoc.linkDescription = this._description;
+
+        this.props.showLinks();
+    }
 
     render() {
+        let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
         return (
             <div className="edit-container">
-                <input onChange={this.onNameChanged} className="name-input" type="text" value={this._nameInput} placeholder="Name . . ."></input>
-                <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._descriptionInput} placeholder="Description . . ."></textarea>
+                <p>linked to: {destination.proto!.title}</p>
+                <b>Groups:</b>
+                {this.renderGroups()}
+
+                {/* <input onChange={this.onTitleChanged} className="name-input" type="text" value={this._title} placeholder="Name . . ."></input>
+                <textarea onChange={this.onDescriptionChanged} className="description-input" value={this._description} placeholder="Description . . ."></textarea> */}
+                {/* {this.renderTags()}
+                <button onClick={this.addTag}>+</button> */}
                 <div className="save-button" onPointerDown={this.onSaveButtonPressed}>SAVE</div>
             </div>
 
         );
     }
-
-    @action
-    onNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this._nameInput = e.target.value;
-    }
-
-    @action
-    onDescriptionChanged = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
-        this._descriptionInput = e.target.value;
-    }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkManager.tsx b/src/client/views/nodes/LinkManager.tsx
deleted file mode 100644
index 1064eaa22..000000000
--- a/src/client/views/nodes/LinkManager.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { observable, computed, action } from "mobx";
-import React = require("react");
-import { SelectionManager } from "../../util/SelectionManager";
-import { observer } from "mobx-react";
-import './LinkEditor.scss';
-import { props } from "bluebird";
-import { DocumentView } from "./DocumentView";
-import { link } from "fs";
-import { StrCast, Cast } from "../../../new_fields/Types";
-import { Doc } from "../../../new_fields/Doc";
-import { listSpec } from "../../../new_fields/Schema";
-
-
-export class LinkManager {
-    private static _instance: LinkManager;
-    public static get Instance(): LinkManager {
-        return this._instance || (this._instance = new this());
-    }
-    private constructor() {
-    }
-
-    @observable
-    public allLinks: Array<Doc> = [];
-
-    public findAllRelatedLinks(source: Doc): Array<Doc> {
-        let related = LinkManager.Instance.allLinks.filter(
-            link => Doc.AreProtosEqual(source, Cast(link.linkedFrom, Doc, new Doc)) || Doc.AreProtosEqual(source, Cast(link.linkedTo, Doc, new Doc)));
-        return related;
-    }
-
-    public findRelatedGroupedLinks(source: Doc): Map<string, Array<Doc>> {
-        let related = this.findAllRelatedLinks(source);
-        let categories = new Map();
-        related.forEach(link => {
-            let group = categories.get(link.linkTags);
-            if (group) group.push(link);
-            else group = [link];
-            categories.set(link.linkTags, group);
-        })
-        return categories;
-    }
-
-    public findOppositeAnchor(link: Doc, source: Doc): Doc {
-        if (Doc.AreProtosEqual(source, Cast(link.linkedFrom, Doc, new Doc))) {
-            return Cast(link.linkedTo, Doc, new Doc);
-        } else {
-            return Cast(link.linkedFrom, Doc, new Doc);
-        }
-    }
-
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 6dc5623d1..affe35e2a 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -8,7 +8,9 @@ import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { Cast, FieldValue, StrCast } from "../../../new_fields/Types";
 import { Id } from "../../../new_fields/FieldSymbols";
-import { LinkManager } from "./LinkManager";
+import { LinkManager } from "../../util/LinkManager";
+import { number } from "prop-types";
+import { listSpec } from "../../../new_fields/Schema";
 
 interface Props {
     docView: DocumentView;
@@ -29,23 +31,20 @@ export class LinkMenu extends React.Component<Props> {
     //     });
     // }
 
-    renderLinkGroup(links: Doc[]) {
-        console.log("render link group");
+    renderLinkGroupItems(links: Doc[]) {
         let source = this.props.docView.Document;
-        console.log("num links", links.length, typeof links);
         return links.map(link => {
-            let destination = (link["linkedTo"] === source) ? link["linkedFrom"] : link["linkedTo"];
+            // let destination = (link["linkedTo"] === source) ? link["linkedFrom"] : link["linkedTo"];
+            let destination = LinkManager.Instance.findOppositeAnchor(link, source);
             let doc = FieldValue(Cast(destination, Doc));
             if (doc) {
                 console.log(doc[Id] + source[Id], "source is", source[Id]);
-                return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={StrCast(link.title)} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
+                return <LinkBox key={doc[Id] + source[Id]} linkDoc={link} linkName={"link"} pairedDoc={doc} showEditor={action(() => this._editingLink = link)} type={""} />;
             }
         });
     }
 
-    renderLinkItems(links: Map<string, Array<Doc>>) {
-        console.log("render link items");
-
+    renderLinkItems = (links: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
 
         links.forEach((links, group) => {
@@ -54,7 +53,7 @@ export class LinkMenu extends React.Component<Props> {
                 <div key={group} className="link-menu-group">
                     <p className="link-menu-group-name">{group}:</p>
                     <div className="link-menu-group-wrapper">
-                        {this.renderLinkGroup(links)}
+                        {this.renderLinkGroupItems(links)}
                     </div>
                 </div>
             )
@@ -80,8 +79,20 @@ export class LinkMenu extends React.Component<Props> {
                 </div>
             );
         } else {
+            let counter = 0;
+            let groups = new Map<number, Doc>();
+            let groupList = (Doc.AreProtosEqual(this.props.docView.props.Document, Cast(this._editingLink.anchor1, Doc, new Doc))) ?
+                Cast(this._editingLink.anchor1Groups, listSpec(Doc), []) : Cast(this._editingLink.anchor2Groups, listSpec(Doc), []);
+            groupList.forEach(group => {
+                if (group instanceof Doc) {
+                    console.log(counter);
+                    groups.set(counter, group);
+                    counter++;
+                }
+            })
+
             return (
-                <LinkEditor linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
+                <LinkEditor groups={groups} sourceDoc={this.props.docView.props.Document} linkDoc={this._editingLink} showLinks={action(() => this._editingLink = undefined)}></LinkEditor>
             );
         }
 
-- 
cgit v1.2.3-70-g09d2


From 218c8b6476621ef0ffe151014f77bb1d506705a3 Mon Sep 17 00:00:00 2001
From: madelinegr <mgriswold99@gmail.com>
Date: Fri, 14 Jun 2019 18:24:01 -0400
Subject: Zooming added to presentation, and done

---
 src/client/util/DocumentManager.ts                 | 31 +++++++-
 src/client/util/TooltipTextMenu.tsx                |  2 +-
 src/client/views/MainOverlayTextBox.tsx            |  2 +-
 src/client/views/MainView.tsx                      |  5 +-
 src/client/views/SearchItem.tsx                    |  2 +-
 .../views/collections/CollectionDockingView.tsx    |  6 +-
 .../views/collections/CollectionSchemaView.tsx     |  4 +-
 .../views/collections/CollectionStackingView.tsx   |  2 +
 .../views/collections/CollectionTreeView.tsx       |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 31 +++++---
 src/client/views/nodes/DocumentView.tsx            | 10 ++-
 src/client/views/nodes/FieldView.tsx               |  5 +-
 .../views/presentationview/PresentationElement.tsx | 31 +++++++-
 .../views/presentationview/PresentationView.tsx    | 93 +++++++++++++++++++---
 14 files changed, 184 insertions(+), 42 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 65c4b9e4b..a613625cb 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,4 +1,4 @@
-import { computed, observable } from 'mobx';
+import { computed, observable, action } from 'mobx';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
 import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
@@ -115,7 +115,7 @@ export class DocumentManager {
     }
 
     @undoBatch
-    public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise<void> => {
+    public jumpToDocument = async (docDelegate: Doc, willZoom: boolean, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number): Promise<void> => {
         let doc = Doc.GetProto(docDelegate);
         const contextDoc = await Cast(doc.annotationOn, Doc);
         if (contextDoc) {
@@ -128,7 +128,7 @@ export class DocumentManager {
         if (!forceDockFunc && (docView = DocumentManager.Instance.getDocumentView(doc))) {
             docView.props.Document.libraryBrush = true;
             if (linkPage !== undefined) docView.props.Document.curPage = linkPage;
-            docView.props.focus(docView.props.Document);
+            docView.props.focus(docView.props.Document, willZoom);
         } else {
             if (!contextDoc) {
                 const actualDoc = Doc.MakeAlias(docDelegate);
@@ -140,11 +140,34 @@ export class DocumentManager {
                 docDelegate.libraryBrush = true;
                 if (!forceDockFunc && (contextView = DocumentManager.Instance.getDocumentView(contextDoc))) {
                     contextDoc.panTransformType = "Ease";
-                    contextView.props.focus(contextDoc);
+                    contextView.props.focus(contextDoc, willZoom);
                 } else {
                     (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
                 }
             }
         }
     }
+
+    @action
+    zoomIntoScale = (docDelegate: Doc, scale: number) => {
+        let doc = Doc.GetProto(docDelegate);
+
+        let docView: DocumentView | null;
+        docView = DocumentManager.Instance.getDocumentView(doc);
+        if (docView) {
+            docView.props.zoomToScale(scale);
+        }
+    }
+
+    getScaleOfDocView = (docDelegate: Doc) => {
+        let doc = Doc.GetProto(docDelegate);
+
+        let docView: DocumentView | null;
+        docView = DocumentManager.Instance.getDocumentView(doc);
+        if (docView) {
+            return docView.props.getScale();
+        } else {
+            return 1;
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx
index f517f757a..34785446b 100644
--- a/src/client/util/TooltipTextMenu.tsx
+++ b/src/client/util/TooltipTextMenu.tsx
@@ -192,7 +192,7 @@ export class TooltipTextMenu {
                         DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
                             if (f instanceof Doc) {
                                 if (DocumentManager.Instance.getDocumentView(f)) {
-                                    DocumentManager.Instance.getDocumentView(f)!.props.focus(f);
+                                    DocumentManager.Instance.getDocumentView(f)!.props.focus(f, false);
                                 }
                                 else if (CollectionDockingView.Instance) CollectionDockingView.Instance.AddRightSplit(f);
                             }
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 24327b995..d1224febe 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -102,6 +102,6 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                 </div>
             </ div>;
         }
-        else return (null); Z
+        else return (null);
     }
 }
\ No newline at end of file
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3d772916e..f78879efe 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -199,7 +199,10 @@ export class MainView extends React.Component {
                 parentActive={returnTrue}
                 whenActiveChanged={emptyFunction}
                 bringToFront={emptyFunction}
-                ContainingCollectionView={undefined} />;
+                ContainingCollectionView={undefined}
+                zoomToScale={emptyFunction}
+                getScale={returnOne}
+            />;
         let castRes = mainCont ? FieldValue(Cast(mainCont.presentationView, Doc)) : undefined;
         console.log("GETTING mainContent()");
         console.log(castRes instanceof Promise);
diff --git a/src/client/views/SearchItem.tsx b/src/client/views/SearchItem.tsx
index 01c7316d6..101d893de 100644
--- a/src/client/views/SearchItem.tsx
+++ b/src/client/views/SearchItem.tsx
@@ -24,7 +24,7 @@ library.add(faFilm);
 export class SearchItem extends React.Component<SearchProps> {
 
     onClick = () => {
-        DocumentManager.Instance.jumpToDocument(this.props.doc);
+        DocumentManager.Instance.jumpToDocument(this.props.doc, false);
     }
 
     //needs help
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index dcc1bd95d..81f574a6c 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -9,7 +9,7 @@ import { Doc, Field, Opt, DocListCast } from "../../../new_fields/Doc";
 import { FieldId } from "../../../new_fields/RefField";
 import { listSpec } from "../../../new_fields/Schema";
 import { Cast, NumCast, StrCast } from "../../../new_fields/Types";
-import { emptyFunction, returnTrue, Utils } from "../../../Utils";
+import { emptyFunction, returnTrue, Utils, returnOne } from "../../../Utils";
 import { DocServer } from "../../DocServer";
 import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
 import { Transform } from '../../util/Transform';
@@ -483,7 +483,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
                     whenActiveChanged={emptyFunction}
                     focus={emptyFunction}
                     addDocTab={this.addDocTab}
-                    ContainingCollectionView={undefined} />
+                    ContainingCollectionView={undefined}
+                    zoomToScale={emptyFunction}
+                    getScale={returnOne} />
             </div >);
     }
 
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 11d71d023..715faafd0 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -7,7 +7,7 @@ import { observer } from "mobx-react";
 import ReactTable, { CellInfo, ComponentPropsGetterR, ReactTableDefaults } from "react-table";
 import { MAX_ROW_HEIGHT } from '../../views/globalCssVariables.scss';
 import "react-table/react-table.css";
-import { emptyFunction, returnFalse, returnZero } from "../../../Utils";
+import { emptyFunction, returnFalse, returnZero, returnOne } from "../../../Utils";
 import { SetupDrag } from "../../util/DragManager";
 import { CompileScript } from "../../util/Scripting";
 import { Transform } from "../../util/Transform";
@@ -451,6 +451,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                         bringToFront={emptyFunction}
                         addDocTab={this.props.addDocTab}
                         collapseToPoint={this.collapseToPoint}
+                        zoomToScale={emptyFunction}
+                        getScale={returnOne}
                     />
                 </div>)}
             {input}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index da7ea50c6..e1453c658 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -146,6 +146,8 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                     bringToFront={emptyFunction}
                     whenActiveChanged={this.props.whenActiveChanged}
                     collapseToPoint={this.collapseToPoint}
+                    zoomToScale={emptyFunction}
+                    getScale={returnOne}
                 />
             </div>);
         })
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 2814c0502..c80bd8fce 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -147,7 +147,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
                 ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
                 if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
-                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document, false)) });
                 }
                 ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
             } else {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5d26cb0c2..419d95b5f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -244,7 +244,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         doc.zIndex = docs.length + 1;
     }
 
-    focusDocument = (doc: Doc) => {
+    focusDocument = (doc: Doc, willZoom: boolean) => {
         const panX = this.Document.panX;
         const panY = this.Document.panY;
         const id = this.Document[Id];
@@ -271,9 +271,12 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         newState.initializers[id] = { panX: newPanX, panY: newPanY };
         HistoryUtil.pushState(newState);
         this.setPan(newPanX, newPanY);
+
         this.props.Document.panTransformType = "Ease";
         this.props.focus(this.props.Document);
-        //this.setScaleToZoom(doc);
+        if (willZoom) {
+            this.setScaleToZoom(doc);
+        }
 
     }
 
@@ -281,21 +284,29 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
         let p = this.props;
         let PanelHeight = p.PanelHeight();
         let panelWidth = p.PanelWidth();
-        // let heightDif: number = PanelHeight - NumCast(doc.height);
-        // let widthDif: number = panelWidth - NumCast(doc.width);
+
         let docHeight = NumCast(doc.height);
         let docWidth = NumCast(doc.width);
-        let targetHeight = 0.8 * PanelHeight;
-        let targetWidth = 0.8 * panelWidth;
+        let targetHeight = 0.5 * PanelHeight;
+        let targetWidth = 0.5 * panelWidth;
 
         let maxScaleX: number = targetWidth / docWidth;
         let maxScaleY: number = targetHeight / docHeight;
-        // let maxScaleX: number = NumCast(doc.width) / (panelWidth - 10);
-        // let maxScaleY: number = NumCast(doc.height) / (PanelHeight - 10);
         let maxApplicableScale = Math.min(maxScaleX, maxScaleY);
         this.Document.scale = maxApplicableScale;
     }
 
+    zoomToScale = (scale: number) => {
+        this.Document.scale = scale;
+    }
+
+    getScale = () => {
+        if (this.Document.scale) {
+            return this.Document.scale;
+        }
+        return 1;
+    }
+
 
     getDocumentViewProps(document: Doc): DocumentViewProps {
         return {
@@ -315,6 +326,8 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             whenActiveChanged: this.props.whenActiveChanged,
             bringToFront: this.bringToFront,
             addDocTab: this.props.addDocTab,
+            zoomToScale: this.zoomToScale,
+            getScale: this.getScale
         };
     }
 
@@ -346,7 +359,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private childViews = () => [
         <CollectionFreeFormBackgroundView key="backgroundView" {...this.props} {...this.getDocumentViewProps(this.props.Document)} />,
         ...this.views
-    ];
+    ]
     render() {
         const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
         const easing = () => this.props.Document.panTransformType === "Ease";
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 449fa76a7..fb7657b68 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -69,13 +69,15 @@ export interface DocumentViewProps {
     ContentScaling: () => number;
     PanelWidth: () => number;
     PanelHeight: () => number;
-    focus: (doc: Doc) => void;
+    focus: (doc: Doc, willZoom: boolean) => void;
     selectOnLoad: boolean;
     parentActive: () => boolean;
     whenActiveChanged: (isActive: boolean) => void;
     bringToFront: (doc: Doc) => void;
     addDocTab: (doc: Doc, where: string) => void;
     collapseToPoint?: (scrpt: number[], expandedDocs: Doc[] | undefined) => void;
+    zoomToScale: (scale: number) => void;
+    getScale: () => number;
 }
 
 const schema = createSchema({
@@ -302,8 +304,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._lastTap = Date.now();
     }
 
-    deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
-    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+    deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); };
+    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"); };
     makeBtnClicked = (): void => {
         let doc = Doc.GetProto(this.props.Document);
         doc.isButton = !BoolCast(doc.isButton, false);
@@ -422,7 +424,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight");
             }, icon: "search"
         });
-        cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
+        cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document, false), icon: "crosshairs" });
         cm.addItem({ description: "Copy URL", event: () => Utils.CopyText(DocServer.prepend("/doc/" + this.props.Document[Id])), icon: "link" });
         cm.addItem({ description: "Copy ID", event: () => Utils.CopyText(this.props.Document[Id]), icon: "fingerprint" });
         cm.addItem({ description: "Delete", event: this.deleteClicked, icon: "trash" });
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 7b642b299..3047d55a3 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -103,7 +103,10 @@ export class FieldView extends React.Component<FieldViewProps> {
                     ContainingCollectionView={this.props.ContainingCollectionView}
                     parentActive={this.props.active}
                     whenActiveChanged={this.props.whenActiveChanged}
-                    bringToFront={emptyFunction} />
+                    bringToFront={emptyFunction}
+                    zoomToScale={emptyFunction}
+                    getScale={returnOne}
+                />
             );
         }
         else if (field instanceof List) {
diff --git a/src/client/views/presentationview/PresentationElement.tsx b/src/client/views/presentationview/PresentationElement.tsx
index c58570798..00dc07921 100644
--- a/src/client/views/presentationview/PresentationElement.tsx
+++ b/src/client/views/presentationview/PresentationElement.tsx
@@ -12,6 +12,7 @@ import { faFile as fileSolid, faLocationArrow, faArrowUp, faSearch } from '@fort
 import { faFile as fileRegular } from '@fortawesome/free-regular-svg-icons';
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
+import { DocumentManager } from "../../util/DocumentManager";
 
 
 
@@ -27,7 +28,7 @@ interface PresentationElementProps {
     document: Doc;
     index: number;
     deleteDocument(index: number): void;
-    gotoDocument(index: number): void;
+    gotoDocument(index: number, fromDoc: number): Promise<void>;
     allListElements: Doc[];
     groupMappings: Map<String, Doc[]>;
     presStatus: boolean;
@@ -300,6 +301,9 @@ export default class PresentationElement extends React.Component<PresentationEle
             this.selectedButtons[buttonIndex.Navigate] = false;
 
         } else {
+            if (this.selectedButtons[buttonIndex.Show]) {
+                this.selectedButtons[buttonIndex.Show] = false;
+            }
             this.selectedButtons[buttonIndex.Navigate] = true;
         }
 
@@ -307,6 +311,27 @@ export default class PresentationElement extends React.Component<PresentationEle
 
     }
 
+    /**
+    * The function that is called on click to turn zoom option of docs on/off.
+    */
+    @action
+    onZoomDocumentClick = (e: React.MouseEvent) => {
+        e.stopPropagation();
+        if (this.selectedButtons[buttonIndex.Show]) {
+            this.selectedButtons[buttonIndex.Show] = false;
+            this.props.document.viewScale = 1;
+
+        } else {
+            if (this.selectedButtons[buttonIndex.Navigate]) {
+                this.selectedButtons[buttonIndex.Navigate] = false;
+            }
+            this.selectedButtons[buttonIndex.Show] = true;
+        }
+
+        this.autoSaveButtonChange(buttonIndex.Show);
+
+    }
+
 
     render() {
         let p = this.props;
@@ -330,13 +355,13 @@ export default class PresentationElement extends React.Component<PresentationEle
                     outlineStyle: "dashed",
                     outlineWidth: BoolCast(p.document.libraryBrush, false) || BoolCast(p.document.protoBrush, false) ? `1px` : "0px",
                 }}
-                onClick={e => { p.gotoDocument(p.index); e.stopPropagation(); }}>
+                onClick={e => { p.gotoDocument(p.index, NumCast(this.props.mainDocument.selectedDoc)); e.stopPropagation(); }}>
                 <strong className="presentationView-name">
                     {`${p.index + 1}. ${title}`}
                 </strong>
                 <button className="presentation-icon" onClick={e => { this.props.deleteDocument(p.index); e.stopPropagation(); }}>X</button>
                 <br></br>
-                <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"}><FontAwesomeIcon icon={"search"} /></button>
+                <button title="Zoom" className={this.selectedButtons[buttonIndex.Show] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onZoomDocumentClick}><FontAwesomeIcon icon={"search"} /></button>
                 <button title="Navigate" className={this.selectedButtons[buttonIndex.Navigate] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onNavigateDocumentClick}><FontAwesomeIcon icon={"location-arrow"} /></button>
                 <button title="Hide Document Till Presented" className={this.selectedButtons[buttonIndex.HideTillPressed] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onHideDocumentUntilPressClick}><FontAwesomeIcon icon={fileSolid} /></button>
                 <button title="Fade Document After Presented" className={this.selectedButtons[buttonIndex.FadeAfter] ? "presentation-interaction-selected" : "presentation-interaction"} onClick={this.onFadeDocumentAfterPresentedClick}><FontAwesomeIcon icon={fileRegular} color={"gray"} /></button>
diff --git a/src/client/views/presentationview/PresentationView.tsx b/src/client/views/presentationview/PresentationView.tsx
index d3365725b..6cf5fad7e 100644
--- a/src/client/views/presentationview/PresentationView.tsx
+++ b/src/client/views/presentationview/PresentationView.tsx
@@ -26,7 +26,7 @@ export interface PresViewProps {
 
 interface PresListProps extends PresViewProps {
     deleteDocument(index: number): void;
-    gotoDocument(index: number): void;
+    gotoDocument(index: number, fromDoc: number): Promise<void>;
     groupMappings: Map<String, Doc[]>;
     presElementsMappings: Map<Doc, PresentationElement>;
     setChildrenDocs: (docList: Doc[]) => void;
@@ -71,9 +71,24 @@ class PresentationViewList extends React.Component<PresListProps> {
         });
     }
 
+    /**
+     * Initially every document starts with a viewScale 1, which means
+     * that they will be displayed in a canvas with scale 1.
+     */
+    @action
+    initializeScaleViews = (docList: Doc[]) => {
+        docList.forEach((doc: Doc) => {
+            let curScale = NumCast(doc.viewScale, null);
+            if (curScale === undefined) {
+                doc.viewScale = 1;
+            }
+        });
+    }
+
     render() {
         const children = DocListCast(this.props.Document.data);
         this.initializeGroupIds(children);
+        this.initializeScaleViews(children);
         this.props.setChildrenDocs(children);
         return (
 
@@ -102,7 +117,6 @@ export class PresentationView extends React.Component<PresViewProps>  {
     @observable presButtonBackUp: Doc = new Doc();
 
 
-
     componentDidMount() {
         //getting both backUp documents
         let castedGroupBackUp = Cast(this.props.Document.presGroupBackUp, Doc);
@@ -195,7 +209,7 @@ export class PresentationView extends React.Component<PresViewProps>  {
             if (nextSelected === current) nextSelected = current + 1;
         }
 
-        this.gotoDocument(nextSelected);
+        this.gotoDocument(nextSelected, current);
 
     }
     back = async () => {
@@ -209,6 +223,7 @@ export class PresentationView extends React.Component<PresViewProps>  {
         //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)) {
@@ -217,11 +232,29 @@ export class PresentationView extends React.Component<PresViewProps>  {
             //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) => {
+                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 (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.gotoDocument(prevSelected);
     }
 
     /**
@@ -285,9 +318,10 @@ export class PresentationView extends React.Component<PresViewProps>  {
      * 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 = (curDoc: Doc) => {
+    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) {
@@ -297,6 +331,11 @@ export class PresentationView extends React.Component<PresViewProps>  {
                     let selectedButtons: boolean[] = this.presElementsMappings.get(doc)!.selected;
                     if (selectedButtons[buttonIndex.Navigate]) {
                         docToJump = doc;
+                        willZoom = false;
+                    }
+                    if (selectedButtons[buttonIndex.Show]) {
+                        docToJump = doc;
+                        willZoom = true;
                     }
                 });
             }
@@ -305,13 +344,35 @@ export class PresentationView extends React.Component<PresViewProps>  {
         //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)!.selected[buttonIndex.Navigate]) {
-                DocumentManager.Instance.jumpToDocument(curDoc);
-            } else {
-                return;
+            let curDocButtons = this.presElementsMappings.get(curDoc)!.selected;
+            if (curDocButtons[buttonIndex.Navigate]) {
+                DocumentManager.Instance.jumpToDocument(curDoc, false);
+            } else if (curDocButtons[buttonIndex.Show]) {
+                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;
+
+                //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]);
+
+        //awaiting jump so that new scale can be found, since jumping is async
+        await DocumentManager.Instance.jumpToDocument(docToJump, willZoom);
+        let newScale = DocumentManager.Instance.getScaleOfDocView(curDoc);
+        curDoc.viewScale = newScale;
+        //saving the scale that user was on
+        if (curScale !== 1) {
+            this.childrenDocs[fromDoc].viewScale = curScale;
         }
-        DocumentManager.Instance.jumpToDocument(docToJump);
+
     }
 
     /**
@@ -340,7 +401,7 @@ export class PresentationView extends React.Component<PresViewProps>  {
         }
     }
     @action
-    public gotoDocument = async (index: number) => {
+    public gotoDocument = async (index: number, fromDoc: number) => {
         const list = FieldValue(Cast(this.props.Document.data, listSpec(Doc)));
         if (!list) {
             return;
@@ -357,7 +418,7 @@ export class PresentationView extends React.Component<PresViewProps>  {
 
         const doc = await list[index];
         if (this.presStatus) {
-            this.navigateToElement(doc);
+            this.navigateToElement(doc, fromDoc);
             this.hideIfNotPresented(index);
             this.showAfterPresented(index);
         }
@@ -407,7 +468,8 @@ export class PresentationView extends React.Component<PresViewProps>  {
         } else {
             this.presStatus = true;
             this.startPresentation(0);
-            this.gotoDocument(0);
+            const current = NumCast(this.props.Document.selectedDoc);
+            this.gotoDocument(0, current);
         }
         this.props.Document.presStatus = this.presStatus;
     }
@@ -423,6 +485,11 @@ export class PresentationView extends React.Component<PresViewProps>  {
             doc.opacity = 1;
         });
         this.props.Document.selectedDoc = 0;
+        if (this.childrenDocs.length === 0) {
+            return;
+        }
+        DocumentManager.Instance.zoomIntoScale(this.childrenDocs[0], 1);
+        this.childrenDocs[0].viewScale = 1;
 
     }
 
-- 
cgit v1.2.3-70-g09d2


From b4bd43f6e79c9dec30842262f270ca6122f1184a Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Sat, 15 Jun 2019 17:17:45 -0400
Subject: cleaned up some leftover linkedfromdocs/linkedtodocs

---
 src/client/documents/Documents.ts                  | 31 ++---------------
 src/client/util/LinkManager.ts                     |  3 +-
 src/client/views/DocumentDecorations.tsx           |  3 --
 .../views/collections/CollectionDockingView.tsx    |  5 +--
 src/client/views/nodes/DocumentView.tsx            | 40 +++++++++-------------
 5 files changed, 24 insertions(+), 58 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9cb41aecc..268dbc927 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -87,21 +87,11 @@ const delegateKeys = ["x", "y", "width", "height", "panX", "panY"];
 export namespace DocUtils {
     export function MakeLink(source: Doc, target: Doc) {
         if (LinkManager.Instance.doesLinkExist(source, target)) return;
-        // let protoSrc = source.proto ? source.proto : source;
-        // let protoTarg = target.proto ? target.proto : target;
-        UndoManager.RunInBatch(() => {
-            // let groupDoc1 = Docs.TextDocument();
-            // groupDoc1.proto!.type = "*";
-            // groupDoc1.proto!.metadata = Docs.TextDocument();
 
-            // let groupDoc2 = Docs.TextDocument();
-            // groupDoc2.proto!.type = "*";
-            // groupDoc2.proto!.metadata = Docs.TextDocument();
+        UndoManager.RunInBatch(() => {
 
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
-            //let linkDoc = new Doc;
-            // linkDoc.proto!.title = source.proto!.title + " and " + target.proto!.title;
-            // linkDoc.proto!.linkDescription = "";
+
             linkDoc.proto!.anchor1 = source;
             linkDoc.proto!.anchor1Page = source.curPage;
             linkDoc.proto!.anchor1Groups = new List<Doc>([]);
@@ -110,23 +100,6 @@ export namespace DocUtils {
             linkDoc.proto!.anchor2Page = target.curPage;
             linkDoc.proto!.anchor2Groups = new List<Doc>([]);
 
-            // linkDoc.proto!.linkedTo = target;
-            // linkDoc.proto!.linkedToPage = target.curPage;
-            // linkDoc.proto!.linkedFrom = source;
-            // linkDoc.proto!.linkedFromPage = source.curPage;
-
-            // let linkedFrom = Cast(protoTarg.linkedFromDocs, listSpec(Doc));
-            // if (!linkedFrom) {
-            //     protoTarg.linkedFromDocs = linkedFrom = new List<Doc>();
-            // }
-            // linkedFrom.push(linkDoc);
-
-            // let linkedTo = Cast(protoSrc.linkedToDocs, listSpec(Doc));
-            // if (!linkedTo) {
-            //     protoSrc.linkedToDocs = linkedTo = new List<Doc>();
-            // }
-            // linkedTo.push(linkDoc);
-
             LinkManager.Instance.allLinks.push(linkDoc);
 
             return linkDoc;
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index aaed7388d..23ba9d2e4 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -30,7 +30,8 @@ export class LinkManager {
     }
 
     @observable public allLinks: Array<Doc> = []; // list of link docs
-    @observable public groupMetadataKeys: Map<string, Array<string>> = new Map(); // map of group type to list of its metadata keys
+    @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
+    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it hodls
 
     // finds all links that contain the given anchor
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index eb45c770e..6ed263ac8 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -511,9 +511,6 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let linkButton = null;
         if (SelectionManager.SelectedDocuments().length > 0) {
             let selFirst = SelectionManager.SelectedDocuments()[0];
-            // let linkToSize = Cast(selFirst.props.Document.linkedToDocs, listSpec(Doc), []).length;
-            // let linkFromSize = Cast(selFirst.props.Document.linkedFromDocs, listSpec(Doc), []).length;
-            // let linkCount = linkToSize + linkFromSize;
             let linkCount = LinkManager.Instance.findAllRelatedLinks(selFirst.props.Document).length;
             linkButton = (<Flyout
                 anchorPoint={anchorPoints.RIGHT_TOP}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index dcc1bd95d..9861b2c20 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -23,6 +23,7 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { CollectionViewType } from './CollectionBaseView';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { LinkManager } from '../../util/LinkManager';
 
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
@@ -338,9 +339,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
                     counter.DashDocId = tab.contentItem.config.props.documentId;
-                    tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+                    tab.reactionDisposer = reaction(() => [LinkManager.Instance.findAllRelatedLinks(doc), doc.title],
                         () => {
-                            counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
+                            counter.innerHTML = LinkManager.Instance.findAllRelatedLinks(doc).length;
                             tab.titleElement[0].textContent = doc.title;
                         }, { fireImmediately: true });
                     tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fc6974e6c..c1a32ad82 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -31,6 +31,7 @@ import React = require("react");
 import { Id, Copy } from '../../../new_fields/FieldSymbols';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { list, object, createSimpleSchema } from 'serializr';
+import { LinkManager } from '../../util/LinkManager';
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(faTrash);
@@ -49,16 +50,16 @@ library.add(faCrosshairs);
 library.add(faDesktop);
 
 
-const linkSchema = createSchema({
-    title: "string",
-    linkDescription: "string",
-    linkTags: "string",
-    linkedTo: Doc,
-    linkedFrom: Doc
-});
+// const linkSchema = createSchema({
+//     title: "string",
+//     linkDescription: "string",
+//     linkTags: "string",
+//     linkedTo: Doc,
+//     linkedFrom: Doc
+// });
 
-type LinkDoc = makeInterface<[typeof linkSchema]>;
-const LinkDoc = makeInterface(linkSchema);
+// type LinkDoc = makeInterface<[typeof linkSchema]>;
+// const LinkDoc = makeInterface(linkSchema);
 
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
@@ -216,8 +217,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 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 linkedToDocs = await DocListCastAsync(this.props.Document.linkedToDocs, []);
-                let linkedFromDocs = await DocListCastAsync(this.props.Document.linkedFromDocs, []);
+                let linkedDocs = LinkManager.Instance.findAllRelatedLinks(this.props.Document);
                 let expandedDocs: Doc[] = [];
                 expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
                 expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -252,18 +252,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                         this.props.collapseToPoint && this.props.collapseToPoint(scrpt, expandedProtoDocs);
                     }
                 }
-                else if (linkedToDocs.length || linkedFromDocs.length) {
-                    let linkedFwdDocs = [
-                        linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : expandedDocs[0],
-                        linkedFromDocs.length ? linkedFromDocs[0].linkedFrom as Doc : linkedToDocs.length ? linkedToDocs[0].linkedTo as Doc : expandedDocs[0]];
-
-                    let linkedFwdPage = [
-                        linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : undefined,
-                        linkedFromDocs.length ? NumCast(linkedFromDocs[0].linkedFromPage, undefined) : linkedToDocs.length ? NumCast(linkedToDocs[0].linkedToPage, undefined) : undefined];
-                    if (!linkedFwdDocs.some(l => l instanceof Promise)) {
-                        let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 0].maximizeLocation, "inTab");
-                        DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0]);
-                    }
+                else if (linkedDocs.length) {
+                    let linkedDoc = linkedDocs.length ? linkedDocs[0] : expandedDocs[0];
+                    let linkedPages = [linkedDocs.length ? NumCast(linkedDocs[0].anchor1Page, undefined) : NumCast(linkedDocs[0].anchor2Page, undefined),
+                    linkedDocs.length ? NumCast(linkedDocs[0].anchor2Page, undefined) : NumCast(linkedDocs[0].anchor1Page, undefined)];
+                    let maxLocation = StrCast(linkedDoc.maximizeLocation, "inTab");
+                    DocumentManager.Instance.jumpToDocument(linkedDoc, ctrlKey, document => this.props.addDocTab(document, maxLocation), linkedPages[altKey ? 1 : 0]);
                 }
             }
         }
-- 
cgit v1.2.3-70-g09d2


From d91e7eec9a62363b383b929166cdf600b124334c Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 18 Jun 2019 15:09:21 -0400
Subject: links to nodes in different contexts render as a circle

---
 src/client/util/DocumentManager.ts                 |  16 ++-
 .../CollectionFreeFormLinkView.tsx                 |  97 +++++++++------
 .../CollectionFreeFormLinksView.tsx                | 135 +++++++++++++++++----
 src/client/views/nodes/DocumentView.tsx            |   4 +-
 4 files changed, 181 insertions(+), 71 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index fc78993b8..85f8bf751 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
 import { computed, observable } from 'mobx';
 import { DocumentView } from '../views/nodes/DocumentView';
 import { Doc, DocListCast, Opt } from '../../new_fields/Doc';
-import { FieldValue, Cast, NumCast, BoolCast } from '../../new_fields/Types';
+import { FieldValue, Cast, NumCast, BoolCast, StrCast } from '../../new_fields/Types';
 import { listSpec } from '../../new_fields/Schema';
 import { undoBatch } from './UndoManager';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
@@ -85,22 +85,26 @@ export class DocumentManager {
     @computed
     public get LinkedDocumentViews() {
         let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-
+            // console.log("FINDING LINKED DVs FOR", StrCast(dv.props.Document.title));
             let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
             if (linksList && linksList.length) {
                 pairs.push(...linksList.reduce((pairs, link) => {
                     if (link) {
                         let destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
                         if (destination) {
-                            DocumentManager.Instance.getDocumentViews(destination).map(docView1 =>
-                                pairs.push({ a: dv, b: docView1, l: link }));
+                            DocumentManager.Instance.getDocumentViews(destination).map(docView1 => {
+                                // console.log("PUSHING LINK BETWEEN", StrCast(dv.props.Document.title), StrCast(docView1.props.Document.title));
+                                // TODO: if any docviews are not in the same context, draw a proxy
+                                // let sameContent = dv.props.ContainingCollectionView === docView1.props.ContainingCollectionView;
+                                pairs.push({ anchor1View: dv, anchor2View: docView1, linkDoc: link });
+                            });
                         }
                     }
                     return pairs;
-                }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
+                }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]));
             }
             return pairs;
-        }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+        }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]);
         return linked;
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index ddde8ece8..36ffac9c8 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -5,58 +5,81 @@ import { InkingControl } from "../../InkingControl";
 import "./CollectionFreeFormLinkView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
+import { DocumentView } from "../../nodes/DocumentView";
 
 export interface CollectionFreeFormLinkViewProps {
-    A: Doc;
-    B: Doc;
-    LinkDocs: Doc[];
-    addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
-    removeDocument: (document: Doc) => boolean;
+    // anchor1: Doc;
+    // anchor2: Doc;
+    // LinkDocs: Doc[];
+    // addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+    // removeDocument: (document: Doc) => boolean;
+    // sameContext: boolean;
+
+    sourceView: DocumentView;
+    targetView: DocumentView;
+    sameContext: boolean;
 }
 
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
 
-    onPointerDown = (e: React.PointerEvent) => {
-        if (e.button === 0 && !InkingControl.Instance.selectedTool) {
-            let a = this.props.A;
-            let b = this.props.B;
-            let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
-            let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
-            let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
-            let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
-            this.props.LinkDocs.map(l => {
-                let width = l[WidthSym]();
-                l.x = (x1 + x2) / 2 - width / 2;
-                l.y = (y1 + y2) / 2 + 10;
-                if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
-            });
-            e.stopPropagation();
-            e.preventDefault();
-        }
-    }
+    // onPointerDown = (e: React.PointerEvent) => {
+    //     if (e.button === 0 && !InkingControl.Instance.selectedTool) {
+    //         let a = this.props.A;
+    //         let b = this.props.B;
+    //         let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+    //         let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
+    //         let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
+    //         let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
+    //         this.props.LinkDocs.map(l => {
+    //             let width = l[WidthSym]();
+    //             l.x = (x1 + x2) / 2 - width / 2;
+    //             l.y = (y1 + y2) / 2 + 10;
+    //             if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
+    //         });
+    //         e.stopPropagation();
+    //         e.preventDefault();
+    //     }
+    // }
+
     render() {
-        let l = this.props.LinkDocs;
-        let a = this.props.A;
-        let b = this.props.B;
-        let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
-        let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
-        let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
-        let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
-        let text = "";
-        this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
-        text = "";
+        // let l = this.props.LinkDocs;
+        // let a = this.props.A;
+        // let b = this.props.B;
+        let a1 = this.props.sourceView;
+        let a2 = this.props.targetView;
+        let x1 = NumCast(a1.Document.x) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.width) / NumCast(a1.Document.zoomBasis, 1) / 2);
+        let y1 = NumCast(a1.Document.y) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.height) / NumCast(a1.Document.zoomBasis, 1) / 2);
+
+        let x2 = NumCast(a2.Document.x) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.width) / NumCast(a2.Document.zoomBasis, 1) / 2);
+        let y2 = NumCast(a2.Document.y) + (BoolCast(a2.Document.isMinimized, false) ? 5 : NumCast(a2.Document.height) / NumCast(a2.Document.zoomBasis, 1) / 2);
+        if (!this.props.sameContext) {
+            x2 = x1 + 300;
+            y2 = y1 - 300;
+        }
+
+        // if (!this.props.sameContext) {
+        //     console.log("not same context", StrCast(a1.title), StrCast(a2.title));
+        //     x2 = x1 + 300;
+        //     y2 = y2 + 300;
+        // } else {
+        //     console.log("same context", StrCast(a1.title), StrCast(a2.title));
+        // }
+        // let text = "";
+        // this.props.LinkDocs.map(l => text += StrCast(l.title) + "(" + StrCast(l.linkDescription) + "), ");
+        // text = "";
         return (
             <>
-                <line key="linkLine" className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${2 * l.length / 2}` }}
+                <line className="collectionfreeformlinkview-linkLine"
+                    style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
+                {!this.props.sameContext ? <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={10}></circle> : <></>}
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
                     cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
-                <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
+                {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
                     {text}
-                </text>
+                </text> */}
             </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index c4dd534ed..fc92c81d5 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -92,40 +92,123 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         return equalViews.filter(sv => sv.props.ContainingCollectionView && sv.props.ContainingCollectionView.props.Document === this.props.Document);
     }
 
-    @computed
-    get uniqueConnections() {
-        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
-            let srcViews = this.documentAnchors(connection.a);
-            let targetViews = this.documentAnchors(connection.b);
-            let possiblePairs: { a: Doc, b: Doc, }[] = [];
-            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
-            possiblePairs.map(possiblePair => {
-                if (!drawnPairs.reduce((found, drawnPair) => {
-                    let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
-                    let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
-                    let match = match1 || match2;
-                    if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
-                        drawnPair.l.push(connection.l);
-                    }
-                    return match || found;
-                }, false)) {
-                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] })
+    // @computed
+    // get uniqueConnections() {
+    //     // console.log("\n");
+    //     let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+    //         // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title));
+    //         let srcViews = this.documentAnchors(connection.anchor1View);
+    //         // srcViews.forEach(sv => {
+    //         //     console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title));
+    //         // });
+
+    //         let targetViews = this.documentAnchors(connection.anchor2View);
+    //         // targetViews.forEach(sv => {
+    //         //     console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title));
+    //         // });
+
+    //         // console.log("lengths", srcViews.length, targetViews.length);
+
+    //         // srcViews.forEach(v => {
+    //         //     console.log("SOURCE VIEW", StrCast(v.props.Document.title));
+    //         // });
+    //         // targetViews.forEach(v => {
+    //         //     console.log("TARGET VIEW", StrCast(v.Document.title));
+    //         // });
+
+    //         let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = [];
+    //         // srcViews.map(sv => {
+    //         //     console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
+    //         //     targetViews.map(tv => {
+    //         //         console.log("TARGET VIEW", StrCast(tv.props.Document.title));
+    //         //         // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
+    //         //         possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
+    //         //     });
+    //         //     console.log("END\n");
+    //         // });
+    //         srcViews.forEach(sv => {
+    //             // console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
+    //             targetViews.forEach(tv => {
+    //                 // console.log("TARGET VIEW", StrCast(tv.props.Document.title));
+    //                 // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
+    //                 possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
+    //             });
+    //             // console.log("END\n");
+    //         });
+    //         // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length);
+    //         possiblePairs.map(possiblePair => {
+    //             // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title));
+    //             if (!drawnPairs.reduce((found, drawnPair) => {
+    //                 let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2));
+    //                 let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1));
+    //                 let match = match1 || match2;
+    //                 if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) {
+    //                     drawnPair.linkDocs.push(connection.linkDoc);
+    //                 }
+    //                 return match || found;
+    //             }, false)) {
+    //                 drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] });
+    //             }
+    //         });
+    //         return drawnPairs;
+    //     }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]);
+    //     return connections.map(c => {
+    //         let x = c.linkDocs.reduce((p, l) => p + l[Id], "");
+    //         return <CollectionFreeFormLinkView key={x} anchor1={c.anchor1} anchor2={c.anchor2} />;
+    //     });
+    // }
+
+    findUniquePairs = (): JSX.Element[] => {
+        // console.log("FIND UNIQUE PAIRS");
+        let connections = DocumentManager.Instance.LinkedDocumentViews;
+
+        let unique: Array<{ sourceView: DocumentView, targetView: DocumentView, sameContext: boolean }> = [];
+        connections.forEach(c => {
+            let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
+            let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
+            let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
+
+            if (!(match1Index > -1 || match2Index > -1)) {
+                // if docview pair does not already exist in unique, push
+                unique.push({ sourceView: c.anchor1View, targetView: c.anchor2View, sameContext: sameContext });
+            } else {
+                // if docview pair exists in unique, push if not in same context
+                if (!sameContext) {
+                    match1Index > -1 ? unique.push({ sourceView: c.anchor2View, targetView: c.anchor1View, sameContext: sameContext })
+                        : unique.push({ sourceView: c.anchor1View, targetView: c.anchor2View, sameContext: sameContext });
                 }
-            });
-            return drawnPairs;
-        }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
-        return connections.map(c => {
-            let x = c.l.reduce((p, l) => p + l[Id], "");
-            return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
-                removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
+            }
+        });
+
+        console.log("\n UNIQUE");
+        unique.forEach(u => {
+            console.log(StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title), u.sameContext);
+        });
+
+        // console.log("\n");
+
+        return unique.map(u => {
+            // TODO: make better key
+            let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
+            let sourceIn = u.sourceView.props.ContainingCollectionView!.props.Document === this.props.Document;
+            let targetIn = u.targetView.props.ContainingCollectionView!.props.Document === this.props.Document;
+            let inContainer = u.sameContext ? sourceIn || targetIn : sourceIn;
+            if (inContainer) {
+                // console.log("key", key, StrCast(u.sourceView.Document.title), StrCast(u.targetView.Document.title));
+                return <CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} sameContext={u.sameContext} />;
+            } else {
+                return <div key={key}></div>;
+            }
         });
     }
 
     render() {
+        this.findUniquePairs();
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
-                    {this.uniqueConnections}
+                    {/* {this.uniqueConnections} */}
+                    {this.findUniquePairs()}
                 </svg>
                 {this.props.children}
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c998b8ea6..e98392a18 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -319,8 +319,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._lastTap = Date.now();
     }
 
-    deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); }
-    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight") };
+    deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); };
+    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"); };
     makeBtnClicked = (): void => {
         let doc = Doc.GetProto(this.props.Document);
         doc.isButton = !BoolCast(doc.isButton, false);
-- 
cgit v1.2.3-70-g09d2


From c5e401cb0a7fec2279ceecbc8d1429dcdd2f04b9 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 19 Jun 2019 22:27:21 -0400
Subject: buttons on cut links functional except for when dragged from link
 menu

---
 src/client/documents/Documents.ts                  |  47 +++++++--
 src/client/util/DragManager.ts                     |   7 ++
 src/client/util/LinkManager.ts                     |  15 ++-
 .../CollectionFreeFormLinkView.scss                |  59 ++++++++---
 .../CollectionFreeFormLinkView.tsx                 |   2 +-
 .../CollectionFreeFormLinkWithProxyView.tsx        | 109 +++++++++++++++++----
 .../CollectionFreeFormLinksView.tsx                |  42 +++++---
 src/client/views/nodes/DocumentContentsView.tsx    |   3 +-
 src/client/views/nodes/DocumentView.tsx            |  14 ++-
 src/client/views/nodes/FieldView.tsx               |   6 ++
 src/client/views/nodes/LinkBox.scss                |  76 --------------
 src/client/views/nodes/LinkBox.tsx                 | 107 --------------------
 src/client/views/nodes/LinkButtonBox.scss          |  18 ++++
 src/client/views/nodes/LinkButtonBox.tsx           |  63 ++++++++++++
 src/client/views/nodes/LinkMenu.tsx                |   4 +-
 src/client/views/nodes/LinkMenuItem.scss           |  76 ++++++++++++++
 src/client/views/nodes/LinkMenuItem.tsx            | 107 ++++++++++++++++++++
 src/new_fields/LinkButtonField.ts                  |  35 +++++++
 18 files changed, 548 insertions(+), 242 deletions(-)
 delete mode 100644 src/client/views/nodes/LinkBox.scss
 delete mode 100644 src/client/views/nodes/LinkBox.tsx
 create mode 100644 src/client/views/nodes/LinkButtonBox.scss
 create mode 100644 src/client/views/nodes/LinkButtonBox.tsx
 create mode 100644 src/client/views/nodes/LinkMenuItem.scss
 create mode 100644 src/client/views/nodes/LinkMenuItem.tsx
 create mode 100644 src/new_fields/LinkButtonField.ts

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 79ba433c8..4d4fa2645 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -26,7 +26,7 @@ import { OmitKeys } from "../../Utils";
 import { ImageField, VideoField, AudioField, PdfField, WebField } from "../../new_fields/URLField";
 import { HtmlField } from "../../new_fields/HtmlField";
 import { List } from "../../new_fields/List";
-import { Cast, NumCast } from "../../new_fields/Types";
+import { Cast, NumCast, StrCast } from "../../new_fields/Types";
 import { IconField } from "../../new_fields/IconField";
 import { listSpec } from "../../new_fields/Schema";
 import { DocServer } from "../DocServer";
@@ -36,6 +36,10 @@ import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
 import { LinkManager } from "../util/LinkManager";
+import { LinkButtonBox } from "../views/nodes/LinkButtonBox";
+import { LinkButtonField, LinkButtonData } from "../../new_fields/LinkButtonField";
+import { DocumentManager } from "../util/DocumentManager";
+import { Id } from "../../new_fields/FieldSymbols";
 var requestImageSize = require('request-image-size');
 var path = require('path');
 
@@ -102,14 +106,32 @@ export namespace DocUtils {
 
             linkDocProto.context = targetContext;
 
-            let proxy1 = Docs.TextDocument({ width: 300, height: 100, borderRounding: 0 });
-            let proxy1Proto = Doc.GetProto(proxy1);
-            let proxy2 = Docs.TextDocument({ width: 300, height: 100, borderRounding: 0 });
-            let proxy2Proto = Doc.GetProto(proxy2);
+            let sourceViews = DocumentManager.Instance.getDocumentViews(source);
+            let targetViews = DocumentManager.Instance.getDocumentViews(target);
+            sourceViews.forEach(sv => {
+                targetViews.forEach(tv => {
 
-            linkDocProto.proxy1 = proxy1; // src: 1 targ: 2
-            linkDocProto.proxy2 = proxy2; // src: 2 targ: 1
+                    // TODO: do only for when diff contexts
+                    let proxy1 = Docs.LinkButtonDocument(
+                        { sourceViewId: StrCast(sv.props.Document[Id]), targetViewId: StrCast(tv.props.Document[Id]) },
+                        { width: 200, height: 100, borderRounding: 0 });
+                    let proxy1Proto = Doc.GetProto(proxy1);
+                    proxy1Proto.sourceViewId = StrCast(sv.props.Document[Id]);
+                    proxy1Proto.targetViewId = StrCast(tv.props.Document[Id]);
+                    proxy1Proto.isLinkButton = true;
 
+                    let proxy2 = Docs.LinkButtonDocument(
+                        { sourceViewId: StrCast(tv.props.Document[Id]), targetViewId: StrCast(sv.props.Document[Id]) },
+                        { width: 200, height: 100, borderRounding: 0 });
+                    let proxy2Proto = Doc.GetProto(proxy2);
+                    proxy2Proto.sourceViewId = StrCast(tv.props.Document[Id]);
+                    proxy2Proto.targetViewId = StrCast(sv.props.Document[Id]);
+                    proxy2Proto.isLinkButton = true;
+
+                    LinkManager.Instance.linkProxies.push(proxy1);
+                    LinkManager.Instance.linkProxies.push(proxy2);
+                });
+            });
 
             LinkManager.Instance.allLinks.push(linkDoc);
 
@@ -131,6 +153,7 @@ export namespace Docs {
     let audioProto: Doc;
     let pdfProto: Doc;
     let iconProto: Doc;
+    let linkProto: Doc;
     const textProtoId = "textProto";
     const histoProtoId = "histoProto";
     const pdfProtoId = "pdfProto";
@@ -141,6 +164,7 @@ export namespace Docs {
     const videoProtoId = "videoProto";
     const audioProtoId = "audioProto";
     const iconProtoId = "iconProto";
+    const linkProtoId = "linkProto";
 
     export function initProtos(): Promise<void> {
         return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
@@ -154,6 +178,7 @@ export namespace Docs {
             audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
             pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
             iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
+            linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype();
         });
     }
 
@@ -186,6 +211,11 @@ export namespace Docs {
             { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
         return iconProto;
     }
+    function CreateLinkPrototype(): Doc {
+        let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
+            { x: 0, y: 0, width: 300 });
+        return linkProto;
+    }
     function CreateTextPrototype(): Doc {
         let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
             { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
@@ -272,6 +302,9 @@ export namespace Docs {
     export function IconDocument(icon: string, options: DocumentOptions = {}) {
         return CreateInstance(iconProto, new IconField(icon), options);
     }
+    export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
+        return CreateInstance(linkProto, new LinkButtonField(data), options);
+    }
     export function PdfDocument(url: string, options: DocumentOptions = {}) {
         return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
     }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 4787ac40f..be00778e7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -7,6 +7,8 @@ import * as globalCssVariables from "../views/globalCssVariables.scss";
 import { LinkManager } from "./LinkManager";
 import { URLField } from "../../new_fields/URLField";
 import { SelectionManager } from "./SelectionManager";
+import { Docs } from "../documents/Documents";
+import { DocumentManager } from "./DocumentManager";
 
 export type dropActionType = "alias" | "copy" | undefined;
 export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: () => Doc | Promise<Doc>, moveFunc?: DragManager.MoveFunction, dropAction?: dropActionType, options?: any, dontHideOnDrop?: boolean) {
@@ -223,6 +225,11 @@ export namespace DragManager {
         StartDrag([ele], dragData, downX, downY, options);
     }
 
+    export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+        runInAction(() => StartDragFunctions.map(func => func()));
+        StartDrag([ele], dragData, downX, downY, options);
+    }
+
     export let AbortDrag: () => void = emptyFunction;
 
     function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: { [id: string]: any }) => void) {
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 23ba9d2e4..544f2edda 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,4 +1,4 @@
-import { observable } from "mobx";
+import { observable, action } from "mobx";
 import { StrCast, Cast } from "../../new_fields/Types";
 import { Doc, DocListCast } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
@@ -32,6 +32,7 @@ export class LinkManager {
     @observable public allLinks: Array<Doc> = []; // list of link docs
     @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
     // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it hodls
+    @observable public linkProxies: Array<Doc> = []; // list of linkbutton docs - used to visualize link when an anchors are not in the same context
 
     // finds all links that contain the given anchor
     public findAllRelatedLinks(anchor: Doc): Array<Doc> {
@@ -134,4 +135,16 @@ export class LinkManager {
         }
     }
 
+    @action
+    public addLinkProxy(proxy: Doc) {
+        LinkManager.Instance.linkProxies.push(proxy);
+    }
+
+    public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
+        let index = LinkManager.Instance.linkProxies.findIndex(p => {
+            return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
+        });
+        return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
+    }
+
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index fc5212edd..d8d518147 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,19 +1,50 @@
-.collectionfreeformlinkview-linkLine {
-    stroke: black;
+// .collectionfreeformlinkview-linkLine {
+//     stroke: black;
+//     transform: translate(10000px,10000px);
+//     opacity: 0.5;
+//     pointer-events: all;
+// }
+// .collectionfreeformlinkview-linkCircle {
+//     stroke: rgb(0,0,0);
+//     opacity: 0.5;
+//     transform: translate(10000px,10000px);
+//     pointer-events: all;
+//     cursor: pointer;
+// }
+// .collectionfreeformlinkview-linkText {
+//     stroke: rgb(0,0,0);
+//     opacity: 0.5;
+//     transform: translate(10000px,10000px);
+//     pointer-events: all;
+// }
+
+.linkview-ele {
     transform: translate(10000px,10000px);
-    opacity: 0.5;
     pointer-events: all;
+
+    &.linkview-line {
+        stroke: black;
+        stroke-width: 2px;
+        opacity: 0.5;
+    }
 }
-.collectionfreeformlinkview-linkCircle {
-    stroke: rgb(0,0,0);
-    opacity: 0.5;
-    transform: translate(10000px,10000px);
-    pointer-events: all;
+
+.linkview-button {
+    width: 200px;
+    height: 100px;
+    border-radius: 5px;
+    padding: 10px;
+    position: relative;
+    background-color: black;
     cursor: pointer;
-}
-.collectionfreeformlinkview-linkText {
-    stroke: rgb(0,0,0);
-    opacity: 0.5;
-    transform: translate(10000px,10000px);
-    pointer-events: all;
+
+    p {
+        width: calc(100% - 20px);
+        color: white;
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+    }
+
 }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index f995a35e3..13b5dc575 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -57,7 +57,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
 
         return (
             <>
-                <line className="collectionfreeformlinkview-linkLine"
+                <line className="linkview-line linkview-ele"
                     style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
index 81a00ba95..a4d122af2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
@@ -7,13 +7,17 @@ import React = require("react");
 import v5 = require("uuid/v5");
 import { DocumentView } from "../../nodes/DocumentView";
 import { Docs } from "../../../documents/Documents";
-import { observable } from "mobx";
+import { observable, action } from "mobx";
+import { CollectionDockingView } from "../CollectionDockingView";
+import { dropActionType, DragManager } from "../../../util/DragManager";
+import { emptyFunction } from "../../../../Utils";
+import { DocumentManager } from "../../../util/DocumentManager";
 
 export interface CollectionFreeFormLinkViewProps {
     sourceView: DocumentView;
     targetView: DocumentView;
     proxyDoc: Doc;
-    addDocTab: (document: Doc, where: string) => void;
+    // addDocTab: (document: Doc, where: string) => void;
 }
 
 @observer
@@ -21,37 +25,104 @@ export class CollectionFreeFormLinkWithProxyView extends React.Component<Collect
 
     // @observable private _proxyX: number = NumCast(this.props.proxyDoc.x);
     // @observable private _proxyY: number = NumCast(this.props.proxyDoc.y);
+    private _ref = React.createRef<HTMLDivElement>();
+    private _downX: number = 0;
+    private _downY: number = 0;
+    @observable _x: number = 0;
+    @observable _y: number = 0;
+    // @observable private _proxyDoc: Doc = Docs.TextDocument(); // used for positioning
+
+    @action
+    componentDidMount() {
+        let a2 = this.props.proxyDoc;
+        this._x = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
+        this._y = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
+    }
+
 
     followButton = (e: React.PointerEvent): void => {
-        // TODO: would be nicer to open docview in context
         e.stopPropagation();
-        console.log("follow");
-        this.props.addDocTab(this.props.targetView.props.Document, "onRight");
+        let open = this.props.targetView.props.ContainingCollectionView ? this.props.targetView.props.ContainingCollectionView.props.Document : this.props.targetView.props.Document;
+        CollectionDockingView.Instance.AddRightSplit(open);
+        DocumentManager.Instance.jumpToDocument(this.props.targetView.props.Document, e.altKey);
+    }
+
+    @action
+    setPosition(x: number, y: number) {
+        this._x = x;
+        this._y = y;
+    }
+
+    startDragging(x: number, y: number) {
+        if (this._ref.current) {
+            let dragData = new DragManager.DocumentDragData([this.props.proxyDoc]);
+
+            DragManager.StartLinkProxyDrag(this._ref.current, dragData, x, y, {
+                handlers: {
+                    dragComplete: action(() => {
+                        let a2 = this.props.proxyDoc;
+                        let offset = NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2;
+                        let x = NumCast(a2.x);// + NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2;
+                        let y = NumCast(a2.y);// + NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2;
+                        this.setPosition(x, y);
+
+                        // this is a hack :'( theres prob a better way to make the input doc not render
+                        let views = DocumentManager.Instance.getDocumentViews(this.props.proxyDoc);
+                        views.forEach(dv => {
+                            dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
+                        });
+                    }),
+                },
+                hideSource: true //?
+            });
+        }
+    }
+
+    onPointerDown = (e: React.PointerEvent): void => {
+        this._downX = e.clientX;
+        this._downY = e.clientY;
+
+        e.stopPropagation();
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.addEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
+        document.addEventListener("pointerup", this.onPointerUp);
+    }
+
+    onPointerMove = (e: PointerEvent): void => {
+        if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
+            document.removeEventListener("pointermove", this.onPointerMove);
+            document.removeEventListener("pointerup", this.onPointerUp);
+            this.startDragging(this._downX, this._downY);
+        }
+        e.stopPropagation();
+        e.preventDefault();
+    }
+    onPointerUp = (e: PointerEvent): void => {
+        document.removeEventListener("pointermove", this.onPointerMove);
+        document.removeEventListener("pointerup", this.onPointerUp);
     }
 
     render() {
         let a1 = this.props.sourceView;
-        let a2 = this.props.proxyDoc;
         let x1 = NumCast(a1.Document.x) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.width) / NumCast(a1.Document.zoomBasis, 1) / 2);
         let y1 = NumCast(a1.Document.y) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.height) / NumCast(a1.Document.zoomBasis, 1) / 2);
 
-        let x2 = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
-        let y2 = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
-
-        // let containing = "";
-        // if (this.props.targetView.props.ContainingCollectionView) {
-        //     containing = StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title);
-        // }
+        let context = this.props.targetView.props.ContainingCollectionView ?
+            (" in the context of " + StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title)) : "";
+        let text = "link to " + StrCast(this.props.targetView.props.Document.title) + context;
 
-        // let text = "link to " + StrCast(this.props.targetView.props.Document.title) + (containing === "" ? "" : (" in the context of " + containing));
         return (
             <>
-                <line className="collectionfreeformlinkview-linkLine"
-                    style={{ strokeWidth: `${2 * 1 / 2}` }}
+                <line className="linkview-line linkview-ele"
+                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
-                    x2={`${x2}`} y2={`${y2}`} />
-                {/* <circle className="collectionfreeformlinkview-linkCircle" cx={x2} cy={y2} r={20} ></circle>
-                <text textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${x2}`} y={`${y2}`}> {text}</text> */}
+                    x2={`${this._x}`} y2={`${this._y}`} />
+                <foreignObject className="linkview-button-wrapper linkview-ele" width={200} height={100} x={this._x - 100} y={this._y - 50}>
+                    <div className="linkview-button" onPointerDown={this.onPointerDown} onPointerUp={this.followButton} ref={this._ref}>
+                        <p>{text}</p>
+                    </div>
+                </foreignObject>
             </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index eaef1f32a..bde68001b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction } from "mobx";
+import { computed, IReactionDisposer, reaction, action } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
@@ -13,6 +13,8 @@ import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
 import React = require("react");
 import { CollectionFreeFormLinkWithProxyView } from "./CollectionFreeFormLinkWithProxyView";
 import { Docs } from "../../../documents/Documents";
+import { LinkButtonField } from "../../../../new_fields/LinkButtonField";
+import { LinkManager } from "../../../util/LinkManager";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -195,18 +197,32 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                 if (sameContext) {
                     uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
                 } else {
-                    let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
-                    let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
-
-                    let context = u.targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(u.targetView.props.ContainingCollectionView.props.Document.title)) : "";
-                    let text = proxyKey + " link to " + StrCast(u.targetView.props.Document.title) + context;
-
-                    let proxyProto = Doc.GetProto(proxy);
-                    proxyProto.data = text;
-
-                    this.props.addDocument(proxy, false);
-                    uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
-                        proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
+                    let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
+                    if (!proxy) {
+                        proxy = Docs.LinkButtonDocument(
+                            { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) },
+                            { width: 200, height: 100, borderRounding: 0 });
+                        let proxy1Proto = Doc.GetProto(proxy);
+                        proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]);
+                        proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]);
+                        proxy1Proto.isLinkButton = true;
+
+                        // LinkManager.Instance.linkProxies.push(proxy);
+                        LinkManager.Instance.addLinkProxy(proxy);
+                    }
+                    uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
+
+                    // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
+                    // if (proxy) {
+                    //     this.props.addDocument(proxy, false);
+                    //     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
+                    // }
+                    // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
+                    // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
+                    // this.props.addDocument(proxy, false);
+
+                    // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
+                    //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
                 }
             }
         });
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 02396c3af..940b00a90 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -12,6 +12,7 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { ImageBox } from "./ImageBox";
 import { IconBox } from "./IconBox";
+import { LinkButtonBox } from "./LinkButtonBox";
 import { KeyValueBox } from "./KeyValueBox";
 import { PDFBox } from "./PDFBox";
 import { VideoBox } from "./VideoBox";
@@ -103,7 +104,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     render() {
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
-            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
+            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, LinkButtonBox }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index e98392a18..c424384a4 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -462,6 +462,17 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         var scaling = this.props.ContentScaling();
         var nativeHeight = this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
+
+        // // for linkbutton docs
+        // let isLinkButton = BoolCast(this.props.Document.isLinkButton);
+        // let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
+        // let display = isLinkButton ? activeDvs.reduce((found, dv) => {
+        //     let matchSv = this.props.Document.sourceViewId === StrCast(dv.props.Document[Id]);
+        //     let matchTv = this.props.Document.targetViewId === StrCast(dv.props.Document[Id]);
+        //     let match = matchSv || matchTv;
+        //     return match || found;
+        // }, false) : true;
+
         return (
             <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
@@ -476,7 +487,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     background: this.Document.backgroundColor || "",
                     width: nativeWidth,
                     height: nativeHeight,
-                    transform: `scale(${scaling}, ${scaling})`
+                    transform: `scale(${scaling}, ${scaling})`,
+                    // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
 
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 5a83de8e3..6fecb34d7 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,6 +18,8 @@ import { FormattedTextBox } from "./FormattedTextBox";
 import { IconBox } from "./IconBox";
 import { ImageBox } from "./ImageBox";
 import { VideoBox } from "./VideoBox";
+import { LinkButtonBox } from "./LinkButtonBox";
+import { LinkButtonField } from "../../../new_fields/LinkButtonField";
 
 
 //
@@ -49,6 +51,7 @@ export interface FieldViewProps {
 @observer
 export class FieldView extends React.Component<FieldViewProps> {
     public static LayoutString(fieldType: { name: string }, fieldStr: string = "data") {
+        console.log("LAYOUT STRING", fieldType.name, fieldStr);
         return `<${fieldType.name} {...props} fieldKey={"${fieldStr}"} />`;
     }
 
@@ -74,6 +77,9 @@ export class FieldView extends React.Component<FieldViewProps> {
         else if (field instanceof IconField) {
             return <IconBox {...this.props} />;
         }
+        else if (field instanceof LinkButtonField) {
+            return <LinkButtonBox {...this.props} />;
+        }
         else if (field instanceof VideoField) {
             return <VideoBox {...this.props} />;
         }
diff --git a/src/client/views/nodes/LinkBox.scss b/src/client/views/nodes/LinkBox.scss
deleted file mode 100644
index 77462f611..000000000
--- a/src/client/views/nodes/LinkBox.scss
+++ /dev/null
@@ -1,76 +0,0 @@
-@import "../globalCssVariables";
-.linkMenu-item {
-    border-top: 0.5px solid $main-accent; 
-    padding: 6px;
-    position: relative;
-    display: flex;
-    font-size: 12px;
-
-    .linkMenu-item-content {
-        width: 100%;
-    }
-    
-    .link-metadata {
-        margin-top: 6px;
-        padding: 3px;
-        border-top: 0.5px solid $light-color-secondary;
-        .link-metadata-row {
-            margin-left: 6px;
-        }
-    }
-
-    &:last-child {
-        border-bottom: 0.5px solid $main-accent;
-    }
-    &:hover {
-        .linkMenu-item-buttons {
-            display: flex;
-        }
-        .linkMenu-item-content {
-            &.expand-two {
-                width: calc(100% - 46px);
-            }
-            &.expand-three {
-                width: calc(100% - 78px);
-            }
-        }
-    }
-}
-
-.linkMenu-item-buttons {
-    display: none;
-    position: absolute;
-    top: 50%;
-    right: 0;
-    transform: translateY(-50%);
-
-    .button {
-        width: 20px;
-        height: 20px;
-        margin: 0;
-        margin-right: 6px;
-        border-radius: 50%;
-        cursor: pointer;
-        pointer-events: auto;
-        background-color: $dark-color;
-        color: $light-color;
-        font-size: 65%;
-        transition: transform 0.2s;
-        text-align: center;
-        position: relative;
-
-        .fa-icon {
-            position: absolute;
-            top: 50%;
-            left: 50%;
-            transform: translate(-50%, -50%);
-        }
-
-        &:last-child {
-            margin-right: 0;
-        }
-        &:hover {
-            background: $main-accent;
-        }
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
deleted file mode 100644
index 8d07547ed..000000000
--- a/src/client/views/nodes/LinkBox.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { observer } from "mobx-react";
-import { DocumentManager } from "../../util/DocumentManager";
-import { undoBatch } from "../../util/UndoManager";
-import './LinkBox.scss';
-import React = require("react");
-import { Doc } from '../../../new_fields/Doc';
-import { StrCast, Cast } from '../../../new_fields/Types';
-import { observable, action } from 'mobx';
-import { LinkManager } from '../../util/LinkManager';
-import { DragLinksAsDocuments, DragLinkAsDocument } from '../../util/DragManager';
-import { SelectionManager } from '../../util/SelectionManager';
-library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
-
-
-interface Props {
-    groupType: string;
-    linkDoc: Doc;
-    sourceDoc: Doc;
-    destinationDoc: Doc;
-    showEditor: () => void;
-}
-
-@observer
-export class LinkBox extends React.Component<Props> {
-    private _drag = React.createRef<HTMLDivElement>();
-    @observable private _showMore: boolean = false;
-    @action toggleShowMore() { this._showMore = !this._showMore; }
-
-    @undoBatch
-    onFollowLink = async (e: React.PointerEvent): Promise<void> => {
-        e.stopPropagation();
-        DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey);
-    }
-
-    onEdit = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        this.props.showEditor();
-    }
-
-    renderMetadata = (): JSX.Element => {
-        let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
-        let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
-        let groupDoc = index > -1 ? groups[index] : undefined;
-
-        let mdRows: Array<JSX.Element> = [];
-        if (groupDoc) {
-            let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
-            let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
-            mdRows = keys!.map(key => {
-                return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
-            });
-        }
-
-        return (<div className="link-metadata">{mdRows}</div>);
-    }
-
-    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._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
-            document.removeEventListener("pointermove", this.onLinkButtonMoved);
-            document.removeEventListener("pointerup", this.onLinkButtonUp);
-
-            DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
-        }
-        e.stopPropagation();
-    }
-
-    render() {
-
-        let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
-        let canExpand = keys ? keys.length > 0 : false;
-
-        return (
-            <div className="linkMenu-item">
-                <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
-                    <div className="link-name">
-                        <p>{StrCast(this.props.destinationDoc.title)}</p>
-                        <div className="linkMenu-item-buttons">
-                            {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>
-                            <div title="Follow link" className="button" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
-                        </div>
-                    </div>
-                    {this._showMore ? this.renderMetadata() : <></>}
-                </div>
-
-            </div >
-        );
-    }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss
new file mode 100644
index 000000000..24bfd2c9f
--- /dev/null
+++ b/src/client/views/nodes/LinkButtonBox.scss
@@ -0,0 +1,18 @@
+.linkBox-cont {
+    width: 200px;
+    height: 100px;
+    background-color: black;
+    text-align: center;
+    color: white;
+    padding: 10px;
+    border-radius: 5px;
+    position: relative;
+
+    .linkBox-cont-wrapper {
+        width: calc(100% - 20px);
+        position: absolute;
+        left: 50%;
+        top: 50%;
+        transform: translate(-50%, -50%);
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.tsx b/src/client/views/nodes/LinkButtonBox.tsx
new file mode 100644
index 000000000..8a7c1ed8b
--- /dev/null
+++ b/src/client/views/nodes/LinkButtonBox.tsx
@@ -0,0 +1,63 @@
+import React = require("react");
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { computed, observable, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { FieldView, FieldViewProps } from './FieldView';
+import "./LinkButtonBox.scss";
+import { DocumentView } from "./DocumentView";
+import { Doc } from "../../../new_fields/Doc";
+import { LinkButtonField } from "../../../new_fields/LinkButtonField";
+import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
+import { CollectionDockingView } from "../collections/CollectionDockingView";
+import { DocumentManager } from "../../util/DocumentManager";
+import { Id } from "../../../new_fields/FieldSymbols";
+
+library.add(faCaretUp);
+library.add(faObjectGroup);
+library.add(faStickyNote);
+library.add(faFilePdf);
+library.add(faFilm);
+
+@observer
+export class LinkButtonBox extends React.Component<FieldViewProps> {
+    public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
+
+    followLink = (): void => {
+        console.log("follow link???");
+        let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
+        let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
+        if (targetView && targetView.props.ContainingCollectionView) {
+            CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
+        }
+    }
+
+    render() {
+
+        let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
+        let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
+
+        let text = "Could not find link";
+        if (targetView) {
+            let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
+            text = "Link to " + StrCast(targetView.props.Document.title) + context;
+        }
+
+        let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
+        let display = activeDvs.reduce((found, dv) => {
+            let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
+            let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
+            let match = matchSv || matchTv;
+            return match || found;
+        }, false);
+
+        return (
+            <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
+                <div className="linkBox-cont-wrapper">
+                    <p>{text}</p>
+                </div>
+            </div >
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index aef56df67..7e4c15312 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -1,7 +1,7 @@
 import { action, observable } from "mobx";
 import { observer } from "mobx-react";
 import { DocumentView } from "./DocumentView";
-import { LinkBox } from "./LinkBox";
+import { LinkMenuItem } from "./LinkMenuItem";
 import { LinkEditor } from "./LinkEditor";
 import './LinkMenu.scss';
 import React = require("react");
@@ -23,7 +23,7 @@ export class LinkMenu extends React.Component<Props> {
         let source = this.props.docView.Document;
         return group.map(linkDoc => {
             let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, source);
-            return <LinkBox key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
+            return <LinkMenuItem key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
         });
     }
 
diff --git a/src/client/views/nodes/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss
new file mode 100644
index 000000000..77462f611
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.scss
@@ -0,0 +1,76 @@
+@import "../globalCssVariables";
+.linkMenu-item {
+    border-top: 0.5px solid $main-accent; 
+    padding: 6px;
+    position: relative;
+    display: flex;
+    font-size: 12px;
+
+    .linkMenu-item-content {
+        width: 100%;
+    }
+    
+    .link-metadata {
+        margin-top: 6px;
+        padding: 3px;
+        border-top: 0.5px solid $light-color-secondary;
+        .link-metadata-row {
+            margin-left: 6px;
+        }
+    }
+
+    &:last-child {
+        border-bottom: 0.5px solid $main-accent;
+    }
+    &:hover {
+        .linkMenu-item-buttons {
+            display: flex;
+        }
+        .linkMenu-item-content {
+            &.expand-two {
+                width: calc(100% - 46px);
+            }
+            &.expand-three {
+                width: calc(100% - 78px);
+            }
+        }
+    }
+}
+
+.linkMenu-item-buttons {
+    display: none;
+    position: absolute;
+    top: 50%;
+    right: 0;
+    transform: translateY(-50%);
+
+    .button {
+        width: 20px;
+        height: 20px;
+        margin: 0;
+        margin-right: 6px;
+        border-radius: 50%;
+        cursor: pointer;
+        pointer-events: auto;
+        background-color: $dark-color;
+        color: $light-color;
+        font-size: 65%;
+        transition: transform 0.2s;
+        text-align: center;
+        position: relative;
+
+        .fa-icon {
+            position: absolute;
+            top: 50%;
+            left: 50%;
+            transform: translate(-50%, -50%);
+        }
+
+        &:last-child {
+            margin-right: 0;
+        }
+        &:hover {
+            background: $main-accent;
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
new file mode 100644
index 000000000..c68365584
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -0,0 +1,107 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faEye, faTimes, faArrowRight, faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { observer } from "mobx-react";
+import { DocumentManager } from "../../util/DocumentManager";
+import { undoBatch } from "../../util/UndoManager";
+import './LinkMenuItem.scss';
+import React = require("react");
+import { Doc } from '../../../new_fields/Doc';
+import { StrCast, Cast } from '../../../new_fields/Types';
+import { observable, action } from 'mobx';
+import { LinkManager } from '../../util/LinkManager';
+import { DragLinksAsDocuments, DragLinkAsDocument } from '../../util/DragManager';
+import { SelectionManager } from '../../util/SelectionManager';
+library.add(faEye, faEdit, faTimes, faArrowRight, faChevronDown, faChevronUp);
+
+
+interface LinkMenuItemProps {
+    groupType: string;
+    linkDoc: Doc;
+    sourceDoc: Doc;
+    destinationDoc: Doc;
+    showEditor: () => void;
+}
+
+@observer
+export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
+    private _drag = React.createRef<HTMLDivElement>();
+    @observable private _showMore: boolean = false;
+    @action toggleShowMore() { this._showMore = !this._showMore; }
+
+    @undoBatch
+    onFollowLink = async (e: React.PointerEvent): Promise<void> => {
+        e.stopPropagation();
+        DocumentManager.Instance.jumpToDocument(this.props.destinationDoc, e.altKey);
+    }
+
+    onEdit = (e: React.PointerEvent): void => {
+        e.stopPropagation();
+        this.props.showEditor();
+    }
+
+    renderMetadata = (): JSX.Element => {
+        let groups = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+        let index = groups.findIndex(groupDoc => StrCast(groupDoc.type).toUpperCase() === this.props.groupType.toUpperCase());
+        let groupDoc = index > -1 ? groups[index] : undefined;
+
+        let mdRows: Array<JSX.Element> = [];
+        if (groupDoc) {
+            let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
+            let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
+            mdRows = keys!.map(key => {
+                return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
+            });
+        }
+
+        return (<div className="link-metadata">{mdRows}</div>);
+    }
+
+    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._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+            document.removeEventListener("pointermove", this.onLinkButtonMoved);
+            document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+            DragLinkAsDocument(this._drag.current, e.x, e.y, this.props.linkDoc, this.props.sourceDoc);
+        }
+        e.stopPropagation();
+    }
+
+    render() {
+
+        let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
+        let canExpand = keys ? keys.length > 0 : false;
+
+        return (
+            <div className="linkMenu-item">
+                <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
+                    <div className="link-name">
+                        <p>{StrCast(this.props.destinationDoc.title)}</p>
+                        <div className="linkMenu-item-buttons">
+                            {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>
+                            <div title="Follow link" className="button" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+                        </div>
+                    </div>
+                    {this._showMore ? this.renderMetadata() : <></>}
+                </div>
+
+            </div >
+        );
+    }
+}
\ No newline at end of file
diff --git a/src/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts
new file mode 100644
index 000000000..92e1ed922
--- /dev/null
+++ b/src/new_fields/LinkButtonField.ts
@@ -0,0 +1,35 @@
+import { Deserializable } from "../client/util/SerializationHelper";
+import { serializable, primitive, createSimpleSchema, object } from "serializr";
+import { ObjectField } from "./ObjectField";
+import { Copy, ToScriptString } from "./FieldSymbols";
+import { Doc } from "./Doc";
+import { DocumentView } from "../client/views/nodes/DocumentView";
+
+export type LinkButtonData = {
+    sourceViewId: string,
+    targetViewId: string
+};
+
+const LinkButtonSchema = createSimpleSchema({
+    sourceViewId: true,
+    targetViewId: true
+});
+
+@Deserializable("linkButton")
+export class LinkButtonField extends ObjectField {
+    @serializable(object(LinkButtonSchema))
+    readonly data: LinkButtonData;
+
+    constructor(data: LinkButtonData) {
+        super();
+        this.data = data;
+    }
+
+    [Copy]() {
+        return new LinkButtonField(this.data);
+    }
+
+    [ToScriptString]() {
+        return "invalid";
+    }
+}
-- 
cgit v1.2.3-70-g09d2


From 73f03785f938542a91b28b35043f2feda2bc1432 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 20 Jun 2019 11:26:33 -0400
Subject: merge

---
 .../collections/collectionFreeForm/CollectionFreeFormLinksView.tsx | 4 ++++
 src/client/views/nodes/DocumentView.tsx                            | 7 ++++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index bde68001b..3c2ccecfc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -167,12 +167,16 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
         let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
         connections.forEach(c => {
+
             // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
             // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
             let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
             let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
             let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
 
+            // console.log("CONNECTION", StrCast(c.anchor1View.props.Document.title), StrCast(c.anchor2View.props.Document.title), match1, match2);
+
+
             // if in same context, push if docview pair does not already exist
             // else push both directions of pair
             if (sameContext) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c424384a4..1d74b47a1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -447,6 +447,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
     onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
+    onDragOver = (e: React.DragEvent): void => {
+        this.props.Document.libraryBrush = true;
+        console.log("dragOver");
+    };
+    onDragLeave = (e: React.DragEvent): void => { this.props.Document.libraryBrush = false; };
 
     isSelected = () => SelectionManager.IsSelected(this);
     @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); }
@@ -491,7 +496,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-
+                onDragOver={this.onDragOver} onDragLeave={this.onDragLeave}
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
             >
                 {this.contents}
-- 
cgit v1.2.3-70-g09d2


From a0246ef84396545f79fc4a8b21de1a56cbf06aca Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Thu, 20 Jun 2019 11:34:28 -0400
Subject: merge

---
 .../collections/collectionFreeForm/CollectionFreeFormLinksView.tsx  | 6 ------
 src/client/views/nodes/DocumentView.tsx                             | 6 ------
 2 files changed, 12 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 211c90c29..3c2ccecfc 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -214,7 +214,6 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
                         // LinkManager.Instance.linkProxies.push(proxy);
                         LinkManager.Instance.addLinkProxy(proxy);
                     }
-<<<<<<< HEAD
                     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
 
                     // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
@@ -228,11 +227,6 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
                     // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
                     //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
-=======
-                    return match || found;
-                }, false)) {
-                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
->>>>>>> e9d62f4ca0dbeb57e46239047041a8a04da7b504
                 }
             }
         });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1d87205ab..dbbae2d59 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -31,12 +31,9 @@ import "./DocumentView.scss";
 import React = require("react");
 import { Id, Copy } from '../../../new_fields/FieldSymbols';
 import { ContextMenuProps } from '../ContextMenuItem';
-<<<<<<< HEAD
 import { list, object, createSimpleSchema } from 'serializr';
 import { LinkManager } from '../../util/LinkManager';
-=======
 import { RouteStore } from '../../../server/RouteStore';
->>>>>>> e9d62f4ca0dbeb57e46239047041a8a04da7b504
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 library.add(fa.faTrash);
@@ -556,7 +553,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     render() {
         var scaling = this.props.ContentScaling();
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
-<<<<<<< HEAD
 
         // // for linkbutton docs
         // let isLinkButton = BoolCast(this.props.Document.isLinkButton);
@@ -568,9 +564,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         //     return match || found;
         // }, false) : true;
 
-=======
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
->>>>>>> e9d62f4ca0dbeb57e46239047041a8a04da7b504
         return (
             <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
-- 
cgit v1.2.3-70-g09d2


From e495d4e32402384dbf7bb72945e83c912fce892b Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Thu, 20 Jun 2019 17:46:56 -0400
Subject: first shot at templates...

---
 src/client/views/MainOverlayTextBox.tsx            |  6 ++--
 src/client/views/MainView.tsx                      |  1 +
 .../views/collections/CollectionDockingView.tsx    |  4 ++-
 .../views/collections/CollectionSchemaView.tsx     |  3 +-
 .../views/collections/CollectionStackingView.tsx   |  4 ++-
 src/client/views/collections/CollectionView.tsx    | 37 ++++++++++++++++++++++
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  1 +
 src/client/views/nodes/DocumentContentsView.tsx    | 12 ++++---
 src/client/views/nodes/DocumentView.tsx            |  1 +
 src/client/views/nodes/FieldView.tsx               |  1 +
 src/client/views/nodes/FormattedTextBox.tsx        | 32 ++++++++++---------
 11 files changed, 77 insertions(+), 25 deletions(-)

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

diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index b4ad5f4d7..64271650b 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -37,7 +37,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
             (box?: FormattedTextBox) => {
                 this._textBox = box;
                 if (box) {
-                    this.TextDoc = box.props.Document;
+                    this.TextDoc = box.props.DataDoc;
                     let sxf = Utils.GetScreenTransform(box ? box.CurrentDiv : undefined);
                     let xf = () => { box.props.ScreenToLocalTransform(); return new Transform(-sxf.translateX, -sxf.translateY, 1 / sxf.scale); };
                     this.setTextDoc(box.props.fieldKey, box.CurrentDiv, xf, BoolCast(box.props.Document.autoHeight, false) || box.props.height === "min-content");
@@ -118,7 +118,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                 <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} hideOnLeave={this._textHideOnLeave} isOverlay={true} Document={FormattedTextBox.InputBoxOverlay.props.Document}
+                        <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true}
+                            Document={FormattedTextBox.InputBoxOverlay.props.Document}
+                            DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
                             isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true}
                             ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
                             ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} />
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 51630c29b..8198b88d2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -203,6 +203,7 @@ export class MainView extends React.Component {
         let mainCont = this.mainContainer;
         let content = !mainCont ? (null) :
             <DocumentView Document={mainCont}
+                DataDoc={mainCont}
                 addDocument={undefined}
                 addDocTab={emptyFunction}
                 removeDocument={undefined}
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 69b9e77eb..9a25e090c 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -492,7 +492,9 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
         return (
             <div className="collectionDockingView-content" ref={this._mainCont}
                 style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
-                <DocumentView key={this._document[Id]} Document={this._document}
+                <DocumentView key={this._document[Id]}
+                    Document={this._document}
+                    DataDoc={this._document}
                     bringToFront={emptyFunction}
                     addDocument={undefined}
                     removeDocument={undefined}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 9cc8961e3..c5b0527cc 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -98,6 +98,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
     renderCell = (rowProps: CellInfo) => {
         let props: FieldViewProps = {
             Document: rowProps.original,
+            DataDoc: rowProps.original,
             fieldKey: rowProps.column.id as string,
             ContainingCollectionView: this.props.CollectionView,
             isSelected: returnFalse,
@@ -423,7 +424,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
         return (<div className="collectionSchemaView-previewRegion" style={{ width: this.props.width(), height: "100%" }}>
             {!this.props.Document || !this.props.width ? (null) : (
                 <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}>
-                    <DocumentView Document={this.props.Document} isTopMost={false} selectOnLoad={false}
+                    <DocumentView DataDoc={this.props.Document} Document={this.props.Document} isTopMost={false} selectOnLoad={false}
                         addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument}
                         ScreenToLocalTransform={this.getTransform}
                         ContentScaling={this.contentScaling}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 368e94a8c..8e61181f4 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -119,7 +119,9 @@ export class CollectionStackingView extends CollectionSubView(doc => doc) {
                     gridColumnEnd: `span 1`,
                     transform: `scale(${renderScale})`
                 }} >
-                <DocumentView key={d[Id]} Document={d}
+                <DocumentView key={d[Id]}
+                    Document={d}
+                    DataDoc={d}
                     addDocument={this.props.addDocument}
                     removeDocument={this.props.removeDocument}
                     moveDocument={this.moveDocument}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 68eefab4c..6d3301ce4 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -14,6 +14,10 @@ import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormV
 import { CollectionSchemaView } from "./CollectionSchemaView";
 import { CollectionStackingView } from './CollectionStackingView';
 import { CollectionTreeView } from "./CollectionTreeView";
+import { Doc } from '../../../new_fields/Doc';
+import { FormattedTextBox } from '../nodes/FormattedTextBox';
+import { Docs } from '../../documents/Documents';
+import { List } from '../../../new_fields/List';
 export const COLLECTION_BORDER_WIDTH = 2;
 
 library.add(faTh);
@@ -54,6 +58,39 @@ export class CollectionView extends React.Component<FieldViewProps> {
             subItems.push({ description: "Treeview", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Tree), icon: "tree" });
             subItems.push({ description: "Stacking", event: undoBatch(() => this.props.Document.viewType = CollectionViewType.Stacking), icon: "th-list" });
             ContextMenu.Instance.addItem({ description: "View Modes...", subitems: subItems });
+            ContextMenu.Instance.addItem({
+                description: "Add Description Template", event: undoBatch(() => {
+                    Doc.GetProto(this.props.Document).description = "my first templated box";
+                    let template = Doc.MakeAlias(this.props.Document);
+                    template.layout = FormattedTextBox.LayoutString("description");
+                    template.x = 0;
+                    template.y = 0;
+                    template.width = 100;
+                    template.height = 25;
+                    Doc.AddDocToList(this.props.Document, "data", template);
+                }), icon: "project-diagram"
+            });
+            ContextMenu.Instance.addItem({
+                description: "Add Summary Template", event: undoBatch(() => {
+                    Doc.GetProto(this.props.Document).summary = "my first templated box";
+                    let template = Doc.MakeAlias(this.props.Document);
+                    template.layout = FormattedTextBox.LayoutString("summary");
+                    template.x = 0;
+                    template.y = 0;
+                    template.width = 100;
+                    template.height = 25;
+                    Doc.AddDocToList(this.props.Document, "data", template);
+                }), icon: "project-diagram"
+            });
+            ContextMenu.Instance.addItem({
+                description: "Apply Template", event: undoBatch(() => {
+                    let otherdoc = Docs.TextDocument({ width: 100, height: 50, title: "applied template" });
+                    Doc.GetProto(otherdoc).description = "THIS DESCRIPTION IS REALLY IMPORTANT!";
+                    Doc.GetProto(otherdoc).summary = "THIS SUMMARY IS MEANINGFUL!";
+                    Doc.GetProto(otherdoc).layout = this.props.Document;
+                    this.props.addDocTab && this.props.addDocTab(otherdoc, "onRight");
+                }), icon: "project-diagram"
+            });
         }
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 84841e469..013c11837 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -296,6 +296,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     getDocumentViewProps(document: Doc): DocumentViewProps {
         return {
             Document: document,
+            DataDoc: this.props.DataDoc,
             addDocument: this.props.addDocument,
             removeDocument: this.props.removeDocument,
             moveDocument: this.props.moveDocument,
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 02396c3af..a4316c143 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -23,6 +23,7 @@ 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";
 const JsxParser = require('react-jsx-parser').default; //TODO Why does this need to be imported like this?
 
 type BindingProps = Without<FieldViewProps, 'fieldKey'>;
@@ -47,11 +48,12 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     hideOnLeave?: boolean
 }> {
     @computed get layout(): string {
-        const layout = Cast(this.props.Document[this.props.layoutKey], "string");
+        let layoutDoc = this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document;
+        const layout = Cast(layoutDoc[this.props.layoutKey], "string");
         if (layout === undefined) {
             return this.props.Document.data ?
                 "<FieldView {...props} fieldKey='data' />" :
-                KeyValueBox.LayoutString(this.props.Document.proto ? "proto" : "");
+                KeyValueBox.LayoutString(layoutDoc.proto ? "proto" : "");
         } else if (typeof layout === "string") {
             return layout;
         } else {
@@ -59,8 +61,8 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         }
     }
 
-    CreateBindings(): JsxBindings {
-        return { props: OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit };
+    CreateBindings(layoutDoc?: Doc): JsxBindings {
+        return { props: { ...OmitKeys(this.props, ['parentActive'], (obj: any) => obj.active = this.props.parentActive).omit, Document: layoutDoc } };
     }
 
     @computed get templates(): List<string> {
@@ -104,7 +106,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
             components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
-            bindings={this.CreateBindings()}
+            bindings={this.CreateBindings(this.props.Document.layout instanceof Doc ? this.props.Document.layout : this.props.Document)}
             jsx={this.finalLayout}
             showWarnings={true}
             onError={(test: any) => { console.log(test); }}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 280804630..93b883533 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -67,6 +67,7 @@ const LinkDoc = makeInterface(linkSchema);
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     Document: Doc;
+    DataDoc: Doc;
     addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
     removeDocument?: (doc: Doc) => boolean;
     moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 1f1582f22..35608af86 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,6 +30,7 @@ export interface FieldViewProps {
     fieldKey: string;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     Document: Doc;
+    DataDoc: Doc;
     isSelected: () => boolean;
     select: (isCtrlPressed: boolean) => void;
     isTopMost: boolean;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 376b5a574..8231cc089 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,6 +1,6 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faEdit, faSmile } from '@fortawesome/free-solid-svg-icons';
-import { action, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, IReactionDisposer, observable, reaction, runInAction, computed } from "mobx";
 import { observer } from "mobx-react";
 import { baseKeymap } from "prosemirror-commands";
 import { history } from "prosemirror-history";
@@ -104,19 +104,21 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
+    @computed get dataDoc() { return this.props.DataDoc; }
+
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
             const state = this._editorView.state.apply(tx);
             this._editorView.updateState(state);
             this._applyingChange = true;
-            Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
-            Doc.SetOnPrototype(this.props.Document, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+            Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
+            Doc.SetOnPrototype(this.dataDoc, "documentText", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
             this._applyingChange = false;
-            let title = StrCast(this.props.Document.title);
+            let title = StrCast(this.dataDoc.title);
             if (title && title.startsWith("-") && this._editorView) {
                 let str = this._editorView.state.doc.textContent;
                 let titlestr = str.substr(0, Math.min(40, str.length));
-                let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+                let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc;
                 target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
             }
         }
@@ -144,14 +146,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
             e.stopPropagation();
         } else {
             if (de.data instanceof DragManager.DocumentDragData) {
-                let ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc));
+                let ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
                 if (!ldocs) {
-                    this.props.Document.subBulletDocs = new List<Doc>([]);
+                    this.dataDoc.subBulletDocs = new List<Doc>([]);
                 }
-                ldocs = Cast(this.props.Document.subBulletDocs, listSpec(Doc));
+                ldocs = Cast(this.dataDoc.subBulletDocs, listSpec(Doc));
                 if (!ldocs) return;
                 if (!ldocs || !ldocs[0] || ldocs[0] instanceof Promise || StrCast((ldocs[0] as Doc).layout).indexOf("CollectionView") === -1) {
-                    ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.props.Document.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 }));
+                    ldocs.splice(0, 0, Docs.StackingDocument([], { title: StrCast(this.dataDoc.title) + "-subBullets", x: NumCast(this.props.Document.x), y: NumCast(this.props.Document.y) + NumCast(this.props.Document.height), width: 300, height: 300 }));
                     this.props.addDocument && this.props.addDocument(ldocs[0] as Doc);
                     this.props.Document.templates = new List<string>([Templates.Bullet.Layout]);
                     this.props.Document.isBullet = true;
@@ -201,13 +203,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
 
         this._reactionDisposer = reaction(
             () => {
-                const field = this.props.Document ? Cast(this.props.Document[this.props.fieldKey], RichTextField) : undefined;
+                const field = this.dataDoc ? Cast(this.dataDoc[this.props.fieldKey], RichTextField) : undefined;
                 return field ? field.Data : `{"doc":{"type":"doc","content":[]},"selection":{"type":"text","anchor":0,"head":0}}`;
             },
             field => this._editorView && !this._applyingChange &&
                 this._editorView.updateState(EditorState.fromJSON(config, JSON.parse(field)))
         );
-        this.setupEditor(config, this.props.Document, this.props.fieldKey);
+        this.setupEditor(config, this.dataDoc, this.props.fieldKey);
     }
 
     private setupEditor(config: any, doc: Doc, fieldKey: string) {
@@ -360,10 +362,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         // stop propagation doesn't seem to stop propagation of native keyboard events.
         // so we set a flag on the native event that marks that the event's been handled.
         (e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
-        if (StrCast(this.props.Document.title).startsWith("-") && this._editorView) {
+        if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView) {
             let str = this._editorView.state.doc.textContent;
             let titlestr = str.substr(0, Math.min(40, str.length));
-            let target = this.props.Document.proto ? this.props.Document.proto : this.props.Document;
+            let target = this.dataDoc.proto ? this.dataDoc.proto : this.dataDoc;
             target.title = "-" + titlestr + (str.length > 40 ? "..." : "");
         }
         if (!this._undoTyping) {
@@ -372,11 +374,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         if (this.props.isOverlay && this.props.Document.autoHeight) {
             let xf = this._ref.current!.getBoundingClientRect();
             let scrBounds = this.props.ScreenToLocalTransform().transformBounds(0, 0, xf.width, xf.height);
-            let nh = NumCast(this.props.Document.nativeHeight, 0);
+            let nh = NumCast(this.dataDoc.nativeHeight, 0);
             let dh = NumCast(this.props.Document.height, 0);
             let sh = scrBounds.height;
             this.props.Document.height = nh ? dh / nh * sh : sh;
-            this.props.Document.proto!.nativeHeight = nh ? sh : undefined;
+            this.dataDoc.proto!.nativeHeight = nh ? sh : undefined;
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 542501fc3247dafa5879482f62a28147ec4b3793 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Fri, 21 Jun 2019 13:13:18 -0400
Subject: lots more template stuff.

---
 src/client/util/DocumentManager.ts                 |   6 +-
 src/client/util/DragManager.ts                     |  18 ++--
 src/client/views/DocumentDecorations.tsx           |  35 ++++----
 src/client/views/MainOverlayTextBox.tsx            |   8 +-
 .../views/collections/CollectionDockingView.tsx    | 100 +++++++++++----------
 .../views/collections/CollectionSchemaView.tsx     |   2 +-
 .../views/collections/CollectionTreeView.tsx       |  33 +++++--
 src/client/views/collections/CollectionView.tsx    |   4 +-
 .../views/collections/ParentDocumentSelector.tsx   |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  27 +++---
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  43 +++++----
 src/client/views/pdf/PDFViewer.tsx                 |   1 -
 src/new_fields/Doc.ts                              |   8 ++
 15 files changed, 174 insertions(+), 119 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 862395d74..8421c902e 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -138,7 +138,7 @@ export class DocumentManager {
                         docContext.panTransformType = "Ease";
                         targetContextView.props.focus(docDelegate);
                     } else {
-                        (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext);
+                        (dockFunc || CollectionDockingView.Instance.AddRightSplit)(docContext, docContext);
                         setTimeout(() => {
                             this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage);
                         }, 10);
@@ -147,7 +147,7 @@ export class DocumentManager {
                     const actualDoc = Doc.MakeAlias(docDelegate);
                     actualDoc.libraryBrush = true;
                     if (linkPage !== undefined) actualDoc.curPage = linkPage;
-                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc);
+                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(actualDoc, actualDoc);
                 }
             } else {
                 let contextView: DocumentView | null;
@@ -156,7 +156,7 @@ export class DocumentManager {
                     contextDoc.panTransformType = "Ease";
                     contextView.props.focus(docDelegate);
                 } else {
-                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc);
+                    (dockFunc || CollectionDockingView.Instance.AddRightSplit)(contextDoc, contextDoc);
                     setTimeout(() => {
                         this.jumpToDocument(docDelegate, forceDockFunc, dockFunc, linkPage);
                     }, 10);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index c3c92daa5..cc83b7346 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -15,7 +15,8 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
 
         document.removeEventListener("pointermove", onRowMove);
         document.removeEventListener('pointerup', onRowUp);
-        var dragData = new DragManager.DocumentDragData([await docFunc()]);
+        let doc = await docFunc();
+        var dragData = new DragManager.DocumentDragData([doc], [doc]);
         dragData.dropAction = dropAction;
         dragData.moveDocument = moveFunc;
         dragData.options = options;
@@ -31,7 +32,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
         if (e.button === 0) {
             e.stopPropagation();
             if (e.shiftKey && CollectionDockingView.Instance) {
-                CollectionDockingView.Instance.StartOtherDrag([await docFunc()], e);
+                CollectionDockingView.Instance.StartOtherDrag(e, [await docFunc()]);
             } else {
                 document.addEventListener("pointermove", onRowMove);
                 document.addEventListener("pointerup", onRowUp);
@@ -59,7 +60,8 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
             let doc = await Cast(draggedDoc.annotationOn, Doc);
             if (doc) moddrag.push(doc);
         }
-        let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
+        let linkDocs = moddrag.length ? moddrag : draggedDocs;
+        let dragData = new DragManager.DocumentDragData(linkDocs, linkDocs); // bcz: dataDocs?
         DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
                 dragComplete: action(emptyFunction),
@@ -141,13 +143,15 @@ export namespace DragManager {
 
     export type MoveFunction = (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     export class DocumentDragData {
-        constructor(dragDoc: Doc[]) {
+        constructor(dragDoc: Doc[], dragDataDocs: Doc[]) {
             this.draggedDocuments = dragDoc;
+            this.draggedDataDocs = dragDataDocs;
             this.droppedDocuments = dragDoc;
             this.xOffset = 0;
             this.yOffset = 0;
         }
         draggedDocuments: Doc[];
+        draggedDataDocs: Doc[];
         droppedDocuments: Doc[];
         xOffset: number;
         yOffset: number;
@@ -238,6 +242,8 @@ export namespace DragManager {
 
         const docs: Doc[] =
             dragData instanceof DocumentDragData ? dragData.draggedDocuments : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
+        const datadocs: Doc[] =
+            dragData instanceof DocumentDragData ? dragData.draggedDataDocs : dragData instanceof AnnotationDragData ? [dragData.dragDocument] : [];
         let dragElements = eles.map(ele => {
             const w = ele.offsetWidth,
                 h = ele.offsetHeight;
@@ -314,12 +320,12 @@ export namespace DragManager {
             }
             if (e.shiftKey && CollectionDockingView.Instance) {
                 AbortDrag();
-                CollectionDockingView.Instance.StartOtherDrag(docs, {
+                CollectionDockingView.Instance.StartOtherDrag({
                     pageX: e.pageX,
                     pageY: e.pageY,
                     preventDefault: emptyFunction,
                     button: 0
-                });
+                }, docs, datadocs);
             }
             //TODO: Why can't we use e.movementX and e.movementY?
             let moveX = e.pageX - lastX;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3a366b4d2..8f02f04d7 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -166,7 +166,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let dragDocView = SelectionManager.SelectedDocuments()[0];
         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));
+        let dragData = new DragManager.DocumentDragData(SelectionManager.SelectedDocuments().map(dv => dv.props.Document), SelectionManager.SelectedDocuments().map(dv => dv.props.DataDoc));
         dragData.xOffset = xoff;
         dragData.yOffset = yoff;
         dragData.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
@@ -485,9 +485,8 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 let doc = PositionDocument(element.props.Document);
                 let nwidth = doc.nativeWidth || 0;
                 let nheight = doc.nativeHeight || 0;
-                let zoomBasis = NumCast(doc.zoomBasis, 1);
-                let width = (doc.width || 0) / zoomBasis;
-                let height = (doc.height || (nheight / nwidth * width)) / zoomBasis;
+                let width = (doc.width || 0);
+                let height = (doc.height || (nheight / nwidth * width));
                 let scale = element.props.ScreenToLocalTransform().Scale;
                 let actualdW = Math.max(width + (dW * scale), 20);
                 let actualdH = Math.max(height + (dH * scale), 20);
@@ -502,25 +501,27 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
                 }
                 if (nwidth > 0 && nheight > 0) {
                     if (Math.abs(dW) > Math.abs(dH)) {
-                        if (!fixedAspect) proto.nativeWidth = zoomBasis * actualdW / (doc.width || 1) * NumCast(proto.nativeWidth);
-                        doc.width = zoomBasis * actualdW;
-                        // doc.zoomBasis = zoomBasis * width / actualdW;
+                        if (!fixedAspect) {
+                            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;
-                        else doc.height = zoomBasis * actualdH;
-                        proto.nativeHeight = (doc.height || 0) / doc.width * NumCast(proto.nativeWidth);
+                        else doc.height = actualdH;
+                        Doc.SetInPlace(element.props.Document, "nativeHeight", (doc.height || 0) / doc.width * (doc.nativeWidth || 0), true);
                     }
                     else {
-                        if (!fixedAspect) proto.nativeHeight = zoomBasis * actualdH / (doc.height || 1) * NumCast(proto.nativeHeight);
-                        doc.height = zoomBasis * actualdH;
-                        //doc.zoomBasis = zoomBasis * height / actualdH;
+                        if (!fixedAspect) {
+                            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;
-                        else doc.width = zoomBasis * actualdW;
-                        proto.nativeWidth = (doc.width || 0) / doc.height * NumCast(proto.nativeHeight);
+                        else doc.width = actualdW;
+                        Doc.SetInPlace(element.props.Document, "nativeWidth", (doc.width || 0) / doc.height * (doc.nativeHeight || 0), true);
                     }
                 } else {
-                    dW && (doc.width = zoomBasis * actualdW);
-                    dH && (doc.height = zoomBasis * actualdH);
-                    proto.autoHeight = undefined;
+                    dW && (doc.width = actualdW);
+                    dH && (doc.height = actualdH);
+                    Doc.SetInPlace(element.props.Document, "autoHeight", undefined, true);
                 }
             }
         });
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index 64271650b..73d59ce31 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -82,10 +82,10 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
     }
     @action
     textBoxMove = (e: PointerEvent) => {
-        if (e.movementX > 1 || e.movementY > 1) {
+        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 ? [FormattedTextBox.InputBoxOverlay.props.Document] : []);
+            let dragData = new DragManager.DocumentDragData([FormattedTextBox.InputBoxOverlay.props.Document], [FormattedTextBox.InputBoxOverlay.props.DataDoc]);
             const [left, top] = this._textXf().inverse().transformPoint(0, 0);
             dragData.xOffset = e.clientX - left;
             dragData.yOffset = e.clientY - top;
@@ -102,9 +102,9 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
         document.removeEventListener('pointerup', this.textBoxUp);
     }
 
-    addDocTab = (doc: Doc, location: string) => {
+    addDocTab = (doc: Doc, dataDoc: Doc, location: string) => {
         if (true) { // location === "onRight") { need to figure out stack to add "inTab"
-            CollectionDockingView.Instance.AddRightSplit(doc);
+            CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
         }
     }
     render() {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 79a0d18df..89fadab57 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -28,7 +28,7 @@ import { MainView } from '../MainView';
 @observer
 export class CollectionDockingView extends React.Component<SubCollectionViewProps> {
     public static Instance: CollectionDockingView;
-    public static makeDocumentConfig(document: Doc, width?: number) {
+    public static makeDocumentConfig(document: Doc, dataDoc: Doc, width?: number) {
         return {
             type: 'react-component',
             component: 'DocumentFrameRenderer',
@@ -36,6 +36,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             width: width,
             props: {
                 documentId: document[Id],
+                dataDocumentId: dataDoc[Id]
                 //collectionDockingView: CollectionDockingView.Instance
             }
         };
@@ -56,19 +57,19 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
     }
     hack: boolean = false;
     undohack: any = null;
-    public StartOtherDrag(dragDocs: Doc[], e: any) {
+    public StartOtherDrag(e: any, dragDocs: Doc[], dragDataDocs?: Doc[]) {
         this.hack = true;
         this.undohack = UndoManager.StartBatch("goldenDrag");
-        dragDocs.map(dragDoc =>
-            this.AddRightSplit(dragDoc, true).contentItems[0].tab._dragListener.
+        dragDocs.map((dragDoc, i) =>
+            this.AddRightSplit(dragDoc, dragDataDocs ? dragDataDocs[i] : dragDoc, true).contentItems[0].tab._dragListener.
                 onMouseDown({ pageX: e.pageX, pageY: e.pageY, preventDefault: emptyFunction, button: 0 }));
     }
 
     @action
-    public OpenFullScreen(document: Doc) {
+    public OpenFullScreen(document: Doc, dataDoc: Doc) {
         let newItemStackConfig = {
             type: 'stack',
-            content: [CollectionDockingView.makeDocumentConfig(document)]
+            content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
         };
         var docconfig = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
         this._goldenLayout.root.contentItems[0].addChild(docconfig);
@@ -123,14 +124,14 @@ 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
     //
     @action
-    public AddRightSplit = (document: Doc, minimize: boolean = false) => {
+    public AddRightSplit = (document: Doc, dataDoc: Doc, minimize: boolean = false) => {
         let docs = Cast(this.props.Document.data, listSpec(Doc));
         if (docs) {
             docs.push(document);
         }
         let newItemStackConfig = {
             type: 'stack',
-            content: [CollectionDockingView.makeDocumentConfig(document)]
+            content: [CollectionDockingView.makeDocumentConfig(document, dataDoc)]
         };
 
         var newContentItem = this._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, this._goldenLayout);
@@ -160,12 +161,12 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
         return newContentItem;
     }
     @action
-    public AddTab = (stack: any, document: Doc) => {
+    public AddTab = (stack: any, document: Doc, dataDocument: Doc) => {
         let docs = Cast(this.props.Document.data, listSpec(Doc));
         if (docs) {
             docs.push(document);
         }
-        let docContentConfig = CollectionDockingView.makeDocumentConfig(document);
+        let docContentConfig = CollectionDockingView.makeDocumentConfig(document, dataDocument);
         var newContentItem = stack.layoutManager.createContentItem(docContentConfig, this._goldenLayout);
         stack.addChild(newContentItem.contentItems[0], undefined);
         this.layoutChanged();
@@ -281,14 +282,16 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                 let x = e.clientX;
                 let y = e.clientY;
                 let docid = (e.target as any).DashDocId;
+                let datadocid = (e.target as any).DashDataDocId;
                 let tab = (e.target as any).parentElement as HTMLElement;
                 let glTab = (e.target as any).Tab;
                 if (glTab && glTab.contentItem && glTab.contentItem.parent) {
                     glTab.contentItem.parent.setActiveContentItem(glTab.contentItem);
                 }
-                DocServer.GetRefField(docid).then(action((f: Opt<Field>) => {
+                DocServer.GetRefField(docid).then(action(async (f: Opt<Field>) => {
                     if (f instanceof Doc) {
-                        DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f]), x, y,
+                        let dataDoc = (datadocid !== docid) ? await DocServer.GetRefField(datadocid) : f;
+                        DragManager.StartDocumentDrag([tab], new DragManager.DocumentDragData([f], [dataDoc instanceof Doc ? dataDoc : f]), x, y,
                             {
                                 handlers: {
                                     dragComplete: emptyFunction,
@@ -338,33 +341,34 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
             if (tab.contentItem.config.fixed) {
                 tab.contentItem.parent.config.fixed = true;
             }
-            DocServer.GetRefField(tab.contentItem.config.props.documentId).then(async doc => {
-                if (doc instanceof Doc) {
-                    let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
-                    tab.element.append(counter);
-                    let upDiv = document.createElement("span");
-                    const stack = tab.contentItem.parent;
-                    // shifts the focus to this tab when another tab is dragged over it
-                    tab.element[0].onmouseenter = (e: any) => {
-                        if (!this._isPointerDown) return;
-                        var activeContentItem = tab.header.parent.getActiveContentItem();
-                        if (tab.contentItem !== activeContentItem) {
-                            tab.header.parent.setActiveContentItem(tab.contentItem);
-                        }
-                        tab.setActive(true);
-                    };
-                    ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, location) => CollectionDockingView.Instance.AddTab(stack, doc)} />, upDiv);
-                    tab.reactComponents = [upDiv];
-                    tab.element.append(upDiv);
-                    counter.DashDocId = tab.contentItem.config.props.documentId;
-                    tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
-                        () => {
-                            counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
-                            tab.titleElement[0].textContent = doc.title;
-                        }, { fireImmediately: true });
-                    tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
-                }
-            });
+            let doc = await DocServer.GetRefField(tab.contentItem.config.props.documentId) as Doc;
+            let dataDoc = await DocServer.GetRefField(tab.contentItem.config.props.dataDocumentId) as Doc;
+            if (doc instanceof Doc && dataDoc instanceof Doc) {
+                let counter: any = this.htmlToElement(`<span class="messageCounter">0</div>`);
+                tab.element.append(counter);
+                let upDiv = document.createElement("span");
+                const stack = tab.contentItem.parent;
+                // shifts the focus to this tab when another tab is dragged over it
+                tab.element[0].onmouseenter = (e: any) => {
+                    if (!this._isPointerDown) return;
+                    var activeContentItem = tab.header.parent.getActiveContentItem();
+                    if (tab.contentItem !== activeContentItem) {
+                        tab.header.parent.setActiveContentItem(tab.contentItem);
+                    }
+                    tab.setActive(true);
+                };
+                ReactDOM.render(<ParentDocSelector Document={doc} addDocTab={(doc, dataDoc, location) => CollectionDockingView.Instance.AddTab(stack, doc, dataDoc)} />, upDiv);
+                tab.reactComponents = [upDiv];
+                tab.element.append(upDiv);
+                counter.DashDocId = tab.contentItem.config.props.documentId;
+                counter.DashDataDocId = tab.contentItem.config.props.dataDocumentId;
+                tab.reactionDisposer = reaction(() => [doc.linkedFromDocs, doc.LinkedToDocs, doc.title],
+                    () => {
+                        counter.innerHTML = DocListCast(doc.linkedFromDocs).length + DocListCast(doc.linkedToDocs).length;
+                        tab.titleElement[0].textContent = doc.title;
+                    }, { fireImmediately: true });
+                tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
+            }
         }
         tab.titleElement[0].Tab = tab;
         tab.closeElement.off('click') //unbind the current click handler
@@ -427,6 +431,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
 
 interface DockedFrameProps {
     documentId: FieldId;
+    dataDocumentId: FieldId;
     //collectionDockingView: CollectionDockingView
 }
 @observer
@@ -435,6 +440,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     @observable private _panelWidth = 0;
     @observable private _panelHeight = 0;
     @observable private _document: Opt<Doc>;
+    @observable private _dataDoc: Opt<Doc>;
     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) {
@@ -444,7 +450,11 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
     constructor(props: any) {
         super(props);
-        DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => this._document = f as Doc));
+        DocServer.GetRefField(this.props.documentId).then(action((f: Opt<Field>) => {
+            this._document = f as Doc;
+            if (!this.props.dataDocumentId || this.props.documentId === this.props.dataDocumentId) this._dataDoc = this._document;
+        }));
+        if (this.props.dataDocumentId && this.props.documentId !== this.props.dataDocumentId) DocServer.GetRefField(this.props.dataDocumentId).then(action((f: Opt<Field>) => this._dataDoc = f as Doc));
     }
 
     nativeWidth = () => NumCast(this._document!.nativeWidth, this._panelWidth);
@@ -481,17 +491,17 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
     }
     get previewPanelCenteringOffset() { return (this._panelWidth - this.nativeWidth() * this.contentScaling()) / 2; }
 
-    addDocTab = (doc: Doc, location: string) => {
+    addDocTab = (doc: Doc, dataDoc: Doc, location: string) => {
         if (doc.dockingConfig) {
             MainView.Instance.openWorkspace(doc);
         } else if (location === "onRight") {
-            CollectionDockingView.Instance.AddRightSplit(doc);
+            CollectionDockingView.Instance.AddRightSplit(doc, dataDoc);
         } else {
-            CollectionDockingView.Instance.AddTab(this._stack, doc);
+            CollectionDockingView.Instance.AddTab(this._stack, doc, dataDoc);
         }
     }
     get content() {
-        if (!this._document) {
+        if (!this._document || !this._dataDoc) {
             return (null);
         }
         return (
@@ -499,7 +509,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
                 style={{ transform: `translate(${this.previewPanelCenteringOffset}px, 0px) scale(${this.scaleToFitMultiplier}, ${this.scaleToFitMultiplier})` }}>
                 <DocumentView key={this._document[Id]}
                     Document={this._document}
-                    DataDoc={this._document}
+                    DataDoc={this._dataDoc}
                     bringToFront={emptyFunction}
                     addDocument={undefined}
                     removeDocument={undefined}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index d539835b9..9f5cef3cd 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -394,7 +394,7 @@ interface CollectionSchemaPreviewProps {
     removeDocument: (document: Doc) => boolean;
     active: () => boolean;
     whenActiveChanged: (isActive: boolean) => void;
-    addDocTab: (document: Doc, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc, where: string) => void;
     setPreviewScript: (script: string) => void;
     previewScript?: string;
 }
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 18d6751c6..83a7c9e3a 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -26,6 +26,8 @@ import { CollectionSubView } from "./CollectionSubView";
 import "./CollectionTreeView.scss";
 import React = require("react");
 import { FormattedTextBox } from '../nodes/FormattedTextBox';
+import { ImageField } from '../../../new_fields/URLField';
+import { ImageBox } from '../nodes/ImageBox';
 
 
 export interface TreeViewProps {
@@ -35,7 +37,7 @@ export interface TreeViewProps {
     deleteDoc: (doc: Doc) => boolean;
     moveDocument: DragManager.MoveFunction;
     dropAction: "alias" | "copy" | undefined;
-    addDocTab: (doc: Doc, where: string) => void;
+    addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void;
     panelWidth: () => number;
     panelHeight: () => number;
     addDocument: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean;
@@ -73,7 +75,7 @@ class TreeView extends React.Component<TreeViewProps> {
     }
 
     @undoBatch delete = () => this.props.deleteDoc(this.props.document);
-    @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "onRight");
+    @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.dataDoc, "onRight");
 
     onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
     onPointerEnter = (e: React.PointerEvent): void => {
@@ -148,6 +150,25 @@ class TreeView extends React.Component<TreeViewProps> {
         SetValue={(value: string) => {
             let res = (Doc.GetProto(this.props.document)[key] = value) ? true : true;
 
+            if (value.startsWith(">>")) {
+                let metaKey = value.slice(2, value.length);
+                let collection = this.props.containingCollection;
+                Doc.GetProto(collection)[metaKey] = new ImageField("http://www.cs.brown.edu/~bcz/face.gif");
+                let template = Doc.MakeAlias(collection);
+                template.title = metaKey;
+                template.embed = true;
+                template.layout = ImageBox.LayoutString(metaKey);
+                template.x = 0;
+                template.y = 0;
+                template.nativeWidth = 300;
+                template.nativeHeight = 300;
+                template.width = 300;
+                template.height = 300;
+                template.isTemplate = true;
+                template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]);
+                Doc.AddDocToList(collection, "data", template);
+                this.delete();
+            }
             if (value.startsWith(">")) {
                 let metaKey = value.slice(1, value.length);
                 let collection = this.props.containingCollection;
@@ -236,10 +257,10 @@ class TreeView extends React.Component<TreeViewProps> {
     onWorkspaceContextMenu = (e: React.MouseEvent): void => {
         if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
             ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.KVPDocument(this.props.document, { width: 300, height: 300 }), "onRight"), icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" });
             if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
-                ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, "inTab"), icon: "folder" });
-                ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, "onRight"), icon: "caret-square-right" });
+                ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.props.dataDoc, "inTab"), icon: "folder" });
+                ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.props.dataDoc, "onRight"), icon: "caret-square-right" });
                 if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
                     ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
                 }
@@ -339,7 +360,7 @@ class TreeView extends React.Component<TreeViewProps> {
         remove: ((doc: Doc) => boolean),
         move: DragManager.MoveFunction,
         dropAction: dropActionType,
-        addDocTab: (doc: Doc, where: string) => void,
+        addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void,
         screenToLocalXf: () => Transform,
         outerXf: () => { translateX: number, translateY: number },
         active: () => boolean,
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 89e5ad74c..10e6fb885 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -18,6 +18,7 @@ import { Doc } from '../../../new_fields/Doc';
 import { FormattedTextBox } from '../nodes/FormattedTextBox';
 import { Docs } from '../../documents/Documents';
 import { List } from '../../../new_fields/List';
+import { ImageField } from '../../../new_fields/URLField';
 export const COLLECTION_BORDER_WIDTH = 2;
 
 library.add(faTh);
@@ -63,8 +64,9 @@ export class CollectionView extends React.Component<FieldViewProps> {
                     let otherdoc = Docs.TextDocument({ width: 100, height: 50, title: "applied template" });
                     Doc.GetProto(otherdoc).description = "THIS DESCRIPTION IS REALLY IMPORTANT!";
                     Doc.GetProto(otherdoc).summary = "THIS SUMMARY IS MEANINGFUL!";
+                    Doc.GetProto(otherdoc).photo = new ImageField("http://www.cs.brown.edu/~bcz/snowbeast.JPG");
                     Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document);
-                    this.props.addDocTab && this.props.addDocTab(otherdoc, "onRight");
+                    this.props.addDocTab && this.props.addDocTab(otherdoc, otherdoc, "onRight");
                 }), icon: "project-diagram"
             });
         }
diff --git a/src/client/views/collections/ParentDocumentSelector.tsx b/src/client/views/collections/ParentDocumentSelector.tsx
index f11af04a3..fa7058d5a 100644
--- a/src/client/views/collections/ParentDocumentSelector.tsx
+++ b/src/client/views/collections/ParentDocumentSelector.tsx
@@ -9,7 +9,7 @@ import { CollectionDockingView } from "./CollectionDockingView";
 import { NumCast } from "../../../new_fields/Types";
 import { CollectionViewType } from "./CollectionBaseView";
 
-type SelectorProps = { Document: Doc, addDocTab(doc: Doc, location: string): void };
+type SelectorProps = { Document: Doc, addDocTab(doc: Doc, dataDoc: Doc, location: string): void };
 @observer
 export class SelectorContextMenu extends React.Component<SelectorProps> {
     @observable private _docs: { col: Doc, target: Doc }[] = [];
@@ -43,7 +43,7 @@ export class SelectorContextMenu extends React.Component<SelectorProps> {
                 col.panX = newPanX;
                 col.panY = newPanY;
             }
-            this.props.addDocTab(col, "inTab");
+            this.props.addDocTab(col, col, "inTab"); // bcz: dataDoc?
         };
     }
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index da50d0b53..08f58dcd6 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -81,7 +81,7 @@ export interface DocumentViewProps {
     parentActive: () => boolean;
     whenActiveChanged: (isActive: boolean) => void;
     bringToFront: (doc: Doc) => void;
-    addDocTab: (doc: Doc, where: string) => void;
+    addDocTab: (doc: Doc, dataDoc: Doc, where: string) => void;
     animateBetweenIcon?: (iconPos: number[], startTime: number, maximizing: boolean) => void;
 }
 
@@ -212,8 +212,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
         if (this._mainCont.current) {
             let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+            let alldataConnected = [this.props.DataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
             const [left, top] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(0, 0);
-            let dragData = new DragManager.DocumentDragData(allConnected);
+            let dragData = new DragManager.DocumentDragData(allConnected, alldataConnected);
             const [xoff, yoff] = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).transformDirection(x - left, y - top);
             dragData.dropAction = dropAction;
             dragData.xOffset = xoff;
@@ -268,7 +269,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         let altKey = e.altKey;
         let ctrlKey = e.ctrlKey;
         if (this._doubleTap && !this.props.isTopMost) {
-            this.props.addDocTab(this.props.Document, "inTab");
+            this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
             SelectionManager.DeselectAll();
             this.props.Document.libraryBrush = false;
         }
@@ -311,7 +312,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                         if (dataDocs) {
                             expandedDocs.forEach(maxDoc =>
                                 (!CollectionDockingView.Instance.CloseRightSplit(Doc.GetProto(maxDoc)) &&
-                                    this.props.addDocTab(getDispDoc(maxDoc), maxLocation)));
+                                    this.props.addDocTab(getDispDoc(maxDoc), getDispDoc(maxDoc), maxLocation)));
                         }
                     } else {
                         let scrpt = this.props.ScreenToLocalTransform().scale(this.props.ContentScaling()).inverse().transformPoint(NumCast(this.Document.width) / 2, NumCast(this.Document.height) / 2);
@@ -334,7 +335,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     if (!linkedFwdDocs.some(l => l instanceof Promise)) {
                         let maxLocation = StrCast(linkedFwdDocs[altKey ? 1 : 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, document => this.props.addDocTab(document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
+                        DocumentManager.Instance.jumpToDocument(linkedFwdDocs[altKey ? 1 : 0], ctrlKey, document => this.props.addDocTab(document, document, maxLocation), linkedFwdPage[altKey ? 1 : 0], targetContext);
                     }
                 }
             }
@@ -345,7 +346,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._downY = e.clientY;
         this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
         if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
-            CollectionDockingView.Instance.StartOtherDrag([Doc.MakeAlias(this.props.Document)], e);
+            CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.props.DataDoc]);
             e.stopPropagation();
         } else {
             if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.  
@@ -376,7 +377,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     }
 
     deleteClicked = (): void => { this.props.removeDocument && this.props.removeDocument(this.props.Document); };
-    fieldsClicked = (): void => { this.props.addDocTab(Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }), "onRight"); };
+    fieldsClicked = (): void => { let kvp = Docs.KVPDocument(this.props.Document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); };
     makeBtnClicked = (): void => {
         let doc = Doc.GetProto(this.props.Document);
         doc.isButton = !BoolCast(doc.isButton, false);
@@ -390,7 +391,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
     fullScreenClicked = (): void => {
-        CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeCopy(this.props.Document, false));
+        CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeAlias(this.props.Document), this.props.DataDoc);
         SelectionManager.DeselectAll();
     }
 
@@ -473,10 +474,10 @@ 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, "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, "onRight"), icon: "caret-square-right" });
-        subitems.push({ description: "Open Right Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), "onRight"), icon: "caret-square-right" });
+        subitems.push({ description: "Open Tab", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab"), icon: "folder" });
+        subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
+        subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.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.props.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" });
         cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });
@@ -486,7 +487,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         cm.addItem({
             description: "Find aliases", event: async () => {
                 const aliases = await SearchUtil.GetAliasesOfDocument(this.props.Document);
-                this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), "onRight");
+                this.props.addDocTab && this.props.addDocTab(Docs.SchemaDocument(["title"], aliases, {}), Docs.SchemaDocument(["title"], aliases, {}), "onRight"); // bcz: dataDoc?
             }, icon: "search"
         });
         cm.addItem({ description: "Center View", event: () => this.props.focus(this.props.Document), icon: "crosshairs" });
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 35608af86..8879da55f 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -36,7 +36,7 @@ export interface FieldViewProps {
     isTopMost: boolean;
     selectOnLoad: boolean;
     addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
-    addDocTab: (document: Doc, where: string) => void;
+    addDocTab: (document: Doc, dataDoc: Doc, where: string) => void;
     removeDocument?: (document: Doc) => boolean;
     moveDocument?: (document: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     ScreenToLocalTransform: () => Transform;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index f59af226d..39444fd6a 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -270,7 +270,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
                     this._linkClicked = href.replace(DocServer.prepend("/doc/"), "").split("?")[0];
                     if (this._linkClicked) {
                         DocServer.GetRefField(this._linkClicked).then(f => {
-                            (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, "inTab"));
+                            (f instanceof Doc) && DocumentManager.Instance.jumpToDocument(f, ctrlKey, document => this.props.addDocTab(document, document, "inTab"));
                         });
                         e.stopPropagation();
                         e.preventDefault();
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index f56a2d926..241a593b0 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -1,13 +1,13 @@
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faImage } from '@fortawesome/free-solid-svg-icons';
-import { action, observable } from 'mobx';
+import { action, observable, computed } from 'mobx';
 import { observer } from "mobx-react";
 import Lightbox from 'react-image-lightbox';
 import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
 import { Doc, HeightSym, WidthSym } from '../../../new_fields/Doc';
 import { List } from '../../../new_fields/List';
 import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
-import { Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
+import { Cast, FieldValue, NumCast, StrCast, BoolCast } from '../../../new_fields/Types';
 import { ImageField } from '../../../new_fields/URLField';
 import { Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
@@ -36,7 +36,7 @@ const ImageDocument = makeInterface(pageSchema, positionSchema);
 @observer
 export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageDocument) {
 
-    public static LayoutString() { return FieldView.LayoutString(ImageBox); }
+    public static LayoutString(fieldKey?: string) { return FieldView.LayoutString(ImageBox, fieldKey); }
     private _imgRef: React.RefObject<HTMLImageElement> = React.createRef();
     private _downX: number = 0;
     private _downY: number = 0;
@@ -46,6 +46,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     private dropDisposer?: DragManager.DragDropDisposer;
 
 
+    @computed get dataDoc() { return this.props.DataDoc && BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; }
+
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         if (this.dropDisposer) {
@@ -66,19 +68,24 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
             de.data.droppedDocuments.forEach(action((drop: Doc) => {
-                let layout = StrCast(drop.backgroundLayout);
-                if (layout.indexOf(ImageBox.name) !== -1) {
-                    let imgData = this.props.Document[this.props.fieldKey];
-                    if (imgData instanceof ImageField) {
-                        Doc.SetOnPrototype(this.props.Document, "data", new List([imgData]));
-                    }
-                    let imgList = Cast(this.props.Document[this.props.fieldKey], listSpec(ImageField), [] as any[]);
-                    if (imgList) {
-                        let field = drop.data;
-                        if (field instanceof ImageField) imgList.push(field);
-                        else if (field instanceof List) imgList.concat(field);
-                    }
+                if (this.dataDoc !== this.props.Document && drop.data instanceof ImageField) {
+                    this.dataDoc[this.props.fieldKey] = new ImageField(drop.data.url);
                     e.stopPropagation();
+                } else {
+                    let layout = StrCast(drop.backgroundLayout);
+                    if (layout.indexOf(ImageBox.name) !== -1) {
+                        let imgData = this.dataDoc[this.props.fieldKey];
+                        if (imgData instanceof ImageField) {
+                            Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new List([imgData]));
+                        }
+                        let imgList = Cast(this.dataDoc[this.props.fieldKey], listSpec(ImageField), [] as any[]);
+                        if (imgList) {
+                            let field = drop.data;
+                            if (field instanceof ImageField) imgList.push(field);
+                            else if (field instanceof List) imgList.concat(field);
+                        }
+                        e.stopPropagation();
+                    }
                 }
             }));
             // de.data.removeDocument()  bcz: need to implement
@@ -206,7 +213,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         let paths: string[] = ["http://www.cs.brown.edu/~bcz/noImage.png"];
         // this._curSuffix = "";
         // if (w > 20) {
-        let field = this.Document[this.props.fieldKey];
+        let field = this.dataDoc[this.props.fieldKey];
         // if (w < 100 && this._smallRetryCount < 10) this._curSuffix = "_s";
         // else if (w < 600 && this._mediumRetryCount < 10) this._curSuffix = "_m";
         // else if (this._largeRetryCount < 10) this._curSuffix = "_l";
@@ -214,8 +221,8 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
         else if (field instanceof List) paths = field.filter(val => val instanceof ImageField).map(p => this.choosePath((p as ImageField).url));
         // }
         let interactive = InkingControl.Instance.selectedTool ? "" : "-interactive";
-        let rotation = NumCast(this.props.Document.rotation, 0);
-        let aspect = (rotation % 180) ? this.props.Document[HeightSym]() / this.props.Document[WidthSym]() : 1;
+        let rotation = NumCast(this.dataDoc.rotation, 0);
+        let aspect = (rotation % 180) ? this.dataDoc[HeightSym]() / this.dataDoc[WidthSym]() : 1;
         let shift = (rotation % 180) ? (nativeHeight - nativeWidth / aspect) / 2 : 0;
         return (
             <div id={id} className={`imageBox-cont${interactive}`} style={{ background: "transparent" }}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 6adead626..7000352e7 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -203,7 +203,6 @@ class Viewer extends React.Component<IViewerProps> {
             this._isPage[page] = "page";
             this._visibleElements[page] = (
                 <Page
-                    size={this._pageSizes[page]}
                     pdf={this.props.pdf}
                     page={page}
                     numPages={this.props.pdf.numPages}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 9bacf49ba..27b3473f8 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -166,6 +166,14 @@ export namespace Doc {
     export function IsPrototype(doc: Doc) {
         return GetT(doc, "isPrototype", "boolean", true);
     }
+    export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
+        let hasProto = doc.proto instanceof Doc;
+        let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
+        let onProto = Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+        if (onDeleg || !hasProto || (!onProto && !defaultProto))
+            doc[key] = value;
+        else doc.proto![key] = value;
+    }
     export async function SetOnPrototype(doc: Doc, key: string, value: Field) {
         const proto = Object.getOwnPropertyNames(doc).indexOf("isPrototype") === -1 ? doc.proto : doc;
 
-- 
cgit v1.2.3-70-g09d2


From a4b34adcb34184728be0b69b33a561f6d10f0a98 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Fri, 21 Jun 2019 16:27:03 -0400
Subject: can drag just a group of links on a doc

---
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/DocumentManager.ts                 |  46 ++-----
 src/client/util/DragManager.ts                     |  47 ++++++-
 .../CollectionFreeFormLinkView.scss                |  38 +++---
 .../CollectionFreeFormLinkView.tsx                 |  88 ++++++------
 .../CollectionFreeFormLinksView.tsx                | 148 +++------------------
 src/client/views/nodes/DocumentView.tsx            |   2 +
 src/client/views/nodes/LinkMenu.scss               |  15 ++-
 src/client/views/nodes/LinkMenu.tsx                |  18 +--
 src/client/views/nodes/LinkMenuGroup.tsx           |  74 +++++++++++
 src/client/views/nodes/LinkMenuItem.tsx            |   8 +-
 src/new_fields/Doc.ts                              |  16 ++-
 12 files changed, 243 insertions(+), 261 deletions(-)
 create mode 100644 src/client/views/nodes/LinkMenuGroup.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index df5c39562..7cef48b98 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -91,7 +91,9 @@ export namespace DocUtils {
     //     let protoSrc = source.proto ? source.proto : source;
     //     let protoTarg = target.proto ? target.proto : target;
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc) {
-        if (LinkManager.Instance.doesLinkExist(source, target)) return;
+        if (LinkManager.Instance.doesLinkExist(source, target)) {
+            console.log("LINK EXISTS"); return;
+        }
 
         UndoManager.RunInBatch(() => {
 
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 325f4894d..c4cb6721a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -85,51 +85,25 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
-        // console.log("linked");
-        // let docs = DocListCast(CurrentUserUtils.UserDocument.data);
-        // docs.forEach(d => {
-        //     console.log("d", StrCast(d.title));
-
-        // });
-
-        // let d = Cast(CurrentUserUtils.UserDocument.activeWorkspace, Doc, new Doc);
-        // console.log("DOC", StrCast(d.title));
-
-
-
-        let linked = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-            // console.log("FINDING LINKED DVs FOR", StrCast(dv.props.Document.title));
+        return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
             let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
+            // let linksList = DocListCast(dv.props.Document.linkedToDocs);
             if (linksList && linksList.length) {
                 pairs.push(...linksList.reduce((pairs, link) => {
-                    // if (link) {
-                    let destination = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
-                    // console.log("FINDING FOR", StrCast(dv.Document.title), StrCast(destination.title));
-
-                    if (destination) {
-                        let dvs = DocumentManager.Instance.getDocumentViews(destination);
-                        if (dvs.length > 0) {
-                            dvs.map(docView1 => {
-                                // console.log("PUSHING LINK BETWEEN", StrCast(dv.props.Document.title), StrCast(docView1.props.Document.title));
-                                // TODO: if any docviews are not in the same context, draw a proxy
-                                // let sameContent = dv.props.ContainingCollectionView === docView1.props.ContainingCollectionView;
-                                pairs.push({ anchor1View: dv, anchor2View: docView1, linkDoc: link });
-                                // console.log("PUSHED", StrCast(dv.props.Document.title), StrCast(docView1.Document.title));
-                            });
-                        } else {
-                            let dv = DocumentManager.Instance.getDocumentView(destination);
-                            dv ? console.log(StrCast(dv.props.Document.title)) : console.log("cant find");
-                        }
+                    if (link) {
+                        let linkToDoc = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
+                        // console.log("FOUND ", DocumentManager.Instance.getDocumentViews(linkToDoc).length, "DOCVIEWS FOR", StrCast(linkToDoc.title), "WITH SOURCE", StrCast(dv.props.Document.title));
+                        DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 =>
+                            pairs.push({ a: dv, b: docView1, l: link }));
                     }
-                    // }
                     return pairs;
-                }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]));
+                }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]));
             }
             return pairs;
-        }, [] as { anchor1View: DocumentView, anchor2View: DocumentView, linkDoc: Doc }[]);
-        return linked;
+        }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
     }
 
+
     @undoBatch
     public jumpToDocument = async (docDelegate: Doc, forceDockFunc: boolean = false, dockFunc?: (doc: Doc) => void, linkPage?: number, docContext?: Doc): Promise<void> => {
         let doc = Doc.GetProto(docDelegate);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 11530ef09..2abcff4f7 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -48,12 +48,10 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
 export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) {
     let draggeddoc = LinkManager.Instance.findOppositeAnchor(linkDoc, sourceDoc);
 
-    // TODO: if not in same context then don't drag
-
     let moddrag = await Cast(draggeddoc.annotationOn, Doc);
     let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]);
     dragData.dropAction = "alias" as dropActionType;
-    DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
+    DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
         handlers: {
             dragComplete: action(emptyFunction),
         },
@@ -83,11 +81,21 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
             if (doc) moddrag.push(doc);
         }
         let dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs);
-        dragData.dropAction = "alias" as dropActionType;
         // dragData.moveDocument = (document, targetCollection, addDocument) => {
         //     return false;
         // };
-        DragManager.StartDocumentDrag([dragEle], dragData, x, y, {
+
+        // runInAction(() => StartDragFunctions.map(func => func()));
+        // (eles, dragData, downX, downY, options,
+        //     (dropData: { [id: string]: any }) => {
+        //         (dropData.droppedDocuments = dragData.userDropAction === "alias" || (!dragData.userDropAction && dragData.dropAction === "alias") ?
+        //             dragData.draggedDocuments.map(d => Doc.MakeAlias(d)) :
+        //             dragData.userDropAction === "copy" || (!dragData.userDropAction && dragData.dropAction === "copy") ?
+        //                 dragData.draggedDocuments.map(d => Doc.MakeCopy(d, true)) :
+        //                 dragData.draggedDocuments
+        //         );
+        //     });
+        DragManager.StartLinkedDocumentDrag([dragEle], dragData, x, y, {
             handlers: {
                 dragComplete: action(emptyFunction),
             },
@@ -215,6 +223,35 @@ export namespace DragManager {
             });
     }
 
+    export function StartLinkedDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+
+        runInAction(() => StartDragFunctions.map(func => func()));
+        StartDrag(eles, dragData, downX, downY, options,
+            (dropData: { [id: string]: any }) => {
+                dropData.droppedDocuments = dragData.draggedDocuments.map(d => {
+                    let dv = DocumentManager.Instance.getDocumentView(d);
+                    // console.log("DRAG", StrCast(d.title));
+
+                    if (dv) {
+                        console.log("DRAG", StrCast(d.title), "has view");
+                        if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
+                            console.log("DRAG", StrCast(d.title), "same");
+                            return d;
+                        } else {
+                            console.log("DRAG", StrCast(d.title), "diff");
+                            return Doc.MakeAlias(d);
+                        }
+                    } else {
+                        console.log("DRAG", StrCast(d.title), "has no view");
+                        return Doc.MakeAlias(d);
+                    }
+                    // return (dv && dv.props.ContainingCollectionView !== SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) || !dv ?
+                    //     Doc.MakeAlias(d) : d;
+                });
+
+            });
+    }
+
     export function StartAnnotationDrag(eles: HTMLElement[], dragData: AnnotationDragData, downX: number, downY: number, options?: DragOptions) {
         StartDrag(eles, dragData, downX, downY, options);
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index d8d518147..239c2ce56 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -1,22 +1,22 @@
-// .collectionfreeformlinkview-linkLine {
-//     stroke: black;
-//     transform: translate(10000px,10000px);
-//     opacity: 0.5;
-//     pointer-events: all;
-// }
-// .collectionfreeformlinkview-linkCircle {
-//     stroke: rgb(0,0,0);
-//     opacity: 0.5;
-//     transform: translate(10000px,10000px);
-//     pointer-events: all;
-//     cursor: pointer;
-// }
-// .collectionfreeformlinkview-linkText {
-//     stroke: rgb(0,0,0);
-//     opacity: 0.5;
-//     transform: translate(10000px,10000px);
-//     pointer-events: all;
-// }
+.collectionfreeformlinkview-linkLine {
+    stroke: black;
+    transform: translate(10000px,10000px);
+    opacity: 0.5;
+    pointer-events: all;
+}
+.collectionfreeformlinkview-linkCircle {
+    stroke: rgb(0,0,0);
+    opacity: 0.5;
+    transform: translate(10000px,10000px);
+    pointer-events: all;
+    cursor: pointer;
+}
+.collectionfreeformlinkview-linkText {
+    stroke: rgb(0,0,0);
+    opacity: 0.5;
+    transform: translate(10000px,10000px);
+    pointer-events: all;
+}
 
 .linkview-ele {
     transform: translate(10000px,10000px);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 65d5ac474..5c7f080e0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -5,70 +5,60 @@ import { InkingControl } from "../../InkingControl";
 import "./CollectionFreeFormLinkView.scss";
 import React = require("react");
 import v5 = require("uuid/v5");
-import { DocumentView } from "../../nodes/DocumentView";
-import { Docs } from "../../../documents/Documents";
 
 export interface CollectionFreeFormLinkViewProps {
-    // anchor1: Doc;
-    // anchor2: Doc;
-    // LinkDocs: Doc[];
-    // addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
-    // removeDocument: (document: Doc) => boolean;
-    // sameContext: boolean;
-
-    // sourceView: DocumentView;
-    // targetView: DocumentView;
-    sourceView: Doc;
-    targetView: Doc;
+    A: Doc;
+    B: Doc;
+    LinkDocs: Doc[];
+    addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
+    removeDocument: (document: Doc) => boolean;
 }
 
 @observer
 export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFormLinkViewProps> {
 
-    // onPointerDown = (e: React.PointerEvent) => {
-    //     if (e.button === 0 && !InkingControl.Instance.selectedTool) {
-    //         let a = this.props.A;
-    //         let b = this.props.B;
-    //         let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
-    //         let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
-    //         let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
-    //         let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
-    //         this.props.LinkDocs.map(l => {
-    //             let width = l[WidthSym]();
-    //             l.x = (x1 + x2) / 2 - width / 2;
-    //             l.y = (y1 + y2) / 2 + 10;
-    //             if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
-    //         });
-    //         e.stopPropagation();
-    //         e.preventDefault();
-    //     }
-    // }
-
-
+    onPointerDown = (e: React.PointerEvent) => {
+        if (e.button === 0 && !InkingControl.Instance.selectedTool) {
+            let a = this.props.A;
+            let b = this.props.B;
+            let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : a[WidthSym]() / 2);
+            let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : a[HeightSym]() / 2);
+            let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : b[WidthSym]() / 2);
+            let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : b[HeightSym]() / 2);
+            this.props.LinkDocs.map(l => {
+                let width = l[WidthSym]();
+                l.x = (x1 + x2) / 2 - width / 2;
+                l.y = (y1 + y2) / 2 + 10;
+                if (!this.props.removeDocument(l)) this.props.addDocument(l, false);
+            });
+            e.stopPropagation();
+            e.preventDefault();
+        }
+    }
     render() {
-        // let l = this.props.LinkDocs;
-        // let a = this.props.A;
-        // let b = this.props.B;
-        let a1 = this.props.sourceView;
-        let a2 = this.props.targetView;
-        let x1 = NumCast(a1.x) + (BoolCast(a1.isMinimized, false) ? 5 : NumCast(a1.width) / NumCast(a1.zoomBasis, 1) / 2);
-        let y1 = NumCast(a1.y) + (BoolCast(a1.isMinimized, false) ? 5 : NumCast(a1.height) / NumCast(a1.zoomBasis, 1) / 2);
-
-        let x2 = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
-        let y2 = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
-
+        let l = this.props.LinkDocs;
+        let a = this.props.A;
+        let b = this.props.B;
+        let x1 = NumCast(a.x) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.width) / NumCast(a.zoomBasis, 1) / 2);
+        let y1 = NumCast(a.y) + (BoolCast(a.isMinimized, false) ? 5 : NumCast(a.height) / NumCast(a.zoomBasis, 1) / 2);
+        let x2 = NumCast(b.x) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.width) / NumCast(b.zoomBasis, 1) / 2);
+        let y2 = NumCast(b.y) + (BoolCast(b.isMinimized, false) ? 5 : NumCast(b.height) / NumCast(b.zoomBasis, 1) / 2);
+        let text = "";
+        let first = this.props.LinkDocs[0];
+        if (this.props.LinkDocs.length === 1) text += first.title + (first.linkDescription ? "(" + StrCast(first.linkDescription) + ")" : "");
+        else text = "-multiple-";
+        text = "";
         return (
             <>
-                <line className="linkview-line linkview-ele"
-                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
+                <line key="linkLine" className="collectionfreeformlinkview-linkLine"
+                    style={{ strokeWidth: `${2 * l.length / 2}` }}
                     x1={`${x1}`} y1={`${y1}`}
                     x2={`${x2}`} y2={`${y2}`} />
-
                 {/* <circle key="linkCircle" className="collectionfreeformlinkview-linkCircle"
                     cx={(x1 + x2) / 2} cy={(y1 + y2) / 2} r={8} onPointerDown={this.onPointerDown} /> */}
-                {/* <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
+                <text key="linkText" textAnchor="middle" className="collectionfreeformlinkview-linkText" x={`${(x1 + x2) / 2}`} y={`${(y1 + y2) / 2}`}>
                     {text}
-                </text> */}
+                </text>
             </>
         );
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 9d2f8946b..854122592 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -1,4 +1,4 @@
-import { computed, IReactionDisposer, reaction, action } from "mobx";
+import { computed, IReactionDisposer, reaction } from "mobx";
 import { observer } from "mobx-react";
 import { Doc, DocListCast } from "../../../../new_fields/Doc";
 import { Id } from "../../../../new_fields/FieldSymbols";
@@ -11,10 +11,6 @@ import { CollectionViewProps } from "../CollectionSubView";
 import "./CollectionFreeFormLinksView.scss";
 import { CollectionFreeFormLinkView } from "./CollectionFreeFormLinkView";
 import React = require("react");
-import { CollectionFreeFormLinkWithProxyView } from "./CollectionFreeFormLinkWithProxyView";
-import { Docs } from "../../../documents/Documents";
-import { LinkButtonField } from "../../../../new_fields/LinkButtonField";
-import { LinkManager } from "../../../util/LinkManager";
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<CollectionViewProps> {
@@ -98,150 +94,42 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
     @computed
     get uniqueConnections() {
-        // console.log("\n");
-        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
-            // console.log("CONNECTION BETWEEN", StrCast(connection.anchor1View.props.Document.title), StrCast(connection.anchor2View.props.Document.title));
-            let srcViews = this.documentAnchors(connection.anchor1View);
-            // srcViews.forEach(sv => {
-            //     console.log("DOCANCHORS SRC", StrCast(connection.anchor1View.Document.title), StrCast(sv.Document.title));
-            // });
-
-            let targetViews = this.documentAnchors(connection.anchor2View);
-            // targetViews.forEach(sv => {
-            //     console.log("DOCANCHORS TARG", StrCast(connection.anchor2View.Document.title), StrCast(sv.Document.title));
-            // });
-
-            // console.log("lengths", srcViews.length, targetViews.length);
+        // DocumentManager.Instance.LinkedDocumentViews.forEach(d => {
+        //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
+        // });
 
-            // srcViews.forEach(v => {
-            //     console.log("SOURCE VIEW", StrCast(v.props.Document.title));
-            // });
-            // targetViews.forEach(v => {
-            //     console.log("TARGET VIEW", StrCast(v.Document.title));
-            // });
-
-            let possiblePairs: { anchor1: Doc, anchor2: Doc }[] = [];
-            // srcViews.map(sv => {
-            //     console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
-            //     targetViews.map(tv => {
-            //         console.log("TARGET VIEW", StrCast(tv.props.Document.title));
-            //         // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
-            //         possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
-            //     });
-            //     console.log("END\n");
-            // });
-            srcViews.forEach(sv => {
-                // console.log("SOURCE VIEW", StrCast(sv.props.Document.title));
-                targetViews.forEach(tv => {
-                    // console.log("TARGET VIEW", StrCast(tv.props.Document.title));
-                    // console.log("PUSHING PAIR", StrCast(sv.props.Document.title), StrCast(tv.props.Document.title));
-                    possiblePairs.push({ anchor1: sv.props.Document, anchor2: tv.props.Document });
-                });
-                // console.log("END\n");
-            });
-            // console.log("POSSIBLE PAIRS LENGTH", possiblePairs.length);
+        let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
+            let srcViews = this.documentAnchors(connection.a);
+            let targetViews = this.documentAnchors(connection.b);
+            let possiblePairs: { a: Doc, b: Doc, }[] = [];
+            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
             possiblePairs.map(possiblePair => {
-                // console.log("POSSIBLEPAIR", StrCast(possiblePair.anchor1.title), StrCast(possiblePair.anchor2.title));
                 if (!drawnPairs.reduce((found, drawnPair) => {
-                    let match1 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor1) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor2));
-                    let match2 = (Doc.AreProtosEqual(possiblePair.anchor1, drawnPair.anchor2) && Doc.AreProtosEqual(possiblePair.anchor2, drawnPair.anchor1));
+                    let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
+                    let match2 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.b) && Doc.AreProtosEqual(possiblePair.b, drawnPair.a));
                     let match = match1 || match2;
-                    if (match && !drawnPair.linkDocs.reduce((found, link) => found || link[Id] === connection.linkDoc[Id], false)) {
-                        drawnPair.linkDocs.push(connection.linkDoc);
+                    if (match && !drawnPair.l.reduce((found, link) => found || link[Id] === connection.l[Id], false)) {
+                        drawnPair.l.push(connection.l);
                     }
                     return match || found;
                 }, false)) {
-                    drawnPairs.push({ anchor1: possiblePair.anchor1, anchor2: possiblePair.anchor2, linkDocs: [connection.linkDoc] });
+                    drawnPairs.push({ a: possiblePair.a, b: possiblePair.b, l: [connection.l] });
                 }
             });
             return drawnPairs;
-        }, [] as { anchor1: Doc, anchor2: Doc, linkDocs: Doc[] }[]);
+        }, [] as { a: Doc, b: Doc, l: Doc[] }[]);
         return connections.map(c => {
-            let x = c.linkDocs.reduce((p, l) => p + l[Id], "");
-            return <CollectionFreeFormLinkView key={x} sourceView={c.anchor1} targetView={c.anchor2} />;
+            let x = c.l.reduce((p, l) => p + l[Id], "");
+            return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
+                removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
         });
     }
 
-    // findUniquePairs = (): JSX.Element[] => {
-    //     let connections = DocumentManager.Instance.LinkedDocumentViews;
-
-    //     // console.log("CONNECTIONS");
-    //     // connections.forEach(c => console.log(StrCast(c.anchor1View.Document.title), StrCast(c.anchor2View.Document.title)));
-
-    //     let unique: Set<{ sourceView: DocumentView, targetView: DocumentView, linkDoc: Doc }> = new Set();
-    //     connections.forEach(c => {
-
-    //         // let match1Index = unique.findIndex(u => (c.anchor1View === u.sourceView) && (c.anchor2View === u.targetView));
-    //         // let match2Index = unique.findIndex(u => (c.anchor1View === u.targetView) && (c.anchor2View === u.sourceView));
-    //         let match1 = unique.has({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-    //         let match2 = unique.has({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
-    //         let sameContext = c.anchor1View.props.ContainingCollectionView === c.anchor2View.props.ContainingCollectionView;
-
-    //         // console.log("CONNECTION", StrCast(c.anchor1View.props.Document.title), StrCast(c.anchor2View.props.Document.title), match1, match2);
-
-
-    //         // if in same context, push if docview pair does not already exist
-    //         // else push both directions of pair
-    //         if (sameContext) {
-    //             if (!(match1 || match2)) unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-    //         } else {
-    //             unique.add({ sourceView: c.anchor1View, targetView: c.anchor2View, linkDoc: c.linkDoc });
-    //             unique.add({ sourceView: c.anchor2View, targetView: c.anchor1View, linkDoc: c.linkDoc });
-    //         }
-    //     });
-
-    //     let uniqueList: JSX.Element[] = [];
-    //     unique.forEach(u => {
-    //         // TODO: make better key
-    //         let key = StrCast(u.sourceView.Document[Id]) + "-link-" + StrCast(u.targetView.Document[Id]) + "-" + Date.now() + Math.random();
-    //         let sourceIn = u.sourceView.props.ContainingCollectionView ? u.sourceView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-    //         let targetIn = u.targetView.props.ContainingCollectionView ? u.targetView.props.ContainingCollectionView.props.Document === this.props.Document : false;
-    //         let sameContext = u.sourceView.props.ContainingCollectionView === u.targetView.props.ContainingCollectionView;
-    //         let inContainer = sameContext ? sourceIn || targetIn : sourceIn;
-
-    //         if (inContainer) {
-    //             // let alias = Doc.MakeAlias(proxy);
-    //             if (sameContext) {
-    //                 uniqueList.push(<CollectionFreeFormLinkView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
-    //             } else {
-    //                 let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
-    //                 if (!proxy) {
-    //                     proxy = Docs.LinkButtonDocument(
-    //                         { sourceViewId: StrCast(u.sourceView.props.Document[Id]), targetViewId: StrCast(u.targetView.props.Document[Id]) },
-    //                         { width: 200, height: 100, borderRounding: 0 });
-    //                     let proxy1Proto = Doc.GetProto(proxy);
-    //                     proxy1Proto.sourceViewId = StrCast(u.sourceView.props.Document[Id]);
-    //                     proxy1Proto.targetViewId = StrCast(u.targetView.props.Document[Id]);
-    //                     proxy1Proto.isLinkButton = true;
-
-    //                     // LinkManager.Instance.linkProxies.push(proxy);
-    //                     LinkManager.Instance.addLinkProxy(proxy);
-    //                 }
-    //                 uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} proxyDoc={proxy} />);
-
-    //                 // let proxy = LinkManager.Instance.findLinkProxy(StrCast(u.sourceView.props.Document[Id]), StrCast(u.targetView.props.Document[Id]));
-    //                 // if (proxy) {
-    //                 //     this.props.addDocument(proxy, false);
-    //                 //     uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView} />);
-    //                 // }
-    //                 // let proxyKey = Doc.AreProtosEqual(u.sourceView.Document, Cast(u.linkDoc.anchor1, Doc, new Doc)) ? "proxy1" : "proxy2";
-    //                 // let proxy = Cast(u.linkDoc[proxyKey], Doc, new Doc);
-    //                 // this.props.addDocument(proxy, false);
-
-    //                 // uniqueList.push(<CollectionFreeFormLinkWithProxyView key={key} sourceView={u.sourceView} targetView={u.targetView}
-    //                 //     proxyDoc={proxy} addDocTab={this.props.addDocTab} />);
-    //             }
-    //         }
-    //     });
-    //     return uniqueList;
-    // }
-
     render() {
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
                     {this.uniqueConnections}
-                    {/* {this.findUniquePairs()} */}
                 </svg>
                 {this.props.children}
             </div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dbbae2d59..1fc2cf770 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -161,7 +161,9 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
             (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
             , { fireImmediately: true });
+        // console.log("CREATED NEW DOC VIEW", StrCast(this.props.Document.title), DocumentManager.Instance.DocumentViews.length);
         DocumentManager.Instance.DocumentViews.push(this);
+        // console.log("ADDED TO DOC MAN", StrCast(this.props.Document.title), DocumentManager.Instance.DocumentViews.length);
     }
 
     animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index d2e411c3d..c01ed23c4 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -18,8 +18,19 @@
     }
 
     .linkMenu-group-name {
-        border-bottom: 0.5px solid lightgray; 
-        margin-bottom: 4px;
+        padding: 4px 6px;
+        line-height: 12px;
+        border-radius: 5px;
+        margin-bottom: 2px;
+
+        &:hover {
+            background-color: lightgray;
+        }
+    }
+
+    .linkMenu-group-wrapper {
+        border-top: 0.5px solid lightgray; 
+        padding-top: 4px;
     }
 }
 
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index 8aca130a5..f96c7d2e4 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -8,6 +8,9 @@ import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { LinkManager } from "../../util/LinkManager";
+import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { emptyFunction } from "../../../Utils";
+import { LinkMenuGroup } from "./LinkMenuGroup";
 
 interface Props {
     docView: DocumentView;
@@ -19,24 +22,11 @@ export class LinkMenu extends React.Component<Props> {
 
     @observable private _editingLink?: Doc;
 
-    renderGroup = (group: Doc[], groupType: string): Array<JSX.Element> => {
-        let source = this.props.docView.Document;
-        return group.map(linkDoc => {
-            let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, source);
-            return <LinkMenuItem key={destination[Id] + source[Id]} groupType={groupType} linkDoc={linkDoc} sourceDoc={source} destinationDoc={destination} showEditor={action(() => this._editingLink = linkDoc)} />;
-        });
-    }
-
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         let linkItems: Array<JSX.Element> = [];
         groups.forEach((group, groupType) => {
             linkItems.push(
-                <div key={groupType} className="linkMenu-group">
-                    <p className="linkMenu-group-name">{groupType}:</p>
-                    <div className="linkMenu-group-wrapper">
-                        {this.renderGroup(group, groupType)}
-                    </div>
-                </div>
+                <LinkMenuGroup key={groupType} sourceDoc={this.props.docView.props.Document} group={group} groupType={groupType} showEditor={action((linkDoc: Doc) => this._editingLink = linkDoc)} />
             );
         });
 
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
new file mode 100644
index 000000000..229143d99
--- /dev/null
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -0,0 +1,74 @@
+import { action, observable } from "mobx";
+import { observer } from "mobx-react";
+import { DocumentView } from "./DocumentView";
+import { LinkMenuItem } from "./LinkMenuItem";
+import { LinkEditor } from "./LinkEditor";
+import './LinkMenu.scss';
+import React = require("react");
+import { Doc, DocListCast } from "../../../new_fields/Doc";
+import { Id } from "../../../new_fields/FieldSymbols";
+import { LinkManager } from "../../util/LinkManager";
+import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { emptyFunction } from "../../../Utils";
+
+interface LinkMenuGroupProps {
+    sourceDoc: Doc;
+    group: Doc[];
+    groupType: string;
+    showEditor: (linkDoc: Doc) => void;
+}
+
+@observer
+export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
+
+    private _drag = React.createRef<HTMLDivElement>();
+
+    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._drag.current !== null && (e.movementX > 1 || e.movementY > 1)) {
+            document.removeEventListener("pointermove", this.onLinkButtonMoved);
+            document.removeEventListener("pointerup", this.onLinkButtonUp);
+
+            let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.findOppositeAnchor(linkDoc, this.props.sourceDoc));
+            let dragData = new DragManager.DocumentDragData(draggedDocs);
+
+            DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
+                handlers: {
+                    dragComplete: action(emptyFunction),
+                },
+                hideSource: false
+            });
+        }
+        e.stopPropagation();
+    }
+
+    render() {
+        let groupItems = this.props.group.map(linkDoc => {
+            let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, this.props.sourceDoc);
+            return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
+                linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
+        });
+
+        return (
+            <div className="linkMenu-group">
+                <p className="linkMenu-group-name" ref={this._drag} onPointerDown={this.onLinkButtonDown} >{this.props.groupType}:</p>
+                <div className="linkMenu-group-wrapper">
+                    {groupItems}
+                </div>
+            </div>
+        )
+    }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index 1ef4690b0..42ef353b7 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -21,7 +21,7 @@ interface LinkMenuItemProps {
     linkDoc: Doc;
     sourceDoc: Doc;
     destinationDoc: Doc;
-    showEditor: () => void;
+    showEditor: (linkDoc: Doc) => void;
 }
 
 @observer
@@ -42,7 +42,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
     onEdit = (e: React.PointerEvent): void => {
         e.stopPropagation();
-        this.props.showEditor();
+        this.props.showEditor(this.props.linkDoc);
     }
 
     renderMetadata = (): JSX.Element => {
@@ -95,12 +95,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
             <div className="linkMenu-item">
                 <div className={canExpand ? "linkMenu-item-content expand-three" : "linkMenu-item-content expand-two"}>
                     <div className="link-name">
-                        <p>{StrCast(this.props.destinationDoc.title)}</p>
+                        <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{StrCast(this.props.destinationDoc.title)}</p>
                         <div className="linkMenu-item-buttons">
                             {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>
-                            <div title="Follow link" className="button" ref={this._drag} onPointerDown={this.onLinkButtonDown} onPointerUp={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
+                            <div title="Follow link" className="button" onPointerDown={this.onFollowLink}><FontAwesomeIcon className="fa-icon" icon="arrow-right" size="sm" /></div>
                         </div>
                     </div>
                     {this._showMore ? this.renderMetadata() : <></>}
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 1b0ff812f..5ce47fc2f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -3,11 +3,13 @@ import { serializable, primitive, map, alias, list } from "serializr";
 import { autoObject, SerializationHelper, Deserializable } from "../client/util/SerializationHelper";
 import { DocServer } from "../client/DocServer";
 import { setter, getter, getField, updateFunction, deleteProperty } from "./util";
-import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast } from "./Types";
+import { Cast, ToConstructor, PromiseValue, FieldValue, NumCast, StrCast } from "./Types";
 import { listSpec } from "./Schema";
 import { ObjectField } from "./ObjectField";
 import { RefField, FieldId } from "./RefField";
 import { ToScriptString, SelfProxy, Parent, OnUpdate, Self, HandleUpdate, Update, Id } from "./FieldSymbols";
+import { LinkManager } from "../client/util/LinkManager";
+import { DocUtils } from "../client/documents/Documents";
 
 export namespace Field {
     export function toScriptString(field: Field): string {
@@ -247,6 +249,18 @@ export namespace Doc {
                 }
             }
         });
+        console.log("COPY", StrCast(doc.title));
+        let links = LinkManager.Instance.findAllRelatedLinks(doc);
+        links.forEach(linkDoc => {
+            let opp = LinkManager.Instance.findOppositeAnchor(linkDoc, doc);
+            console.log("OPP", StrCast(opp.title));
+            DocUtils.MakeLink(opp, copy);
+        });
+
+        LinkManager.Instance.allLinks.forEach(l => {
+            console.log("LINK", StrCast(Cast(l.anchor1, Doc, new Doc).title), StrCast(Cast(l.anchor2, Doc, new Doc).title));
+        });
+
         return copy;
     }
 
-- 
cgit v1.2.3-70-g09d2


From c9a36bc25ed8bb8144dbef6c7cc2a09447867aa8 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Fri, 21 Jun 2019 22:46:24 -0400
Subject: fixes to allow collections to be template fields

---
 src/client/views/DocumentDecorations.tsx           | 21 +++++++
 .../views/collections/CollectionBaseView.tsx       | 45 +++++---------
 src/client/views/collections/CollectionSubView.tsx |  4 +-
 .../views/collections/CollectionTreeView.tsx       | 71 ++++++++++++++--------
 src/client/views/collections/CollectionView.tsx    |  7 +++
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  5 +-
 src/client/views/nodes/DocumentView.tsx            |  1 +
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 8 files changed, 96 insertions(+), 60 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 8f02f04d7..bdc814546 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -26,6 +26,7 @@ import { Template, Templates } from "./Templates";
 import React = require("react");
 import { URLField } from '../../new_fields/URLField';
 import { templateLiteral } from 'babel-types';
+import { CollectionViewType } from './collections/CollectionBaseView';
 const higflyout = require("@hig/flyout");
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -74,6 +75,26 @@ 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 metaKey = text.slice(3, text.length);
+                let collection = SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView!.props.Document;
+                Doc.GetProto(collection)[metaKey] = new List<Doc>([
+                    Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }),
+                    Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }),
+                ]);
+                let template = Doc.MakeAlias(collection);
+                template.title = metaKey;
+                template.embed = true;
+                template.layout = CollectionView.LayoutString(metaKey);
+                template.viewType = CollectionViewType.Freeform;
+                template.x = 0;
+                template.y = 0;
+                template.width = 300;
+                template.height = 300;
+                template.isTemplate = true;
+                template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]);
+                Doc.AddDocToList(collection, "data", template);
+                SelectionManager.SelectedDocuments().map(dv => dv.props.removeDocument && dv.props.removeDocument(dv.props.Document));
             } else if (text[0] === ">") {
                 let metaKey = text.slice(1, text.length);
                 let first = SelectionManager.SelectedDocuments()[0].props.Document!;
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 038a73626..75bdf755c 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -5,7 +5,7 @@ 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';
-import { Cast, FieldValue, NumCast, PromiseValue, StrCast } from '../../../new_fields/Types';
+import { Cast, FieldValue, NumCast, PromiseValue, StrCast, BoolCast } from '../../../new_fields/Types';
 import { SelectionManager } from '../../util/SelectionManager';
 import { ContextMenu } from '../ContextMenu';
 import { FieldViewProps } from '../nodes/FieldView';
@@ -60,6 +60,8 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         }
     }
 
+    @computed get dataDoc() { return (BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document); }
+
     active = (): boolean => {
         var isSelected = this.props.isSelected();
         var topMost = this.props.isTopMost;
@@ -102,30 +104,21 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
 
     @action.bound
     addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
-        let props = this.props;
-        var curPage = NumCast(props.Document.curPage, -1);
+        var curPage = NumCast(this.props.Document.curPage, -1);
         Doc.GetProto(doc).page = curPage;
         if (curPage >= 0) {
-            Doc.GetProto(doc).annotationOn = props.Document;
+            Doc.GetProto(doc).annotationOn = this.props.Document;
         }
         allowDuplicates = true;
-        if (!this.createsCycle(doc, props.Document)) {
+        if (!this.createsCycle(doc, this.dataDoc)) {
             //TODO This won't create the field if it doesn't already exist
-            const value = Cast(props.Document[props.fieldKey], listSpec(Doc));
-            let alreadyAdded = true;
+            const value = Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc));
             if (value !== undefined) {
                 if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
-                    alreadyAdded = false;
                     value.push(doc);
                 }
             } else {
-                alreadyAdded = false;
-                Doc.SetOnPrototype(this.props.Document, this.props.fieldKey, new List([doc]));
-            }
-            // set the ZoomBasis only if hasn't already been set -- bcz: maybe set/resetting the ZoomBasis should be a parameter to addDocument?
-            if (!alreadyAdded && (this.collectionViewType === CollectionViewType.Freeform || this.collectionViewType === CollectionViewType.Invalid)) {
-                let zoom = NumCast(this.props.Document.scale, 1);
-                // Doc.GetProto(doc).zoomBasis = zoom;
+                Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new List([doc]));
             }
             return true;
         }
@@ -136,22 +129,12 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
     removeDocument(doc: Doc): boolean {
         let docView = DocumentManager.Instance.getDocumentView(doc, this.props.ContainingCollectionView);
         docView && SelectionManager.DeselectDoc(docView);
-        const props = this.props;
         //TODO This won't create the field if it doesn't already exist
-        const value = Cast(props.Document[props.fieldKey], listSpec(Doc), []);
-        let index = -1;
-        for (let i = 0; i < value.length; i++) {
-            let v = value[i];
-            if (v instanceof Doc && v[Id] === doc[Id]) {
-                index = i;
-                break;
-            }
-        }
-        PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn => {
-            if (annotationOn === props.Document) {
-                doc.annotationOn = undefined;
-            }
-        });
+        const value = Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc), []);
+        let index = value.reduce((p, v, i) => (v instanceof Doc && v[Id] === doc[Id]) ? i : p, -1);
+        PromiseValue(Cast(doc.annotationOn, Doc)).then(annotationOn =>
+            annotationOn === this.dataDoc.Document && (doc.annotationOn = undefined)
+        );
 
         if (index !== -1) {
             value.splice(index, 1);
@@ -165,7 +148,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
 
     @action.bound
     moveDocument(doc: Doc, targetCollection: Doc, addDocument: (doc: Doc) => boolean): boolean {
-        if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
+        if (Doc.AreProtosEqual(this.dataDoc, targetCollection)) {
             return true;
         }
         if (this.removeDocument(doc)) {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 699bddc7c..a887d8ec8 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -4,7 +4,7 @@ import CursorField from "../../../new_fields/CursorField";
 import { Doc, DocListCast, Opt } from "../../../new_fields/Doc";
 import { List } from "../../../new_fields/List";
 import { listSpec } from "../../../new_fields/Schema";
-import { Cast, PromiseValue } from "../../../new_fields/Types";
+import { Cast, PromiseValue, BoolCast } from "../../../new_fields/Types";
 import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
 import { RouteStore } from "../../../server/RouteStore";
 import { DocServer } from "../../DocServer";
@@ -48,7 +48,7 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
         get childDocs() {
             //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 DocListCast(this.props.Document[this.props.fieldKey]);
+            return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document)[this.props.fieldKey]);
         }
 
         @action
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 83a7c9e3a..05252f632 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -28,6 +28,8 @@ import React = require("react");
 import { FormattedTextBox } from '../nodes/FormattedTextBox';
 import { ImageField } from '../../../new_fields/URLField';
 import { ImageBox } from '../nodes/ImageBox';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionView } from './CollectionView';
 
 
 export interface TreeViewProps {
@@ -150,42 +152,63 @@ class TreeView extends React.Component<TreeViewProps> {
         SetValue={(value: string) => {
             let res = (Doc.GetProto(this.props.document)[key] = value) ? true : true;
 
-            if (value.startsWith(">>")) {
-                let metaKey = value.slice(2, value.length);
+            if (value.startsWith(">>>")) {
+                let metaKey = value.slice(3, value.length);
                 let collection = this.props.containingCollection;
-                Doc.GetProto(collection)[metaKey] = new ImageField("http://www.cs.brown.edu/~bcz/face.gif");
+                Doc.GetProto(collection)[metaKey] = new List<Doc>([
+                    Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }),
+                    Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }),
+                ]);
                 let template = Doc.MakeAlias(collection);
                 template.title = metaKey;
                 template.embed = true;
-                template.layout = ImageBox.LayoutString(metaKey);
+                template.layout = CollectionView.LayoutString(metaKey);
+                template.viewType = CollectionViewType.Freeform;
                 template.x = 0;
                 template.y = 0;
-                template.nativeWidth = 300;
-                template.nativeHeight = 300;
                 template.width = 300;
                 template.height = 300;
                 template.isTemplate = true;
                 template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]);
                 Doc.AddDocToList(collection, "data", template);
                 this.delete();
-            }
-            if (value.startsWith(">")) {
-                let metaKey = value.slice(1, value.length);
-                let collection = this.props.containingCollection;
-                Doc.GetProto(collection)[metaKey] = "-empty field-";
-                let template = Doc.MakeAlias(collection);
-                template.title = metaKey;
-                template.embed = true;
-                template.layout = FormattedTextBox.LayoutString(metaKey);
-                template.x = 0;
-                template.y = 0;
-                template.width = 100;
-                template.height = 50;
-                template.isTemplate = true;
-                template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]);
-                Doc.AddDocToList(collection, "data", template);
-                this.delete();
-            }
+            } else
+                if (value.startsWith(">>")) {
+                    let metaKey = value.slice(2, value.length);
+                    let collection = this.props.containingCollection;
+                    Doc.GetProto(collection)[metaKey] = new ImageField("http://www.cs.brown.edu/~bcz/face.gif");
+                    let template = Doc.MakeAlias(collection);
+                    template.title = metaKey;
+                    template.embed = true;
+                    template.layout = ImageBox.LayoutString(metaKey);
+                    template.x = 0;
+                    template.y = 0;
+                    template.nativeWidth = 300;
+                    template.nativeHeight = 300;
+                    template.width = 300;
+                    template.height = 300;
+                    template.isTemplate = true;
+                    template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]);
+                    Doc.AddDocToList(collection, "data", template);
+                    this.delete();
+                } else
+                    if (value.startsWith(">")) {
+                        let metaKey = value.slice(1, value.length);
+                        let collection = this.props.containingCollection;
+                        Doc.GetProto(collection)[metaKey] = "-empty field-";
+                        let template = Doc.MakeAlias(collection);
+                        template.title = metaKey;
+                        template.embed = true;
+                        template.layout = FormattedTextBox.LayoutString(metaKey);
+                        template.x = 0;
+                        template.y = 0;
+                        template.width = 100;
+                        template.height = 50;
+                        template.isTemplate = true;
+                        template.templates = new List<string>([Templates.TitleBar(metaKey)]);//`{props.DataDoc.${metaKey}_text}`)]);
+                        Doc.AddDocToList(collection, "data", template);
+                        this.delete();
+                    }
 
             return res;
         }}
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 10e6fb885..872cb3f1c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -66,6 +66,13 @@ export class CollectionView extends React.Component<FieldViewProps> {
                     Doc.GetProto(otherdoc).summary = "THIS SUMMARY IS MEANINGFUL!";
                     Doc.GetProto(otherdoc).photo = new ImageField("http://www.cs.brown.edu/~bcz/snowbeast.JPG");
                     Doc.GetProto(otherdoc).layout = Doc.MakeDelegate(this.props.Document);
+                    Doc.GetProto(otherdoc).publication = new List<Doc>([
+                        Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }),
+                        Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }),
+                        Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }),
+                        Docs.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { width: 300, height: 300 }),
+                        Docs.TextDocument({ documentText: "hello world!", width: 300, height: 300 }),
+                    ]);
                     this.props.addDocTab && this.props.addDocTab(otherdoc, otherdoc, "onRight");
                 }), icon: "project-diagram"
             });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5c2ea3ef0..20a9a172c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -45,6 +45,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     private get _pwidth() { return this.props.PanelWidth(); }
     private get _pheight() { return this.props.PanelHeight(); }
 
+    @computed get dataDoc() { return this.props.DataDoc && BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; }
     @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
     @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
     public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldKey === "annotations"; }
@@ -163,7 +164,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
                     [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
                 }, [[minx, maxx], [miny, maxy]]);
-                let ink = Cast(this.props.Document.ink, InkField);
+                let ink = Cast(this.dataDoc.ink, InkField);
                 if (ink && ink.inkData) {
                     ink.inkData.forEach((value: StrokeData, key: string) => {
                         let bounds = InkingCanvas.StrokeRect(value);
@@ -296,7 +297,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     getDocumentViewProps(layoutDoc: Doc): DocumentViewProps {
         return {
-            DataDoc: this.props.DataDoc !== this.props.Document ? this.props.DataDoc : layoutDoc,
+            DataDoc: this.props.DataDoc !== this.props.Document && !BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : layoutDoc,
             Document: layoutDoc,
             addDocument: this.props.addDocument,
             removeDocument: this.props.removeDocument,
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 08f58dcd6..1a92d7152 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -536,6 +536,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
     @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
     @computed get contents() {
+        console.log("dv = " + this.props.Document.title + " " + this.props.DataDoc.title);
         return (
             <DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} />);
     }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 241a593b0..008a09130 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -68,7 +68,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     drop = (e: Event, de: DragManager.DropEvent) => {
         if (de.data instanceof DragManager.DocumentDragData) {
             de.data.droppedDocuments.forEach(action((drop: Doc) => {
-                if (this.dataDoc !== this.props.Document && drop.data instanceof ImageField) {
+                if (/*this.dataDoc !== this.props.Document &&*/ drop.data instanceof ImageField) {
                     this.dataDoc[this.props.fieldKey] = new ImageField(drop.data.url);
                     e.stopPropagation();
                 } else {
-- 
cgit v1.2.3-70-g09d2


From 34fb7f7b334c8255908ec161bedbe699f2c5539b Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Sat, 22 Jun 2019 00:50:01 -0400
Subject: a few more bug fixes.

---
 .../views/collections/CollectionTreeView.tsx       | 87 ++++++++++++----------
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 2 files changed, 51 insertions(+), 40 deletions(-)

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

diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 05252f632..4c877b3fa 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -66,9 +66,20 @@ class TreeView extends React.Component<TreeViewProps> {
     private _header?: React.RefObject<HTMLDivElement> = React.createRef();
     private _treedropDisposer?: DragManager.DragDropDisposer;
     private _dref = React.createRef<HTMLDivElement>();
-    @observable _chosenKey: string = "data";
+    @observable __chosenKey: string = "";
+    @computed get _chosenKey() { return this.__chosenKey ? this.__chosenKey : this.fieldKey; }
     @observable _collapsed: boolean = true;
 
+    @computed get fieldKey() {
+        let layout = StrCast(this.props.document.layout);
+        if (layout.indexOf("fieldKey={\"") !== -1) {
+            return layout.split("fieldKey={\"")[1].split("\"")[0];
+        }
+        return "data";
+    }
+
+    @computed get dataDoc() { return (BoolCast(this.props.document.isTemplate) ? this.props.dataDoc : this.props.document); }
+
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer && this._treedropDisposer();
         if (ele) {
@@ -76,7 +87,7 @@ class TreeView extends React.Component<TreeViewProps> {
         }
     }
 
-    @undoBatch delete = () => this.props.deleteDoc(this.props.document);
+    @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);
     @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.dataDoc, "onRight");
 
     onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
@@ -108,7 +119,7 @@ class TreeView extends React.Component<TreeViewProps> {
 
     @action
     remove = (document: Document, key: string): boolean => {
-        let children = Cast(this.props.document[key], listSpec(Doc), []);
+        let children = Cast(this.dataDoc[key], listSpec(Doc), []);
         if (children.indexOf(document) !== -1) {
             children.splice(children.indexOf(document), 1);
             return true;
@@ -124,8 +135,8 @@ class TreeView extends React.Component<TreeViewProps> {
     indent = () => this.props.addDocument(this.props.document) && this.delete()
 
     renderBullet() {
-        let docList = Cast(this.props.document.data, listSpec(Doc));
-        let doc = Cast(this.props.document.data, Doc);
+        let docList = Cast(this.dataDoc[this.fieldKey], listSpec(Doc));
+        let doc = Cast(this.dataDoc[this.fieldKey], Doc);
         let isDoc = doc instanceof Doc || docList;
         return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}>
             {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />}
@@ -143,14 +154,14 @@ class TreeView extends React.Component<TreeViewProps> {
     editableView = (key: string, style?: string) => (<EditableView
         oneLine={true}
         display={"inline"}
-        editing={this.props.document[Id] === TreeView.loadId}
+        editing={this.dataDoc[Id] === TreeView.loadId}
         contents={StrCast(this.props.document[key])}
         onClick={this.titleClicked}
         height={36}
         fontStyle={style}
         GetValue={() => StrCast(this.props.document[key])}
         SetValue={(value: string) => {
-            let res = (Doc.GetProto(this.props.document)[key] = value) ? true : true;
+            let res = (Doc.GetProto(this.dataDoc)[key] = value) ? true : true;
 
             if (value.startsWith(">>>")) {
                 let metaKey = value.slice(3, value.length);
@@ -213,7 +224,7 @@ class TreeView extends React.Component<TreeViewProps> {
             return res;
         }}
         OnFillDown={(value: string) => {
-            Doc.GetProto(this.props.document)[key] = value;
+            Doc.GetProto(this.dataDoc)[key] = value;
             let doc = Docs.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);
@@ -222,23 +233,23 @@ class TreeView extends React.Component<TreeViewProps> {
     />)
 
     @computed get keyList() {
-        let keys = Array.from(Object.keys(this.props.document));
-        if (this.props.document.proto instanceof Doc) {
-            keys.push(...Array.from(Object.keys(this.props.document.proto)));
+        let keys = Array.from(Object.keys(this.dataDoc));
+        if (this.dataDoc.proto instanceof Doc) {
+            keys.push(...Array.from(Object.keys(this.dataDoc.proto)));
             while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
         }
         let keyList: string[] = [];
         keys.map(key => {
-            let docList = Cast(this.props.document[key], listSpec(Doc));
-            let doc = Cast(this.props.document[key], Doc);
+            let docList = Cast(this.dataDoc[key], listSpec(Doc));
+            let doc = Cast(this.dataDoc[key], Doc);
             if (doc instanceof Doc || docList) {
                 keyList.push(key);
             }
         });
-        if (keyList.indexOf("data") !== -1) {
-            keyList.splice(keyList.indexOf("data"), 1);
+        if (keyList.indexOf(this.fieldKey) !== -1) {
+            keyList.splice(keyList.indexOf(this.fieldKey), 1);
         }
-        keyList.splice(0, 0, "data");
+        keyList.splice(0, 0, this.fieldKey);
         return keyList;
     }
     /**
@@ -246,19 +257,19 @@ class TreeView extends React.Component<TreeViewProps> {
      */
     renderTitle() {
         let reference = React.createRef<HTMLDivElement>();
-        let onItemDown = SetupDrag(reference, () => this.props.document, this.move, this.props.dropAction, this.props.treeViewId, true);
+        let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
 
         let headerElements = (
             <span className="collectionTreeView-keyHeader" key={this._chosenKey}
                 onPointerDown={action(() => {
                     let ind = this.keyList.indexOf(this._chosenKey);
                     ind = (ind + 1) % this.keyList.length;
-                    this._chosenKey = this.keyList[ind];
+                    this.__chosenKey = this.keyList[ind];
                 })} >
                 {this._chosenKey}
             </span>);
-        let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document.data, listSpec(Doc), []) : [];
-        let openRight = dataDocs && dataDocs.indexOf(this.props.document) !== -1 ? (null) : (
+        let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
+        let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (
             <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
                 <FontAwesomeIcon icon="angle-right" size="lg" />
             </div>);
@@ -279,17 +290,17 @@ class TreeView extends React.Component<TreeViewProps> {
 
     onWorkspaceContextMenu = (e: React.MouseEvent): void => {
         if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
-            ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.props.document)) });
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.dataDoc)) });
+            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.dataDoc, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" });
             if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
                 ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.props.dataDoc, "inTab"), icon: "folder" });
                 ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.props.dataDoc, "onRight"), icon: "caret-square-right" });
-                if (DocumentManager.Instance.getDocumentViews(this.props.document).length) {
-                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.props.document).map(view => view.props.focus(this.props.document)) });
+                if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
+                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document)) });
                 }
-                ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+                ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.dataDoc)) });
             } else {
-                ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
+                ContextMenu.Instance.addItem({ description: "Delete Workspace", event: undoBatch(() => this.props.deleteDoc(this.dataDoc)) });
             }
             ContextMenu.Instance.displayMenu(e.pageX > 156 ? e.pageX - 156 : 0, e.pageY - 15);
             e.stopPropagation();
@@ -302,9 +313,9 @@ class TreeView extends React.Component<TreeViewProps> {
         let before = x[1] < bounds[1];
         let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
         if (de.data instanceof DragManager.DocumentDragData) {
-            let addDoc = (doc: Doc) => this.props.addDocument(doc, this.props.document, before);
+            let addDoc = (doc: Doc) => this.props.addDocument(doc, this.dataDoc, before);
             if (inside) {
-                let docList = Cast(this.props.document.data, listSpec(Doc));
+                let docList = Cast(this.dataDoc.data, listSpec(Doc));
                 if (docList !== undefined) {
                     addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
                 }
@@ -312,10 +323,10 @@ class TreeView extends React.Component<TreeViewProps> {
             e.stopPropagation();
             let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
             return (de.data.dropAction || de.data.userDropAction) ?
-                de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before) || added, false)
+                de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.dataDoc, before) || added, false)
                 : (de.data.moveDocument) ?
-                    movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.props.document, addDoc) || added, false)
-                    : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.props.document, before), false);
+                    movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.dataDoc, addDoc) || added, false)
+                    : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.dataDoc, before), false);
         }
         return false;
     }
@@ -330,10 +341,10 @@ class TreeView extends React.Component<TreeViewProps> {
 
     render() {
         let contentElement: (JSX.Element | null) = null;
-        let docList = Cast(this.props.document[this._chosenKey], listSpec(Doc));
+        let docList = Cast(this.dataDoc[this._chosenKey], listSpec(Doc));
         let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
-        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before);
-        let doc = Cast(this.props.document[this._chosenKey], Doc);
+        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, this._chosenKey, doc, addBefore, before);
+        let doc = Cast(this.dataDoc[this._chosenKey], Doc);
         let docWidth = () => NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
         if (!this._collapsed) {
             if (!this.props.document.embed) {
@@ -389,7 +400,7 @@ class TreeView extends React.Component<TreeViewProps> {
         active: () => boolean,
         panelWidth: () => number,
     ) {
-        let docList = docs.filter(child => !child.excludeFromLibrary && (key !== "data" || !child.isMinimized));
+        let docList = docs.filter(child => !child.excludeFromLibrary && (key !== this.fieldKey || !child.isMinimized));
         let rowWidth = () => panelWidth() - 20;
         return docList.map((child, i) => {
             let indent = i === 0 ? undefined : () => {
@@ -478,11 +489,11 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                 ref={this.createTreeDropTarget}>
                 <div className="coll-title">
                     <EditableView
-                        contents={this.props.Document.title}
+                        contents={this.props.DataDoc.title}
                         display={"inline"}
                         height={72}
-                        GetValue={() => StrCast(this.props.Document.title)}
-                        SetValue={(value: string) => (Doc.GetProto(this.props.Document).title = value) ? true : true}
+                        GetValue={() => StrCast(this.props.DataDoc.title)}
+                        SetValue={(value: string) => (Doc.GetProto(this.props.DataDoc).title = value) ? true : true}
                         OnFillDown={(value: string) => {
                             Doc.GetProto(this.props.Document).title = value;
                             let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1a92d7152..5530d5c01 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -536,7 +536,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
     @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
     @computed get contents() {
-        console.log("dv = " + this.props.Document.title + " " + this.props.DataDoc.title);
         return (
             <DocumentContentsView {...this.props} isSelected={this.isSelected} select={this.select} selectOnLoad={this.props.selectOnLoad} layoutKey={"layout"} />);
     }
@@ -545,6 +544,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (this.Document.hidden) {
             return null;
         }
+        let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor;
         var scaling = this.props.ContentScaling();
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
@@ -559,7 +559,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                         `${1 * this.props.ScreenToLocalTransform().Scale}px`
                         : "0px",
                     borderRadius: "inherit",
-                    background: this.Document.backgroundColor || "",
+                    background: backgroundColor || "",
                     width: nativeWidth,
                     height: nativeHeight,
                     transform: `scale(${scaling}, ${scaling})`
-- 
cgit v1.2.3-70-g09d2


From 45ded4da2592bc2a8201e1260dfb6a80485684c7 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Tue, 25 Jun 2019 09:35:09 -0400
Subject: converted isTopMost into renderDepth and capped renderDepth at 7. 
 removed previous cycle detection.

---
 src/client/documents/Documents.ts                  |  6 +--
 src/client/util/DragManager.ts                     |  2 -
 src/client/views/DocumentDecorations.tsx           |  2 +-
 src/client/views/MainOverlayTextBox.tsx            |  2 +-
 src/client/views/MainView.tsx                      |  2 +-
 .../views/collections/CollectionBaseView.tsx       | 48 ++++------------------
 .../views/collections/CollectionDockingView.tsx    |  2 +-
 .../views/collections/CollectionSchemaView.tsx     |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  8 ++--
 src/client/views/nodes/DocumentContentsView.tsx    |  1 +
 src/client/views/nodes/DocumentView.tsx            | 11 ++---
 src/client/views/nodes/FieldView.tsx               |  4 +-
 src/client/views/nodes/KeyValuePair.tsx            |  2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 14 files changed, 31 insertions(+), 65 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index b04fc401a..ac8a892b8 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -344,7 +344,7 @@ export namespace Docs {
         {layout}
     </div>
     <div style="height:(100% + 25px); width:100%; position:absolute">
-        <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+        <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/>
     </div>
 </div>       
         `);
@@ -356,7 +356,7 @@ export namespace Docs {
             {layout}
         </div>
         <div style="height:25px; width:100%; position:absolute">
-            <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+            <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/>
         </div>
     </div>       
             `);
@@ -379,7 +379,7 @@ export namespace Docs {
             {layout}
         </div>
         <div style="height:15%; width:100%; position:absolute">
-            <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} isTopMost={isTopMost}/>
+            <FormattedTextBox doc={Document} DocumentViewForField={DocumentView} bindings={bindings} fieldKey={"caption"} isSelected={isSelected} select={select} selectOnLoad={SelectOnLoad} renderDepth={renderDepth}/>
         </div>
     </div>       
             `);
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 3143924fc..442187912 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -28,7 +28,6 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
         document.removeEventListener('pointerup', onRowUp);
     };
     let onItemDown = async (e: React.PointerEvent) => {
-        // if (this.props.isSelected() || this.props.isTopMost) {
         if (e.button === 0) {
             e.stopPropagation();
             if (e.shiftKey && CollectionDockingView.Instance) {
@@ -38,7 +37,6 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
                 document.addEventListener("pointerup", onRowUp);
             }
         }
-        //}
     };
     return onItemDown;
 }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 9d5844426..042306997 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -153,7 +153,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
     @computed
     get Bounds(): { x: number, y: number, b: number, r: number } {
         return SelectionManager.SelectedDocuments().reduce((bounds, documentView) => {
-            if (documentView.props.isTopMost) {
+            if (documentView.props.renderDepth === 0) {
                 return bounds;
             }
             let transform = (documentView.props.ScreenToLocalTransform().scale(documentView.props.ContentScaling())).inverse();
diff --git a/src/client/views/MainOverlayTextBox.tsx b/src/client/views/MainOverlayTextBox.tsx
index b2cfc82c4..7363b08ef 100644
--- a/src/client/views/MainOverlayTextBox.tsx
+++ b/src/client/views/MainOverlayTextBox.tsx
@@ -123,7 +123,7 @@ export class MainOverlayTextBox extends React.Component<MainOverlayTextBoxProps>
                         <FormattedTextBox color={`${this._textColor}`} fieldKey={this.TextFieldKey} hideOnLeave={this._textHideOnLeave} isOverlay={true}
                             Document={FormattedTextBox.InputBoxOverlay.props.Document}
                             DataDoc={FormattedTextBox.InputBoxOverlay.props.DataDoc}
-                            isSelected={returnTrue} select={emptyFunction} isTopMost={true} selectOnLoad={true}
+                            isSelected={returnTrue} select={emptyFunction} renderDepth={0} selectOnLoad={true}
                             ContainingCollectionView={undefined} whenActiveChanged={emptyFunction} active={returnTrue}
                             ScreenToLocalTransform={this._textXf} PanelWidth={returnZero} PanelHeight={returnZero} focus={emptyFunction} addDocTab={this.addDocTab} outer_div={(dominus: HTMLElement) => this._dominus = dominus} />
                     </div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index a72f25b99..d26e24748 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -211,7 +211,7 @@ export class MainView extends React.Component {
                 ContentScaling={returnOne}
                 PanelWidth={this.getPWidth}
                 PanelHeight={this.getPHeight}
-                isTopMost={true}
+                renderDepth={0}
                 selectOnLoad={false}
                 focus={emptyFunction}
                 parentActive={returnTrue}
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index 79a9f3be0..50d1a5071 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -64,8 +64,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
 
     active = (): boolean => {
         var isSelected = this.props.isSelected();
-        var topMost = this.props.isTopMost;
-        return isSelected || this._isChildActive || topMost;
+        return isSelected || this._isChildActive || this.props.renderDepth === 0;
     }
 
     //TODO should this be observable?
@@ -75,32 +74,6 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         this.props.whenActiveChanged(isActive);
     }
 
-    createsCycle(documentToAdd: Doc, containerDocument: Doc): boolean {
-        if (!(documentToAdd instanceof Doc)) {
-            return false;
-        }
-        if (StrCast(documentToAdd.layout).indexOf("CollectionView") !== -1) {
-            let data = DocListCast(documentToAdd.data);
-            for (const doc of data) {
-                if (this.createsCycle(doc, containerDocument)) {
-                    return true;
-                }
-            }
-        }
-        let annots = DocListCast(documentToAdd.annotations);
-        for (const annot of annots) {
-            if (this.createsCycle(annot, containerDocument)) {
-                return true;
-            }
-        }
-        for (let containerProto: Opt<Doc> = containerDocument; containerProto !== undefined; containerProto = FieldValue(containerProto.proto)) {
-            if (containerProto[Id] === documentToAdd[Id]) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     @action.bound
     addDocument(doc: Doc, allowDuplicates: boolean = false): boolean {
         var curPage = NumCast(this.props.Document.curPage, -1);
@@ -109,19 +82,16 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
             Doc.GetProto(doc).annotationOn = this.props.Document;
         }
         allowDuplicates = true;
-        if (!this.createsCycle(doc, this.dataDoc)) {
-            //TODO This won't create the field if it doesn't already exist
-            const value = Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc));
-            if (value !== undefined) {
-                if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
-                    value.push(doc);
-                }
-            } else {
-                Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new List([doc]));
+        //TODO This won't create the field if it doesn't already exist
+        const value = Cast(this.dataDoc[this.props.fieldKey], listSpec(Doc));
+        if (value !== undefined) {
+            if (allowDuplicates || !value.some(v => v instanceof Doc && v[Id] === doc[Id])) {
+                value.push(doc);
             }
-            return true;
+        } else {
+            Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new List([doc]));
         }
-        return false;
+        return true;
     }
 
     @action.bound
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 82cb5dbbb..6d2f61173 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -535,7 +535,7 @@ export class DockedFrameRenderer extends React.Component<DockedFrameProps> {
                     PanelWidth={this.nativeWidth}
                     PanelHeight={this.nativeHeight}
                     ScreenToLocalTransform={this.ScreenToLocalTransform}
-                    isTopMost={true}
+                    renderDepth={0}
                     selectOnLoad={false}
                     parentActive={returnTrue}
                     whenActiveChanged={emptyFunction}
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index b4158a5b1..f03afe7e0 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -104,7 +104,7 @@ export class CollectionSchemaView extends CollectionSubView(doc => doc) {
             ContainingCollectionView: this.props.CollectionView,
             isSelected: returnFalse,
             select: emptyFunction,
-            isTopMost: false,
+            renderDepth: this.props.renderDepth + 1,
             selectOnLoad: false,
             ScreenToLocalTransform: Transform.Identity,
             focus: emptyFunction,
@@ -454,7 +454,7 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                 <div className="collectionSchemaView-previewDoc" style={{ transform: `translate(${this.centeringOffset}px, 0px)`, height: "100%" }}>
                     <DocumentView
                         DataDoc={this.props.DataDocument}
-                        Document={this.props.Document} isTopMost={false} selectOnLoad={false}
+                        Document={this.props.Document} renderDepth={0} selectOnLoad={false}
                         addDocument={this.props.addDocument} removeDocument={this.props.removeDocument} moveDocument={this.props.moveDocument}
                         ScreenToLocalTransform={this.getTransform}
                         ContentScaling={this.contentScaling}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index c6f003a81..05909c9f7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -199,7 +199,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             var dv = DocumentManager.Instance.getDocumentView(doc);
             return dv && SelectionManager.IsSelected(dv) ? true : false;
         });
-        if (!this.props.isSelected() && !childSelected && !this.props.isTopMost) {
+        if (!this.props.isSelected() && !childSelected && this.props.renderDepth > 0) {
             return;
         }
         e.stopPropagation();
@@ -303,7 +303,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
             removeDocument: this.props.removeDocument,
             moveDocument: this.props.moveDocument,
             ScreenToLocalTransform: this.getTransform,
-            isTopMost: false,
+            renderDepth: this.props.renderDepth + 1,
             selectOnLoad: layoutDoc[Id] === this._selectOnLoaded,
             PanelWidth: layoutDoc[WidthSym],
             PanelHeight: layoutDoc[HeightSym],
@@ -404,7 +404,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 class CollectionFreeFormOverlayView extends React.Component<DocumentViewProps & { isSelected: () => boolean }> {
     @computed get overlayView() {
         return (<DocumentContentsView {...this.props} layoutKey={"overlayLayout"}
-            isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
     }
     render() {
         return this.overlayView;
@@ -416,7 +416,7 @@ class CollectionFreeFormBackgroundView extends React.Component<DocumentViewProps
     @computed get backgroundView() {
         let props = this.props;
         return (<DocumentContentsView {...this.props} layoutKey={"backgroundLayout"}
-            isTopMost={this.props.isTopMost} isSelected={this.props.isSelected} select={emptyFunction} />);
+            renderDepth={this.props.renderDepth} isSelected={this.props.isSelected} select={emptyFunction} />);
     }
     render() {
         return this.props.Document.backgroundLayout ? this.backgroundView : (null);
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index a4316c143..0da4888a1 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -103,6 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     }
 
     render() {
+        if (this.props.renderDepth > 7) return (null);
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
             components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5530d5c01..c586885a3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -72,7 +72,7 @@ export interface DocumentViewProps {
     removeDocument?: (doc: Doc) => boolean;
     moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
     ScreenToLocalTransform: () => Transform;
-    isTopMost: boolean;
+    renderDepth: number;
     ContentScaling: () => number;
     PanelWidth: () => number;
     PanelHeight: () => number;
@@ -120,7 +120,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     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.isTopMost; }
+    @computed get topMost(): boolean { return this.props.renderDepth === 0; }
     @computed get templates(): List<string> {
         let field = this.props.Document.templates;
         if (field && field instanceof List) {
@@ -268,7 +268,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         e.stopPropagation();
         let altKey = e.altKey;
         let ctrlKey = e.ctrlKey;
-        if (this._doubleTap && !this.props.isTopMost) {
+        if (this._doubleTap && !this.props.renderDepth) {
             this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
             SelectionManager.DeselectAll();
             this.props.Document.libraryBrush = false;
@@ -436,7 +436,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @action
     addTemplate = (template: Template) => {
         this.templates.push(template.Layout);
-        this.templates = this.templates;
     }
 
     @action
@@ -447,7 +446,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 break;
             }
         }
-        this.templates = this.templates;
     }
 
     freezeNativeDimensions = (): void => {
@@ -549,7 +547,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         return (
-            <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
+            <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
                 ref={this._mainCont}
                 style={{
                     outlineColor: "maroon",
@@ -565,7 +563,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     transform: `scale(${scaling}, ${scaling})`
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
             >
                 {this.contents}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 8879da55f..374a523cb 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -33,7 +33,7 @@ export interface FieldViewProps {
     DataDoc: Doc;
     isSelected: () => boolean;
     select: (isCtrlPressed: boolean) => void;
-    isTopMost: boolean;
+    renderDepth: number;
     selectOnLoad: boolean;
     addDocument?: (document: Doc, allowDuplicates?: boolean) => boolean;
     addDocTab: (document: Doc, dataDoc: Doc, where: string) => void;
@@ -97,7 +97,7 @@ export class FieldView extends React.Component<FieldViewProps> {
             //         ContentScaling={returnOne}
             //         PanelWidth={returnHundred}
             //         PanelHeight={returnHundred}
-            //         isTopMost={true} //TODO Why is this top most?
+            //         renderDepth={0} //TODO Why is renderDepth reset?
             //         selectOnLoad={false}
             //         focus={emptyFunction}
             //         isSelected={this.props.isSelected}
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 3e03e7e75..3dda81db7 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -32,7 +32,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             fieldKey: this.props.keyName,
             isSelected: returnFalse,
             select: emptyFunction,
-            isTopMost: false,
+            renderDepth: 1,
             selectOnLoad: false,
             active: returnFalse,
             whenActiveChanged: emptyFunction,
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b9e93b4da..10c66d318 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -492,7 +492,7 @@ interface IAnnotationProps {
 //                         <DocumentView Document={targetDoc}
 //                             ContainingCollectionView={undefined}
 //                             ScreenToLocalTransform={this.props.parent.props.parent.props.ScreenToLocalTransform}
-//                             isTopMost={false}
+//                             renderDepth={1} // what should renderDepth be?
 //                             ContentScaling={() => 1}
 //                             PanelWidth={() => NumCast(this.props.parent.props.parent.Document.nativeWidth)}
 //                             PanelHeight={() => NumCast(this.props.parent.props.parent.Document.nativeHeight)}
-- 
cgit v1.2.3-70-g09d2


From e910a6cd56936234e451994c893d8592e430f828 Mon Sep 17 00:00:00 2001
From: yipstanley <stanley_yip@brown.edu>
Date: Tue, 25 Jun 2019 13:54:20 -0400
Subject: pdf view specs fixes/changes

---
 src/client/views/collections/CollectionPDFView.tsx |  4 ++
 src/client/views/nodes/DocumentView.tsx            | 13 +++-
 src/client/views/nodes/PDFBox.tsx                  | 44 ++++++++++---
 src/client/views/pdf/PDFMenu.scss                  |  7 ++
 src/client/views/pdf/PDFMenu.tsx                   | 55 +++++++++++-----
 src/client/views/pdf/PDFViewer.tsx                 | 75 ++++++++++++++++------
 6 files changed, 152 insertions(+), 46 deletions(-)

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

diff --git a/src/client/views/collections/CollectionPDFView.tsx b/src/client/views/collections/CollectionPDFView.tsx
index b2d016934..31a73ab36 100644
--- a/src/client/views/collections/CollectionPDFView.tsx
+++ b/src/client/views/collections/CollectionPDFView.tsx
@@ -43,6 +43,10 @@ export class CollectionPDFView extends React.Component<FieldViewProps> {
         );
     }
 
+    componentWillUnmount() {
+        this._reactionDisposer && this._reactionDisposer();
+    }
+
     public static LayoutString(fieldKey: string = "data") {
         return FieldView.LayoutString(CollectionPDFView, fieldKey);
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 522c37989..f5a3f4d5d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -117,6 +117,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _mainCont = React.createRef<HTMLDivElement>();
     private _dropDisposer?: DragManager.DragDropDisposer;
 
+    @observable private _opacity: number = this.Document.opacity ? NumCast(this.Document.opacity) : 1;
+
     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.isTopMost; }
@@ -136,6 +138,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     _animateToIconDisposer?: IReactionDisposer;
     _reactionDisposer?: IReactionDisposer;
+    _opacityDisposer?: IReactionDisposer;
     @action
     componentDidMount() {
         if (this._mainCont.current) {
@@ -160,6 +163,12 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
             , { fireImmediately: true });
         DocumentManager.Instance.DocumentViews.push(this);
+        this._opacityDisposer = reaction(
+            () => NumCast(this.props.Document.opacity),
+            () => {
+                runInAction(() => this._opacity = NumCast(this.props.Document.opacity));
+            }
+        );
     }
 
     animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
@@ -201,6 +210,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (this._reactionDisposer) this._reactionDisposer();
         if (this._animateToIconDisposer) this._animateToIconDisposer();
         if (this._dropDisposer) this._dropDisposer();
+        if (this._opacityDisposer) this._opacityDisposer();
         DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
     }
 
@@ -559,7 +569,8 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     background: this.Document.backgroundColor || "",
                     width: nativeWidth,
                     height: nativeHeight,
-                    transform: `scale(${scaling}, ${scaling})`
+                    transform: `scale(${scaling}, ${scaling})`,
+                    opacity: this._opacity
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
 
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index a129e89b9..c0f2d313a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -18,9 +18,9 @@ import { pageSchema } from "./ImageBox";
 import "./PDFBox.scss";
 import React = require("react");
 import { CompileScript } from '../../util/Scripting';
-import { ScriptField } from '../../../fields/ScriptField';
 import { Flyout, anchorPoints } from '../DocumentDecorations';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { ScriptField } from '../../../new_fields/ScriptField';
 
 type PdfDocument = makeInterface<[typeof positionSchema, typeof pageSchema]>;
 const PdfDocument = makeInterface(positionSchema, pageSchema);
@@ -37,6 +37,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
     private _keyValue: string = "";
     private _valueValue: string = "";
     private _scriptValue: string = "";
+    private _keyRef: React.RefObject<HTMLInputElement>;
+    private _valueRef: React.RefObject<HTMLInputElement>;
+    private _scriptRef: React.RefObject<HTMLInputElement>;
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -51,10 +54,9 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
             }
         );
 
-        let script = CompileScript("return this.page === 0", { params: { this: Doc.name } });
-        if (script.compiled) {
-            this.props.Document.filterScript = new ScriptField(script);
-        }
+        this._keyRef = React.createRef();
+        this._valueRef = React.createRef();
+        this._scriptRef = React.createRef();
     }
 
     componentDidMount() {
@@ -99,17 +101,21 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
         this._valueValue = e.currentTarget.value;
     }
 
+    @action
     private newScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         this._scriptValue = e.currentTarget.value;
     }
 
-    private applyFilter = (e: React.MouseEvent<HTMLButtonElement>) => {
+    private applyFilter = () => {
         let scriptText = "";
         if (this._scriptValue.length > 0) {
             scriptText = this._scriptValue;
         } else if (this._keyValue.length > 0 && this._valueValue.length > 0) {
             scriptText = `return this.${this._keyValue} === ${this._valueValue}`;
         }
+        else {
+            scriptText = "return true";
+        }
         let script = CompileScript(scriptText, { params: { this: Doc.name } });
         if (script.compiled) {
             this.props.Document.filterScript = new ScriptField(script);
@@ -121,6 +127,22 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
         this._flyout = !this._flyout;
     }
 
+    @action
+    private resetFilters = () => {
+        this._keyValue = this._valueValue = "";
+        this._scriptValue = "return true";
+        if (this._keyRef.current) {
+            this._keyRef.current.value = "";
+        }
+        if (this._valueRef.current) {
+            this._valueRef.current.value = "";
+        }
+        if (this._scriptRef.current) {
+            this._scriptRef.current.value = "";
+        }
+        this.applyFilter();
+    }
+
     settingsPanel() {
         return !this.props.active() ? (null) :
             (
@@ -144,14 +166,18 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
                         </div>
                         <div className="pdfBox-settingsFlyout-kvpInput">
                             <input placeholder="Key" className="pdfBox-settingsFlyout-input" onChange={this.newKeyChange}
-                                style={{ gridColumn: 1 }} />
+                                style={{ gridColumn: 1 }} ref={this._keyRef} />
                             <input placeholder="Value" className="pdfBox-settingsFlyout-input" onChange={this.newValueChange}
-                                style={{ gridColumn: 3 }} />
+                                style={{ gridColumn: 3 }} ref={this._valueRef} />
                         </div>
                         <div className="pdfBox-settingsFlyout-kvpInput">
-                            <input placeholder="Custom Script" onChange={this.newScriptChange} style={{ gridColumn: "1 / 4" }} />
+                            <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}>
+                                <FontAwesomeIcon style={{ color: "white" }} icon="trash" size="lg" />
+                                &nbsp; Reset Filters
+                            </button>
                             <button style={{ gridColumn: 3 }} onClick={this.applyFilter}>
                                 <FontAwesomeIcon style={{ color: "white" }} icon="check" size="lg" />
                                 &nbsp; Apply
diff --git a/src/client/views/pdf/PDFMenu.scss b/src/client/views/pdf/PDFMenu.scss
index 22868082a..a4624b1f6 100644
--- a/src/client/views/pdf/PDFMenu.scss
+++ b/src/client/views/pdf/PDFMenu.scss
@@ -22,4 +22,11 @@
         height: 100%;
         transition: width .2s;
     }
+
+    .pdfMenu-addTag {
+        display: grid;
+        width: 200px;
+        padding: 5px;
+        grid-template-columns: 90px 20px 90px;
+    }
 }
\ No newline at end of file
diff --git a/src/client/views/pdf/PDFMenu.tsx b/src/client/views/pdf/PDFMenu.tsx
index a8e176858..e58ad9ccc 100644
--- a/src/client/views/pdf/PDFMenu.tsx
+++ b/src/client/views/pdf/PDFMenu.tsx
@@ -1,9 +1,9 @@
 import React = require("react");
 import "./PDFMenu.scss";
-import { observable, action } from "mobx";
+import { observable, action, runInAction } from "mobx";
 import { observer } from "mobx-react";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { emptyFunction } from "../../../Utils";
+import { emptyFunction, returnZero, returnTrue, returnFalse } from "../../../Utils";
 import { Doc } from "../../../new_fields/Doc";
 import { DragManager } from "../../util/DragManager";
 import { DocUtils } from "../../documents/Documents";
@@ -23,6 +23,7 @@ export default class PDFMenu extends React.Component {
     Highlight: (d: Doc | undefined, color: string | undefined) => void = emptyFunction;
     Delete: () => void = emptyFunction;
     Snippet: (marquee: { left: number, top: number, width: number, height: number }) => void = emptyFunction;
+    AddTag: (key: string, value: string) => boolean = returnFalse;
 
     @observable public Highlighting: boolean = false;
     @observable public Status: "pdf" | "annotation" | "snippet" | "" = "";
@@ -35,6 +36,9 @@ export default class PDFMenu extends React.Component {
     private _mainCont: React.RefObject<HTMLDivElement>;
     private _dragging: boolean = false;
     private _snippetButton: React.RefObject<HTMLButtonElement>;
+    @observable private _keyValue: string = "";
+    @observable private _valueValue: string = "";
+    @observable private _added: boolean = false;;
 
     constructor(props: Readonly<{}>) {
         super(props);
@@ -209,35 +213,56 @@ export default class PDFMenu extends React.Component {
         e.preventDefault();
     }
 
+    @action
+    keyChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._keyValue = e.currentTarget.value;
+    }
+
+    @action
+    valueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
+        this._valueValue = e.currentTarget.value;
+    }
+
+    @action
+    addTag = (e: React.PointerEvent) => {
+        if (this._keyValue.length > 0 && this._valueValue.length > 0) {
+            this._added = this.AddTag(this._keyValue, this._valueValue);
+
+            setTimeout(
+                () => {
+                    runInAction(() => {
+                        this._added = false;
+                    });
+                }, 1000
+            );
+        }
+    }
+
     render() {
         let buttons = this.Status === "pdf" || this.Status === "snippet" ? [
-            <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked}
+            <button className="pdfMenu-button" title="Click to Highlight" onClick={this.highlightClicked} key="1"
                 style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
                 <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
             </button>,
-            <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>,
+            <button className="pdfMenu-button" title="Drag to Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" key="2" /></button>,
             this.Status === "snippet" ? <button className="pdfMenu-button" title="Drag to Snippetize Selection" onPointerDown={this.snippetStart} ref={this._snippetButton}><FontAwesomeIcon icon="cut" size="lg" /></button> : undefined,
-            <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin}
+            <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin} key="3"
                 style={this.Pinned ? { backgroundColor: "#121212" } : {}}>
                 <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this.Pinned ? "rotate(45deg)" : "" }} />
             </button>
         ] : [
-                <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" /></button>
+                <button className="pdfMenu-button" title="Delete Anchor" onPointerDown={this.deleteClicked}><FontAwesomeIcon icon="trash-alt" size="lg" key="1" /></button>,
+                <div className="pdfMenu-addTag" key="2">
+                    <input onChange={this.keyChanged} placeholder="Key" style={{ gridColumn: 1 }} />
+                    <input onChange={this.valueChanged} placeholder="Value" style={{ gridColumn: 3 }} />
+                </div>,
+                <button className="pdfMenu-button" title={`Add tag: ${this._keyValue} with value: ${this._valueValue}`} onPointerDown={this.addTag}><FontAwesomeIcon style={{ transition: "all .2s" }} color={this._added ? "#42f560" : "white"} icon="check" size="lg" key="3" /></button>,
             ];
 
         return (
             <div className="pdfMenu-cont" onPointerLeave={this.pointerLeave} onPointerEnter={this.pointerEntered} ref={this._mainCont} onContextMenu={this.handleContextMenu}
                 style={{ left: this._left, top: this._top, opacity: this._opacity, transition: this._transition, transitionDelay: this._transitionDelay }}>
                 {buttons}
-                {/* <button className="pdfMenu-button" title="Highlight" onClick={this.highlightClicked}
-                    style={this.Highlighting ? { backgroundColor: "#121212" } : {}}>
-                    <FontAwesomeIcon icon="highlighter" size="lg" style={{ transition: "transform 0.1s", transform: this.Highlighting ? "" : "rotate(-45deg)" }} />
-                </button>
-                <button className="pdfMenu-button" title="Annotate" onPointerDown={this.pointerDown}><FontAwesomeIcon icon="comment-alt" size="lg" /></button>
-                <button className="pdfMenu-button" title="Pin Menu" onClick={this.togglePin}
-                    style={this._pinned ? { backgroundColor: "#121212" } : {}}>
-                    <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transition: "transform 0.1s", transform: this._pinned ? "rotate(45deg)" : "" }} />
-                </button> */}
                 <div className="pdfMenu-dragger" onPointerDown={this.dragStart} style={{ width: this.Pinned ? "20px" : "0px" }} />
             </div >
         );
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 1eab13bc5..3df7dd77b 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -20,8 +20,8 @@ import "./PDFViewer.scss";
 import React = require("react");
 import PDFMenu from "./PDFMenu";
 import { UndoManager } from "../../util/UndoManager";
-import { ScriptField } from "../../../fields/ScriptField";
 import { CompileScript, CompiledScript, CompileResult } from "../../util/Scripting";
+import { ScriptField } from "../../../new_fields/ScriptField";
 
 export const scale = 2;
 interface IPDFViewerProps {
@@ -63,8 +63,6 @@ interface IViewerProps {
     url: string;
 }
 
-const PinRadius = 25;
-
 /**
  * Handles rendering and virtualization of the pdf
  */
@@ -85,14 +83,18 @@ class Viewer extends React.Component<IViewerProps> {
     private _annotationReactionDisposer?: IReactionDisposer;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _filterReactionDisposer?: IReactionDisposer;
+    private _viewer: React.RefObject<HTMLDivElement>;
+    private _mainCont: React.RefObject<HTMLDivElement>;
+    private _textContent: Pdfjs.TextContent[] = [];
 
-    @action
-        constructor(props: IViewerProps) {
-            super(props);
+    constructor(props: IViewerProps) {
+        super(props);
 
-            let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
-            this._script = scriptfield ? CompileScript(scriptfield.scriptString, { params: { this: Doc.name } }) : CompileScript("return true");;
-        }
+        let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
+        this._script = scriptfield ? scriptfield.script : CompileScript("return true");
+        this._viewer = React.createRef();
+        this._mainCont = React.createRef();
+    }
 
     componentDidUpdate = (prevProps: IViewerProps) => {
         if (this.scrollY !== prevProps.scrollY) {
@@ -118,21 +120,37 @@ class Viewer extends React.Component<IViewerProps> {
 
         if (this.props.parent.props.ContainingCollectionView) {
             this._filterReactionDisposer = reaction(
-                () => this.props.parent.Document.filterScript || this.props.parent.props.ContainingCollectionView!.props.Document.filterScript,
+                () => this.props.parent.Document.filterScript,
                 () => {
                     runInAction(() => {
                         let scriptfield = Cast(this.props.parent.Document.filterScript, ScriptField);
-                        this._script = scriptfield ? CompileScript(scriptfield.scriptString, { params: { this: Doc.name } }) : CompileScript("return true");;
+                        this._script = scriptfield ? scriptfield.script : CompileScript("return true");
+                        if (this.props.parent.props.ContainingCollectionView) {
+                            let ccvAnnos = DocListCast(this.props.parent.props.ContainingCollectionView.props.Document.annotations);
+                            ccvAnnos.forEach(d => {
+                                if (this._script && this._script.compiled) {
+                                    let run = this._script.run(d);
+                                    if (run.success) {
+                                        d.opacity = run.result ? 1 : 0;
+                                    }
+                                }
+                            })
+                        }
                     });
                 }
             );
         }
+
+        if (this._mainCont.current) {
+            this._dropDisposer = this._mainCont.current && DragManager.MakeDropTarget(this._mainCont.current, { handlers: { drop: this.drop.bind(this) } });
+        }
     }
 
     componentWillUnmount = () => {
         this._reactionDisposer && this._reactionDisposer();
         this._annotationReactionDisposer && this._annotationReactionDisposer();
         this._filterReactionDisposer && this._filterReactionDisposer();
+        this._dropDisposer && this._dropDisposer();
     }
 
     @action
@@ -140,10 +158,14 @@ class Viewer extends React.Component<IViewerProps> {
         if (this._pageSizes.length === 0) {
             let pageSizes = Array<{ width: number, height: number }>(this.props.pdf.numPages);
             this._isPage = Array<string>(this.props.pdf.numPages);
+            this._textContent = Array<Pdfjs.TextContent>(this.props.pdf.numPages);
             for (let i = 0; i < this.props.pdf.numPages; i++) {
                 await this.props.pdf.getPage(i + 1).then(page => runInAction(() => {
                     // pageSizes[i] = { width: page.view[2] * scale, height: page.view[3] * scale };
                     let x = page.getViewport(scale);
+                    page.getTextContent().then((text: Pdfjs.TextContent) => {
+                        this._textContent[i] = text;
+                    })
                     pageSizes[i] = { width: x.width, height: x.height };
                 }));
             }
@@ -162,13 +184,6 @@ class Viewer extends React.Component<IViewerProps> {
         }
     }
 
-    private mainCont = (div: HTMLDivElement | null) => {
-        this._dropDisposer && this._dropDisposer();
-        if (div) {
-            this._dropDisposer = div && DragManager.MakeDropTarget(div, { handlers: { drop: this.drop.bind(this) } });
-        }
-    }
-
     makeAnnotationDocument = (sourceDoc: Doc | undefined, s: number, color: string): Doc => {
         let annoDocs: Doc[] = [];
         let mainAnnoDoc = Docs.CreateInstance(new Doc(), "", {});
@@ -222,6 +237,7 @@ class Viewer extends React.Component<IViewerProps> {
     pageLoaded = (index: number, page: Pdfjs.PDFPageViewport): void => {
         this.props.loaded(page.width, page.height, this.props.pdf.numPages);
     }
+
     @action
     getPlaceholderPage = (page: number) => {
         if (this._isPage[page] !== "none") {
@@ -232,6 +248,7 @@ class Viewer extends React.Component<IViewerProps> {
             );
         }
     }
+
     @action
     getRenderedPage = (page: number) => {
         if (this._isPage[page] !== "page") {
@@ -374,11 +391,16 @@ class Viewer extends React.Component<IViewerProps> {
         return res;
     }
 
+    pointerDown = () => {
+
+        let x = this._textContent;
+    }
+
     render() {
         let compiled = this._script;
         return (
-            <div ref={this.mainCont} style={{ pointerEvents: "all" }}>
-                <div className="viewer">
+            <div ref={this._mainCont} style={{ pointerEvents: "all" }} onPointerDown={this.pointerDown}>
+                <div className="viewer" ref={this._viewer}>
                     {this._visibleElements}
                 </div>
                 <div className="pdfViewer-annotationLayer"
@@ -472,12 +494,23 @@ class RegionAnnotation extends React.Component<IAnnotationProps> {
         }
         if (e.button === 2) {
             PDFMenu.Instance.Status = "annotation";
-            PDFMenu.Instance.Delete = this.deleteAnnotation;
+            PDFMenu.Instance.Delete = this.deleteAnnotation.bind(this);
             PDFMenu.Instance.Pinned = false;
+            PDFMenu.Instance.AddTag = this.addTag.bind(this);
             PDFMenu.Instance.jumpTo(e.clientX, e.clientY, true);
         }
     }
 
+    addTag = (key: string, value: string): boolean => {
+        let group = FieldValue(Cast(this.props.document.group, Doc));
+        if (group) {
+            let valNum = parseInt(value);
+            group[key] = isNaN(valNum) ? value : valNum;
+            return true;
+        }
+        return false;
+    }
+
     render() {
         return (
             <div className="pdfViewer-annotationBox" onPointerDown={this.onPointerDown} ref={this._mainCont}
-- 
cgit v1.2.3-70-g09d2


From 41cf1e8536964764f18ab752140e484e36cbe464 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 25 Jun 2019 17:09:36 -0400
Subject: links can save

---
 src/client/documents/Documents.ts                  |  58 +---
 src/client/util/DocumentManager.ts                 |  14 +-
 src/client/util/DragManager.ts                     |  24 +-
 src/client/util/LinkManager.ts                     | 239 +++++++++++----
 src/client/views/DocumentDecorations.tsx           |   2 +-
 src/client/views/MainView.tsx                      |   7 +
 .../views/collections/CollectionDockingView.tsx    |   4 +-
 .../views/collections/CollectionTreeView.tsx       |  18 +-
 .../CollectionFreeFormLinksView.tsx                |   7 +-
 src/client/views/nodes/DocumentView.tsx            |   8 +-
 src/client/views/nodes/LinkButtonBox.scss          |  34 +--
 src/client/views/nodes/LinkButtonBox.tsx           | 126 ++++----
 src/client/views/nodes/LinkEditor.tsx              | 323 +++++++++------------
 src/client/views/nodes/LinkMenu.tsx                |   2 +-
 src/client/views/nodes/LinkMenuGroup.tsx           |   8 +-
 src/client/views/nodes/LinkMenuItem.tsx            |   6 +-
 src/new_fields/Doc.ts                              |   9 +-
 src/new_fields/LinkButtonField.ts                  |  58 ++--
 18 files changed, 503 insertions(+), 444 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 64032e096..fbd96fb66 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -35,8 +35,8 @@ import { DateField } from "../../new_fields/DateField";
 import { UndoManager } from "../util/UndoManager";
 import { RouteStore } from "../../server/RouteStore";
 import { LinkManager } from "../util/LinkManager";
-import { LinkButtonBox } from "../views/nodes/LinkButtonBox";
-import { LinkButtonField, LinkButtonData } from "../../new_fields/LinkButtonField";
+// import { LinkButtonBox } from "../views/nodes/LinkButtonBox";
+// import { LinkButtonField, LinkButtonData } from "../../new_fields/LinkButtonField";
 import { DocumentManager } from "../util/DocumentManager";
 import { Id } from "../../new_fields/FieldSymbols";
 var requestImageSize = require('request-image-size');
@@ -100,6 +100,7 @@ export namespace DocUtils {
             let linkDoc = Docs.TextDocument({ width: 100, height: 30, borderRounding: -1 });
             let linkDocProto = Doc.GetProto(linkDoc);
 
+            linkDocProto.context = targetContext;
             linkDocProto.title = title; //=== "" ? source.title + " to " + target.title : title;
             linkDocProto.linkDescription = description;
             linkDocProto.linkTags = tags;
@@ -111,36 +112,7 @@ export namespace DocUtils {
             linkDocProto.anchor2Page = target.curPage;
             linkDocProto.anchor2Groups = new List<Doc>([]);
 
-            linkDocProto.context = targetContext;
-
-            let sourceViews = DocumentManager.Instance.getDocumentViews(source);
-            let targetViews = DocumentManager.Instance.getDocumentViews(target);
-            sourceViews.forEach(sv => {
-                targetViews.forEach(tv => {
-
-                    // TODO: do only for when diff contexts
-                    let proxy1 = Docs.LinkButtonDocument(
-                        { sourceViewId: StrCast(sv.props.Document[Id]), targetViewId: StrCast(tv.props.Document[Id]) },
-                        { width: 200, height: 100, borderRounding: 0 });
-                    let proxy1Proto = Doc.GetProto(proxy1);
-                    proxy1Proto.sourceViewId = StrCast(sv.props.Document[Id]);
-                    proxy1Proto.targetViewId = StrCast(tv.props.Document[Id]);
-                    proxy1Proto.isLinkButton = true;
-
-                    let proxy2 = Docs.LinkButtonDocument(
-                        { sourceViewId: StrCast(tv.props.Document[Id]), targetViewId: StrCast(sv.props.Document[Id]) },
-                        { width: 200, height: 100, borderRounding: 0 });
-                    let proxy2Proto = Doc.GetProto(proxy2);
-                    proxy2Proto.sourceViewId = StrCast(tv.props.Document[Id]);
-                    proxy2Proto.targetViewId = StrCast(sv.props.Document[Id]);
-                    proxy2Proto.isLinkButton = true;
-
-                    LinkManager.Instance.linkProxies.push(proxy1);
-                    LinkManager.Instance.linkProxies.push(proxy2);
-                });
-            });
-
-            LinkManager.Instance.allLinks.push(linkDoc);
+            LinkManager.Instance.addLink(linkDoc);
 
             return linkDoc;
         }, "make link");
@@ -160,7 +132,7 @@ export namespace Docs {
     let audioProto: Doc;
     let pdfProto: Doc;
     let iconProto: Doc;
-    let linkProto: Doc;
+    // let linkProto: Doc;
     const textProtoId = "textProto";
     const histoProtoId = "histoProto";
     const pdfProtoId = "pdfProto";
@@ -171,7 +143,7 @@ export namespace Docs {
     const videoProtoId = "videoProto";
     const audioProtoId = "audioProto";
     const iconProtoId = "iconProto";
-    const linkProtoId = "linkProto";
+    // const linkProtoId = "linkProto";
 
     export function initProtos(): Promise<void> {
         return DocServer.GetRefFields([textProtoId, histoProtoId, collProtoId, imageProtoId, webProtoId, kvpProtoId, videoProtoId, audioProtoId, pdfProtoId, iconProtoId]).then(fields => {
@@ -185,7 +157,7 @@ export namespace Docs {
             audioProto = fields[audioProtoId] as Doc || CreateAudioPrototype();
             pdfProto = fields[pdfProtoId] as Doc || CreatePdfPrototype();
             iconProto = fields[iconProtoId] as Doc || CreateIconPrototype();
-            linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype();
+            // linkProto = fields[linkProtoId] as Doc || CreateLinkPrototype();
         });
     }
 
@@ -218,11 +190,11 @@ export namespace Docs {
             { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
         return iconProto;
     }
-    function CreateLinkPrototype(): Doc {
-        let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
-            { x: 0, y: 0, width: 300 });
-        return linkProto;
-    }
+    // function CreateLinkPrototype(): Doc {
+    //     let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
+    //         { x: 0, y: 0, width: 300 });
+    //     return linkProto;
+    // }
     function CreateTextPrototype(): Doc {
         let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
             { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
@@ -309,9 +281,9 @@ export namespace Docs {
     export function IconDocument(icon: string, options: DocumentOptions = {}) {
         return CreateInstance(iconProto, new IconField(icon), options);
     }
-    export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
-        return CreateInstance(linkProto, new LinkButtonField(data), options);
-    }
+    // export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
+    //     return CreateInstance(linkProto, new LinkButtonField(data), options);
+    // }
     export function PdfDocument(url: string, options: DocumentOptions = {}) {
         return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
     }
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 89e6183d6..767abe63f 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -85,12 +85,11 @@ export class DocumentManager {
 
     @computed
     public get LinkedDocumentViews() {
-        console.log("link");
-        return DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
-            let linksList = LinkManager.Instance.findAllRelatedLinks(dv.props.Document);
+        let pairs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false)).reduce((pairs, dv) => {
+            let linksList = LinkManager.Instance.getAllRelatedLinks(dv.props.Document);
             pairs.push(...linksList.reduce((pairs, link) => {
                 if (link) {
-                    let linkToDoc = LinkManager.Instance.findOppositeAnchor(link, dv.props.Document);
+                    let linkToDoc = LinkManager.Instance.getOppositeAnchor(link, dv.props.Document);
                     DocumentManager.Instance.getDocumentViews(linkToDoc).map(docView1 => {
                         pairs.push({ a: dv, b: docView1, l: link });
                     });
@@ -100,6 +99,13 @@ export class DocumentManager {
             // }
             return pairs;
         }, [] as { a: DocumentView, b: DocumentView, l: Doc }[]);
+
+        // console.log("LINKED DOCUMENT VIEWS");
+        // pairs.forEach(p => {
+        //     console.log(StrCast(p.a.Document.title), p.a.props.Document[Id], StrCast(p.b.Document.title), p.b.props.Document[Id]);
+        // });
+
+        return pairs;
     }
 
 
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f4c8adc8e..1aacf2c53 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -46,7 +46,7 @@ export function SetupDrag(_reference: React.RefObject<HTMLElement>, docFunc: ()
 }
 
 export async function DragLinkAsDocument(dragEle: HTMLElement, x: number, y: number, linkDoc: Doc, sourceDoc: Doc) {
-    let draggeddoc = LinkManager.Instance.findOppositeAnchor(linkDoc, sourceDoc);
+    let draggeddoc = LinkManager.Instance.getOppositeAnchor(linkDoc, sourceDoc);
 
     let moddrag = await Cast(draggeddoc.annotationOn, Doc);
     let dragData = new DragManager.DocumentDragData(moddrag ? [moddrag] : [draggeddoc]);
@@ -66,10 +66,10 @@ export async function DragLinksAsDocuments(dragEle: HTMLElement, x: number, y: n
     // TODO: if not in same context then don't drag
 
     if (srcTarg) {
-        let linkDocs = LinkManager.Instance.findAllRelatedLinks(srcTarg);
+        let linkDocs = LinkManager.Instance.getAllRelatedLinks(srcTarg);
         if (linkDocs) {
             draggedDocs = linkDocs.map(link => {
-                return LinkManager.Instance.findOppositeAnchor(link, sourceDoc);
+                return LinkManager.Instance.getOppositeAnchor(link, sourceDoc);
             });
         }
     }
@@ -236,10 +236,16 @@ export namespace DragManager {
                         if (dv.props.ContainingCollectionView === SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) {
                             return d;
                         } else {
-                            return Doc.MakeAlias(d);
+                            // return d;
+                            let r = Doc.MakeAlias(d);
+                            // DocUtils.MakeLink(sourceDoc, r);
+                            return r;
                         }
                     } else {
-                        return Doc.MakeAlias(d);
+                        // return d;
+                        let r = Doc.MakeAlias(d);
+                        // DocUtils.MakeLink(sourceDoc, r);
+                        return r;
                     }
                     // return (dv && dv.props.ContainingCollectionView !== SelectionManager.SelectedDocuments()[0].props.ContainingCollectionView) || !dv ?
                     //     Doc.MakeAlias(d) : d;
@@ -282,10 +288,10 @@ export namespace DragManager {
         StartDrag([ele], dragData, downX, downY, options);
     }
 
-    export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
-        runInAction(() => StartDragFunctions.map(func => func()));
-        StartDrag([ele], dragData, downX, downY, options);
-    }
+    // export function StartLinkProxyDrag(ele: HTMLElement, dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions) {
+    //     runInAction(() => StartDragFunctions.map(func => func()));
+    //     StartDrag([ele], dragData, downX, downY, options);
+    // }
 
     export let AbortDrag: () => void = emptyFunction;
 
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 745255f31..82c3a9acd 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -1,9 +1,10 @@
 import { observable, action } from "mobx";
-import { StrCast, Cast } from "../../new_fields/Types";
+import { StrCast, Cast, FieldValue } from "../../new_fields/Types";
 import { Doc, DocListCast } from "../../new_fields/Doc";
 import { listSpec } from "../../new_fields/Schema";
 import { List } from "../../new_fields/List";
 import { Id } from "../../new_fields/FieldSymbols";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
 
 
 /* 
@@ -23,6 +24,11 @@ import { Id } from "../../new_fields/FieldSymbols";
  * - user defined kvps 
  */
 export class LinkManager {
+    // static Instance: LinkManager;
+    // private constructor() {
+    //     LinkManager.Instance = this;
+    // }
+
     private static _instance: LinkManager;
     public static get Instance(): LinkManager {
         return this._instance || (this._instance = new this());
@@ -30,25 +36,138 @@ export class LinkManager {
     private constructor() {
     }
 
-    @observable public allLinks: Array<Doc> = []; // list of link docs
-    @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
-    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it hodls
-    @observable public linkProxies: Array<Doc> = []; // list of linkbutton docs - used to visualize link when an anchors are not in the same context
+    public get LinkManagerDoc(): Doc | undefined {
+        return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
+    }
+    // @observable public allLinks: Array<Doc> = []; //List<Doc> = new List<Doc>([]); // list of link docs
+    // @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
+    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it holds
+
+    public getAllLinks(): Doc[] {
+        return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
+    }
+
+    public addLink(linkDoc: Doc): boolean {
+        let linkList = LinkManager.Instance.getAllLinks();
+        linkList.push(linkDoc);
+        console.log("link man doc", LinkManager.Instance.LinkManagerDoc);
+        if (LinkManager.Instance.LinkManagerDoc) {
+            LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+            return true;
+        }
+        return false;
+    }
+
+    public deleteLink(linkDoc: Doc): boolean {
+        let linkList = LinkManager.Instance.getAllLinks();
+        let index = LinkManager.Instance.getAllLinks().indexOf(linkDoc);
+        if (index > -1) {
+            linkList.splice(index, 1);
+            if (LinkManager.Instance.LinkManagerDoc) {
+                LinkManager.Instance.LinkManagerDoc.allLinks = new List<Doc>(linkList);
+                return true;
+            }
+        }
+        return false;
+    }
 
     // finds all links that contain the given anchor
-    public findAllRelatedLinks(anchor: Doc): Array<Doc> {
-        return LinkManager.Instance.allLinks.filter(link => {
+    public getAllRelatedLinks(anchor: Doc): Doc[] {//List<Doc> {
+        let related = LinkManager.Instance.getAllLinks().filter(link => {
             let protomatch1 = Doc.AreProtosEqual(anchor, Cast(link.anchor1, Doc, new Doc));
             let protomatch2 = Doc.AreProtosEqual(anchor, Cast(link.anchor2, Doc, new Doc));
-            // let idmatch1 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor1, Doc, new Doc)[Id]);
-            // let idmatch2 = StrCast(anchor[Id]) === StrCast(Cast(link.anchor2, Doc, new Doc)[Id]);
-            return protomatch1 || protomatch2;// || idmatch1 || idmatch2;
+            return protomatch1 || protomatch2;
         });
+        return related;
+    }
+
+    public addGroupType(groupType: string): boolean {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>([]);
+            let groupTypes = LinkManager.Instance.getAllGroupTypes();
+            groupTypes.push(groupType);
+            LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>(groupTypes);
+            return true;
+        }
+        return false;
+    }
+
+    // removes all group docs from all links with the given group type
+    public deleteGroupType(groupType: string): boolean {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+                LinkManager.Instance.LinkManagerDoc[groupType] = undefined;
+                LinkManager.Instance.getAllLinks().forEach(linkDoc => {
+                    LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
+                    LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
+                });
+            }
+            return true;
+        } else return false;
+    }
+
+    public getAllGroupTypes(): string[] {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            if (LinkManager.Instance.LinkManagerDoc.allGroupTypes) {
+                return Cast(LinkManager.Instance.LinkManagerDoc.allGroupTypes, listSpec("string"), []);
+            } else {
+                LinkManager.Instance.LinkManagerDoc.allGroupTypes = new List<string>([]);
+                return [];
+            }
+        }
+        return [];
+    }
+
+    // gets the groups associates with an anchor in a link
+    public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            return DocListCast(linkDoc.anchor1Groups);
+        } else {
+            return DocListCast(linkDoc.anchor2Groups);
+        }
+    }
+
+    // sets the groups of the given anchor in the given link
+    public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
+        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
+            linkDoc.anchor1Groups = new List<Doc>(groups);
+        } else {
+            linkDoc.anchor2Groups = new List<Doc>(groups);
+        }
+    }
+
+    public addGroupToAnchor(linkDoc: Doc, anchor: Doc, groupDoc: Doc, replace: boolean = false) {
+        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        let index = groups.findIndex(gDoc => {
+            return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
+        });
+        if (index > -1 && replace) {
+            groups[index] = groupDoc;
+        }
+        if (index === -1) {
+            groups.push(groupDoc);
+        }
+        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, groups);
+    }
+
+    // removes group doc of given group type only from given anchor on given link
+    public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
+        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
+    // public doesAnchorHaveGroup(linkDoc: Doc, anchor: Doc, groupDoc: Doc): boolean {
+    //     let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
+    //     let index = groups.findIndex(gDoc => {
+    //         return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
+    //     });
+    //     return index > -1;
+    // }
+
     // returns map of group type to anchor's links in that group type
-    public findRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
-        let related = this.findAllRelatedLinks(anchor);
+    public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
+        let related = this.getAllRelatedLinks(anchor);
         let anchorGroups = new Map<string, Array<Doc>>();
         related.forEach(link => {
             let groups = LinkManager.Instance.getAnchorGroups(link, anchor);
@@ -73,10 +192,41 @@ export class LinkManager {
         return anchorGroups;
     }
 
+    // public addMetadataKeyToGroup(groupType: string, key: string): boolean {
+    //     if (LinkManager.Instance.LinkManagerDoc) {
+    //         if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+    //             let keyList = LinkManager.Instance.findMetadataKeysInGroup(groupType);
+    //             keyList.push(key);
+    //             LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keyList);
+    //             return true;
+    //         }
+    //         return false;
+    //     }
+    //     return false;
+    // }
+
+    public getMetadataKeysInGroup(groupType: string): string[] {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
+        }
+        return [];
+    }
+
+    public setMetadataKeysForGroup(groupType: string, keys: string[]): boolean {
+        if (LinkManager.Instance.LinkManagerDoc) {
+            // if (LinkManager.Instance.LinkManagerDoc[groupType]) {
+            LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keys);
+            return true;
+            // }
+            // return false;
+        }
+        return false;
+    }
+
     // returns a list of all metadata docs associated with the given group type
-    public findAllMetadataDocsInGroup(groupType: string): Array<Doc> {
+    public getAllMetadataDocsInGroup(groupType: string): Array<Doc> {
         let md: Doc[] = [];
-        let allLinks = LinkManager.Instance.allLinks;
+        let allLinks = LinkManager.Instance.getAllLinks();
         allLinks.forEach(linkDoc => {
             let anchor1Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc));
             let anchor2Groups = LinkManager.Instance.getAnchorGroups(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc));
@@ -86,27 +236,9 @@ export class LinkManager {
         return md;
     }
 
-    // removes all group docs from all links with the given group type
-    public deleteGroup(groupType: string): void {
-        let deleted = LinkManager.Instance.groupMetadataKeys.delete(groupType);
-        if (deleted) {
-            LinkManager.Instance.allLinks.forEach(linkDoc => {
-                LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor1, Doc, new Doc), groupType);
-                LinkManager.Instance.removeGroupFromAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, new Doc), groupType);
-            });
-        }
-    }
-
-    // removes group doc of given group type only from given anchor on given link
-    public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
-        let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
-        LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
-    }
-
     // checks if a link with the given anchors exists
-    public doesLinkExist(anchor1: Doc, anchor2: Doc) {
-        let allLinks = LinkManager.Instance.allLinks;
+    public doesLinkExist(anchor1: Doc, anchor2: Doc): boolean {
+        let allLinks = LinkManager.Instance.getAllLinks();
         let index = allLinks.findIndex(linkDoc => {
             return (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor1) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor2)) ||
                 (Doc.AreProtosEqual(Cast(linkDoc.anchor1, Doc, new Doc), anchor2) && Doc.AreProtosEqual(Cast(linkDoc.anchor2, Doc, new Doc), anchor1));
@@ -115,7 +247,7 @@ export class LinkManager {
     }
 
     // finds the opposite anchor of a given anchor in a link
-    public findOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
+    public getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc {
         if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
             return Cast(linkDoc.anchor2, Doc, new Doc);
         } else {
@@ -123,34 +255,17 @@ export class LinkManager {
         }
     }
 
-    // gets the groups associates with an anchor in a link
-    public getAnchorGroups(linkDoc: Doc, anchor: Doc): Array<Doc> {
-        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
-            return DocListCast(linkDoc.anchor1Groups);
-        } else {
-            return DocListCast(linkDoc.anchor2Groups);
-        }
-    }
 
-    // sets the groups of the given anchor in the given link
-    public setAnchorGroups(linkDoc: Doc, anchor: Doc, groups: Doc[]) {
-        if (Doc.AreProtosEqual(anchor, Cast(linkDoc.anchor1, Doc, new Doc))) {
-            linkDoc.anchor1Groups = new List<Doc>(groups);
-        } else {
-            linkDoc.anchor2Groups = new List<Doc>(groups);
-        }
-    }
-
-    @action
-    public addLinkProxy(proxy: Doc) {
-        LinkManager.Instance.linkProxies.push(proxy);
-    }
+    // @action
+    // public addLinkProxy(proxy: Doc) {
+    //     LinkManager.Instance.linkProxies.push(proxy);
+    // }
 
-    public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
-        let index = LinkManager.Instance.linkProxies.findIndex(p => {
-            return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
-        });
-        return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
-    }
+    // public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
+    //     let index = LinkManager.Instance.linkProxies.findIndex(p => {
+    //         return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
+    //     });
+    //     return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
+    // }
 
 }
\ No newline at end of file
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 926273633..3a2752d7e 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -573,7 +573,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let linkButton = null;
         if (SelectionManager.SelectedDocuments().length > 0) {
             let selFirst = SelectionManager.SelectedDocuments()[0];
-            let linkCount = LinkManager.Instance.findAllRelatedLinks(selFirst.props.Document).length;
+            let linkCount = LinkManager.Instance.getAllRelatedLinks(selFirst.props.Document).length;
             linkButton = (<Flyout
                 anchorPoint={anchorPoints.RIGHT_TOP}
                 content={<LinkMenu docView={selFirst}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 51630c29b..d90e9d23e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -35,6 +35,8 @@ import { HistoryUtil } from '../util/History';
 import { CollectionBaseView } from './collections/CollectionBaseView';
 import PDFMenu from './pdf/PDFMenu';
 import { InkTool } from '../../new_fields/InkField';
+import { LinkManager } from '../util/LinkManager';
+import { List } from '../../new_fields/List';
 
 
 @observer
@@ -146,6 +148,11 @@ export class MainView extends React.Component {
             let freeformDoc = Docs.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(CurrentUserUtils.UserDocument, 150), CollectionDockingView.makeDocumentConfig(freeformDoc, 600)] }] };
             let mainDoc = Docs.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;
+            }
             list.push(mainDoc);
             // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container)
             setTimeout(() => {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index c82027da5..4140f8029 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -354,9 +354,9 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp
                     tab.reactComponents = [upDiv];
                     tab.element.append(upDiv);
                     counter.DashDocId = tab.contentItem.config.props.documentId;
-                    tab.reactionDisposer = reaction(() => [LinkManager.Instance.findAllRelatedLinks(doc), doc.title],
+                    tab.reactionDisposer = reaction(() => [LinkManager.Instance.getAllRelatedLinks(doc), doc.title],
                         () => {
-                            counter.innerHTML = LinkManager.Instance.findAllRelatedLinks(doc).length;
+                            counter.innerHTML = LinkManager.Instance.getAllRelatedLinks(doc).length;
                             tab.titleElement[0].textContent = doc.title;
                         }, { fireImmediately: true });
                     tab.titleElement[0].DashDocId = tab.contentItem.config.props.documentId;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 0b922b3c4..7bc3ad124 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -73,7 +73,7 @@ class TreeView extends React.Component<TreeViewProps> {
     @undoBatch delete = () => this.props.deleteDoc(this.props.document);
     @undoBatch openRight = async () => this.props.addDocTab(this.props.document, "openRight");
 
-    onPointerDown = (e: React.PointerEvent) => e.stopPropagation()
+    onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
     onPointerEnter = (e: React.PointerEvent): void => {
         this.props.active() && (this.props.document.libraryBrush = true);
         if (e.buttons === 1 && SelectionManager.GetIsDragging()) {
@@ -115,7 +115,7 @@ class TreeView extends React.Component<TreeViewProps> {
         return this.props.document !== target && this.props.deleteDoc(doc) && addDoc(doc);
     }
     @action
-    indent = () => this.props.addDocument(this.props.document) && this.delete();
+    indent = () => this.props.addDocument(this.props.document) && this.delete()
 
     renderBullet() {
         let docList = Cast(this.props.document["data"], listSpec(Doc));
@@ -167,7 +167,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 keyList.push(key);
             }
         });
-        if (LinkManager.Instance.findAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
+        if (LinkManager.Instance.getAllRelatedLinks(this.props.document).length > 0) keyList.push("links");
         if (keyList.indexOf("data") !== -1) {
             keyList.splice(keyList.indexOf("data"), 1);
         }
@@ -281,9 +281,9 @@ class TreeView extends React.Component<TreeViewProps> {
         let ele: JSX.Element[] = [];
         let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
         let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => TreeView.AddDocToList(this.props.document, this._chosenKey, doc, addBefore, before);
-        let groups = LinkManager.Instance.findRelatedGroupedLinks(this.props.document);
+        let groups = LinkManager.Instance.getRelatedGroupedLinks(this.props.document);
         groups.forEach((groupLinkDocs, groupType) => {
-            let destLinks = groupLinkDocs.map(d => LinkManager.Instance.findOppositeAnchor(d, this.props.document));
+            let destLinks = groupLinkDocs.map(d => LinkManager.Instance.getOppositeAnchor(d, this.props.document));
             ele.push(
                 <div key={"treeviewlink-" + groupType + "subtitle"}>
                     <div className="collectionTreeView-subtitle">{groupType}:</div>
@@ -325,7 +325,7 @@ class TreeView extends React.Component<TreeViewProps> {
                         addDocTab={this.props.addDocTab}
                         setPreviewScript={emptyFunction}>
                     </CollectionSchemaPreview>
-                </div>
+                </div>;
             }
         }
         return <div className="treeViewItem-container" ref={this.createTreeDropTarget} onContextMenu={this.onWorkspaceContextMenu}>
@@ -364,14 +364,14 @@ class TreeView extends React.Component<TreeViewProps> {
                     TreeView.AddDocToList(docList[i - 1], fieldKey, child);
                     remove(child);
                 }
-            }
+            };
             let addDocument = (doc: Doc, relativeTo?: Doc, before?: boolean) => {
                 return add(doc, relativeTo ? relativeTo : docList[i], before !== undefined ? before : false);
-            }
+            };
             let rowHeight = () => {
                 let aspect = NumCast(child.nativeWidth, 0) / NumCast(child.nativeHeight, 0);
                 return aspect ? Math.min(child[WidthSym](), rowWidth()) / aspect : child[HeightSym]();
-            }
+            };
             return <TreeView
                 document={child}
                 treeViewId={treeViewId}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index 76e089f08..bb8e8a5c2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -98,17 +98,17 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
         //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
         // });
 
-        console.log("CONNECTIONS");
+        // console.log("CONNECTIONS");
 
         let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             let srcViews = this.documentAnchors(connection.a);
             let targetViews = this.documentAnchors(connection.b);
 
-            console.log(srcViews.length, targetViews.length);
+            // console.log(srcViews.length, targetViews.length);
             let possiblePairs: { a: Doc, b: Doc, }[] = [];
             srcViews.map(sv => {
                 targetViews.map(tv => {
-                    console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
+                    // console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
                     possiblePairs.push({ a: sv.props.Document, b: tv.props.Document });
                 });
             });
@@ -142,7 +142,6 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
     }
 
     render() {
-        console.log("\n");
         return (
             <div className="collectionfreeformlinksview-container">
                 <svg className="collectionfreeformlinksview-svgCanvas">
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1fc2cf770..7b185336b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -285,7 +285,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 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.findAllRelatedLinks(this.props.Document);
+                let linkedDocs = LinkManager.Instance.getAllRelatedLinks(this.props.Document);
                 let expandedDocs: Doc[] = [];
                 expandedDocs = subBulletDocs ? [...subBulletDocs, ...expandedDocs] : expandedDocs;
                 expandedDocs = maximizedDocs ? [...maximizedDocs, ...expandedDocs] : expandedDocs;
@@ -536,11 +536,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     onPointerEnter = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = true; };
     onPointerLeave = (e: React.PointerEvent): void => { this.props.Document.libraryBrush = false; };
-    onDragOver = (e: React.DragEvent): void => {
-        this.props.Document.libraryBrush = true;
-        console.log("dragOver");
-    };
-    onDragLeave = (e: React.DragEvent): void => { this.props.Document.libraryBrush = false; };
 
     isSelected = () => SelectionManager.IsSelected(this);
     @action select = (ctrlPressed: boolean) => { SelectionManager.SelectDoc(this, ctrlPressed); };
@@ -585,7 +580,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
-                onDragOver={this.onDragOver} onDragLeave={this.onDragLeave}
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
             >
                 {this.contents}
diff --git a/src/client/views/nodes/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss
index 24bfd2c9f..6be2dcf60 100644
--- a/src/client/views/nodes/LinkButtonBox.scss
+++ b/src/client/views/nodes/LinkButtonBox.scss
@@ -1,18 +1,18 @@
-.linkBox-cont {
-    width: 200px;
-    height: 100px;
-    background-color: black;
-    text-align: center;
-    color: white;
-    padding: 10px;
-    border-radius: 5px;
-    position: relative;
+// .linkBox-cont {
+//     width: 200px;
+//     height: 100px;
+//     background-color: black;
+//     text-align: center;
+//     color: white;
+//     padding: 10px;
+//     border-radius: 5px;
+//     position: relative;
 
-    .linkBox-cont-wrapper {
-        width: calc(100% - 20px);
-        position: absolute;
-        left: 50%;
-        top: 50%;
-        transform: translate(-50%, -50%);
-    }
-}
\ No newline at end of file
+//     .linkBox-cont-wrapper {
+//         width: calc(100% - 20px);
+//         position: absolute;
+//         left: 50%;
+//         top: 50%;
+//         transform: translate(-50%, -50%);
+//     }
+// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.tsx b/src/client/views/nodes/LinkButtonBox.tsx
index 8a7c1ed8b..440847ead 100644
--- a/src/client/views/nodes/LinkButtonBox.tsx
+++ b/src/client/views/nodes/LinkButtonBox.tsx
@@ -1,63 +1,63 @@
-import React = require("react");
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { FieldView, FieldViewProps } from './FieldView';
-import "./LinkButtonBox.scss";
-import { DocumentView } from "./DocumentView";
-import { Doc } from "../../../new_fields/Doc";
-import { LinkButtonField } from "../../../new_fields/LinkButtonField";
-import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import { DocumentManager } from "../../util/DocumentManager";
-import { Id } from "../../../new_fields/FieldSymbols";
-
-library.add(faCaretUp);
-library.add(faObjectGroup);
-library.add(faStickyNote);
-library.add(faFilePdf);
-library.add(faFilm);
-
-@observer
-export class LinkButtonBox extends React.Component<FieldViewProps> {
-    public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
-
-    followLink = (): void => {
-        console.log("follow link???");
-        let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-        let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-        if (targetView && targetView.props.ContainingCollectionView) {
-            CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
-        }
-    }
-
-    render() {
-
-        let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-        let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-
-        let text = "Could not find link";
-        if (targetView) {
-            let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
-            text = "Link to " + StrCast(targetView.props.Document.title) + context;
-        }
-
-        let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
-        let display = activeDvs.reduce((found, dv) => {
-            let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
-            let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
-            let match = matchSv || matchTv;
-            return match || found;
-        }, false);
-
-        return (
-            <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
-                <div className="linkBox-cont-wrapper">
-                    <p>{text}</p>
-                </div>
-            </div >
-        );
-    }
-}
\ No newline at end of file
+// import React = require("react");
+// import { library } from '@fortawesome/fontawesome-svg-core';
+// import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
+// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+// import { computed, observable, runInAction } from "mobx";
+// import { observer } from "mobx-react";
+// import { FieldView, FieldViewProps } from './FieldView';
+// import "./LinkButtonBox.scss";
+// import { DocumentView } from "./DocumentView";
+// import { Doc } from "../../../new_fields/Doc";
+// import { LinkButtonField } from "../../../new_fields/LinkButtonField";
+// import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
+// import { CollectionDockingView } from "../collections/CollectionDockingView";
+// import { DocumentManager } from "../../util/DocumentManager";
+// import { Id } from "../../../new_fields/FieldSymbols";
+
+// library.add(faCaretUp);
+// library.add(faObjectGroup);
+// library.add(faStickyNote);
+// library.add(faFilePdf);
+// library.add(faFilm);
+
+// @observer
+// export class LinkButtonBox extends React.Component<FieldViewProps> {
+//     public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
+
+//     followLink = (): void => {
+//         console.log("follow link???");
+//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
+//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
+//         if (targetView && targetView.props.ContainingCollectionView) {
+//             CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
+//         }
+//     }
+
+//     render() {
+
+//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
+//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
+
+//         let text = "Could not find link";
+//         if (targetView) {
+//             let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
+//             text = "Link to " + StrCast(targetView.props.Document.title) + context;
+//         }
+
+//         let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
+//         let display = activeDvs.reduce((found, dv) => {
+//             let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
+//             let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
+//             let match = matchSv || matchTv;
+//             return match || found;
+//         }, false);
+
+//         return (
+//             <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
+//                 <div className="linkBox-cont-wrapper">
+//                     <p>{text}</p>
+//                 </div>
+//             </div >
+//         );
+//     }
+// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 95199bae2..5f4f7d4f0 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -17,9 +17,8 @@ library.add(faArrowLeft, faEllipsisV, faTable, faTrash, faCog, faExchangeAlt, fa
 
 
 interface GroupTypesDropdownProps {
-    groupId: string;
     groupType: string;
-    setGroup: (groupId: string, group: string) => void;
+    setGroupType: (group: string) => void;
 }
 // this dropdown could be generalized
 @observer
@@ -32,20 +31,20 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
 
     @action
     createGroup = (groupType: string): void => {
-        this.props.setGroup(this.props.groupId, groupType);
-        LinkManager.Instance.groupMetadataKeys.set(groupType, []);
+        this.props.setGroupType(groupType);
+        LinkManager.Instance.addGroupType(groupType);
     }
 
     renderOptions = (): JSX.Element[] | JSX.Element => {
         if (this._searchTerm === "") return <></>;
 
-        let allGroupTypes = Array.from(LinkManager.Instance.groupMetadataKeys.keys());
+        let allGroupTypes = Array.from(LinkManager.Instance.getAllGroupTypes());
         let groupOptions = allGroupTypes.filter(groupType => groupType.toUpperCase().indexOf(this._searchTerm.toUpperCase()) > -1);
         let exactFound = groupOptions.findIndex(groupType => groupType.toUpperCase() === this._searchTerm.toUpperCase()) > -1;
 
         let options = groupOptions.map(groupType => {
             return <div key={groupType} className="linkEditor-option"
-                onClick={() => { this.props.setGroup(this.props.groupId, groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
+                onClick={() => { this.props.setGroupType(groupType); this.setGroupType(groupType); this.setSearchTerm(""); }}>{groupType}</div>;
         });
 
         // if search term does not already exist as a group type, give option to create new group type
@@ -85,7 +84,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
 
     @action
     setMetadataKey = (value: string): void => {
-        let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
         // don't allow user to create existing key
         let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
@@ -98,12 +97,15 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         }
 
         // set new value for key
-        let currIndex = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
+        let currIndex = groupMdKeys.findIndex(key => {
+            console.log("finding index this", key.toUpperCase(), "that", this._key.toUpperCase());
+            return StrCast(key).toUpperCase() === this._key.toUpperCase();
+        });
         if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys[currIndex] = value;
 
         this._key = value;
-        LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+        LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
     }
 
     @action
@@ -116,13 +118,13 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
 
     @action
     removeMetadata = (): void => {
-        let groupMdKeys = new Array(...LinkManager.Instance.groupMetadataKeys.get(this.props.groupType)!);
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
         let index = groupMdKeys.findIndex(key => key.toUpperCase() === this._key.toUpperCase());
         if (index === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys.splice(index, 1);
 
-        LinkManager.Instance.groupMetadataKeys.set(this.props.groupType, groupMdKeys);
+        LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, groupMdKeys);
         this._key = "";
     }
 
@@ -137,215 +139,176 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
     }
 }
 
-
-interface LinkEditorProps {
+interface LinkGroupEditorProps {
     sourceDoc: Doc;
     linkDoc: Doc;
-    showLinks: () => void;
+    groupDoc: Doc;
 }
 @observer
-export class LinkEditor extends React.Component<LinkEditorProps> {
+export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
-    // map of temporary group id to the corresponding group doc
-    @observable private _groups: Map<string, Doc> = new Map();
-
-    constructor(props: LinkEditorProps) {
-        super(props);
+    @action
+    setGroupType = (groupType: string): void => {
+        console.log("SET GROUP TYPE TO", groupType);
+        this.props.groupDoc.type = groupType;
+        console.log("GROUP TYPE HAS BEEN SET TO ", StrCast(this.props.groupDoc.type));
+    }
 
-        let groups = new Map<string, Doc>();
-        let groupList = LinkManager.Instance.getAnchorGroups(props.linkDoc, props.sourceDoc);
-        groupList.forEach(groupDoc => {
-            let id = Utils.GenerateGuid();
-            groups.set(id, groupDoc);
-        });
-        this._groups = groups;
+    removeGroupFromLink = (groupType: string): void => {
+        LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
     }
 
-    @action
-    deleteLink = (): void => {
-        let index = LinkManager.Instance.allLinks.indexOf(this.props.linkDoc);
-        LinkManager.Instance.allLinks.splice(index, 1);
-        this.props.showLinks();
+    deleteGroup = (groupType: string): void => {
+        LinkManager.Instance.deleteGroupType(groupType);
     }
 
-    @action
-    addGroup = (): void => {
-        // new group only gets added if there is not already a group with type "new group"
-        let index = Array.from(this._groups.values()).findIndex(groupDoc => {
-            return groupDoc.type === "New Group";
-        });
-        if (index > -1) return;
+    copyGroup = (groupType: string): void => {
+        let sourceGroupDoc = this.props.groupDoc;
+        let sourceMdDoc = Cast(sourceGroupDoc.metadata, Doc, new Doc);
 
-        // create new metadata document for group
-        let mdDoc = Docs.TextDocument();
-        mdDoc.proto!.anchor1 = this.props.sourceDoc.title;
-        mdDoc.proto!.anchor2 = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+        let destDoc = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        // let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
-        // create new group document
-        let groupDoc = Docs.TextDocument();
-        groupDoc.proto!.type = "New Group";
-        groupDoc.proto!.metadata = mdDoc;
+        // create new metadata doc with copied kvp
+        let destMdDoc = new Doc();
+        destMdDoc.anchor1 = StrCast(sourceMdDoc.anchor2);
+        destMdDoc.anchor2 = StrCast(sourceMdDoc.anchor1);
+        keys.forEach(key => {
+            let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
+            destMdDoc[key] = val;
+        });
 
-        this._groups.set(Utils.GenerateGuid(), groupDoc);
+        // create new group doc with new metadata doc
+        let destGroupDoc = new Doc();
+        destGroupDoc.type = groupType;
+        destGroupDoc.metadata = destMdDoc;
 
-        let linkDoc = this.props.linkDoc.proto ? this.props.linkDoc.proto : this.props.linkDoc;
-        LinkManager.Instance.setAnchorGroups(linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
+        LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, destDoc, destGroupDoc, true);
     }
 
     @action
-    setGroupType = (groupId: string, groupType: string): void => {
-        let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-            groupDoc.proto!.type = groupType;
-            this._groups.set(groupId, groupDoc);
-            LinkManager.Instance.setAnchorGroups(this.props.linkDoc, this.props.sourceDoc, Array.from(this._groups.values()));
-        }
-    }
-
-    removeGroupFromLink = (groupId: string, groupType: string): void => {
-        let groupDoc = this._groups.get(groupId);
-        if (!groupDoc) console.error("LinkEditor: group not found");
-        LinkManager.Instance.removeGroupFromAnchor(this.props.linkDoc, this.props.sourceDoc, groupType);
-        this._groups.delete(groupId);
-    }
-
-    deleteGroup = (groupId: string, groupType: string): void => {
-        let groupDoc = this._groups.get(groupId);
-        if (!groupDoc) console.error("LinkEditor: group not found");
-        LinkManager.Instance.deleteGroup(groupType);
-        this._groups.delete(groupId);
+    addMetadata = (groupType: string): void => {
+        let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        // only add "new key" if there is no other key with value "new key"; prevents spamming
+        if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
+        LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
     }
 
-    copyGroup = (groupId: string, groupType: string): void => {
-        let sourceGroupDoc = this._groups.get(groupId);
-        let sourceMdDoc = Cast(sourceGroupDoc!.metadata, Doc, new Doc);
-        let destDoc = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-        let destGroupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, destDoc);
-        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-
-        // create new metadata doc with copied kvp
-        let destMdDoc = Docs.TextDocument();
-        destMdDoc.proto!.anchor1 = StrCast(sourceMdDoc.anchor2);
-        destMdDoc.proto!.anchor2 = StrCast(sourceMdDoc.anchor1);
-        if (keys) {
-            keys.forEach(key => {
-                let val = sourceMdDoc[key] === undefined ? "" : StrCast(sourceMdDoc[key]);
-                destMdDoc[key] = val;
+    renderMetadata = (): JSX.Element[] => {
+        let metadata: Array<JSX.Element> = [];
+        let groupDoc = this.props.groupDoc;
+        let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
+        let groupType = StrCast(groupDoc.type);
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        if (groupMdKeys) {
+            groupMdKeys.forEach((key, index) => {
+                metadata.push(
+                    <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
+                );
             });
         }
-
-        // create new group doc with new metadata doc
-        let destGroupDoc = Docs.TextDocument();
-        destGroupDoc.proto!.type = groupType;
-        destGroupDoc.proto!.metadata = destMdDoc;
-
-        // if group does not already exist on opposite anchor, create group doc
-        let index = destGroupList.findIndex(groupDoc => { StrCast(groupDoc.type).toUpperCase() === groupType.toUpperCase(); });
-        if (index > -1) {
-            destGroupList[index] = destGroupDoc;
-        } else {
-            destGroupList.push(destGroupDoc);
-        }
-
-        LinkManager.Instance.setAnchorGroups(this.props.linkDoc, destDoc, destGroupList);
+        return metadata;
     }
 
-    viewGroupAsTable = (groupId: string, groupType: string): JSX.Element => {
-        let keys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-        let groupDoc = this._groups.get(groupId);
-        if (keys && groupDoc) {
-            let docs: Doc[] = LinkManager.Instance.findAllMetadataDocsInGroup(groupType);
-            let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys!], docs, { width: 200, height: 200, title: groupType + " table" }));
-            let ref = React.createRef<HTMLDivElement>();
-            return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
-        } else {
-            return <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>;
-        }
+    viewGroupAsTable = (groupType: string): JSX.Element => {
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+        let createTable = action(() => Docs.SchemaDocument(["anchor1", "anchor2", ...keys], docs, { width: 500, height: 300, title: groupType + " table" }));
+        let ref = React.createRef<HTMLDivElement>();
+        return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
 
-    renderGroup = (groupId: string, groupDoc: Doc): JSX.Element => {
-        let type = StrCast(groupDoc.type);
-        if ((type && LinkManager.Instance.groupMetadataKeys.get(type)) || type === "New Group") {
-            let buttons;
-            if (type === "New Group") {
-                buttons = (
-                    <>
-                        <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
-                        <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
-                        <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
-                        <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
-                    </>
-                );
-            } else {
-                buttons = (
-                    <>
-                        <button className="linkEditor-button" onClick={() => this.addMetadata(StrCast(groupDoc.proto!.type))} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.copyGroup(groupId, type)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupId, type)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
-                        <button className="linkEditor-button" onClick={() => this.deleteGroup(groupId, type)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
-                        {this.viewGroupAsTable(groupId, type)}
-                    </>
-                );
-            }
-
-            return (
-                <div key={groupId} className="linkEditor-group">
-                    <div className="linkEditor-group-row">
-                        <p className="linkEditor-group-row-label">type:</p>
-                        <GroupTypesDropdown groupId={groupId} groupType={StrCast(groupDoc.proto!.type)} setGroup={this.setGroupType} />
-                    </div>
-                    {this.renderMetadata(groupId)}
-                    <div className="linkEditor-group-buttons">
-                        {buttons}
-                    </div>
-                </div>
+    render() {
+        let groupType = StrCast(this.props.groupDoc.type);
+        // if ((groupType && LinkManager.Instance.getMetadataKeysInGroup(groupType).length > 0) || groupType === "") {
+        let buttons;
+        if (groupType === "") {
+            buttons = (
+                <>
+                    <button className="linkEditor-button" disabled={true} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+                    <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+                    <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                    <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
+                </>
             );
         } else {
-            return <></>;
+            buttons = (
+                <>
+                    <button className="linkEditor-button" onClick={() => this.addMetadata(groupType)} title="Add KVP"><FontAwesomeIcon icon="plus" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.copyGroup(groupType)} title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
+                    <button className="linkEditor-button" onClick={() => this.deleteGroup(groupType)} title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
+                    {this.viewGroupAsTable(groupType)}
+                </>
+            );
         }
+
+        return (
+            <div className="linkEditor-group">
+                <div className="linkEditor-group-row">
+                    <p className="linkEditor-group-row-label">type:</p>
+                    <GroupTypesDropdown groupType={groupType} setGroupType={this.setGroupType} />
+                </div>
+                {this.renderMetadata()}
+                <div className="linkEditor-group-buttons">
+                    {buttons}
+                </div>
+            </div>
+        );
     }
+    // else {
+    //     return <></>;
+    // }
+    // }
+}
+
 
+interface LinkEditorProps {
+    sourceDoc: Doc;
+    linkDoc: Doc;
+    showLinks: () => void;
+}
+@observer
+export class LinkEditor extends React.Component<LinkEditorProps> {
 
     @action
-    addMetadata = (groupType: string): void => {
-        console.log("ADD MD");
-        let mdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-        if (mdKeys) {
-            // only add "new key" if there is no other key with value "new key"; prevents spamming
-            if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
-        } else {
-            mdKeys = ["new key"];
-        }
-        LinkManager.Instance.groupMetadataKeys.set(groupType, mdKeys);
+    deleteLink = (): void => {
+        LinkManager.Instance.deleteLink(this.props.linkDoc);
+        this.props.showLinks();
     }
 
-    renderMetadata = (groupId: string): JSX.Element[] => {
-        let metadata: Array<JSX.Element> = [];
-        let groupDoc = this._groups.get(groupId);
-        if (groupDoc) {
-            let mdDoc = Cast(groupDoc.proto!.metadata, Doc, new Doc);
-            let groupType = StrCast(groupDoc.proto!.type);
-            let groupMdKeys = LinkManager.Instance.groupMetadataKeys.get(groupType);
-            if (groupMdKeys) {
-                groupMdKeys.forEach((key, index) => {
-                    metadata.push(
-                        <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
-                    );
-                });
-            }
-        }
-        return metadata;
+    @action
+    addGroup = (): void => {
+        // create new metadata document for group
+        let mdDoc = new Doc();
+        mdDoc.anchor1 = this.props.sourceDoc.title;
+        mdDoc.anchor2 = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc).title;
+
+        // create new group document
+        let groupDoc = new Doc();
+        groupDoc.type = "";
+        groupDoc.metadata = mdDoc;
+
+        LinkManager.Instance.addGroupToAnchor(this.props.linkDoc, this.props.sourceDoc, groupDoc);
     }
 
     render() {
-        let destination = LinkManager.Instance.findOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
+        let destination = LinkManager.Instance.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
 
-        let groups: Array<JSX.Element> = [];
-        this._groups.forEach((groupDoc, groupId) => {
-            groups.push(this.renderGroup(groupId, groupDoc));
+        let groupList = LinkManager.Instance.getAnchorGroups(this.props.linkDoc, this.props.sourceDoc);
+        console.log("NUM GROUPS", groupList.length);
+        let groups = groupList.map(groupDoc => {
+            return <LinkGroupEditor key={"gred-" + StrCast(groupDoc.type)} linkDoc={this.props.linkDoc} sourceDoc={this.props.sourceDoc} groupDoc={groupDoc} />;
         });
 
+
+        // let groups: Array<JSX.Element> = [];
+        // this._groups.forEach((groupDoc, groupId) => {
+        //     groups.push(this.renderGroup(groupId, groupDoc));
+        // });
+
         return (
             <div className="linkEditor">
                 <button className="linkEditor-back" onPointerDown={() => this.props.showLinks()}><FontAwesomeIcon icon="arrow-left" size="sm" /></button>
diff --git a/src/client/views/nodes/LinkMenu.tsx b/src/client/views/nodes/LinkMenu.tsx
index f96c7d2e4..04ca47db3 100644
--- a/src/client/views/nodes/LinkMenu.tsx
+++ b/src/client/views/nodes/LinkMenu.tsx
@@ -38,7 +38,7 @@ export class LinkMenu extends React.Component<Props> {
 
     render() {
         let sourceDoc = this.props.docView.props.Document;
-        let groups: Map<string, Doc[]> = LinkManager.Instance.findRelatedGroupedLinks(sourceDoc);
+        let groups: Map<string, Doc[]> = LinkManager.Instance.getRelatedGroupedLinks(sourceDoc);
         if (this._editingLink === undefined) {
             return (
                 <div className="linkMenu">
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index 229143d99..71326f703 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -42,10 +42,10 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
             document.removeEventListener("pointermove", this.onLinkButtonMoved);
             document.removeEventListener("pointerup", this.onLinkButtonUp);
 
-            let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.findOppositeAnchor(linkDoc, this.props.sourceDoc));
+            let draggedDocs = this.props.group.map(linkDoc => LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc));
             let dragData = new DragManager.DocumentDragData(draggedDocs);
 
-            DragManager.StartLinkedDocumentDrag([this._drag.current], dragData, e.x, e.y, {
+            DragManager.StartLinkedDocumentDrag([this._drag.current], this.props.sourceDoc, dragData, e.x, e.y, {
                 handlers: {
                     dragComplete: action(emptyFunction),
                 },
@@ -57,7 +57,7 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
 
     render() {
         let groupItems = this.props.group.map(linkDoc => {
-            let destination = LinkManager.Instance.findOppositeAnchor(linkDoc, this.props.sourceDoc);
+            let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
             return <LinkMenuItem key={destination[Id] + this.props.sourceDoc[Id]} groupType={this.props.groupType}
                 linkDoc={linkDoc} sourceDoc={this.props.sourceDoc} destinationDoc={destination} showEditor={this.props.showEditor} />;
         });
@@ -69,6 +69,6 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
                     {groupItems}
                 </div>
             </div>
-        )
+        );
     }
 }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkMenuItem.tsx b/src/client/views/nodes/LinkMenuItem.tsx
index 42ef353b7..28694721d 100644
--- a/src/client/views/nodes/LinkMenuItem.tsx
+++ b/src/client/views/nodes/LinkMenuItem.tsx
@@ -53,8 +53,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         let mdRows: Array<JSX.Element> = [];
         if (groupDoc) {
             let mdDoc = Cast(groupDoc.metadata, Doc, new Doc);
-            let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
-            mdRows = keys!.map(key => {
+            let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
+            mdRows = keys.map(key => {
                 return (<div key={key} className="link-metadata-row"><b>{key}</b>: {StrCast(mdDoc[key])}</div>);
             });
         }
@@ -88,7 +88,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
     render() {
 
-        let keys = LinkManager.Instance.groupMetadataKeys.get(this.props.groupType);
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);//groupMetadataKeys.get(this.props.groupType);
         let canExpand = keys ? keys.length > 0 : false;
 
         return (
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index fda788f2d..9b104184f 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -234,23 +234,20 @@ export namespace Doc {
     export function MakeCopy(doc: Doc, copyProto: boolean = false): Doc {
         const copy = new Doc;
         Object.keys(doc).forEach(key => {
-            console.log(key);
             const field = doc[key];
             if (key === "proto" && copyProto) {
-                console.log(1);
                 if (field instanceof Doc) {
-                    console.log(2);
                     copy[key] = Doc.MakeCopy(field);
                 }
             } else {
                 if (field instanceof RefField) {
-                    console.log(3);
+                    console.log("equals field, ref", key);
                     copy[key] = field;
                 } else if (field instanceof ObjectField) {
-                    console.log(4);
+                    console.log("copy field, object", key);
                     copy[key] = ObjectField.MakeCopy(field);
                 } else {
-                    console.log(5);
+                    console.log("equals field", key);
                     copy[key] = field;
                 }
             }
diff --git a/src/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts
index 92e1ed922..e6d1de749 100644
--- a/src/new_fields/LinkButtonField.ts
+++ b/src/new_fields/LinkButtonField.ts
@@ -1,35 +1,35 @@
-import { Deserializable } from "../client/util/SerializationHelper";
-import { serializable, primitive, createSimpleSchema, object } from "serializr";
-import { ObjectField } from "./ObjectField";
-import { Copy, ToScriptString } from "./FieldSymbols";
-import { Doc } from "./Doc";
-import { DocumentView } from "../client/views/nodes/DocumentView";
+// import { Deserializable } from "../client/util/SerializationHelper";
+// import { serializable, primitive, createSimpleSchema, object } from "serializr";
+// import { ObjectField } from "./ObjectField";
+// import { Copy, ToScriptString } from "./FieldSymbols";
+// import { Doc } from "./Doc";
+// import { DocumentView } from "../client/views/nodes/DocumentView";
 
-export type LinkButtonData = {
-    sourceViewId: string,
-    targetViewId: string
-};
+// export type LinkButtonData = {
+//     sourceViewId: string,
+//     targetViewId: string
+// };
 
-const LinkButtonSchema = createSimpleSchema({
-    sourceViewId: true,
-    targetViewId: true
-});
+// const LinkButtonSchema = createSimpleSchema({
+//     sourceViewId: true,
+//     targetViewId: true
+// });
 
-@Deserializable("linkButton")
-export class LinkButtonField extends ObjectField {
-    @serializable(object(LinkButtonSchema))
-    readonly data: LinkButtonData;
+// @Deserializable("linkButton")
+// export class LinkButtonField extends ObjectField {
+//     @serializable(object(LinkButtonSchema))
+//     readonly data: LinkButtonData;
 
-    constructor(data: LinkButtonData) {
-        super();
-        this.data = data;
-    }
+//     constructor(data: LinkButtonData) {
+//         super();
+//         this.data = data;
+//     }
 
-    [Copy]() {
-        return new LinkButtonField(this.data);
-    }
+//     [Copy]() {
+//         return new LinkButtonField(this.data);
+//     }
 
-    [ToScriptString]() {
-        return "invalid";
-    }
-}
+//     [ToScriptString]() {
+//         return "invalid";
+//     }
+// }
-- 
cgit v1.2.3-70-g09d2


From 2d300b0cd3d02c900865c61eacd539efed5289e6 Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Tue, 25 Jun 2019 20:18:14 -0400
Subject: fixed link metadata rendering bug

---
 src/client/documents/Documents.ts                  |  11 +-
 src/client/util/LinkManager.ts                     |   2 +-
 .../CollectionFreeFormLinkView.scss                |  31 -----
 .../CollectionFreeFormLinkView.tsx                 |   1 -
 .../CollectionFreeFormLinkWithProxyView.tsx        | 131 ---------------------
 src/client/views/nodes/DocumentContentsView.tsx    |   3 +-
 src/client/views/nodes/DocumentView.tsx            |  11 --
 src/client/views/nodes/LinkButtonBox.scss          |  18 ---
 src/client/views/nodes/LinkButtonBox.tsx           |  63 ----------
 src/client/views/nodes/LinkEditor.tsx              |  45 ++++---
 src/new_fields/LinkButtonField.ts                  |  35 ------
 11 files changed, 35 insertions(+), 316 deletions(-)
 delete mode 100644 src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
 delete mode 100644 src/client/views/nodes/LinkButtonBox.scss
 delete mode 100644 src/client/views/nodes/LinkButtonBox.tsx
 delete mode 100644 src/new_fields/LinkButtonField.ts

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d2300e4d2..547687921 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -88,7 +88,7 @@ export namespace DocUtils {
     //     let protoTarg = target.proto ? target.proto : target;
     export function MakeLink(source: Doc, target: Doc, targetContext?: Doc, title: string = "", description: string = "", tags: string = "Default") {
         if (LinkManager.Instance.doesLinkExist(source, target)) {
-            console.log("LINK EXISTS"); return;
+            return;
         }
 
         UndoManager.RunInBatch(() => {
@@ -186,11 +186,6 @@ export namespace Docs {
             { x: 0, y: 0, width: Number(MINIMIZED_ICON_SIZE), height: Number(MINIMIZED_ICON_SIZE) });
         return iconProto;
     }
-    // function CreateLinkPrototype(): Doc {
-    //     let linkProto = setupPrototypeOptions(linkProtoId, "LINK_PROTO", LinkButtonBox.LayoutString(),
-    //         { x: 0, y: 0, width: 300 });
-    //     return linkProto;
-    // }
     function CreateTextPrototype(): Doc {
         let textProto = setupPrototypeOptions(textProtoId, "TEXT_PROTO", FormattedTextBox.LayoutString(),
             { x: 0, y: 0, width: 300, backgroundColor: "#f1efeb" });
@@ -277,9 +272,7 @@ export namespace Docs {
     export function IconDocument(icon: string, options: DocumentOptions = {}) {
         return CreateInstance(iconProto, new IconField(icon), options);
     }
-    // export function LinkButtonDocument(data: LinkButtonData, options: DocumentOptions = {}) {
-    //     return CreateInstance(linkProto, new LinkButtonField(data), options);
-    // }
+
     export function PdfDocument(url: string, options: DocumentOptions = {}) {
         return CreateInstance(pdfProto, new PdfField(new URL(url)), options);
     }
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 60de7fc52..db814082f 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -156,7 +156,7 @@ export class LinkManager {
     // removes group doc of given group type only from given anchor on given link
     public removeGroupFromAnchor(linkDoc: Doc, anchor: Doc, groupType: string) {
         let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-        let newGroups = groups.filter(groupDoc => StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase());
+        let newGroups = groups.filter(groupDoc => { StrCast(groupDoc.type).toUpperCase() !== groupType.toUpperCase() });
         LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
index 239c2ce56..fc5212edd 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.scss
@@ -17,34 +17,3 @@
     transform: translate(10000px,10000px);
     pointer-events: all;
 }
-
-.linkview-ele {
-    transform: translate(10000px,10000px);
-    pointer-events: all;
-
-    &.linkview-line {
-        stroke: black;
-        stroke-width: 2px;
-        opacity: 0.5;
-    }
-}
-
-.linkview-button {
-    width: 200px;
-    height: 100px;
-    border-radius: 5px;
-    padding: 10px;
-    position: relative;
-    background-color: black;
-    cursor: pointer;
-
-    p {
-        width: calc(100% - 20px);
-        color: white;
-        position: absolute;
-        top: 50%;
-        left: 50%;
-        transform: translate(-50%, -50%);
-    }
-
-}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 5dc3b5c16..b546d1b78 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -9,7 +9,6 @@ import v5 = require("uuid/v5");
 export interface CollectionFreeFormLinkViewProps {
     A: Doc;
     B: Doc;
-    // LinkDoc: Doc;
     LinkDocs: Doc[];
     addDocument: (document: Doc, allowDuplicates?: boolean) => boolean;
     removeDocument: (document: Doc) => boolean;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
deleted file mode 100644
index a4d122af2..000000000
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkWithProxyView.tsx
+++ /dev/null
@@ -1,131 +0,0 @@
-import { observer } from "mobx-react";
-import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
-import { BoolCast, NumCast, StrCast } from "../../../../new_fields/Types";
-import { InkingControl } from "../../InkingControl";
-import "./CollectionFreeFormLinkView.scss";
-import React = require("react");
-import v5 = require("uuid/v5");
-import { DocumentView } from "../../nodes/DocumentView";
-import { Docs } from "../../../documents/Documents";
-import { observable, action } from "mobx";
-import { CollectionDockingView } from "../CollectionDockingView";
-import { dropActionType, DragManager } from "../../../util/DragManager";
-import { emptyFunction } from "../../../../Utils";
-import { DocumentManager } from "../../../util/DocumentManager";
-
-export interface CollectionFreeFormLinkViewProps {
-    sourceView: DocumentView;
-    targetView: DocumentView;
-    proxyDoc: Doc;
-    // addDocTab: (document: Doc, where: string) => void;
-}
-
-@observer
-export class CollectionFreeFormLinkWithProxyView extends React.Component<CollectionFreeFormLinkViewProps> {
-
-    // @observable private _proxyX: number = NumCast(this.props.proxyDoc.x);
-    // @observable private _proxyY: number = NumCast(this.props.proxyDoc.y);
-    private _ref = React.createRef<HTMLDivElement>();
-    private _downX: number = 0;
-    private _downY: number = 0;
-    @observable _x: number = 0;
-    @observable _y: number = 0;
-    // @observable private _proxyDoc: Doc = Docs.TextDocument(); // used for positioning
-
-    @action
-    componentDidMount() {
-        let a2 = this.props.proxyDoc;
-        this._x = NumCast(a2.x) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2);
-        this._y = NumCast(a2.y) + (BoolCast(a2.isMinimized, false) ? 5 : NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2);
-    }
-
-
-    followButton = (e: React.PointerEvent): void => {
-        e.stopPropagation();
-        let open = this.props.targetView.props.ContainingCollectionView ? this.props.targetView.props.ContainingCollectionView.props.Document : this.props.targetView.props.Document;
-        CollectionDockingView.Instance.AddRightSplit(open);
-        DocumentManager.Instance.jumpToDocument(this.props.targetView.props.Document, e.altKey);
-    }
-
-    @action
-    setPosition(x: number, y: number) {
-        this._x = x;
-        this._y = y;
-    }
-
-    startDragging(x: number, y: number) {
-        if (this._ref.current) {
-            let dragData = new DragManager.DocumentDragData([this.props.proxyDoc]);
-
-            DragManager.StartLinkProxyDrag(this._ref.current, dragData, x, y, {
-                handlers: {
-                    dragComplete: action(() => {
-                        let a2 = this.props.proxyDoc;
-                        let offset = NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2;
-                        let x = NumCast(a2.x);// + NumCast(a2.width) / NumCast(a2.zoomBasis, 1) / 2;
-                        let y = NumCast(a2.y);// + NumCast(a2.height) / NumCast(a2.zoomBasis, 1) / 2;
-                        this.setPosition(x, y);
-
-                        // this is a hack :'( theres prob a better way to make the input doc not render
-                        let views = DocumentManager.Instance.getDocumentViews(this.props.proxyDoc);
-                        views.forEach(dv => {
-                            dv.props.removeDocument && dv.props.removeDocument(dv.props.Document);
-                        });
-                    }),
-                },
-                hideSource: true //?
-            });
-        }
-    }
-
-    onPointerDown = (e: React.PointerEvent): void => {
-        this._downX = e.clientX;
-        this._downY = e.clientY;
-
-        e.stopPropagation();
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.addEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-        document.addEventListener("pointerup", this.onPointerUp);
-    }
-
-    onPointerMove = (e: PointerEvent): void => {
-        if (Math.abs(this._downX - e.clientX) > 3 || Math.abs(this._downY - e.clientY) > 3) {
-            document.removeEventListener("pointermove", this.onPointerMove);
-            document.removeEventListener("pointerup", this.onPointerUp);
-            this.startDragging(this._downX, this._downY);
-        }
-        e.stopPropagation();
-        e.preventDefault();
-    }
-    onPointerUp = (e: PointerEvent): void => {
-        document.removeEventListener("pointermove", this.onPointerMove);
-        document.removeEventListener("pointerup", this.onPointerUp);
-    }
-
-    render() {
-        let a1 = this.props.sourceView;
-        let x1 = NumCast(a1.Document.x) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.width) / NumCast(a1.Document.zoomBasis, 1) / 2);
-        let y1 = NumCast(a1.Document.y) + (BoolCast(a1.Document.isMinimized, false) ? 5 : NumCast(a1.Document.height) / NumCast(a1.Document.zoomBasis, 1) / 2);
-
-        let context = this.props.targetView.props.ContainingCollectionView ?
-            (" in the context of " + StrCast(this.props.targetView.props.ContainingCollectionView.props.Document.title)) : "";
-        let text = "link to " + StrCast(this.props.targetView.props.Document.title) + context;
-
-        return (
-            <>
-                <line className="linkview-line linkview-ele"
-                    // style={{ strokeWidth: `${2 * 1 / 2}` }}
-                    x1={`${x1}`} y1={`${y1}`}
-                    x2={`${this._x}`} y2={`${this._y}`} />
-                <foreignObject className="linkview-button-wrapper linkview-ele" width={200} height={100} x={this._x - 100} y={this._y - 50}>
-                    <div className="linkview-button" onPointerDown={this.onPointerDown} onPointerUp={this.followButton} ref={this._ref}>
-                        <p>{text}</p>
-                    </div>
-                </foreignObject>
-            </>
-        );
-    }
-}
-
-//onPointerDown={this.followButton}
\ No newline at end of file
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 940b00a90..02396c3af 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -12,7 +12,6 @@ import "./DocumentView.scss";
 import { FormattedTextBox } from "./FormattedTextBox";
 import { ImageBox } from "./ImageBox";
 import { IconBox } from "./IconBox";
-import { LinkButtonBox } from "./LinkButtonBox";
 import { KeyValueBox } from "./KeyValueBox";
 import { PDFBox } from "./PDFBox";
 import { VideoBox } from "./VideoBox";
@@ -104,7 +103,7 @@ export class DocumentContentsView extends React.Component<DocumentViewProps & {
     render() {
         if (!this.layout && (this.props.layoutKey !== "overlayLayout" || !this.templates.length)) return (null);
         return <ObserverJsxParser
-            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox, LinkButtonBox }}
+            components={{ FormattedTextBox, ImageBox, IconBox, FieldView, CollectionFreeFormView, CollectionDockingView, CollectionSchemaView, CollectionView, CollectionPDFView, CollectionVideoView, WebBox, KeyValueBox, PDFBox, VideoBox, AudioBox, HistogramBox }}
             bindings={this.CreateBindings()}
             jsx={this.finalLayout}
             showWarnings={true}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b705276a6..c71d7ed68 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -555,16 +555,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         var scaling = this.props.ContentScaling();
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
 
-        // // for linkbutton docs
-        // let isLinkButton = BoolCast(this.props.Document.isLinkButton);
-        // let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
-        // let display = isLinkButton ? activeDvs.reduce((found, dv) => {
-        //     let matchSv = this.props.Document.sourceViewId === StrCast(dv.props.Document[Id]);
-        //     let matchTv = this.props.Document.targetViewId === StrCast(dv.props.Document[Id]);
-        //     let match = matchSv || matchTv;
-        //     return match || found;
-        // }, false) : true;
-
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         return (
             <div className={`documentView-node${this.props.isTopMost ? "-topmost" : ""}`}
@@ -581,7 +571,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                     width: nativeWidth,
                     height: nativeHeight,
                     transform: `scale(${scaling}, ${scaling})`,
-                    // display: display ? "block" : "none"
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
diff --git a/src/client/views/nodes/LinkButtonBox.scss b/src/client/views/nodes/LinkButtonBox.scss
deleted file mode 100644
index 6be2dcf60..000000000
--- a/src/client/views/nodes/LinkButtonBox.scss
+++ /dev/null
@@ -1,18 +0,0 @@
-// .linkBox-cont {
-//     width: 200px;
-//     height: 100px;
-//     background-color: black;
-//     text-align: center;
-//     color: white;
-//     padding: 10px;
-//     border-radius: 5px;
-//     position: relative;
-
-//     .linkBox-cont-wrapper {
-//         width: calc(100% - 20px);
-//         position: absolute;
-//         left: 50%;
-//         top: 50%;
-//         transform: translate(-50%, -50%);
-//     }
-// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkButtonBox.tsx b/src/client/views/nodes/LinkButtonBox.tsx
deleted file mode 100644
index 440847ead..000000000
--- a/src/client/views/nodes/LinkButtonBox.tsx
+++ /dev/null
@@ -1,63 +0,0 @@
-// import React = require("react");
-// import { library } from '@fortawesome/fontawesome-svg-core';
-// import { faCaretUp, faFilePdf, faFilm, faImage, faObjectGroup, faStickyNote } from '@fortawesome/free-solid-svg-icons';
-// import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-// import { computed, observable, runInAction } from "mobx";
-// import { observer } from "mobx-react";
-// import { FieldView, FieldViewProps } from './FieldView';
-// import "./LinkButtonBox.scss";
-// import { DocumentView } from "./DocumentView";
-// import { Doc } from "../../../new_fields/Doc";
-// import { LinkButtonField } from "../../../new_fields/LinkButtonField";
-// import { Cast, StrCast, BoolCast } from "../../../new_fields/Types";
-// import { CollectionDockingView } from "../collections/CollectionDockingView";
-// import { DocumentManager } from "../../util/DocumentManager";
-// import { Id } from "../../../new_fields/FieldSymbols";
-
-// library.add(faCaretUp);
-// library.add(faObjectGroup);
-// library.add(faStickyNote);
-// library.add(faFilePdf);
-// library.add(faFilm);
-
-// @observer
-// export class LinkButtonBox extends React.Component<FieldViewProps> {
-//     public static LayoutString() { return FieldView.LayoutString(LinkButtonBox); }
-
-//     followLink = (): void => {
-//         console.log("follow link???");
-//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-//         if (targetView && targetView.props.ContainingCollectionView) {
-//             CollectionDockingView.Instance.AddRightSplit(targetView.props.ContainingCollectionView.props.Document);
-//         }
-//     }
-
-//     render() {
-
-//         let field = Cast(this.props.Document[this.props.fieldKey], LinkButtonField, new LinkButtonField({ sourceViewId: "-1", targetViewId: "-1" }));
-//         let targetView = DocumentManager.Instance.getDocumentViewById(field.data.targetViewId);
-
-//         let text = "Could not find link";
-//         if (targetView) {
-//             let context = targetView.props.ContainingCollectionView ? (" in the context of " + StrCast(targetView.props.ContainingCollectionView.props.Document.title)) : "";
-//             text = "Link to " + StrCast(targetView.props.Document.title) + context;
-//         }
-
-//         let activeDvs = DocumentManager.Instance.DocumentViews.filter(dv => dv.isSelected() || BoolCast(dv.props.Document.libraryBrush, false));
-//         let display = activeDvs.reduce((found, dv) => {
-//             let matchSv = field.data.sourceViewId === StrCast(dv.props.Document[Id]);
-//             let matchTv = field.data.targetViewId === StrCast(dv.props.Document[Id]);
-//             let match = matchSv || matchTv;
-//             return match || found;
-//         }, false);
-
-//         return (
-//             <div className="linkBox-cont" style={{ display: display ? "block" : "none" }}>
-//                 <div className="linkBox-cont-wrapper">
-//                     <p>{text}</p>
-//                 </div>
-//             </div >
-//         );
-//     }
-// }
\ No newline at end of file
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index a6511c3fe..eed34b21f 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -71,10 +71,12 @@ class GroupTypesDropdown extends React.Component<GroupTypesDropdownProps> {
 
 
 interface LinkMetadataEditorProps {
+    id: string;
     groupType: string;
     mdDoc: Doc;
     mdKey: string;
     mdValue: string;
+    changeMdIdKey: (id: string, newKey: string) => void;
 }
 @observer
 class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
@@ -86,8 +88,6 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
     setMetadataKey = (value: string): void => {
         let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(this.props.groupType);
 
-        // console.log("set", ...groupMdKeys, typeof (groupMdKeys[0]));
-
         // don't allow user to create existing key
         let newIndex = groupMdKeys.findIndex(key => key.toUpperCase() === value.toUpperCase());
         if (newIndex > -1) {
@@ -105,6 +105,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         if (currIndex === -1) console.error("LinkMetadataEditor: key was not found");
         groupMdKeys[currIndex] = value;
 
+        this.props.changeMdIdKey(this.props.id, value);
         this._key = value;
         LinkManager.Instance.setMetadataKeysForGroup(this.props.groupType, [...groupMdKeys]);
     }
@@ -133,7 +134,7 @@ class LinkMetadataEditor extends React.Component<LinkMetadataEditorProps> {
         return (
             <div className="linkEditor-metadata-row">
                 <input className={this._keyError ? "linkEditor-error" : ""} type="text" value={this._key} placeholder="key" onChange={e => this.setMetadataKey(e.target.value)}></input>:
-                <input type="text" value={this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
+                <input type="text" value={this._value === "new key" ? "" : this._value} placeholder="value" onChange={e => this.setMetadataValue(e.target.value)}></input>
                 <button onClick={() => this.removeMetadata()}><FontAwesomeIcon icon="times" size="sm" /></button>
             </div>
         );
@@ -148,6 +149,17 @@ interface LinkGroupEditorProps {
 @observer
 export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
+    private _metadataIds: Map<string, string> = new Map();
+
+    constructor(props: LinkGroupEditorProps) {
+        super(props);
+
+        let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(StrCast(props.groupDoc.type));
+        groupMdKeys.forEach(key => {
+            this._metadataIds.set(key, Utils.GenerateGuid());
+        });
+    }
+
     @action
     setGroupType = (groupType: string): void => {
         this.props.groupDoc.type = groupType;
@@ -188,12 +200,18 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
     @action
     addMetadata = (groupType: string): void => {
+        this._metadataIds.set("new key", Utils.GenerateGuid());
         let mdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
         // only add "new key" if there is no other key with value "new key"; prevents spamming
         if (mdKeys.indexOf("new key") === -1) mdKeys.push("new key");
         LinkManager.Instance.setMetadataKeysForGroup(groupType, mdKeys);
     }
 
+    // for key rendering purposes
+    changeMdIdKey = (id: string, newKey: string) => {
+        this._metadataIds.set(newKey, id);
+    }
+
     renderMetadata = (): JSX.Element[] => {
         let metadata: Array<JSX.Element> = [];
         let groupDoc = this.props.groupDoc;
@@ -201,25 +219,24 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
         let groupType = StrCast(groupDoc.type);
         let groupMdKeys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
 
-        if (groupMdKeys) {
-            groupMdKeys.forEach((key, index) => {
-                metadata.push(
-                    <LinkMetadataEditor key={"mded-" + index} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={(mdDoc[key] === undefined) ? "" : StrCast(mdDoc[key])} />
-                );
-            });
-        }
+        groupMdKeys.forEach((key) => {
+            let val = StrCast(mdDoc[key]);
+            metadata.push(
+                <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
+            );
+        });
         return metadata;
     }
 
     viewGroupAsTable = (groupType: string): JSX.Element => {
         let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        let index = keys.indexOf("");
+        if (index > -1) keys.splice(index, 1);
         let cols = ["anchor1", "anchor2", ...[...keys]];
-        // keys.forEach(k => cols.push(k));
-        // console.log("COLS", ...cols);
         let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
         let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
         let ref = React.createRef<HTMLDivElement>();
-        return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)}><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+        return <div ref={ref}><button className="linkEditor-button" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
     }
 
     render() {
@@ -233,7 +250,7 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
                     <button className="linkEditor-button" disabled title="Copy group to opposite anchor"><FontAwesomeIcon icon="exchange-alt" size="sm" /></button>
                     <button className="linkEditor-button" onClick={() => this.removeGroupFromLink(groupType)} title="Remove group from link"><FontAwesomeIcon icon="times" size="sm" /></button>
                     <button className="linkEditor-button" disabled title="Delete group"><FontAwesomeIcon icon="trash" size="sm" /></button>
-                    <button className="linkEditor-button" disabled><FontAwesomeIcon icon="table" size="sm" /></button>
+                    <button className="linkEditor-button" disabled title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button>
                 </>
             );
         } else {
diff --git a/src/new_fields/LinkButtonField.ts b/src/new_fields/LinkButtonField.ts
deleted file mode 100644
index e6d1de749..000000000
--- a/src/new_fields/LinkButtonField.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-// import { Deserializable } from "../client/util/SerializationHelper";
-// import { serializable, primitive, createSimpleSchema, object } from "serializr";
-// import { ObjectField } from "./ObjectField";
-// import { Copy, ToScriptString } from "./FieldSymbols";
-// import { Doc } from "./Doc";
-// import { DocumentView } from "../client/views/nodes/DocumentView";
-
-// export type LinkButtonData = {
-//     sourceViewId: string,
-//     targetViewId: string
-// };
-
-// const LinkButtonSchema = createSimpleSchema({
-//     sourceViewId: true,
-//     targetViewId: true
-// });
-
-// @Deserializable("linkButton")
-// export class LinkButtonField extends ObjectField {
-//     @serializable(object(LinkButtonSchema))
-//     readonly data: LinkButtonData;
-
-//     constructor(data: LinkButtonData) {
-//         super();
-//         this.data = data;
-//     }
-
-//     [Copy]() {
-//         return new LinkButtonField(this.data);
-//     }
-
-//     [ToScriptString]() {
-//         return "invalid";
-//     }
-// }
-- 
cgit v1.2.3-70-g09d2


From f089dba7a271512bcebca2741f2f4f31243ffd47 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Wed, 26 Jun 2019 09:08:13 -0400
Subject: fixed treeview drag drop. fixed  isAnnotationOverlay

---
 src/client/views/collections/CollectionTreeView.tsx               | 2 +-
 src/client/views/collections/CollectionView.tsx                   | 2 +-
 .../collections/collectionFreeForm/CollectionFreeFormView.tsx     | 2 +-
 src/client/views/nodes/DocumentView.tsx                           | 8 ++++++--
 4 files changed, 9 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index e93faa502..d33a4786c 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -85,7 +85,7 @@ class TreeView extends React.Component<TreeViewProps> {
         return keyList.length ? keyList[0] : "data";
     }
 
-    @computed get dataDoc() { return this.props.dataDoc; }
+    @computed get dataDoc() { return BoolCast(this.props.document.isTemplate) ? this.props.dataDoc : this.props.document; }
 
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer && this._treedropDisposer();
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 21936c8aa..0517c17bf 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -43,7 +43,7 @@ export class CollectionView extends React.Component<FieldViewProps> {
         return (null);
     }
 
-    get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldExt === "annotations"; } // bcz: ? Why do we need to compare Id's?
+    get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; }
 
     onContextMenu = (e: React.MouseEvent): void => {
         if (!this.isAnnotationOverlay && !e.isPropagationStopped() && this.props.Document[Id] !== CurrentUserUtils.MainDocId) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index dc916a613..a3ca02941 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -47,7 +47,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
     @computed get nativeWidth() { return this.Document.nativeWidth || 0; }
     @computed get nativeHeight() { return this.Document.nativeHeight || 0; }
-    public get isAnnotationOverlay() { return this.props.fieldKey && this.props.fieldExt === "annotations"; }
+    public get isAnnotationOverlay() { return this.props.fieldKey === "annotations" || this.props.fieldExt === "annotations"; }
     private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; }
     private panX = () => this.Document.panX || 0;
     private panY = () => this.Document.panY || 0;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c586885a3..9c3479ec2 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -268,8 +268,10 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         e.stopPropagation();
         let altKey = e.altKey;
         let ctrlKey = e.ctrlKey;
-        if (this._doubleTap && !this.props.renderDepth) {
-            this.props.addDocTab(this.props.Document, this.props.DataDoc, "inTab");
+        if (this._doubleTap && this.props.renderDepth) {
+            let fullScreenAlias = Doc.MakeAlias(this.props.Document);
+            fullScreenAlias.templates = new List<string>();
+            this.props.addDocTab(fullScreenAlias, this.props.DataDoc, "inTab");
             SelectionManager.DeselectAll();
             this.props.Document.libraryBrush = false;
         }
@@ -436,6 +438,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     @action
     addTemplate = (template: Template) => {
         this.templates.push(template.Layout);
+        this.templates = this.templates;
     }
 
     @action
@@ -446,6 +449,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 break;
             }
         }
+        this.templates = this.templates;
     }
 
     freezeNativeDimensions = (): void => {
-- 
cgit v1.2.3-70-g09d2


From d0ff42632f8a155303e11945a1a974a15052f0db Mon Sep 17 00:00:00 2001
From: Fawn <fangrui_tong@brown.edu>
Date: Wed, 26 Jun 2019 11:40:36 -0400
Subject: link menu styling

---
 src/client/util/LinkManager.ts                     | 44 +---------------------
 .../CollectionFreeFormLinksView.tsx                | 21 +----------
 src/client/views/nodes/DocumentView.tsx            |  2 -
 src/client/views/nodes/LinkEditor.tsx              |  1 -
 src/client/views/nodes/LinkMenu.scss               | 25 +++++++++---
 src/client/views/nodes/LinkMenuGroup.tsx           | 21 ++++++++++-
 src/client/views/nodes/LinkMenuItem.scss           |  2 +
 7 files changed, 44 insertions(+), 72 deletions(-)

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

diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index db814082f..97c816001 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -24,10 +24,6 @@ import { CurrentUserUtils } from "../../server/authentication/models/current_use
  * - user defined kvps 
  */
 export class LinkManager {
-    // static Instance: LinkManager;
-    // private constructor() {
-    //     LinkManager.Instance = this;
-    // }
 
     private static _instance: LinkManager;
     public static get Instance(): LinkManager {
@@ -36,12 +32,11 @@ export class LinkManager {
     private constructor() {
     }
 
+    // the linkmanagerdoc stores a list of docs representing all linkdocs in 'allLinks' and a list of strings representing all group types in 'allGroupTypes'
+    // lists of strings representing the metadata keys for each group type is stored under a key that is the same as the group type 
     public get LinkManagerDoc(): Doc | undefined {
         return FieldValue(Cast(CurrentUserUtils.UserDocument.linkManagerDoc, Doc));
     }
-    // @observable public allLinks: Array<Doc> = []; //List<Doc> = new List<Doc>([]); // list of link docs
-    // @observable public groupMetadataKeys: Map<string, Array<string>> = new Map();
-    // map of group type to list of its metadata keys; serves as a dictionary of groups to what kind of metadata it holds
 
     public getAllLinks(): Doc[] {
         return LinkManager.Instance.LinkManagerDoc ? LinkManager.Instance.LinkManagerDoc.allLinks ? DocListCast(LinkManager.Instance.LinkManagerDoc.allLinks) : [] : [];
@@ -160,14 +155,6 @@ export class LinkManager {
         LinkManager.Instance.setAnchorGroups(linkDoc, anchor, newGroups);
     }
 
-    // public doesAnchorHaveGroup(linkDoc: Doc, anchor: Doc, groupDoc: Doc): boolean {
-    //     let groups = LinkManager.Instance.getAnchorGroups(linkDoc, anchor);
-    //     let index = groups.findIndex(gDoc => {
-    //         return StrCast(groupDoc.type).toUpperCase() === StrCast(gDoc.type).toUpperCase();
-    //     });
-    //     return index > -1;
-    // }
-
     // returns map of group type to anchor's links in that group type
     public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> {
         let related = this.getAllRelatedLinks(anchor);
@@ -195,19 +182,6 @@ export class LinkManager {
         return anchorGroups;
     }
 
-    // public addMetadataKeyToGroup(groupType: string, key: string): boolean {
-    //     if (LinkManager.Instance.LinkManagerDoc) {
-    //         if (LinkManager.Instance.LinkManagerDoc[groupType]) {
-    //             let keyList = LinkManager.Instance.findMetadataKeysInGroup(groupType);
-    //             keyList.push(key);
-    //             LinkManager.Instance.LinkManagerDoc[groupType] = new List<string>(keyList);
-    //             return true;
-    //         }
-    //         return false;
-    //     }
-    //     return false;
-    // }
-
     public getMetadataKeysInGroup(groupType: string): string[] {
         if (LinkManager.Instance.LinkManagerDoc) {
             return LinkManager.Instance.LinkManagerDoc[groupType] ? Cast(LinkManager.Instance.LinkManagerDoc[groupType], listSpec("string"), []) : [];
@@ -254,18 +228,4 @@ export class LinkManager {
             return Cast(linkDoc.anchor1, Doc, new Doc);
         }
     }
-
-
-    // @action
-    // public addLinkProxy(proxy: Doc) {
-    //     LinkManager.Instance.linkProxies.push(proxy);
-    // }
-
-    // public findLinkProxy(sourceViewId: string, targetViewId: string): Doc | undefined {
-    //     let index = LinkManager.Instance.linkProxies.findIndex(p => {
-    //         return StrCast(p.sourceViewId) === sourceViewId && StrCast(p.targetViewId) === targetViewId;
-    //     });
-    //     return index > -1 ? LinkManager.Instance.linkProxies[index] : undefined;
-    // }
-
 }
\ No newline at end of file
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index bb8e8a5c2..ebeb1fcee 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -94,24 +94,12 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
 
     @computed
     get uniqueConnections() {
-        // DocumentManager.Instance.LinkedDocumentViews.forEach(d => {
-        //     console.log("CONNECTION", StrCast(d.a.props.Document.title), StrCast(d.b.props.Document.title));
-        // });
-
-        // console.log("CONNECTIONS");
-
         let connections = DocumentManager.Instance.LinkedDocumentViews.reduce((drawnPairs, connection) => {
             let srcViews = this.documentAnchors(connection.a);
             let targetViews = this.documentAnchors(connection.b);
 
-            // console.log(srcViews.length, targetViews.length);
             let possiblePairs: { a: Doc, b: Doc, }[] = [];
-            srcViews.map(sv => {
-                targetViews.map(tv => {
-                    // console.log("PUSH", StrCast(sv.props.Document.title), StrCast(sv.props.Document.id), StrCast(tv.props.Document.title), StrCast(tv.props.Document.id));
-                    possiblePairs.push({ a: sv.props.Document, b: tv.props.Document });
-                });
-            });
+            srcViews.map(sv => targetViews.map(tv => possiblePairs.push({ a: sv.props.Document, b: tv.props.Document })));
             possiblePairs.map(possiblePair => {
                 if (!drawnPairs.reduce((found, drawnPair) => {
                     let match1 = (Doc.AreProtosEqual(possiblePair.a, drawnPair.a) && Doc.AreProtosEqual(possiblePair.b, drawnPair.b));
@@ -132,13 +120,6 @@ export class CollectionFreeFormLinksView extends React.Component<CollectionViewP
             return <CollectionFreeFormLinkView key={x} A={c.a} B={c.b} LinkDocs={c.l}
                 removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
         });
-
-        // return DocumentManager.Instance.LinkedDocumentViews.map(c => {
-        //     // let x = c.l.reduce((p, l) => p + l[Id], "");
-        //     let x = c.a.Document[Id] + c.b.Document[Id];
-        //     return <CollectionFreeFormLinkView key={x} A={c.a.props.Document} B={c.b.props.Document} LinkDoc={c.l}
-        //         removeDocument={this.props.removeDocument} addDocument={this.props.addDocument} />;
-        // });
     }
 
     render() {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c71d7ed68..d77e08089 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -162,9 +162,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._animateToIconDisposer = reaction(() => this.props.Document.isIconAnimating, (values) =>
             (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
             , { fireImmediately: true });
-        // console.log("CREATED NEW DOC VIEW", StrCast(this.props.Document.title), DocumentManager.Instance.DocumentViews.length);
         DocumentManager.Instance.DocumentViews.push(this);
-        // console.log("ADDED TO DOC MAN", StrCast(this.props.Document.title), DocumentManager.Instance.DocumentViews.length);
     }
 
     animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
diff --git a/src/client/views/nodes/LinkEditor.tsx b/src/client/views/nodes/LinkEditor.tsx
index 80eadf668..87ebeefdb 100644
--- a/src/client/views/nodes/LinkEditor.tsx
+++ b/src/client/views/nodes/LinkEditor.tsx
@@ -224,7 +224,6 @@ export class LinkGroupEditor extends React.Component<LinkGroupEditorProps> {
 
         groupMdKeys.forEach((key) => {
             let val = StrCast(mdDoc[key]);
-            console.log(key, val);
             metadata.push(
                 <LinkMetadataEditor key={"mded-" + this._metadataIds.get(key)} id={this._metadataIds.get(key)!} groupType={groupType} mdDoc={mdDoc} mdKey={key} mdValue={val} changeMdIdKey={this.changeMdIdKey} />
             );
diff --git a/src/client/views/nodes/LinkMenu.scss b/src/client/views/nodes/LinkMenu.scss
index ae3446e25..1dd933c32 100644
--- a/src/client/views/nodes/LinkMenu.scss
+++ b/src/client/views/nodes/LinkMenu.scss
@@ -20,13 +20,28 @@
     }
 
     .linkMenu-group-name {
-        padding: 4px 6px;
-        line-height: 12px;
-        border-radius: 5px;
-        font-weight: bold;
+        display: flex;
 
         &:hover {
-            background-color: lightgray;
+            p {
+                background-color: lightgray;
+                width: calc(100% - 26px);
+            }
+            .linkEditor-tableButton {
+                display: block;
+            }
+        }
+
+        p {
+            width: 100%;
+            padding: 4px 6px;
+            line-height: 12px;
+            border-radius: 5px;
+            font-weight: bold;
+        }
+
+        .linkEditor-tableButton {
+            display: none;
         }
     }
 }
diff --git a/src/client/views/nodes/LinkMenuGroup.tsx b/src/client/views/nodes/LinkMenuGroup.tsx
index 71326f703..732e76997 100644
--- a/src/client/views/nodes/LinkMenuGroup.tsx
+++ b/src/client/views/nodes/LinkMenuGroup.tsx
@@ -8,8 +8,10 @@ import React = require("react");
 import { Doc, DocListCast } from "../../../new_fields/Doc";
 import { Id } from "../../../new_fields/FieldSymbols";
 import { LinkManager } from "../../util/LinkManager";
-import { DragLinksAsDocuments, DragManager } from "../../util/DragManager";
+import { DragLinksAsDocuments, DragManager, SetupDrag } from "../../util/DragManager";
 import { emptyFunction } from "../../../Utils";
+import { Docs } from "../../documents/Documents";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 
 interface LinkMenuGroupProps {
     sourceDoc: Doc;
@@ -22,6 +24,7 @@ interface LinkMenuGroupProps {
 export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
 
     private _drag = React.createRef<HTMLDivElement>();
+    private _table = React.createRef<HTMLDivElement>();
 
     onLinkButtonDown = (e: React.PointerEvent): void => {
         e.stopPropagation();
@@ -55,6 +58,17 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
         e.stopPropagation();
     }
 
+    viewGroupAsTable = (groupType: string): JSX.Element => {
+        let keys = LinkManager.Instance.getMetadataKeysInGroup(groupType);
+        let index = keys.indexOf("");
+        if (index > -1) keys.splice(index, 1);
+        let cols = ["anchor1", "anchor2", ...[...keys]];
+        let docs: Doc[] = LinkManager.Instance.getAllMetadataDocsInGroup(groupType);
+        let createTable = action(() => Docs.SchemaDocument(cols, docs, { width: 500, height: 300, title: groupType + " table" }));
+        let ref = React.createRef<HTMLDivElement>();
+        return <div ref={ref}><button className="linkEditor-button linkEditor-tableButton" onPointerDown={SetupDrag(ref, createTable)} title="Drag to view relationship table"><FontAwesomeIcon icon="table" size="sm" /></button></div>;
+    }
+
     render() {
         let groupItems = this.props.group.map(linkDoc => {
             let destination = LinkManager.Instance.getOppositeAnchor(linkDoc, this.props.sourceDoc);
@@ -64,7 +78,10 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
 
         return (
             <div className="linkMenu-group">
-                <p className="linkMenu-group-name" ref={this._drag} onPointerDown={this.onLinkButtonDown} >{this.props.groupType}:</p>
+                <div className="linkMenu-group-name">
+                    <p ref={this._drag} onPointerDown={this.onLinkButtonDown}>{this.props.groupType}:</p>
+                    {this.viewGroupAsTable(this.props.groupType)}
+                </div>
                 <div className="linkMenu-group-wrapper">
                     {groupItems}
                 </div>
diff --git a/src/client/views/nodes/LinkMenuItem.scss b/src/client/views/nodes/LinkMenuItem.scss
index 25d167231..175a93cb2 100644
--- a/src/client/views/nodes/LinkMenuItem.scss
+++ b/src/client/views/nodes/LinkMenuItem.scss
@@ -1,4 +1,5 @@
 @import "../globalCssVariables";
+
 .linkMenu-item {
     // border-top: 0.5px solid $main-accent; 
     position: relative;
@@ -13,6 +14,7 @@
             padding: 4px 6px;
             line-height: 12px;
             border-radius: 5px;
+            overflow-wrap: break-word;
         }
     }
 
-- 
cgit v1.2.3-70-g09d2


From 084dd6d9b0ad51133025ba6bd2702fc44b1b6c31 Mon Sep 17 00:00:00 2001
From: bob <bcz@cs.brown.edu>
Date: Wed, 26 Jun 2019 12:56:22 -0400
Subject: fixes to datadoc usage.  fixed tree view for textboxes.

---
 src/client/views/DocumentDecorations.tsx           |  2 +-
 src/client/views/MainView.tsx                      |  2 +-
 .../views/collections/CollectionBaseView.tsx       |  2 +-
 .../views/collections/CollectionSchemaView.tsx     | 10 +--
 src/client/views/collections/CollectionSubView.tsx |  5 +-
 .../views/collections/CollectionTreeView.tsx       | 81 +++++++++++-----------
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 +--
 src/client/views/nodes/DocumentView.tsx            | 19 ++---
 src/client/views/nodes/FieldView.tsx               |  2 +-
 src/client/views/nodes/FormattedTextBox.tsx        |  6 +-
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/client/views/nodes/PDFBox.tsx                  |  2 +-
 src/new_fields/Doc.ts                              |  2 +-
 13 files changed, 75 insertions(+), 69 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index b9537fca6..557db3639 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -176,7 +176,7 @@ export class DocumentDecorations extends React.Component<{}, { value: string }>
         let dragDocView = SelectionManager.SelectedDocuments()[0];
         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));
+        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.moveDocument = SelectionManager.SelectedDocuments()[0].props.moveDocument;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index d26e24748..e48de27d7 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -203,7 +203,7 @@ export class MainView extends React.Component {
         let mainCont = this.mainContainer;
         let content = !mainCont ? (null) :
             <DocumentView Document={mainCont}
-                DataDoc={mainCont}
+                DataDoc={undefined}
                 addDocument={undefined}
                 addDocTab={emptyFunction}
                 removeDocument={undefined}
diff --git a/src/client/views/collections/CollectionBaseView.tsx b/src/client/views/collections/CollectionBaseView.tsx
index a4a9ec994..22f0cf6e2 100644
--- a/src/client/views/collections/CollectionBaseView.tsx
+++ b/src/client/views/collections/CollectionBaseView.tsx
@@ -59,7 +59,7 @@ export class CollectionBaseView extends React.Component<CollectionViewProps> {
         }
     }
 
-    @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document, this.props.fieldKey, this.props.fieldExt); }
+    @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 dataField() { return this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey; }
 
     active = (): boolean => {
diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx
index 796882724..a8061f9f1 100644
--- a/src/client/views/collections/CollectionSchemaView.tsx
+++ b/src/client/views/collections/CollectionSchemaView.tsx
@@ -437,15 +437,16 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
         }
         return true;
     }
-    private PanelWidth = () => this.nativeWidth * this.contentScaling();
-    private PanelHeight = () => this.nativeHeight * this.contentScaling();
+    private PanelWidth = () => this.nativeWidth ? this.nativeWidth * this.contentScaling() : this.props.width();
+    private PanelHeight = () => this.nativeHeight ? this.nativeHeight * this.contentScaling() : this.props.height();
     private getTransform = () => this.props.getTransform().translate(-this.centeringOffset, 0).scale(1 / this.contentScaling());
-    get centeringOffset() { return (this.props.width() - this.nativeWidth * this.contentScaling()) / 2; }
+    get centeringOffset() { return this.nativeWidth ? (this.props.width() - this.nativeWidth * this.contentScaling()) / 2 : 0; }
     @action
     onPreviewScriptChange = (e: React.ChangeEvent<HTMLInputElement>) => {
         this.props.setPreviewScript(e.currentTarget.value);
     }
     render() {
+        let self = this;
         let input = this.props.previewScript === undefined ? (null) :
             <div ref={this.createTarget}><input className="collectionSchemaView-input" value={this.props.previewScript} onChange={this.onPreviewScriptChange}
                 style={{ left: `calc(50% - ${Math.min(75, (this.props.Document ? this.PanelWidth() / 2 : 75))}px)` }} /></div>;
@@ -462,7 +463,8 @@ export class CollectionSchemaPreview extends React.Component<CollectionSchemaPre
                         moveDocument={this.props.moveDocument}
                         ScreenToLocalTransform={this.getTransform}
                         ContentScaling={this.contentScaling}
-                        PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight}
+                        PanelWidth={this.PanelWidth}
+                        PanelHeight={this.PanelHeight}
                         ContainingCollectionView={this.props.CollectionView}
                         focus={emptyFunction}
                         parentActive={this.props.active}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 58251cd6e..0bed196e6 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -45,13 +45,12 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
             this.createDropTarget(ele);
         }
 
-        @computed get dataDoc() { return Doc.resolvedFieldDataDoc(BoolCast(this.props.Document.isTemplate) ? 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; }
+        @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); }
 
         get childDocs() {
             //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 DocListCast((BoolCast(this.props.Document.isTemplate) ? this.dataDoc : this.props.Document)[this.dataField]);
+            return DocListCast((BoolCast(this.props.Document.isTemplate) ? this.extensionDoc : this.props.Document)[this.props.fieldExt ? this.props.fieldExt : this.props.fieldKey]);
         }
 
         @action
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index d33a4786c..a8324eba2 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -29,7 +29,7 @@ import React = require("react");
 
 export interface TreeViewProps {
     document: Doc;
-    dataDoc: Doc;
+    dataDoc?: Doc;
     containingCollection: Doc;
     deleteDoc: (doc: Doc) => boolean;
     moveDocument: DragManager.MoveFunction;
@@ -66,14 +66,14 @@ class TreeView extends React.Component<TreeViewProps> {
     @observable _collapsed: boolean = true;
 
     @computed get fieldKey() {
-        let keys = Array.from(Object.keys(this.dataDoc));
-        if (this.dataDoc.proto instanceof Doc) {
-            keys.push(...Array.from(Object.keys(this.dataDoc.proto)));
+        let keys = Array.from(Object.keys(this.resolvedDataDoc));
+        if (this.resolvedDataDoc.proto instanceof Doc) {
+            keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
             while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
         }
         let keyList: string[] = [];
         keys.map(key => {
-            let docList = Cast(this.dataDoc[key], listSpec(Doc));
+            let docList = Cast(this.resolvedDataDoc[key], listSpec(Doc));
             if (docList && docList.length > 0) {
                 keyList.push(key);
             }
@@ -85,7 +85,7 @@ class TreeView extends React.Component<TreeViewProps> {
         return keyList.length ? keyList[0] : "data";
     }
 
-    @computed get dataDoc() { return BoolCast(this.props.document.isTemplate) ? this.props.dataDoc : this.props.document; }
+    @computed get resolvedDataDoc() { return BoolCast(this.props.document.isTemplate) && this.props.dataDoc ? this.props.dataDoc : this.props.document; }
 
     protected createTreeDropTarget = (ele: HTMLDivElement) => {
         this._treedropDisposer && this._treedropDisposer();
@@ -94,7 +94,7 @@ class TreeView extends React.Component<TreeViewProps> {
         }
     }
 
-    @undoBatch delete = () => this.props.deleteDoc(this.dataDoc);
+    @undoBatch delete = () => this.props.deleteDoc(this.resolvedDataDoc);
     @undoBatch openRight = async () => this.props.addDocTab(this.props.document, this.props.document, "onRight");
 
     onPointerDown = (e: React.PointerEvent) => e.stopPropagation();
@@ -126,7 +126,7 @@ class TreeView extends React.Component<TreeViewProps> {
 
     @action
     remove = (document: Document, key: string): boolean => {
-        let children = Cast(this.dataDoc[key], listSpec(Doc), []);
+        let children = Cast(this.resolvedDataDoc[key], listSpec(Doc), []);
         if (children.indexOf(document) !== -1) {
             children.splice(children.indexOf(document), 1);
             return true;
@@ -142,8 +142,8 @@ class TreeView extends React.Component<TreeViewProps> {
     indent = () => this.props.addDocument(this.props.document) && this.delete()
 
     renderBullet() {
-        let docList = Cast(this.dataDoc[this.fieldKey], listSpec(Doc));
-        let doc = Cast(this.dataDoc[this.fieldKey], Doc);
+        let docList = Cast(this.resolvedDataDoc[this.fieldKey], listSpec(Doc));
+        let doc = Cast(this.resolvedDataDoc[this.fieldKey], Doc);
         let isDoc = doc instanceof Doc || docList;
         return <div className="bullet" onClick={action(() => this._collapsed = !this._collapsed)}>
             {<FontAwesomeIcon icon={this._collapsed ? (isDoc ? "caret-square-right" : "caret-right") : (isDoc ? "caret-square-down" : "caret-down")} />}
@@ -161,15 +161,15 @@ class TreeView extends React.Component<TreeViewProps> {
     editableView = (key: string, style?: string) => (<EditableView
         oneLine={true}
         display={"inline"}
-        editing={this.dataDoc[Id] === TreeView.loadId}
+        editing={this.resolvedDataDoc[Id] === TreeView.loadId}
         contents={StrCast(this.props.document[key])}
         onClick={this.titleClicked}
         height={36}
         fontStyle={style}
         GetValue={() => StrCast(this.props.document[key])}
-        SetValue={(value: string) => (Doc.GetProto(this.dataDoc)[key] = value) ? true : true}
+        SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc)[key] = value) ? true : true}
         OnFillDown={(value: string) => {
-            Doc.GetProto(this.dataDoc)[key] = value;
+            Doc.GetProto(this.resolvedDataDoc)[key] = value;
             let doc = Docs.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);
@@ -178,13 +178,13 @@ class TreeView extends React.Component<TreeViewProps> {
     />)
 
     @computed get keyList() {
-        let keys = Array.from(Object.keys(this.dataDoc));
-        if (this.dataDoc.proto instanceof Doc) {
-            keys.push(...Array.from(Object.keys(this.dataDoc.proto)));
+        let keys = Array.from(Object.keys(this.resolvedDataDoc));
+        if (this.resolvedDataDoc.proto instanceof Doc) {
+            keys.push(...Array.from(Object.keys(this.resolvedDataDoc.proto)));
             while (keys.indexOf("proto") !== -1) keys.splice(keys.indexOf("proto"), 1);
         }
-        let keyList: string[] = keys.reduce((l, key) => Cast(this.dataDoc[key], listSpec(Doc)) ? [...l, key] : l, [] as string[]);
-        keys.map(key => Cast(this.dataDoc[key], Doc) instanceof Doc && keyList.push(key));
+        let keyList: string[] = keys.reduce((l, key) => Cast(this.resolvedDataDoc[key], listSpec(Doc)) ? [...l, key] : l, [] as string[]);
+        keys.map(key => Cast(this.resolvedDataDoc[key], Doc) instanceof Doc && keyList.push(key));
         if (keyList.indexOf(this.fieldKey) !== -1) {
             keyList.splice(keyList.indexOf(this.fieldKey), 1);
         }
@@ -196,7 +196,7 @@ class TreeView extends React.Component<TreeViewProps> {
      */
     renderTitle() {
         let reference = React.createRef<HTMLDivElement>();
-        let onItemDown = SetupDrag(reference, () => this.dataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
+        let onItemDown = SetupDrag(reference, () => this.resolvedDataDoc, this.move, this.props.dropAction, this.props.treeViewId, true);
 
         let headerElements = (
             <span className="collectionTreeView-keyHeader" key={this._chosenKey}
@@ -208,7 +208,7 @@ class TreeView extends React.Component<TreeViewProps> {
                 {this._chosenKey}
             </span>);
         let dataDocs = CollectionDockingView.Instance ? Cast(CollectionDockingView.Instance.props.Document[this.fieldKey], listSpec(Doc), []) : [];
-        let openRight = dataDocs && dataDocs.indexOf(this.dataDoc) !== -1 ? (null) : (
+        let openRight = dataDocs && dataDocs.indexOf(this.resolvedDataDoc) !== -1 ? (null) : (
             <div className="treeViewItem-openRight" onPointerDown={this.onPointerDown} onClick={this.openRight}>
                 <FontAwesomeIcon icon="angle-right" size="lg" />
             </div>);
@@ -229,13 +229,13 @@ class TreeView extends React.Component<TreeViewProps> {
 
     onWorkspaceContextMenu = (e: React.MouseEvent): void => {
         if (!e.isPropagationStopped()) { // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7
-            ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.dataDoc)) });
+            ContextMenu.Instance.addItem({ description: "Open as Workspace", event: undoBatch(() => MainView.Instance.openWorkspace(this.resolvedDataDoc)) });
             ContextMenu.Instance.addItem({ description: "Open Fields", event: () => { let kvp = Docs.KVPDocument(this.props.document, { width: 300, height: 300 }); this.props.addDocTab(kvp, kvp, "onRight"); }, icon: "layer-group" });
             if (NumCast(this.props.document.viewType) !== CollectionViewType.Docking) {
-                ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.dataDoc, "inTab"), icon: "folder" });
-                ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.dataDoc, "onRight"), icon: "caret-square-right" });
-                if (DocumentManager.Instance.getDocumentViews(this.dataDoc).length) {
-                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.dataDoc).map(view => view.props.focus(this.props.document)) });
+                ContextMenu.Instance.addItem({ description: "Open Tab", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "inTab"), icon: "folder" });
+                ContextMenu.Instance.addItem({ description: "Open Right", event: () => this.props.addDocTab(this.props.document, this.resolvedDataDoc, "onRight"), icon: "caret-square-right" });
+                if (DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).length) {
+                    ContextMenu.Instance.addItem({ description: "Focus", event: () => DocumentManager.Instance.getDocumentViews(this.resolvedDataDoc).map(view => view.props.focus(this.props.document)) });
                 }
                 ContextMenu.Instance.addItem({ description: "Delete Item", event: undoBatch(() => this.props.deleteDoc(this.props.document)) });
             } else {
@@ -252,9 +252,9 @@ class TreeView extends React.Component<TreeViewProps> {
         let before = x[1] < bounds[1];
         let inside = x[0] > bounds[0] + 75 || (!before && !this._collapsed);
         if (de.data instanceof DragManager.DocumentDragData) {
-            let addDoc = (doc: Doc) => this.props.addDocument(doc, this.dataDoc, before);
+            let addDoc = (doc: Doc) => this.props.addDocument(doc, this.resolvedDataDoc, before);
             if (inside) {
-                let docList = Cast(this.dataDoc.data, listSpec(Doc));
+                let docList = Cast(this.resolvedDataDoc.data, listSpec(Doc));
                 if (docList !== undefined) {
                     addDoc = (doc: Doc) => { docList && docList.push(doc); return true; };
                 }
@@ -262,10 +262,10 @@ class TreeView extends React.Component<TreeViewProps> {
             e.stopPropagation();
             let movedDocs = (de.data.options === this.props.treeViewId ? de.data.draggedDocuments : de.data.droppedDocuments);
             return (de.data.dropAction || de.data.userDropAction) ?
-                de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.dataDoc, before) || added, false)
+                de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before) || added, false)
                 : (de.data.moveDocument) ?
-                    movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.dataDoc, addDoc) || added, false)
-                    : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.dataDoc, before), false);
+                    movedDocs.reduce((added: boolean, d) => de.data.moveDocument(d, this.resolvedDataDoc, addDoc) || added, false)
+                    : de.data.droppedDocuments.reduce((added: boolean, d) => this.props.addDocument(d, this.resolvedDataDoc, before), false);
         }
         return false;
     }
@@ -280,22 +280,23 @@ class TreeView extends React.Component<TreeViewProps> {
 
     render() {
         let contentElement: (JSX.Element | null) = null;
-        let docList = Cast(this.dataDoc[this._chosenKey], listSpec(Doc));
+        let docList = Cast(this.resolvedDataDoc[this._chosenKey], listSpec(Doc));
         let remDoc = (doc: Doc) => this.remove(doc, this._chosenKey);
-        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.dataDoc, this._chosenKey, doc, addBefore, before);
-        let doc = Cast(this.dataDoc[this._chosenKey], Doc);
+        let addDoc = (doc: Doc, addBefore?: Doc, before?: boolean) => Doc.AddDocToList(this.resolvedDataDoc, this._chosenKey, doc, addBefore, before);
+        let doc = Cast(this.resolvedDataDoc[this._chosenKey], Doc);
         let docWidth = () => NumCast(this.props.document.nativeWidth) ? Math.min(this.props.document[WidthSym](), this.props.panelWidth() - 5) : this.props.panelWidth() - 5;
         if (!this._collapsed) {
             if (!this.props.document.embed) {
                 contentElement = <ul key={this._chosenKey + "more"}>
-                    {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.dataDoc, this._chosenKey, addDoc, remDoc, this.move,
+                    {TreeView.GetChildElements(doc instanceof Doc ? [doc] : DocListCast(docList), this.props.treeViewId, this.props.document, this.props.dataDoc, this._chosenKey, addDoc, remDoc, this.move,
                         this.props.dropAction, this.props.addDocTab, this.props.ScreenToLocalTransform, this.props.outerXf, this.props.active, this.props.panelWidth)}
                 </ul >;
             } else {
+                console.log("PW = " + this.props.panelWidth());
                 contentElement = <div ref={this._dref} style={{ display: "inline-block", height: this.props.panelHeight() }} key={this.props.document[Id]}>
                     <CollectionSchemaPreview
                         Document={this.props.document}
-                        DataDocument={this.dataDoc}
+                        DataDocument={this.resolvedDataDoc}
                         width={docWidth}
                         height={this.props.panelHeight}
                         getTransform={this.docTransform}
@@ -327,7 +328,7 @@ class TreeView extends React.Component<TreeViewProps> {
         docs: Doc[],
         treeViewId: string,
         containingCollection: Doc,
-        dataDoc: Doc,
+        dataDoc: Doc | undefined,
         key: string,
         add: (doc: Doc, relativeTo?: Doc, before?: boolean) => boolean,
         remove: ((doc: Doc) => boolean),
@@ -412,6 +413,8 @@ export class CollectionTreeView extends CollectionSubView(Document) {
         }
     }
 
+    @computed get resolvedDataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
+
     outerXf = () => Utils.GetScreenTransform(this._mainEle!);
     onTreeDrop = (e: React.DragEvent) => this.onDrop(e, {});
 
@@ -428,11 +431,11 @@ export class CollectionTreeView extends CollectionSubView(Document) {
                 onDrop={this.onTreeDrop}
                 ref={this.createTreeDropTarget}>
                 <EditableView
-                    contents={this.props.DataDoc.title}
+                    contents={this.resolvedDataDoc.title}
                     display={"block"}
                     height={72}
-                    GetValue={() => StrCast(this.props.DataDoc.title)}
-                    SetValue={(value: string) => (Doc.GetProto(this.props.DataDoc).title = value) ? true : true}
+                    GetValue={() => StrCast(this.resolvedDataDoc.title)}
+                    SetValue={(value: string) => (Doc.GetProto(this.resolvedDataDoc).title = value) ? true : true}
                     OnFillDown={(value: string) => {
                         Doc.GetProto(this.props.Document).title = value;
                         let doc = Docs.FreeformDocument([], { title: "", x: 0, y: 0, width: 100, height: 25, templates: new List<string>([Templates.Title.Layout]) });
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index a3ca02941..6672b3a07 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -163,7 +163,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                     return [[range[0][0] > x ? x : range[0][0], range[0][1] < xe ? xe : range[0][1]],
                     [range[1][0] > y ? y : range[1][0], range[1][1] < ye ? ye : range[1][1]]];
                 }, [[minx, maxx], [miny, maxy]]);
-                let ink = Cast(this.dataDoc.ink, InkField);
+                let ink = Cast(this.extensionDoc.ink, InkField);
                 if (ink && ink.inkData) {
                     ink.inkData.forEach((value: StrokeData, key: string) => {
                         let bounds = InkingCanvas.StrokeRect(value);
@@ -295,8 +295,9 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
 
 
     getDocumentViewProps(layoutDoc: Doc): DocumentViewProps {
+        let datadoc = BoolCast(this.props.Document.isTemplate) || this.props.DataDoc === this.props.Document ? undefined : this.props.DataDoc;
         return {
-            DataDoc: BoolCast(this.props.Document.isTemplate) ? layoutDoc : this.props.DataDoc,
+            DataDoc: datadoc,
             Document: layoutDoc,
             addDocument: this.props.addDocument,
             removeDocument: this.props.removeDocument,
@@ -375,7 +376,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
     render() {
         const containerName = `collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`;
         const easing = () => this.props.Document.panTransformType === "Ease";
-        if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+        if (this.props.fieldExt) Doc.UpdateDocumentExtensionForField(this.extensionDoc, this.props.fieldKey);
         return (
             <div className={containerName} ref={this.createDropTarget} onWheel={this.onPointerWheel}
                 style={{ borderRadius: "inherit" }}
@@ -387,7 +388,7 @@ export class CollectionFreeFormView extends CollectionSubView(PanZoomDocument) {
                         easing={easing} zoomScaling={this.zoomScaling} panX={this.panX} panY={this.panY}>
 
                         <CollectionFreeFormLinksView {...this.props} key="freeformLinks">
-                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.dataDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} >
+                            <InkingCanvas getScreenTransform={this.getTransform} Document={this.extensionDoc} inkFieldKey={this.props.fieldExt ? "ink" : this.props.fieldKey + "_ink"} >
                                 {this.childViews}
                             </InkingCanvas>
                         </CollectionFreeFormLinksView>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9c3479ec2..eb4f56af1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -67,7 +67,7 @@ const LinkDoc = makeInterface(linkSchema);
 export interface DocumentViewProps {
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     Document: Doc;
-    DataDoc: Doc;
+    DataDoc?: Doc;
     addDocument?: (doc: Doc, allowDuplicates?: boolean) => boolean;
     removeDocument?: (doc: Doc) => boolean;
     moveDocument?: (doc: Doc, targetCollection: Doc, addDocument: (document: Doc) => boolean) => boolean;
@@ -209,10 +209,11 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         e.stopPropagation();
     }
 
+    get dataDoc() { return this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
     startDragging(x: number, y: number, dropAction: dropActionType, dragSubBullets: boolean) {
         if (this._mainCont.current) {
             let allConnected = [this.props.Document, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
-            let alldataConnected = [this.props.DataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
+            let alldataConnected = [this.dataDoc, ...(dragSubBullets ? DocListCast(this.props.Document.subBulletDocs) : [])];
             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);
@@ -271,7 +272,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (this._doubleTap && this.props.renderDepth) {
             let fullScreenAlias = Doc.MakeAlias(this.props.Document);
             fullScreenAlias.templates = new List<string>();
-            this.props.addDocTab(fullScreenAlias, this.props.DataDoc, "inTab");
+            this.props.addDocTab(fullScreenAlias, this.dataDoc, "inTab");
             SelectionManager.DeselectAll();
             this.props.Document.libraryBrush = false;
         }
@@ -348,7 +349,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         this._downY = e.clientY;
         this._hitExpander = DocListCast(this.props.Document.subBulletDocs).length > 0;
         if (e.shiftKey && e.buttons === 1 && CollectionDockingView.Instance) {
-            CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.props.DataDoc]);
+            CollectionDockingView.Instance.StartOtherDrag(e, [Doc.MakeAlias(this.props.Document)], [this.dataDoc]);
             e.stopPropagation();
         } else {
             if (this.active) e.stopPropagation(); // events stop at the lowest document that is active.  
@@ -393,7 +394,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         }
     }
     fullScreenClicked = (): void => {
-        CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeAlias(this.props.Document), this.props.DataDoc);
+        CollectionDockingView.Instance && CollectionDockingView.Instance.OpenFullScreen(Doc.MakeAlias(this.props.Document), this.dataDoc);
         SelectionManager.DeselectAll();
     }
 
@@ -476,10 +477,10 @@ 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.props.DataDoc, "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Tab Alias", event: () => this.props.addDocTab && this.props.addDocTab(Doc.MakeAlias(this.props.Document), this.props.DataDoc, "inTab"), icon: "folder" });
-        subitems.push({ description: "Open Right", event: () => this.props.addDocTab && this.props.addDocTab(this.props.Document, this.props.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.props.DataDoc, "onRight"), icon: "caret-square-right" });
+        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" });
         cm.addItem({ description: "Open...", subitems: subitems, icon: "external-link-alt" });
         cm.addItem({ description: BoolCast(this.props.Document.ignoreAspect, false) || !this.props.Document.nativeWidth || !this.props.Document.nativeHeight ? "Freeze" : "Unfreeze", event: this.freezeNativeDimensions, icon: "edit" });
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index 8a481144e..55f61ddff 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -30,7 +30,7 @@ export interface FieldViewProps {
     fieldExt: string;
     ContainingCollectionView: Opt<CollectionView | CollectionPDFView | CollectionVideoView>;
     Document: Doc;
-    DataDoc: Doc;
+    DataDoc?: Doc;
     isSelected: () => boolean;
     select: (isCtrlPressed: boolean) => void;
     renderDepth: number;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 0a1a86226..e1a496574 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -110,15 +110,15 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
         }
     }
 
-    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; }
+    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
 
     dispatchTransaction = (tx: Transaction) => {
         if (this._editorView) {
             const state = this._editorView.state.apply(tx);
             this._editorView.updateState(state);
             this._applyingChange = true;
-            Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey, new RichTextField(JSON.stringify(state.toJSON())));
-            Doc.SetOnPrototype(this.dataDoc, this.props.fieldKey + "_text", state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+            Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
+            Doc.GetProto(this.dataDoc)[this.props.fieldKey + "_text"] = state.doc.textBetween(0, state.doc.content.size, "\n\n");
             this._applyingChange = false;
             let title = StrCast(this.dataDoc.title);
             if (title && title.startsWith("-") && this._editorView) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 0da774db1..0e6c1ee19 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -46,7 +46,7 @@ export class ImageBox extends DocComponent<FieldViewProps, ImageDocument>(ImageD
     private dropDisposer?: DragManager.DragDropDisposer;
 
 
-    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; }
+    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
 
 
     protected createDropTarget = (ele: HTMLDivElement) => {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index f8bf5faf3..b863ee4fc 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -27,7 +27,7 @@ export class PDFBox extends DocComponent<FieldViewProps, PdfDocument>(PdfDocumen
 
     @observable private _alt = false;
     @observable private _scrollY: number = 0;
-    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) ? this.props.DataDoc : this.props.Document; }
+    @computed get dataDoc() { return BoolCast(this.props.Document.isTemplate) && this.props.DataDoc ? this.props.DataDoc : this.props.Document; }
 
     private _reactionDisposer?: IReactionDisposer;
 
diff --git a/src/new_fields/Doc.ts b/src/new_fields/Doc.ts
index 8825bc13a..ddbbc1436 100644
--- a/src/new_fields/Doc.ts
+++ b/src/new_fields/Doc.ts
@@ -178,7 +178,7 @@ export namespace Doc {
     export async function SetInPlace(doc: Doc, key: string, value: Field | undefined, defaultProto: boolean) {
         let hasProto = doc.proto instanceof Doc;
         let onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1;
-        let onProto = Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
+        let onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1;
         if (onDeleg || !hasProto || (!onProto && !defaultProto))
             doc[key] = value;
         else doc.proto![key] = value;
-- 
cgit v1.2.3-70-g09d2


From 6f0c33a231a0bb1eb641169a97c44712f896e1d6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Thu, 27 Jun 2019 08:06:18 -0400
Subject: fixed opacity for Docs?

---
 src/client/views/nodes/DocumentView.tsx | 23 +++++------------------
 1 file changed, 5 insertions(+), 18 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 9acfe6cff..4ac43ef4d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -125,8 +125,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
     private _mainCont = React.createRef<HTMLDivElement>();
     private _dropDisposer?: DragManager.DragDropDisposer;
 
-    @observable private _opacity: number = this.Document.opacity ? NumCast(this.Document.opacity) : 1;
-
     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; }
@@ -146,7 +144,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
 
     _animateToIconDisposer?: IReactionDisposer;
     _reactionDisposer?: IReactionDisposer;
-    _opacityDisposer?: IReactionDisposer;
     @action
     componentDidMount() {
         if (this._mainCont.current) {
@@ -171,12 +168,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             (values instanceof List) && this.animateBetweenIcon(values, values[2], values[3] ? true : false)
             , { fireImmediately: true });
         DocumentManager.Instance.DocumentViews.push(this);
-        this._opacityDisposer = reaction(
-            () => NumCast(this.props.Document.opacity),
-            () => {
-                runInAction(() => this._opacity = NumCast(this.props.Document.opacity));
-            }
-        );
     }
 
     animateBetweenIcon = (iconPos: number[], startTime: number, maximizing: boolean) => {
@@ -218,7 +209,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
         if (this._reactionDisposer) this._reactionDisposer();
         if (this._animateToIconDisposer) this._animateToIconDisposer();
         if (this._dropDisposer) this._dropDisposer();
-        if (this._opacityDisposer) this._opacityDisposer();
         DocumentManager.Instance.DocumentViews.splice(DocumentManager.Instance.DocumentViews.indexOf(this), 1);
     }
 
@@ -571,9 +561,7 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
             return null;
         }
         let backgroundColor = this.props.Document.layout instanceof Doc ? StrCast(this.props.Document.layout.backgroundColor) : this.Document.backgroundColor;
-        var scaling = this.props.ContentScaling();
         var nativeWidth = this.nativeWidth > 0 ? `${this.nativeWidth}px` : "100%";
-
         var nativeHeight = BoolCast(this.props.Document.ignoreAspect) ? this.props.PanelHeight() / this.props.ContentScaling() : this.nativeHeight > 0 ? `${this.nativeHeight}px` : "100%";
         return (
             <div className={`documentView-node${this.topMost ? "-topmost" : ""}`}
@@ -581,15 +569,14 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
                 style={{
                     outlineColor: "maroon",
                     outlineStyle: "dashed",
-                    outlineWidth: BoolCast(this.props.Document.libraryBrush, false) ||
-                        BoolCast(this.props.Document.protoBrush, false) ?
-                        `${1 * this.props.ScreenToLocalTransform().Scale}px`
-                        : "0px",
+                    outlineWidth: BoolCast(this.props.Document.libraryBrush) || BoolCast(this.props.Document.protoBrush) ?
+                        `${this.props.ScreenToLocalTransform().Scale}px` : "0px",
                     borderRadius: "inherit",
-                    background: backgroundColor || "",
+                    background: backgroundColor,
                     width: nativeWidth,
                     height: nativeHeight,
-                    transform: `scale(${scaling}, ${scaling})`,
+                    transform: `scale(${this.props.ContentScaling()})`,
+                    opacity: this.Document.opacity
                 }}
                 onDrop={this.onDrop} onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
                 onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}
-- 
cgit v1.2.3-70-g09d2

