From fe98c7d46df1852a74cd84dbe9ad010bfb3d5550 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 4 Nov 2022 12:43:59 -0400
Subject: more fixes to pdf and text to allow dashfieldview nodes to be link
 anchors and make sidebar annotations work better.

---
 src/client/views/nodes/LinkDocPreview.tsx | 2 ++
 1 file changed, 2 insertions(+)

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

diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 27e79a83b..0e66214d1 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -26,6 +26,7 @@ interface LinkDocPreviewProps {
     location: number[];
     hrefs?: string[];
     showHeader?: boolean;
+    noPreview?: boolean;
 }
 @observer
 export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@@ -111,6 +112,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                     this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                                 }
                                 this._toolTipText = '';
+                                if (LinkDocPreview.LinkInfo?.noPreview) this.followLink();
                             }
                         })
                     );
-- 
cgit v1.2.3-70-g09d2


From bbc993dd70d105c3de078208763e6415eedb1ff7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 11:18:46 -0500
Subject: partial fix for previewing multiple links to different sidebar notes.
  fixed opening sidebar to create an annotation the first time.   fixed
 honoring dashFieldView's non-editable flag.  moved isLinkButton setter to
 expert and make isLinkButton default for Buttons.  fixed following Links with
 Zoom to reset view.  fixed view for editing onClick for documents

---
 src/client/documents/Documents.ts                         |  2 +-
 src/client/util/CurrentUserUtils.ts                       |  2 +-
 src/client/util/LinkFollower.ts                           |  5 +++--
 src/client/views/DocumentButtonBar.tsx                    |  4 ++--
 src/client/views/DocumentDecorations.tsx                  |  2 +-
 src/client/views/nodes/DocumentView.tsx                   |  2 +-
 src/client/views/nodes/LinkDocPreview.tsx                 | 12 +++++++-----
 src/client/views/nodes/formattedText/DashFieldView.tsx    |  3 +++
 src/client/views/nodes/formattedText/FormattedTextBox.tsx | 13 ++++++++-----
 src/fields/Proxy.ts                                       |  2 +-
 10 files changed, 28 insertions(+), 19 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d789d4ee3..f1b8c3034 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1607,7 +1607,7 @@ export namespace DocUtils {
         const iconViews = DocListCast(Cast(Doc.UserDoc()['template-icons'], Doc, null)?.data);
         const templBtns = DocListCast(Cast(Doc.UserDoc()['template-buttons'], Doc, null)?.data);
         const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data);
-        const clickFuncs = DocListCast(Cast(Doc.UserDoc().clickFuncs, Doc, null)?.data);
+        const clickFuncs = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data);
         const allTemplates = iconViews
             .concat(templBtns)
             .concat(noteTypes)
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f2d851a40..d13209c4f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -267,7 +267,7 @@ export class CurrentUserUtils {
             {key: "Map",        creator: opts => Docs.Create.MapDocument([], opts),         opts: { _width: 800, _height: 600, _showSidebar: true,  }},
             {key: "Screengrab", creator: Docs.Create.ScreenshotDocument,                    opts: { _width: 400, _height: 200 }},
             {key: "WebCam",     creator: opts => Docs.Create.WebCamDocument("", opts),      opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }},
-            {key: "Button",     creator: Docs.Create.ButtonDocument,                        opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, }},
+            {key: "Button",     creator: Docs.Create.ButtonDocument,                        opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, _isLinkButton: true }},
             {key: "Script",     creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }},
             // {key: "DataViz",    creator: opts => Docs.Create.DataVizDocument(opts),         opts: { _width: 300, _height: 300 }},
             {key: "Header",     creator: headerTemplate,                                    opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true,}},
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index ea0531fa2..43b2caf88 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -48,9 +48,10 @@ export class LinkFollower {
                                 },
                             });
                         } else {
-                            res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
+                            finished?.();
+                            res(where !== 'inPlace' || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
                         }
-                    });
+                    }, 100);
                 });
 
             if (!sourceDoc.followLinkZoom) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index ce77b7446..3f12c7c9d 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -356,7 +356,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}>
                 <div
                     className="documentButtonBar-icon"
-                    style={{ backgroundColor: this._isRecording ? 'red' : targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
+                    style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }}
                     onClick={undoBatch(
                         action(e => {
                             this._isRecording = true;
@@ -463,7 +463,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         {this.metadataButton}
                     </div> */
                 }
-                {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
+                {Doc.noviceMode || !SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : (
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index cec03c991..3fac137fe 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -767,7 +767,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
 
         const useLock = bounds.r - bounds.x > 135 && seldocview.props.CollectionFreeFormDocumentView;
-        const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
+        const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
         const rotation = NumCast(seldocview.rootDoc._rotation);
 
         const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : '';
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4ee871ba2..973363f78 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -618,7 +618,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         clickFunc();
                     }, 350);
                 } else clickFunc();
-            } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+            } else if (this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 SelectionManager.DeselectAll();
                 this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
             } else {
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 0e66214d1..a47577701 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -41,6 +41,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
     _linkDocRef = React.createRef<HTMLDivElement>();
     @observable public static LinkInfo: Opt<LinkDocPreviewProps>;
     @observable _targetDoc: Opt<Doc>;
+    @observable _markerTargetDoc: Opt<Doc>;
     @observable _linkDoc: Opt<Doc>;
     @observable _linkSrc: Opt<Doc>;
     @observable _toolTipText = '';
@@ -57,9 +58,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         }
         if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) {
             // want to show annotation context document if annotation is not text
-            linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._targetDoc = anno)));
+            linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno)));
         } else {
-            this._targetDoc = linkTarget;
+            this._markerTargetDoc = this._targetDoc = linkTarget;
         }
         this._toolTipText = '';
     }
@@ -105,10 +106,11 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                     // automatic links specify the target in the link info, not the source
                                     const linkTarget = anchor;
                                     this._linkSrc = LinkManager.getOppositeAnchor(this._linkDoc, linkTarget);
-                                    this._targetDoc = linkTarget;
+                                    this._markerTargetDoc = this._targetDoc = linkTarget;
                                 } else {
                                     this._linkSrc = anchor;
                                     const linkTarget = LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
+                                    this._markerTargetDoc = linkTarget;
                                     this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                                 }
                                 this._toolTipText = '';
@@ -175,10 +177,10 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225));
     };
     @computed get previewHeader() {
-        return !this._linkDoc || !this._targetDoc || !this._linkSrc ? null : (
+        return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
             <div className="linkDocPreview-info">
                 <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}>
-                    {StrCast(this._targetDoc.title).length > 16 ? StrCast(this._targetDoc.title).substr(0, 16) + '...' : StrCast(this._targetDoc.title)}
+                    {StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)}
                     <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
                 </div>
                 <div className="linkDocPreview-buttonBar">
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 39b38add2..41b92a3c7 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -171,6 +171,9 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
             }
             e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
         }
+        if (!this.props.editable) {
+            e.preventDefault();
+        }
         e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fc8d2b7dd..2984feba5 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -231,7 +231,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
 
-    getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection');
+    getAnchor = () => this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', false);
 
     @action
     setupAnchorMenu = () => {
@@ -239,7 +239,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
         AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
             !this.layoutDoc.showSidebar && this.toggleSidebar();
-            this._sidebarRef.current?.anchorMenuClick(this.getAnchor());
+            setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, 'add:right', undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
         };
         AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
             !this.layoutDoc.showSidebar && this.toggleSidebar();
@@ -891,9 +891,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     // TODO: nda -- Look at how link anchors are added
-    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) {
+    makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean) {
         const state = this._editorView?.state;
         if (state) {
+            let selectedText = '';
             const sel = state.selection;
             const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
             let tr = state.tr.addMark(sel.from, sel.to, splitter);
@@ -905,13 +906,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                         const allAnchors = [{ href, title, anchorId: anchor[Id] }];
                         allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? []));
-                        const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location });
+                        const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location, noPreview });
                         tr = tr.addMark(pos, pos + node.nodeSize, link);
+                        selectedText += (node as Node).textContent;
                     }
                 });
                 this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents
                 this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter));
                 this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false;
+                anchor.text = selectedText;
                 return anchor;
             }
             return anchorDoc ?? this.rootDoc;
@@ -1471,7 +1474,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
-            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview);
+            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index e924ef7a3..72ae13035 100644
--- a/src/fields/Proxy.ts
+++ b/src/fields/Proxy.ts
@@ -66,7 +66,7 @@ export class ProxyField<T extends RefField> extends ObjectField {
 
         const cached = DocServer.GetCachedRefField(this.fieldId) as T;
         if (cached !== undefined) {
-            this.cache = cached;
+            setTimeout(action(() => (this.cache = cached)));
         } else if (!this.promise) {
             this.promise = DocServer.GetRefField(this.fieldId).then(
                 action((field: any) => {
-- 
cgit v1.2.3-70-g09d2


From b7d4f932d826d48aca4c7c058e05ceaea9c43057 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 17 Nov 2022 14:35:13 -0500
Subject: mostly changing strings to enums

---
 src/client/util/DictationManager.ts                |   6 +-
 src/client/util/LinkFollower.ts                    |   4 +-
 src/client/views/LightboxView.tsx                  |  16 +-
 src/client/views/MainView.tsx                      |  28 +-
 .../views/PropertiesDocBacklinksSelector.tsx       |  45 ++-
 src/client/views/PropertiesDocContextSelector.tsx  |   6 +-
 src/client/views/PropertiesView.tsx                |   4 +-
 .../views/collections/CollectionDockingView.tsx    |  26 +-
 .../views/collections/CollectionNoteTakingView.tsx |  12 +-
 .../views/collections/CollectionPileView.tsx       |  22 +-
 .../views/collections/CollectionStackingView.tsx   |   4 +-
 .../views/collections/CollectionTimeView.tsx       | 351 +++++++++-------
 src/client/views/collections/CollectionView.tsx    |  11 +-
 src/client/views/collections/TabDocView.tsx        |  34 +-
 src/client/views/collections/TreeView.tsx          |   8 +-
 .../CollectionFreeFormLayoutEngines.tsx            |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  23 +-
 .../CollectionMulticolumnView.tsx                  |   9 +-
 .../collectionSchema/CollectionSchemaCells.tsx     | 447 +++++++++++----------
 .../CollectionSchemaMovableRow.tsx                 |   3 +-
 .../collections/collectionSchema/SchemaTable.tsx   |   6 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   4 +-
 src/client/views/nodes/DocumentView.tsx            |  35 +-
 src/client/views/nodes/KeyValueBox.tsx             |   5 +-
 src/client/views/nodes/KeyValuePair.tsx            |  65 +--
 src/client/views/nodes/LinkAnchorBox.tsx           |   5 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   4 +-
 src/client/views/nodes/VideoBox.tsx                |   3 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   3 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   4 +-
 .../formattedText/ProsemirrorExampleTransfer.ts    |   3 +-
 src/client/views/nodes/trails/PresBox.tsx          |  13 +-
 src/mobile/MobileInterface.tsx                     |   2 +-
 34 files changed, 668 insertions(+), 550 deletions(-)

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

diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts
index 0a61f3478..203d4ad62 100644
--- a/src/client/util/DictationManager.ts
+++ b/src/client/util/DictationManager.ts
@@ -11,7 +11,7 @@ import { Utils } from '../../Utils';
 import { Docs } from '../documents/Documents';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DictationOverlay } from '../views/DictationOverlay';
-import { DocumentView } from '../views/nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
 import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
 
@@ -328,7 +328,7 @@ export namespace DictationManager {
                 {
                     action: (target: DocumentView) => {
                         const kvp = Docs.Create.KVPDocument(target.props.Document, { _width: 300, _height: 300 });
-                        target.props.addDocTab(kvp, 'add:right');
+                        target.props.addDocTab(kvp, OpenWhere.addRight);
                     },
                 },
             ],
@@ -345,7 +345,7 @@ export namespace DictationManager {
                         const proseMirrorState = `{"doc":{"type":"doc","content":[{"type":"ordered_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"type":"text","text":"${prompt}"}]}]}]}]},"selection":{"type":"text","anchor":${anchor},"head":${head}}}`;
                         proto.data = new RichTextField(proseMirrorState);
                         proto.backgroundColor = '#eeffff';
-                        target.props.addDocTab(newBox, 'add:right');
+                        target.props.addDocTab(newBox, OpenWhere.addRight);
                     },
                 },
             ],
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 68716a207..a3eb7ed7a 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -4,7 +4,7 @@ import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { UndoManager } from './UndoManager';
@@ -32,7 +32,7 @@ export class LinkFollower {
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
             const createTabForTarget = (didFocus: boolean) =>
                 new Promise<ViewAdjustment>(res => {
-                    const where = LightboxView.LightboxDoc ? 'inPlace' : StrCast(sourceDoc.followLinkLocation, followLoc);
+                    const where = LightboxView.LightboxDoc ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
                     docViewProps.addDocTab(doc, where);
                     setTimeout(() => {
                         const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 5660a34e9..1f58763d1 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -18,7 +18,7 @@ import { TabDocView } from './collections/TabDocView';
 import { GestureOverlay } from './GestureOverlay';
 import './LightboxView.scss';
 import { MainView } from './MainView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { DefaultStyleProvider, wavyBorderPath } from './StyleProvider';
 
 interface LightboxViewProps {
@@ -141,11 +141,13 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`);
         }
     }
-    public static AddDocTab = (doc: Doc, location: string, layoutTemplate?: Doc, openInTabFunc?: any) => {
-        const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
-        if (inPlaceView) {
-            inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
-            return true;
+    public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc, openInTabFunc?: any) => {
+        if (location !== OpenWhere.lightbox) {
+            const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
+            if (inPlaceView) {
+                inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
+                return true;
+            }
         }
         LightboxView.openInTabFunc = openInTabFunc;
         SelectionManager.DeselectAll();
@@ -360,7 +362,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     title={'open in tab'}
                     onClick={e => {
                         e.stopPropagation();
-                        CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, '');
+                        CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
                         //LightboxView.openInTabFunc(LightboxView._docTarget || LightboxView._doc!, "inPlace");
                         SelectionManager.DeselectAll();
                         LightboxView.SetLightboxDoc(undefined);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 392b4eeeb..c151aebcd 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -49,7 +49,7 @@ import { LinkMenu } from './linking/LinkMenu';
 import './MainView.scss';
 import { AudioBox } from './nodes/AudioBox';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { DashFieldViewMenu } from './nodes/formattedText/DashFieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from './nodes/formattedText/RichTextMenu';
@@ -538,7 +538,7 @@ export class MainView extends React.Component {
     @action
     createNewPresentation = () => {
         const pres = Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true);
-        CollectionDockingView.AddSplit(pres, 'right');
+        CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
         Doc.MyTrails && Doc.AddDocToList(Doc.MyTrails, 'data', pres); // Doc.MyTrails should be created in createDashboard
         Doc.ActivePresentation = pres;
     };
@@ -546,7 +546,7 @@ export class MainView extends React.Component {
     @action
     openPresentation = (pres: Doc) => {
         if (pres.type === DocumentType.PRES) {
-            CollectionDockingView.AddSplit(pres, 'right');
+            CollectionDockingView.AddSplit(pres, OpenWhereMod.right);
             Doc.MyTrails && (Doc.ActivePresentation = pres);
             Doc.AddDocToList(Doc.MyTrails, 'data', pres);
             this.closeFlyout();
@@ -683,20 +683,20 @@ export class MainView extends React.Component {
 
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
-    addDocTabFunc = (doc: Doc, location: string): boolean => {
-        const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
-        const locationParams = locationFields.length > 1 ? locationFields[1] : '';
+    addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => {
+        const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+        const whereMods = whereFields.length > 1 ? whereFields[1] : '';
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
         // prettier-ignore
-        switch (locationFields[0]) {
+        switch (whereFields[0]) {
             default:
-            case 'inPlace':
-            case 'lightbox':   return LightboxView.AddDocTab(doc, location);
-            case 'add':        return CollectionDockingView.AddSplit(doc, locationParams);
-            case 'dashboard':  return DashboardView.openDashboard(doc);
-            case 'close':      return CollectionDockingView.CloseSplit(doc, locationParams);
-            case 'fullScreen': return CollectionDockingView.OpenFullScreen(doc);
-            case 'toggle':     return CollectionDockingView.ToggleSplit(doc, locationParams);
+            case OpenWhere.inPlace:
+            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
+            case OpenWhere.add:        return CollectionDockingView.AddSplit(doc, whereMods as OpenWhereMod);
+            case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
+            case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
+            case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
+            case OpenWhere.toggle:     return CollectionDockingView.ToggleSplit(doc, whereMods as OpenWhereMod);
         }
     };
 
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 4ead8eaf0..25ac44078 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -1,20 +1,21 @@
-import { computed } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast } from "../../fields/Doc";
-import { Cast } from "../../fields/Types";
-import { emptyFunction } from "../../Utils";
-import { DocumentType } from "../documents/DocumentTypes";
-import { LinkManager } from "../util/LinkManager";
-import { SelectionManager } from "../util/SelectionManager";
-import { LinkMenu } from "./linking/LinkMenu";
+import { computed } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast } from '../../fields/Doc';
+import { Cast } from '../../fields/Types';
+import { emptyFunction } from '../../Utils';
+import { DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
+import { SelectionManager } from '../util/SelectionManager';
+import { LinkMenu } from './linking/LinkMenu';
+import { OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import './PropertiesDocBacklinksSelector.scss';
 
 type PropertiesDocBacklinksSelectorProps = {
-    Document: Doc,
-    Stack?: any,
-    hideTitle?: boolean,
-    addDocTab(doc: Doc, location: string): void
+    Document: Doc;
+    Stack?: any;
+    hideTitle?: boolean;
+    addDocTab(doc: Doc, location: OpenWhere): void;
 };
 
 @observer
@@ -40,14 +41,16 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
 
         if (otherdoc) {
             otherdoc.hidden = false;
-            this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, "toggle:right");
+            this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, (OpenWhere.toggle + ':' + OpenWhereMod.right) as OpenWhere);
         }
-    }
+    };
 
     render() {
-        return !SelectionManager.Views().length ? (null) : <div>
-            {this.props.hideTitle ? (null) : <p key="contexts">Contexts:</p>}
-            <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
-        </div>;
+        return !SelectionManager.Views().length ? null : (
+            <div>
+                {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>}
+                <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
+            </div>
+        );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx
index 9d89ee036..2c7da5931 100644
--- a/src/client/views/PropertiesDocContextSelector.tsx
+++ b/src/client/views/PropertiesDocContextSelector.tsx
@@ -7,14 +7,14 @@ import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { CollectionViewType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen } from '../util/DocumentManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import './PropertiesDocContextSelector.scss';
 
 type PropertiesDocContextSelectorProps = {
     DocView?: DocumentView;
     Stack?: any;
     hideTitle?: boolean;
-    addDocTab(doc: Doc, location: string): void;
+    addDocTab(doc: Doc, location: OpenWhere): void;
 };
 
 @observer
@@ -53,7 +53,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC
             col._panY = NumCast(target.y) + NumCast(target._height) / 2;
         }
         col.hidden = false;
-        this.props.addDocTab(col, 'toggle:right');
+        this.props.addDocTab(col, (OpenWhere.toggle + ':' + OpenWhereMod.right) as OpenWhere);
         setTimeout(() => DocFocusOrOpen(Doc.GetProto(this.props.DocView!.props.Document), col), 100);
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e5ff9e267..93a3fd253 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -24,7 +24,7 @@ import { Transform } from '../util/Transform';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { EditableView } from './EditableView';
 import { InkStrokeProperties } from './InkStrokeProperties';
-import { DocumentView, StyleProviderFunc } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
 import { FilterBox } from './nodes/FilterBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PresBox } from './nodes/trails';
@@ -42,7 +42,7 @@ interface PropertiesViewProps {
     width: number;
     height: number;
     styleProvider?: StyleProviderFunc;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
 }
 
 @observer
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 434466505..8cbe548c7 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -25,6 +25,7 @@ import { CollectionFreeFormView } from './collectionFreeForm';
 import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
 import { TabDocView } from './TabDocView';
 import React = require('react');
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 @observer
@@ -142,7 +143,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
     @undoBatch
     @action
-    public static ReplaceTab(document: Doc, panelName: string, stack: any, addToSplit?: boolean): boolean {
+    public static ReplaceTab(document: Doc, panelName: OpenWhereMod, stack: any, addToSplit?: boolean): boolean {
         const instance = CollectionDockingView.Instance;
         if (!instance) return false;
         const newConfig = CollectionDockingView.makeDocumentConfig(document, panelName);
@@ -164,7 +165,7 @@ export class CollectionDockingView extends CollectionSubView() {
     }
 
     @undoBatch
-    public static ToggleSplit(doc: Doc, location: string, stack?: any, panelName?: string) {
+    public static ToggleSplit(doc: Doc, location: OpenWhereMod, stack?: any, panelName?: string) {
         return CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap.keys()).findIndex(tab => tab.DashDoc === doc) !== -1
             ? CollectionDockingView.CloseSplit(doc)
             : CollectionDockingView.AddSplit(doc, location, stack, panelName);
@@ -175,7 +176,7 @@ export class CollectionDockingView extends CollectionSubView() {
     //
     @undoBatch
     @action
-    public static AddSplit(document: Doc, pullSide: string, stack?: any, panelName?: string) {
+    public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string) {
         if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document);
         if (!CollectionDockingView.Instance) return false;
         const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document);
@@ -208,14 +209,15 @@ export class CollectionDockingView extends CollectionSubView() {
                 // if row
                 switch (pullSide) {
                     default:
-                    case 'right':
+                    case OpenWhereMod.none:
+                    case OpenWhereMod.right:
                         glayRoot.contentItems[0].addChild(newContentItem());
                         break;
-                    case 'left':
+                    case OpenWhereMod.left:
                         glayRoot.contentItems[0].addChild(newContentItem(), 0);
                         break;
-                    case 'top':
-                    case 'bottom':
+                    case OpenWhereMod.top:
+                    case OpenWhereMod.bottom:
                         // if not going in a row layout, must add already existing content into column
                         const rowlayout = glayRoot.contentItems[0];
                         const newColumn = rowlayout.layoutManager.createContentItem({ type: 'column' }, instance._goldenLayout);
@@ -496,7 +498,7 @@ export class CollectionDockingView extends CollectionSubView() {
                     title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
                 });
                 this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
-                CollectionDockingView.AddSplit(docToAdd, '', stack);
+                CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
             }
         });
 
@@ -539,7 +541,7 @@ export class CollectionDockingView extends CollectionSubView() {
                             title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`,
                         });
                         this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd);
-                        CollectionDockingView.AddSplit(docToAdd, '', stack);
+                        CollectionDockingView.AddSplit(docToAdd, OpenWhereMod.none, stack);
                     }
                 })
             );
@@ -568,14 +570,14 @@ export class CollectionDockingView extends CollectionSubView() {
 
 ScriptingGlobals.add(
     function openInLightbox(doc: any) {
-        LightboxView.AddDocTab(doc, 'lightbox');
+        LightboxView.AddDocTab(doc, OpenWhere.lightbox);
     },
     'opens up document in a lightbox',
     '(doc: any)'
 );
 ScriptingGlobals.add(
     function openOnRight(doc: any) {
-        return CollectionDockingView.AddSplit(doc, 'right');
+        return CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
     },
     'opens up document in tab on right side of the screen',
     '(doc: any)'
@@ -588,5 +590,5 @@ ScriptingGlobals.add(
     '(doc: any)'
 );
 ScriptingGlobals.add(function useRightSplit(doc: any, shiftKey?: boolean) {
-    CollectionDockingView.ReplaceTab(doc, 'right', undefined, shiftKey);
+    CollectionDockingView.ReplaceTab(doc, OpenWhereMod.right, undefined, shiftKey);
 });
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index b0f64ed60..29670a1a7 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -19,7 +19,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { LightboxView } from '../LightboxView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
@@ -180,14 +180,6 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     }
 
-    addDocTab = (doc: Doc, where: string) => {
-        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
-            this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
-            return true;
-        }
-        return this.props.addDocTab(doc, where);
-    };
-
     scrollToBottom = () => {
         smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
     };
@@ -274,7 +266,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
                 removeDocument={this.props.removeDocument}
                 contentPointerEvents={StrCast(this.layoutDoc.contentPointerEvents)}
                 whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
-                addDocTab={this.addDocTab}
+                addDocTab={this.props.addDocTab}
                 bringToFront={returnFalse}
                 scriptContext={this.props.scriptContext}
                 pinToPres={this.props.pinToPres}
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index 38e240ac6..e95622630 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -12,6 +12,8 @@ import './CollectionPileView.scss';
 import { CollectionSubView } from './CollectionSubView';
 import React = require('react');
 import { ScriptField } from '../../../fields/ScriptField';
+import { OpenWhere } from '../nodes/DocumentView';
+import { computePassLayout, computeStarburstLayout } from './collectionFreeForm';
 
 @observer
 export class CollectionPileView extends CollectionSubView() {
@@ -19,8 +21,8 @@ export class CollectionPileView extends CollectionSubView() {
     _disposers: { [name: string]: IReactionDisposer } = {};
 
     componentDidMount() {
-        if (this.layoutEngine() !== 'pass' && this.layoutEngine() !== 'starburst') {
-            this.Document._pileLayoutEngine = 'pass';
+        if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) {
+            this.Document._pileLayoutEngine = computePassLayout.name;
         }
         this._originalChrome = this.layoutDoc._chromeHidden;
         this.layoutDoc._chromeHidden = true;
@@ -56,7 +58,7 @@ export class CollectionPileView extends CollectionSubView() {
 
     // returns the contents of the pileup in a CollectionFreeFormView
     @computed get contents() {
-        const isStarburst = this.layoutEngine() === 'starburst';
+        const isStarburst = this.layoutEngine() === computeStarburstLayout.name;
         return (
             <div className="collectionPileView-innards" style={{ pointerEvents: isStarburst || SnappingManager.GetIsDragging() ? undefined : 'none' }}>
                 <CollectionFreeFormView
@@ -74,7 +76,7 @@ export class CollectionPileView extends CollectionSubView() {
 
     // toggles the pileup between starburst to compact
     toggleStarburst = action(() => {
-        if (this.layoutEngine() === 'starburst') {
+        if (this.layoutEngine() === computeStarburstLayout.name) {
             const defaultSize = 110;
             this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2;
             this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2;
@@ -83,12 +85,12 @@ export class CollectionPileView extends CollectionSubView() {
             DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false);
             this.layoutDoc._panX = 0;
             this.layoutDoc._panY = -10;
-            this.props.Document._pileLayoutEngine = 'pass';
+            this.props.Document._pileLayoutEngine = computePassLayout.name;
         } else {
             const defaultSize = 25;
             !this.layoutDoc._starburstRadius && (this.layoutDoc._starburstRadius = 250);
             !this.layoutDoc._starburstDocScale && (this.layoutDoc._starburstDocScale = 2.5);
-            if (this.layoutEngine() === 'pass') {
+            if (this.layoutEngine() === computePassLayout.name) {
                 this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2;
                 this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2;
                 this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym]();
@@ -96,7 +98,7 @@ export class CollectionPileView extends CollectionSubView() {
             }
             this.layoutDoc._panX = this.layoutDoc._panY = 0;
             this.layoutDoc._width = this.layoutDoc._height = defaultSize;
-            this.props.Document._pileLayoutEngine = 'starburst';
+            this.props.Document._pileLayoutEngine = computeStarburstLayout.name;
         }
     });
 
@@ -118,7 +120,7 @@ export class CollectionPileView extends CollectionSubView() {
                         const doc = this.childDocs[0];
                         doc.x = e.clientX;
                         doc.y = e.clientY;
-                        this.props.addDocTab(doc, 'inParent') && (this.props.removeDocument?.(doc) || false);
+                        this.props.addDocTab(doc, OpenWhere.inParent) && (this.props.removeDocument?.(doc) || false);
                         dist = 0;
                     }
                 }
@@ -130,8 +132,8 @@ export class CollectionPileView extends CollectionSubView() {
                 SnappingManager.SetIsDragging(false);
             },
             emptyFunction,
-            e.shiftKey && this.layoutEngine() === 'pass',
-            this.layoutEngine() === 'pass' && e.shiftKey
+            e.shiftKey && this.layoutEngine() === computePassLayout.name,
+            this.layoutEngine() === computePassLayout.name && e.shiftKey
         ); // this sets _doubleTap
     };
 
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 175051d5c..aa4583af6 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -22,7 +22,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { EditableView } from '../EditableView';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
@@ -241,7 +241,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         return () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     }
 
-    addDocTab = (doc: Doc, where: string) => {
+    addDocTab = (doc: Doc, where: OpenWhere) => {
         if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
             this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
             return true;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 3dd9d2d84..ac896a8fd 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -1,32 +1,32 @@
-import { toUpper } from "lodash";
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { Doc, Opt, StrListCast } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { ObjectField } from "../../../fields/ObjectField";
-import { RichTextField } from "../../../fields/RichTextField";
-import { listSpec } from "../../../fields/Schema";
-import { ComputedField, ScriptField } from "../../../fields/ScriptField";
-import { Cast, NumCast, StrCast } from "../../../fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from "../../../Utils";
-import { Docs } from "../../documents/Documents";
-import { DocumentType } from "../../documents/DocumentTypes";
-import { DocumentManager } from "../../util/DocumentManager";
-import { ScriptingGlobals } from "../../util/ScriptingGlobals";
-import { ContextMenu } from "../ContextMenu";
-import { ContextMenuProps } from "../ContextMenuItem";
-import { EditableView } from "../EditableView";
-import { ViewSpecPrefix } from "../nodes/DocumentView";
-import { ViewDefBounds } from "./collectionFreeForm/CollectionFreeFormLayoutEngines";
-import { CollectionFreeFormView } from "./collectionFreeForm/CollectionFreeFormView";
-import { CollectionSubView } from "./CollectionSubView";
-import "./CollectionTimeView.scss";
-import React = require("react");
+import { toUpper } from 'lodash';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { ObjectField } from '../../../fields/ObjectField';
+import { RichTextField } from '../../../fields/RichTextField';
+import { listSpec } from '../../../fields/Schema';
+import { ComputedField, ScriptField } from '../../../fields/ScriptField';
+import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { emptyFunction, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
+import { ScriptingGlobals } from '../../util/ScriptingGlobals';
+import { ContextMenu } from '../ContextMenu';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { EditableView } from '../EditableView';
+import { ViewSpecPrefix } from '../nodes/DocumentView';
+import { computePivotLayout, computeTimelineLayout, ViewDefBounds } from './collectionFreeForm/CollectionFreeFormLayoutEngines';
+import { CollectionFreeFormView } from './collectionFreeForm/CollectionFreeFormView';
+import { CollectionSubView } from './CollectionSubView';
+import './CollectionTimeView.scss';
+import React = require('react');
 
 @observer
 export class CollectionTimeView extends CollectionSubView() {
     _changing = false;
-    @observable _layoutEngine = "pivot";
+    @observable _layoutEngine = computePivotLayout.name;
     @observable _collapsed: boolean = false;
     @observable _childClickedScript: Opt<ScriptField>;
     @observable _viewDefDivClick: Opt<ScriptField>;
@@ -35,7 +35,7 @@ export class CollectionTimeView extends CollectionSubView() {
     getAnchor = () => {
         const anchor = Docs.Create.HTMLAnchorDocument([], {
             title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any,
-            annotationOn: this.rootDoc
+            annotationOn: this.rootDoc,
         });
 
         // save view spec information for anchor
@@ -43,81 +43,103 @@ export class CollectionTimeView extends CollectionSubView() {
         proto.pivotField = this.pivotField;
         proto.docFilters = ObjectField.MakeCopy(this.layoutDoc._docFilters as ObjectField) || new List<string>([]);
         proto.docRangeFilters = ObjectField.MakeCopy(this.layoutDoc._docRangeFilters as ObjectField) || new List<string>([]);
-        proto[ViewSpecPrefix + "_viewType"] = this.layoutDoc._viewType;
+        proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
 
         // store anchor in annotations list of document (not technically needed since these anchors are never drawn)
-        if (Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), null) !== undefined) {
-            Cast(this.dataDoc[this.props.fieldKey + "-annotations"], listSpec(Doc), []).push(anchor);
+        if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) {
+            Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor);
         } else {
-            this.dataDoc[this.props.fieldKey + "-annotations"] = new List<Doc>([anchor]);
+            this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]);
         }
         return anchor;
-    }
+    };
 
     async componentDidMount() {
         this.props.setContentView?.(this);
         //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), "");
         ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; alias.removeDropProperties=new List<string>(['dropAction']); useRightSplit(alias, shiftKey); ";
         runInAction(() => {
-            this._childClickedScript = ScriptField.MakeScript("openInLightbox(self)", { this: Doc.name });
-            this._viewDefDivClick = ScriptField.MakeScript("pivotColumnClick(this,payload)", { payload: "any" });
+            this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
+            this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
         });
     }
 
-    get pivotField() { return this._focusPivotField || StrCast(this.layoutDoc._pivotField); }
+    get pivotField() {
+        return this._focusPivotField || StrCast(this.layoutDoc._pivotField);
+    }
     @action
     setViewSpec = (anchor: Doc, preview: boolean) => {
-        if (preview) {   // if in preview, then override document's fields with view spec
+        if (preview) {
+            // if in preview, then override document's fields with view spec
             this._focusFilters = StrListCast(Doc.GetProto(anchor).docFilters);
             this._focusRangeFilters = StrListCast(Doc.GetProto(anchor).docRangeFilters);
             this._focusPivotField = StrCast(anchor.pivotField);
-        } else if (anchor.pivotField !== undefined) {  // otherwise set document's fields based on anchor view spec
+        } else if (anchor.pivotField !== undefined) {
+            // otherwise set document's fields based on anchor view spec
             this.layoutDoc._prevFilterIndex = 1;
             this.layoutDoc._pivotField = StrCast(anchor.pivotField);
             this.layoutDoc._docFilters = new List<string>(StrListCast(anchor.docFilters));
             this.layoutDoc._docRangeFilters = new List<string>(StrListCast(anchor.docRangeFilters));
         }
         return 0;
-    }
+    };
 
     layoutEngine = () => this._layoutEngine;
-    toggleVisibility = action(() => this._collapsed = !this._collapsed);
+    toggleVisibility = action(() => (this._collapsed = !this._collapsed));
 
     onMinDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
-            const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
-            const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
-            this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            this.props.Document[this.props.fieldKey + "-timelineSpan"] = undefined;
-            return false;
-        }), returnFalse, emptyFunction);
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+                const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+                this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                this.props.Document[this.props.fieldKey + '-timelineSpan'] = undefined;
+                return false;
+            }),
+            returnFalse,
+            emptyFunction
+        );
+    };
 
     onMaxDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
-            const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
-            const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
-            this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq + (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            return false;
-        }), returnFalse, emptyFunction);
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+                const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+                this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq + ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                return false;
+            }),
+            returnFalse,
+            emptyFunction
+        );
+    };
 
     onMidDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => {
-            const minReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMinReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMin"], 0));
-            const maxReq = NumCast(this.props.Document[this.props.fieldKey + "-timelineMaxReq"], NumCast(this.props.Document[this.props.fieldKey + "-timelineMax"], 10));
-            this.props.Document[this.props.fieldKey + "-timelineMinReq"] = minReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            this.props.Document[this.props.fieldKey + "-timelineMaxReq"] = maxReq - (maxReq - minReq) * delta[0] / this.props.PanelWidth();
-            return false;
-        }), returnFalse, emptyFunction);
-    }
+        setupMoveUpEvents(
+            this,
+            e,
+            action((e: PointerEvent, down: number[], delta: number[]) => {
+                const minReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMinReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMin'], 0));
+                const maxReq = NumCast(this.props.Document[this.props.fieldKey + '-timelineMaxReq'], NumCast(this.props.Document[this.props.fieldKey + '-timelineMax'], 10));
+                this.props.Document[this.props.fieldKey + '-timelineMinReq'] = minReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                this.props.Document[this.props.fieldKey + '-timelineMaxReq'] = maxReq - ((maxReq - minReq) * delta[0]) / this.props.PanelWidth();
+                return false;
+            }),
+            returnFalse,
+            emptyFunction
+        );
+    };
 
     goTo = (prevFilterIndex: number) => {
-        this.layoutDoc._pivotField = this.layoutDoc["_prevPivotFields" + prevFilterIndex];
-        this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocFilter" + prevFilterIndex] as ObjectField);
-        this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc["_prevDocRangeFilters" + prevFilterIndex] as ObjectField);
+        this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex];
+        this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField);
+        this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField);
         this.layoutDoc._prevFilterIndex = prevFilterIndex;
-    }
+    };
 
     @action
     contentsDown = (e: React.MouseEvent) => {
@@ -127,37 +149,58 @@ export class CollectionTimeView extends CollectionSubView() {
         } else {
             this.layoutDoc._docFilters = new List([]);
         }
-    }
+    };
 
     dontScaleFilter = (doc: Doc) => doc.type === DocumentType.RTF;
     @computed get contents() {
-        return <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : "none" }}
-            onClick={this.contentsDown}>
-            <CollectionFreeFormView {...this.props}
-                engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
-                fitContentsToBox={returnTrue}
-                childClickScript={this._childClickedScript}
-                viewDefDivClick={this._viewDefDivClick}
-                //dontScaleFilter={this.dontScaleFilter}
-                layoutEngine={this.layoutEngine} />
-        </div>;
+        return (
+            <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}>
+                <CollectionFreeFormView
+                    {...this.props}
+                    engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }}
+                    fitContentsToBox={returnTrue}
+                    childClickScript={this._childClickedScript}
+                    viewDefDivClick={this._viewDefDivClick}
+                    //dontScaleFilter={this.dontScaleFilter}
+                    layoutEngine={this.layoutEngine}
+                />
+            </div>
+        );
     }
 
     public static SyncTimelineToPresentation(doc: Doc) {
         const fieldKey = Doc.LayoutFieldKey(doc);
-        doc[fieldKey + "-timelineCur"] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
+        doc[fieldKey + '-timelineCur'] = ComputedField.MakeFunction("(activePresentationItem()[this._pivotField || 'year'] || 0)");
     }
     specificMenu = (e: React.MouseEvent) => {
         const layoutItems: ContextMenuProps[] = [];
         const doc = this.layoutDoc;
 
-        layoutItems.push({ description: "Force Timeline", event: () => { doc._forceRenderEngine = "timeline"; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Force Pivot", event: () => { doc._forceRenderEngine = "pivot"; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Auto Time/Pivot layout", event: () => { doc._forceRenderEngine = undefined; }, icon: "compress-arrows-alt" });
-        layoutItems.push({ description: "Sync with presentation", event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: "compress-arrows-alt" });
+        layoutItems.push({
+            description: 'Force Timeline',
+            event: () => {
+                doc._forceRenderEngine = computeTimelineLayout.name;
+            },
+            icon: 'compress-arrows-alt',
+        });
+        layoutItems.push({
+            description: 'Force Pivot',
+            event: () => {
+                doc._forceRenderEngine = computePivotLayout.name;
+            },
+            icon: 'compress-arrows-alt',
+        });
+        layoutItems.push({
+            description: 'Auto Time/Pivot layout',
+            event: () => {
+                doc._forceRenderEngine = undefined;
+            },
+            icon: 'compress-arrows-alt',
+        });
+        layoutItems.push({ description: 'Sync with presentation', event: () => CollectionTimeView.SyncTimelineToPresentation(doc), icon: 'compress-arrows-alt' });
 
-        ContextMenu.Instance.addItem({ description: "Options...", subitems: layoutItems, icon: "eye" });
-    }
+        ContextMenu.Instance.addItem({ description: 'Options...', subitems: layoutItems, icon: 'eye' });
+    };
     @computed get _allFacets() {
         const facets = new Set<string>();
         this.childDocs.forEach(child => Object.keys(Doc.GetProto(child)).forEach(key => facets.add(key)));
@@ -169,37 +212,40 @@ export class CollectionTimeView extends CollectionSubView() {
         const docItems: ContextMenuProps[] = [];
         const keySet: Set<string> = new Set();
 
-        this.childLayoutPairs.map(pair => this._allFacets.filter(fieldKey =>
-            pair.layout[fieldKey] instanceof RichTextField ||
-            typeof (pair.layout[fieldKey]) === "number" ||
-            typeof (pair.layout[fieldKey]) === "boolean" ||
-            typeof (pair.layout[fieldKey]) === "string").filter(fieldKey => fieldKey[0] !== "_" && (fieldKey[0] !== "#" || fieldKey === "#") && (fieldKey === "tags" || fieldKey[0] === toUpper(fieldKey)[0])).map(fieldKey => keySet.add(fieldKey)));
-        Array.from(keySet).map(fieldKey =>
-            docItems.push({ description: ":" + fieldKey, event: () => this.layoutDoc._pivotField = fieldKey, icon: "compress-arrows-alt" }));
-        docItems.push({ description: ":default", event: () => this.layoutDoc._pivotField = undefined, icon: "compress-arrows-alt" });
-        ContextMenu.Instance.addItem({ description: "Pivot Fields ...", subitems: docItems, icon: "eye" });
+        this.childLayoutPairs.map(pair =>
+            this._allFacets
+                .filter(fieldKey => pair.layout[fieldKey] instanceof RichTextField || typeof pair.layout[fieldKey] === 'number' || typeof pair.layout[fieldKey] === 'boolean' || typeof pair.layout[fieldKey] === 'string')
+                .filter(fieldKey => fieldKey[0] !== '_' && (fieldKey[0] !== '#' || fieldKey === '#') && (fieldKey === 'tags' || fieldKey[0] === toUpper(fieldKey)[0]))
+                .map(fieldKey => keySet.add(fieldKey))
+        );
+        Array.from(keySet).map(fieldKey => docItems.push({ description: ':' + fieldKey, event: () => (this.layoutDoc._pivotField = fieldKey), icon: 'compress-arrows-alt' }));
+        docItems.push({ description: ':default', event: () => (this.layoutDoc._pivotField = undefined), icon: 'compress-arrows-alt' });
+        ContextMenu.Instance.addItem({ description: 'Pivot Fields ...', subitems: docItems, icon: 'eye' });
         const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(x, y);
-        ContextMenu.Instance.displayMenu(x, y, ":");
-    }
+        ContextMenu.Instance.displayMenu(x, y, ':');
+    };
 
     @computed get pivotKeyUI() {
-        return <div className={"pivotKeyEntry"}>
-            <EditableView
-                GetValue={returnEmptyString}
-                SetValue={(value: any) => {
-                    if (value?.length) {
-                        this.layoutDoc._pivotField = value;
-                        return true;
-                    }
-                    return false;
-                }}
-                toggle={this.toggleVisibility}
-                background={"#f1efeb"} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
-                contents={":" + StrCast(this.layoutDoc._pivotField)}
-                showMenuOnLoad={true}
-                display={"inline"}
-                menuCallback={this.menuCallback} />
-        </div>;
+        return (
+            <div className={'pivotKeyEntry'}>
+                <EditableView
+                    GetValue={returnEmptyString}
+                    SetValue={(value: any) => {
+                        if (value?.length) {
+                            this.layoutDoc._pivotField = value;
+                            return true;
+                        }
+                        return false;
+                    }}
+                    toggle={this.toggleVisibility}
+                    background={'#f1efeb'} // this.props.headingObject ? this.props.headingObject.color : "#f1efeb";
+                    contents={':' + StrCast(this.layoutDoc._pivotField)}
+                    showMenuOnLoad={true}
+                    display={'inline'}
+                    menuCallback={this.menuCallback}
+                />
+            </div>
+        );
     }
 
     render() {
@@ -211,55 +257,62 @@ export class CollectionTimeView extends CollectionSubView() {
             }
         });
         const forceLayout = StrCast(this.layoutDoc._forceRenderEngine);
-        const doTimeline = forceLayout ? (forceLayout === "timeline") : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
-        if (doTimeline !== (this._layoutEngine === "timeline")) {
+        const doTimeline = forceLayout ? forceLayout === computeTimelineLayout.name : nonNumbers / this.childDocs.length < 0.1 && this.props.PanelWidth() / this.props.PanelHeight() > 6;
+        if (doTimeline !== (this._layoutEngine === computeTimelineLayout.name)) {
             if (!this._changing) {
                 this._changing = true;
-                setTimeout(action(() => {
-                    this._layoutEngine = doTimeline ? "timeline" : "pivot";
-                    this._changing = false;
-                }), 0);
+                setTimeout(
+                    action(() => {
+                        this._layoutEngine = doTimeline ? computeTimelineLayout.name : computePivotLayout.name;
+                        this._changing = false;
+                    }),
+                    0
+                );
             }
         }
 
-        return <div className={"collectionTimeView" + (doTimeline ? "" : "-pivot")} onContextMenu={this.specificMenu}
-            style={{ width: this.props.PanelWidth(), height: "100%" }}>
-            {this.pivotKeyUI}
-            {this.contents}
-            {!this.props.isSelected() || !doTimeline ? (null) : <>
-                <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
-                <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
-                <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
-            </>}
-        </div>;
+        return (
+            <div className={'collectionTimeView' + (doTimeline ? '' : '-pivot')} onContextMenu={this.specificMenu} style={{ width: this.props.PanelWidth(), height: '100%' }}>
+                {this.pivotKeyUI}
+                {this.contents}
+                {!this.props.isSelected() || !doTimeline ? null : (
+                    <>
+                        <div className="collectionTimeView-thumb-min collectionTimeView-thumb" key="min" onPointerDown={this.onMinDown} />
+                        <div className="collectionTimeView-thumb-max collectionTimeView-thumb" key="mid" onPointerDown={this.onMaxDown} />
+                        <div className="collectionTimeView-thumb-mid collectionTimeView-thumb" key="max" onPointerDown={this.onMidDown} />
+                    </>
+                )}
+            </div>
+        );
     }
 }
 
 ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) {
-    const pivotField = StrCast(pivotDoc._pivotField) || "author";
+    const pivotField = StrCast(pivotDoc._pivotField) || 'author';
     let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex);
     const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField));
-    pivotDoc["_prevDocFilter" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
-    pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
-    pivotDoc["_prevPivotFields" + prevFilterIndex] = pivotField;
+    pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField);
+    pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField);
+    pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField;
     pivotDoc._prevFilterIndex = ++prevFilterIndex;
     pivotDoc._docFilters = new List();
-    setTimeout(action(() => {
-        const filterVals = (bounds.payload as string[]);
-        filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, "check"));
-        const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
-        if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
-            if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
-                pivotDoc._pivotField = filterVals[0];
+    setTimeout(
+        action(() => {
+            const filterVals = bounds.payload as string[];
+            filterVals.map(filterVal => Doc.setDocFilter(pivotDoc, pivotField, filterVal, 'check'));
+            const pivotView = DocumentManager.Instance.getDocumentView(pivotDoc);
+            if (pivotDoc && pivotView?.ComponentView instanceof CollectionTimeView && filterVals.length === 1) {
+                if (pivotView?.ComponentView.childDocs.length && pivotView.ComponentView.childDocs[0][filterVals[0]]) {
+                    pivotDoc._pivotField = filterVals[0];
+                }
             }
-        }
-        const newFilters = StrListCast(pivotDoc._docFilters);
-        if (newFilters.length && originalFilter.length &&
-            newFilters.lastElement() === originalFilter.lastElement()) {
-            pivotDoc._prevFilterIndex = --prevFilterIndex;
-            pivotDoc["_prevDocFilter" + prevFilterIndex] = undefined;
-            pivotDoc["_prevDocRangeFilters" + prevFilterIndex] = undefined;
-            pivotDoc["_prevPivotFields" + prevFilterIndex] = undefined;
-        }
-    }));
-});
\ No newline at end of file
+            const newFilters = StrListCast(pivotDoc._docFilters);
+            if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) {
+                pivotDoc._prevFilterIndex = --prevFilterIndex;
+                pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined;
+                pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = undefined;
+                pivotDoc['_prevPivotFields' + prevFilterIndex] = undefined;
+            }
+        })
+    );
+});
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 625d4e9e5..917d7618c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -16,6 +16,7 @@ import { InteractionUtils } from '../../util/InteractionUtils';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
+import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
 import { CollectionCarousel3DView } from './CollectionCarousel3DView';
 import { CollectionCarouselView } from './CollectionCarouselView';
@@ -174,7 +175,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
                 vtype => {
                     const newRendition = Doc.MakeAlias(this.rootDoc);
                     newRendition._viewType = vtype;
-                    this.props.addDocTab(newRendition, 'add:right');
+                    this.props.addDocTab(newRendition, OpenWhere.addRight);
                     return newRendition;
                 },
                 false
@@ -184,17 +185,17 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             const optionItems = options && 'subitems' in options ? options.subitems : [];
             !Doc.noviceMode ? optionItems.splice(0, 0, { description: `${this.rootDoc.forceActive ? 'Select' : 'Force'} Contents Active`, event: () => (this.rootDoc.forceActive = !this.rootDoc.forceActive), icon: 'project-diagram' }) : null;
             if (this.rootDoc.childLayout instanceof Doc) {
-                optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, 'add:right'), icon: 'project-diagram' });
+                optionItems.push({ description: 'View Child Layout', event: () => this.props.addDocTab(this.rootDoc.childLayout as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
             if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) {
-                optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, 'add:right'), icon: 'project-diagram' });
+                optionItems.push({ description: 'View Child Detailed Layout', event: () => this.props.addDocTab(this.rootDoc.childClickedOpenTemplateView as Doc, OpenWhere.addRight), icon: 'project-diagram' });
             }
             !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc.isInPlaceContainer ? 'Unset' : 'Set'} inPlace Container`, event: () => (this.rootDoc.isInPlaceContainer = !this.rootDoc.isInPlaceContainer), icon: 'project-diagram' });
 
             if (!Doc.noviceMode && false) {
                 optionItems.push({
                     description: 'Create Branch',
-                    event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), 'add:right'),
+                    event: async () => this.props.addDocTab(await BranchCreate(this.rootDoc), OpenWhere.addRight),
                     icon: 'project-diagram',
                 });
                 optionItems.push({
@@ -225,7 +226,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
                         event: (obj: any) => {
                             const alias = Doc.MakeAlias(this.rootDoc);
                             DocUtils.makeCustomViewClicked(alias, undefined, func.key);
-                            this.props.addDocTab(alias, 'add:right');
+                            this.props.addDocTab(alias, OpenWhere.addRight);
                         },
                     })
                 );
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e21649648..2cc588b78 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -26,7 +26,7 @@ import { DashboardView } from '../DashboardView';
 import { Colors, Shadows } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { MainView } from '../MainView';
-import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, OpenWhereMod } from '../nodes/DocumentView';
 import { DashFieldView } from '../nodes/formattedText/DashFieldView';
 import { PinProps, PresBox, PresMovement } from '../nodes/trails';
 import { DefaultStyleProvider, StyleProp } from '../StyleProvider';
@@ -296,7 +296,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         ) {
             const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []);
             if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1);
-            CollectionDockingView.AddSplit(curPres, 'right');
+            CollectionDockingView.AddSplit(curPres, OpenWhereMod.right);
             setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
         }
         setTimeout(batch.end, 500); // need to wait until dockingview (goldenlayout) updates all its structurs
@@ -343,34 +343,30 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     //                                  "replace:right" - will replace the stack on the right named "right" if it exists, or create a stack on the right with that name,
     //                                   "replace:monkeys" - will replace any tab that has the label 'monkeys', or a tab with that label will be created by default on the right
     //  inPlace - will add the document to any collection along the path from the document to the docking view that has a field isInPlaceContainer. if none is found, inPlace adds a tab to current stack
-    addDocTab = (doc: Doc, location: string) => {
+    addDocTab = (doc: Doc, location: OpenWhere) => {
         SelectionManager.DeselectAll();
-        const locationFields = doc._viewType === CollectionViewType.Docking ? ['dashboard'] : location.split(':');
-        const locationParams = locationFields.length > 1 ? locationFields[1] : '';
+        const locationFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
+        const locationParams: OpenWhereMod = locationFields.length > 1 ? (locationFields[1] as OpenWhereMod) : OpenWhereMod.none;
         switch (locationFields[0]) {
-            case 'dashboard':
+            case OpenWhere.dashboard:
                 return DashboardView.openDashboard(doc);
-            case 'close':
+            case OpenWhere.close:
                 return CollectionDockingView.CloseSplit(doc, locationParams);
-            case 'fullScreen':
+            case OpenWhere.fullScreen:
                 return CollectionDockingView.OpenFullScreen(doc);
-            case 'replace':
+            case OpenWhere.replace:
                 return CollectionDockingView.ReplaceTab(doc, locationParams, this.stack);
-            // case "lightbox": {
-            //     // TabDocView.PinDoc(doc, { hidePresBox: true });
-            //     return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
-            // }
-            case 'inPlace':
+            case OpenWhere.inPlace:
                 const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
                 if (inPlaceView) {
                     inPlaceView.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
                     return true;
-                }
-            case 'lightbox':
+                } // fall through to lightbox
+            case OpenWhere.lightbox:
                 return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
-            case 'toggle':
+            case OpenWhere.toggle:
                 return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack);
-            case 'add':
+            case OpenWhere.add:
             default:
                 return CollectionDockingView.AddSplit(doc, locationParams, this.stack);
         }
@@ -509,7 +505,7 @@ interface TabMinimapViewProps {
     document: Doc;
     hideMinimap: () => boolean;
     tabView: () => DocumentView | undefined;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
     PanelWidth: () => number;
     PanelHeight: () => number;
     background: () => string;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 13cf64558..bd326f917 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -22,7 +22,7 @@ import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { EditableView } from '../EditableView';
 import { TREE_BULLET_WIDTH } from '../global/globalCssVariables.scss';
-import { DocumentView, DocumentViewInternal, DocumentViewProps, StyleProviderFunc } from '../nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, DocumentViewProps, OpenWhere, StyleProviderFunc } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
@@ -44,7 +44,7 @@ export interface TreeViewProps {
     containerCollection: Doc;
     renderDepth: number;
     dropAction: dropActionType;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
     panelWidth: () => number;
     panelHeight: () => number;
     addDocument: (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => boolean;
@@ -236,7 +236,7 @@ export class TreeView extends React.Component<TreeViewProps> {
             const bestAlias =
                 docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsPrototype(docView.props.Document) ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail);
             const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail);
-            this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), 'lightbox');
+            this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), OpenWhere.lightbox);
         }
     };
 
@@ -1109,7 +1109,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         remove: undefined | ((doc: Doc | Doc[]) => boolean),
         move: DragManager.MoveFunction,
         dropAction: dropActionType,
-        addDocTab: (doc: Doc, where: string) => boolean,
+        addDocTab: (doc: Doc, where: OpenWhere) => boolean,
         styleProvider: undefined | StyleProviderFunc,
         screenToLocalXf: () => Transform,
         isContentActive: (outsideReaction?: boolean) => boolean,
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index 54be6ba0f..7dd9cdb8b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -82,7 +82,7 @@ interface PivotColumn {
     filters: string[];
 }
 
-export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const docMap = new Map<string, PoolData>();
     childPairs.forEach(({ layout, data }, i) => {
         docMap.set(layout[Id], {
@@ -97,7 +97,7 @@ export function computerPassLayout(poolData: Map<string, PoolData>, pivotDoc: Do
     return normalizeResults(panelDim, 12, docMap, poolData, viewDefsToJSX, [], 0, []);
 }
 
-export function computerStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
+export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) {
     const mustFit = pivotDoc[WidthSym]() !== panelDim[0]; // if a panel size is set that's not the same as the pivot doc's size, then assume this is in a panel for a content fitting view (like a grid) in which case everything must be scaled to stay within the panel
     const docMap = new Map<string, PoolData>();
     const docSize = mustFit ? panelDim[0] * 0.33 : 75; // assume an icon sized at 75
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 57cccec4a..8cabf060d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -37,7 +37,7 @@ import { GestureOverlay } from '../../GestureOverlay';
 import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment, ViewSpecPrefix } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PresBox } from '../../nodes/trails/PresBox';
@@ -47,7 +47,7 @@ import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { TabDocView } from '../TabDocView';
-import { computePivotLayout, computerPassLayout, computerStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePivotLayout, computePassLayout as computePassLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
@@ -1272,7 +1272,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
     pointerEvents = () => {
         const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
-        const pointerEvents = this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === 'pass' && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+        const pointerEvents =
+            this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
         return pointerEvents;
     };
     getChildDocView(entry: PoolData) {
@@ -1328,8 +1329,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             />
         );
     }
-    addDocTab = action((doc: Doc, where: string) => {
-        if (where === 'inParent') {
+    addDocTab = action((doc: Doc, where: OpenWhere) => {
+        if (where === OpenWhere.inParent) {
             (doc instanceof Doc ? [doc] : doc).forEach(doc => {
                 const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y));
                 doc.x = pt[0];
@@ -1337,7 +1338,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             });
             return this.props.addDocument?.(doc) || false;
         }
-        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
+        if (where === OpenWhere.inPlace && this.layoutDoc.isInPlaceContainer) {
             this.dataDoc[this.props.fieldKey] = doc instanceof Doc ? doc : new List<Doc>(doc as any as Doc[]);
             return true;
         }
@@ -1457,10 +1458,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newPool = new Map<string, PoolData>();
         // prettier-ignore
         switch (this.layoutEngine) {
-            case 'pass':      return { newPool, computedElementData: this.doEngineLayout(newPool, computerPassLayout) };
-            case 'timeline':  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
-            case 'pivot':     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
-            case 'starburst': return { newPool, computedElementData: this.doEngineLayout(newPool, computerStarburstLayout) };
+            case computePassLayout.name :      return { newPool, computedElementData: this.doEngineLayout(newPool, computePassLayout) };
+            case computeTimelineLayout.name:  return { newPool, computedElementData: this.doEngineLayout(newPool, computeTimelineLayout) };
+            case computePivotLayout.name:     return { newPool, computedElementData: this.doEngineLayout(newPool, computePivotLayout) };
+            case computeStarburstLayout.name: return { newPool, computedElementData: this.doEngineLayout(newPool, computeStarburstLayout) };
         }
         return { newPool, computedElementData: this.doFreeformLayout(newPool) };
     }
@@ -1724,7 +1725,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             doc.x = scr?.[0];
             doc.y = scr?.[1];
         });
-        this.props.addDocTab(childDocs as any as Doc, 'inParent');
+        this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParent);
         this.props.ContainingCollectionView?.removeDocument(this.props.Document);
     };
 
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index e5a2d9007..b88da5191 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -234,13 +234,6 @@ export class CollectionMulticolumnView extends CollectionSubView() {
     onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
 
-    addDocTab = (doc: Doc, where: string) => {
-        if (where === 'inPlace' && this.layoutDoc.isInPlaceContainer) {
-            this.dataDoc[this.props.fieldKey] = new List<Doc>([doc]);
-            return true;
-        }
-        return this.props.addDocTab(doc, where);
-    };
     focusDocument = (doc: Doc, options: DocFocusOptions) => this.props.focus(this.rootDoc, options);
     isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive();
     isChildContentActive = () => (((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.isContentActive() ? true : false);
@@ -278,7 +271,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
                 moveDocument={this.props.moveDocument}
                 removeDocument={this.props.removeDocument}
                 whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
-                addDocTab={this.addDocTab}
+                addDocTab={this.props.addDocTab}
                 pinToPres={this.props.pinToPres}
                 bringToFront={returnFalse}
             />
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index adcd9e1e3..ef75fb159 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -1,35 +1,36 @@
-import React = require("react");
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { action, computed, observable } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import DatePicker from "react-datepicker";
-import { CellInfo } from "react-table";
-import { DateField } from "../../../../fields/DateField";
-import { Doc, DocListCast, Field, Opt } from "../../../../fields/Doc";
-import { Id } from "../../../../fields/FieldSymbols";
-import { List } from "../../../../fields/List";
-import { SchemaHeaderField } from "../../../../fields/SchemaHeaderField";
-import { ComputedField } from "../../../../fields/ScriptField";
-import { BoolCast, Cast, DateCast, FieldValue, StrCast } from "../../../../fields/Types";
-import { ImageField } from "../../../../fields/URLField";
-import { emptyFunction, Utils } from "../../../../Utils";
-import { Docs } from "../../../documents/Documents";
-import { DocumentType } from "../../../documents/DocumentTypes";
-import { DocumentManager } from "../../../util/DocumentManager";
-import { DragManager } from "../../../util/DragManager";
-import { KeyCodes } from "../../../util/KeyCodes";
-import { CompileScript } from "../../../util/Scripting";
-import { SearchUtil } from "../../../util/SearchUtil";
-import { SnappingManager } from "../../../util/SnappingManager";
-import { undoBatch } from "../../../util/UndoManager";
+import React = require('react');
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, computed, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import DatePicker from 'react-datepicker';
+import { CellInfo } from 'react-table';
+import { DateField } from '../../../../fields/DateField';
+import { Doc, DocListCast, Field, Opt } from '../../../../fields/Doc';
+import { Id } from '../../../../fields/FieldSymbols';
+import { List } from '../../../../fields/List';
+import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField';
+import { ComputedField } from '../../../../fields/ScriptField';
+import { BoolCast, Cast, DateCast, FieldValue, StrCast } from '../../../../fields/Types';
+import { ImageField } from '../../../../fields/URLField';
+import { emptyFunction, Utils } from '../../../../Utils';
+import { Docs } from '../../../documents/Documents';
+import { DocumentType } from '../../../documents/DocumentTypes';
+import { DocumentManager } from '../../../util/DocumentManager';
+import { DragManager } from '../../../util/DragManager';
+import { KeyCodes } from '../../../util/KeyCodes';
+import { CompileScript } from '../../../util/Scripting';
+import { SearchUtil } from '../../../util/SearchUtil';
+import { SnappingManager } from '../../../util/SnappingManager';
+import { undoBatch } from '../../../util/UndoManager';
 import '../../../views/DocumentDecorations.scss';
-import { EditableView } from "../../EditableView";
+import { EditableView } from '../../EditableView';
 import { MAX_ROW_HEIGHT } from '../../global/globalCssVariables.scss';
-import { DocumentIconContainer } from "../../nodes/DocumentIcon";
-import { OverlayView } from "../../OverlayView";
-import { CollectionView } from "../CollectionView";
-import "./CollectionSchemaView.scss";
+import { DocumentIconContainer } from '../../nodes/DocumentIcon';
+import { OverlayView } from '../../OverlayView';
+import { CollectionView } from '../CollectionView';
+import './CollectionSchemaView.scss';
+import { OpenWhere } from '../../nodes/DocumentView';
 
 // intialize cell properties
 export interface CellProps {
@@ -46,10 +47,9 @@ export interface CellProps {
     // currently unused
     renderDepth: number;
     // called when a button is pressed on the node itself
-    addDocTab: (document: Doc, where: string) => boolean;
+    addDocTab: (document: Doc, where: OpenWhere) => boolean;
     pinToPres: (document: Doc) => void;
-    moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined,
-        addDocument: (document: Doc | Doc[]) => boolean) => boolean;
+    moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
     isFocused: boolean;
     changeFocusedCellByIndex: (row: number, col: number) => void;
     // set whether the cell is in the isEditing mode
@@ -67,7 +67,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
     // return a field key that is corrected for whether it COMMENT
     public static resolvedFieldKey(column: string, rowDoc: Doc) {
         const fieldKey = column;
-        if (fieldKey.startsWith("*")) {
+        if (fieldKey.startsWith('*')) {
             const rootKey = fieldKey.substring(1);
             const allKeys = [...Array.from(Object.keys(rowDoc)), ...Array.from(Object.keys(Doc.GetProto(rowDoc)))];
             const matchedKeys = allKeys.filter(key => key.includes(rootKey));
@@ -82,33 +82,37 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
     protected _rowDataDoc = Doc.GetProto(this.props.rowProps.original);
     // methods for dragging and dropping
     protected _dropDisposer?: DragManager.DragDropDisposer;
-    @observable contents: string = "";
+    @observable contents: string = '';
 
-    componentDidMount() { document.addEventListener("keydown", this.onKeyDown); }
-    componentWillUnmount() { document.removeEventListener("keydown", this.onKeyDown); }
+    componentDidMount() {
+        document.addEventListener('keydown', this.onKeyDown);
+    }
+    componentWillUnmount() {
+        document.removeEventListener('keydown', this.onKeyDown);
+    }
 
     @action
     onKeyDown = (e: KeyboardEvent): void => {
         // If a cell is editable and clicked, hitting enter shoudl allow the user to edit it
         if (this.props.isFocused && this.props.isEditable && e.keyCode === KeyCodes.ENTER) {
-            document.removeEventListener("keydown", this.onKeyDown);
+            document.removeEventListener('keydown', this.onKeyDown);
             this._isEditing = true;
             this.props.setIsEditing(true);
         }
-    }
+    };
 
     @action
     isEditingCallback = (isEditing: boolean): void => {
         // a general method that takes a boolean that determines whether the cell should be in
         // is-editing mode
         // remove the event listener if it's there
-        document.removeEventListener("keydown", this.onKeyDown);
+        document.removeEventListener('keydown', this.onKeyDown);
         // it's not already in is-editing mode, re-add the event listener
-        isEditing && document.addEventListener("keydown", this.onKeyDown);
+        isEditing && document.addEventListener('keydown', this.onKeyDown);
         this._isEditing = isEditing;
         this.props.setIsEditing(isEditing);
         this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
-    }
+    };
 
     @action
     onPointerDown = async (e: React.PointerEvent): Promise<void> => {
@@ -119,19 +123,19 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         this.props.setPreviewDoc(this.props.rowProps.original);
 
         let url: string;
-        if (url = StrCast(this.props.rowProps.row.href)) {
+        if ((url = StrCast(this.props.rowProps.row.href))) {
             // opens up the the doc in a new window, blurring the old one
             try {
                 new URL(url);
                 const temp = window.open(url)!;
                 temp.blur();
                 window.focus();
-            } catch { }
+            } catch {}
         }
 
         const doc = Cast(this._rowDoc[this.renderFieldKey], Doc, null);
         doc && this.props.setPreviewDoc(doc);
-    }
+    };
 
     @undoBatch
     applyToDoc = (doc: Doc, row: number, col: number, run: (args?: { [name: string]: any }) => any) => {
@@ -142,7 +146,7 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
         doc[this.renderFieldKey] = res.result;
         return true;
         // return whether the change was successful
-    }
+    };
 
     private drop = (e: Event, de: DragManager.DropEvent) => {
         // if the drag has data at its completion
@@ -151,41 +155,51 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             if (de.complete.docDragData.draggedDocuments.length === 1) {
                 // update the renderFieldKey
                 this._rowDataDoc[this.renderFieldKey] = de.complete.docDragData.draggedDocuments[0];
-            }
-            else {
+            } else {
                 // create schema document reflecting the new column arrangement
-                const coll = Docs.Create.SchemaDocument([new SchemaHeaderField("title", "#f1efeb")], de.complete.docDragData.draggedDocuments, {});
+                const coll = Docs.Create.SchemaDocument([new SchemaHeaderField('title', '#f1efeb')], de.complete.docDragData.draggedDocuments, {});
                 this._rowDataDoc[this.renderFieldKey] = coll;
             }
             e.stopPropagation();
         }
-    }
+    };
 
     protected dropRef = (ele: HTMLElement | null) => {
         // if the drop disposer is not undefined, run its function
         this._dropDisposer?.();
         // if ele is not null, give ele a non-undefined drop disposer
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this)));
-    }
+    };
 
     returnHighlights(contents: string, positions?: number[]) {
         if (positions) {
             const results = [];
             StrCast(this.props.Document._searchString);
             const length = StrCast(this.props.Document._searchString).length;
-            const color = contents ? "black" : "grey";
+            const color = contents ? 'black' : 'grey';
 
-            results.push(<span key="-1" style={{ color }}>{contents?.slice(0, positions[0])}</span>);
+            results.push(
+                <span key="-1" style={{ color }}>
+                    {contents?.slice(0, positions[0])}
+                </span>
+            );
             positions.forEach((num, cur) => {
-                results.push(<span key={"start" + cur} style={{ backgroundColor: "#FFFF00", color }}>{contents?.slice(num, num + length)}</span>);
+                results.push(
+                    <span key={'start' + cur} style={{ backgroundColor: '#FFFF00', color }}>
+                        {contents?.slice(num, num + length)}
+                    </span>
+                );
                 let end = 0;
-                cur === positions.length - 1 ? end = contents.length : end = positions[cur + 1];
-                results.push(<span key={"end" + cur} style={{ color }}>{contents?.slice(num + length, end)}</span>);
-            }
-            );
+                cur === positions.length - 1 ? (end = contents.length) : (end = positions[cur + 1]);
+                results.push(
+                    <span key={'end' + cur} style={{ color }}>
+                        {contents?.slice(num + length, end)}
+                    </span>
+                );
+            });
             return results;
         }
-        return <span style={{ color: contents ? "black" : "grey" }}>{contents ? contents?.valueOf() : "undefined"}</span>;
+        return <span style={{ color: contents ? 'black' : 'grey' }}>{contents ? contents?.valueOf() : 'undefined'}</span>;
     }
 
     @computed get renderFieldKey() {
@@ -199,10 +213,9 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
             const aliasdoc = await SearchUtil.GetAliasesOfDocument(this._rowDataDoc);
             const targetContext = aliasdoc.length <= 0 ? undefined : Cast(aliasdoc[0].context, Doc, null);
             // Jump to the this document
-            DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [],
-                undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
+            DocumentManager.Instance.jumpToDocument(this._rowDoc, false, emptyFunction, targetContext ? [targetContext] : [], undefined, undefined, undefined, () => this.props.setPreviewDoc(this._rowDoc));
         }
-    }
+    };
 
     renderCellWithType(type: string | undefined) {
         const dragRef: React.RefObject<HTMLDivElement> = React.createRef();
@@ -214,29 +227,29 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
 
         const onPointerEnter = (e: React.PointerEvent): void => {
             // e.buttons === 1 means the left moue pointer is down
-            if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === "document" || type === undefined)) {
-                dragRef.current!.className = "collectionSchemaView-cellContainer doc-drag-over";
+            if (e.buttons === 1 && SnappingManager.GetIsDragging() && (type === 'document' || type === undefined)) {
+                dragRef.current!.className = 'collectionSchemaView-cellContainer doc-drag-over';
             }
         };
         const onPointerLeave = (e: React.PointerEvent): void => {
             // change the class name to indicate that the cell is no longer being dragged
-            dragRef.current!.className = "collectionSchemaView-cellContainer";
+            dragRef.current!.className = 'collectionSchemaView-cellContainer';
         };
 
         let contents = Field.toString(field as Field);
         // display 2 hyphens instead of a blank box for empty cells
-        contents = contents === "" ? "--" : contents;
+        contents = contents === '' ? '--' : contents;
 
         // classname reflects the tatus of the cell
-        let className = "collectionSchemaView-cellWrapper";
-        if (this._isEditing) className += " editing";
-        if (this.props.isFocused && this.props.isEditable) className += " focused";
-        if (this.props.isFocused && !this.props.isEditable) className += " inactive";
+        let className = 'collectionSchemaView-cellWrapper';
+        if (this._isEditing) className += ' editing';
+        if (this.props.isFocused && this.props.isEditable) className += ' focused';
+        if (this.props.isFocused && !this.props.isEditable) className += ' inactive';
 
         const positions = [];
-        if (StrCast(this.props.Document._searchString).toLowerCase() !== "") {
+        if (StrCast(this.props.Document._searchString).toLowerCase() !== '') {
             // term is ...promise pending... if the field is a Promise, otherwise it is the cell's contents
-            let term = (field instanceof Promise) ? "...promise pending..." : contents.toLowerCase();
+            let term = field instanceof Promise ? '...promise pending...' : contents.toLowerCase();
             const search = StrCast(this.props.Document._searchString).toLowerCase();
             let start = term.indexOf(search);
             let tally = 0;
@@ -256,56 +269,60 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                 positions.pop();
             }
         }
-        const placeholder = type === "number" ? "0" : contents === "" ? "--" : "undefined";
+        const placeholder = type === 'number' ? '0' : contents === '' ? '--' : 'undefined';
         return (
-            <div className="collectionSchemaView-cellContainer" style={{ cursor: field instanceof Doc ? "grab" : "auto" }}
-                ref={dragRef} onPointerDown={this.onPointerDown} onClick={action(e => this._isEditing = true)} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave}>
+            <div
+                className="collectionSchemaView-cellContainer"
+                style={{ cursor: field instanceof Doc ? 'grab' : 'auto' }}
+                ref={dragRef}
+                onPointerDown={this.onPointerDown}
+                onClick={action(e => (this._isEditing = true))}
+                onPointerEnter={onPointerEnter}
+                onPointerLeave={onPointerLeave}>
                 <div className={className} ref={this._focusRef} tabIndex={-1}>
-                    <div className="collectionSchemaView-cellContents" ref={type === undefined || type === "document" ? this.dropRef : null}>
-                        {!this.props.Document._searchDoc ?
+                    <div className="collectionSchemaView-cellContents" ref={type === undefined || type === 'document' ? this.dropRef : null}>
+                        {!this.props.Document._searchDoc ? (
                             <EditableView
                                 editing={this._isEditing}
                                 isEditingCallback={this.isEditingCallback}
-                                display={"inline"}
+                                display={'inline'}
                                 contents={contents}
-                                height={"auto"}
+                                height={'auto'}
                                 maxHeight={Number(MAX_ROW_HEIGHT)}
                                 placeholder={placeholder}
                                 GetValue={() => {
                                     const cfield = ComputedField.WithoutComputed(() => FieldValue(field));
                                     const cscript = cfield instanceof ComputedField ? cfield.script.originalScript : undefined;
-                                    const cfinalScript = cscript?.split("return")[cscript.split("return").length - 1];
-                                    return cscript ? (cfinalScript?.endsWith(";") ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) :
-                                        Field.IsField(cfield) ? Field.toScriptString(cfield) : "";
+                                    const cfinalScript = cscript?.split('return')[cscript.split('return').length - 1];
+                                    return cscript ? (cfinalScript?.endsWith(';') ? `:=${cfinalScript?.substring(0, cfinalScript.length - 2)}` : cfinalScript) : Field.IsField(cfield) ? Field.toScriptString(cfield) : '';
                                 }}
                                 SetValue={action((value: string) => {
                                     // sets what is displayed after the user makes an input
                                     let retVal = false;
-                                    if (value.startsWith(":=") || value.startsWith("=:=")) {
+                                    if (value.startsWith(':=') || value.startsWith('=:=')) {
                                         // decides how to compute a value when given either of the above strings
-                                        const script = value.substring(value.startsWith("=:=") ? 3 : 2);
-                                        retVal = this.props.setComputed(script, value.startsWith(":=") ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
+                                        const script = value.substring(value.startsWith('=:=') ? 3 : 2);
+                                        retVal = this.props.setComputed(script, value.startsWith(':=') ? this._rowDataDoc : this._rowDoc, this.renderFieldKey, this.props.row, this.props.col);
                                     } else {
                                         // check if the input is a number
                                         let inputIsNum = true;
                                         for (const s of value) {
-                                            if (isNaN(parseInt(s)) && !(s === ".") && !(s === ",")) {
+                                            if (isNaN(parseInt(s)) && !(s === '.') && !(s === ',')) {
                                                 inputIsNum = false;
                                             }
                                         }
                                         // check if the input is a boolean
-                                        const inputIsBool: boolean = value === "false" || value === "true";
-                                        // what to do in the case 
-                                        if (!inputIsNum && !inputIsBool && !value.startsWith("=")) {
+                                        const inputIsBool: boolean = value === 'false' || value === 'true';
+                                        // what to do in the case
+                                        if (!inputIsNum && !inputIsBool && !value.startsWith('=')) {
                                             // if it's not a number, it's a string, and should be processed as such
-                                            // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically 
+                                            // strips the string of quotes when it is edited to prevent quotes form being added to the text automatically
                                             // after each edit
                                             let valueSansQuotes = value;
                                             if (this._isEditing) {
                                                 const vsqLength = valueSansQuotes.length;
                                                 // get rid of outer quotes
-                                                valueSansQuotes = valueSansQuotes.substring(value.startsWith("\"") ? 1 : 0,
-                                                    valueSansQuotes.charAt(vsqLength - 1) === "\"" ? vsqLength - 1 : vsqLength);
+                                                valueSansQuotes = valueSansQuotes.substring(value.startsWith('"') ? 1 : 0, valueSansQuotes.charAt(vsqLength - 1) === '"' ? vsqLength - 1 : vsqLength);
                                             }
                                             let inputAsString = '"';
                                             // escape any quotes in the string
@@ -319,27 +336,27 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                                             // add a closing quote
                                             inputAsString += '"';
                                             //two options here: we can strip off outer quotes or we can figure out what's going on with the script
-                                            const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                            const script = CompileScript(inputAsString, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
                                             const changeMade = inputAsString.length !== value.length || inputAsString.length - 2 !== value.length;
                                             // change it if a change is made, otherwise, just compile using the old cell conetnts
                                             script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
                                             // handle numbers and expressions
-                                        } else if (inputIsNum || value.startsWith("=")) {
+                                        } else if (inputIsNum || value.startsWith('=')) {
                                             //TODO: make accept numbers
-                                            const inputscript = value.substring(value.startsWith("=") ? 1 : 0);
+                                            const inputscript = value.substring(value.startsWith('=') ? 1 : 0);
                                             // if commas are not stripped, the parser only considers the numbers after the last comma
-                                            let inputSansCommas = "";
+                                            let inputSansCommas = '';
                                             for (const s of inputscript) {
-                                                if (!(s === ",")) {
+                                                if (!(s === ',')) {
                                                     inputSansCommas += s;
                                                 }
                                             }
-                                            const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                            const script = CompileScript(inputSansCommas, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
                                             const changeMade = value.length - 2 !== value.length;
                                             script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
                                             // handle booleans
                                         } else if (inputIsBool) {
-                                            const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
+                                            const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
                                             const changeMade = value.length - 2 !== value.length;
                                             script.compiled && (retVal = this.applyToDoc(changeMade ? this._rowDoc : this._rowDataDoc, this.props.row, this.props.col, script.run));
                                         }
@@ -352,33 +369,47 @@ export class CollectionSchemaCell extends React.Component<CellProps> {
                                 })}
                                 OnFillDown={async (value: string) => {
                                     // computes all of the value preceded by :=
-                                    const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: "number", $c: "number", $: "any" } });
-                                    script.compiled && DocListCast(this.props.Document[this.props.fieldKey]).
-                                        forEach((doc, i) => value.startsWith(":=") ?
-                                            this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) :
-                                            this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run));
+                                    const script = CompileScript(value, { requiredType: type, typecheck: false, editable: true, addReturn: true, params: { this: Doc.name, $r: 'number', $c: 'number', $: 'any' } });
+                                    script.compiled &&
+                                        DocListCast(this.props.Document[this.props.fieldKey]).forEach((doc, i) =>
+                                            value.startsWith(':=') ? this.props.setComputed(value.substring(2), Doc.GetProto(doc), this.renderFieldKey, i, this.props.col) : this.applyToDoc(Doc.GetProto(doc), i, this.props.col, script.run)
+                                        );
                                 }}
                             />
-                            :
+                        ) : (
                             this.returnHighlights(contents, positions)
-                        }
-                    </div >
+                        )}
+                    </div>
                 </div>
             </div>
         );
     }
 
-    render() { return this.renderCellWithType(undefined); }
+    render() {
+        return this.renderCellWithType(undefined);
+    }
 }
 
 @observer
-export class CollectionSchemaNumberCell extends CollectionSchemaCell { render() { return this.renderCellWithType("number"); } }
+export class CollectionSchemaNumberCell extends CollectionSchemaCell {
+    render() {
+        return this.renderCellWithType('number');
+    }
+}
 
 @observer
-export class CollectionSchemaBooleanCell extends CollectionSchemaCell { render() { return this.renderCellWithType("boolean"); } }
+export class CollectionSchemaBooleanCell extends CollectionSchemaCell {
+    render() {
+        return this.renderCellWithType('boolean');
+    }
+}
 
 @observer
-export class CollectionSchemaStringCell extends CollectionSchemaCell { render() { return this.renderCellWithType("string"); } }
+export class CollectionSchemaStringCell extends CollectionSchemaCell {
+    render() {
+        return this.renderCellWithType('string');
+    }
+}
 
 @observer
 export class CollectionSchemaDateCell extends CollectionSchemaCell {
@@ -396,24 +427,24 @@ export class CollectionSchemaDateCell extends CollectionSchemaCell {
         // ^ DateCast is always undefined for some reason, but that is what the field should be set to
         this._rowDoc[this.renderFieldKey] = new DateField(date as Date);
         //}
-    }
+    };
 
     render() {
-        return !this.props.isFocused ? <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : "--"}</span> :
-            <DatePicker
-                selected={this._date?.date || new Date}
-                onSelect={date => this.handleChange(date)}
-                onChange={date => this.handleChange(date)}
-            />;
+        return !this.props.isFocused ? (
+            <span onPointerDown={this.onPointerDown}>{this._date ? Field.toString(this._date as Field) : '--'}</span>
+        ) : (
+            <DatePicker selected={this._date?.date || new Date()} onSelect={date => this.handleChange(date)} onChange={date => this.handleChange(date)} />
+        );
     }
 }
 
 @observer
 export class CollectionSchemaDocCell extends CollectionSchemaCell {
-
     _overlayDisposer?: () => void;
 
-    @computed get _doc() { return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc)); }
+    @computed get _doc() {
+        return FieldValue(Cast(this._rowDoc[this.renderFieldKey], Doc));
+    }
 
     @action
     onSetValue = (value: string) => {
@@ -422,7 +453,7 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
         const script = CompileScript(value, {
             addReturn: true,
             typecheck: true,
-            transformer: DocumentIconContainer.getTransformer()
+            transformer: DocumentIconContainer.getTransformer(),
         });
         // compile the script
         const results = script.compiled && script.run();
@@ -432,44 +463,43 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
             return true;
         }
         return false;
-    }
+    };
 
-    componentWillUnmount() { this.onBlur(); }
+    componentWillUnmount() {
+        this.onBlur();
+    }
 
-    onBlur = () => { this._overlayDisposer?.(); };
+    onBlur = () => {
+        this._overlayDisposer?.();
+    };
     onFocus = () => {
         this.onBlur();
         this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
-    }
+    };
 
     @action
     isEditingCallback = (isEditing: boolean): void => {
         // the isEditingCallback from a general CollectionSchemaCell
-        document.removeEventListener("keydown", this.onKeyDown);
-        isEditing && document.addEventListener("keydown", this.onKeyDown);
+        document.removeEventListener('keydown', this.onKeyDown);
+        isEditing && document.addEventListener('keydown', this.onKeyDown);
         this._isEditing = isEditing;
         this.props.setIsEditing(isEditing);
         this.props.changeFocusedCellByIndex(this.props.row, this.props.col);
-    }
+    };
 
     render() {
         // if there's a doc, render it
-        return !this._doc ? this.renderCellWithType("document") :
-            <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1}
-                onPointerDown={this.onPointerDown}
-            >
-                <div className="collectionSchemaView-cellContents-document"
-                    style={{ padding: "5.9px" }}
-                    ref={this.dropRef}
-                    onFocus={this.onFocus}
-                    onBlur={this.onBlur}
-                >
+        return !this._doc ? (
+            this.renderCellWithType('document')
+        ) : (
+            <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+                <div className="collectionSchemaView-cellContents-document" style={{ padding: '5.9px' }} ref={this.dropRef} onFocus={this.onFocus} onBlur={this.onBlur}>
                     <EditableView
                         editing={this._isEditing}
                         isEditingCallback={this.isEditingCallback}
-                        display={"inline"}
-                        contents={this._doc.title || "--"}
-                        height={"auto"}
+                        display={'inline'}
+                        contents={this._doc.title || '--'}
+                        height={'auto'}
                         maxHeight={Number(MAX_ROW_HEIGHT)}
                         GetValue={() => StrCast(this._doc?.title)}
                         SetValue={action((value: string) => {
@@ -477,33 +507,36 @@ export class CollectionSchemaDocCell extends CollectionSchemaCell {
                             return true;
                         })}
                     />
-                </div >
-                <div onClick={() => this._doc && this.props.addDocTab(this._doc, "add:right")} className="collectionSchemaView-cellContents-docButton">
+                </div>
+                <div onClick={() => this._doc && this.props.addDocTab(this._doc, OpenWhere.addRight)} className="collectionSchemaView-cellContents-docButton">
                     <FontAwesomeIcon icon="external-link-alt" size="lg" />
                 </div>
-            </div>;
+            </div>
+        );
     }
 }
 
 @observer
 export class CollectionSchemaImageCell extends CollectionSchemaCell {
-
     choosePath(url: URL) {
-        if (url.protocol === "data") return url.href; // if the url ises the data protocol, just return the href
+        if (url.protocol === 'data') return url.href; // if the url ises the data protocol, just return the href
         if (url.href.indexOf(window.location.origin) === -1) return Utils.CorsProxy(url.href); // otherwise, put it through the cors proxy erver
-        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href;//Why is this here — good question
+        if (!/\.(png|jpg|jpeg|gif|webp)$/.test(url.href.toLowerCase())) return url.href; //Why is this here — good question
 
         const ext = extname(url.href);
-        return url.href.replace(ext, "_o" + ext);
+        return url.href.replace(ext, '_o' + ext);
     }
 
     render() {
         const field = Cast(this._rowDoc[this.renderFieldKey], ImageField, null); // retrieve the primary image URL that is being rendered from the data doc
-        const alts = DocListCast(this._rowDoc[this.renderFieldKey + "-alternates"]); // retrieve alternate documents that may be rendered as alternate images
-        const altpaths = alts.map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url).filter(url => url).map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
+        const alts = DocListCast(this._rowDoc[this.renderFieldKey + '-alternates']); // retrieve alternate documents that may be rendered as alternate images
+        const altpaths = alts
+            .map(doc => Cast(doc[Doc.LayoutFieldKey(doc)], ImageField, null)?.url)
+            .filter(url => url)
+            .map(url => this.choosePath(url)); // access the primary layout data of the alternate documents
         const paths = field ? [this.choosePath(field.url), ...altpaths] : altpaths;
         // If there is a path, follow it; otherwise, follow a link to a default image icon
-        const url = paths.length ? paths : [Utils.CorsProxy("http://www.cs.brown.edu/~bcz/noImage.png")];
+        const url = paths.length ? paths : [Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')];
 
         const aspect = Doc.NativeAspect(this._rowDoc); // aspect ratio
         let width = Math.min(75, this.props.rowProps.width); // get a with that is no smaller than 75px
@@ -511,25 +544,28 @@ export class CollectionSchemaImageCell extends CollectionSchemaCell {
         width = height * aspect; // increase the width of the image if necessary to maintain proportionality
 
         const reference = React.createRef<HTMLDivElement>();
-        return <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
-            <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
-                <img src={url[0]}
-                    width={paths.length ? width : "20px"}
-                    height={paths.length ? height : "20px"} />
-            </div >
-        </div>;
+        return (
+            <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
+                <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
+                    <img src={url[0]} width={paths.length ? width : '20px'} height={paths.length ? height : '20px'} />
+                </div>
+            </div>
+        );
     }
 }
 
-
 @observer
 export class CollectionSchemaListCell extends CollectionSchemaCell {
     _overlayDisposer?: () => void;
 
-    @computed get _field() { return this._rowDoc[this.renderFieldKey]; }
-    @computed get _optionsList() { return this._field as List<any>; }
+    @computed get _field() {
+        return this._rowDoc[this.renderFieldKey];
+    }
+    @computed get _optionsList() {
+        return this._field as List<any>;
+    }
     @observable private _opened = false; // whether the list is opened
-    @observable private _text = "select an item";
+    @observable private _text = 'select an item';
     @observable private _selectedNum = 0; // the index of the list item selected
 
     @action
@@ -538,102 +574,109 @@ export class CollectionSchemaListCell extends CollectionSchemaCell {
         this._optionsList[this._selectedNum] = this._text = value;
 
         (this._field as List<any>).splice(this._selectedNum, 1, value);
-    }
+    };
 
     @action
     onSelected = (element: string, index: number) => {
         // if an item is selected, the private variables should update to reflect this
         this._text = element;
         this._selectedNum = index;
-    }
+    };
 
     onFocus = () => {
         this._overlayDisposer?.();
         this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 });
-    }
+    };
 
     render() {
         const link = false;
         const reference = React.createRef<HTMLDivElement>();
 
-        // if the list is not opened, don't display it; otherwise, do. 
+        // if the list is not opened, don't display it; otherwise, do.
         if (this._optionsList?.length) {
-            const options = !this._opened ? (null) :
+            const options = !this._opened ? null : (
                 <div>
                     {this._optionsList.map((element, index) => {
                         const val = Field.toString(element);
-                        return <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: "6px" }} onPointerDown={(e) => this.onSelected(StrCast(element), index)} >
-                            {val}
-                        </div>;
+                        return (
+                            <div className="collectionSchemaView-dropdownOption" key={index} style={{ padding: '6px' }} onPointerDown={e => this.onSelected(StrCast(element), index)}>
+                                {val}
+                            </div>
+                        );
                     })}
-                </div>;
-
-            const plainText = <div style={{ padding: "5.9px" }}>{this._text}</div>;
-            const textarea = <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: "5.9px" }} ref={this.dropRef} >
-                <EditableView
-                    editing={this._isEditing}
-                    isEditingCallback={this.isEditingCallback}
-                    display={"inline"}
-                    contents={this._text}
-                    height={"auto"}
-                    maxHeight={Number(MAX_ROW_HEIGHT)}
-                    GetValue={() => this._text}
-                    SetValue={action((value: string) => {
-                        // add special for params 
-                        this.onSetValue(value);
-                        return true;
-                    })}
-                />
-            </div >;
+                </div>
+            );
+
+            const plainText = <div style={{ padding: '5.9px' }}>{this._text}</div>;
+            const textarea = (
+                <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} style={{ padding: '5.9px' }} ref={this.dropRef}>
+                    <EditableView
+                        editing={this._isEditing}
+                        isEditingCallback={this.isEditingCallback}
+                        display={'inline'}
+                        contents={this._text}
+                        height={'auto'}
+                        maxHeight={Number(MAX_ROW_HEIGHT)}
+                        GetValue={() => this._text}
+                        SetValue={action((value: string) => {
+                            // add special for params
+                            this.onSetValue(value);
+                            return true;
+                        })}
+                    />
+                </div>
+            );
 
             //☰
             return (
                 <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
                     <div className="collectionSchemaView-cellContents" key={this._rowDoc[Id]} ref={reference}>
                         <div className="collectionSchemaView-dropDownWrapper">
-                            <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: "length", position: "relative" }}
-                                onClick={action(e => this._opened = !this._opened)} >
-                                <FontAwesomeIcon icon={this._opened ? "caret-up" : "caret-down"} size="sm" />
+                            <button type="button" className="collectionSchemaView-dropdownButton" style={{ right: 'length', position: 'relative' }} onClick={action(e => (this._opened = !this._opened))}>
+                                <FontAwesomeIcon icon={this._opened ? 'caret-up' : 'caret-down'} size="sm" />
                             </button>
                             <div className="collectionSchemaView-dropdownText"> {link ? plainText : textarea} </div>
                         </div>
                         {options}
-                    </div >
+                    </div>
                 </div>
             );
         }
-        return this.renderCellWithType("list");
+        return this.renderCellWithType('list');
     }
 }
 
-
 @observer
 export class CollectionSchemaCheckboxCell extends CollectionSchemaCell {
-    @computed get _isChecked() { return BoolCast(this._rowDoc[this.renderFieldKey]); }
+    @computed get _isChecked() {
+        return BoolCast(this._rowDoc[this.renderFieldKey]);
+    }
 
     render() {
         const reference = React.createRef<HTMLDivElement>();
         return (
             <div className="collectionSchemaView-cellWrapper" ref={this._focusRef} tabIndex={-1} onPointerDown={this.onPointerDown}>
-                <input type="checkbox" checked={this._isChecked} onChange={e => this._rowDoc[this.renderFieldKey] = e.target.checked} />
+                <input type="checkbox" checked={this._isChecked} onChange={e => (this._rowDoc[this.renderFieldKey] = e.target.checked)} />
             </div>
         );
     }
 }
 
-
 @observer
 export class CollectionSchemaButtons extends CollectionSchemaCell {
     // the navigation buttons for schema view when it is used for search.
     render() {
-        return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? <></> :
-            <div style={{ paddingTop: 8, paddingLeft: 3 }} >
+        return !this.props.Document._searchDoc || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this._rowDoc.type) as DocumentType) ? (
+            <></>
+        ) : (
+            <div style={{ paddingTop: 8, paddingLeft: 3 }}>
                 <button style={{ padding: 2, left: 77 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, true)}>
                     <FontAwesomeIcon icon="arrow-up" size="sm" />
                 </button>
-                <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)} >
+                <button style={{ padding: 2 }} onClick={() => Doc.SearchMatchNext(this._rowDoc, false)}>
                     <FontAwesomeIcon icon="arrow-down" size="sm" />
                 </button>
-            </div>;
+            </div>
+        );
     }
 }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
index f872637e5..3cb2df7d3 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaMovableRow.tsx
@@ -10,6 +10,7 @@ import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
+import { OpenWhere } from '../../nodes/DocumentView';
 import './CollectionSchemaView.scss';
 
 export interface MovableRowProps {
@@ -138,7 +139,7 @@ export class MovableRow extends React.Component<React.PropsWithChildren<MovableR
                             <div className="row-option" style={{ cursor: 'grab' }} ref={reference} onPointerDown={onItemDown}>
                                 <FontAwesomeIcon icon="grip-vertical" size="sm" />
                             </div>
-                            <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, 'add:right')}>
+                            <div className="row-option" onClick={() => this.props.addDocTab(this.props.rowInfo.original, OpenWhere.addRight)}>
                                 <FontAwesomeIcon icon="external-link-alt" size="sm" />
                             </div>
                         </div>
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index fafea5ce3..45ad4f86b 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -23,7 +23,7 @@ import { undoBatch } from '../../../util/UndoManager';
 import '../../../views/DocumentDecorations.scss';
 import { ContextMenu } from '../../ContextMenu';
 import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
-import { DocumentView } from '../../nodes/DocumentView';
+import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { DefaultStyleProvider } from '../../StyleProvider';
 import { CollectionView } from '../CollectionView';
 import {
@@ -86,7 +86,7 @@ export interface SchemaTableProps {
     ScreenToLocalTransform: () => Transform;
     active: (outsideReaction: boolean | undefined) => boolean | undefined;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
-    addDocTab: (document: Doc, where: string) => boolean;
+    addDocTab: (document: Doc, where: OpenWhere) => boolean;
     pinToPres: (document: Doc) => void;
     isSelected: (outsideReaction?: boolean) => boolean;
     isFocused: (document: Doc, outsideReaction: boolean) => boolean;
@@ -625,7 +625,7 @@ export class SchemaTable extends React.Component<SchemaTableProps> {
     };
 
     onOpenClick = () => {
-        this._showDoc && this.props.addDocTab(this._showDoc, 'add:right');
+        this._showDoc && this.props.addDocTab(this._showDoc, OpenWhere.addRight);
     };
 
     getPreviewTransform = (): Transform => {
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 570039550..868822fbf 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -16,7 +16,7 @@ import { CollectionFreeFormView } from '../collections/collectionFreeForm/Collec
 import { DocComponent } from '../DocComponent';
 import { StyleProp } from '../StyleProvider';
 import './CollectionFreeFormDocumentView.scss';
-import { DocumentView, DocumentViewProps } from './DocumentView';
+import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import React = require('react');
 
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
@@ -199,7 +199,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 topDoc.x = spt[0];
                 topDoc.y = spt[1];
                 this.props.removeDocument?.(topDoc);
-                this.props.addDocTab(topDoc, 'inParent');
+                this.props.addDocTab(topDoc, OpenWhere.inParent);
             } else {
                 const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0);
                 const fpt = screenXf.transformPoint(spt[0], spt[1]);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb07f1033..2729a5047 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -69,6 +69,27 @@ export enum ViewAdjustment {
     doNothing = 0,
 }
 
+export enum OpenWhere {
+    inPlace = 'inPlace',
+    lightbox = 'lightbox',
+    add = 'add',
+    addRight = 'add:right',
+    addBottom = 'add:bottom',
+    dashboard = 'dashboard',
+    close = 'close',
+    fullScreen = 'fullScreen',
+    toggle = 'toggle',
+    replace = 'replace',
+    inParent = 'inParent',
+}
+export enum OpenWhereMod {
+    none = '',
+    left = 'left',
+    right = 'right',
+    top = 'top',
+    bottom = 'bottom',
+}
+
 export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that are immediately copied over to the target document when link is followed.  Other anchor properties will be copied over in the specific setViewSpec() method on their view (which allows for seting preview values instead of writing to the document)
 
 export interface DocFocusOptions {
@@ -139,7 +160,7 @@ export interface DocumentViewSharedProps {
     showTitle?: () => string;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
     filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
     addDocument?: (doc: Doc | Doc[]) => boolean;
     removeDocument?: (doc: Doc | Doc[]) => boolean;
@@ -474,7 +495,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const pt = me.touchEvent.touches[me.touchEvent.touches.length - 1];
         RadialMenu.Instance.openMenu(pt.pageX - 15, pt.pageY - 15);
 
-        // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), "add:right"), icon: "map-pin", selected: -1 });
+        // RadialMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: "map-pin", selected: -1 });
         const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]);
         (effectiveAcl === AclEdit || effectiveAcl === AclAdmin) &&
             RadialMenu.Instance.addItem({
@@ -485,7 +506,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 icon: 'external-link-square-alt',
                 selected: -1,
             });
-        // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, "add:right"), icon: "trash", selected: -1 });
+        // RadialMenu.Instance.addItem({ description: "Open in a new tab", event: () => this.props.addDocTab(this.props.Document, OpenWhere.addRight), icon: "trash", selected: -1 });
         RadialMenu.Instance.addItem({ description: 'Pin', event: () => this.props.pinToPres(this.props.Document, {}), icon: 'map-pin', selected: -1 });
         RadialMenu.Instance.addItem({ description: 'Open', event: () => MobileInterface.Instance.handleClick(this.props.Document), icon: 'trash', selected: -1 });
 
@@ -586,7 +607,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         );
                     UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click');
                 } else if (!Doc.IsSystem(this.rootDoc) && !this.rootDoc.isLinkButton) {
-                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, 'lightbox', this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
+                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
@@ -857,7 +878,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null);
             const appearance = cm.findByDescription('UI Controls...');
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
-            !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, 'add:right'), icon: 'eye' });
+            !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
             !Doc.noviceMode &&
                 appearanceItems.push({
                     description: 'Add a Field',
@@ -957,8 +978,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
-        helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), 'add:right'), icon: 'layer-group' });
-        !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), 'add:right'), icon: 'keyboard' });
+        helpItems.push({ description: 'Show Metadata', event: () => this.props.addDocTab(Docs.Create.KVPDocument(this.props.Document, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
+        !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' });
         !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' });
         !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' });
 
diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx
index 7d04c4b64..18c5b81ec 100644
--- a/src/client/views/nodes/KeyValueBox.tsx
+++ b/src/client/views/nodes/KeyValueBox.tsx
@@ -20,6 +20,7 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import e = require('express');
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { ImageBox } from './ImageBox';
+import { OpenWhere } from './DocumentView';
 
 export type KVPScript = {
     script: CompiledScript;
@@ -259,8 +260,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> {
         openItems.push({
             description: 'Default Perspective',
             event: () => {
-                this.props.addDocTab(this.props.Document, 'close');
-                this.props.addDocTab(this.fieldDocToLayout, 'add:right');
+                this.props.addDocTab(this.props.Document, OpenWhere.close);
+                this.props.addDocTab(this.fieldDocToLayout, OpenWhere.addRight);
             },
             icon: 'image',
         });
diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx
index 80def3025..e74ef4a39 100644
--- a/src/client/views/nodes/KeyValuePair.tsx
+++ b/src/client/views/nodes/KeyValuePair.tsx
@@ -1,18 +1,19 @@
 import { action, observable } from 'mobx';
-import { observer } from "mobx-react";
+import { observer } from 'mobx-react';
 import { Doc, Field, Opt } from '../../../fields/Doc';
 import { emptyFunction, returnFalse, returnOne, returnZero, returnEmptyFilter, returnEmptyDoclist, emptyPath } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
-import { EditableView } from "../EditableView";
+import { EditableView } from '../EditableView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { KeyValueBox } from './KeyValueBox';
-import "./KeyValueBox.scss";
-import "./KeyValuePair.scss";
-import React = require("react");
+import './KeyValueBox.scss';
+import './KeyValuePair.scss';
+import React = require('react');
 import { DefaultStyleProvider } from '../StyleProvider';
+import { OpenWhere } from './DocumentView';
 
 // Represents one row in a key value plane
 
@@ -23,7 +24,7 @@ export interface KeyValuePairProps {
     keyWidth: number;
     PanelHeight: () => number;
     PanelWidth: () => number;
-    addDocTab: (doc: Doc, where: string) => boolean;
+    addDocTab: (doc: Doc, where: OpenWhere) => boolean;
 }
 @observer
 export class KeyValuePair extends React.Component<KeyValuePairProps> {
@@ -34,23 +35,23 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
     @action
     handleCheck = (e: React.ChangeEvent<HTMLInputElement>) => {
         this.isChecked = e.currentTarget.checked;
-    }
+    };
 
     @action
     uncheck = () => {
         this.checkbox.current!.checked = false;
         this.isChecked = false;
-    }
+    };
 
     onContextMenu = (e: React.MouseEvent) => {
         const value = this.props.doc[this.props.keyName];
         if (value instanceof Doc) {
             e.stopPropagation();
             e.preventDefault();
-            ContextMenu.Instance.addItem({ description: "Open Fields", event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), "add:right"), icon: "layer-group" });
+            ContextMenu.Instance.addItem({ description: 'Open Fields', event: () => this.props.addDocTab(Docs.Create.KVPDocument(value, { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'layer-group' });
             ContextMenu.Instance.displayMenu(e.clientX, e.clientY);
         }
-    }
+    };
 
     render() {
         const props: FieldViewProps = {
@@ -68,7 +69,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             isSelected: returnFalse,
             setHeight: returnFalse,
             select: emptyFunction,
-            dropAction: "alias",
+            dropAction: 'alias',
             bringToFront: emptyFunction,
             renderDepth: 1,
             isContentActive: returnFalse,
@@ -92,30 +93,30 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
             doc = doc.proto;
         }
         const parenCount = Math.max(0, protoCount - 1);
-        const keyStyle = protoCount === 0 ? "black" : "blue";
+        const keyStyle = protoCount === 0 ? 'black' : 'blue';
 
-        const hover = { transition: "0.3s ease opacity", opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
+        const hover = { transition: '0.3s ease opacity', opacity: this.isPointerOver || this.isChecked ? 1 : 0 };
 
         return (
-            <tr className={this.props.rowStyle} onPointerEnter={action(() => this.isPointerOver = true)} onPointerLeave={action(() => this.isPointerOver = false)}>
+            <tr className={this.props.rowStyle} onPointerEnter={action(() => (this.isPointerOver = true))} onPointerLeave={action(() => (this.isPointerOver = false))}>
                 <td className="keyValuePair-td-key" style={{ width: `${this.props.keyWidth}%` }}>
                     <div className="keyValuePair-td-key-container">
-                        <button style={hover} className="keyValuePair-td-key-delete" onClick={undoBatch(() => {
-                            if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
-                                delete props.Document[props.fieldKey];
-                            }
-                            else delete props.Document.proto![props.fieldKey];
-                        })}>
+                        <button
+                            style={hover}
+                            className="keyValuePair-td-key-delete"
+                            onClick={undoBatch(() => {
+                                if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) {
+                                    delete props.Document[props.fieldKey];
+                                } else delete props.Document.proto![props.fieldKey];
+                            })}>
                             X
                         </button>
-                        <input
-                            className={"keyValuePair-td-key-check"}
-                            type="checkbox"
-                            style={hover}
-                            onChange={this.handleCheck}
-                            ref={this.checkbox}
-                        />
-                        <div className="keyValuePair-keyField" style={{ color: keyStyle }}>{"(".repeat(parenCount)}{props.fieldKey}{")".repeat(parenCount)}</div>
+                        <input className={'keyValuePair-td-key-check'} type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} />
+                        <div className="keyValuePair-keyField" style={{ color: keyStyle }}>
+                            {'('.repeat(parenCount)}
+                            {props.fieldKey}
+                            {')'.repeat(parenCount)}
+                        </div>
                     </div>
                 </td>
                 <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}>
@@ -123,13 +124,13 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> {
                         <EditableView
                             contents={contents}
                             maxHeight={36}
-                            height={"auto"}
+                            height={'auto'}
                             GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)}
-                            SetValue={(value: string) =>
-                                KeyValueBox.SetField(props.Document, props.fieldKey, value)} />
+                            SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)}
+                        />
                     </div>
                 </td>
             </tr>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index d6cf79f87..be9565452 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -17,6 +17,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import './LinkAnchorBox.scss';
 import { LinkDocPreview } from './LinkDocPreview';
 import React = require('react');
+import { OpenWhere } from './DocumentView';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -88,13 +89,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     openLinkDocOnRight = (e: React.MouseEvent) => {
-        this.props.addDocTab(this.rootDoc, 'add:right');
+        this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
     };
     openLinkTargetOnRight = (e: React.MouseEvent) => {
         const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null));
         alias._isLinkButton = undefined;
         alias.layoutKey = 'layout';
-        this.props.addDocTab(alias, 'add:right');
+        this.props.addDocTab(alias, OpenWhere.addRight);
     };
     @action
     openLinkEditor = action((e: React.MouseEvent) => {
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index a47577701..135fbca31 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -14,7 +14,7 @@ import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { Transform } from '../../util/Transform';
 import { undoBatch } from '../../util/UndoManager';
-import { DocumentView, DocumentViewSharedProps } from './DocumentView';
+import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
 import { LinkEditor } from '../linking/LinkEditor';
@@ -156,7 +156,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
             LinkDocPreview.Clear();
             LinkFollower.FollowLink(this._linkDoc, this._linkSrc, this.props.docProps, false);
         } else if (this.props.hrefs?.length) {
-            this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), 'add:right');
+            this.props.docProps?.addDocTab(Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }), OpenWhere.addRight);
         }
     };
 
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 70ac84fa4..82d5b00f9 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -31,6 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 import { ObjectField } from '../../../fields/ObjectField';
+import { OpenWhere } from './DocumentView';
 const path = require('path');
 
 /**
@@ -273,7 +274,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             this.player && this._contentRef && this._contentRef.requestFullscreen();
         }
         try {
-            this._youtubePlayer && this.props.addDocTab(this.rootDoc, 'add');
+            this._youtubePlayer && this.props.addDocTab(this.rootDoc, OpenWhere.add);
         } catch (e) {
             console.log('Video FullScreen Exception:', e);
         }
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 42d20ba99..c71552a6f 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -26,6 +26,7 @@ import { ActiveFillColor, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, SetAc
 import { InkTranscription } from '../../InkTranscription';
 import { StyleProp } from '../../StyleProvider';
 import { FieldView, FieldViewProps } from '.././FieldView';
+import { OpenWhere } from '../DocumentView';
 import { RichTextMenu } from '../formattedText/RichTextMenu';
 import { WebBox } from '../WebBox';
 import { FontIconBadge } from './FontIconBadge';
@@ -61,7 +62,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
     }
     showTemplate = (): void => {
         const dragFactory = Cast(this.layoutDoc.dragFactory, Doc, null);
-        dragFactory && this.props.addDocTab(dragFactory, 'add:right');
+        dragFactory && this.props.addDocTab(dragFactory, OpenWhere.addRight);
     };
     dragAsTemplate = (): void => {
         this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)');
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 1e7cb6ea5..63347015b 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -17,6 +17,7 @@ import { Tooltip } from '@material-ui/core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { NodeSelection } from 'prosemirror-state';
+import { OpenWhere } from '../DocumentView';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -227,7 +228,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
             list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb'));
             list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb'));
             alias._pivotField = this._fieldKey.startsWith('#') ? '#' : this._fieldKey;
-            this.props.tbox.props.addDocTab(alias, 'add:right');
+            this.props.tbox.props.addDocTab(alias, OpenWhere.addRight);
         }
     };
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index fdd61463d..ce4639b76 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -45,7 +45,7 @@ import { LightboxView } from '../../LightboxView';
 import { AnchorMenu } from '../../pdf/AnchorMenu';
 import { SidebarAnnos } from '../../SidebarAnnos';
 import { StyleProp } from '../../StyleProvider';
-import { DocumentViewInternal } from '../DocumentView';
+import { DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
 import { DashDocCommentView } from './DashDocCommentView';
@@ -1428,7 +1428,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     const func = () => {
                         const docView = DocumentManager.Instance.getDocumentView(audiodoc);
                         if (!docView) {
-                            this.props.addDocTab(audiodoc, 'add:bottom');
+                            this.props.addDocTab(audiodoc, OpenWhere.addBottom);
                             setTimeout(func);
                         } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that
                     };
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
index 3d9bd6add..68b0488a2 100644
--- a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -9,6 +9,7 @@ import { GetEffectiveAcl } from '../../../../fields/util';
 import { Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { SelectionManager } from '../../../util/SelectionManager';
+import { OpenWhere } from '../DocumentView';
 import { liftListItem, sinkListItem } from './prosemirrorPatches.js';
 
 const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
@@ -135,7 +136,7 @@ export function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKey
     //Command to create a new Tab with a PDF of all the command shortcuts
     bind('Mod-/', (state: EditorState, dispatch: (tx: Transaction) => void) => {
         const newDoc = Docs.Create.PdfDocument(Utils.prepend('/assets/cheat-sheet.pdf'), { _width: 300, _height: 300 });
-        props.addDocTab(newDoc, 'add:right');
+        props.addDocTab(newDoc, OpenWhere.addRight);
     });
 
     //Commands to modify BlockType
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e19b53f50..adfd2fda1 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -23,7 +23,7 @@ import { SelectionManager } from '../../../util/SelectionManager';
 import { SettingsManager } from '../../../util/SettingsManager';
 import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { CollectionDockingView } from '../../collections/CollectionDockingView';
-import { CollectionFreeFormView, MarqueeViewBounds } from '../../collections/collectionFreeForm';
+import { CollectionFreeFormView, computeTimelineLayout, MarqueeViewBounds } from '../../collections/collectionFreeForm';
 import { CollectionView } from '../../collections/CollectionView';
 import { TabDocView } from '../../collections/TabDocView';
 import { ViewBoxBaseComponent } from '../../DocComponent';
@@ -35,6 +35,7 @@ import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
 import { map } from 'bluebird';
+import { OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -196,7 +197,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
         this.props.setContentView?.(this);
         this._unmounting = false;
-        this.rootDoc._forceRenderEngine = 'timeline';
+        this.rootDoc._forceRenderEngine = computeTimelineLayout.name;
         this.layoutDoc.presStatus = PresStatus.Edit;
         this.layoutDoc._gridGap = 0;
         this.layoutDoc._yMargin = 0;
@@ -563,7 +564,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
         });
         const openInTab = (doc: Doc, finished?: () => void) => {
-            (collectionDocView ?? this).props.addDocTab(doc, '');
+            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.add);
             this.layoutDoc.presCollection = targetDoc;
             // this still needs some fixing
             setTimeout(resetSelection, 500);
@@ -725,7 +726,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
             this.layoutDoc.presStatus = PresStatus.Edit;
             Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
-            CollectionDockingView.AddSplit(this.rootDoc, 'right');
+            CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
         } else {
             this.layoutDoc.presStatus = PresStatus.Edit;
             clearTimeout(this._presTimer);
@@ -1760,7 +1761,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 TabDocView.PinDoc(doc, {});
                 this.gotoDocument(this.childDocs.length, this.activeItem);
             } else {
-                this.props.addDocTab(doc, 'add:right');
+                this.props.addDocTab(doc, OpenWhere.addRight);
             }
         }
     };
@@ -2322,7 +2323,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
         const srcContext = Cast(bestTarget.context, Doc, null) ?? Cast(Cast(bestTarget.annotationOn, Doc, null)?.context, Doc, null);
         const openInTab = (doc: Doc, finished?: () => void) => {
-            CollectionDockingView.AddSplit(doc, 'right');
+            CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
             finished?.();
         };
         PresBox.NavigateToTarget(bestTarget, activeItem, openInTab, srcContext);
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 8265de445..2ae597b0b 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -700,7 +700,7 @@ export class MobileInterface extends React.Component {
                     className="docButton"
                     title={Doc.isDocPinned(this._activeDoc) ? 'Unpin from presentation' : 'Pin to presentation'}
                     style={{ backgroundColor: isPinned ? 'black' : 'white', color: isPinned ? 'white' : 'black' }}
-                    onClick={e => TabDocView.PinDoc(this._activeDoc)}>
+                    onClick={e => TabDocView.PinDoc(this._activeDoc, {})}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
                 </div>
             );
-- 
cgit v1.2.3-70-g09d2


From cdd1dd95c67fcb95914913bb5346c5117c0abc27 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 18 Nov 2022 17:26:17 -0500
Subject: migration of link properties to propertiesView

---
 src/client/util/LinkFollower.ts                    |  14 +-
 src/client/util/LinkManager.ts                     |   3 +-
 src/client/util/SelectionManager.ts                |   4 +
 src/client/views/DocumentButtonBar.tsx             |   4 +-
 src/client/views/PropertiesView.scss               |   8 +-
 src/client/views/PropertiesView.tsx                | 307 +++++++++++++++------
 .../CollectionFreeFormLinkView.tsx                 |  27 +-
 src/client/views/linking/LinkEditor.tsx            |  16 +-
 src/client/views/linking/LinkMenu.scss             |   8 +-
 src/client/views/linking/LinkMenuItem.scss         |  24 +-
 src/client/views/linking/LinkMenuItem.tsx          |  42 ++-
 src/client/views/nodes/LinkDocPreview.tsx          |   2 +
 src/client/views/nodes/trails/PresBox.tsx          |  30 +-
 src/client/views/nodes/trails/PresEnums.ts         |   3 +
 src/fields/Doc.ts                                  |   2 +-
 src/fields/documentSchemas.ts                      |   2 +-
 16 files changed, 327 insertions(+), 169 deletions(-)

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

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index a3eb7ed7a..52885e428 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -111,7 +111,19 @@ export class LinkFollower {
                             containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
                         }
                         const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
-                        DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished);
+                        DocumentManager.Instance.jumpToDocument(
+                            target,
+                            zoom,
+                            (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished),
+                            targetContexts,
+                            linkDoc,
+                            undefined,
+                            sourceDoc,
+                            allFinished,
+                            undefined,
+                            undefined,
+                            Cast(target.presZoom, 'number', null)
+                        );
                     }
                 } else {
                     allFinished();
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 7a12a8580..49cc3218d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -19,7 +19,8 @@ import { Cast, StrCast } from '../../fields/Types';
 export class LinkManager {
     @observable static _instance: LinkManager;
     @observable static userLinkDBs: Doc[] = [];
-    public static currentLink: Opt<Doc>;
+    @observable public static currentLink: Opt<Doc>;
+    @observable public static currentLinkAnchor: Opt<Doc>;
     public static get Instance() {
         return LinkManager._instance;
     }
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index a3d6f5227..1a2dda953 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,6 +4,7 @@ import { Doc, Opt } from '../../fields/Doc';
 import { DocCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocumentView } from '../views/nodes/DocumentView';
+import { LinkManager } from './LinkManager';
 import { ScriptingGlobals } from './ScriptingGlobals';
 
 export namespace SelectionManager {
@@ -15,6 +16,7 @@ export namespace SelectionManager {
         @action
         SelectSchemaViewDoc(doc: Opt<Doc>) {
             manager.SelectedSchemaDocument = doc;
+            if (doc?.type === DocumentType.LINK) LinkManager.currentLink = doc;
         }
         @action
         SelectView(docView: DocumentView, ctrlPressed: boolean): void {
@@ -32,6 +34,7 @@ export namespace SelectionManager {
                 manager.SelectedViews.clear();
                 manager.SelectedViews.set(docView, docView.rootDoc);
             }
+            if (docView.rootDoc.type === DocumentType.LINK) LinkManager.currentLink = docView.rootDoc;
         }
         @action
         DeselectView(docView: DocumentView): void {
@@ -42,6 +45,7 @@ export namespace SelectionManager {
         }
         @action
         DeselectAll(): void {
+            LinkManager.currentLink = undefined;
             manager.SelectedSchemaDocument = undefined;
             Array.from(manager.SelectedViews.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
             manager.SelectedViews.clear();
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index ecf330792..5969d55e9 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -21,7 +21,7 @@ import './DocumentButtonBar.scss';
 import { Colors } from './global/globalEnums';
 import { MetadataEntryMenu } from './MetadataEntryMenu';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
-import { DocumentView, DocumentViewInternal } from './nodes/DocumentView';
+import { DocumentView, DocumentViewInternal, OpenWhereMod } from './nodes/DocumentView';
 import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
 import { TemplateMenu } from './TemplateMenu';
@@ -180,7 +180,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                                 googleDoc = Docs.Create.WebDocument(googleDocUrl, options);
                                 dataDoc.googleDoc = googleDoc;
                             }
-                            CollectionDockingView.AddSplit(googleDoc, 'right');
+                            CollectionDockingView.AddSplit(googleDoc, OpenWhereMod.right);
                         } else if (e.altKey) {
                             e.preventDefault();
                             window.open(googleDocUrl);
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 437df4739..30806f718 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -1,9 +1,9 @@
-@import "./global/globalCssVariables.scss";
+@import './global/globalCssVariables.scss';
 
 .propertiesView {
     height: 100%;
     width: 250;
-    font-family: "Roboto";
+    font-family: 'Roboto';
     cursor: auto;
 
     overflow-x: hidden;
@@ -843,7 +843,7 @@
 }
 
 .propertiesView-section {
-    padding: 10px 0;
+    padding-left: 20px;
 }
 
 .propertiesView-input {
@@ -877,4 +877,4 @@
     color: white;
     padding-left: 8px;
     background-color: rgb(51, 51, 51);
-}
\ No newline at end of file
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 93a3fd253..076dc8261 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -4,7 +4,7 @@ import { faAnchor, faArrowRight } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Checkbox, Tooltip } from '@material-ui/core';
 import { intersection } from 'lodash';
-import { action, autorun, computed, Lambda, observable } from 'mobx';
+import { action, autorun, computed, Lambda, observable, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
 import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSelfEdit, AclSym, AclUnset, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
@@ -27,12 +27,13 @@ import { InkStrokeProperties } from './InkStrokeProperties';
 import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
 import { FilterBox } from './nodes/FilterBox';
 import { KeyValueBox } from './nodes/KeyValueBox';
-import { PresBox } from './nodes/trails';
+import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
 import { PropertiesButtons } from './PropertiesButtons';
 import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector';
 import { PropertiesDocContextSelector } from './PropertiesDocContextSelector';
 import './PropertiesView.scss';
 import { DefaultStyleProvider } from './StyleProvider';
+import { Colors } from './global/globalEnums';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -54,7 +55,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     }
 
     @computed get selectedDoc() {
-        return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard;
+        return LinkManager.currentLink || SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard;
     }
     @computed get selectedDocumentView() {
         if (SelectionManager.Views().length) return SelectionManager.Views()[0];
@@ -405,7 +406,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
      */
     @computed get expansionIcon() {
         return (
-            <Tooltip title={<div className="dash-tooltip">{'Show more permissions'}</div>}>
+            <Tooltip title={<div className="dash-tooltip">Show more permissions</div>}>
                 <div
                     className="expansion-button"
                     onPointerDown={() => {
@@ -1406,6 +1407,37 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         }
     });
 
+    @undoBatch
+    changeAnimationBehavior = action((behavior: string) => {
+        const lanch = this.destinationAnchor;
+        if (lanch) {
+            lanch.presEffect = behavior;
+        }
+    });
+    @undoBatch
+    @action
+    updateEffectDirection = (effect: PresEffectDirection) => {
+        const lanch = this.destinationAnchor;
+        if (lanch) {
+            lanch.presEffectDirection = effect;
+        }
+    };
+
+    @undoBatch
+    animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
+        const lanch = this.destinationAnchor;
+        const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+        return (
+            <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
+                <div
+                    style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+                    onClick={() => this.updateEffectDirection(direction)}>
+                    {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
+                </div>
+            </Tooltip>
+        );
+    };
+
     @undoBatch
     changeFollowBehavior = action((follow: string) => {
         if (LinkManager.currentLink && this.selectedDoc) {
@@ -1438,19 +1470,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         }
     };
 
-    toggleAnchor = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc.linkAutoMove = !this.selectedDoc.linkAutoMove))));
+    toggleProp = (e: React.PointerEvent, prop: string) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc[prop] = !this.selectedDoc[prop]))));
     };
 
-    toggleArrow = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc.displayArrow = !this.selectedDoc.displayArrow))));
-    };
+    @computed get destinationAnchor() {
+        const ldoc = LinkManager.currentLink;
+        const lanch = LinkManager.currentLinkAnchor;
+        if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch;
+        return ldoc ? DocCast(ldoc.anchor2) : ldoc;
+    }
 
-    toggleZoomToTarget1 = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (DocCast(this.selectedDoc.anchor1).followLinkZoom = !DocCast(this.selectedDoc.anchor1).followLinkZoom))));
-    };
-    toggleZoomToTarget2 = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (DocCast(this.selectedDoc.anchor2).followLinkZoom = !DocCast(this.selectedDoc.anchor2).followLinkZoom))));
+    @computed get sourceAnchor() {
+        return LinkManager.currentLinkAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
+    }
+
+    toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc) => {
+        anchor && setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (anchor[prop] = !anchor[prop]))));
     };
 
     @computed
@@ -1485,6 +1521,23 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         );
     }
 
+    // Converts seconds to ms and updates presTransition
+    setZoom = (number: String, change?: number) => {
+        let scale = Number(number) / 100;
+        if (change) scale += change;
+        if (scale < 0.01) scale = 0.01;
+        if (scale > 1) scale = 1;
+        this.destinationAnchor && (this.destinationAnchor.presZoom = scale);
+    };
+    // Converts seconds to ms and updates presTransition
+    setZoomSrc = (number: String, change?: number) => {
+        let scale = Number(number) / 100;
+        if (change) scale += change;
+        if (scale < 0.01) scale = 0.01;
+        if (scale > 1) scale = 1;
+        this.sourceAnchor && (this.sourceAnchor.presZoom = scale);
+    };
+
     /**
      * Handles adding and removing members from the sharing panel
      */
@@ -1498,6 +1551,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     render() {
         const isNovice = Doc.noviceMode;
+        const zoom = Number((NumCast(this.destinationAnchor?.presZoom) * 100).toPrecision(3));
+        const zoomSrc = Number((NumCast(this.sourceAnchor?.presZoom) * 100).toPrecision(3));
+        const targZoom = this.destinationAnchor?.followLinkZoom;
+        const srcZooom = this.sourceAnchor?.followLinkZoom;
         if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this.props.width }}>
@@ -1507,81 +1564,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 </div>
             );
         } else {
-            if (this.selectedDoc && this.isLink) {
-                return (
-                    <div className="propertiesView">
-                        <div className="propertiesView-title">Linking</div>
-                        <div className="propertiesView-section">
-                            <p className="propertiesView-label">Information</p>
-                            <div className="propertiesView-input first">
-                                <p>Link Relationship</p>
-                                {this.editRelationship}
-                            </div>
-                            <div className="propertiesView-input">
-                                <p>Description</p>
-                                {this.editDescription}
-                            </div>
-                        </div>
-                        <div className="propertiesView-section">
-                            <p className="propertiesView-label">Behavior</p>
-                            <div className="propertiesView-input inline first">
-                                <p>Follow</p>
-                                <select name="selectList" id="selectList" onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.selectedDoc.followLinkLocation, 'default')}>
-                                    <option value="default">Default</option>
-                                    <option value="add:left">Open in new left pane</option>
-                                    <option value="add:right">Open in new right pane</option>
-                                    <option value="replace:left">Replace left tab</option>
-                                    <option value="replace:right">Replace right tab</option>
-                                    <option value="fullScreen">Open full screen</option>
-                                    <option value="add">Open in new tab</option>
-                                    <option value="replace">Replace current tab</option>
-                                    {this.selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
-                                </select>
-                            </div>
-                            <div className="propertiesView-input inline">
-                                <p>Auto-move anchor</p>
-                                <button
-                                    style={{ background: this.selectedDoc.hidden ? 'gray' : !this.selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
-                                    onPointerDown={this.toggleAnchor}
-                                    onClick={e => e.stopPropagation()}
-                                    className="propertiesButton">
-                                    <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                                </button>
-                            </div>
-                            <div className="propertiesView-input inline">
-                                <p>Display arrow</p>
-                                <button
-                                    style={{ background: this.selectedDoc.hidden ? 'gray' : !this.selectedDoc.displayArrow ? '' : '#4476f7', borderRadius: 3 }}
-                                    onPointerDown={this.toggleArrow}
-                                    onClick={e => e.stopPropagation()}
-                                    className="propertiesButton">
-                                    <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                </button>
-                            </div>
-                            <div className="propertiesView-input inline">
-                                <p>Zoom to target</p>
-                                <button
-                                    style={{ background: this.selectedDoc.hidden ? 'gray' : !Cast(this.selectedDoc.anchor1, Doc, null).followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
-                                    onPointerDown={this.toggleZoomToTarget1}
-                                    onClick={e => e.stopPropagation()}
-                                    className="propertiesButton">
-                                    <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                </button>
-                            </div>
-                            <div className="propertiesView-input inline">
-                                <p>Zoom to source</p>
-                                <button
-                                    style={{ background: this.selectedDoc.hidden ? 'gray' : !Cast(this.selectedDoc.anchor2, Doc, null).followLinkZoom ? '' : '#4476f7', borderRadius: 3 }}
-                                    onPointerDown={this.toggleZoomToTarget2}
-                                    onClick={e => e.stopPropagation()}
-                                    className="propertiesButton">
-                                    <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                </button>
-                            </div>
-                        </div>
-                    </div>
-                );
-            }
             if (this.selectedDoc && !this.isPres) {
                 return (
                     <div
@@ -1599,6 +1581,153 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         {this.contextsSubMenu}
 
                         {this.linksSubMenu}
+                        {!this.selectedDoc || !LinkManager.currentLink ? null : (
+                            <>
+                                <div className="propertiesView-section">
+                                    <div className="propertiesView-input first">
+                                        <p>Link Relationship</p>
+                                        {this.editRelationship}
+                                    </div>
+                                    <div className="propertiesView-input">
+                                        <p>Description</p>
+                                        {this.editDescription}
+                                    </div>
+                                </div>
+                                <div className="propertiesView-section">
+                                    <div className="propertiesView-input inline first">
+                                        <p>Follow Behavior</p>
+                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.selectedDoc.followLinkLocation, 'default')}>
+                                            <option value="default">Default</option>
+                                            <option value="add:left">Open in new left pane</option>
+                                            <option value="add:right">Open in new right pane</option>
+                                            <option value="replace:left">Replace left tab</option>
+                                            <option value="replace:right">Replace right tab</option>
+                                            <option value="fullScreen">Open full screen</option>
+                                            <option value="add">Open in new tab</option>
+                                            <option value="replace">Replace current tab</option>
+                                            <option value="inPlace">Opin in place</option>
+                                            {this.selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+                                        </select>
+                                    </div>
+                                    <div className="propertiesView-input inline first">
+                                        <p>Target Effect</p>
+                                        <select style={{ width: '100%' }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.destinationAnchor?.presEffect, 'default')}>
+                                            <option value="default">Default</option>
+                                            {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
+                                                <option value={effect.toString()}>{effect.toString()}</option>
+                                            ))}
+                                        </select>
+                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40 }}>
+                                            {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
+                                            {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
+                                            {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
+                                            {this.animationDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
+                                            {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
+                                        </div>
+                                    </div>
+                                    {PresBox.inputter('0.1', '0.1', '10', NumCast(this.destinationAnchor?.presTransition) / 1000, true, (val: string) =>
+                                        PresBox.SetTransitionTime(val, (timeInMS: number) => this.destinationAnchor && (this.destinationAnchor.presTransition = timeInMS))
+                                    )}{' '}
+                                    <div
+                                        className={'slider-headers'}
+                                        style={{
+                                            display: 'grid',
+                                            justifyContent: 'space-between',
+                                            width: '100%',
+                                            gridTemplateColumns: 'auto auto auto',
+                                            borderTop: 'solid',
+                                        }}>
+                                        <div className="slider-text">Fast</div>
+                                        <div className="slider-text">Medium</div>
+                                        <div className="slider-text">Slow</div>
+                                    </div>{' '}
+                                    <div className="propertiesView-input inline">
+                                        <p>Play Target Audio</p>
+                                        <button
+                                            style={{ background: !this.sourceAnchor?.followLinkAudio ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleAnchorProp(e, 'followLinkAudio', this.sourceAnchor)}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline">
+                                        <p>Auto-move anchor</p>
+                                        <button
+                                            style={{ background: !this.selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleProp(e, 'linkAutoMove')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline">
+                                        <p>Show link</p>
+                                        <button
+                                            style={{ background: !this.selectedDoc.linkDisplay ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleProp(e, 'linkDisplay')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline">
+                                        <p>Display arrow</p>
+                                        <button
+                                            style={{ background: !this.selectedDoc.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleProp(e, 'linkDisplayArrow')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline">
+                                        <p>Zoom to target (% screen filled)</p>
+                                        <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'flex' }}>
+                                            <input className="presBox-input" type="number" value={zoom} />
+                                            <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
+                                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
+                                                    <FontAwesomeIcon icon={'caret-up'} />
+                                                </div>
+                                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), -0.1))}>
+                                                    <FontAwesomeIcon icon={'caret-down'} />
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <button
+                                            style={{ background: !targZoom ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.destinationAnchor)}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom)}
+                                    <div className="propertiesView-input inline">
+                                        <p>Zoom to source (% screen filled)</p>
+                                        <div className="ribbon-property" style={{ display: !srcZooom ? 'none' : 'flex' }}>
+                                            <input className="presBox-input" type="number" value={zoomSrc} />
+                                            <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
+                                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoomSrc(String(zoomSrc), 0.1))}>
+                                                    <FontAwesomeIcon icon={'caret-up'} />
+                                                </div>
+                                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoomSrc(String(zoomSrc), -0.1))}>
+                                                    <FontAwesomeIcon icon={'caret-down'} />
+                                                </div>
+                                            </div>
+                                        </div>
+                                        <button
+                                            style={{ background: !srcZooom ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    {!srcZooom ? null : PresBox.inputter('0', '1', '100', zoomSrc, true, this.setZoomSrc)}
+                                </div>
+                            </>
+                        )}
 
                         {this.inkSubMenu}
 
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index bf9de6760..a4131d7c0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -44,7 +44,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                 this._start = Date.now();
                 this._timeout && clearTimeout(this._timeout);
                 this._timeout = setTimeout(this.timeout, 25);
-                setTimeout(this.placeAnchors);
+                setTimeout(this.placeAnchors, 10); // when docs are dragged, their transforms will update before a render has been performed.  placeanchors needs to come after a render to find things in the dom.  a 0 timeout will still come before the render
             }),
             { fireImmediately: true }
         );
@@ -163,9 +163,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
 
     @action
     toggleProperties = () => {
-        if (SettingsManager.propertiesWidth > 0) {
-            SettingsManager.propertiesWidth = 0;
-        } else {
+        if ((SettingsManager.propertiesWidth ?? 0) < 100) {
             SettingsManager.propertiesWidth = 250;
         }
     };
@@ -217,14 +215,25 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
 
         const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
         const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
-        return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
+        return {
+            a,
+            b,
+            pt1norm,
+            pt2norm,
+            aActive,
+            bActive,
+            textX,
+            textY,
+            pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13],
+            pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13],
+        };
     }
 
     render() {
+        //setTimeout(action(() => (LinkManager.currentLink = this.props.LinkDocs[0])));
         if (!this.renderData) return null;
 
         const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
-        LinkManager.currentLink = this.props.LinkDocs[0];
         const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship
         const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
         const linkColorList = Doc.UserDoc().linkColorList as List<string>;
@@ -241,8 +250,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         //thickness varies linearly from 3px to 12px for increasing link count
         const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px';
 
-        if (this.props.LinkDocs[0].displayArrow === undefined) {
-            this.props.LinkDocs[0].displayArrow = false;
+        if (this.props.LinkDocs[0].linkDisplayArrow === undefined) {
+            this.props.LinkDocs[0].linkDisplayArrow = false;
         }
 
         return this.props.LinkDocs[0].opacity === 0 || !a.width || !b.width || (!this.props.LinkDocs[0].linkDisplay && !aActive && !bActive) ? null : (
@@ -257,7 +266,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     style={{ pointerEvents: 'all', opacity: this._opacity, stroke: SelectionManager.SelectedSchemaDoc() === this.props.LinkDocs[0] ? Colors.MEDIUM_BLUE : stroke, strokeWidth }}
                     onClick={this.onClickLine}
                     d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`}
-                    markerEnd={this.props.LinkDocs[0].displayArrow ? 'url(#arrowhead)' : ''}
+                    markerEnd={this.props.LinkDocs[0].linkDisplayArrow ? 'url(#arrowhead)' : ''}
                 />
                 {textX === undefined ? null : (
                     <text className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}>
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 8c4d756d2..07c20dae4 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -327,21 +327,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                         <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openEffectDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
                     </div>
                     <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openEffectDropdown ? '' : 'none' }}>
-                        {[
-                            PresEffect.None,
-                            PresEffect.Zoom,
-                            PresEffect.Lightspeed,
-                            PresEffect.Fade,
-                            PresEffect.Flip,
-                            PresEffect.Rotate,
-                            PresEffect.Bounce,
-                            PresEffect.Roll,
-                            PresEffect.Left,
-                            PresEffect.Right,
-                            PresEffect.Center,
-                            PresEffect.Top,
-                            PresEffect.Bottom,
-                        ].map(effect => (
+                        {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
                             <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeEffect(effect.toString())}>
                                 {effect.toString()}
                             </div>
diff --git a/src/client/views/linking/LinkMenu.scss b/src/client/views/linking/LinkMenu.scss
index 77c16a28f..80cf93ed8 100644
--- a/src/client/views/linking/LinkMenu.scss
+++ b/src/client/views/linking/LinkMenu.scss
@@ -1,4 +1,4 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
 
 .linkMenu {
     width: auto;
@@ -13,7 +13,6 @@
         border: 1px solid #e4e4e4;
         box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
         background: white;
-        min-width: 170px;
         max-height: 230px;
         overflow-y: scroll;
         z-index: 10;
@@ -22,7 +21,7 @@
     .linkMenu-list {
         white-space: nowrap;
         overflow-x: hidden;
-        width: 240px;
+        width: 100%;
         scrollbar-color: white;
 
         &:last-child {
@@ -39,7 +38,6 @@
     border-bottom: 0.5px solid lightgray;
     //@extend: 5px 0;
 
-
     &:last-child {
         border-bottom: none;
     }
@@ -76,4 +74,4 @@
             display: none;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 8333aa374..2ca97a27d 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -1,7 +1,7 @@
-@import "../global/globalCssVariables";
+@import '../global/globalCssVariables';
 
 .linkMenu-item {
-    // border-top: 0.5px solid $medium-gray; 
+    // border-top: 0.5px solid $medium-gray;
     position: relative;
     display: flex;
     border-top: 0.5px solid #cdcdcd;
@@ -14,7 +14,6 @@
 
     background-color: white;
 
-
     .linkMenu-name {
         position: relative;
         width: auto;
@@ -22,9 +21,7 @@
         display: flex;
 
         .linkMenu-text {
-
-            // padding: 4px 2px;
-            //display: inline;
+            width: 100%;
 
             .linkMenu-source-title {
                 text-decoration: none;
@@ -35,9 +32,7 @@
                 margin-left: 20px;
             }
 
-
             .linkMenu-title-wrapper {
-
                 display: flex;
                 align-items: center;
                 min-height: 20px;
@@ -59,7 +54,7 @@
 
                 .linkMenu-destination-title {
                     text-decoration: none;
-                    color: #4476F7;
+                    color: #4476f7;
                     font-size: 13px;
                     line-height: 0.9;
                     padding-bottom: 2px;
@@ -96,9 +91,7 @@
                 //overflow-wrap: break-word;
                 user-select: none;
             }
-
         }
-
     }
 
     .linkMenu-item-content {
@@ -114,7 +107,6 @@
     }
 
     &:hover {
-
         background-color: rgb(201, 239, 252);
 
         .linkMenu-item-buttons {
@@ -122,7 +114,6 @@
         }
 
         .linkMenu-item-content {
-
             .linkMenu-destination-title {
                 text-decoration: underline;
                 color: rgb(60, 90, 156);
@@ -135,10 +126,7 @@
 
 .linkMenu-item-buttons {
     //@extend: right;
-    position: absolute;
-    top: 50%;
-    right: 0;
-    transform: translateY(-50%);
+    position: relative;
     display: none;
 
     .button {
@@ -172,4 +160,4 @@
             cursor: pointer;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 387e0e3d5..c3b5fa997 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -1,12 +1,12 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, observable } from 'mobx';
+import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, DocListCast } from '../../../fields/Doc';
-import { Cast, StrCast } from '../../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
-import { emptyFunction, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
@@ -16,6 +16,9 @@ import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
 import React = require('react');
+import { SettingsManager } from '../../util/SettingsManager';
+import { SelectionManager } from '../../util/SelectionManager';
+import { undoBatch } from '../../util/UndoManager';
 
 interface LinkMenuItemProps {
     groupType: string;
@@ -76,8 +79,19 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         this._showMore = !this._showMore;
     }
 
-    onEdit = (e: React.PointerEvent): void => {
-        LinkManager.currentLink = this.props.linkDoc;
+    @computed get sourceAnchor() {
+        const ldoc = this.props.linkDoc;
+        if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) {
+            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1);
+            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2);
+        }
+        return this.props.sourceDoc;
+    }
+    @action
+    onEdit = (e: React.PointerEvent) => {
+        const sel = SelectionManager.Views();
+        LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc;
+        LinkManager.currentLinkAnchor = this.sourceAnchor;
         setupMoveUpEvents(
             this,
             e,
@@ -88,7 +102,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                 return true;
             },
             emptyFunction,
-            () => this.props.showEditor(this.props.linkDoc)
+            action(() => {
+                if ((SettingsManager.propertiesWidth ?? 0) < 100) {
+                    SettingsManager.propertiesWidth = 250;
+                }
+                //this.props.showEditor(this.props.linkDoc);
+            })
         );
     };
 
@@ -123,6 +142,8 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
         );
     };
 
+    deleteLink = (e: React.PointerEvent): void => setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
+
     render() {
         const destinationIcon = Doc.toIcon(this.props.destinationDoc) as any as IconProp;
 
@@ -141,7 +162,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                 : undefined;
 
         return (
-            <div className="linkMenu-item">
+            <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}>
                 <div className={'linkMenu-item-content expand-two'}>
                     <div
                         ref={this._drag}
@@ -180,7 +201,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                         <div className="linkMenu-item-buttons" ref={this._buttonRef}>
                             <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
                                 <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
-                                    <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
+                                    <FontAwesomeIcon className="fa-icon" icon="chevron-down" size="sm" />
+                                </div>
+                            </Tooltip>
+                            <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
+                                <div className="button" style={{ background: 'red' }} ref={this._editRef} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
+                                    <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
                                 </div>
                             </Tooltip>
                         </div>
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 135fbca31..b326865b3 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -124,6 +124,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         return undefined;
     }
     @observable _showEditor = false;
+
+    @action
     editLink = (e: React.PointerEvent): void => {
         LinkManager.currentLink = this.props.linkDoc;
         setupMoveUpEvents(
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index adfd2fda1..1cb6bc3a2 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -33,7 +33,7 @@ import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentVie
 import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
-import { PresEffect, PresMovement, PresStatus } from './PresEnums';
+import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import { map } from 'bluebird';
 import { OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
@@ -71,10 +71,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc, root: Doc) {
         const effectProps = {
-            left: presEffectDoc?.presEffectDirection === PresEffect.Left,
-            right: presEffectDoc?.presEffectDirection === PresEffect.Right,
-            top: presEffectDoc?.presEffectDirection === PresEffect.Top,
-            bottom: presEffectDoc?.presEffectDirection === PresEffect.Bottom,
+            left: presEffectDoc?.presEffectDirection === PresEffectDirection.Left,
+            right: presEffectDoc?.presEffectDirection === PresEffectDirection.Right,
+            top: presEffectDoc?.presEffectDirection === PresEffectDirection.Top,
+            bottom: presEffectDoc?.presEffectDirection === PresEffectDirection.Bottom,
             opposite: true,
             delay: 0,
             duration: Cast(presEffectDoc?.presTransition, 'number', null),
@@ -1191,7 +1191,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @undoBatch
     @action
-    updateEffectDirection = (effect: PresEffect, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect));
+    updateEffectDirection = (effect: PresEffectDirection, all?: boolean) => (all ? this.childDocs : this.selectedArray).forEach(doc => (doc.presEffectDirection = effect));
 
     @undoBatch
     @action
@@ -1236,12 +1236,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 {movement}
             </div>
         );
-        const presDirection = (direction: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => {
-            const color = this.activeItem.presEffectDirection === direction || (direction === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+        const presDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
+            const color = this.activeItem.presEffectDirection === direction || (direction === PresEffectDirection.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
             return (
                 <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                     <div
-                        style={{ ...opts, border: direction === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+                        style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
                         onClick={() => this.updateEffectDirection(direction)}>
                         {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
                     </div>
@@ -1405,11 +1405,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div>
                             </div>
                             <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}>
-                                {presDirection(PresEffect.Left, 'angle-right', 1, 2, {})}
-                                {presDirection(PresEffect.Right, 'angle-left', 3, 2, {})}
-                                {presDirection(PresEffect.Top, 'angle-down', 2, 1, {})}
-                                {presDirection(PresEffect.Bottom, 'angle-up', 2, 3, {})}
-                                {presDirection(PresEffect.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
+                                {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
+                                {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
+                                {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
+                                {presDirection(PresEffectDirection.Bottom, 'angle-up', 2, 3, {})}
+                                {presDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
                             </div>
                         </div>
                     )}
@@ -1428,7 +1428,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     applyTo = (array: Doc[]) => {
         this.updateMovement(this.activeItem.presMovement as PresMovement, true);
         this.updateEffect(this.activeItem.presEffect as PresEffect, true);
-        this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffect, true);
+        this.updateEffectDirection(this.activeItem.presEffectDirection as PresEffectDirection, true);
         const { presTransition, presDuration, presHideBefore, presHideAfter } = this.activeItem;
         array.forEach(curDoc => {
             curDoc.presTransition = presTransition;
diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts
index c6a222c3a..034f7588b 100644
--- a/src/client/views/nodes/trails/PresEnums.ts
+++ b/src/client/views/nodes/trails/PresEnums.ts
@@ -14,6 +14,9 @@ export enum PresEffect {
     Bounce = 'Bounce',
     Roll = 'Roll',
     None = 'None',
+}
+
+export enum PresEffectDirection {
     Left = 'Enter from left',
     Right = 'Enter from right',
     Center = 'Enter from center',
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 75801b68c..c10751698 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1268,7 +1268,7 @@ export namespace Doc {
 
     let _lastDate = 0;
     export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
-        linkFollowUnhighlight();
+        //linkFollowUnhighlight();
         // runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = undefined));
         // setTimeout(() => runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect)));
         runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect));
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 24b5a359d..10324449f 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -93,7 +93,7 @@ export const documentSchema = createSchema({
     layers: listSpec('string'), // which layers the document is part of
     _lockedPosition: 'boolean', // whether the document can be moved (dragged)
     _lockedTransform: 'boolean', // whether a freeformview can pan/zoom
-    displayArrow: 'boolean', // toggles directed arrows
+    linkDisplayArrow: 'boolean', // toggles directed arrows
 
     // drag drop properties
     _stayInCollection: 'boolean', // whether document can be dropped into a different collection
-- 
cgit v1.2.3-70-g09d2


From 1f5db9cfc594dbf337d752ec94dab5fca7d8b6f7 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 21 Nov 2022 14:50:25 -0500
Subject: working cleaned up version of link editor properties in
 propertiesView

---
 src/client/util/DocumentManager.ts                 |   6 +-
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/util/SelectionManager.ts                |   3 -
 .../views/PropertiesDocBacklinksSelector.tsx       |   5 +-
 src/client/views/PropertiesView.tsx                | 161 ++++++++++-----------
 .../CollectionFreeFormLinkView.tsx                 |   1 -
 .../collectionSchema/CollectionSchemaCells.tsx     |   3 +-
 .../collections/collectionSchema/SchemaTable.tsx   |   3 +-
 src/client/views/linking/LinkEditor.tsx            |  22 +--
 src/client/views/linking/LinkMenu.tsx              |  31 ++--
 src/client/views/linking/LinkMenuGroup.tsx         |  65 +++++----
 src/client/views/linking/LinkMenuItem.scss         |   6 +-
 src/client/views/linking/LinkMenuItem.tsx          |  57 ++++----
 src/client/views/nodes/LinkDocPreview.tsx          |  33 +++--
 src/client/views/nodes/LoadingBox.scss             |  15 +-
 src/client/views/nodes/trails/PresBox.tsx          |   3 +-
 16 files changed, 205 insertions(+), 211 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 00f6bc40a..7120eeb88 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -5,7 +5,7 @@ import { Cast } from '../../fields/Types';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentView, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
@@ -167,7 +167,7 @@ export class DocumentManager {
     }
 
     static addView = (doc: Doc, finished?: () => void) => {
-        CollectionDockingView.AddSplit(doc, 'right');
+        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
         finished?.();
     };
     public jumpToDocument = (
@@ -347,7 +347,7 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) {
     } else {
         const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
         const showDoc = context || doc;
-        CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), 'right') && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
+        CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), OpenWhereMod.right) && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
     }
 }
 ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 52885e428..5374bf44b 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -122,7 +122,7 @@ export class LinkFollower {
                             allFinished,
                             undefined,
                             undefined,
-                            Cast(target.presZoom, 'number', null)
+                            Cast(sourceDoc.presZoom, 'number', null)
                         );
                     }
                 } else {
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 1a2dda953..02d672a65 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -16,7 +16,6 @@ export namespace SelectionManager {
         @action
         SelectSchemaViewDoc(doc: Opt<Doc>) {
             manager.SelectedSchemaDocument = doc;
-            if (doc?.type === DocumentType.LINK) LinkManager.currentLink = doc;
         }
         @action
         SelectView(docView: DocumentView, ctrlPressed: boolean): void {
@@ -34,7 +33,6 @@ export namespace SelectionManager {
                 manager.SelectedViews.clear();
                 manager.SelectedViews.set(docView, docView.rootDoc);
             }
-            if (docView.rootDoc.type === DocumentType.LINK) LinkManager.currentLink = docView.rootDoc;
         }
         @action
         DeselectView(docView: DocumentView): void {
@@ -45,7 +43,6 @@ export namespace SelectionManager {
         }
         @action
         DeselectAll(): void {
-            LinkManager.currentLink = undefined;
             manager.SelectedSchemaDocument = undefined;
             Array.from(manager.SelectedViews.keys()).forEach(dv => dv.props.whenChildContentsActiveChanged(false));
             manager.SelectedViews.clear();
diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx
index 25ac44078..f7173a593 100644
--- a/src/client/views/PropertiesDocBacklinksSelector.tsx
+++ b/src/client/views/PropertiesDocBacklinksSelector.tsx
@@ -3,7 +3,6 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { Cast } from '../../fields/Types';
-import { emptyFunction } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
@@ -47,9 +46,9 @@ export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDo
 
     render() {
         return !SelectionManager.Views().length ? null : (
-            <div>
+            <div className="preroptiesDocBacklinksSelector">
                 {this.props.hideTitle ? null : <p key="contexts">Contexts:</p>}
-                <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={emptyFunction} itemHandler={this.getOnClick} position={{ x: 0 }} />
+                <LinkMenu docView={SelectionManager.Views().lastElement()} clearLinkEditor={undefined} itemHandler={this.getOnClick} style={{ left: 0, top: 0 }} />
             </div>
         );
     }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 076dc8261..ad3f62990 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1409,7 +1409,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => {
-        const lanch = this.destinationAnchor;
+        const lanch = this.sourceAnchor;
         if (lanch) {
             lanch.presEffect = behavior;
         }
@@ -1417,7 +1417,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @undoBatch
     @action
     updateEffectDirection = (effect: PresEffectDirection) => {
-        const lanch = this.destinationAnchor;
+        const lanch = this.sourceAnchor;
         if (lanch) {
             lanch.presEffectDirection = effect;
         }
@@ -1425,7 +1425,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     @undoBatch
     animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
-        const lanch = this.destinationAnchor;
+        const lanch = this.sourceAnchor;
         const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
         return (
             <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
@@ -1451,7 +1451,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         document.getElementById('link_description_input')?.blur();
     };
 
-    onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
+    onDescriptionKey = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
         if (e.key === 'Enter') {
             this.setDescripValue(this.description);
             document.getElementById('link_description_input')?.blur();
@@ -1508,29 +1508,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @computed
     get editDescription() {
         return (
-            <input
-                autoComplete={'off'}
+            <textarea
+                autoComplete="off"
+                style={{ textAlign: 'left' }}
                 id="link_description_input"
                 value={StrCast(this.selectedDoc?.description)}
                 onKeyDown={this.onDescriptionKey}
                 onBlur={this.onSelectOutDesc}
                 onChange={e => this.handleDescriptionChange(e.currentTarget.value)}
                 className="text"
-                type="text"
             />
         );
     }
 
     // Converts seconds to ms and updates presTransition
     setZoom = (number: String, change?: number) => {
-        let scale = Number(number) / 100;
-        if (change) scale += change;
-        if (scale < 0.01) scale = 0.01;
-        if (scale > 1) scale = 1;
-        this.destinationAnchor && (this.destinationAnchor.presZoom = scale);
-    };
-    // Converts seconds to ms and updates presTransition
-    setZoomSrc = (number: String, change?: number) => {
         let scale = Number(number) / 100;
         if (change) scale += change;
         if (scale < 0.01) scale = 0.01;
@@ -1551,11 +1543,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     render() {
         const isNovice = Doc.noviceMode;
-        const zoom = Number((NumCast(this.destinationAnchor?.presZoom) * 100).toPrecision(3));
-        const zoomSrc = Number((NumCast(this.sourceAnchor?.presZoom) * 100).toPrecision(3));
-        const targZoom = this.destinationAnchor?.followLinkZoom;
-        const srcZooom = this.sourceAnchor?.followLinkZoom;
-        if (!this.selectedDoc && !this.isPres) {
+        const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3));
+        const targZoom = this.sourceAnchor?.followLinkZoom;
+        const selectedDoc = this.selectedDoc;
+        const indent = 30;
+        if (!selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this.props.width }}>
                     <div className="propertiesView-title" style={{ width: this.props.width }}>
@@ -1564,7 +1556,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 </div>
             );
         } else {
-            if (this.selectedDoc && !this.isPres) {
+            if (selectedDoc && !this.isPres) {
                 return (
                     <div
                         className="propertiesView"
@@ -1581,43 +1573,43 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         {this.contextsSubMenu}
 
                         {this.linksSubMenu}
-                        {!this.selectedDoc || !LinkManager.currentLink ? null : (
+                        {!selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(dv.rootDoc.links).includes(LinkManager.currentLink!)) ? null : (
                             <>
                                 <div className="propertiesView-section">
-                                    <div className="propertiesView-input first">
-                                        <p>Link Relationship</p>
+                                    <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
+                                        <p>Relationship</p>
                                         {this.editRelationship}
                                     </div>
-                                    <div className="propertiesView-input">
+                                    <div className="propertiesView-input" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
                                         <p>Description</p>
                                         {this.editDescription}
                                     </div>
                                 </div>
                                 <div className="propertiesView-section">
-                                    <div className="propertiesView-input inline first">
-                                        <p>Follow Behavior</p>
-                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.selectedDoc.followLinkLocation, 'default')}>
+                                    <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}>
+                                        <p>Follow by</p>
+                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(selectedDoc.followLinkLocation, 'default')}>
                                             <option value="default">Default</option>
-                                            <option value="add:left">Open in new left pane</option>
-                                            <option value="add:right">Open in new right pane</option>
-                                            <option value="replace:left">Replace left tab</option>
-                                            <option value="replace:right">Replace right tab</option>
-                                            <option value="fullScreen">Open full screen</option>
-                                            <option value="add">Open in new tab</option>
-                                            <option value="replace">Replace current tab</option>
-                                            <option value="inPlace">Opin in place</option>
-                                            {this.selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+                                            <option value="add:left">Opening in new left pane</option>
+                                            <option value="add:right">Opening in new right pane</option>
+                                            <option value="replace:left">Replacing left tab</option>
+                                            <option value="replace:right">Replacing right tab</option>
+                                            <option value="fullScreen">Opening full screen</option>
+                                            <option value="add">Opening in new tab</option>
+                                            <option value="replace">Replacing current tab</option>
+                                            <option value="inPlace">Opening in place</option>
+                                            {selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
                                         </select>
                                     </div>
-                                    <div className="propertiesView-input inline first">
-                                        <p>Target Effect</p>
-                                        <select style={{ width: '100%' }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.destinationAnchor?.presEffect, 'default')}>
+                                    <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
+                                        <p>Animation</p>
+                                        <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.presEffect, 'default')}>
                                             <option value="default">Default</option>
                                             {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
                                                 <option value={effect.toString()}>{effect.toString()}</option>
                                             ))}
                                         </select>
-                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40 }}>
+                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, gridColumn: 3 }}>
                                             {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
                                             {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
                                             {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
@@ -1625,20 +1617,27 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             {this.animationDirection(PresEffectDirection.Center, '', 2, 2, { width: 10, height: 10, alignSelf: 'center' })}
                                         </div>
                                     </div>
-                                    {PresBox.inputter('0.1', '0.1', '10', NumCast(this.destinationAnchor?.presTransition) / 1000, true, (val: string) =>
-                                        PresBox.SetTransitionTime(val, (timeInMS: number) => this.destinationAnchor && (this.destinationAnchor.presTransition = timeInMS))
+                                    {PresBox.inputter(
+                                        '0.1',
+                                        '0.1',
+                                        '10',
+                                        NumCast(this.sourceAnchor?.presTransition) / 1000,
+                                        true,
+                                        (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.presTransition = timeInMS)),
+                                        indent
                                     )}{' '}
                                     <div
                                         className={'slider-headers'}
                                         style={{
                                             display: 'grid',
                                             justifyContent: 'space-between',
-                                            width: '100%',
-                                            gridTemplateColumns: 'auto auto auto',
+                                            width: `calc(100% - ${indent * 2}px)`,
+                                            marginLeft: indent,
+                                            marginRight: indent,
+                                            gridTemplateColumns: 'auto auto',
                                             borderTop: 'solid',
                                         }}>
                                         <div className="slider-text">Fast</div>
-                                        <div className="slider-text">Medium</div>
                                         <div className="slider-text">Slow</div>
                                     </div>{' '}
                                     <div className="propertiesView-input inline">
@@ -1652,39 +1651,35 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         </button>
                                     </div>
                                     <div className="propertiesView-input inline">
+                                        <p>Show link</p>
+                                        <button style={{ background: !selectedDoc.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} onPointerDown={e => this.toggleProp(e, 'linkDisplay')} onClick={e => e.stopPropagation()} className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
                                         <p>Auto-move anchor</p>
                                         <button
-                                            style={{ background: !this.selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
+                                            style={{ background: !selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
                                             onPointerDown={e => this.toggleProp(e, 'linkAutoMove')}
                                             onClick={e => e.stopPropagation()}
                                             className="propertiesButton">
                                             <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
                                         </button>
                                     </div>
-                                    <div className="propertiesView-input inline">
-                                        <p>Show link</p>
-                                        <button
-                                            style={{ background: !this.selectedDoc.linkDisplay ? '' : '#4476f7', borderRadius: 3 }}
-                                            onPointerDown={e => this.toggleProp(e, 'linkDisplay')}
-                                            onClick={e => e.stopPropagation()}
-                                            className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline">
+                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
                                         <p>Display arrow</p>
                                         <button
-                                            style={{ background: !this.selectedDoc.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
+                                            style={{ background: !selectedDoc.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
                                             onPointerDown={e => this.toggleProp(e, 'linkDisplayArrow')}
                                             onClick={e => e.stopPropagation()}
                                             className="propertiesButton">
                                             <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
                                         </button>
                                     </div>
-                                    <div className="propertiesView-input inline">
-                                        <p>Zoom to target (% screen filled)</p>
-                                        <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'flex' }}>
-                                            <input className="presBox-input" type="number" value={zoom} />
+                                    <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 128px) 50px' }}>
+                                        <p>Zoom % screen</p>
+                                        <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
+                                            <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
                                             <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
                                                 <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}>
                                                     <FontAwesomeIcon icon={'caret-up'} />
@@ -1695,36 +1690,28 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             </div>
                                         </div>
                                         <button
-                                            style={{ background: !targZoom ? '' : '#4476f7', borderRadius: 3 }}
-                                            onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.destinationAnchor)}
-                                            onClick={e => e.stopPropagation()}
-                                            className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom)}
-                                    <div className="propertiesView-input inline">
-                                        <p>Zoom to source (% screen filled)</p>
-                                        <div className="ribbon-property" style={{ display: !srcZooom ? 'none' : 'flex' }}>
-                                            <input className="presBox-input" type="number" value={zoomSrc} />
-                                            <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
-                                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoomSrc(String(zoomSrc), 0.1))}>
-                                                    <FontAwesomeIcon icon={'caret-up'} />
-                                                </div>
-                                                <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoomSrc(String(zoomSrc), -0.1))}>
-                                                    <FontAwesomeIcon icon={'caret-down'} />
-                                                </div>
-                                            </div>
-                                        </div>
-                                        <button
-                                            style={{ background: !srcZooom ? '' : '#4476f7', borderRadius: 3 }}
+                                            style={{ background: !targZoom ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }}
                                             onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoom', this.sourceAnchor)}
                                             onClick={e => e.stopPropagation()}
                                             className="propertiesButton">
                                             <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
                                         </button>
                                     </div>
-                                    {!srcZooom ? null : PresBox.inputter('0', '1', '100', zoomSrc, true, this.setZoomSrc)}
+                                    {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
+                                    <div
+                                        className={'slider-headers'}
+                                        style={{
+                                            display: !targZoom ? 'none' : 'grid',
+                                            justifyContent: 'space-between',
+                                            width: `calc(100% - ${indent * 2}px)`,
+                                            marginLeft: indent,
+                                            marginRight: indent,
+                                            gridTemplateColumns: 'auto auto',
+                                            borderTop: 'solid',
+                                        }}>
+                                        <div className="slider-text">0%</div>
+                                        <div className="slider-text">100%</div>
+                                    </div>{' '}
                                 </div>
                             </>
                         )}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index a4131d7c0..b39fcbd48 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -230,7 +230,6 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
     }
 
     render() {
-        //setTimeout(action(() => (LinkManager.currentLink = this.props.LinkDocs[0])));
         if (!this.renderData) return null;
 
         const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index ef75fb159..a22999f52 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -31,6 +31,7 @@ import { OverlayView } from '../../OverlayView';
 import { CollectionView } from '../CollectionView';
 import './CollectionSchemaView.scss';
 import { OpenWhere } from '../../nodes/DocumentView';
+import { PinProps } from '../../nodes/trails';
 
 // intialize cell properties
 export interface CellProps {
@@ -48,7 +49,7 @@ export interface CellProps {
     renderDepth: number;
     // called when a button is pressed on the node itself
     addDocTab: (document: Doc, where: OpenWhere) => boolean;
-    pinToPres: (document: Doc) => void;
+    pinToPres: (document: Doc, pinProps: PinProps) => void;
     moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
     isFocused: boolean;
     changeFocusedCellByIndex: (row: number, col: number) => void;
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index 45ad4f86b..16910cc83 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -24,6 +24,7 @@ import '../../../views/DocumentDecorations.scss';
 import { ContextMenu } from '../../ContextMenu';
 import { COLLECTION_BORDER_WIDTH, SCHEMA_DIVIDER_WIDTH } from '../../global/globalCssVariables.scss';
 import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
+import { PinProps } from '../../nodes/trails';
 import { DefaultStyleProvider } from '../../StyleProvider';
 import { CollectionView } from '../CollectionView';
 import {
@@ -87,7 +88,7 @@ export interface SchemaTableProps {
     active: (outsideReaction: boolean | undefined) => boolean | undefined;
     onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
     addDocTab: (document: Doc, where: OpenWhere) => boolean;
-    pinToPres: (document: Doc) => void;
+    pinToPres: (document: Doc, pinProps: PinProps) => void;
     isSelected: (outsideReaction?: boolean) => boolean;
     isFocused: (document: Doc, outsideReaction: boolean) => boolean;
     setFocused: (document: Doc) => void;
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 07c20dae4..01e33708a 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -252,7 +252,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     get followingDropdown() {
         return (
             <div className="linkEditor-followingDropdown">
-                <div className="linkEditor-followingDropdown-label">Follow Behavior:</div>
+                <div className="linkEditor-followingDropdown-label">Follow by:</div>
                 <div className="linkEditor-followingDropdown-dropdown">
                     <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}>
                         {StrCast(this.props.linkDoc.followLinkLocation, 'default')}
@@ -263,28 +263,28 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                             Default
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:left')}>
-                            Always open in new left pane
+                            Always opening in new left pane
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:right')}>
-                            Always open in new right pane
+                            Always opening in new right pane
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:right')}>
-                            Always replace right tab
+                            Always replacing right tab
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:left')}>
-                            Always replace left tab
+                            Always replacing left tab
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('fullScreen')}>
-                            Always open full screen
+                            Always opening full screen
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add')}>
-                            Always open in a new tab
+                            Always opening in a new tab
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace')}>
-                            Replace Tab
+                            Replacing Tab
                         </div>
                         <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('inPlace')}>
-                            In Place
+                            Opening in Place
                         </div>
                         {this.props.linkDoc.linksToAnnotation ? (
                             <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('openExternal')}>
@@ -320,7 +320,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     get effectDropdown() {
         return (
             <div className="linkEditor-followingDropdown">
-                <div className="linkEditor-followingDropdown-label">Transition Effect:</div>
+                <div className="linkEditor-followingDropdown-label">Animation:</div>
                 <div className="linkEditor-followingDropdown-dropdown">
                     <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}>
                         {StrCast(this.destinationAnchor.presEffect, 'default')}
@@ -437,6 +437,8 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 <div
                     className={'slider-headers'}
                     style={{
+                        marginLeft: 30,
+                        marginRight: 30,
                         display: 'grid',
                         justifyContent: 'space-between',
                         width: '100%',
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 0096a58bd..0c46a6d96 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -8,12 +8,13 @@ import { LinkEditor } from './LinkEditor';
 import './LinkMenu.scss';
 import { LinkMenuGroup } from './LinkMenuGroup';
 import React = require('react');
+import { emptyFunction } from '../../../Utils';
 
 interface Props {
     docView: DocumentView;
-    position?: { x?: number; y?: number };
+    style?: { left: number; top: number };
     itemHandler?: (doc: Doc) => void;
-    clearLinkEditor: () => void;
+    clearLinkEditor?: () => void;
 }
 
 /**
@@ -25,26 +26,24 @@ export class LinkMenu extends React.Component<Props> {
     @observable _editingLink?: Doc;
     @observable _linkMenuRef = React.createRef<HTMLDivElement>();
 
-    @computed get position() {
-        return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds());
-    }
-
-    clear = action(() => {
-        this.props.clearLinkEditor();
-        this._editingLink = undefined;
-    });
+    clear = !this.props.clearLinkEditor
+        ? undefined
+        : action(() => {
+              this.props.clearLinkEditor?.();
+              this._editingLink = undefined;
+          });
 
     componentDidMount() {
-        document.addEventListener('pointerdown', this.onPointerDown, true);
+        this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true);
     }
     componentWillUnmount() {
-        document.removeEventListener('pointerdown', this.onPointerDown, true);
+        this.props.clearLinkEditor && document.removeEventListener('pointerdown', this.onPointerDown, true);
     }
 
     onPointerDown = action((e: PointerEvent) => {
         LinkDocPreview.Clear();
         if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) {
-            this.clear();
+            this.clear?.();
         }
     });
 
@@ -67,13 +66,15 @@ export class LinkMenu extends React.Component<Props> {
             />
         ));
 
-        return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
+        return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
     };
 
     render() {
         const sourceDoc = this.props.docView.props.Document;
+        const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds());
+
         return (
-            <div className="linkMenu" ref={this._linkMenuRef} style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }}>
+            <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}>
                 {this._editingLink ? (
                     <div className="linkMenu-listEditor">
                         <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} />
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index fa6a2f506..9d2082e21 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -1,19 +1,19 @@
-import { observer } from "mobx-react";
-import { observable, action } from "mobx";
-import { Doc, StrListCast } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { Cast } from "../../../fields/Types";
-import { LinkManager } from "../../util/LinkManager";
-import { DocumentView } from "../nodes/DocumentView";
+import { observer } from 'mobx-react';
+import { observable, action } from 'mobx';
+import { Doc, StrListCast } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { Cast } from '../../../fields/Types';
+import { LinkManager } from '../../util/LinkManager';
+import { DocumentView } from '../nodes/DocumentView';
 import './LinkMenu.scss';
-import { LinkMenuItem } from "./LinkMenuItem";
-import React = require("react");
+import { LinkMenuItem } from './LinkMenuItem';
+import React = require('react');
 
 interface LinkMenuGroupProps {
     sourceDoc: Doc;
     group: Doc[];
     groupType: string;
-    clearLinkEditor: () => void;
+    clearLinkEditor?: () => void;
     showEditor: (linkDoc: Doc) => void;
     docView: DocumentView;
     itemHandler?: (doc: Doc) => void;
@@ -26,49 +26,52 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
     getBackgroundColor = (): string => {
         const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
         const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
-        let color = "white";
+        let color = 'white';
         // if this link's relationship property is not default "link", set its color
         if (linkRelationshipList) {
             const relationshipIndex = linkRelationshipList.indexOf(this.props.groupType);
             const RGBcolor: string = linkColorList[relationshipIndex];
             if (RGBcolor) {
                 //set opacity to 0.25 by modifiying the rgb string
-                color = RGBcolor.slice(0, RGBcolor.length - 1) + ", 0.25)";
+                color = RGBcolor.slice(0, RGBcolor.length - 1) + ', 0.25)';
             }
         }
         return color;
-    }
+    };
 
     @observable _collapsed = false;
 
     render() {
         const set = new Set<Doc>(this.props.group);
         const groupItems = Array.from(set.keys()).map(linkDoc => {
-            const destination = LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) ||
+            const destination =
+                LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) ||
                 LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
             if (destination && this.props.sourceDoc) {
-                return <LinkMenuItem key={linkDoc[Id]}
-                    itemHandler={this.props.itemHandler}
-                    groupType={this.props.groupType}
-                    docView={this.props.docView}
-                    linkDoc={linkDoc}
-                    sourceDoc={this.props.sourceDoc}
-                    destinationDoc={destination}
-                    clearLinkEditor={this.props.clearLinkEditor}
-                    showEditor={this.props.showEditor}
-                    menuRef={this._menuRef} />;
+                return (
+                    <LinkMenuItem
+                        key={linkDoc[Id]}
+                        itemHandler={this.props.itemHandler}
+                        groupType={this.props.groupType}
+                        docView={this.props.docView}
+                        linkDoc={linkDoc}
+                        sourceDoc={this.props.sourceDoc}
+                        destinationDoc={destination}
+                        clearLinkEditor={this.props.clearLinkEditor}
+                        showEditor={this.props.showEditor}
+                        menuRef={this._menuRef}
+                    />
+                );
             }
         });
 
         return (
             <div className="linkMenu-group" ref={this._menuRef}>
-                <div className="linkMenu-group-name" onClick={action(() => this._collapsed = !this._collapsed)} style={{ background: this.getBackgroundColor() }}>
-                    <p className={this.props.groupType === "*" || this.props.groupType === "" ? "" : "expand-one"}> {this.props.groupType}:</p>
+                <div className="linkMenu-group-name" onClick={action(() => (this._collapsed = !this._collapsed))} style={{ background: this.getBackgroundColor() }}>
+                    <p className={this.props.groupType === '*' || this.props.groupType === '' ? '' : 'expand-one'}> {this.props.groupType}:</p>
                 </div>
-                {this._collapsed ? (null) : <div className="linkMenu-group-wrapper">
-                    {groupItems}
-                </div>}
-            </div >
+                {this._collapsed ? null : <div className="linkMenu-group-wrapper">{groupItems}</div>}
+            </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenuItem.scss b/src/client/views/linking/LinkMenuItem.scss
index 2ca97a27d..806a2c381 100644
--- a/src/client/views/linking/LinkMenuItem.scss
+++ b/src/client/views/linking/LinkMenuItem.scss
@@ -79,7 +79,6 @@
                 font-size: 9px;
                 line-height: 0.9;
                 margin-left: 20px;
-                max-width: 125px;
                 height: auto;
                 white-space: break-spaces;
             }
@@ -110,7 +109,7 @@
         background-color: rgb(201, 239, 252);
 
         .linkMenu-item-buttons {
-            display: flex;
+            opacity: 1;
         }
 
         .linkMenu-item-content {
@@ -127,7 +126,8 @@
 .linkMenu-item-buttons {
     //@extend: right;
     position: relative;
-    display: none;
+    display: flex;
+    opacity: 0;
 
     .button {
         width: 20px;
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index c3b5fa997..c3705b0e1 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -12,13 +12,12 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
+import { SettingsManager } from '../../util/SettingsManager';
+import { undoBatch } from '../../util/UndoManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
 import React = require('react');
-import { SettingsManager } from '../../util/SettingsManager';
-import { SelectionManager } from '../../util/SelectionManager';
-import { undoBatch } from '../../util/UndoManager';
 
 interface LinkMenuItemProps {
     groupType: string;
@@ -26,7 +25,7 @@ interface LinkMenuItemProps {
     docView: DocumentView;
     sourceDoc: Doc;
     destinationDoc: Doc;
-    clearLinkEditor: () => void;
+    clearLinkEditor?: () => void;
     showEditor: (linkDoc: Doc) => void;
     menuRef: React.Ref<HTMLDivElement>;
     itemHandler?: (doc: Doc) => void;
@@ -71,7 +70,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     private _drag = React.createRef<HTMLDivElement>();
 
     _editRef = React.createRef<HTMLDivElement>();
-    _buttonRef = React.createRef<HTMLDivElement>();
 
     @observable private _showMore: boolean = false;
     @action toggleShowMore(e: React.PointerEvent) {
@@ -89,7 +87,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
     }
     @action
     onEdit = (e: React.PointerEvent) => {
-        const sel = SelectionManager.Views();
         LinkManager.currentLink = this.props.linkDoc === LinkManager.currentLink ? undefined : this.props.linkDoc;
         LinkManager.currentLinkAnchor = this.sourceAnchor;
         setupMoveUpEvents(
@@ -119,12 +116,12 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                 const eleClone: any = this._drag.current!.cloneNode(true);
                 eleClone.style.transform = `translate(${e.x}px, ${e.y}px)`;
                 StartLinkTargetsDrag(eleClone, this.props.docView, e.x, e.y, this.props.sourceDoc, [this.props.linkDoc]);
-                this.props.clearLinkEditor();
+                this.props.clearLinkEditor?.();
                 return true;
             },
             emptyFunction,
             () => {
-                this.props.clearLinkEditor();
+                this.props.clearLinkEditor?.();
                 if (this.props.itemHandler) {
                     this.props.itemHandler?.(this.props.linkDoc);
                 } else {
@@ -167,20 +164,29 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                     <div
                         ref={this._drag}
                         className="linkMenu-name" //title="drag to view target. click to customize."
-                        onPointerLeave={LinkDocPreview.Clear}
-                        onPointerEnter={e =>
-                            this.props.linkDoc &&
-                            LinkDocPreview.SetLinkInfo({
-                                docProps: this.props.docView.props,
-                                linkSrc: this.props.sourceDoc,
-                                linkDoc: this.props.linkDoc,
-                                showHeader: false,
-                                location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
-                                noPreview: false,
-                            })
-                        }
                         onPointerDown={this.onLinkButtonDown}>
-                        <div className="linkMenu-text">
+                        <div className="linkMenu-item-buttons">
+                            <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
+                                <div className="button" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'black' : 'gray' }} ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
+                                    <FontAwesomeIcon className="fa-icon" icon="edit" size="sm" />
+                                </div>
+                            </Tooltip>
+                        </div>
+                        <div
+                            className="linkMenu-text"
+                            onPointerLeave={LinkDocPreview.Clear}
+                            onPointerEnter={e =>
+                                this.props.linkDoc &&
+                                this.props.clearLinkEditor &&
+                                LinkDocPreview.SetLinkInfo({
+                                    docProps: this.props.docView.props,
+                                    linkSrc: this.props.sourceDoc,
+                                    linkDoc: this.props.linkDoc,
+                                    showHeader: false,
+                                    location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
+                                    noPreview: false,
+                                })
+                            }>
                             {source ? (
                                 <p className="linkMenu-source-title">
                                     {' '}
@@ -198,14 +204,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                             {!this.props.linkDoc.description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.description)}</p>}
                         </div>
 
-                        <div className="linkMenu-item-buttons" ref={this._buttonRef}>
-                            <Tooltip title={<div className="dash-tooltip">Edit Link</div>}>
-                                <div className="button" ref={this._editRef} onPointerDown={this.onEdit} onClick={e => e.stopPropagation()}>
-                                    <FontAwesomeIcon className="fa-icon" icon="chevron-down" size="sm" />
-                                </div>
-                            </Tooltip>
+                        <div className="linkMenu-item-buttons">
                             <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
-                                <div className="button" style={{ background: 'red' }} ref={this._editRef} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
+                                <div className="button" style={{ background: 'red' }} onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
                                     <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
                                 </div>
                             </Tooltip>
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index b326865b3..097035661 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -12,12 +12,11 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
+import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
-import { undoBatch } from '../../util/UndoManager';
 import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
-import { LinkEditor } from '../linking/LinkEditor';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
@@ -123,17 +122,21 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
         }
         return undefined;
     }
-    @observable _showEditor = false;
 
     @action
     editLink = (e: React.PointerEvent): void => {
-        LinkManager.currentLink = this.props.linkDoc;
         setupMoveUpEvents(
             this,
             e,
             returnFalse,
             emptyFunction,
-            action(() => (this._showEditor = !this._showEditor))
+            action(() => {
+                LinkManager.currentLink = this._linkDoc === LinkManager.currentLink ? undefined : this._linkDoc;
+                LinkManager.currentLinkAnchor = this._linkSrc;
+                if ((SettingsManager.propertiesWidth ?? 0) < 100) {
+                    SettingsManager.propertiesWidth = 250;
+                }
+            })
         );
     };
     nextHref = (e: React.PointerEvent) => {
@@ -181,22 +184,23 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
     @computed get previewHeader() {
         return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : (
             <div className="linkDocPreview-info">
+                <div className="linkDocPreview-buttonBar" style={{ float: 'left' }}>
+                    <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
+                        <div className="linkDocPreview-button" onPointerDown={this.editLink}>
+                            <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" />
+                        </div>
+                    </Tooltip>
+                </div>
                 <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}>
                     {StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)}
                     <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p>
                 </div>
-                <div className="linkDocPreview-buttonBar">
+                <div className="linkDocPreview-buttonBar" style={{ float: 'right' }}>
                     <Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top">
                         <div className="linkDocPreview-button" style={{ background: (this.props.hrefs?.length || 0) <= 1 ? 'gray' : 'green' }} onPointerDown={this.nextHref}>
                             <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="chevron-right" color="white" size="sm" />
                         </div>
                     </Tooltip>
-
-                    <Tooltip title={<div className="dash-tooltip">Edit Link</div>} placement="top">
-                        <div className="linkDocPreview-button" onPointerDown={this.editLink}>
-                            <FontAwesomeIcon className="linkDocPreview-fa-icon" icon="edit" color="white" size="sm" />
-                        </div>
-                    </Tooltip>
                 </div>
             </div>
         );
@@ -280,9 +284,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                 className="linkDocPreview"
                 ref={this._linkDocRef}
                 onPointerDown={this.followLinkPointerDown}
-                style={{ left: this.props.location[0], top: this.props.location[1], width: this._showEditor ? 'auto' : this.width() + borders, height: this._showEditor ? 'max-content' : this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
-                {this._showEditor ? null : this.docPreview}
-                {!this._showEditor || !this._linkSrc || !this._linkDoc ? null : <LinkEditor sourceDoc={this._linkSrc} linkDoc={this._linkDoc} showLinks={action(() => (this._showEditor = !this._showEditor))} />}
+                style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}>
+                {this.docPreview}
             </div>
         );
     }
diff --git a/src/client/views/nodes/LoadingBox.scss b/src/client/views/nodes/LoadingBox.scss
index d63ed2575..4c3b8dabe 100644
--- a/src/client/views/nodes/LoadingBox.scss
+++ b/src/client/views/nodes/LoadingBox.scss
@@ -6,6 +6,13 @@
     background-color: #fdfdfd;
     height: 100%;
     align-items: center;
+    .textContainer,
+    .text {
+        overflow: hidden;
+        text-overflow: ellipsis;
+        max-width: 80%;
+        text-align: center;
+    }
 }
 
 .textContainer {
@@ -17,14 +24,6 @@
     align-content: center;
 }
 
-.textContainer,
-.text {
-    overflow: hidden;
-    text-overflow: ellipsis;
-    max-width: 80%;
-    text-align: center;
-}
-
 .headerText {
     text-align: center;
     font-weight: bold;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 1cb6bc3a2..c04b79a1e 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1199,7 +1199,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     _batch: UndoManager.Batch | undefined = undefined;
 
-    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => {
+    public static inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void, hmargin?:number) => {
         let batch: any;
         return (
             <input
@@ -1208,6 +1208,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 min={min}
                 max={max}
                 value={value}
+                style={{marginLeft: hmargin, marginRight:hmargin, width: `calc(100% - ${2*(hmargin??0)}px)`}}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
                     batch = UndoManager.StartBatch('pres slider');
-- 
cgit v1.2.3-70-g09d2


From ea5c60384399e29f7b34e27e2607320134bc5432 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 6 Dec 2022 14:08:12 -0500
Subject: added long press to select documents.  fixed/reactivated link
 size/colors.  fixed stackedTimelineItems so that they don't get pointer
 events until container content is active.

---
 src/client/views/PropertiesView.tsx                | 14 +++++----
 src/client/views/StyleProvider.tsx                 |  4 +--
 .../collections/CollectionStackedTimeline.tsx      | 33 ++++------------------
 .../CollectionFreeFormLinkView.tsx                 |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 32 ++++++++++++++++-----
 src/client/views/nodes/LinkDocPreview.tsx          |  4 ++-
 6 files changed, 45 insertions(+), 44 deletions(-)

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

diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index bc08e920a..905f9e2d0 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1332,8 +1332,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         );
     }
 
-    @observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
-    @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
+    @computed get description() {
+        return Field.toString(LinkManager.currentLink?.description as any as Field);
+    }
+    @computed get relationship() {
+        return StrCast(LinkManager.currentLink?.linkRelationship);
+    }
     @observable private relationshipButtonColor: string = '';
 
     // @action
@@ -1343,8 +1347,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @undoBatch
     handleDescriptionChange = action((value: string) => {
         if (LinkManager.currentLink && this.selectedDoc) {
-            this.selectedDoc.description = value;
-            this.description = value;
+            this.setDescripValue(value);
             return true;
         }
     });
@@ -1352,8 +1355,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @undoBatch
     handleLinkRelationshipChange = action((value: string) => {
         if (LinkManager.currentLink && this.selectedDoc) {
-            this.selectedDoc.linkRelationship = value;
-            this.relationship = value;
+            this.setLinkRelationshipValue(value);
             return true;
         }
     });
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index dc7a4e047..b77f01250 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, runInAction } from 'mobx';
 import { extname } from 'path';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
 import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
@@ -252,7 +252,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.BoxShadow: {
             if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
             if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
-            if (doc?.isLinkButton && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
+            if (doc?.isLinkButton && DocListCast(doc?.links).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
             switch (doc?.type) {
                 case DocumentType.COL:
                     return StrCast(
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 39ae470b6..e9bf03208 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -8,6 +8,7 @@ import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
 import { emptyFunction, formatTime, OmitKeys, returnFalse, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
@@ -23,13 +24,10 @@ import { AudioWaveform } from '../AudioWaveform';
 import { CollectionSubView } from '../collections/CollectionSubView';
 import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
-import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, DocumentViewSharedProps } from '../nodes/DocumentView';
+import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { LabelBox } from '../nodes/LabelBox';
-import './CollectionStackedTimeline.scss';
 import { VideoBox } from '../nodes/VideoBox';
-import { ImageField } from '../../../fields/URLField';
-import { StyleProp } from '../StyleProvider';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import './CollectionStackedTimeline.scss';
 
 export type CollectionStackedTimelineProps = {
     Play: () => void;
@@ -610,7 +608,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                         height={height}
                                         toTimeline={this.toTimeline}
                                         layoutDoc={this.layoutDoc}
-                                        // isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
+                                        isDocumentActive={this.isContentActive}
                                         currentTimecode={this.currentTimecode}
                                         _timeline={this._timeline}
                                         stackedTimeline={this}
@@ -702,7 +700,7 @@ interface StackedTimelineAnchorProps {
     endTag: string;
     renderDepth: number;
     layoutDoc: Doc;
-    isDocumentActive?: () => boolean;
+    isDocumentActive?: () => boolean | undefined;
     ScreenToLocalTransform: () => Transform;
     _timeline: HTMLDivElement | null;
     focus: DocFocusFunc;
@@ -806,25 +804,6 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
         return [resetTitle];
     };
 
-    innerStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
-        if (property === StyleProp.Decorations && doc && NumCast(doc.timecodeToHide) - NumCast(doc.timecodeToShow) < 0.0002) {
-            return (
-                <div className="styleProvider-lock">
-                    <FontAwesomeIcon
-                        icon={'camera'}
-                        style={{ color: 'red' }}
-                        onClick={e => {
-                            LinkFollower.FollowLink(undefined, doc, props as DocumentViewSharedProps, e.altKey);
-                            e.stopPropagation();
-                        }}
-                        size="lg"
-                    />
-                </div>
-            );
-        }
-        return this.props.styleProvider?.(doc, props, property);
-    };
-
     // renders anchor LabelBox
     renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
         const anchor = observable({ view: undefined as any });
@@ -841,7 +820,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                     ref={action((r: DocumentView | null) => (anchor.view = r))}
                     Document={mark}
                     DataDoc={undefined}
-                    styleProvider={this.innerStyleProvider}
+                    styleProvider={this.props.styleProvider}
                     renderDepth={this.props.renderDepth + 1}
                     LayoutTemplate={undefined}
                     LayoutTemplateString={LabelBox.LayoutStringWithTitle('data', this.computeTitle())}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index c57810a98..b85da2ce7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -241,7 +241,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
 
         const link = this.props.LinkDocs[0];
         const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData;
-        const linkRelationship = Field.toString(LinkManager.currentLink?.linkRelationship as any as Field); //get string representing relationship
+        const linkRelationship = Field.toString(link?.linkRelationship as any as Field); //get string representing relationship
         const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>;
         const linkColorList = Doc.UserDoc().linkColorList as List<string>;
         const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 4abfef563..778965553 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -239,9 +239,8 @@ export interface DocumentViewInternalProps extends DocumentViewProps {
 @observer
 export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps>() {
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
-    @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views.  defaults to 300 if not uset
-    @observable _animateScalingTo = 0;
-    @observable _pendingDoubleClick = false;
+    private _cursorTimer: NodeJS.Timeout | undefined;
+    private _longPress = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _downX: number = 0;
     private _downY: number = 0;
@@ -255,7 +254,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _holdDisposer?: InteractionUtils.MultiTouchEventDisposer;
     protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
+
     @observable _componentView: Opt<DocComponentView>; // needs to be accessed from DocumentView wrapper class
+    @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views.  defaults to 300 if not uset
+    @observable _animateScalingTo = 0;
+    @observable _pendingDoubleClick = false;
+    @observable _cursorPress = false;
 
     private get topMost() {
         return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
@@ -623,7 +627,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
-            } else if (this.onClickHandler?.script && !isScriptBox()) {
+            } else if (!this._longPress && this.onClickHandler?.script && !isScriptBox()) {
                 // bcz: hack? don't execute script if you're clicking on a scripting box itself
                 const { clientX, clientY, shiftKey, altKey } = e;
                 const func = () =>
@@ -652,12 +656,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         clickFunc();
                     }, 350);
                 } else clickFunc();
-            } else if (this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+            } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 SelectionManager.DeselectAll();
                 this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
             } else {
                 if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
-                    // onDragStart implies a button doc that we don't want to select when clicking.   RootDocument & isTemplaetForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
+                    // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part
                     stopPropagate = false; // don't stop propagation for field templates -- want the selection to propagate up to the root document of the template
                 } else {
                     runInAction(() => (this._pendingDoubleClick = true));
@@ -688,6 +692,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             }
             return;
         }
+        this._cursorTimer = setTimeout(
+            action(() => {
+                this._cursorPress = true;
+                this.props.select(false);
+            }),
+            1000 // long press required duration
+        );
         this._downX = e.clientX;
         this._downY = e.clientY;
         if ((Doc.ActiveTool === InkTool.None || this.props.addDocTab === returnFalse) && !(this.props.Document.rootDocument && !(e.ctrlKey || e.button > 0))) {
@@ -713,6 +724,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     };
 
+    @action
     onPointerMove = (e: PointerEvent): void => {
         if (e.cancelBubble) return;
         if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) return;
@@ -722,6 +734,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 if (!e.altKey && (!this.topMost || this.layoutDoc.onDragStart || this.onClickHandler) && (e.buttons === 1 || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE))) {
                     document.removeEventListener('pointermove', this.onPointerMove);
                     document.removeEventListener('pointerup', this.onPointerUp);
+                    this._cursorTimer && clearTimeout(this._cursorTimer);
+                    this._cursorPress = false;
                     this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.props.dropAction || this.Document.dropAction || undefined) as dropActionType));
                 }
             }
@@ -736,8 +750,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         document.removeEventListener('pointerup', this.onPointerUp);
     };
 
+    @action
     onPointerUp = (e: PointerEvent): void => {
         this.cleanupPointerEvents();
+        this._longPress = this._cursorPress;
+        this._cursorTimer && clearTimeout(this._cursorTimer);
+        this._cursorPress = false;
 
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
@@ -1395,7 +1413,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         ...style,
                         background: isButton || thumb ? undefined : this.backgroundColor,
                         opacity: this.opacity,
-                        cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
+                        cursor: this._cursorPress ? 'wait' : Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair',
                         color: StrCast(this.layoutDoc.color, 'inherit'),
                         fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
                         fontSize: Cast(this.Document._fontSize, 'string', null),
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 097035661..b2fdd93fc 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -17,6 +17,7 @@ import { Transform } from '../../util/Transform';
 import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
+import { SelectionManager } from '../../util/SelectionManager';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
@@ -131,8 +132,9 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
             returnFalse,
             emptyFunction,
             action(() => {
-                LinkManager.currentLink = this._linkDoc === LinkManager.currentLink ? undefined : this._linkDoc;
+                LinkManager.currentLink = this._linkDoc;
                 LinkManager.currentLinkAnchor = this._linkSrc;
+                this.props.docProps.DocumentView?.().select(false);
                 if ((SettingsManager.propertiesWidth ?? 0) < 100) {
                     SettingsManager.propertiesWidth = 250;
                 }
-- 
cgit v1.2.3-70-g09d2


From d5f796b433d7e72130d4109a3775347ccb10c454 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 17 Jan 2023 22:34:32 -0500
Subject: fixed linkint to trail to follow trail immediately in lightbox and
 show trail ui in minimized mode.  fixed overlay of pres box to not disappear
 when lightbox appears.   closing /ending trail hackily restores collecftion
 to prior pan/zoom.

---
 src/client/util/LinkFollower.ts                    |  19 +-
 src/client/views/MainView.tsx                      |  14 +-
 src/client/views/OverlayView.tsx                   | 136 ++++++-----
 .../CollectionFreeFormLinkView.tsx                 |   6 +-
 src/client/views/nodes/DocumentView.tsx            |   2 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   3 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   1 +
 src/client/views/nodes/trails/PresBox.scss         |  26 +-
 src/client/views/nodes/trails/PresBox.tsx          | 267 +++++++++++++--------
 src/client/views/nodes/trails/PresElementBox.tsx   |  12 +-
 src/fields/FieldLoader.scss                        |   9 +-
 src/fields/FieldLoader.tsx                         |  14 +-
 12 files changed, 298 insertions(+), 211 deletions(-)

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

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 807b54cd5..0f216e349 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -2,11 +2,14 @@ import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { PresBox } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
+import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
 
 type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
@@ -116,6 +119,20 @@ export class LinkFollower {
                         LightboxView.SetLightboxDoc(currentContext, undefined, tour);
                         setTimeout(LightboxView.Next);
                         allFinished();
+                    } else if (target.type === DocumentType.PRES) {
+                        const containerAnnoDoc = Cast(sourceDoc, Doc, null);
+                        const containerDoc = containerAnnoDoc || sourceDoc;
+                        var containerDocContext = containerDoc?.context ? [Cast(await containerDoc?.context, Doc, null)] : ([] as Doc[]);
+                        while (containerDocContext.length && containerDocContext[0]?.context && DocCast(containerDocContext[0].context)?.viewType !== CollectionViewType.Docking) {
+                            containerDocContext = [Cast(await containerDocContext[0].context, Doc, null), ...containerDocContext];
+                        }
+                        if (!DocumentManager.Instance.getDocumentView(containerDocContext[0])) {
+                            CollectionDockingView.AddSplit(containerDocContext[0], OpenWhereMod.right);
+                        }
+                        SelectionManager.DeselectAll();
+                        DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && (dv.ComponentView as PresBox).PlayTrail(containerDocContext[0]));
+                        PresBox.OpenPresMinimized(target, [0, 0]);
+                        finished?.();
                     } else {
                         const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
                         const containerDoc = containerAnnoDoc || target;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 39cc9ba8e..4494166f2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -574,7 +574,7 @@ export class MainView extends React.Component {
                     Document={this.headerBarDoc}
                     DataDoc={undefined}
                     addDocument={undefined}
-                    addDocTab={this.addDocTabFunc}
+                    addDocTab={MainView.addDocTabFunc}
                     pinToPres={emptyFunction}
                     docViewPath={returnEmptyDoclist}
                     styleProvider={DefaultStyleProvider}
@@ -611,7 +611,7 @@ export class MainView extends React.Component {
                     Document={this.mainContainer!}
                     DataDoc={undefined}
                     addDocument={undefined}
-                    addDocTab={this.addDocTabFunc}
+                    addDocTab={MainView.addDocTabFunc}
                     pinToPres={emptyFunction}
                     docViewPath={returnEmptyDoclist}
                     styleProvider={this._hideUI ? DefaultStyleProvider : undefined}
@@ -682,7 +682,7 @@ export class MainView extends React.Component {
 
     sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1);
     mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0);
-    addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => {
+    static addDocTabFunc = (doc: Doc, location: OpenWhere): boolean => {
         const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
         const whereMods = whereFields.length > 1 ? (whereFields[1] as OpenWhereMod) : OpenWhereMod.none;
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
@@ -710,7 +710,7 @@ export class MainView extends React.Component {
                         Document={this._sidebarContent.proto || this._sidebarContent}
                         DataDoc={undefined}
                         addDocument={undefined}
-                        addDocTab={this.addDocTabFunc}
+                        addDocTab={MainView.addDocTabFunc}
                         pinToPres={emptyFunction}
                         docViewPath={returnEmptyDoclist}
                         styleProvider={this._sidebarContent.proto === Doc.MyDashboards || this._sidebarContent.proto === Doc.MyFilesystem ? DashboardStyleProvider : DefaultStyleProvider}
@@ -744,7 +744,7 @@ export class MainView extends React.Component {
                     Document={Doc.MyLeftSidebarMenu}
                     DataDoc={undefined}
                     addDocument={undefined}
-                    addDocTab={this.addDocTabFunc}
+                    addDocTab={MainView.addDocTabFunc}
                     pinToPres={emptyFunction}
                     rootSelected={returnTrue}
                     removeDocument={returnFalse}
@@ -808,7 +808,7 @@ export class MainView extends React.Component {
                             </div>
                         )}
                         <div className="properties-container" style={{ width: this.propertiesWidth() }}>
-                            {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={this.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
+                            {this.propertiesWidth() < 10 ? null : <PropertiesView styleProvider={DefaultStyleProvider} addDocTab={MainView.addDocTabFunc} width={this.propertiesWidth()} height={this.propertiesHeight()} />}
                         </div>
                     </div>
                 </div>
@@ -888,7 +888,7 @@ export class MainView extends React.Component {
                     moveDocument={this.moveButtonDoc}
                     CollectionView={undefined}
                     addDocument={this.addButtonDoc}
-                    addDocTab={this.addDocTabFunc}
+                    addDocTab={MainView.addDocTabFunc}
                     pinToPres={emptyFunction}
                     removeDocument={this.remButtonDoc}
                     ScreenToLocalTransform={this.buttonBarXf}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index 6d7f2c037..08285ff0c 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, observable } from 'mobx';
+import { action, computed, observable, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
@@ -8,15 +8,17 @@ import { Id } from '../../fields/FieldSymbols';
 import { NumCast } from '../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
+import { DocumentType } from '../documents/DocumentTypes';
 import { DragManager } from '../util/DragManager';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
 import { Transform } from '../util/Transform';
 import { CollectionFreeFormLinksView } from './collections/collectionFreeForm/CollectionFreeFormLinksView';
 import { LightboxView } from './LightboxView';
+import { MainView } from './MainView';
 import { DocumentView } from './nodes/DocumentView';
 import './OverlayView.scss';
 import { ScriptingRepl } from './ScriptingRepl';
-import { DefaultStyleProvider } from './StyleProvider';
+import { DefaultStyleProvider, testDocProps } from './StyleProvider';
 
 export type OverlayDisposer = () => void;
 
@@ -170,74 +172,70 @@ export class OverlayView extends React.Component {
     );
 
     @computed get overlayDocs() {
-        return LightboxView.LightboxDoc
-            ? null
-            : DocListCast(Doc.MyOverlayDocs?.data).map(d => {
-                  let offsetx = 0,
-                      offsety = 0;
-                  const dref = React.createRef<HTMLDivElement>();
-                  const onPointerMove = action((e: PointerEvent, down: number[]) => {
-                      if (e.buttons === 1) {
-                          d.overlayX = e.clientX + offsetx;
-                          d.overlayY = e.clientY + offsety;
-                      }
-                      if (e.metaKey) {
-                          const dragData = new DragManager.DocumentDragData([d]);
-                          dragData.offset = [-offsetx, -offsety];
-                          dragData.dropAction = 'move';
-                          dragData.removeDocument = (doc: Doc | Doc[]) => {
-                              const docs = doc instanceof Doc ? [doc] : doc;
-                              docs.forEach(d => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, d));
-                              return true;
-                          };
-                          dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
-                              return dragData.removeDocument!(doc) ? addDocument(doc) : false;
-                          };
-                          DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
-                          return true;
-                      }
-                      return false;
-                  });
+        return DocListCast(Doc.MyOverlayDocs?.data)
+            .filter(d => !LightboxView.LightboxDoc || d.type === DocumentType.PRES)
+            .map(d => {
+                let offsetx = 0,
+                    offsety = 0;
+                const dref = React.createRef<HTMLDivElement>();
+                const onPointerMove = action((e: PointerEvent, down: number[]) => {
+                    if (e.buttons === 1) {
+                        d.overlayX = e.clientX + offsetx;
+                        d.overlayY = e.clientY + offsety;
+                    }
+                    if (e.metaKey) {
+                        const dragData = new DragManager.DocumentDragData([d]);
+                        dragData.offset = [-offsetx, -offsety];
+                        dragData.dropAction = 'move';
+                        dragData.removeDocument = this.removeOverlayDoc;
+                        dragData.moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean): boolean => {
+                            return dragData.removeDocument!(doc) ? addDocument(doc) : false;
+                        };
+                        DragManager.StartDocumentDrag([dref.current!], dragData, down[0], down[1]);
+                        return true;
+                    }
+                    return false;
+                });
 
-                  const onPointerDown = (e: React.PointerEvent) => {
-                      setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
-                      offsetx = NumCast(d.overlayX) - e.clientX;
-                      offsety = NumCast(d.overlayY) - e.clientY;
-                  };
-                  return (
-                      <div
-                          className="overlayView-doc"
-                          ref={dref}
-                          key={d[Id]}
-                          onPointerDown={onPointerDown}
-                          style={{ top: d.type === 'presentation' ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}>
-                          <DocumentView
-                              Document={d}
-                              rootSelected={returnTrue}
-                              bringToFront={emptyFunction}
-                              addDocument={undefined}
-                              removeDocument={this.removeOverlayDoc}
-                              PanelWidth={d[WidthSym]}
-                              PanelHeight={d[HeightSym]}
-                              ScreenToLocalTransform={this.docScreenToLocalXf(d)}
-                              renderDepth={1}
-                              isDocumentActive={returnTrue}
-                              isContentActive={returnTrue}
-                              whenChildContentsActiveChanged={emptyFunction}
-                              focus={DocUtils.DefaultFocus}
-                              styleProvider={DefaultStyleProvider}
-                              docViewPath={returnEmptyDoclist}
-                              addDocTab={returnFalse}
-                              pinToPres={emptyFunction}
-                              docFilters={returnEmptyFilter}
-                              docRangeFilters={returnEmptyFilter}
-                              searchFilterDocs={returnEmptyDoclist}
-                              ContainingCollectionView={undefined}
-                              ContainingCollectionDoc={undefined}
-                          />
-                      </div>
-                  );
-              });
+                const onPointerDown = (e: React.PointerEvent) => {
+                    setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction);
+                    offsetx = NumCast(d.overlayX) - e.clientX;
+                    offsety = NumCast(d.overlayY) - e.clientY;
+                };
+                return (
+                    <div
+                        className="overlayView-doc"
+                        ref={dref}
+                        key={d[Id]}
+                        onPointerDown={onPointerDown}
+                        style={{ top: d.type === DocumentType.PRES ? 0 : undefined, width: NumCast(d._width), height: NumCast(d._height), transform: `translate(${d.overlayX}px, ${d.overlayY}px)` }}>
+                        <DocumentView
+                            Document={d}
+                            rootSelected={returnTrue}
+                            bringToFront={emptyFunction}
+                            addDocument={undefined}
+                            removeDocument={this.removeOverlayDoc}
+                            PanelWidth={d[WidthSym]}
+                            PanelHeight={d[HeightSym]}
+                            ScreenToLocalTransform={this.docScreenToLocalXf(d)}
+                            renderDepth={1}
+                            isDocumentActive={returnTrue}
+                            isContentActive={returnTrue}
+                            whenChildContentsActiveChanged={emptyFunction}
+                            focus={DocUtils.DefaultFocus}
+                            styleProvider={DefaultStyleProvider}
+                            docViewPath={returnEmptyDoclist}
+                            addDocTab={d.type === DocumentType.PRES ? MainView.addDocTabFunc : returnFalse}
+                            pinToPres={emptyFunction}
+                            docFilters={returnEmptyFilter}
+                            docRangeFilters={returnEmptyFilter}
+                            searchFilterDocs={returnEmptyDoclist}
+                            ContainingCollectionView={undefined}
+                            ContainingCollectionDoc={undefined}
+                        />
+                    </div>
+                );
+            });
     }
 
     public static ShowSpinner() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 4c8c65707..8919b1c01 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -204,10 +204,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
         const bclipped = bleft !== b.left || btop !== b.top;
         if (aclipped && bclipped) return undefined;
         const clipped = aclipped || bclipped;
+        const pt1inside = NumCast(LinkDocs[0].anchor1_x) % 100 !== 0 && NumCast(LinkDocs[0].anchor1_y) % 100 !== 0;
+        const pt2inside = NumCast(LinkDocs[0].anchor2_x) % 100 !== 0 && NumCast(LinkDocs[0].anchor2_y) % 100 !== 0;
         const pt1 = [aleft + a.width / 2, atop + a.height / 2];
         const pt2 = [bleft + b.width / 2, btop + b.width / 2];
-        const pt2vec = [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]];
-        const pt1vec = [(aDocBounds.left + aDocBounds.right) / 2 - pt1[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt1[1]];
+        const pt2vec = pt2inside ? [-0.7071, 0.7071] : [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]];
+        const pt1vec = pt1inside ? [-0.7071, 0.7071] : [(aDocBounds.left + aDocBounds.right) / 2 - pt1[0], (aDocBounds.top + aDocBounds.bottom) / 2 - pt1[1]];
         const pt1len = Math.sqrt(pt1vec[0] * pt1vec[0] + pt1vec[1] * pt1vec[1]);
         const pt2len = Math.sqrt(pt2vec[0] * pt2vec[0] + pt2vec[1] * pt2vec[1]);
         const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7fe74b876..1df46488b 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1707,7 +1707,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     toggleNativeDimensions = () => this.docView && Doc.toggleNativeDimensions(this.layoutDoc, this.docView.NativeDimScaling, this.props.PanelWidth(), this.props.PanelHeight());
     focus = (doc: Doc, options: DocFocusOptions) => this.docView?.focus(doc, options);
     getBounds = () => {
-        if (!this.docView || !this.docView.ContentDiv || this.props.Document.presBox || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
+        if (!this.docView || !this.docView.ContentDiv || this.props.Document.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
             return undefined;
         }
         const xf = this.docView?.props
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index b2fdd93fc..232c3459e 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -17,7 +17,6 @@ import { Transform } from '../../util/Transform';
 import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
-import { SelectionManager } from '../../util/SelectionManager';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
@@ -114,7 +113,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                     this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                                 }
                                 this._toolTipText = '';
-                                if (LinkDocPreview.LinkInfo?.noPreview) this.followLink();
+                                if (LinkDocPreview.LinkInfo?.noPreview || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink();
                             }
                         })
                     );
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index b895043de..62e215521 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1476,6 +1476,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
             FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
+            if (target) return;
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index 4d60a02f1..fd202590e 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -938,14 +938,19 @@
         min-width: 15px;
         max-width: 100px;
         left: 8px;
+        margin: auto;
+        margin-left: unset;
+        height: 100%;
     }
 
     .presBox-presentPanel {
         display: flex;
         justify-self: end;
         width: 100%;
-        max-width: 300px;
-        min-width: 150px;
+        margin: auto;
+        margin-right: unset;
+        height: 100%;
+        position: relative;
     }
 
     select {
@@ -955,7 +960,7 @@
 
     .presBox-button {
         cursor: pointer;
-        height: 25px;
+        //height: 100%;
         border-radius: 5px;
         display: none;
         justify-content: center;
@@ -986,6 +991,9 @@
         width: max-content;
         position: absolute;
         right: 10px;
+        margin: auto;
+        margin-right: unset;
+        height: 100%;
 
         .present-icon {
             margin-right: 7px;
@@ -999,9 +1007,16 @@
         position: relative;
         display: inline-block;
         left: 8px;
+        margin: auto;
+        margin-left: unset;
     }
 }
 
+.presBox-buttons.inOverlay {
+    padding-top: unset;
+    padding-bottom: unset;
+}
+
 .presBox-backward,
 .presBox-forward {
     width: 25px;
@@ -1052,6 +1067,8 @@
             font-size: 30px;
             position: absolute;
             min-width: 50px;
+            margin: auto;
+            margin-left: unset;
         }
     }
 
@@ -1087,13 +1104,12 @@
     color: $white;
     border-radius: 5px;
     grid-template-rows: 100%;
-    height: 25;
+    height: 100%;
     width: max-content;
     min-width: max-content;
     justify-content: space-evenly;
     align-items: center;
     display: flex;
-    position: absolute;
     transition: all 0.2s;
 
     .presPanel-button-text {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index a609b46e4..855a7f171 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -12,7 +12,7 @@ import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
 import { AudioField } from '../../../../fields/URLField';
-import { emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { emptyFunction, emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -136,6 +136,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     componentWillUnmount() {
         this._unmounting = true;
+        if (this._presTimer) clearTimeout(this._presTimer);
         document.removeEventListener('keydown', PresBox.keyEventsWrapper, true);
         this.resetPresentation();
         // Turn of progressivize editors
@@ -143,7 +144,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
-    @action
     componentDidMount() {
         this._disposers.keyboard = reaction(
             () => this.selectedDoc,
@@ -167,10 +167,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
         this.props.setContentView?.(this);
         this._unmounting = false;
-        this.rootDoc._forceRenderEngine = computeTimelineLayout.name;
-        this.layoutDoc.presStatus = PresStatus.Edit;
-        this.layoutDoc._gridGap = 0;
-        this.layoutDoc._yMargin = 0;
+        if (this.props.renderDepth > 0) {
+            runInAction(() => {
+                this.rootDoc._forceRenderEngine = computeTimelineLayout.name;
+                this.layoutDoc._gridGap = 0;
+                this.layoutDoc._yMargin = 0;
+            });
+        }
         this.turnOffEdit(true);
         this._disposers.selection = reaction(
             () => SelectionManager.Views(),
@@ -247,9 +250,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const slides = DocListCast(this.rootDoc[StrCast(this.presFieldKey, 'data')]);
             const curLast = this.selectedArray.size ? Math.max(...Array.from(this.selectedArray).map(d => slides.indexOf(DocCast(d)))) : this.itemIndex;
             this.nextSlide(curLast + 1 === this.childDocs.length ? (this.layoutDoc.presLoop ? 0 : curLast) : curLast + 1);
-        } else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
-            // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode
-            this.nextSlide(0);
+        } else {
+            if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
+                // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode
+                this.nextSlide(0);
+            }
+            return 0;
         }
         return this.itemIndex;
     };
@@ -510,28 +516,28 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const srcContext = Cast(targetDoc.context, Doc, null) ?? Cast(Cast(targetDoc.annotationOn, Doc, null)?.context, Doc, null);
         const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
         const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined;
-        const includesDoc: boolean = DocumentManager.Instance.getDocumentView(targetDoc) ? true : false; //   DocListCast(presCollection?.data).includes(targetDoc);
+        const includesDoc = () => (DocumentManager.Instance.getDocumentView(targetDoc) ? true : false); //   DocListCast(presCollection?.data).includes(targetDoc);
         const tabMap = CollectionDockingView.Instance?.tabMap;
         const tab = tabMap && Array.from(tabMap).find(tab => tab.DashDoc === srcContext || tab.DashDoc === targetDoc);
         // Handles the setting of presCollection
-        if (includesDoc) {
+        if (includesDoc()) {
             //Case 1: Pres collection should not change as it is already the same
         } else if (tab !== undefined) {
             // Case 2: Pres collection should update
             this.layoutDoc.presCollection = srcContext;
         }
-        const presStatus = this.rootDoc.presStatus;
         const selViewCache = Array.from(this.selectedArray);
         const dragViewCache = Array.from(this._dragArray);
         const eleViewCache = Array.from(this._eleArray);
         const resetSelection = action(() => {
-            const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
-            if (presDocView) SelectionManager.SelectView(presDocView, false);
-            this.rootDoc.presStatus = presStatus;
-            this.clearSelectedArray();
-            selViewCache.forEach(doc => this.addToSelectedArray(doc));
-            this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
-            this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
+            if (!includesDoc()) {
+                const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
+                if (presDocView) SelectionManager.SelectView(presDocView, false);
+                this.clearSelectedArray();
+                selViewCache.forEach(doc => this.addToSelectedArray(doc));
+                this._dragArray.splice(0, this._dragArray.length, ...dragViewCache);
+                this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
+            }
             finished();
         });
         const createDocView = (doc: Doc, finished?: () => void) => {
@@ -539,7 +545,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox);
             this.layoutDoc.presCollection = targetDoc;
         };
-        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? finished : resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc() || tab ? finished : resetSelection);
     };
 
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) {
@@ -562,7 +568,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             setTimeout(restoreLayout);
         } else {
             if (targetDoc) {
-                LightboxView.SetLightboxDoc(undefined);
                 const options: DocFocusOptions = {
                     willPan: activeItem.presMovement !== PresMovement.None,
                     willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
@@ -579,10 +584,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
                     containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
                 }
-
+                const testTarget = containerDocContext.length ? containerDocContext[0] : targetDoc;
+                if (LightboxView.LightboxDoc && !DocumentManager.Instance.getLightboxDocumentView(testTarget)) {
+                    DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => {
+                        if (LightboxView.LightboxDoc && !DocumentManager.Instance.getLightboxDocumentView(LightboxView.LightboxDoc)) {
+                            DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished);
+                            restoreLayout();
+                        } else {
+                            LightboxView.SetLightboxDoc(undefined);
+                            DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished);
+                            restoreLayout();
+                        }
+                    });
+                    return;
+                }
                 DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished);
-            }
-            restoreLayout();
+                restoreLayout();
+            } else restoreLayout();
         }
     }
 
@@ -633,20 +651,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         });
     };
 
-    //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc
-    @action
-    startAutoPres = async (startSlide: number) => {
-        if (!this.childDocs.length) return;
-        this.layoutDoc.presStatus = PresStatus.Autoplay;
-        this.startPresentation(startSlide);
-        clearTimeout(this._presTimer);
-        const func = (itemIndex: number) => {
-            this._presTimer = setTimeout(() => {
-                if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual;
-                this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex);
-            }, NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition));
+    _exitTrail: Opt<() => void>;
+    PlayTrail = (doc: Doc) => {
+        const savedState = { c: doc, x: NumCast(doc.panX), y: NumCast(doc.panY), s: NumCast(doc.viewScale) };
+        this.startPresentation(0);
+        this._exitTrail = () => {
+            const { x, y, s, c } = savedState;
+            c._panX = x;
+            c._panY = y;
+            c._viewScale = s;
+            LightboxView.SetLightboxDoc(undefined);
+            Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
+            return PresStatus.Edit;
         };
-        func(this.itemIndex);
     };
 
     // The function pauses the auto presentation
@@ -695,40 +712,54 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * @param startIndex: index that the presentation will start at
      */
     startPresentation = (startIndex: number) => {
-        this.childDocs.forEach(doc => {
-            const tagDoc = doc.presentationTargetDoc as Doc;
-            if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) {
-                tagDoc.opacity = 0;
-            }
-            if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) {
-                tagDoc.opacity = 0;
-            }
-        });
-        this.gotoDocument(startIndex, this.activeItem);
+        clearTimeout(this._presTimer);
+        if (this.childDocs.length) {
+            this.layoutDoc.presStatus = PresStatus.Autoplay;
+            this.childDocs.forEach(doc => {
+                const tagDoc = doc.presentationTargetDoc as Doc;
+                if (doc.presHideBefore && this.childDocs.indexOf(doc) > startIndex) tagDoc.opacity = 0;
+                if (doc.presHideAfter && this.childDocs.indexOf(doc) < startIndex) tagDoc.opacity = 0;
+                // if (doc.presHide && this.childDocs.indexOf(doc) === startIndex)  tagDoc.opacity = 0;
+            });
+            const func = () => {
+                const delay = NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition);
+                this._presTimer = setTimeout(() => {
+                    if (!this.next()) this.layoutDoc.presStatus = this._exitTrail?.() ?? PresStatus.Manual;
+                    this.layoutDoc.presStatus === PresStatus.Autoplay && func();
+                }, delay);
+            };
+            this.gotoDocument(startIndex, this.activeItem, undefined, func);
+        }
     };
 
     /**
      * The method called to open the presentation as a minimized view
      */
     @action
-    updateMinimize = async () => {
+    enterMinimize = () => {
+        clearTimeout(this._presTimer);
+        const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
+        this.props.removeDocument?.(this.layoutDoc);
+        return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]);
+    };
+    exitMinimize = () => {
         if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
-            this.layoutDoc.presStatus = PresStatus.Edit;
             Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc);
             CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right);
-        } else {
-            this.layoutDoc.presStatus = PresStatus.Edit;
-            clearTimeout(this._presTimer);
-            const pt = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
-            this.rootDoc.overlayX = pt[0] + (this.props.PanelWidth() - 250);
-            this.rootDoc.overlayY = pt[1] + 10;
-            this.rootDoc._height = 30;
-            this.rootDoc._width = 248;
-            Doc.AddDocToList(Doc.MyOverlayDocs, undefined, this.rootDoc);
-            this.props.removeDocument?.(this.layoutDoc);
         }
+        return PresStatus.Edit;
     };
 
+    public static minimizedWidth = 198;
+    public static OpenPresMinimized(doc: Doc, pt: number[]) {
+        doc.overlayX = pt[0];
+        doc.overlayY = pt[1];
+        doc._height = 30;
+        doc._width = PresBox.minimizedWidth;
+        Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
+        return (doc.presStatus = PresStatus.Manual);
+    }
+
     /**
      * Called when the user changes the view type
      * Either 'List' (stacking) or 'Slides' (carousel)
@@ -940,11 +971,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 break;
             case 'Escape':
                 if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) {
-                    this.updateMinimize();
-                } else if (this.layoutDoc.presStatus === 'edit') {
+                    this.exitClicked();
+                } else if (this.layoutDoc.presStatus === PresStatus.Edit) {
                     this.clearSelectedArray();
                     this._eleArray.length = this._dragArray.length = 0;
-                } else this.layoutDoc.presStatus = 'edit';
+                } else {
+                    this.layoutDoc.presStatus = PresStatus.Edit;
+                }
                 if (this._presTimer) clearTimeout(this._presTimer);
                 handled = true;
                 break;
@@ -1797,7 +1830,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     className="dropdown-play-button"
                     onClick={undoBatch(
                         action(() => {
-                            this.updateMinimize();
+                            this.enterMinimize();
                             this.turnOffEdit(true);
                             this.gotoDocument(this.itemIndex, this.activeItem);
                         })
@@ -1820,8 +1853,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     scrollFocus = () => {
-        this.gotoDocument(0);
-        this.startOrPause(false);
+        // this.gotoDocument(0);
+        // this.startOrPause(false);
         return undefined;
     };
 
@@ -1951,8 +1984,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get topPanel() {
         const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
+        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         return (
-            <div className="presBox-buttons" style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
+            <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}>
                 {isMini ? null : (
                     <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}>
                         <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}>
@@ -1969,7 +2003,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     </select>
                 )}
                 <div className="presBox-presentPanel" style={{ opacity: this.childDocs.length ? 1 : 0.3 }}>
-                    <span className={`presBox-button ${this.layoutDoc.presStatus === 'edit' ? 'present' : ''}`}>
+                    <span className={`presBox-button ${this.layoutDoc.presStatus === PresStatus.Edit ? 'present' : ''}`}>
                         <div
                             className="presBox-button-left"
                             onClick={undoBatch(() => {
@@ -2001,11 +2035,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get playButtons() {
         const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
         const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
+        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         // Case 1: There are still other frames and should go through all frames before going to next slide
         return (
             <div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}>
                 <Tooltip title={<div className="dash-tooltip">{'Loop'}</div>}>
-                    <div className="presPanel-button" style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }} onClick={() => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop)}>
+                    <div
+                        className="presPanel-button"
+                        style={{ color: this.layoutDoc.presLoop ? Colors.MEDIUM_BLUE : 'white' }}
+                        onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => (this.layoutDoc.presLoop = !this.layoutDoc.presLoop), false, false)}>
                         <FontAwesomeIcon icon={'redo-alt'} />
                     </div>
                 </Tooltip>
@@ -2013,42 +2051,77 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 <div
                     className="presPanel-button"
                     style={{ opacity: presStart ? 0.4 : 1 }}
-                    onClick={e => {
-                        this.back();
-                        if (this._presTimer) {
-                            clearTimeout(this._presTimer);
-                            this.layoutDoc.presStatus = PresStatus.Manual;
-                        }
-                        e.stopPropagation();
-                    }}>
+                    onPointerDown={e =>
+                        setupMoveUpEvents(
+                            this,
+                            e,
+                            returnFalse,
+                            emptyFunction,
+                            () => {
+                                this.back();
+                                if (this._presTimer) {
+                                    clearTimeout(this._presTimer);
+                                    this.layoutDoc.presStatus = PresStatus.Manual;
+                                }
+                                e.stopPropagation();
+                            },
+                            false,
+                            false
+                        )
+                    }>
                     <FontAwesomeIcon icon={'arrow-left'} />
                 </div>
                 <Tooltip title={<div className="dash-tooltip">{this.layoutDoc.presStatus === PresStatus.Autoplay ? 'Pause' : 'Autoplay'}</div>}>
-                    <div className="presPanel-button" onClick={e => this.startOrPause(true)}>
+                    <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.startOrPause(true), false, false)}>
                         <FontAwesomeIcon icon={this.layoutDoc.presStatus === PresStatus.Autoplay ? 'pause' : 'play'} />
                     </div>
                 </Tooltip>
                 <div
                     className="presPanel-button"
                     style={{ opacity: presEnd ? 0.4 : 1 }}
-                    onClick={e => {
-                        this.next();
-                        if (this._presTimer) {
-                            clearTimeout(this._presTimer);
-                            this.layoutDoc.presStatus = PresStatus.Manual;
-                        }
-                        e.stopPropagation();
-                    }}>
+                    onPointerDown={e =>
+                        setupMoveUpEvents(
+                            this,
+                            e,
+                            returnFalse,
+                            emptyFunction,
+                            () => {
+                                this.next();
+                                if (this._presTimer) {
+                                    clearTimeout(this._presTimer);
+                                    this.layoutDoc.presStatus = PresStatus.Manual;
+                                }
+                                e.stopPropagation();
+                            },
+                            false,
+                            false
+                        )
+                    }>
                     <FontAwesomeIcon icon={'arrow-right'} />
                 </div>
                 <div className="presPanel-divider"></div>
                 <Tooltip title={<div className="dash-tooltip">{'Click to return to 1st slide'}</div>}>
-                    <div className="presPanel-button" style={{ border: 'solid 1px white' }} onClick={() => this.nextSlide(0)}>
+                    <div
+                        className="presPanel-button"
+                        style={{ border: 'solid 1px white' }}
+                        onPointerDown={e =>
+                            setupMoveUpEvents(
+                                this,
+                                e,
+                                returnFalse,
+                                emptyFunction,
+                                () => {
+                                    this.nextSlide(0);
+                                },
+                                false,
+                                false
+                            )
+                        }>
                         <b>1</b>
                     </div>
                 </Tooltip>
-                <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
-                    Slide {this.itemIndex + 1} / {this.childDocs.length}
+                <div className="presPanel-button-text" onClick={() => this.gotoDocument(0, this.activeItem)} style={{ display: inOverlay || this.props.PanelWidth() > 250 ? 'inline-flex' : 'none' }}>
+                    {`${inOverlay ? '' : 'Slide'} ${this.itemIndex + 1} / ${this.childDocs.length}`}
                 </div>
                 <div className="presPanel-divider"></div>
                 {this.props.PanelWidth() > 250 ? (
@@ -2056,14 +2129,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         className="presPanel-button-text"
                         onClick={undoBatch(
                             action(() => {
-                                this.layoutDoc.presStatus = 'edit';
+                                this.layoutDoc.presStatus = PresStatus.Edit;
                                 clearTimeout(this._presTimer);
                             })
                         )}>
                         EXIT
                     </div>
                 ) : (
-                    <div className="presPanel-button" onClick={undoBatch(action(() => (this.layoutDoc.presStatus = 'edit')))}>
+                    <div className="presPanel-button" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.exitClicked, false, false)}>
                         <FontAwesomeIcon icon={'times'} />
                     </div>
                 )}
@@ -2074,7 +2147,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     startOrPause = (makeActive = true) => {
         makeActive && this.updateCurrentPresentation();
-        if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
+        if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startPresentation(this.itemIndex);
         else this.pauseAutoPres();
     };
 
@@ -2097,9 +2170,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
     @undoBatch
     @action
-    exitClicked = (e: PointerEvent) => {
-        this.updateMinimize();
-        this.layoutDoc.presStatus = PresStatus.Edit;
+    exitClicked = () => {
+        this.layoutDoc.presStatus = this._exitTrail?.() ?? this.exitMinimize();
         clearTimeout(this._presTimer);
     };
 
@@ -2134,8 +2206,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // needed to ensure that the childDocs are loaded for looking up fields
         this.childDocs.slice();
         const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
-        const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
-        const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
+        const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
+        const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0;
+        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player
             <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}>
                 <div
@@ -2177,7 +2250,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </div>
             </div>
         ) : (
-            <div className="presBox-cont" style={{ minWidth: DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) ? 240 : undefined }}>
+            <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}>
                 {this.topPanel}
                 {this.toolbar}
                 {this.newDocumentToolbarDropdown}
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 5e1474b89..f1c97d26a 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -50,9 +50,6 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @computed get collapsedHeight() {
         return 35;
     } // the collapsed height changes depending on the state of the presBox.  We could store this on the presentation element template if it's used by only one presentation - but if it's shared by multiple, then this value must be looked up
-    @computed get presStatus() {
-        return this.presBox.presStatus;
-    }
     @computed get selectedArray() {
         return this.presBoxView?.selectedArray;
     }
@@ -364,14 +361,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     @computed get recordingIsInOverlay() {
-        let isInOverlay = false;
-        DocListCast(Doc.MyOverlayDocs.data).forEach(doc => {
-            if (doc.slides === this.rootDoc) {
-                isInOverlay = true;
-                // return
-            }
-        });
-        return isInOverlay;
+        return DocListCast(Doc.MyOverlayDocs.data).some(doc => doc.slides === this.rootDoc);
     }
 
     // a previously recorded video will have timecode defined
diff --git a/src/fields/FieldLoader.scss b/src/fields/FieldLoader.scss
index 123488c7d..9a23c3e4f 100644
--- a/src/fields/FieldLoader.scss
+++ b/src/fields/FieldLoader.scss
@@ -1,12 +1,15 @@
 .fieldLoader {
     z-index: 10000;
     width: 200px;
-    height: 50;
-    background: white;
+    height: 30;
+    background: lightblue;
     position: absolute;
     left: calc(50% - 99px);
-    top: calc(50% + 99px);
+    top: calc(50% + 110px);
     display: flex;
     align-items: center;
     padding: 20px;
+    margin: auto;
+    display: 'block';
+    box-shadow: darkslategrey 0.2vw 0.2vw 0.8vw;
 }
diff --git a/src/fields/FieldLoader.tsx b/src/fields/FieldLoader.tsx
index 36dca89f2..2a7b936f7 100644
--- a/src/fields/FieldLoader.tsx
+++ b/src/fields/FieldLoader.tsx
@@ -10,18 +10,6 @@ export class FieldLoader extends React.Component {
     public static active = false;
 
     render() {
-        return (
-            <div
-                className="fieldLoader"
-                style={{
-                    zIndex: 10000,
-                    margin: 'auto',
-                    width: 200,
-                    height: 75,
-                    background: 'lightblue',
-                    display: 'block',
-                    position: 'absolute',
-                }}>{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>
-        );
+        return <div className="fieldLoader">{`Requested: ${FieldLoader.ServerLoadStatus.requested} ... ${FieldLoader.ServerLoadStatus.retrieved} `}</div>;
     }
 }
-- 
cgit v1.2.3-70-g09d2

