From 4923b457cb2e47d9cef999e714efb9032aaab2dc Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 13:47:03 -0500
Subject: fixed losing of data when pinning the current tab's view and then
 navigating to that pres element.  fixed being able to zoom in on images in
 presentations.

---
 src/client/views/nodes/trails/PresBox.tsx | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4e8aed8a6..1dfad510f 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -372,8 +372,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
             }
         }
-        if (dataview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
-        if (textview) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+        if (dataview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+        if (textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
         if (poslayoutview) {
             StrListCast(activeItem.presPinLayoutData)
                 .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
@@ -407,13 +407,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 const dv = DocumentManager.Instance.getDocumentView(bestTarget);
                 if (dv) {
                     const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height);
-                    activeItem.presMovement === 'zoom' && (bestTarget._viewScale = computedScale);
+                    activeItem.presMovement === PresMovement.Zoom && (bestTarget._viewScale = computedScale);
                     dv.ComponentView?.brushView?.(viewport);
                 }
             } else {
                 bestTarget._panX = activeItem.presPinViewX;
                 bestTarget._panY = activeItem.presPinViewY;
-                activeItem.presMovement === 'zoom' && (bestTarget._viewScale = activeItem.presPinViewScale);
+                bestTarget._viewScale = activeItem.presPinViewScale;
             }
         }
         return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
@@ -779,7 +779,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     movementName = action((activeItem: Doc) => {
         if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
-            activeItem.presMovement = 'zoom';
+            activeItem.presMovement = PresMovement.Zoom;
         }
         return StrCast(activeItem.presMovement);
     });
@@ -1252,8 +1252,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const zoom = NumCast(activeItem.presZoom, 1) * 100;
             let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
             if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
-            const effect = this.activeItem.presEffect ? this.activeItem.presEffect : 'None';
-            activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : 'Zoom';
+            const effect = this.activeItem.presEffect ? this.activeItem.presEffect : PresMovement.None;
+            activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
             return (
                 <div
                     className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
-- 
cgit v1.2.3-70-g09d2


From fe2ee94451bfef6942ad8538f6766848ff433a3c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 14:34:44 -0500
Subject: fixed playing presentation when a slide is a tab.

---
 src/client/views/nodes/ImageBox.tsx       |  2 +-
 src/client/views/nodes/trails/PresBox.tsx | 15 ++++-----------
 2 files changed, 5 insertions(+), 12 deletions(-)

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

diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 08c0065d1..d02f181cc 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -71,7 +71,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this._disposers.sizer = reaction(
             () => ({
                 forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes,
-                scrSize: this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth,
+                scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._viewScale, 1),
                 selected: this.props.isSelected(),
             }),
             ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'),
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 1dfad510f..ccf19efc9 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -246,7 +246,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // Called when the user activates 'next' - to move to the next part of the pres. trail
     @action
     next = () => {
-        const activeNext = Cast(this.childDocs[this.itemIndex + 1], Doc, null);
         const activeItem: Doc = this.activeItem;
         const targetDoc: Doc = this.targetDoc;
         const lastFrame = Cast(targetDoc?.lastFrame, 'number', null);
@@ -478,7 +477,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const presCollection = Cast(this.layoutDoc.presCollection, Doc, null);
         const collectionDocView = presCollection ? DocumentManager.Instance.getDocumentView(presCollection) : undefined;
         const includesDoc: boolean = DocListCast(presCollection?.data).includes(targetDoc);
-        const tab = CollectionDockingView.Instance && Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === srcContext);
+        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) {
             //Case 1: Pres collection should not change as it is already the same
@@ -766,14 +766,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (this.childDocs[stopDocIndex - 1].mediaStopTriggerList) {
             const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList);
             list.push(activeItem);
-            // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;
-            console.log(list);
+            // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;\
         } else {
             this.childDocs[stopDocIndex - 1].mediaStopTriggerList = new List<Doc>();
             const list = DocListCast(this.childDocs[stopDocIndex - 1].mediaStopTriggerList);
             list.push(activeItem);
             // this.childDocs[stopDocIndex - 1].mediaStopTriggerList = list;
-            console.log(list);
         }
     });
 
@@ -2418,12 +2416,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         </div>
                     </Tooltip>
                     <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}>
-                        <div
-                            className={'headerButton'}
-                            onClick={e => {
-                                e.stopPropagation();
-                                console.log('New frame');
-                            }}>
+                        <div className={'headerButton'} onClick={e => e.stopPropagation()}>
                             <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
                         </div>
                     </Tooltip>
-- 
cgit v1.2.3-70-g09d2


From e4bd0a61a3c21179bd84d7f42b60b12a94886385 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 15:02:41 -0500
Subject: fixed restarting presentation by hitting play at end of presentation

---
 src/client/views/nodes/trails/PresBox.tsx | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index ccf19efc9..88cb3ce1d 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -634,19 +634,21 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     //The function that starts or resets presentaton functionally, depending on presStatus of the layoutDoc
     @action
-    startAutoPres = (startSlide: number) => {
+    startAutoPres = async (startSlide: number) => {
         this.layoutDoc.presStatus = PresStatus.Autoplay;
-        this.startPresentation(startSlide);
+        if (startSlide + 1 === this.childDocs.length) {
+            this.gotoDocument(0);
+            await new Promise<void>(res => setTimeout(() => res(), NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)));
+        }
+        this.startPresentation(startSlide + 1 === this.childDocs.length ? 0 : startSlide);
         clearTimeout(this._presTimer);
         const func = (itemIndex: number) => {
             if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual;
-            else
-                this._presTimer = setTimeout(
-                    () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex),
-                    NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)
-                );
+            this._presTimer = setTimeout(
+                () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex),
+                NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)
+            );
         };
-
         func(this.itemIndex);
     };
 
-- 
cgit v1.2.3-70-g09d2


From 898fee3d4178eab6bdacea156b8aabc47213fc06 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 17:52:30 -0500
Subject: fixed preselements to always have a pres transition slider/input in
 properties view

---
 src/client/views/nodes/trails/PresBox.tsx | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 88cb3ce1d..14f767595 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1297,8 +1297,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                         </div>
                         {inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
-                        <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}>
-                            <div className="presBox-subheading">Movement Speed</div>
+                        <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+                            <div className="presBox-subheading">Transition Speed</div>
                             <div className="ribbon-property">
                                 <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.setTransitionTime(e.target.value))} /> s
                             </div>
@@ -1312,7 +1312,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             </div>
                         </div>
                         {inputter('0.1', '0.1', '10', transitionSpeed, [PresMovement.Pan, PresMovement.Zoom].includes(activeItem.presMovement as any), this.setTransitionTime)}
-                        <div className={`slider-headers ${activeItem.presMovement === PresMovement.Pan || activeItem.presMovement === PresMovement.Zoom ? '' : 'none'}`}>
+                        <div className={'slider-headers'}>
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
                             <div className="slider-text">Slow</div>
-- 
cgit v1.2.3-70-g09d2


From 90bf9ea9e32ed695a36d0334e99fd859bbe51c15 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 19:59:13 -0500
Subject: made alternate image use a field data-useAlt.  added alt images to
 preselements. fixed removing TabDoc documentviews from document view list. 
 added playing audio tags to preselements

---
 src/client/goldenLayout.js                         |  1 +
 src/client/util/LinkFollower.ts                    |  7 ++-----
 src/client/views/nodes/ImageBox.tsx                |  9 ++++++---
 .../nodes/formattedText/DashDocCommentView.tsx     |  3 +++
 .../views/nodes/formattedText/DashDocView.tsx      |  1 +
 .../views/nodes/formattedText/DashFieldView.tsx    |  2 +-
 .../views/nodes/formattedText/EquationView.tsx     |  1 +
 .../views/nodes/formattedText/SummaryView.tsx      |  1 +
 src/client/views/nodes/trails/PresBox.tsx          | 22 ++++++++++++++++++----
 9 files changed, 34 insertions(+), 13 deletions(-)

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

diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index bc08b4d0b..9cb20d834 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -5408,6 +5408,7 @@
          * @returns {void}
          */
         _destroy: function () {
+            this._reactComponent.unmount();
           //  ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
             this._container.off('open', this._render, this);
             this._container.off('destroy', this._destroy, this);
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 43b2caf88..1608a77f2 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,8 +1,5 @@
-import { action, observable, observe, runInAction } from 'mobx';
-import { computedFn } from 'mobx-utils';
-import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
-import { List } from '../../fields/List';
-import { ProxyField } from '../../fields/Proxy';
+import { action, runInAction } from 'mobx';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, StrCast } from '../../fields/Types';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d02f181cc..d0df41023 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,7 +8,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { createSchema } from '../../../fields/Schema';
 import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -120,6 +120,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     @undoBatch
     resolution = () => (this.layoutDoc._showFullRes = !this.layoutDoc._showFullRes);
+    @undoBatch
+    setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']);
 
     @undoBatch
     rotate = action(() => {
@@ -182,6 +184,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             const funcs: ContextMenuProps[] = [];
             funcs.push({ description: 'Rotate Clockwise 90', event: this.rotate, icon: 'expand-arrows-alt' });
             funcs.push({ description: `Show ${this.layoutDoc._showFullRes ? 'Dynamic Res' : 'Full Res'}`, event: this.resolution, icon: 'expand-arrows-alt' });
+            funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Primary' : 'Show Alternate'}`, event: this.setUseAlt, icon: 'expand-arrows-alt' });
             funcs.push({ description: 'Copy path', event: () => Utils.CopyText(this.choosePath(field.url)), icon: 'expand-arrows-alt' });
             if (!Doc.noviceMode) {
                 funcs.push({ description: 'Export to Google Photos', event: () => GooglePhotos.Transactions.UploadImages([this.props.Document]), icon: 'caret-square-right' });
@@ -335,8 +338,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 <div className="imageBox-fader" style={{ overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}>
                     <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                     {fadepath === srcpath ? null : (
-                        <div className={`imageBox-fadeBlocker${this.props.isHovering?.() ? '-hover' : ''}`}>
-                            <img className="imageBox-fadeaway" key={'fadeaway'} src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
+                        <div className={`imageBox-fadeBlocker${(this.props.isHovering?.() && this.layoutDoc[this.fieldKey + '-useAlt'] === undefined) || BoolCast(this.layoutDoc[this.fieldKey + '-useAlt']) ? '-hover' : ''}`}>
+                            <img className="imageBox-fadeaway" key="fadeaway" src={fadepath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                         </div>
                     )}
                 </div>
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 4bff57842..fcd6e0c55 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -36,6 +36,9 @@ export class DashDocCommentView {
         (this as any).dom = this.dom;
     }
 
+    destroy() {
+        this.root.unmount();
+    }
     deselectNode() {
         this.dom.classList.remove('ProseMirror-selectednode');
     }
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 63ee7d1f3..722d0cc4f 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -45,6 +45,7 @@ export class DashDocView {
         );
     }
     destroy() {
+        this.root.unmount();
         // ReactDOM.unmountComponentAtNode(this.dom);
     }
     selectNode() {}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 41b92a3c7..1e7cb6ea5 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -47,7 +47,7 @@ export class DashFieldView {
         );
     }
     destroy() {
-        //this.root.unmount();
+        this.root.unmount();
     }
     deselectNode() {
         this.dom.classList.remove('ProseMirror-selectednode');
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 89f2a7c81..714ae458c 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -32,6 +32,7 @@ export class EquationView {
     _editor: EquationEditor | undefined;
     setEditor = (editor?: EquationEditor) => (this._editor = editor);
     destroy() {
+        this.root.unmount();
         // ReactDOM.unmountComponentAtNode(this.dom);
     }
     setSelection() {
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 1fe6d822b..4e75d374c 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -42,6 +42,7 @@ export class SummaryView {
 
     className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed');
     destroy() {
+        this.root.unmount();
         // ReactDOM.unmountComponentAtNode(this.dom);
     }
     selectNode() {}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 14f767595..c1973c4b4 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -349,7 +349,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
         const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
         const clippable = [DocumentType.COMPARISON].includes(target.type as any);
-        const dataview = [DocumentType.INK, DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
+        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(target.type as any) && target.activeFrame === undefined;
         const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
         const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
         return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview };
@@ -371,7 +371,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
             }
         }
-        if (dataview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+        if (dataview && activeItem.presData !== undefined) {
+            const fkey = Doc.LayoutFieldKey(bestTarget);
+            Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+            bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt;
+        }
         if (textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
         if (poslayoutview) {
             StrListCast(activeItem.presPinLayoutData)
@@ -434,7 +438,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         if (pinProps?.pinDocContent) {
             pinDoc.presPinData = scrollable || temporal || pannable || clippable || dataview || textview || poslayoutview || pinProps.activeFrame !== undefined;
-            if (dataview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.data;
+            if (dataview) {
+                const fkey = Doc.LayoutFieldKey(targetDoc);
+                pinDoc.presUseAlt = targetDoc[fkey + '-useAlt'];
+                pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
+            }
             if (textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
             if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
             if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
@@ -541,8 +549,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         if (activeItem.presPinData || activeItem.presPinView) {
             clearTimeout(PresBox._navTimer);
             // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
-            const bestTarget = DocumentManager.Instance.getFirstDocumentView(targetDoc)?.props.Document;
+            const bestTargetView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
+            const bestTarget = bestTargetView?.props.Document;
             if (bestTarget) PresBox._navTimer = PresBox.restoreTargetDocView(bestTarget, activeItem);
+            activeItem.presPinAudioPlay && bestTargetView?.docView?.playAnnotation();
         }
     }
 
@@ -1369,6 +1379,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     {isPresCollection ? null : (
                         <div className="ribbon-box">
                             Effects
+                            <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
+                                <div className="presBox-subheading">Play Audio Annotation</div>
+                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presPinAudioPlay = !BoolCast(activeItem.presPinAudioPlay))} checked={BoolCast(activeItem.presPinAudioPlay)} />
+                            </div>
                             <div
                                 className="presBox-dropdown"
                                 onClick={action(e => {
-- 
cgit v1.2.3-70-g09d2


From 778b251de77078ecb6d3570641b4472d842a1f77 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 10 Nov 2022 21:06:39 -0500
Subject: playing a presentation that's at its end, resets it to slide 0.

---
 src/client/views/nodes/trails/PresBox.tsx | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index c1973c4b4..5e9301e6a 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -646,10 +646,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     startAutoPres = async (startSlide: number) => {
         this.layoutDoc.presStatus = PresStatus.Autoplay;
-        if (startSlide + 1 === this.childDocs.length) {
-            this.gotoDocument(0);
-            await new Promise<void>(res => setTimeout(() => res(), NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)));
-        }
         this.startPresentation(startSlide + 1 === this.childDocs.length ? 0 : startSlide);
         clearTimeout(this._presTimer);
         const func = (itemIndex: number) => {
@@ -2539,9 +2535,13 @@ 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);
-        else this.pauseAutoPres();
+        if (this.itemIndex + 1 === this.childDocs.length) {
+            this.gotoDocument(0);
+        } else {
+            makeActive && this.updateCurrentPresentation();
+            if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
+            else this.pauseAutoPres();
+        }
     };
 
     @action
-- 
cgit v1.2.3-70-g09d2


From 7bde06fc756684d47c89c65199492daef5fe5b63 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 14 Nov 2022 09:51:49 -0500
Subject: made audio anno recording last as long as record button is pressed.
 added a Hide option to allow an pres item to be animated but not visible
 (used to setup state for subsequent activity))

---
 src/client/views/DocumentButtonBar.tsx    | 36 ++++++++++---------
 src/client/views/nodes/DocumentView.tsx   | 10 +++---
 src/client/views/nodes/trails/PresBox.tsx | 60 ++++++++++++++++++-------------
 3 files changed, 61 insertions(+), 45 deletions(-)

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

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 3f12c7c9d..a110bf51a 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -6,7 +6,7 @@ import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
 import { Cast, NumCast } from '../../fields/Types';
-import { emptyFunction, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
 import { Docs } from '../documents/Documents';
@@ -14,7 +14,7 @@ import { DragManager } from '../util/DragManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
-import { undoBatch } from '../util/UndoManager';
+import { undoBatch, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { TabDocView } from './collections/TabDocView';
 import './DocumentButtonBar.scss';
@@ -349,28 +349,30 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
     }
 
     @observable _isRecording = false;
+    _stopFunc: () => void = emptyFunction;
     @computed
     get recordButton() {
         const targetDoc = this.view0?.props.Document;
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{'Click to record 5 second annotation'}</div>}>
+            <Tooltip title={<div className="dash-tooltip">Press to record audio annotation</div>}>
                 <div
                     className="documentButtonBar-icon"
                     style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }}
-                    onClick={undoBatch(
-                        action(e => {
-                            this._isRecording = true;
-                            this.props.views().map(
-                                view =>
-                                    view &&
-                                    DocumentViewInternal.recordAudioAnnotation(
-                                        view.dataDoc,
-                                        view.LayoutFieldKey,
-                                        action(() => (this._isRecording = false))
-                                    )
-                            );
-                        })
-                    )}>
+                    onPointerDown={action((e: React.PointerEvent) => {
+                        this._isRecording = true;
+                        this.props.views().map(
+                            view =>
+                                view &&
+                                DocumentViewInternal.recordAudioAnnotation(
+                                    view.dataDoc,
+                                    view.LayoutFieldKey,
+                                    stopFunc => (this._stopFunc = stopFunc),
+                                    action(() => (this._isRecording = false))
+                                )
+                        );
+                        const b = UndoManager.StartBatch('Recording');
+                        setupMoveUpEvents(this, e, returnFalse, () => this._stopFunc(), emptyFunction);
+                    })}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
                 </div>
             </Tooltip>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 6b96362bf..c669d2b96 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1208,7 +1208,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     };
 
-    static recordAudioAnnotation(dataDoc: Doc, field: string, onEnd?: () => void) {
+    static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
         let gumStream: any;
         let recorder: any;
         navigator.mediaDevices
@@ -1245,12 +1245,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 };
                 runInAction(() => (dataDoc.audioAnnoState = 'recording'));
                 recorder.start();
-                setTimeout(() => {
+                const stopFunc = () => {
                     recorder.stop();
                     DictationManager.Controls.stop(false);
                     runInAction(() => (dataDoc.audioAnnoState = 'stopped'));
                     gumStream.getAudioTracks()[0].stop();
-                }, 5000);
+                };
+                if (onRecording) onRecording(stopFunc);
+                else setTimeout(stopFunc, 5000);
             });
     }
 
@@ -1385,7 +1387,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 : this.boxShadow || (this.props.Document.isTemplateForField ? 'black 0.2vw 0.2vw 0.8vw' : undefined);
         const renderDoc = this.renderDoc({
             borderRadius: this.borderRounding,
-            outline: highlighting && !this.borderRounding && !highlighting.highlightStroke? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
+            outline: highlighting && !this.borderRounding && !highlighting.highlightStroke ? `${highlighting.highlightColor} ${highlighting.highlightStyle} ${highlighting.highlightIndex}px` : 'solid 0px',
             border: highlighting && this.borderRounding && highlighting.highlightStyle === 'dashed' ? `${highlighting.highlightStyle} ${highlighting.highlightColor} ${highlighting.highlightIndex}px` : undefined,
             boxShadow,
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 5e9301e6a..9e3b42cf6 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -330,7 +330,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 this.startTempMedia(targetDoc, activeItem);
             }
             if (targetDoc) {
-                Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
+                //  Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
                 targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500)));
                 setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10);
             }
@@ -392,12 +392,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 () =>
                     StrListCast(activeItem.presPinLayoutData)
                         .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
-                        .forEach(
-                            action(data => {
-                                const doc = DocServer.GetCachedRefField(data.id) as Doc;
-                                doc._dataTransition = undefined;
-                            })
-                        ),
+                        .forEach(action(data => ((DocServer.GetCachedRefField(data.id) as Doc)._dataTransition = undefined))),
                 transTime + 10
             );
         }
@@ -616,26 +611,29 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (tagDoc === this.layoutDoc.presCollection) {
                 tagDoc.opacity = 1;
             } else {
-                if (curDoc.presHideBefore) {
-                    if (itemIndexes.length > 1 && curInd !== 0) {
+                const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex);
+                if (curDoc.presHideBefore && index === hidingIndBef) {
+                    if (index > this.itemIndex) {
+                        tagDoc.opacity = 0;
+                    } else if (index === this.itemIndex || !curDoc.presHideAfter) {
                         tagDoc.opacity = 1;
-                    } else {
-                        if (index > this.itemIndex) {
-                            tagDoc.opacity = 0;
-                        } else if (index === this.itemIndex || !curDoc.presHideAfter) {
-                            tagDoc.opacity = 1;
-                        }
                     }
                 }
-                if (curDoc.presHideAfter) {
-                    if (itemIndexes.length > 1 && curInd !== itemIndexes.length - 1) {
+                const hidingIndAft = itemIndexes
+                    .slice()
+                    .reverse()
+                    .find(item => item < this.itemIndex);
+                if (curDoc.presHideAfter && index === hidingIndAft) {
+                    if (index < this.itemIndex) {
+                        tagDoc.opacity = 0;
+                    } else if (index === this.itemIndex || !curDoc.presHideBefore) {
                         tagDoc.opacity = 1;
-                    } else {
-                        if (index < this.itemIndex) {
-                            tagDoc.opacity = 0;
-                        } else if (index === this.itemIndex || !curDoc.presHideBefore) {
-                            tagDoc.opacity = 1;
-                        }
+                    }
+                }
+                const hidingInd = itemIndexes.find(item => item === this.itemIndex);
+                if (curDoc.presHide && index === hidingInd) {
+                    if (index === this.itemIndex) {
+                        tagDoc.opacity = 0;
                     }
                 }
             }
@@ -1180,6 +1178,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.selectedArray.forEach(doc => (doc.presHideBefore = activeItem.presHideBefore));
     };
 
+    @undoBatch
+    @action
+    updateHide = (activeItem: Doc) => {
+        activeItem.presHide = !activeItem.presHide;
+        this.selectedArray.forEach(doc => (doc.presHide = activeItem.presHide));
+    };
+
     @undoBatch
     @action
     updateHideAfter = (activeItem: Doc) => {
@@ -1317,7 +1322,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                             </div>
                         </div>
-                        {inputter('0.1', '0.1', '10', transitionSpeed, [PresMovement.Pan, PresMovement.Zoom].includes(activeItem.presMovement as any), this.setTransitionTime)}
+                        {inputter('0.1', '0.1', '10', transitionSpeed, true, this.setTransitionTime)}
                         <div className={'slider-headers'}>
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
@@ -1334,6 +1339,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     </div>
                                 </Tooltip>
                             )}
+                            {isPresCollection ? null : (
+                                <Tooltip title={<div className="dash-tooltip">{'Hide while presented'}</div>}>
+                                    <div className={`ribbon-toggle ${activeItem.presHide ? 'active' : ''}`} onClick={() => this.updateHide(activeItem)}>
+                                        Hide
+                                    </div>
+                                </Tooltip>
+                            )}
                             {isPresCollection ? null : (
                                 <Tooltip title={<div className="dash-tooltip">{'Hide after presented'}</div>}>
                                     <div className={`ribbon-toggle ${activeItem.presHideAfter ? 'active' : ''}`} onClick={() => this.updateHideAfter(activeItem)}>
-- 
cgit v1.2.3-70-g09d2


From 3d8bd17fa4f031f2223840b336c313a18e7e87aa Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 14 Nov 2022 14:29:48 -0500
Subject: fixed following link 'inPlace' from within lightbox to do same thing
 as if out of lightbox.  fixed contentActive behavior for multicolumn view. 
 removed progressivize from presbox.

---
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/DocumentManager.ts                 |  24 +-
 src/client/util/LinkFollower.ts                    |   7 +-
 src/client/views/DocumentButtonBar.tsx             |   2 +-
 src/client/views/LightboxView.tsx                  |   8 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/PropertiesView.tsx                |  14 -
 src/client/views/collections/TabDocView.tsx        |   9 +-
 .../collectionFreeForm/CollectionFreeFormView.scss |  50 ---
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  54 +--
 .../CollectionMulticolumnView.tsx                  |  10 +-
 .../CollectionMultirowView.tsx                     |   8 +-
 src/client/views/nodes/DocumentView.scss           |   6 +-
 src/client/views/nodes/trails/PresBox.tsx          | 475 +--------------------
 14 files changed, 75 insertions(+), 595 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index f1b8c3034..e44004f45 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -259,7 +259,6 @@ export class DocumentOptions {
     appearFrame?: number; // the frame in which the document appears
     presTransition?: number; //the time taken for the transition TO a document
     presDuration?: number; //the duration of the slide in presentation view
-    presProgressivize?: boolean;
     borderRounding?: string;
     boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow
     data?: any;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index b046d950f..01ca24439 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -131,6 +131,7 @@ export class DocumentManager {
         return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
     };
     public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
+        if (LightboxView.LightboxDoc) return DocumentManager.Instance.getLightboxDocumentView(toFind, originatingDoc);
         const views = this.getDocumentViews(toFind).filter(view => view.rootDoc !== originatingDoc);
         return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
     };
@@ -168,8 +169,7 @@ export class DocumentManager {
         presZoomScale?: number
     ): void => {
         originalTarget = originalTarget ?? targetDoc;
-        const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
-        const docView = getFirstDocView(targetDoc, originatingDoc);
+        const docView = this.getFirstDocumentView(targetDoc, originatingDoc);
         const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
         const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
         var wasHidden = resolvedTarget.hidden;
@@ -206,11 +206,11 @@ export class DocumentManager {
             }
             finished?.();
         };
-        const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && getFirstDocView(annotatedDoc);
+        const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
         const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
         const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
         const targetDocContext = contextDoc || annotatedDoc;
-        const targetDocContextView = (targetDocContext && getFirstDocView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
+        const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
         const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (annoContainerView) {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
@@ -281,7 +281,7 @@ export class DocumentManager {
                     } else {
                         // no timecode means we need to find the context view and focus on our target
                         const findView = (delay: number) => {
-                            const retryDocView = getFirstDocView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
+                            const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
                             if (retryDocView) {
                                 // we found the target in the context.
                                 Doc.linkFollowHighlight(retryDocView.rootDoc);
@@ -316,10 +316,20 @@ export class DocumentManager {
                         }
                     }
                     // there's no context view so we need to create one first and try again when that finishes
-                    const finishFunc = () => this.jumpToDocument(targetDoc, true, createViewFunc, docContext, linkDoc, true /* if we don't find the target, we want to get rid of the context just created */, undefined, finished, originalTarget);
                     createViewFunc(
                         targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
-                        finishFunc
+                        () =>
+                            this.jumpToDocument(
+                                targetDoc,
+                                true,
+                                (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished),
+                                docContext,
+                                linkDoc,
+                                true /* if target not found, get rid of context just created */,
+                                undefined,
+                                finished,
+                                originalTarget
+                            )
                     );
                 }
             }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 1608a77f2..c09c9d1c5 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -30,11 +30,10 @@ export class LinkFollower {
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
             const createTabForTarget = (didFocus: boolean) =>
                 new Promise<ViewAdjustment>(res => {
-                    const where = LightboxView.LightboxDoc ? 'lightbox' : StrCast(sourceDoc.followLinkLocation, followLoc);
+                    const where = LightboxView.LightboxDoc ? 'inPlace' : StrCast(sourceDoc.followLinkLocation, followLoc);
                     docViewProps.addDocTab(doc, where);
                     setTimeout(() => {
-                        const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView;
-                        const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
+                        const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
                         if (targDocView) {
                             targDocView.props.focus(doc, {
                                 willZoom: BoolCast(sourceDoc.followLinkZoom, false),
@@ -110,7 +109,7 @@ 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, 'lightbox'), 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);
                     }
                 } else {
                     allFinished();
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index a110bf51a..36875290e 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -458,7 +458,6 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
                     </div>
                 ) : null}
-                {Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.recordButton}</div>}
                 {
                     Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
                     /*<div className="documentButtonBar-button">
@@ -467,6 +466,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                 }
                 {Doc.noviceMode || !SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
+                <div className="documentButtonBar-button">{this.recordButton}</div>
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : (
                     <div className="documentButtonBar-button" style={{ display: !considerPush ? 'none' : '' }}>
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 91773419a..5660a34e9 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -4,7 +4,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { InkTool } from '../../fields/InkField';
-import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { List } from '../../fields/List';
+import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
@@ -141,6 +142,11 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         }
     }
     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;
+        }
         LightboxView.openInTabFunc = openInTabFunc;
         SelectionManager.DeselectAll();
         return LightboxView.SetLightboxDoc(
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 9648a7807..987bfc23d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -690,11 +690,11 @@ export class MainView extends React.Component {
         switch (locationFields[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 'lightbox':   return LightboxView.AddDocTab(doc, location);
             case 'toggle':     return CollectionDockingView.ToggleSplit(doc, locationParams);
         }
     };
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 842664402..1f161c01b 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -95,7 +95,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     //Pres Trails booleans:
     @observable openPresTransitions: boolean = false;
-    @observable openPresProgressivize: boolean = false;
     @observable openAddSlide: boolean = false;
     @observable openSlideOptions: boolean = false;
 
@@ -1651,19 +1650,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                 {this.openPresTransitions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.transitionDropdown}</div> : null}
                             </div>
                         )}
-                        {/* {!selectedItem || type === DocumentType.VID || type === DocumentType.AUDIO ? (null) : <div className="propertiesView-presTrails">
-                        <div className="propertiesView-presTrails-title"
-                            onPointerDown={action(() => this.openPresProgressivize = !this.openPresProgressivize)}
-                            style={{ backgroundColor: this.openPresProgressivize ? "black" : "" }}>
-                            &nbsp; <FontAwesomeIcon style={{ alignSelf: "center" }} icon={"tasks"} /> &nbsp; Progressivize
-                        <div className="propertiesView-presTrails-title-icon">
-                                <FontAwesomeIcon icon={this.openPresProgressivize ? "caret-down" : "caret-right"} size="lg" color="white" />
-                            </div>
-                        </div>
-                        {this.openPresProgressivize ? <div className="propertiesView-presTrails-content">
-                            {PresBox.Instance.progressivizeDropdown}
-                        </div> : null}
-                    </div>} */}
                         {!selectedItem || (type !== DocumentType.VID && type !== DocumentType.AUDIO) ? null : (
                             <div className="propertiesView-presTrails">
                                 <div
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e0c422f94..1a9006356 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -11,7 +11,7 @@ import { List } from '../../../fields/List';
 import { FieldId } from '../../../fields/RefField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils';
 import { DocServer } from '../../DocServer';
 import { DocUtils } from '../../documents/Documents';
@@ -360,11 +360,16 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             //     // TabDocView.PinDoc(doc, { hidePresBox: true });
             //     return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
             // }
+            case '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':
                 return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
             case 'toggle':
                 return CollectionDockingView.ToggleSplit(doc, locationParams, this.stack);
-            case 'inPlace':
             case 'add':
             default:
                 return CollectionDockingView.AddSplit(doc, locationParams, this.stack);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
index d80fcdfc3..7a7ae3f40 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.scss
@@ -96,37 +96,6 @@
     border-color: #69a5db;
 }
 
-.progressivizeButton {
-    position: absolute;
-    display: grid;
-    grid-template-columns: auto 20px auto;
-    transform: translate(-105%, 0);
-    align-items: center;
-    border: black solid 1px;
-    border-radius: 3px;
-    justify-content: center;
-    width: 40;
-    z-index: 30000;
-    height: 20;
-    overflow: hidden;
-    background-color: #d5dce2;
-    transition: all 1s;
-
-    .progressivizeButton-prev:hover {
-        color: #5a9edd;
-    }
-
-    .progressivizeButton-frame {
-        justify-self: center;
-        text-align: center;
-        width: 15px;
-    }
-
-    .progressivizeButton-next:hover {
-        color: #5a9edd;
-    }
-}
-
 .resizable {
     background: rgba(0, 0, 0, 0.2);
     width: 100px;
@@ -178,25 +147,6 @@
     }
 }
 
-.progressivizeMove-frame {
-    width: 20px;
-    border-radius: 2px;
-    z-index: 100000;
-    color: white;
-    text-align: center;
-    background-color: #5a9edd;
-    transform: translate(-110%, 110%);
-}
-
-.progressivizeButton:hover {
-    box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.5);
-
-    .progressivizeButton-frame {
-        background-color: #5a9edd;
-        color: white;
-    }
-}
-
 .collectionFreeform-customText {
     position: absolute;
     text-align: center;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d44c2f160..e24b116d0 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,5 +1,5 @@
 import { Bezier } from 'bezier-js';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import { DateField } from '../../../../fields/DateField';
@@ -71,6 +71,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return 'CollectionFreeFormView(' + this.props.Document.title?.toString() + ')';
     } // this makes mobx trace() statements more descriptive
 
+    @observable
+    public static ShowPresPaths = false;
+
     private _lastNudge: any;
     private _lastX: number = 0;
     private _lastY: number = 0;
@@ -1866,6 +1869,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
 
+    showPresPaths = () => (CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.getPaths(this.rootDoc) : null);
+
     @computed get marqueeView() {
         TraceMobx();
         return (
@@ -1912,8 +1917,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
                         transform={this.contentTransform}
                         zoomScaling={this.zoomScaling}
-                        presPaths={BoolCast(this.Document.presPathView)}
-                        progressivize={BoolCast(this.Document.editProgressivize)}
+                        presPaths={this.showPresPaths}
                         presPinView={BoolCast(this.Document.presPinView)}
                         transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
                         viewDefDivClick={this.props.viewDefDivClick}>
@@ -2039,8 +2043,7 @@ interface CollectionFreeFormViewPannableContentsProps {
     viewDefDivClick?: ScriptField;
     children: () => JSX.Element[];
     transition?: string;
-    presPaths?: boolean;
-    progressivize?: boolean;
+    presPaths: () => JSX.Element | null;
     presPinView?: boolean;
     isAnnotationOverlay: boolean | undefined;
     isAnnotationOverlayScrollable: boolean | undefined;
@@ -2093,42 +2096,11 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
         return true;
     };
 
-    // scale: NumCast(targetDoc._viewScale),
-    @computed get zoomProgressivizeContainer() {
-        const activeItem = PresBox.Instance.activeItem;
-        // const targetDoc = PresBox.Instance.targetDoc;
-        if (activeItem && activeItem.presPinView && activeItem.id) {
-            const left = NumCast(activeItem.presPinViewX);
-            const top = NumCast(activeItem.presPinViewY);
-            const width = 100;
-            const height = 100;
-            return !this.props.presPinView ? null : (
-                <div key="resizable" className="resizable" onPointerDown={this.onPointerDown} style={{ width, height, top, left, position: 'absolute' }}>
-                    <div className="resizers" key={'resizer' + activeItem.id}>
-                        <div className="resizer top-left" onPointerDown={this.onPointerDown} />
-                        <div className="resizer top-right" onPointerDown={this.onPointerDown} />
-                        <div className="resizer bottom-left" onPointerDown={this.onPointerDown} />
-                        <div className="resizer bottom-right" onPointerDown={this.onPointerDown} />
-                    </div>
-                </div>
-            );
-        }
-    }
-
-    @computed get zoomProgressivize() {
-        return PresBox.Instance?.activeItem?.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : null;
-    }
-
-    @computed get progressivize() {
-        return PresBox.Instance && this.props.progressivize ? PresBox.Instance.progressivizeChildDocs : null;
-    }
-
     @computed get presPaths() {
-        const presPaths = 'presPaths' + (this.props.presPaths ? '' : '-hidden');
-        return !PresBox.Instance || !this.props.presPaths ? null : (
+        return !this.props.presPaths() ? null : (
             <>
-                <div key="presorder">{PresBox.Instance.order}</div>
-                <svg key="svg" className={presPaths}>
+                <div key="presorder">{PresBox.Instance?.order}</div>
+                <svg key="svg" className="presPaths">
                     <defs>
                         <marker id="markerSquare" markerWidth="3" markerHeight="3" refX="1.5" refY="1.5" orient="auto" overflow="visible">
                             <rect x="0" y="0" width="3" height="3" stroke="#69a6db" strokeWidth="1" fill="white" fillOpacity="0.8" />
@@ -2140,7 +2112,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
                             <path d="M2,2 L2,6 L6,4 L2,2 Z" stroke="#69a6db" strokeLinejoin="round" strokeWidth="1" fill="white" fillOpacity="0.8" />
                         </marker>
                     </defs>
-                    {PresBox.Instance.paths}
+                    {this.props.presPaths()}
                 </svg>
             </>
         );
@@ -2180,8 +2152,6 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
                     />
                 )}
                 {this.presPaths}
-                {this.progressivize}
-                {this.zoomProgressivize}
             </div>
         );
     }
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 465dbfe6d..e5a2d9007 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -9,7 +9,7 @@ import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
-import { DocumentView } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionMulticolumnView.scss';
 import ResizeBar from './MulticolumnResizer';
@@ -241,9 +241,9 @@ export class CollectionMulticolumnView extends CollectionSubView() {
         }
         return this.props.addDocTab(doc, where);
     };
-    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
-    isChildContentActive = () =>
-        ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
+    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);
     getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
         return (
             <DocumentView
@@ -267,7 +267,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
                 hideResizeHandles={this.props.childHideResizeHandles?.()}
                 hideDecorationTitle={this.props.childHideDecorationTitle?.()}
                 fitContentsToBox={this.props.fitContentsToBox}
-                focus={this.props.focus}
+                focus={this.focusDocument}
                 docFilters={this.childDocFilters}
                 docRangeFilters={this.childDocRangeFilters}
                 searchFilterDocs={this.searchFilterDocs}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index f8de4e5de..d7f317f4f 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -9,7 +9,7 @@ import { DragManager, dropActionType } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
 import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
-import { DocumentView } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
 import { CollectionSubView } from '../CollectionSubView';
 import './CollectionMultirowView.scss';
 import HeightLabel from './MultirowHeightLabel';
@@ -241,9 +241,9 @@ export class CollectionMultirowView extends CollectionSubView() {
         }
         return this.props.addDocTab(doc, where);
     };
-    isContentActive = () => this.props.isSelected() || this.props.isContentActive();
-    isChildContentActive = () =>
-        ((this.props.childDocumentsActive?.() || this.Document._childDocumentsActive) && this.props.isDocumentActive?.() && SnappingManager.GetIsDragging()) || this.props.isSelected() || this.props.isAnyChildContentActive() ? true : false;
+    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);
     getDisplayDoc = (layout: Doc, dxf: () => Transform, width: () => number, height: () => number) => {
         return (
             <DocumentView
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 6cadeec41..e437d295e 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -198,9 +198,9 @@
         > .documentView-titleWrapper-hover {
             display: inline-block;
         }
-    }
-
-    > .documentView-styleWrapper {
+        > .documentView-contentsView {
+            opacity: 0.5;
+        }
         > .documentView-captionWrapper {
             opacity: 1;
         }
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 9e3b42cf6..b495a9399 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -93,7 +93,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @observable _expandBoolean: boolean = false;
     @observable _transitionTools: boolean = false;
     @observable _newDocumentTools: boolean = false;
-    @observable _progressivizeTools: boolean = false;
     @observable _openMovementDropdown: boolean = false;
     @observable _openEffectDropdown: boolean = false;
     @observable _presentTools: boolean = false;
@@ -206,8 +205,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         targetDoc._viewTransition = 'all 1s';
         setTimeout(() => (targetDoc._viewTransition = undefined), 1010);
         this.nextKeyframe(targetDoc, activeItem);
-        if (activeItem.presProgressivize) CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, targetDoc);
-        else targetDoc.keyFrameEditing = true;
+        targetDoc.keyFrameEditing = true;
     };
 
     _mediaTimer!: [NodeJS.Timeout, Doc];
@@ -246,22 +244,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // Called when the user activates 'next' - to move to the next part of the pres. trail
     @action
     next = () => {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        const lastFrame = Cast(targetDoc?.lastFrame, 'number', null);
-        const curFrame = NumCast(targetDoc?._currentFrame);
-        let internalFrames: boolean = false;
-        if (activeItem.presProgressivize || activeItem.zoomProgressivize || targetDoc.scrollProgressivize) internalFrames = true;
-        if (internalFrames && lastFrame !== undefined && curFrame < lastFrame) {
-            // Case 1: There are still other frames and should go through all frames before going to next slide
-            this.nextInternalFrame(targetDoc, activeItem);
-        } else if (this.childDocs[this.itemIndex + 1] !== undefined) {
-            // Case 2: No more frames in current doc and next slide is defined, therefore move to next slide
+        if (this.childDocs[this.itemIndex + 1] !== undefined) {
+            // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide
             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);
         } else if (this.childDocs[this.itemIndex + 1] === undefined && (this.layoutDoc.presLoop || this.layoutDoc.presStatus === PresStatus.Edit)) {
-            // Case 3: Last slide and presLoop is toggled ON or it is in Edit mode
+            // Case 2: Last slide and presLoop is toggled ON or it is in Edit mode
             this.nextSlide(0);
         }
         return this.itemIndex;
@@ -551,51 +540,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     }
 
-    /**
-     * Uses the viewfinder to progressivize through the different views of a single collection.
-     * @param activeItem: document for which internal zoom is used
-     */
-    zoomProgressivizeNext = (activeItem: Doc) => {
-        const targetDoc: Doc = this.targetDoc;
-        const srcContext = Cast(targetDoc?.context, Doc, null);
-        const docView = DocumentManager.Instance.getDocumentView(targetDoc);
-        const vfLeft = this.checkList(targetDoc, activeItem['viewfinder-left-indexed']);
-        const vfWidth = this.checkList(targetDoc, activeItem['viewfinder-width-indexed']);
-        const vfTop = this.checkList(targetDoc, activeItem['viewfinder-top-indexed']);
-        const vfHeight = this.checkList(targetDoc, activeItem['viewfinder-height-indexed']);
-        // Case 1: document that is not a Golden Layout tab
-        if (srcContext) {
-            const srcDocView = DocumentManager.Instance.getDocumentView(srcContext);
-            if (srcDocView) {
-                const layoutdoc = Doc.Layout(targetDoc);
-                const panelWidth: number = srcDocView.props.PanelWidth();
-                const panelHeight: number = srcDocView.props.PanelHeight();
-                const newPanX = NumCast(targetDoc.x) + NumCast(layoutdoc._width) / 2;
-                const newPanY = NumCast(targetDoc.y) + NumCast(layoutdoc._height) / 2;
-                const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight);
-                srcContext._panX = newPanX + (vfLeft + vfWidth / 2);
-                srcContext._panY = newPanY + (vfTop + vfHeight / 2);
-                srcContext._viewScale = newScale;
-            }
-        }
-        // Case 2: document is the containing collection
-        if (docView && !srcContext) {
-            const panelWidth: number = docView.props.PanelWidth();
-            const panelHeight: number = docView.props.PanelHeight();
-            const newScale = 0.9 * Math.min(Number(panelWidth) / vfWidth, Number(panelHeight) / vfHeight);
-            targetDoc._panX = vfLeft + vfWidth / 2;
-            targetDoc._panY = vfTop + vfWidth / 2;
-            targetDoc._viewScale = newScale;
-        }
-        const resize = document.getElementById('resizable');
-        if (resize) {
-            resize.style.width = vfWidth + 'px';
-            resize.style.height = vfHeight + 'px';
-            resize.style.top = vfTop + 'px';
-            resize.style.left = vfLeft + 'px';
-        }
-    };
-
     /**
      * For 'Hide Before' and 'Hide After' buttons making sure that
      * they are hidden each time the presentation is updated.
@@ -643,6 +587,7 @@ 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 + 1 === this.childDocs.length ? 0 : startSlide);
         clearTimeout(this._presTimer);
@@ -682,14 +627,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     // The function allows for viewing the pres path on toggle
-    @action togglePath = (srcContext: Doc, off?: boolean) => {
-        if (off) {
-            this._pathBoolean = false;
-            srcContext.presPathView = false;
-        } else {
-            runInAction(() => (this._pathBoolean = !this._pathBoolean));
-            srcContext.presPathView = this._pathBoolean;
-        }
+    @action togglePath = (off?: boolean) => {
+        this._pathBoolean = off ? false : !this._pathBoolean;
+        CollectionFreeFormView.ShowPresPaths = this._pathBoolean;
     };
 
     // The function allows for expanding the view of pres on toggle
@@ -1014,17 +954,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     };
 
-    /**
-     *
-     */
-    @action
-    viewPaths = () => {
-        const srcContext = Cast(this.rootDoc.presCollection, Doc, null);
-        if (srcContext) {
-            this.togglePath(srcContext);
-        }
-    };
-
     getAllIndexes = (arr: Doc[], val: Doc) => arr.map((doc, i) => (doc === val ? i : -1)).filter(i => i !== -1);
 
     // Adds the index in the pres path graphically
@@ -1104,16 +1033,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      */
     @computed get paths() {
         let pathPoints = '';
-        const presCollection = Cast(this.rootDoc.presCollection, Doc, null);
         this.childDocs.forEach((doc, index) => {
             const tagDoc = Cast(doc.presentationTargetDoc, Doc, null);
-            const srcContext = Cast(tagDoc?.context, Doc, null);
-            if (tagDoc && presCollection === srcContext) {
+            if (tagDoc) {
                 const n1x = NumCast(tagDoc.x) + NumCast(tagDoc._width) / 2;
                 const n1y = NumCast(tagDoc.y) + NumCast(tagDoc._height) / 2;
                 if ((index = 0)) pathPoints = n1x + ',' + n1y;
                 else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
-            } else if (doc.presPinView && presCollection === tagDoc) {
+            } else if (doc.presPinView) {
                 const n1x = NumCast(doc.presPinViewX);
                 const n1y = NumCast(doc.presPinViewY);
                 if ((index = 0)) pathPoints = n1x + ',' + n1y;
@@ -1137,6 +1064,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             />
         );
     }
+    getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection.  or better yet, draw everything in an overlay layer instad of within a collection
 
     // Converts seconds to ms and updates presTransition
     setTransitionTime = (number: String, change?: number) => {
@@ -1840,7 +1768,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
             // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
         }
-        // if (tagDoc.editScrollProgressivize) CollectionFreeFormDocumentView.updateScrollframe(tagDoc, currentFrame);
         CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
         tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
         tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
@@ -1882,123 +1809,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @observable private openActiveColorPicker: boolean = false;
     @observable private openViewedColorPicker: boolean = false;
 
-    @computed get progressivizeDropdown() {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        if (activeItem && targetDoc) {
-            const activeFontColor = targetDoc['pres-text-color'] ? StrCast(targetDoc['pres-text-color']) : 'Black';
-            const viewedFontColor = targetDoc['pres-text-viewed-color'] ? StrCast(targetDoc['pres-text-viewed-color']) : 'Black';
-            return (
-                <div className={`presBox-ribbon ${this._progressivizeTools && this.layoutDoc.presStatus === 'edit' ? 'active' : ''}`} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
-                    <div className="ribbon-box">
-                        {this.stringType} selected
-                        <div
-                            className="ribbon-doubleButton"
-                            style={{
-                                borderTop: 'solid 1px darkgrey',
-                                display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
-                            }}>
-                            <div className="ribbon-toggle" style={{ backgroundColor: activeItem.presProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeChild}>
-                                Contents
-                            </div>
-                            <div className="ribbon-toggle" style={{ opacity: activeItem.presProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editProgressivize}>
-                                Edit
-                            </div>
-                        </div>
-                        <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
-                            <div className="presBox-subheading">Active text color</div>
-                            <div
-                                className="ribbon-colorBox"
-                                style={{ backgroundColor: activeFontColor, height: 15, width: 15 }}
-                                onClick={action(() => {
-                                    this.openActiveColorPicker = !this.openActiveColorPicker;
-                                })}></div>
-                        </div>
-                        {this.activeColorPicker}
-                        <div className="ribbon-doubleButton" style={{ display: activeItem.presProgressivize ? 'inline-flex' : 'none' }}>
-                            <div className="presBox-subheading">Viewed font color</div>
-                            <div className="ribbon-colorBox" style={{ backgroundColor: viewedFontColor, height: 15, width: 15 }} onClick={action(() => (this.openViewedColorPicker = !this.openViewedColorPicker))}></div>
-                        </div>
-                        {this.viewedColorPicker}
-                        <div
-                            className="ribbon-doubleButton"
-                            style={{ borderTop: 'solid 1px darkgrey', display: (targetDoc.type === DocumentType.COL && targetDoc._viewType === 'freeform') || targetDoc.type === DocumentType.IMG ? 'inline-flex' : 'none' }}>
-                            <div className="ribbon-toggle" style={{ backgroundColor: activeItem.zoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeZoom}>
-                                Zoom
-                            </div>
-                            <div className="ribbon-toggle" style={{ opacity: activeItem.zoomProgressivize ? 1 : 0.4, backgroundColor: activeItem.editZoomProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editZoomProgressivize}>
-                                Edit
-                            </div>
-                        </div>
-                        <div
-                            className="ribbon-doubleButton"
-                            style={{
-                                borderTop: 'solid 1px darkgrey',
-                                display: targetDoc._viewType === 'stacking' || targetDoc.type === DocumentType.PDF || targetDoc.type === DocumentType.WEB || targetDoc.type === DocumentType.RTF ? 'inline-flex' : 'none',
-                            }}>
-                            <div className="ribbon-toggle" style={{ backgroundColor: activeItem.scrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.progressivizeScroll}>
-                                Scroll
-                            </div>
-                            <div className="ribbon-toggle" style={{ opacity: activeItem.scrollProgressivize ? 1 : 0.4, backgroundColor: targetDoc.editScrollProgressivize ? Colors.LIGHT_BLUE : '' }} onClick={this.editScrollProgressivize}>
-                                Edit
-                            </div>
-                        </div>
-                    </div>
-                    <div className="ribbon-final-box">
-                        Frames
-                        <div className="ribbon-doubleButton">
-                            <div className="ribbon-frameSelector">
-                                <div
-                                    key="back"
-                                    title="back frame"
-                                    className="backKeyframe"
-                                    onClick={e => {
-                                        e.stopPropagation();
-                                        this.prevKeyframe(targetDoc, activeItem);
-                                    }}>
-                                    <FontAwesomeIcon icon={'caret-left'} size={'lg'} />
-                                </div>
-                                <div
-                                    key="num"
-                                    title="toggle view all"
-                                    className="numKeyframe"
-                                    style={{ color: targetDoc.keyFrameEditing ? 'white' : 'black', backgroundColor: targetDoc.keyFrameEditing ? Colors.MEDIUM_BLUE : Colors.LIGHT_BLUE }}
-                                    onClick={action(() => (targetDoc.keyFrameEditing = !targetDoc.keyFrameEditing))}>
-                                    {NumCast(targetDoc._currentFrame)}
-                                </div>
-                                <div
-                                    key="fwd"
-                                    title="forward frame"
-                                    className="fwdKeyframe"
-                                    onClick={e => {
-                                        e.stopPropagation();
-                                        this.nextKeyframe(targetDoc, activeItem);
-                                    }}>
-                                    <FontAwesomeIcon icon={'caret-right'} size={'lg'} />
-                                </div>
-                            </div>
-                            <Tooltip
-                                title={
-                                    <>
-                                        <div className="dash-tooltip">{'Last frame'}</div>
-                                    </>
-                                }>
-                                <div className="ribbon-property">{NumCast(targetDoc.lastFrame)}</div>
-                            </Tooltip>
-                        </div>
-                        <div className="ribbon-frameList">
-                            {this.frameListHeader}
-                            {this.frameList}
-                        </div>
-                        <div className="ribbon-toggle" style={{ height: 20, backgroundColor: Colors.LIGHT_BLUE }} onClick={() => console.log(' TODO: play frames')}>
-                            Play
-                        </div>
-                    </div>
-                </div>
-            );
-        }
-    }
-
     @undoBatch
     @action
     switchActive = (color: ColorState) => {
@@ -2033,262 +1843,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     @action
-    turnOffEdit = (paths?: boolean) => {
-        // Turn off paths
-        if (paths) {
-            const srcContext = Cast(this.rootDoc.presCollection, Doc, null);
-            if (srcContext) this.togglePath(srcContext, true);
-        }
-        // Turn off the progressivize editors for each document
-        this.childDocs.forEach(doc => {
-            doc.editSnapZoomProgressivize = false;
-            doc.editZoomProgressivize = false;
-            const targetDoc = Cast(doc.presentationTargetDoc, Doc, null);
-            if (targetDoc) {
-                targetDoc.editZoomProgressivize = false;
-                // targetDoc.editScrollProgressivize = false;
-            }
-        });
-    };
-
-    //Toggle whether the user edits or not
-    @action
-    editZoomProgressivize = (e: React.MouseEvent) => {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        if (!targetDoc.editZoomProgressivize) {
-            if (!activeItem.zoomProgressivize) activeItem.zoomProgressivize = true;
-            targetDoc.zoomProgressivize = true;
-            targetDoc.editZoomProgressivize = true;
-            activeItem.editZoomProgressivize = true;
-        } else {
-            targetDoc.editZoomProgressivize = false;
-            activeItem.editZoomProgressivize = false;
-        }
-    };
-
-    //Toggle whether the user edits or not
-    @action
-    editScrollProgressivize = (e: React.MouseEvent) => {
-        const targetDoc: Doc = this.targetDoc;
-        if (!targetDoc.editScrollProgressivize) {
-            if (!targetDoc.scrollProgressivize) {
-                targetDoc.scrollProgressivize = true;
-                this.activeItem.scrollProgressivize = true;
-            }
-            targetDoc.editScrollProgressivize = true;
-        } else {
-            targetDoc.editScrollProgressivize = false;
-        }
-    };
-
-    //Progressivize Zoom
-    @action
-    progressivizeScroll = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        this.activeItem.scrollProgressivize = !this.activeItem.scrollProgressivize;
-        const targetDoc: Doc = this.targetDoc;
-        targetDoc.scrollProgressivize = !targetDoc.scrollProgressivize;
-        // CollectionFreeFormDocumentView.setupScroll(targetDoc, NumCast(targetDoc._currentFrame));
-        if (targetDoc.editScrollProgressivize) {
-            targetDoc.editScrollProgressivize = false;
-            targetDoc._currentFrame = 0;
-            targetDoc.lastFrame = 0;
-        }
-    };
-
-    //Progressivize Zoom
-    @action
-    progressivizeZoom = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        const activeItem: Doc = this.activeItem;
-        activeItem.zoomProgressivize = !activeItem.zoomProgressivize;
-        const targetDoc: Doc = this.targetDoc;
-        targetDoc.zoomProgressivize = !targetDoc.zoomProgressivize;
-        CollectionFreeFormDocumentView.setupZoom(activeItem, targetDoc);
-        if (activeItem.editZoomProgressivize) {
-            activeItem.editZoomProgressivize = false;
-            targetDoc._currentFrame = 0;
-            targetDoc.lastFrame = 0;
-        }
-    };
-
-    //Progressivize Child Docs
-    @action
-    editProgressivize = (e: React.MouseEvent) => {
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        targetDoc._currentFrame = targetDoc.lastFrame;
-        if (!targetDoc.editProgressivize) {
-            if (!activeItem.presProgressivize) {
-                activeItem.presProgressivize = true;
-                targetDoc.presProgressivize = true;
-            }
-            targetDoc.editProgressivize = true;
-        } else {
-            targetDoc.editProgressivize = false;
-        }
-    };
-
-    @action
-    progressivizeChild = (e: React.MouseEvent) => {
-        e.stopPropagation();
-        const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
-        const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
-        if (!activeItem.presProgressivize) {
-            targetDoc.keyFrameEditing = false;
-            activeItem.presProgressivize = true;
-            targetDoc.presProgressivize = true;
-            targetDoc._currentFrame = 0;
-            docs.forEach((doc, i) => CollectionFreeFormDocumentView.setupKeyframes([doc], i, true));
-            targetDoc.lastFrame = targetDoc.lastFrame ? NumCast(targetDoc.lastFrame) : docs.length - 1;
-        } else {
-            // targetDoc.editProgressivize = false;
-            activeItem.presProgressivize = false;
-            targetDoc.presProgressivize = false;
-            targetDoc._currentFrame = 0;
-            targetDoc.keyFrameEditing = true;
-        }
-    };
-
-    @action
-    checkMovementLists = (doc: Doc, xlist: any, ylist: any) => {
-        const x: List<number> = xlist;
-        const y: List<number> = ylist;
-        const tags: JSX.Element[] = [];
-        let pathPoints = ''; //List of all of the pathpoints that need to be added
-        for (let i = 0; i < x.length - 1; i++) {
-            if (y[i] || x[i]) {
-                if (i === 0) pathPoints = x[i] - 11 + ',' + (y[i] + 33);
-                else pathPoints = pathPoints + ' ' + (x[i] - 11) + ',' + (y[i] + 33);
-                tags.push(
-                    <div className="progressivizeMove-frame" style={{ position: 'absolute', top: y[i], left: x[i] }}>
-                        {i}
-                    </div>
-                );
-            }
-        }
-        tags.push(
-            <svg style={{ overflow: 'visible', position: 'absolute' }}>
-                <polyline
-                    points={pathPoints}
-                    style={{
-                        position: 'absolute',
-                        opacity: 1,
-                        stroke: '#000000',
-                        strokeWidth: 2,
-                        strokeDasharray: '10 5',
-                    }}
-                    fill="none"
-                />
-            </svg>
-        );
-        return tags;
-    };
-
-    @observable
-    toggleDisplayMovement = (doc: Doc) => (doc.displayMovement = !doc.displayMovement);
-
-    @action
-    checkList = (doc: Doc, list: any): number => {
-        const x: List<number> = list;
-        if (x?.length >= NumCast(doc._currentFrame) + 1) {
-            return x[NumCast(doc._currentFrame)];
-        } else if (x) {
-            x.length = NumCast(doc._currentFrame) + 1;
-            x[NumCast(doc._currentFrame)] = x[NumCast(doc._currentFrame) - 1];
-            return x[NumCast(doc._currentFrame)];
-        }
-        return 100;
-    };
-
-    @computed get progressivizeChildDocs() {
-        const targetDoc: Doc = this.targetDoc;
-        const docs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
-        const tags: JSX.Element[] = [];
-        docs.forEach((doc, index) => {
-            if (doc['x-indexed'] && doc['y-indexed']) {
-                tags.push(<div style={{ position: 'absolute', display: doc.displayMovement ? 'block' : 'none' }}>{this.checkMovementLists(doc, doc['x-indexed'], doc['y-indexed'])}</div>);
-            }
-            tags.push(
-                <div
-                    className="progressivizeButton"
-                    key={index}
-                    onPointerLeave={() => {
-                        if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0;
-                    }}
-                    onPointerOver={() => {
-                        if (NumCast(targetDoc._currentFrame) < NumCast(doc.appearFrame)) doc.opacity = 0.5;
-                    }}
-                    onClick={e => {
-                        this.toggleDisplayMovement(doc);
-                        e.stopPropagation();
-                    }}
-                    style={{ backgroundColor: doc.displayMovement ? Colors.LIGHT_BLUE : '#c8c8c8', top: NumCast(doc.y), left: NumCast(doc.x) }}>
-                    <div className="progressivizeButton-prev">
-                        <FontAwesomeIcon
-                            icon={'caret-left'}
-                            size={'lg'}
-                            onClick={e => {
-                                e.stopPropagation();
-                                this.prevAppearFrame(doc, index);
-                            }}
-                        />
-                    </div>
-                    <div className="progressivizeButton-frame">{NumCast(doc.appearFrame)}</div>
-                    <div className="progressivizeButton-next">
-                        <FontAwesomeIcon
-                            icon={'caret-right'}
-                            size={'lg'}
-                            onClick={e => {
-                                e.stopPropagation();
-                                this.nextAppearFrame(doc, index);
-                            }}
-                        />
-                    </div>
-                </div>
-            );
-        });
-        return tags;
-    }
-
-    @action
-    nextAppearFrame = (doc: Doc, i: number) => {
-        doc.appearFrame = (Cast(doc.appearFrame, 'number', null) ?? 0) + 1;
-        this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
-    };
-
-    @action
-    prevAppearFrame = (doc: Doc, i: number) => {
-        doc.appearFrame = Math.max(0, (Cast(doc.appearFrame, 'number', null) ?? 0) - 1);
-        this.updateOpacityList(doc['opacity-indexed'], NumCast(doc.appearFrame));
-    };
-
-    @action
-    updateOpacityList = (list: any, frame: number) => {
-        const x: List<number> = list;
-        if (x && x.length >= frame) {
-            for (let i = 0; i < x.length; i++) {
-                if (i < frame) {
-                    x[i] = 0;
-                } else if (i >= frame) {
-                    x[i] = 1;
-                }
-            }
-            list = x;
-        } else {
-            x.length = frame + 1;
-            for (let i = 0; i < x.length; i++) {
-                if (i < frame) {
-                    x[i] = 0;
-                } else if (i >= frame) {
-                    x[i] = 1;
-                }
-            }
-            list = x;
-        }
-    };
+    turnOffEdit = (paths?: boolean) => paths && this.togglePath(true); // Turn off paths
 
     @computed
     get toolbarWidth(): number {
@@ -2311,11 +1866,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     <FontAwesomeIcon icon={"plus"} />
                     <FontAwesomeIcon className={`dropdown ${this.newDocumentTools ? "active" : ""}`} icon={"angle-down"} />
                 </div></Tooltip> */}
-                <Tooltip title={<div className="dash-tooltip">{'View paths'}</div>}>
+                <Tooltip title={<div className="dash-tooltip">View paths</div>}>
                     <div
-                        style={{ opacity: this.childDocs.length > 1 && this.layoutDoc.presCollection ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
+                        style={{ opacity: this.childDocs.length > 1 ? 1 : 0.3, color: this._pathBoolean ? Colors.MEDIUM_BLUE : 'white', width: isMini ? '100%' : undefined }}
                         className={'toolbar-button'}
-                        onClick={this.childDocs.length > 1 && this.layoutDoc.presCollection ? this.viewPaths : undefined}>
+                        onClick={this.childDocs.length > 1 ? () => this.togglePath() : undefined}>
                         <FontAwesomeIcon icon={'exchange-alt'} />
                     </div>
                 </Tooltip>
-- 
cgit v1.2.3-70-g09d2


From 19f317bd43a7cc8df0daf1c0642011cc8754e14b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 15 Nov 2022 12:49:53 -0500
Subject: made InPlace container tool button apply more settings to be more
 like a template. added followAllLInks flag.  added image anchors to save pan
 zoom.  added follow link button bar option for follow all links.  added
 hideDecorations flag and property

---
 src/client/documents/Documents.ts                  |  5 ++
 src/client/util/LinkFollower.ts                    | 11 ++--
 src/client/views/DocumentButtonBar.scss            | 31 ++++++++++-
 src/client/views/DocumentButtonBar.tsx             | 60 +++++++++++++++-------
 src/client/views/DocumentDecorations.tsx           | 19 +++----
 src/client/views/PropertiesButtons.tsx             | 17 ++++--
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  2 +-
 src/client/views/linking/LinkEditor.tsx            |  1 -
 src/client/views/nodes/DocumentView.tsx            |  3 +-
 src/client/views/nodes/ImageBox.tsx                | 52 ++++++++++++++++---
 src/client/views/nodes/trails/PresBox.tsx          |  3 +-
 11 files changed, 154 insertions(+), 50 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index e44004f45..8f45802fe 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -257,6 +257,7 @@ export class DocumentOptions {
     lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide)
     activeFrame?: number; // the active frame of a document in a frame base collection
     appearFrame?: number; // the frame in which the document appears
+    viewTransitionTime?: number; // transition duration for view parameters
     presTransition?: number; //the time taken for the transition TO a document
     presDuration?: number; //the duration of the slide in presentation view
     borderRounding?: string;
@@ -1047,6 +1048,10 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
         }
 
+        export function ImageanchorDocument(options: DocumentOptions = {}, id?: string) {
+            return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id);
+        }
+
         export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) {
             return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id);
         }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index c09c9d1c5..c6fc7b372 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,6 +1,7 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, StrCast } from '../../fields/Types';
+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';
@@ -76,11 +77,11 @@ export class LinkFollower {
         const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
         const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
         const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
-        const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor2 as Doc).length === 0);
-        const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0);
+        const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor2 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0);
+        const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0);
         const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
-        const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
-        const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1);
+        const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
+        const followLinks = sourceDoc.isPushpin || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
         var count = 0;
         const allFinished = () => ++count === followLinks.length && finished?.();
         followLinks.forEach(async linkDoc => {
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index 1e93ba5e2..f9c988fdd 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -18,15 +18,44 @@ $linkGap: 3px;
     cursor: pointer;
 }
 
+.documentButtonBar-followTypes,
 .documentButtonBar-pinTypes {
     position: absolute;
-    display: flex;
+    display: none;
     width: 60px;
     top: -14px;
     background: black;
     height: 20px;
     align-items: center;
 }
+.documentButtonBar-followTypes {
+    width: 20px;
+    display: none;
+}
+.documentButtonBar-followIcon {
+    align-items: center;
+    display: flex;
+    height: 100%;
+    &:hover {
+        background-color: lightblue;
+    }
+}
+.documentButtonBar-follow {
+    &:hover {
+        .documentButtonBar-followTypes {
+            display: flex;
+        }
+    }
+}
+.documentButtonBar-pin {
+    color: white;
+    &:hover {
+        .documentButtonBar-pinTypes {
+            display: flex;
+        }
+    }
+}
+
 .documentButtonBar-pinIcon {
     &:hover {
         background-color: lightblue;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 36875290e..794b51cc5 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -5,7 +5,7 @@ import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
-import { Cast, NumCast } from '../../fields/Types';
+import { BoolCast, Cast, NumCast } from '../../fields/Types';
 import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
@@ -209,21 +209,52 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             </Tooltip>
         );
     }
+    @observable subFollow = '';
     @computed
     get followLinkButton() {
         const targetDoc = this.view0?.props.Document;
+        const followBtn = (allDocs: boolean, click: (doc: Doc) => void, isSet: (doc?: Doc) => boolean, icon: IconProp) => {
+            const tooltip = `Follow ${this.subPin}documents`;
+            return !tooltip ? null : (
+                <Tooltip title={<div className="dash-tooltip">{tooltip}</div>}>
+                    <div className="documentButtonBar-followIcon" style={{ backgroundColor: isSet(targetDoc) ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: isSet(targetDoc) ? Colors.BLACK : Colors.WHITE }}>
+                        <FontAwesomeIcon
+                            className="documentdecorations-icon"
+                            style={{ width: 20 }}
+                            key={icon.toString()}
+                            size="sm"
+                            icon={icon}
+                            onPointerEnter={action(e => (this.subPin = allDocs ? 'All ' : ''))}
+                            onPointerLeave={action(e => (this.subPin = ''))}
+                            onClick={e => {
+                                this.props.views().forEach(dv => click(dv!.rootDoc));
+                                e.stopPropagation();
+                            }}
+                        />
+                    </div>
+                </Tooltip>
+            );
+        };
         return !targetDoc ? null : (
-            <Tooltip title={<div className="dash-tooltip">{'Set onClick to follow primary link'}</div>}>
+            <Tooltip title={<div className="dash-tooltip">Set onClick to follow primary link</div>}>
                 <div
-                    className="documentButtonBar-icon"
+                    className="documentButtonBar-icon documentButtonBar-follow"
                     style={{ backgroundColor: targetDoc.isLinkButton ? Colors.LIGHT_BLUE : Colors.DARK_GRAY, color: targetDoc.isLinkButton ? Colors.BLACK : Colors.WHITE }}
                     onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, undefined, false)))}>
+                    <div className="documentButtonBar-followTypes">
+                        {followBtn(
+                            true,
+                            (doc: Doc) => (doc.followAllLinks = !doc.followAllLinks),
+                            (doc?: Doc) => (doc?.followAllLinks ? true : false),
+                            'window-maximize'
+                        )}
+                    </div>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="hand-point-right" />
                 </div>
             </Tooltip>
         );
     }
-    @observable expandPin = false;
+
     @observable subPin = '';
     @computed
     get pinButton() {
@@ -264,10 +295,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-tooltip">{`Pin Document ${SelectionManager.Views().length > 1 ? 'multiple documents' : ''} to Trail`}</div>}>
                 <div
-                    className="documentButtonBar-icon"
-                    style={{ color: 'white' }}
-                    onPointerEnter={action(e => (this.expandPin = true))}
-                    onPointerLeave={action(e => (this.expandPin = false))}
+                    className="documentButtonBar-icon documentButtonBar-pin"
                     onClick={e => {
                         const docs = this.props
                             .views()
@@ -276,13 +304,11 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                         TabDocView.PinDoc(docs, { pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
                         e.stopPropagation();
                     }}>
-                    {this.expandPin ? (
-                        <div className="documentButtonBar-pinTypes">
-                            {pinBtn(true, false, 'window-maximize')}
-                            {pinBtn(false, true, 'address-card')}
-                            {pinBtn(true, true, 'id-card')}
-                        </div>
-                    ) : null}
+                    <div className="documentButtonBar-pinTypes">
+                        {pinBtn(true, false, 'window-maximize')}
+                        {pinBtn(false, true, 'address-card')}
+                        {pinBtn(true, true, 'id-card')}
+                    </div>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="map-pin" />
                 </div>
             </Tooltip>
@@ -460,9 +486,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                 ) : null}
                 {
                     Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
-                    /*<div className="documentButtonBar-button">
-                        {this.metadataButton}
-                    </div> */
+                    /*<div className="documentButtonBar-button"> {this.metadataButton} </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>
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3fac137fe..3efb5fb37 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -708,24 +708,25 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
     }
 
     render() {
-        const { b, c, r, x, y } = this.Bounds;
-        const bounds = { b, c, r, x, y };
+        const { b, r, x, y } = this.Bounds;
+        const bounds = { b, r, x, y };
         const seldocview = SelectionManager.Views().slice(-1)[0];
         if (SnappingManager.GetIsDragging() || bounds.r - bounds.x < 1 || bounds.x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(bounds.r) || isNaN(bounds.b) || isNaN(bounds.x) || isNaN(bounds.y)) {
             return null;
         }
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
-        const hideResizers = seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
-        const hideTitle = seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
-        const hideDocumentButtonBar = seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
+        const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations;
+        const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || seldocview.rootDoc._isGroup || this._isRounding || this._isRotating;
+        const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
+        const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
         // if multiple documents have been opened at the same time, then don't show open button
-        const hideOpenButton =
+        const hideOpenButton =hideDecorations || 
             seldocview.props.hideOpenButton ||
             seldocview.rootDoc.hideOpenButton ||
             SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
-        const hideDeleteButton =
+        const hideDeleteButton =hideDecorations || 
             this._isRounding ||
             this._isRotating ||
             seldocview.props.hideDeleteButton ||
@@ -767,7 +768,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 && seldocview.props.CollectionFreeFormDocumentView; // when do we want an object to not rotate?
+        const useRotation = !hideResizers && 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 : '';
@@ -837,7 +838,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}>
                                 {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')}
                                 {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')}
-                                {titleArea}
+                                {hideTitle ? null : titleArea}
                                 {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')}
                             </div>
                             {hideResizers ? null : (
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 5992b3eb9..d27848a90 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -121,15 +121,24 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
-                    containerDoc.noShadow = containerDoc.noHighlighting = containerDoc._isLinkButton = containerDoc._fitContentsToBox = containerDoc._forceActive = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
+                    containerDoc.followAllLinks =
+                        containerDoc.noShadow =
+                        containerDoc.noHighlighting =
+                        containerDoc._isLinkButton =
+                        containerDoc._fitContentsToBox =
+                        containerDoc._forceActive =
+                        containerDoc._isInPlaceContainer =
+                            !containerDoc._isInPlaceContainer;
                     containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? 'inPlace' : undefined;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
                     const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
                     if (menuDoc) {
-                        menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer;
-                        DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
+                        menuDoc.hideDecorations = menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer;
+                        if (!dv.allLinks.find(link => link.anchor1 === menuDoc || link.anchor2 === menuDoc)) {
+                            DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
+                        }
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
-                            menuItem._isLinkButton = true;
+                            menuItem.followAllLinks = menuItem._isLinkButton = true;
                             menuItem._followLinkLocation = 'inPlace';
                         });
                     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index e24b116d0..8fe5ad63f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1157,7 +1157,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
         //     SelectionManager.DeselectAll();
         // }
-        if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) {
+        if (this.props.Document.scrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined || doc.type === DocumentType.MARKER) {
             this.props.focus(doc, options);
         } else {
             const xfToCollection = options?.docTransform ?? Transform.Identity();
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 2fb8e3d4c..3e8867c50 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -206,7 +206,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @computed
     get editAudioFollow() {
         //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        console.log('AudioFollow:' + this.audioFollow);
         return (
             <div className="linkEditor-zoomFollow">
                 <div className="linkEditor-zoomFollow-label">Play Target Audio:</div>
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 7accd49a4..76d6d3532 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -166,7 +166,8 @@ export interface DocumentViewSharedProps {
 // these props are specific to DocuentViews
 export interface DocumentViewProps extends DocumentViewSharedProps {
     // properties specific to DocumentViews but not to FieldView
-    hideResizeHandles?: boolean; // whether to suppress DocumentDecorations when this document is selected
+    hideDecorations?: boolean; // whether to suppress all DocumentDecorations when doc is selected
+    hideResizeHandles?: boolean; // whether to suppress resized handles on doc decorations when this document is selected
     hideTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
     hideDecorationTitle?: boolean; // forces suppression of title. e.g, treeView document labels suppress titles in case they are globally active via settings
     hideDocumentButtonBar?: boolean;
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d0df41023..d67481b22 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -14,7 +14,7 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
-import { DocUtils } from '../../documents/Documents';
+import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
@@ -49,21 +49,58 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     private _dropDisposer?: DragManager.DragDropDisposer;
+    private _transitioning: any; // timer for setting view spec properties
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     @observable _curSuffix = '';
     @observable _uploadIcon = uploadIcons.idle;
+    @observable _focusViewScale: number | undefined = 1;
+    @observable _focusPanX: number | undefined = 0;
+    @observable _focusPanY: number | undefined = 0;
+    get viewScale() {
+        return this._focusViewScale || StrCast(this.layoutDoc._viewScale);
+    }
+    get panX() {
+        return this._focusPanX || StrCast(this.layoutDoc._panX);
+    }
+    get panY() {
+        return this._focusPanY || StrCast(this.layoutDoc._panY);
+    }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer?.();
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
     };
-    setViewSpec = (anchor: Doc, preview: boolean) => {}; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
+
+    @action
+    setViewSpec = (anchor: Doc, preview: boolean) => {
+        if (preview && anchor._viewScale !== undefined) {
+            this._focusViewScale = Cast(anchor._viewScale, 'number', null);
+            this._focusPanX = Cast(anchor._panX, 'number', null);
+            this._focusPanY = Cast(anchor._panX, 'number', null);
+        } else if (anchor._viewScale !== undefined) {
+            const smoothTime = NumCast(anchor.viewTransitionTime);
+            this.layoutDoc.viewTransition = `all ${smoothTime}ms`;
+            this.layoutDoc._panX = NumCast(anchor._panX, NumCast(this.layoutDoc._panY));
+            this.layoutDoc._panY = NumCast(anchor._panY, NumCast(this.layoutDoc._panX));
+            this.layoutDoc._viewScale = NumCast(anchor._viewScale, NumCast(this.layoutDoc._viewScale));
+            if (anchor.type === DocumentType.MARKER) {
+                this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(anchor._annotations));
+            }
+            clearTimeout(this._transitioning);
+            this._transitioning = setTimeout(() => (this.layoutDoc.viewTransition = undefined), smoothTime);
+        }
+    }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
 
     getAnchor = () => {
-        const anchor = this._getAnchor?.(this._savedAnnotations);
-        anchor && this.addDocument(anchor);
-        return anchor ?? this.rootDoc;
+        const zoomedAnchor = () => Docs.Create.ImageanchorDocument({ viewTransitionTime: 1000, _viewScale: NumCast(this.layoutDoc._viewScale), _panX: NumCast(this.layoutDoc._panX), _panY: NumCast(this.layoutDoc._panY) });
+        const anchor = this._getAnchor?.(this._savedAnnotations) ?? (this.layoutDoc.viewScale ? zoomedAnchor() : undefined);
+        if (anchor) {
+            anchor._annotations = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey]));
+            this.addDocument(anchor);
+            return anchor;
+        }
+        return this.rootDoc;
     };
 
     componentDidMount() {
@@ -90,6 +127,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
 
     componentWillUnmount() {
+        clearTimeout(this._transitioning);
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
@@ -283,9 +321,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     setTimeout(
                         action(() => {
                             this._uploadIcon = idle;
-                            if (data) {
-                                dataDoc[this.fieldKey] = data;
-                            }
+                            data && (dataDoc[this.fieldKey] = data);
                         }),
                         2000
                     );
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index b495a9399..8d805c663 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -509,8 +509,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
         if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) {
             const transTime = NumCast(activeItem.presTransition, 500);
-            const presTransitionTime = `all ${transTime}ms`;
-            targetDoc._dataTransition = presTransitionTime;
+            targetDoc._dataTransition = `all ${transTime}ms`;
             targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x));
             targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y));
             targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation));
-- 
cgit v1.2.3-70-g09d2


From ae324ff50865929be836edf3bbf129207638a9c9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Nov 2022 16:26:31 -0500
Subject: big changes to make link following use the same code as pinning docs
 for trails.

---
 src/client/documents/Documents.ts                  |   1 +
 src/client/util/DocumentManager.ts                 |  30 ++---
 src/client/util/LinkFollower.ts                    |   7 +-
 src/client/views/DocumentButtonBar.tsx             |   6 +-
 src/client/views/GlobalKeyHandler.ts               |   4 +-
 src/client/views/MarqueeAnnotator.tsx              |   1 +
 src/client/views/StyleProvider.tsx                 |   2 +-
 .../collections/CollectionStackedTimeline.tsx      |   2 +-
 src/client/views/collections/TabDocView.tsx        |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  18 ++-
 src/client/views/nodes/ComparisonBox.tsx           |   2 +-
 src/client/views/nodes/DocumentView.tsx            |   8 +-
 src/client/views/nodes/ImageBox.tsx                |  60 ++++------
 src/client/views/nodes/PDFBox.tsx                  |  16 ++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          | 131 +++++++++++++--------
 src/client/views/pdf/Annotation.tsx                |   2 +-
 src/client/views/pdf/PDFViewer.tsx                 |  11 +-
 18 files changed, 178 insertions(+), 129 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 8f45802fe..e68b9e27b 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -237,6 +237,7 @@ export class DocumentOptions {
     childContextMenuScripts?: List<ScriptField>;
     childContextMenuLabels?: List<string>;
     childContextMenuIcons?: List<string>;
+    followLinkZoom?: boolean; // whether to zoom to the target of a link
     hideLinkButton?: boolean; // whether the blue link counter button should be hidden
     hideDecorationTitle?: boolean;
     hideOpenButton?: boolean;
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 9336717c0..a60c1ed6b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -151,6 +151,21 @@ export class DocumentManager {
         return toReturn;
     }
 
+    static playAudioAnno(doc: Doc) {
+        const anno = Cast(doc[Doc.LayoutFieldKey(doc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement();
+        if (anno) {
+            if (anno instanceof AudioField) {
+                new Howl({
+                    src: [anno.url.href],
+                    format: ['mp3'],
+                    autoplay: true,
+                    loop: false,
+                    volume: 0.5,
+                });
+            }
+        }
+    }
+
     static addView = (doc: Doc, finished?: () => void) => {
         CollectionDockingView.AddSplit(doc, 'right');
         finished?.();
@@ -189,20 +204,6 @@ export class DocumentManager {
             } else {
                 finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
                 !noSelect && docView?.select(false);
-                if (originatingDoc?.followLinkAudio) {
-                    const anno = Cast(finalTargetDoc[Doc.LayoutFieldKey(finalTargetDoc) + '-audioAnnotations'], listSpec(AudioField), null)?.lastElement();
-                    if (anno) {
-                        if (anno instanceof AudioField) {
-                            new Howl({
-                                src: [anno.url.href],
-                                format: ['mp3'],
-                                autoplay: true,
-                                loop: false,
-                                volume: 0.5,
-                            });
-                        }
-                    }
-                }
             }
             finished?.();
         };
@@ -233,6 +234,7 @@ export class DocumentManager {
         }
         if (focusView) {
             !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
+            if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
                 focusView.focus(originalTarget ?? targetDoc, {
                     originalTarget,
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index c6fc7b372..68716a207 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -6,6 +6,7 @@ import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
 import { DocumentManager } from './DocumentManager';
+import { LinkManager } from './LinkManager';
 import { UndoManager } from './UndoManager';
 
 type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
@@ -25,7 +26,7 @@ export class LinkFollower {
     // follows a link - if the target is on screen, it highlights/pans to it.
     // if the target isn't onscreen, then it will open up the target in the lightbox, or in place
     // depending on the followLinkLocation property of the source (or the link itself as a fallback);
-    public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => {
+    public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean) => {
         const batch = UndoManager.StartBatch('follow link click');
         // open up target if it's not already in view ...
         const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => {
@@ -63,7 +64,6 @@ export class LinkFollower {
             linkDoc,
             sourceDoc,
             createViewFunc,
-            BoolCast(sourceDoc.followLinkZoom, zoom),
             docViewProps.ContainingCollectionDoc,
             action(() => {
                 batch.end();
@@ -73,7 +73,7 @@ export class LinkFollower {
         );
     };
 
-    public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
+    public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
         const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
         const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1
         const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2
@@ -96,6 +96,7 @@ export class LinkFollower {
                         : linkDoc.anchor1
                 ) as Doc;
                 if (target) {
+                    const zoom = BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false);
                     if (target.TourMap) {
                         const fieldKey = Doc.LayoutFieldKey(target);
                         const tour = DocListCast(target[fieldKey]).reverse();
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 794b51cc5..681349ccf 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -284,7 +284,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                                     .views()
                                     .filter(v => v)
                                     .map(dv => dv!.rootDoc);
-                                TabDocView.PinDoc(docs, { pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                                TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
                                 e.stopPropagation();
                             }}
                         />
@@ -301,7 +301,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                             .views()
                             .filter(v => v)
                             .map(dv => dv!.rootDoc);
-                        TabDocView.PinDoc(docs, { pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                        TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout: e.shiftKey, pinDocContent: e.altKey, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
                         e.stopPropagation();
                     }}>
                     <div className="documentButtonBar-pinTypes">
@@ -488,7 +488,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                     Doc.noviceMode ? null : <div className="documentButtonBar-button">{this.templateButton}</div>
                     /*<div className="documentButtonBar-button"> {this.metadataButton} </div> */
                 }
-                {Doc.noviceMode || !SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
+                {!SelectionManager.Views()?.some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
                 <div className="documentButtonBar-button">{this.pinButton}</div>
                 <div className="documentButtonBar-button">{this.recordButton}</div>
                 {!Doc.UserDoc()['documentLinksButton-fullMenu'] ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 5a6caf995..4890d9624 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -6,7 +6,7 @@ import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
-import { Cast, PromiseValue } from '../../fields/Types';
+import { Cast, DocCast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -245,7 +245,7 @@ export class KeyManager {
                 if (SelectionManager.Views().length === 1 && SelectionManager.Views()[0].ComponentView?.search) {
                     SelectionManager.Views()[0].ComponentView?.search?.('', false, false);
                 } else {
-                    const searchBtn = Doc.MySearcher;
+                    const searchBtn = DocListCast(Doc.MyLeftSidebarMenu.data).find(d => d.target === Doc.MySearcher);
                     if (searchBtn) {
                         MainView.Instance.selectMenu(searchBtn);
                     }
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index c0dd62a05..1162cde50 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -93,6 +93,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                 dragComplete: e => {
                     if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
                         e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+                        e.annoDragData.linkSourceDoc.followLinkZoom = false;
                     }
                 },
             });
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index a04f4a4f4..8ee5ebdb6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -236,7 +236,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                                 ? Colors.DARK_GRAY
                                 : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background
                             : doc.annotationOn
-                            ? '#00000015' // faint interior for collections on PDFs, images, etc
+                            ? '#00000010' // faint interior for collections on PDFs, images, etc
                             : doc?._isGroup
                             ? undefined
                             : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)'));
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 7bf798656..39ae470b6 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -748,7 +748,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                     time < NumCast(this.props.mark[this.props.endTag]) &&
                     this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5
                 ) {
-                    LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false, true);
+                    LinkFollower.FollowLink(undefined, this.props.mark, this.props as any as DocumentViewProps, false);
                 }
                 this._lastTimecode = time;
             }
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 1a9006356..e21649648 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -229,7 +229,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
      * Adds a document to the presentation view
      **/
     @action
-    public static PinDoc(docs: Doc | Doc[], pinProps?: PinProps) {
+    public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) {
         const docList = docs instanceof Doc ? [docs] : docs;
 
         const batch = UndoManager.StartBatch('pinning doc');
@@ -271,7 +271,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 pinDoc.presStartTime = NumCast(doc.clipStart);
                 pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
             }
-            PresBox.pinDocView(pinDoc, pinProps, doc);
+            PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, doc);
             pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
             Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
             //save position
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 932bd789d..8a97797c7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1530,11 +1530,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return 0;
     };
 
+    @action
+    scrollFocus = (anchor: Doc, smooth: boolean) => {
+        let focusSpeed: Opt<number>;
+        PresBox.restoreTargetDocView(
+            this.rootDoc, //
+            { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
+            anchor,
+            (focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition)),
+            {
+                pannable: anchor.presPinData ? true : false,
+            }
+        );
+        return focusSpeed;
+    }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
+
     getAnchor = () => {
         if (this.props.Document.annotationOn) {
             return this.rootDoc;
         }
-        const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), annotationOn: this.rootDoc });
+        const anchor = Docs.Create.TextanchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), presTransition: 500, annotationOn: this.rootDoc });
+        PresBox.pinDocView(anchor, { pinData: { pannable: true } }, this.rootDoc);
         const proto = Doc.GetProto(anchor);
         proto[ViewSpecPrefix + '_viewType'] = this.layoutDoc._viewType;
         proto.docFilters = ObjectField.MakeCopy(this.layoutDoc.docFilters as ObjectField) || new List<string>([]);
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index d74da9748..dd03b9b99 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -103,7 +103,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                 <>
                     <DocumentView
                         ref={r => {
-                            whichDoc !== targetDoc && r?.focus(whichDoc, {});
+                            whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
                         }}
                         {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
                         isContentActive={returnFalse}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 84cacd919..dc468cf89 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -51,7 +51,7 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { LinkDocPreview } from './LinkDocPreview';
 import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
-import { PresBox } from './trails/PresBox';
+import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
 const { Howl } = require('howler');
 
@@ -144,7 +144,7 @@ export interface DocumentViewSharedProps {
     addDocument?: (doc: Doc | Doc[]) => boolean;
     removeDocument?: (doc: Doc | Doc[]) => boolean;
     moveDocument?: (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
-    pinToPres: (document: Doc) => void;
+    pinToPres: (document: Doc, pinProps: PinProps) => void;
     ScreenToLocalTransform: () => Transform;
     bringToFront: (doc: Doc, sendToBack?: boolean) => void;
     canEmbedOnDrag?: boolean;
@@ -486,7 +486,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 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: 'Pin', event: () => this.props.pinToPres(this.props.Document), icon: 'map-pin', 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 });
 
         SelectionManager.DeselectAll();
@@ -545,7 +545,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec])));
         // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs
         setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
-        const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant === false || !LinkDocPreview.LinkInfo);
+        const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant !== true && !LinkDocPreview.LinkInfo);
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
         this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
             ...options,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 76ba7765c..2e594d96a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -30,6 +30,8 @@ import { FaceRectangles } from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import React = require('react');
+import { PresBox } from './trails';
+import { DocumentViewProps } from './DocumentView';
 
 export const pageSchema = createSchema({
     googlePhotosUrl: 'string',
@@ -49,23 +51,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
     private _dropDisposer?: DragManager.DragDropDisposer;
-    private _transitioning: any; // timer for setting view spec properties
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     @observable _curSuffix = '';
     @observable _uploadIcon = uploadIcons.idle;
-    @observable _focusViewScale: number | undefined = 1;
-    @observable _focusPanX: number | undefined = 0;
-    @observable _focusPanY: number | undefined = 0;
-    get viewScale() {
-        return this._focusViewScale || StrCast(this.layoutDoc._viewScale);
-    }
-    get panX() {
-        return this._focusPanX || StrCast(this.layoutDoc._panX);
-    }
-    get panY() {
-        return this._focusPanY || StrCast(this.layoutDoc._panY);
-    }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer?.();
@@ -73,33 +62,30 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     };
 
     @action
-    setViewSpec = (anchor: Doc, preview: boolean) => {
-        if (preview && anchor._viewScale !== undefined) {
-            this._focusViewScale = Cast(anchor._viewScale, 'number', null);
-            this._focusPanX = Cast(anchor._panX, 'number', null);
-            this._focusPanY = Cast(anchor._panX, 'number', null);
-        } else if (anchor._viewScale !== undefined) {
-            const smoothTime = NumCast(anchor.viewTransitionTime);
-            this.layoutDoc.viewTransition = `all ${smoothTime}ms`;
-            this.layoutDoc._panX = NumCast(anchor._panX, NumCast(this.layoutDoc._panY));
-            this.layoutDoc._panY = NumCast(anchor._panY, NumCast(this.layoutDoc._panX));
-            this.layoutDoc._viewScale = NumCast(anchor._viewScale, NumCast(this.layoutDoc._viewScale));
-            this.layoutDoc[this.fieldKey + '-useAlt'] = Cast(anchor._useAlt, 'boolean', null);
-            if (anchor.type === DocumentType.MARKER) {
-                this.dataDoc[this.annotationKey] = new List<Doc>(DocListCast(anchor._annotations));
-            }
-            clearTimeout(this._transitioning);
-            this._transitioning = setTimeout(() => (this.layoutDoc.viewTransition = undefined), smoothTime);
-        }
+    scrollFocus = (anchor: Doc, smooth: boolean) => {
+        let focusSpeed: Opt<number>;
+        PresBox.restoreTargetDocView(
+            this.rootDoc, //
+            { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
+            anchor,
+            (focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition)),
+            !anchor.presPinData
+                ? {}
+                : {
+                      pannable: true,
+                      dataannos: anchor.presAnnotations !== undefined,
+                      dataview: true,
+                  }
+        );
+        return focusSpeed;
     }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
 
     getAnchor = () => {
         const anchor =
             this._getAnchor?.(this._savedAnnotations) ?? // use marquee anchor, otherwise, save zoom/pan as anchor
-            Docs.Create.ImageanchorDocument({ viewTransitionTime: 1000, unrendered: true, annotationOn: this.rootDoc, _viewScale: NumCast(this.layoutDoc._viewScale, 1), _panX: NumCast(this.layoutDoc._panX), _panY: NumCast(this.layoutDoc._panY) });
+            Docs.Create.ImageanchorDocument({ presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
         if (anchor) {
-            anchor._useAlt = Cast(this.layoutDoc[this.fieldKey + '-useAlt'], 'boolean', null);
-            anchor._annotations = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey]));
+            PresBox.pinDocView(anchor, { pinData: { pannable: true, dataview: true, dataannos: true } }, this.rootDoc);
             this.addDocument(anchor);
             return anchor;
         }
@@ -130,7 +116,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }
 
     componentWillUnmount() {
-        clearTimeout(this._transitioning);
         Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
@@ -425,6 +410,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         this.props.select(false);
     };
     savedAnnotations = () => this._savedAnnotations;
+    styleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
+        if (property === StyleProp.BoxShadow) return undefined;
+        return this.props.styleProvider?.(doc, props, property);
+    };
 
     render() {
         TraceMobx();
@@ -445,6 +434,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
                     renderDepth={this.props.renderDepth + 1}
                     fieldKey={this.annotationKey}
+                    styleProvider={this.styleProvider}
                     CollectionView={undefined}
                     isAnnotationOverlay={true}
                     annotationLayerHostsContent={true}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index fcb3ccb07..39e323247 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,7 +5,7 @@ import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -27,6 +27,7 @@ import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
 import { VideoBox } from './VideoBox';
 import React = require('react');
+import { PresBox } from './trails';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -207,16 +208,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        return this._pdfViewer?.scrollFocus(doc, smooth) ?? (didToggle ? 1 : undefined);
+        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, NumCast(doc.presTransition, NumCast(doc.focusSpeed, 500)), { pannable: doc.presPinData ? true : false });
+        return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), smooth) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
-        const anchor =
-            this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ??
-            Docs.Create.TextanchorDocument({
+        const docAnchor = () => {
+            const anchor = Docs.Create.TextanchorDocument({
                 title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
-                y: NumCast(this.layoutDoc._scrollTop),
                 unrendered: true,
             });
+            PresBox.pinDocView(anchor, { pinData: { scrollable: true, pannable: true } }, this.rootDoc);
+            return anchor;
+        };
+        const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? docAnchor();
         this.addDocument(anchor);
         return anchor;
     };
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 2984feba5..fdd61463d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -686,7 +686,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     };
 
     @undoBatch
-    pinToPres = (anchor: Doc) => this.props.pinToPres(anchor);
+    pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {});
 
     @undoBatch
     makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin);
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 8d805c663..10f2dc016 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -12,7 +12,8 @@ import { List } from '../../../../fields/List';
 import { ObjectField } from '../../../../fields/ObjectField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, returnFalse, returnOne, returnTrue, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { AudioField } from '../../../../fields/URLField';
+import { emptyFunction, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -33,6 +34,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
+const { Howl } = require('howler');
 
 export interface PinProps {
     audioRange?: boolean;
@@ -41,6 +43,17 @@ export interface PinProps {
     pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
     pinDocLayout?: boolean; // pin layout info (width/height/x/y)
     pinDocContent?: boolean; // pin data info (scroll/pan/zoom/text)
+    pinAudioPlay?: boolean; // pin audio annotation
+    pinData?: {
+        scrollable?: boolean | undefined;
+        pannable?: boolean | undefined;
+        temporal?: boolean | undefined;
+        clippable?: boolean | undefined;
+        dataview?: boolean | undefined;
+        textview?: boolean | undefined;
+        poslayoutview?: boolean | undefined;
+        dataannos?: boolean | undefined;
+    };
 }
 
 @observer
@@ -333,26 +346,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target: Doc) {
+    static pinDataTypes(target: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
         const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
-        const pannable = [DocumentType.IMG].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
+        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
         const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
         const clippable = [DocumentType.COMPARISON].includes(target.type as any);
         const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(target.type as any) && target.activeFrame === undefined;
         const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
         const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
-        return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview };
+        const dataannos = false;
+        return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
 
     @action
-    static restoreTargetDocView(bestTarget: Doc, activeItem: Doc) {
-        const transTime = NumCast(activeItem.presTransition, 500);
+    playAnnotation = (anno: AudioField) => {};
+    @action
+    static restoreTargetDocView(bestTarget: Doc, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTarget)) {
         const presTransitionTime = `all ${transTime}ms`;
-        const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(bestTarget);
         bestTarget._viewTransition = presTransitionTime;
-        if (clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
-        if (temporal) bestTarget._currentTimecode = activeItem.presStartTime;
-        if (scrollable) {
+        if (pinProps?.pinDocLayout) {
+            const transTime = NumCast(activeItem.presTransition, 500);
+            bestTarget._dataTransition = `all ${transTime}ms`;
+            bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x));
+            bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y));
+            bestTarget.rotation = NumCast(activeItem.presRot, NumCast(bestTarget.rotation));
+            bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
+            bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
+            setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+        }
+        if (pinDataTypes.clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
+        if (pinDataTypes.temporal) bestTarget._currentTimecode = activeItem.presStartTime;
+        if (pinDataTypes.scrollable) {
             bestTarget._scrollTop = activeItem.presPinViewScroll;
             const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
             if (contentBounds) {
@@ -360,13 +384,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
             }
         }
-        if (dataview && activeItem.presData !== undefined) {
+        if (pinDataTypes.dataannos) {
+            const fkey = Doc.LayoutFieldKey(bestTarget);
+            Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>(DocListCast(activeItem.presAnnotations));
+        }
+        if (pinDataTypes.dataview && activeItem.presData !== undefined) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
             Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
             bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt;
         }
-        if (textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
-        if (poslayoutview) {
+        if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
+        if (pinDataTypes.poslayoutview) {
             StrListCast(activeItem.presPinLayoutData)
                 .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
                 .forEach(data => {
@@ -385,7 +413,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 transTime + 10
             );
         }
-        if (pannable) {
+        if (pinDataTypes.pannable) {
             const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
             if (contentBounds) {
                 const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] };
@@ -410,9 +438,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     /// reserved fields on the pinDoc so that those values can be restored to the
     /// target doc when navigating to it.
     @action
-    static pinDocView(pinDoc: Doc, pinProps: PinProps | undefined, targetDoc: Doc) {
-        const { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview } = this.pinDataTypes(pinDoc);
-        if (pinProps?.pinDocLayout) {
+    static pinDocView(pinDoc: Doc, pinProps: PinProps, targetDoc: Doc) {
+        if (pinProps.pinDocLayout) {
             pinDoc.presPinLayout = true;
             pinDoc.presX = NumCast(targetDoc.x);
             pinDoc.presY = NumCast(targetDoc.y);
@@ -420,33 +447,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presWidth = NumCast(targetDoc.width);
             pinDoc.presHeight = NumCast(targetDoc.height);
         }
-        if (pinProps?.pinDocContent) {
-            pinDoc.presPinData = scrollable || temporal || pannable || clippable || dataview || textview || poslayoutview || pinProps.activeFrame !== undefined;
-            if (dataview) {
+        if (pinProps.pinAudioPlay) pinDoc.followLinkAudio = true;
+        if (pinProps.pinData) {
+            pinDoc.presPinData =
+                pinProps.pinData.scrollable ||
+                pinProps.pinData.temporal ||
+                pinProps.pinData.pannable ||
+                pinProps.pinData.clippable ||
+                pinProps.pinData.dataview ||
+                pinProps.pinData.textview ||
+                pinProps.pinData.poslayoutview ||
+                pinProps?.activeFrame !== undefined;
+            if (pinProps.pinData.dataview) {
                 const fkey = Doc.LayoutFieldKey(targetDoc);
                 pinDoc.presUseAlt = targetDoc[fkey + '-useAlt'];
                 pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data;
             }
-            if (textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
-            if (scrollable) pinDoc.presPinViewScroll = pinDoc._scrollTop;
-            if (clippable) pinDoc.presPinClipWidth = pinDoc._clipWidth;
-            if (poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(pinDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
-            if (pannable) {
-                pinDoc.presPinViewX = NumCast(pinDoc._panX);
-                pinDoc.presPinViewY = NumCast(pinDoc._panY);
-                pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1);
+            if (pinProps.pinData.dataannos) {
+                const fkey = Doc.LayoutFieldKey(targetDoc);
+                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']));
+            }
+            if (pinProps.pinData.textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
+            if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
+            if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth;
+            if (pinProps.pinData.poslayoutview) pinDoc.presPinLayoutData = new List<string>(DocListCast(targetDoc.presData).map(d => JSON.stringify({ id: d[Id], x: NumCast(d.x), y: NumCast(d.y), w: NumCast(d._width), h: NumCast(d._height) })));
+            if (pinProps.pinData.pannable) {
+                pinDoc.presPinViewX = NumCast(targetDoc._panX);
+                pinDoc.presPinViewY = NumCast(targetDoc._panY);
+                pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1);
             }
-            if (temporal) {
-                pinDoc.presStartTime = pinDoc._currentTimecode;
-                const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(pinDoc.presStartTime) + 0.1);
-                pinDoc.presEndTime = NumCast(pinDoc.clipEnd, duration);
+            if (pinProps.pinData.temporal) {
+                pinDoc.presStartTime = targetDoc._currentTimecode;
+                const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(targetDoc.presStartTime) + 0.1);
+                pinDoc.presEndTime = NumCast(targetDoc.clipEnd, duration);
             }
         }
         if (pinProps?.pinViewport) {
             // If pinWithView option set then update scale and x / y props of slide
             const bounds = pinProps.pinViewport;
             pinDoc.presPinView = true;
-            pinDoc.presPinViewScale = NumCast(pinDoc._viewScale, 1);
+            pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1);
             pinDoc.presPinViewX = bounds.left + bounds.width / 2;
             pinDoc.presPinViewY = bounds.top + bounds.height / 2;
             pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
@@ -507,35 +547,24 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     };
 
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
-        if ((activeItem.presPinLayout || activeItem.presPinView) && DocCast(targetDoc.context)?._currentFrame === undefined) {
-            const transTime = NumCast(activeItem.presTransition, 500);
-            targetDoc._dataTransition = `all ${transTime}ms`;
-            targetDoc.x = NumCast(activeItem.presX, NumCast(targetDoc.x));
-            targetDoc.y = NumCast(activeItem.presY, NumCast(targetDoc.y));
-            targetDoc.rotation = NumCast(activeItem.presRot, NumCast(targetDoc.rotation));
-            targetDoc.width = NumCast(activeItem.presWidth, NumCast(targetDoc.width));
-            targetDoc.height = NumCast(activeItem.presHeight, NumCast(targetDoc.height));
-            setTimeout(() => (targetDoc._dataTransition = undefined), transTime + 10);
-        }
         // If openDocument is selected then it should open the document for the user
         if (activeItem.openDocument) {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
         } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
             LightboxView.SetLightboxDoc(undefined);
             const zooming = activeItem.presMovement !== PresMovement.Pan;
-            DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, undefined, finished, undefined, true, NumCast(activeItem.presZoom, 1));
+            DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, activeItem, finished, undefined, true, NumCast(activeItem.presZoom, 1));
         } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
         }
         // After navigating to the document, if it is added as a presPinView then it will
         // adjust the pan and scale to that of the pinView when it was added.
-        if (activeItem.presPinData || activeItem.presPinView) {
+        const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
+        if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
             clearTimeout(PresBox._navTimer);
             // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
             const bestTargetView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
-            const bestTarget = bestTargetView?.props.Document;
-            if (bestTarget) PresBox._navTimer = PresBox.restoreTargetDocView(bestTarget, activeItem);
-            activeItem.presPinAudioPlay && bestTargetView?.docView?.playAnnotation();
+            if (bestTargetView?.props.Document) PresBox._navTimer = PresBox.restoreTargetDocView(bestTargetView?.props.Document, { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
         }
     }
 
@@ -747,7 +776,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             } else {
                 if (!doc.aliasOf) {
                     const original = Doc.MakeAlias(doc);
-                    TabDocView.PinDoc(original);
+                    TabDocView.PinDoc(original, {});
                     setTimeout(() => this.removeDocument(doc), 0);
                     return false;
                 } else {
@@ -1316,7 +1345,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             Effects
                             <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                                 <div className="presBox-subheading">Play Audio Annotation</div>
-                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.presPinAudioPlay = !BoolCast(activeItem.presPinAudioPlay))} checked={BoolCast(activeItem.presPinAudioPlay)} />
+                                <input className="presBox-checkbox" style={{ margin: 10 }} type="checkbox" onChange={() => (activeItem.followLinkAudio = !BoolCast(activeItem.followLinkAudio))} checked={BoolCast(activeItem.followLinkAudio)} />
                             </div>
                             <div
                                 className="presBox-dropdown"
@@ -1694,7 +1723,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const presData = Cast(this.rootDoc.data, listSpec(Doc));
             if (data && presData) {
                 data.push(doc);
-                TabDocView.PinDoc(doc);
+                TabDocView.PinDoc(doc, {});
                 this.gotoDocument(this.childDocs.length, this.activeItem);
             } else {
                 this.props.addDocTab(doc, 'add:right');
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 9af0949eb..133b882f6 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -52,7 +52,7 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     };
 
     @undoBatch
-    pinToPres = () => this.props.pinToPres(this.annoTextRegion);
+    pinToPres = () => this.props.pinToPres(this.annoTextRegion, {});
 
     @undoBatch
     makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 6ff87ef9f..5a5c63c3d 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -166,12 +166,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     // scrolls to focus on a nested annotation document.  if this is part a link preview then it will jump to the scroll location,
     // otherwise it will scroll smoothly.
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, scrollTop: number, smooth: boolean) => {
         const mainCont = this._mainCont.current;
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
-            const scrollTo = doc.unrendered ? NumCast(doc.y) : Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
+            const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = scrollTo;
                 else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo);
@@ -203,6 +203,11 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
         document.removeEventListener('pagesinit', this.pagesinit);
         var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : '';
+        this._disposers.scale = reaction(
+            () => NumCast(this.props.layoutDoc._viewScale, 1),
+            scale => (this._pdfViewer.currentScaleValue = scale),
+            { fireImmediately: true }
+        );
         this._disposers.scroll = reaction(
             () => Math.abs(NumCast(this.props.Document._scrollTop)),
             pos => {
@@ -290,7 +295,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
         if (scrollToAnnotation) {
-            this.scrollFocus(scrollToAnnotation, true);
+            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), true);
             Doc.linkFollowHighlight(scrollToAnnotation);
         }
     };
-- 
cgit v1.2.3-70-g09d2


From 56116231c4c0aa78d54a9ed4c1f167514596953c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 16 Nov 2022 23:27:05 -0500
Subject: added presEffects to link anchors

---
 src/client/util/DocumentManager.ts                 |  2 +-
 src/client/views/MainView.tsx                      |  1 +
 .../views/collections/CollectionDockingView.tsx    |  7 +-
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/linking/LinkEditor.tsx            | 74 ++++++++++++++++++-
 src/client/views/nodes/DocumentView.scss           |  2 +-
 src/client/views/nodes/DocumentView.tsx            |  4 +-
 src/client/views/nodes/trails/PresBox.tsx          | 86 ++++++++++++----------
 src/fields/Doc.ts                                  |  7 +-
 9 files changed, 136 insertions(+), 49 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index a60c1ed6b..00f6bc40a 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -233,7 +233,7 @@ export class DocumentManager {
             }
         }
         if (focusView) {
-            !noSelect && Doc.linkFollowHighlight(focusView.rootDoc); //TODO:glr make this a setting in PresBox
+            !noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
             if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
                 focusView.focus(originalTarget ?? targetDoc, {
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 987bfc23d..392b4eeeb 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -484,6 +484,7 @@ export class MainView extends React.Component {
     }
 
     globalPointerDown = action((e: PointerEvent) => {
+        runInAction(() => (Doc.HighlightBrush.linkFollowEffect = undefined));
         AudioBox.Enabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets.length) {
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 92319d080..434466505 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -452,7 +452,7 @@ export class CollectionDockingView extends CollectionSubView() {
                   .map(id => DocServer.GetCachedRefField(id))
                   .filter(f => f)
                   .map(f => f as Doc);
-        const changesMade = this.props.Document.dockcingConfig !== json;
+        const changesMade = this.props.Document.dockingConfig !== json;
         if (changesMade && !this._flush) {
             UndoManager.RunInBatch(() => {
                 this.props.Document.dockingConfig = json;
@@ -507,7 +507,12 @@ export class CollectionDockingView extends CollectionSubView() {
                 action(() => {
                     //if (confirm('really close this?')) {
                     if ((!stack.parent.isRoot && !stack.parent.parent.isRoot) || stack.parent.contentItems.length > 1) {
+                        const batch = UndoManager.StartBatch('close stack');
                         stack.remove();
+                        setTimeout(() => {
+                            this.stateChanged();
+                            batch.end();
+                        });
                     } else {
                         alert('cant delete the last stack');
                     }
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 7bc273d7d..b66dc0aa2 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -111,7 +111,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
             }
 
-            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate).map(d => d as Doc);
+            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && !d.unrendered).map(d => d as Doc);
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
 
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index 3e8867c50..d90a91ab7 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -3,13 +3,14 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, NumListCast, StrListCast, Field } from '../../../fields/Doc';
-import { DateCast, StrCast, Cast, BoolCast } from '../../../fields/Types';
+import { DateCast, StrCast, Cast, BoolCast, DocCast, NumCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import './LinkEditor.scss';
 import { LinkRelationshipSearch } from './LinkRelationshipSearch';
 import React = require('react');
 import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { PresBox, PresEffect } from '../nodes/trails';
 
 interface LinkEditorProps {
     sourceDoc: Doc;
@@ -24,6 +25,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom);
     @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio);
     @observable openDropdown: boolean = false;
+    @observable openEffectDropdown: boolean = false;
     @observable private buttonColor: string = '';
     @observable private relationshipButtonColor: string = '';
     @observable private relationshipSearchVisibility: string = 'none';
@@ -295,6 +297,61 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         );
     }
 
+    @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
+    changeEffectDropdown = () => {
+        this.openEffectDropdown = !this.openEffectDropdown;
+    };
+
+    @undoBatch
+    changeEffect = action((follow: string) => {
+        this.openEffectDropdown = false;
+        this.sourceAnchor.presEffect = follow;
+    });
+
+    @computed
+    get effectDropdown() {
+        return (
+            <div className="linkEditor-followingDropdown">
+                <div className="linkEditor-followingDropdown-label">Transition Effect:</div>
+                <div className="linkEditor-followingDropdown-dropdown">
+                    <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}>
+                        {StrCast(this.sourceAnchor.presEffect, 'default')}
+                        <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 => (
+                            <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeEffect(effect.toString())}>
+                                {effect.toString()}
+                            </div>
+                        ))}
+                    </div>
+                </div>
+            </div>
+        );
+    }
+
     autoMove = (e: React.PointerEvent) => {
         setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
     };
@@ -348,7 +405,6 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                         </>
                     ) : null}
                 </div>
-
                 {this.editDescription}
                 {this.editRelationship}
                 {this.editZoomFollow}
@@ -390,6 +446,20 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                     </Tooltip>
                 </div>
                 {this.followingDropdown}
+                {this.effectDropdown}
+                {PresBox.inputter('0.1', '0.1', '10', NumCast(this.sourceAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.sourceAnchor.presTransition = timeInMS)))}
+                <div
+                    className={'slider-headers'}
+                    style={{
+                        display: 'grid',
+                        justifyContent: 'space-between',
+                        width: '100%',
+                        gridTemplateColumns: 'auto auto auto',
+                    }}>
+                    <div className="slider-text">Fast</div>
+                    <div className="slider-text">Medium</div>
+                    <div className="slider-text">Slow</div>
+                </div>{' '}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 01188d3fa..abf6e37ab 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -28,7 +28,7 @@
     transition: outline 0.3s linear;
 
     // background: $white; //overflow: hidden;
-    transform-origin: left top;
+    transform-origin: center;
 
     &.minimized {
         width: 30px;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index dc468cf89..fb07f1033 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
-import { observer } from 'mobx-react';
+import { observer, renderReporter } from 'mobx-react';
 import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
@@ -1394,7 +1394,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
-        const animRenderDoc = PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance.activeItem) : renderDoc;
+        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? Doc.HighlightBrush.linkFollowEffect, this.rootDoc) : renderDoc;
         return (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 10f2dc016..7235481e0 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -34,6 +34,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresMovement, PresStatus } from './PresEnums';
+import { map } from 'bluebird';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -67,17 +68,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * @param presEffectDoc presentation effects document that specifies the animation effect parameters
      * @returns a function that will wrap a JSX animation element wrapping any JSX element
      */
-    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc) {
+    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 === PresEffect.Left,
+            right: presEffectDoc?.presEffectDirection === PresEffect.Right,
+            top: presEffectDoc?.presEffectDirection === PresEffect.Top,
+            bottom: presEffectDoc?.presEffectDirection === PresEffect.Bottom,
             opposite: true,
-            delay: NumCast(presEffectDoc.presTransition),
+            delay: 0,
+            duration: Cast(presEffectDoc?.presTransition, 'number', null),
         };
         //prettier-ignore
-        switch (StrCast(presEffectDoc.presEffect)) {
+        switch (StrCast(presEffectDoc?.presEffect)) {
             default: 
             case PresEffect.None:       return renderDoc;
             case PresEffect.Zoom:       return <Zoom {...effectProps}>{renderDoc}</Zoom>;
@@ -386,7 +388,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         if (pinDataTypes.dataannos) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
-            Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>(DocListCast(activeItem.presAnnotations));
+            Doc.GetProto(bestTarget)[fkey + '-annotations'] = new List<Doc>([...DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered), ...DocListCast(activeItem.presAnnotations)]);
         }
         if (pinDataTypes.dataview && activeItem.presData !== undefined) {
             const fkey = Doc.LayoutFieldKey(bestTarget);
@@ -465,7 +467,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
             if (pinProps.pinData.dataannos) {
                 const fkey = Doc.LayoutFieldKey(targetDoc);
-                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']));
+                pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']).filter(doc => !doc.unrendered));
             }
             if (pinProps.pinData.textview) pinDoc.presData = targetDoc[Doc.LayoutFieldKey(targetDoc)] instanceof ObjectField ? (targetDoc[Doc.LayoutFieldKey(targetDoc)] as ObjectField)[Copy]() : targetDoc.text;
             if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
@@ -1095,12 +1097,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     getPaths = (collection: Doc) => this.paths; // needs to be smarter and figure out the paths to draw for this specific collection.  or better yet, draw everything in an overlay layer instad of within a collection
 
     // Converts seconds to ms and updates presTransition
-    setTransitionTime = (number: String, change?: number) => {
+    public static SetTransitionTime = (number: String, setter: (timeInMS: number) => void, change?: number) => {
         let timeInMS = Number(number) * 1000;
         if (change) timeInMS += change;
         if (timeInMS < 100) timeInMS = 100;
         if (timeInMS > 10000) timeInMS = 10000;
-        this.selectedArray.forEach(doc => (doc.presTransition = timeInMS));
+        setter(timeInMS);
+    };
+    setTransitionTime = (number: String, change?: number) => {
+        PresBox.SetTransitionTime(number, (timeInMS: number) => this.selectedArray.forEach(doc => (doc.presTransition = timeInMS)), change);
     };
 
     // Converts seconds to ms and updates presTransition
@@ -1165,6 +1170,28 @@ 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) => {
+        let batch: any;
+        return (
+            <input
+                type="range"
+                step={step}
+                min={min}
+                max={max}
+                value={value}
+                className={`toolbar-slider ${active ? '' : 'none'}`}
+                onPointerDown={e => {
+                    batch = UndoManager.StartBatch('pres slider');
+                    e.stopPropagation();
+                }}
+                onPointerUp={() => batch?.end()}
+                onChange={e => {
+                    e.stopPropagation();
+                    change(e.target.value);
+                }}
+            />
+        );
+    };
     @computed get transitionDropdown() {
         const activeItem: Doc = this.activeItem;
         const targetDoc: Doc = this.targetDoc;
@@ -1180,39 +1207,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 {movement}
             </div>
         );
-        const presDirection = (diretion: PresEffect, icon: string, gridColumn: number, gridRow: number, opts: object) => {
-            const color = this.activeItem.presEffectDirection === diretion || (diretion === PresEffect.Center && !this.activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+        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';
             return (
-                <Tooltip title={<div className="dash-tooltip">{diretion}</div>}>
+                <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                     <div
-                        style={{ ...opts, border: diretion === PresEffect.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
-                        onClick={() => this.updateEffectDirection(diretion)}>
+                        style={{ ...opts, border: direction === PresEffect.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>
             );
         };
-        const inputter = (min: string, step: string, max: string, value: number, active: boolean, change: (val: string) => void) => {
-            return (
-                <input
-                    type="range"
-                    step={step}
-                    min={min}
-                    max={max}
-                    value={value}
-                    className={`toolbar-slider ${active ? '' : 'none'}`}
-                    onPointerDown={e => {
-                        this._batch = UndoManager.StartBatch('pres slider');
-                        e.stopPropagation();
-                    }}
-                    onPointerUp={() => this._batch?.end()}
-                    onChange={e => {
-                        e.stopPropagation();
-                        change(e.target.value);
-                    }}
-                />
-            );
-        };
         if (activeItem && targetDoc) {
             const type = targetDoc.type;
             const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
@@ -1263,7 +1269,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                             </div>
                         </div>
-                        {inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
+                        {PresBox.inputter('0', '1', '100', zoom, activeItem.presMovement === PresMovement.Zoom, this.setZoom)}
                         <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}>
                             <div className="presBox-subheading">Transition Speed</div>
                             <div className="ribbon-property">
@@ -1278,7 +1284,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                             </div>
                         </div>
-                        {inputter('0.1', '0.1', '10', transitionSpeed, true, this.setTransitionTime)}
+                        {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.setTransitionTime)}
                         <div className={'slider-headers'}>
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
@@ -1331,7 +1337,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                         </div>
                                     </div>
                                 </div>
-                                {inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
+                                {PresBox.inputter('0.1', '0.1', '20', duration, targetDoc.type !== DocumentType.AUDIO, this.setDurationTime)}
                                 <div className={'slider-headers'} style={{ display: targetDoc.type === DocumentType.AUDIO ? 'none' : 'grid' }}>
                                     <div className="slider-text">Short</div>
                                     <div className="slider-text">Medium</div>
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 70cb10970..75801b68c 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1263,11 +1263,15 @@ export namespace Doc {
     export function linkFollowUnhighlight() {
         Doc.UnhighlightAll();
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
+        runInAction(() => (HighlightBrush.linkFollowEffect = undefined));
     }
 
     let _lastDate = 0;
-    export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true) {
+    export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
         linkFollowUnhighlight();
+        // runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = undefined));
+        // setTimeout(() => runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect)));
+        runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect));
         (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
         document.addEventListener('pointerdown', linkFollowUnhighlight);
@@ -1277,6 +1281,7 @@ export namespace Doc {
 
     export class HighlightBrush {
         @observable HighlightedDoc: Map<Doc, boolean> = new Map();
+        @observable static linkFollowEffect: Doc | undefined;
     }
     const highlightManager = new HighlightBrush();
     export function IsHighlighted(doc: Doc) {
-- 
cgit v1.2.3-70-g09d2


From 30e7fc1b2cb4b5c5f8d5f5e4f808b91e69629245 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 17 Nov 2022 11:00:06 -0500
Subject: fixed pushpin behaviors by not animating when anchor viewspec already
 matched document.  fixed recording button highlighting. switched LinkEditor
 to edit properties of destination insteqad of source anchor

---
 src/client/views/DocumentButtonBar.tsx             | 22 +++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 11 ++--
 src/client/views/linking/LinkEditor.tsx            | 14 ++---
 src/client/views/nodes/ImageBox.tsx                | 11 ++--
 src/client/views/nodes/trails/PresBox.tsx          | 72 +++++++++++++++-------
 5 files changed, 80 insertions(+), 50 deletions(-)

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

diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 681349ccf..ecf330792 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -386,18 +386,18 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                     style={{ backgroundColor: this._isRecording ? Colors.ERROR_RED : Colors.DARK_GRAY, color: Colors.WHITE }}
                     onPointerDown={action((e: React.PointerEvent) => {
                         this._isRecording = true;
-                        this.props.views().map(
-                            view =>
-                                view &&
-                                DocumentViewInternal.recordAudioAnnotation(
-                                    view.dataDoc,
-                                    view.LayoutFieldKey,
-                                    stopFunc => (this._stopFunc = stopFunc),
-                                    action(() => (this._isRecording = false))
-                                )
-                        );
+                        this.props.views().map(view => view && DocumentViewInternal.recordAudioAnnotation(view.dataDoc, view.LayoutFieldKey, stopFunc => (this._stopFunc = stopFunc), emptyFunction));
                         const b = UndoManager.StartBatch('Recording');
-                        setupMoveUpEvents(this, e, returnFalse, () => this._stopFunc(), emptyFunction);
+                        setupMoveUpEvents(
+                            this,
+                            e,
+                            returnFalse,
+                            action(() => {
+                                this._isRecording = false;
+                                this._stopFunc();
+                            }),
+                            emptyFunction
+                        );
                     })}>
                     <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="microphone" />
                 </div>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8a97797c7..57cccec4a 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1532,17 +1532,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     scrollFocus = (anchor: Doc, smooth: boolean) => {
-        let focusSpeed: Opt<number>;
-        PresBox.restoreTargetDocView(
+        const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition);
+        return PresBox.restoreTargetDocView(
             this.rootDoc, //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
-            (focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition)),
+            focusSpeed,
             {
                 pannable: anchor.presPinData ? true : false,
             }
-        );
-        return focusSpeed;
+        )
+            ? focusSpeed
+            : undefined;
     }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
 
     getAnchor = () => {
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
index d90a91ab7..8c4d756d2 100644
--- a/src/client/views/linking/LinkEditor.tsx
+++ b/src/client/views/linking/LinkEditor.tsx
@@ -297,13 +297,13 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
         );
     }
 
-    @computed get sourceAnchor() {
+    @computed get destinationAnchor() {
         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);
+            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2);
+            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1);
         }
-        return this.props.sourceDoc;
+        return LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc) ?? this.props.sourceDoc;
     }
     @action
     changeEffectDropdown = () => {
@@ -313,7 +313,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
     @undoBatch
     changeEffect = action((follow: string) => {
         this.openEffectDropdown = false;
-        this.sourceAnchor.presEffect = follow;
+        this.destinationAnchor.presEffect = follow;
     });
 
     @computed
@@ -323,7 +323,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 <div className="linkEditor-followingDropdown-label">Transition Effect:</div>
                 <div className="linkEditor-followingDropdown-dropdown">
                     <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}>
-                        {StrCast(this.sourceAnchor.presEffect, 'default')}
+                        {StrCast(this.destinationAnchor.presEffect, 'default')}
                         <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' }}>
@@ -447,7 +447,7 @@ export class LinkEditor extends React.Component<LinkEditorProps> {
                 </div>
                 {this.followingDropdown}
                 {this.effectDropdown}
-                {PresBox.inputter('0.1', '0.1', '10', NumCast(this.sourceAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.sourceAnchor.presTransition = timeInMS)))}
+                {PresBox.inputter('0.1', '0.1', '10', NumCast(this.destinationAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.destinationAnchor.presTransition = timeInMS)))}
                 <div
                     className={'slider-headers'}
                     style={{
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 4092a2575..1d79febdf 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -63,12 +63,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     @action
     scrollFocus = (anchor: Doc, smooth: boolean) => {
-        let focusSpeed: Opt<number>;
-        PresBox.restoreTargetDocView(
+        const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition);
+        return PresBox.restoreTargetDocView(
             this.rootDoc, //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
-            (focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition)),
+            focusSpeed,
             !anchor.presPinData
                 ? {}
                 : {
@@ -76,8 +76,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                       dataannos: anchor.presAnnotations !== undefined,
                       dataview: true,
                   }
-        );
-        return focusSpeed;
+        )
+            ? focusSpeed
+            : undefined;
     }; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
 
     getAnchor = () => {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 7235481e0..e19b53f50 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -365,25 +365,46 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     static restoreTargetDocView(bestTarget: Doc, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTarget)) {
         const presTransitionTime = `all ${transTime}ms`;
-        bestTarget._viewTransition = presTransitionTime;
+        let changed = false;
         if (pinProps?.pinDocLayout) {
-            const transTime = NumCast(activeItem.presTransition, 500);
-            bestTarget._dataTransition = `all ${transTime}ms`;
-            bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x));
-            bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y));
-            bestTarget.rotation = NumCast(activeItem.presRot, NumCast(bestTarget.rotation));
-            bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
-            bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
-            setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+            if (
+                bestTarget.x !== NumCast(activeItem.presX, NumCast(bestTarget.x)) ||
+                bestTarget.y !== NumCast(activeItem.presY, NumCast(bestTarget.y)) ||
+                bestTarget.rotation !== NumCast(activeItem.presRot, NumCast(bestTarget.rotation)) ||
+                bestTarget.width !== NumCast(activeItem.presWidth, NumCast(bestTarget.width)) ||
+                bestTarget.height !== NumCast(activeItem.presHeight, NumCast(bestTarget.height))
+            ) {
+                bestTarget._dataTransition = `all ${transTime}ms`;
+                bestTarget.x = NumCast(activeItem.presX, NumCast(bestTarget.x));
+                bestTarget.y = NumCast(activeItem.presY, NumCast(bestTarget.y));
+                bestTarget.rotation = NumCast(activeItem.presRot, NumCast(bestTarget.rotation));
+                bestTarget.width = NumCast(activeItem.presWidth, NumCast(bestTarget.width));
+                bestTarget.height = NumCast(activeItem.presHeight, NumCast(bestTarget.height));
+                setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
+                changed = true;
+            }
+        }
+        if (pinDataTypes.clippable) {
+            if (bestTarget._clipWidth !== activeItem.presPinClipWidth) {
+                bestTarget._clipWidth = activeItem.presPinClipWidth;
+                changed = true;
+            }
+        }
+        if (pinDataTypes.temporal) {
+            if (bestTarget._currentTimecode !== activeItem.presStartTime) {
+                bestTarget._currentTimecode = activeItem.presStartTime;
+                changed = true;
+            }
         }
-        if (pinDataTypes.clippable) bestTarget._clipWidth = activeItem.presPinClipWidth;
-        if (pinDataTypes.temporal) bestTarget._currentTimecode = activeItem.presStartTime;
         if (pinDataTypes.scrollable) {
-            bestTarget._scrollTop = activeItem.presPinViewScroll;
-            const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
-            if (contentBounds) {
-                const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView;
-                dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
+            if (bestTarget._scrollTop !== activeItem.presPinViewScroll) {
+                bestTarget._scrollTop = activeItem.presPinViewScroll;
+                changed = true;
+                const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
+                if (contentBounds) {
+                    const dv = DocumentManager.Instance.getDocumentView(bestTarget)?.ComponentView;
+                    dv?.brushView?.({ panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] });
+                }
             }
         }
         if (pinDataTypes.dataannos) {
@@ -397,6 +418,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
         if (pinDataTypes.poslayoutview) {
+            changed = true;
             StrListCast(activeItem.presPinLayoutData)
                 .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
                 .forEach(data => {
@@ -428,12 +450,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     dv.ComponentView?.brushView?.(viewport);
                 }
             } else {
-                bestTarget._panX = activeItem.presPinViewX;
-                bestTarget._panY = activeItem.presPinViewY;
-                bestTarget._viewScale = activeItem.presPinViewScale;
+                if (bestTarget._panX !== activeItem.presPinViewX || bestTarget._panY !== activeItem.presPinViewY || bestTarget._viewScale !== activeItem.presPinViewScale) {
+                    bestTarget._panX = activeItem.presPinViewX;
+                    bestTarget._panY = activeItem.presPinViewY;
+                    bestTarget._viewScale = activeItem.presPinViewScale;
+                    changed = true;
+                }
             }
         }
-        return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
+        if (changed) {
+            bestTarget._viewTransition = presTransitionTime;
+            return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
+        }
     }
 
     /// copies values from the targetDoc (which is the prototype of the pinDoc) to
@@ -495,7 +523,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
     }
 
-    static _navTimer: NodeJS.Timeout;
+    static _navTimer: NodeJS.Timeout | undefined;
     /**
      * This method makes sure that cursor navigates to the element that
      * has the option open and last in the group.
@@ -563,7 +591,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // adjust the pan and scale to that of the pinView when it was added.
         const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
         if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
-            clearTimeout(PresBox._navTimer);
+            PresBox._navTimer && clearTimeout(PresBox._navTimer);
             // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
             const bestTargetView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
             if (bestTargetView?.props.Document) PresBox._navTimer = PresBox.restoreTargetDocView(bestTargetView?.props.Document, { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
-- 
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/trails/PresBox.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/trails/PresBox.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/trails/PresBox.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 66184a172006de4d4bf72d9da33858e04d298181 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 1 Dec 2022 10:13:03 -0500
Subject: refactored process of following links / jumping to docs and added
 following options for zoomTime, etc instead of setting temporary fields on
 docs.

---
 src/client/documents/Documents.ts                  |   8 +-
 src/client/util/DocumentManager.ts                 |  98 +++---
 .../util/Import & Export/DirectoryImportBox.tsx    | 334 +++++++++++----------
 src/client/util/LinkFollower.ts                    |  84 +++---
 src/client/util/SharingManager.tsx                 |   2 +-
 src/client/views/DocComponent.tsx                  |   2 +-
 src/client/views/InkingStroke.tsx                  |   6 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/MarqueeAnnotator.tsx              |   2 +-
 src/client/views/PropertiesView.tsx                |  28 +-
 .../views/collections/CollectionNoteTakingView.tsx |   5 +-
 .../views/collections/CollectionStackingView.tsx   |   5 +-
 src/client/views/collections/TabDocView.tsx        |   4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   8 +-
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +-
 .../collectionLinear/CollectionLinearView.tsx      |   2 +-
 .../collectionSchema/CollectionSchemaCells.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  26 +-
 src/client/views/nodes/ImageBox.tsx                |   8 +-
 src/client/views/nodes/PDFBox.tsx                  |  13 +-
 src/client/views/nodes/VideoBox.tsx                |   6 +-
 src/client/views/nodes/WebBox.tsx                  |  10 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  12 +-
 src/client/views/nodes/trails/PresBox.tsx          |  24 +-
 src/client/views/pdf/Annotation.tsx                |   4 +-
 src/client/views/pdf/PDFViewer.tsx                 |   8 +-
 src/client/views/search/SearchBox.tsx              |   2 +-
 27 files changed, 354 insertions(+), 353 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index eed839520..d13d96dd3 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -157,7 +157,7 @@ export class DocumentOptions {
     _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom]
     _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged
     _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
-    _isPushpin?: boolean; // whether document, when clicked, toggles display of its link target
+    _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target
     _showTitle?: string; // field name to display in header (:hover is an optional suffix)
     _showCaption?: string; // which field to display in the caption area.  leave empty to have no caption
     _scrollTop?: number; // scroll location for pdfs
@@ -272,7 +272,7 @@ export class DocumentOptions {
     clipWidth?: number; // percent transition from before to after in comparisonBox
     dockingConfig?: string;
     annotationOn?: Doc;
-    isPushpin?: boolean;
+    followLinkToggle?: boolean;
     isGroup?: boolean; // whether a collection should use a grouping UI behavior
     _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped.  e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document
     noteType?: string;
@@ -1703,7 +1703,7 @@ export namespace DocUtils {
     }
 
     export function LeavePushpin(doc: Doc, annotationField: string) {
-        if (doc.isPushpin) return undefined;
+        if (doc.followLinkToggle) return undefined;
         const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
         const hasContextAnchor = DocListCast(doc.links).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context));
         if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) {
@@ -1711,7 +1711,7 @@ export namespace DocUtils {
                 title: 'pushpin',
                 label: '',
                 annotationOn: Cast(doc.annotationOn, Doc, null),
-                isPushpin: true,
+                followLinkToggle: true,
                 icon: 'map-pin',
                 x: Cast(doc.x, 'number', null),
                 y: Cast(doc.y, 'number', null),
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 4f02a8202..1b63b615b 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -5,7 +5,7 @@ import { Cast, DocCast } from '../../fields/Types';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, 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';
@@ -171,19 +171,13 @@ export class DocumentManager {
     };
     public jumpToDocument = (
         targetDoc: Doc, // document to display
-        willZoom: boolean, // whether to zoom doc to take up most of screen
+        options: DocFocusOptions, // options for how to navigate to target
         createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
         docContext: Doc[], // context to load that should contain the target
-        linkDoc?: Doc, // link that's being followed
-        closeContextIfNotFound: boolean = false, // after opening a context where the document should be, this determines whether the context should be closed if the Doc isn't actually there
-        originatingDoc: Opt<Doc> = undefined, // doc that initiated the display of the target odoc
-        finished?: () => void,
-        originalTarget?: Doc,
-        noSelect?: boolean,
-        presZoomScale?: number
+        finished?: () => void
     ): void => {
-        originalTarget = originalTarget ?? targetDoc;
-        const docView = this.getFirstDocumentView(targetDoc, originatingDoc);
+        const originalTarget = options.originalTarget ?? targetDoc;
+        const docView = this.getFirstDocumentView(targetDoc, options.originatingDoc);
         const annotatedDoc = Cast(targetDoc.annotationOn, Doc, null);
         const resolvedTarget = targetDoc.type === DocumentType.MARKER ? annotatedDoc ?? docView?.rootDoc ?? targetDoc : docView?.rootDoc ?? targetDoc; // if target is a marker, then focus toggling should apply to the document it's on since the marker itself doesn't have a hidden field
         var wasHidden = resolvedTarget.hidden;
@@ -195,14 +189,14 @@ export class DocumentManager {
         }
         const focusAndFinish = (didFocus: boolean) => {
             const finalTargetDoc = resolvedTarget;
-            if (originatingDoc?.isPushpin) {
+            if (options.toggleTarget) {
                 if (!didFocus && !wasHidden) {
                     // don't toggle the hidden state if the doc was already un-hidden as part of this document traversal
                     finalTargetDoc.hidden = !finalTargetDoc.hidden;
                 }
             } else {
                 finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
-                !noSelect && docView?.select(false);
+                !options.noSelect && docView?.select(false);
             }
             finished?.();
         };
@@ -216,9 +210,8 @@ export class DocumentManager {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
                 annoContainerView.iconify(() =>
                     annoContainerView.focus(targetDoc, {
+                        ...options,
                         originalTarget,
-                        willZoom,
-                        scale: presZoomScale,
                         afterFocus: (didFocus: boolean) =>
                             new Promise<ViewAdjustment>(res => {
                                 focusAndFinish(true);
@@ -232,13 +225,12 @@ export class DocumentManager {
             }
         }
         if (focusView) {
-            !noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
-            if (originatingDoc?.followLinkAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
+            !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
+            if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
-                focusView.focus(originalTarget ?? targetDoc, {
+                focusView.focus(originalTarget, {
+                    ...options,
                     originalTarget,
-                    willZoom,
-                    scale: presZoomScale,
                     afterFocus: (didFocus: boolean) =>
                         new Promise<ViewAdjustment>(res => {
                             focusAndFinish(forceDidFocus || didFocus);
@@ -262,13 +254,12 @@ export class DocumentManager {
                     targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
                     targetDocContext._viewTransition = 'transform 500ms';
                     targetDocContextView.props.focus(targetDocContextView.rootDoc, {
-                        willZoom,
+                        ...options,
+                        // originalTarget, // needed?
                         afterFocus: async () => {
                             targetDocContext._viewTransition = undefined;
                             if (targetDocContext.layoutKey === 'layout_icon') {
-                                targetDocContextView.iconify(() =>
-                                    this.jumpToDocument(resolvedTarget ?? targetDoc, willZoom, createViewFunc, docContext, linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
-                                );
+                                targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished));
                             }
                             return ViewAdjustment.doNothing;
                         },
@@ -281,56 +272,35 @@ export class DocumentManager {
                         finished?.();
                     } else {
                         // no timecode means we need to find the context view and focus on our target
-                        const findView = (delay: number) => {
-                            const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
-                            if (retryDocView) {
-                                // we found the target in the context.
-                                Doc.linkFollowHighlight(retryDocView.rootDoc);
-                                retryDocView.focus(targetDoc, {
-                                    willZoom,
-                                    afterFocus: (didFocus: boolean) =>
-                                        new Promise<ViewAdjustment>(res => {
-                                            !noSelect && focusAndFinish(true);
-                                            res(ViewAdjustment.doNothing);
-                                        }),
-                                }); // focus on the target in the context
-                            } else if (delay > 1000) {
-                                // we didn't find the target, so it must have moved out of the context.  Go back to just creating it.
-                                if (closeContextIfNotFound) targetDocContextView.props.removeDocument?.(targetDocContextView.rootDoc);
-                                if (targetDoc.layout) {
-                                    // there will no layout for a TEXTANCHOR type document
-                                    createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
-                                }
-                            } else {
-                                setTimeout(() => findView(delay + 200), 200);
-                            }
-                        };
-                        setTimeout(() => findView(0), 0);
+                        const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
+                        if (retryDocView) {
+                            // we found the target in the context.
+                            Doc.linkFollowHighlight(retryDocView.rootDoc);
+                            retryDocView.focus(targetDoc, {
+                                ...options,
+                                // originalTarget -- needed?
+                                afterFocus: (didFocus: boolean) =>
+                                    new Promise<ViewAdjustment>(res => {
+                                        !options.noSelect && focusAndFinish(true);
+                                        res(ViewAdjustment.doNothing);
+                                    }),
+                            }); // focus on the target in the context
+                        } else if (targetDoc.layout) {
+                            // there will no layout for a TEXTANCHOR type document
+                            createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
+                        }
                     }
                 } else {
                     if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
                         const docContextView = this.getFirstDocumentView(docContext[0]);
                         if (docContextView) {
-                            return docContextView.iconify(() =>
-                                this.jumpToDocument(targetDoc, willZoom, createViewFunc, docContext.slice(1, docContext.length), linkDoc, closeContextIfNotFound, originatingDoc, finished, originalTarget, noSelect, presZoomScale)
-                            );
+                            return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished));
                         }
                     }
                     // there's no context view so we need to create one first and try again when that finishes
                     createViewFunc(
                         targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
-                        () =>
-                            this.jumpToDocument(
-                                targetDoc,
-                                willZoom,
-                                (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished),
-                                docContext,
-                                linkDoc,
-                                true /* if target not found, get rid of context just created */,
-                                originatingDoc,
-                                finished,
-                                originalTarget
-                            )
+                        () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished)
                     );
                 }
             }
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 37571ae01..916eee4b7 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -1,27 +1,27 @@
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { BatchedArray } from "array-batcher";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { extname } from "path";
-import Measure, { ContentRect } from "react-measure";
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../fields/Doc";
-import { Id } from "../../../fields/FieldSymbols";
-import { List } from "../../../fields/List";
-import { listSpec } from "../../../fields/Schema";
-import { SchemaHeaderField } from "../../../fields/SchemaHeaderField";
-import { BoolCast, Cast, NumCast } from "../../../fields/Types";
-import { AcceptableMedia, Upload } from "../../../server/SharedMediaTypes";
-import { Utils } from "../../../Utils";
-import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
-import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents";
-import { Networking } from "../../Network";
-import { FieldView, FieldViewProps } from "../../views/nodes/FieldView";
-import { DocumentManager } from "../DocumentManager";
-import "./DirectoryImportBox.scss";
-import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from "./ImportMetadataEntry";
-import React = require("react");
+import { BatchedArray } from 'array-batcher';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { extname } from 'path';
+import Measure, { ContentRect } from 'react-measure';
+import { Doc, DocListCast, DocListCastAsync, Opt } from '../../../fields/Doc';
+import { Id } from '../../../fields/FieldSymbols';
+import { List } from '../../../fields/List';
+import { listSpec } from '../../../fields/Schema';
+import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
+import { BoolCast, Cast, NumCast } from '../../../fields/Types';
+import { AcceptableMedia, Upload } from '../../../server/SharedMediaTypes';
+import { Utils } from '../../../Utils';
+import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
+import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents';
+import { Networking } from '../../Network';
+import { FieldView, FieldViewProps } from '../../views/nodes/FieldView';
+import { DocumentManager } from '../DocumentManager';
+import './DirectoryImportBox.scss';
+import ImportMetadataEntry, { keyPlaceholder, valuePlaceholder } from './ImportMetadataEntry';
+import React = require('react');
 
-const unsupported = ["text/html", "text/plain"];
+const unsupported = ['text/html', 'text/plain'];
 
 @observer
 export class DirectoryImportBox extends React.Component<FieldViewProps> {
@@ -29,7 +29,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
     @observable private top = 0;
     @observable private left = 0;
     private dimensions = 50;
-    @observable private phase = "";
+    @observable private phase = '';
     private disposer: Opt<IReactionDisposer>;
 
     @observable private entries: ImportMetadataEntry[] = [];
@@ -40,7 +40,9 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
     @observable private uploading = false;
     @observable private removeHover = false;
 
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(DirectoryImportBox, fieldKey); }
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(DirectoryImportBox, fieldKey);
+    }
 
     constructor(props: FieldViewProps) {
         super(props);
@@ -71,7 +73,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
     handleSelection = async (e: React.ChangeEvent<HTMLInputElement>) => {
         runInAction(() => {
             this.uploading = true;
-            this.phase = "Initializing download...";
+            this.phase = 'Initializing download...';
         });
 
         const docs: Doc[] = [];
@@ -79,7 +81,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const files = e.target.files;
         if (!files || files.length === 0) return;
 
-        const directory = (files.item(0) as any).webkitRelativePath.split("/", 1)[0];
+        const directory = (files.item(0) as any).webkitRelativePath.split('/', 1)[0];
 
         const validated: File[] = [];
         for (let i = 0; i < files.length; i++) {
@@ -100,7 +102,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const sizes: number[] = [];
         const modifiedDates: number[] = [];
 
-        runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
+        runInAction(() => (this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`));
 
         const batched = BatchedArray.from(validated, { batchSize: 15 });
         const uploads = await batched.batchedMapAsync<Upload.FileResponse<Upload.ImageInformation>>(async (batch, collector) => {
@@ -109,23 +111,28 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
                 modifiedDates.push(file.lastModified);
             });
             collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch)));
-            runInAction(() => this.completed += batch.length);
+            runInAction(() => (this.completed += batch.length));
         });
 
-        await Promise.all(uploads.map(async response => {
-            const { source: { type }, result } = response;
-            if (result instanceof Error) {
-                return;
-            }
-            const { accessPaths, exifData } = result;
-            const path = Utils.prepend(accessPaths.agnostic.client);
-            const document = type && await DocUtils.DocumentFromType(type, path, { _width: 300 });
-            const { data, error } = exifData;
-            if (document) {
-                Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
-                docs.push(document);
-            }
-        }));
+        await Promise.all(
+            uploads.map(async response => {
+                const {
+                    source: { type },
+                    result,
+                } = response;
+                if (result instanceof Error) {
+                    return;
+                }
+                const { accessPaths, exifData } = result;
+                const path = Utils.prepend(accessPaths.agnostic.client);
+                const document = type && (await DocUtils.DocumentFromType(type, path, { _width: 300 }));
+                const { data, error } = exifData;
+                if (document) {
+                    Doc.GetProto(document).exif = error || Doc.Get.FromJson({ data });
+                    docs.push(document);
+                }
+            })
+        );
 
         for (let i = 0; i < docs.length; i++) {
             const doc = docs[i];
@@ -146,7 +153,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             _height: 500,
             _chromeHidden: true,
             x: NumCast(doc.x),
-            y: NumCast(doc.y) + offset
+            y: NumCast(doc.y) + offset,
         };
         const parent = this.props.ContainingCollectionView;
         if (parent) {
@@ -154,14 +161,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             if (docs.length < 50) {
                 importContainer = Docs.Create.MasonryDocument(docs, options);
             } else {
-                const headers = [new SchemaHeaderField("title"), new SchemaHeaderField("size")];
+                const headers = [new SchemaHeaderField('title'), new SchemaHeaderField('size')];
                 importContainer = Docs.Create.SchemaDocument(headers, docs, options);
             }
-            runInAction(() => this.phase = 'External: uploading files to Google Photos...');
+            runInAction(() => (this.phase = 'External: uploading files to Google Photos...'));
             await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
-            Doc.AddDocToList(Doc.GetProto(parent.props.Document), "data", importContainer);
+            Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer);
             !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
-            DocumentManager.Instance.jumpToDocument(importContainer, true, undefined, []);
+            DocumentManager.Instance.jumpToDocument(importContainer, { willZoom: true }, undefined, []);
         }
 
         runInAction(() => {
@@ -169,14 +176,14 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             this.quota = 1;
             this.completed = 0;
         });
-    }
+    };
 
     componentDidMount() {
-        this.selector.current!.setAttribute("directory", "");
-        this.selector.current!.setAttribute("webkitdirectory", "");
+        this.selector.current!.setAttribute('directory', '');
+        this.selector.current!.setAttribute('webkitdirectory', '');
         this.disposer = reaction(
             () => this.completed,
-            completed => runInAction(() => this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`)
+            completed => runInAction(() => (this.phase = `Internal: uploading ${this.quota - completed} files to Dash...`))
         );
     }
 
@@ -193,7 +200,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const offset = this.dimensions / 2;
         this.left = bounds.width / 2 - offset;
         this.top = bounds.height / 2 - offset;
-    }
+    };
 
     @action
     addMetadataEntry = async () => {
@@ -201,8 +208,8 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         entryDoc.checked = false;
         entryDoc.key = keyPlaceholder;
         entryDoc.value = valuePlaceholder;
-        Doc.AddDocToList(this.props.Document, "data", entryDoc);
-    }
+        Doc.AddDocToList(this.props.Document, 'data', entryDoc);
+    };
 
     @action
     remove = async (entry: ImportMetadataEntry) => {
@@ -217,7 +224,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
                 }
             }
         }
-    }
+    };
 
     render() {
         const dimensions = 50;
@@ -228,193 +235,204 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
         const uploading = this.uploading;
         const showRemoveLabel = this.removeHover;
         const persistent = this.persistent;
-        let percent = `${completed / quota * 100}`;
-        percent = percent.split(".")[0];
-        percent = percent.startsWith("100") ? "99" : percent;
+        let percent = `${(completed / quota) * 100}`;
+        percent = percent.split('.')[0];
+        percent = percent.startsWith('100') ? '99' : percent;
         const marginOffset = (percent.length === 1 ? 5 : 0) - 1.6;
-        const message = <span className={"phase"}>{this.phase}</span>;
-        const centerPiece = this.phase.includes("Google Photos") ?
-            <img src={"/assets/google_photos.png"} style={{
-                transition: "0.4s opacity ease",
-                width: 30,
-                height: 30,
-                opacity: uploading ? 1 : 0,
-                pointerEvents: "none",
-                position: "absolute",
-                left: 12,
-                top: this.top + 10,
-                fontSize: 18,
-                color: "white",
-                marginLeft: this.left + marginOffset
-            }} />
-            : <div
+        const message = <span className={'phase'}>{this.phase}</span>;
+        const centerPiece = this.phase.includes('Google Photos') ? (
+            <img
+                src={'/assets/google_photos.png'}
                 style={{
-                    transition: "0.4s opacity ease",
+                    transition: '0.4s opacity ease',
+                    width: 30,
+                    height: 30,
                     opacity: uploading ? 1 : 0,
-                    pointerEvents: "none",
-                    position: "absolute",
+                    pointerEvents: 'none',
+                    position: 'absolute',
+                    left: 12,
+                    top: this.top + 10,
+                    fontSize: 18,
+                    color: 'white',
+                    marginLeft: this.left + marginOffset,
+                }}
+            />
+        ) : (
+            <div
+                style={{
+                    transition: '0.4s opacity ease',
+                    opacity: uploading ? 1 : 0,
+                    pointerEvents: 'none',
+                    position: 'absolute',
                     left: 10,
                     top: this.top + 12.3,
                     fontSize: 18,
-                    color: "white",
-                    marginLeft: this.left + marginOffset
-                }}>{percent}%</div>;
+                    color: 'white',
+                    marginLeft: this.left + marginOffset,
+                }}>
+                {percent}%
+            </div>
+        );
         return (
             <Measure offset onResize={this.preserveCentering}>
-                {({ measureRef }) =>
-                    <div ref={measureRef} style={{ width: "100%", height: "100%", pointerEvents: "all" }} >
+                {({ measureRef }) => (
+                    <div ref={measureRef} style={{ width: '100%', height: '100%', pointerEvents: 'all' }}>
                         {message}
                         <input
-                            id={"selector"}
+                            id={'selector'}
                             ref={this.selector}
                             onChange={this.handleSelection}
                             type="file"
                             style={{
-                                position: "absolute",
-                                display: "none"
-                            }} />
+                                position: 'absolute',
+                                display: 'none',
+                            }}
+                        />
                         <label
-                            htmlFor={"selector"}
+                            htmlFor={'selector'}
                             style={{
                                 opacity: isEditing ? 0 : 1,
-                                pointerEvents: isEditing ? "none" : "all",
-                                transition: "0.4s ease opacity"
-                            }}
-                        >
-                            <div style={{
-                                width: dimensions,
-                                height: dimensions,
-                                borderRadius: "50%",
-                                background: "black",
-                                position: "absolute",
-                                left: this.left,
-                                top: this.top
-                            }} />
-                            <div style={{
-                                position: "absolute",
-                                left: this.left + 8,
-                                top: this.top + 10,
-                                opacity: uploading ? 0 : 1,
-                                transition: "0.4s opacity ease"
+                                pointerEvents: isEditing ? 'none' : 'all',
+                                transition: '0.4s ease opacity',
                             }}>
-                                <FontAwesomeIcon icon={"cloud-upload-alt"} color="#FFFFFF" size={"2x"} />
+                            <div
+                                style={{
+                                    width: dimensions,
+                                    height: dimensions,
+                                    borderRadius: '50%',
+                                    background: 'black',
+                                    position: 'absolute',
+                                    left: this.left,
+                                    top: this.top,
+                                }}
+                            />
+                            <div
+                                style={{
+                                    position: 'absolute',
+                                    left: this.left + 8,
+                                    top: this.top + 10,
+                                    opacity: uploading ? 0 : 1,
+                                    transition: '0.4s opacity ease',
+                                }}>
+                                <FontAwesomeIcon icon={'cloud-upload-alt'} color="#FFFFFF" size={'2x'} />
                             </div>
                             <img
                                 style={{
                                     width: 80,
                                     height: 80,
-                                    transition: "0.4s opacity ease",
+                                    transition: '0.4s opacity ease',
                                     opacity: uploading ? 0.7 : 0,
-                                    position: "absolute",
+                                    position: 'absolute',
                                     top: this.top - 15,
-                                    left: this.left - 15
+                                    left: this.left - 15,
                                 }}
-                                src={"/assets/loading.gif"}></img>
+                                src={'/assets/loading.gif'}></img>
                         </label>
                         <input
-                            type={"checkbox"}
-                            onChange={e => runInAction(() => this.persistent = e.target.checked)}
+                            type={'checkbox'}
+                            onChange={e => runInAction(() => (this.persistent = e.target.checked))}
                             style={{
                                 margin: 0,
-                                position: "absolute",
+                                position: 'absolute',
                                 left: 10,
                                 bottom: 10,
                                 opacity: isEditing || uploading ? 0 : 1,
-                                transition: "0.4s opacity ease",
-                                pointerEvents: isEditing || uploading ? "none" : "all"
+                                transition: '0.4s opacity ease',
+                                pointerEvents: isEditing || uploading ? 'none' : 'all',
                             }}
                             checked={this.persistent}
-                            onPointerEnter={action(() => this.removeHover = true)}
-                            onPointerLeave={action(() => this.removeHover = false)}
+                            onPointerEnter={action(() => (this.removeHover = true))}
+                            onPointerLeave={action(() => (this.removeHover = false))}
                         />
                         <p
                             style={{
-                                position: "absolute",
+                                position: 'absolute',
                                 left: 27,
                                 bottom: 8.4,
                                 fontSize: 12,
                                 opacity: showRemoveLabel ? 1 : 0,
-                                transition: "0.4s opacity ease"
-                            }}>Template will be <span style={{ textDecoration: "underline", textDecorationColor: persistent ? "green" : "red", color: persistent ? "green" : "red" }}>{persistent ? "kept" : "removed"}</span> after upload</p>
+                                transition: '0.4s opacity ease',
+                            }}>
+                            Template will be <span style={{ textDecoration: 'underline', textDecorationColor: persistent ? 'green' : 'red', color: persistent ? 'green' : 'red' }}>{persistent ? 'kept' : 'removed'}</span> after upload
+                        </p>
                         {centerPiece}
                         <div
                             style={{
-                                position: "absolute",
+                                position: 'absolute',
                                 top: 10,
                                 right: 10,
-                                borderRadius: "50%",
+                                borderRadius: '50%',
                                 width: 25,
                                 height: 25,
-                                background: "black",
-                                pointerEvents: uploading ? "none" : "all",
+                                background: 'black',
+                                pointerEvents: uploading ? 'none' : 'all',
                                 opacity: uploading ? 0 : 1,
-                                transition: "0.4s opacity ease"
+                                transition: '0.4s opacity ease',
                             }}
-                            title={isEditing ? "Back to Upload" : "Add Metadata"}
-                            onClick={action(() => this.editingMetadata = !this.editingMetadata)}
+                            title={isEditing ? 'Back to Upload' : 'Add Metadata'}
+                            onClick={action(() => (this.editingMetadata = !this.editingMetadata))}
                         />
                         <FontAwesomeIcon
                             style={{
-                                pointerEvents: "none",
-                                position: "absolute",
+                                pointerEvents: 'none',
+                                position: 'absolute',
                                 right: isEditing ? 14 : 15,
                                 top: isEditing ? 15.4 : 16,
                                 opacity: uploading ? 0 : 1,
-                                transition: "0.4s opacity ease"
+                                transition: '0.4s opacity ease',
                             }}
-                            icon={isEditing ? "cloud-upload-alt" : "tag"}
+                            icon={isEditing ? 'cloud-upload-alt' : 'tag'}
                             color="#FFFFFF"
-                            size={"1x"}
+                            size={'1x'}
                         />
                         <div
                             style={{
-                                transition: "0.4s ease opacity",
-                                width: "100%",
-                                height: "100%",
-                                pointerEvents: isEditing ? "all" : "none",
+                                transition: '0.4s ease opacity',
+                                width: '100%',
+                                height: '100%',
+                                pointerEvents: isEditing ? 'all' : 'none',
                                 opacity: isEditing ? 1 : 0,
-                                overflowY: "scroll"
-                            }}
-                        >
+                                overflowY: 'scroll',
+                            }}>
                             <div
                                 style={{
-                                    borderRadius: "50%",
+                                    borderRadius: '50%',
                                     width: 25,
                                     height: 25,
                                     marginLeft: 10,
-                                    position: "absolute",
+                                    position: 'absolute',
                                     right: 41,
-                                    top: 10
+                                    top: 10,
                                 }}
-                                title={"Add Metadata Entry"}
-                                onClick={this.addMetadataEntry}
-                            >
+                                title={'Add Metadata Entry'}
+                                onClick={this.addMetadataEntry}>
                                 <FontAwesomeIcon
                                     style={{
-                                        pointerEvents: "none",
+                                        pointerEvents: 'none',
                                         marginLeft: 6.4,
-                                        marginTop: 5.2
+                                        marginTop: 5.2,
                                     }}
-                                    icon={"plus"}
-                                    size={"1x"}
+                                    icon={'plus'}
+                                    size={'1x'}
                                 />
                             </div>
-                            <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }} >Add metadata to your import...</p>
-                            <hr style={{ margin: "6px 10px 12px 10px" }} />
-                            {entries.map(doc =>
+                            <p style={{ paddingLeft: 10, paddingTop: 8, paddingBottom: 7 }}>Add metadata to your import...</p>
+                            <hr style={{ margin: '6px 10px 12px 10px' }} />
+                            {entries.map(doc => (
                                 <ImportMetadataEntry
                                     Document={doc}
                                     key={doc[Id]}
                                     remove={this.remove}
-                                    ref={(el) => { if (el) this.entries.push(el); }}
+                                    ref={el => {
+                                        if (el) this.entries.push(el);
+                                    }}
                                     next={this.addMetadataEntry}
                                 />
-                            )}
+                            ))}
                         </div>
                     </div>
-                }
+                )}
             </Measure>
         );
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 282116f1b..4f742817a 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,10 +1,11 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, DocCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
-import { DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
+import { PresEffect, PresEffectDirection } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { UndoManager } from './UndoManager';
@@ -56,7 +57,7 @@ export class LinkFollower {
                 createTabForTarget(false);
             } else {
                 // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
-                docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
+                docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget });
             }
         };
         runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
@@ -81,52 +82,45 @@ export class LinkFollower {
         const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0);
         const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
         const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
-        const followLinks = sourceDoc.isPushpin || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
+        const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1);
         var count = 0;
         const allFinished = () => ++count === followLinks.length && finished?.();
         followLinks.forEach(async linkDoc => {
-            if (linkDoc) {
-                const target = (
-                    sourceDoc === linkDoc.anchor1
-                        ? linkDoc.anchor2
-                        : sourceDoc === linkDoc.anchor2
-                        ? linkDoc.anchor1
-                        : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)
-                        ? linkDoc.anchor2
-                        : linkDoc.anchor1
-                ) as Doc;
-                if (target) {
-                    const zoom = BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false);
-                    if (target.TourMap) {
-                        const fieldKey = Doc.LayoutFieldKey(target);
-                        const tour = DocListCast(target[fieldKey]).reverse();
-                        LightboxView.SetLightboxDoc(currentContext, undefined, tour);
-                        setTimeout(LightboxView.Next);
-                        allFinished();
-                    } else {
-                        const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
-                        const containerDoc = containerAnnoDoc || target;
-                        var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
-                        while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
-                            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,
-                            undefined,
-                            undefined,
-                            Cast(sourceDoc.presZoom, 'number', null)
-                        );
-                    }
-                } else {
+            const target = (
+                sourceDoc === linkDoc.anchor1
+                    ? linkDoc.anchor2
+                    : sourceDoc === linkDoc.anchor2
+                    ? linkDoc.anchor1
+                    : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)
+                    ? linkDoc.anchor2
+                    : linkDoc.anchor1
+            ) as Doc;
+            if (target) {
+                const options: DocFocusOptions = {
+                    playAudio: BoolCast(sourceDoc.followLinkAudio),
+                    toggleTarget: BoolCast(sourceDoc.followLinkToggle),
+                    willZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
+                    zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500),
+                    zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null),
+                    effect: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffect) as PresEffect,
+                    effectDirection: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffectDirection) as PresEffectDirection,
+                    originatingDoc: sourceDoc,
+                };
+                if (target.TourMap) {
+                    const fieldKey = Doc.LayoutFieldKey(target);
+                    const tour = DocListCast(target[fieldKey]).reverse();
+                    LightboxView.SetLightboxDoc(currentContext, undefined, tour);
+                    setTimeout(LightboxView.Next);
                     allFinished();
+                } else {
+                    const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
+                    const containerDoc = containerAnnoDoc || target;
+                    var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
+                    while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
+                        containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                    }
+                    const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
+                    DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), targetContexts, allFinished);
                 }
             } else {
                 allFinished();
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 4b0310e76..4cd45fc9c 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -392,7 +392,7 @@ export class SharingManager extends React.Component<{}> {
                 onClick={() => {
                     let context: Opt<CollectionView>;
                     if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) {
-                        DocumentManager.Instance.jumpToDocument(this.targetDoc, true, undefined, [context.props.Document]);
+                        DocumentManager.Instance.jumpToDocument(this.targetDoc, { willZoom: true }, undefined, [context.props.Document]);
                     }
                 }}
                 onPointerEnter={action(() => {
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 043a83d16..c9c09b63b 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -147,7 +147,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>()
                 setTimeout(() =>
                     docs.map(doc => {
                         // this allows 'addDocument' to see the annotationOn field in order to create a pushin
-                        Doc.SetInPlace(doc, 'isPushpin', undefined, true);
+                        Doc.SetInPlace(doc, 'followLinkToggle', undefined, true);
                         doc.annotationOn === this.props.Document && Doc.SetInPlace(doc, 'annotationOn', undefined, true);
                     })
                 );
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index aa2818e0d..36ea3ef18 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -41,7 +41,7 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
-import { DocComponentView } from './nodes/DocumentView';
+import { DocComponentView, DocFocusOptions } from './nodes/DocumentView';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { StyleProp } from './StyleProvider';
@@ -83,8 +83,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return this._subContentView?.getAnchor?.() || this.rootDoc;
     };
 
-    scrollFocus = (textAnchor: Doc, smooth: boolean) => {
-        return this._subContentView?.scrollFocus?.(textAnchor, smooth);
+    scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
+        return this._subContentView?.scrollFocus?.(textAnchor, options);
     };
     /**
      * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 165bb69bb..965cd560f 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -206,7 +206,7 @@ export class MainView extends React.Component {
         document.addEventListener('dash', (e: any) => {
             // event used by chrome plugin to tell Dash which document to focus on
             const id = FormattedTextBox.GetDocFromUrl(e.detail);
-            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, false, undefined, []) : null));
+            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, { willZoom: false }, undefined, []) : null));
         });
         document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
         this.initEventListeners();
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 1162cde50..07371c9d5 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -92,7 +92,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
             DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, {
                 dragComplete: e => {
                     if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) {
-                        e.annoDragData.linkSourceDoc.isPushpin = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
+                        e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc;
                         e.annoDragData.linkSourceDoc.followLinkZoom = false;
                     }
                 },
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 3e199919e..f7cc32cff 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1412,14 +1412,14 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     changeFollowBehavior = action((follow: string) => this.selectedDoc && (this.selectedDoc.followLinkLocation = follow));
 
     @undoBatch
-    changeAnimationBehavior = action((behavior: string) => this.destinationAnchor && (this.destinationAnchor.presEffect = behavior);
+    changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior));
 
     @undoBatch
-    changeEffectDirection = action((effect: PresEffectDirection) => this.destinationAnchor && (this.destinationAnchor.presEffectDirection = effect);
+    changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.linkAnimDirection = effect));
 
     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.MEDIUM_BLUE : '';
+        const lanch = this.sourceAnchor;
+        const color = lanch?.linkAnimDirection === direction || (direction === PresEffectDirection.Center && !lanch?.linkAnimDirection) ? Colors.MEDIUM_BLUE : '';
         return (
             <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                 <div
@@ -1514,7 +1514,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         if (change) scale += change;
         if (scale < 0.01) scale = 0.01;
         if (scale > 1) scale = 1;
-        this.sourceAnchor && (this.sourceAnchor.presZoom = scale);
+        this.sourceAnchor && (this.sourceAnchor.linkZoomScale = scale);
     };
 
     /**
@@ -1530,7 +1530,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     render() {
         const isNovice = Doc.noviceMode;
-        const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3));
+        const zoom = Number((NumCast(this.sourceAnchor?.linkZoomScale, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
         const indent = 30;
         const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!));
@@ -1621,7 +1621,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         </div>
                                         <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.destinationAnchor?.presEffect, 'default')}>
+                                            <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.linkAnimEffect, '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>
@@ -1639,9 +1639,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             '0.1',
                                             '0.1',
                                             '10',
-                                            NumCast(this.destinationAnchor?.presTransition) / 1000,
+                                            NumCast(this.sourceAnchor?.linkTransitionTime) / 1000,
                                             true,
-                                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.destinationAnchor && (this.destinationAnchor.presTransition = timeInMS)),
+                                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.linkTransitionTime = timeInMS)),
                                             indent
                                         )}{' '}
                                         <div
@@ -1668,6 +1668,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
                                             </button>
                                         </div>
+                                        <div className="propertiesView-input inline">
+                                            <p>Toggle Target (Show/Hide)</p>
+                                            <button
+                                                style={{ background: !this.sourceAnchor?.followLinkToggle ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkToggle', 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" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                                             <p>Zoom %</p>
                                             <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 29670a1a7..5e389e17e 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -193,13 +193,12 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             const top = found.getBoundingClientRect().top;
             const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
             }
         }
         const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
         this.props.focus(this.rootDoc, {
-            willZoom: options?.willZoom,
-            scale: options?.scale,
+            ...options,
             afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
         });
     };
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index ac6391365..08aebc62d 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -255,13 +255,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             const top = found.getBoundingClientRect().top;
             const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
             }
         }
         const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing;
         this.props.focus(this.rootDoc, {
-            willZoom: options?.willZoom,
-            scale: options?.scale,
+            ...options,
             afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
         });
     };
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 7696777fd..13984171c 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -297,7 +297,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, OpenWhereMod.right);
-            setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), false, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+            setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), { willZoom: 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
     }
@@ -376,7 +376,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     focusFunc = (doc: Doc, options: DocFocusOptions) => {
         const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap;
         if (options?.willZoom !== false && shrinkwrap && this._document) {
-            const focusSpeed = NumCast(this._document.focusSpeed, 500);
+            const focusSpeed = options.zoomTime ?? 500;
             shrinkwrap();
             this._document._viewTransition = `transform ${focusSpeed}ms`;
             setTimeout(
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 56a5c3dcc..5fb3c1ac6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1164,7 +1164,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined };
             const newState = HistoryUtil.getState();
             const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc;
-            const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.scale || 0.75 : undefined);
+            const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.zoomScale || 0.75 : undefined);
             if (!cantTransform) {
                 // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
                 newState.initializers![this.Document[Id]] = { panX, panY };
@@ -1172,7 +1172,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
             // focus on the document in the collection
             const didMove = !cantTransform && !doc.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
-            const focusSpeed = options?.instant ? 0 : didMove ? NumCast(doc.focusSpeed, 500) : 0;
+            const focusSpeed = options?.instant ? 0 : didMove ? options.zoomTime ?? 500 : 0;
             // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
             if (didMove) {
                 scale && (this.Document[this.scaleFieldKey] = scale);
@@ -1538,8 +1538,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     @action
-    scrollFocus = (anchor: Doc, smooth: boolean) => {
-        const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition);
+    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
+        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
             this.rootDoc, //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 3fd6ca803..9df3e195f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -541,7 +541,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             d.y = NumCast(d.y) - this.Bounds.top;
             return d;
         });
-        const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, isPushpin: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
+        const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
         const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
         DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', '');
 
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 9778fc4fe..924e07daa 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -150,7 +150,7 @@ export class CollectionLinearView extends CollectionSubView() {
                 <span className="bottomPopup-text">
                     Currently playing:
                     {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => (
-                        <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, true, undefined, [])}>
+                        <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, { willZoom: true }, undefined, [])}>
                             {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')}
                         </span>
                     ))}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index a22999f52..c0dfeedfa 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -214,7 +214,7 @@ 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, { willZoom: false }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc));
         }
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 48e32c071..fb20887cb 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -53,6 +53,7 @@ import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
 import { PinProps, PresBox } from './trails/PresBox';
 import React = require('react');
+import { PresEffect, PresEffectDirection } from './trails';
 const { Howl } = require('howler');
 
 interface Window {
@@ -95,10 +96,17 @@ export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that
 export interface DocFocusOptions {
     originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
     willZoom?: boolean; // determines whether to zoom in on target document
-    scale?: number; // percent of containing frame to zoom into document
+    zoomScale?: number; // percent of containing frame to zoom into document
+    zoomTime?: number;
     afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
     docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
     instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
+    effect?: PresEffect; // animation effect for focus
+    effectDirection?: PresEffectDirection;
+    noSelect?: boolean; // whether target should be selected after focusing
+    playAudio?: boolean; // whether to play audio annotation on focus
+    toggleTarget?: boolean; // whether to toggle target on and off
+    originatingDoc?: Doc; // document that triggered the focus
 }
 export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
 export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
@@ -106,7 +114,7 @@ export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, p
 export interface DocComponentView {
     updateIcon?: () => void; // updates the icon representation of the document
     getAnchor?: () => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box)
-    scrollFocus?: (doc: Doc, smooth: boolean) => Opt<number>; // returns the duration of the focus
+    scrollFocus?: (doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
     setViewSpec?: (anchor: Doc, preview: boolean) => void; // sets viewing information for a componentview, typically when following a link. 'preview' tells the view to use the values without writing to the document
     reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height.  However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling.
@@ -566,7 +574,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             .forEach(spec => (this.layoutDoc[spec.replace(ViewSpecPrefix, '')] = (field => (field instanceof ObjectField ? ObjectField.MakeCopy(field) : field))(anchor[spec])));
         // after a render the general viewSpec should have created the right _componentView, so after a timeout, call the componentview to update its specific view specs
         setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
-        const focusSpeed = this._componentView?.scrollFocus?.(anchor, options?.instant !== true && !LinkDocPreview.LinkInfo);
+        const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false });
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
         this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
             ...options,
@@ -741,8 +749,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     toggleFollowLink = (location: Opt<string>, zoom?: boolean, setPushpin?: boolean): void => {
         this.Document.ignoreClick = false;
         if (setPushpin) {
-            this.Document.isPushpin = !this.Document.isPushpin;
-            this.Document._isLinkButton = this.Document.isPushpin || this.Document._isLinkButton;
+            this.Document.followLinkToggle = !this.Document.followLinkToggle;
+            this.Document._isLinkButton = this.Document.followLinkToggle || this.Document._isLinkButton;
         } else {
             this.Document._isLinkButton = !this.Document._isLinkButton;
         }
@@ -759,14 +767,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     toggleTargetOnClick = (): void => {
         this.Document.ignoreClick = false;
         this.Document._isLinkButton = true;
-        this.Document.isPushpin = true;
+        this.Document.followLinkToggle = true;
     };
     @undoBatch
     @action
     followLinkOnClick = (location: Opt<string>, zoom: boolean): void => {
         this.Document.ignoreClick = false;
         this.Document._isLinkButton = true;
-        this.Document.isPushpin = false;
+        this.Document.followLinkToggle = false;
         this.Document.followLinkZoom = zoom;
         this.Document.followLinkLocation = location;
     };
@@ -775,7 +783,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     selectOnClick = (): void => {
         this.Document.ignoreClick = false;
         this.Document._isLinkButton = false;
-        this.Document.isPushpin = false;
+        this.Document.followLinkToggle = false;
         this.Document.onClick = this.layoutDoc.onClick = undefined;
     };
     @undoBatch
@@ -934,7 +942,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     onClicks.push({ description: this.Document.isLinkButton ? 'Remove Follow Behavior' : 'Follow Link in Place', event: () => this.toggleFollowLink('inPlace', false, false), icon: 'link' });
                     !this.Document.isLinkButton && onClicks.push({ description: 'Follow Link on Right', event: () => this.toggleFollowLink('add:right', false, false), icon: 'link' });
                     onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(undefined, false, false), icon: 'link' });
-                    onClicks.push({ description: (this.Document.isPushpin ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
+                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Pushpin', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
                     onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', addDivider: true, noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
                 } else if (DocListCast(this.Document.links).length) {
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 1d79febdf..416107859 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -31,7 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import React = require('react');
 import { PresBox } from './trails';
-import { DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentViewProps } from './DocumentView';
 
 export const pageSchema = createSchema({
     googlePhotosUrl: 'string',
@@ -62,8 +62,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     };
 
     @action
-    scrollFocus = (anchor: Doc, smooth: boolean) => {
-        const focusSpeed = !smooth ? 0 : NumCast(anchor.presTransition);
+    scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
+        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
             this.rootDoc, //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
@@ -168,7 +168,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const cropping = Doc.MakeCopy(region, true);
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
-        Doc.GetProto(region).isPushpin = true;
+        Doc.GetProto(region).followLinkToggle = true;
         this.addDocument(region);
         const anchx = NumCast(cropping.x);
         const anchy = NumCast(cropping.y);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 39e323247..b19c4a9e2 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -28,6 +28,7 @@ import './PDFBox.scss';
 import { VideoBox } from './VideoBox';
 import React = require('react');
 import { PresBox } from './trails';
+import { DocFocusOptions } from './DocumentView';
 
 @observer
 export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps>() {
@@ -94,7 +95,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const cropping = Doc.MakeCopy(region, true);
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
-        Doc.GetProto(region).isPushpin = true;
+        Doc.GetProto(region).followLinkToggle = true;
         this.addDocument(region);
 
         const docViewContent = this.props.docViewPath().lastElement().ContentDiv!;
@@ -200,16 +201,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => {
         this._pdfViewer?.brushView(view);
     };
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
-            this.toggleSidebar(!smooth);
+            this.toggleSidebar(!options.instant);
             didToggle = true;
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, NumCast(doc.presTransition, NumCast(doc.focusSpeed, 500)), { pannable: doc.presPinData ? true : false });
-        return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), smooth) ?? (didToggle ? 1 : undefined);
+        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
+        return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
         const docAnchor = () => {
@@ -277,7 +278,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     setPdfViewer = (pdfViewer: PDFViewer) => {
         this._pdfViewer = pdfViewer;
         if (this._initialScrollTarget) {
-            this.scrollFocus(this._initialScrollTarget, false);
+            this.scrollFocus(this._initialScrollTarget, { instant: true });
             this._initialScrollTarget = undefined;
         }
     };
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 82d5b00f9..607cd6187 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -31,7 +31,7 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { RecordingBox } from './RecordingBox';
 import './VideoBox.scss';
 import { ObjectField } from '../../../fields/ObjectField';
-import { OpenWhere } from './DocumentView';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
 const path = require('path');
 
 /**
@@ -950,7 +950,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     };
 
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
         if (doc !== this.rootDoc) {
             const showTime = Cast(doc._timecodeToShow, 'number', null);
             showTime !== undefined && setTimeout(() => this.Seek(showTime), 100);
@@ -1006,7 +1006,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Doc.GetProto(region).backgroundColor = 'transparent';
         Doc.GetProto(region).lockedPosition = true;
         Doc.GetProto(region).title = 'region:' + this.rootDoc.title;
-        Doc.GetProto(region).isPushpin = true;
+        Doc.GetProto(region).followLinkToggle = true;
         region._timecodeToHide = NumCast(region._timecodeToShow) + 0.0001;
         this.addDocument(region);
         const anchx = NumCast(cropping.x);
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 5ce6a0eb1..64b186489 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -29,7 +29,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
-import { DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
@@ -284,17 +284,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
-    scrollFocus = (doc: Doc, smooth: boolean) => {
-        if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), !smooth);
+    scrollFocus = (doc: Doc, options: DocFocusOptions) => {
+        if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant);
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
-            this.toggleSidebar(!smooth);
+            this.toggleSidebar(options.instant);
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         if (doc !== this.rootDoc && this._outerRef.current) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
             const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight()));
             if (scrollTo !== undefined && this._initialScroll === undefined) {
-                const focusSpeed = smooth ? NumCast(doc.focusSpeed, 500) : 0;
+                const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
                 this.goTo(scrollTo, focusSpeed);
                 return focusSpeed;
             } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ce4639b76..9e91f6c46 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, OpenWhere } from '../DocumentView';
+import { DocFocusOptions, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
 import { DashDocCommentView } from './DashDocCommentView';
@@ -689,9 +689,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     pinToPres = (anchor: Doc) => this.props.pinToPres(anchor, {});
 
     @undoBatch
-    makePushpin = (anchor: Doc) => (anchor.isPushpin = !anchor.isPushpin);
+    makePushpin = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle);
 
-    isPushpin = (anchor: Doc) => BoolCast(anchor.isPushpin);
+    isPushpin = (anchor: Doc) => BoolCast(anchor.followLinkToggle);
 
     specificContextMenu = (e: React.MouseEvent): void => {
         const cm = ContextMenu.Instance;
@@ -922,10 +922,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return anchorDoc ?? this.rootDoc;
     }
 
-    scrollFocus = (textAnchor: Doc, smooth: boolean) => {
+    scrollFocus = (textAnchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
-            this.toggleSidebar(!smooth);
+            this.toggleSidebar(options.instant);
             didToggle = true;
         }
         const textAnchorId = textAnchor[Id];
@@ -966,7 +966,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
             const content = (ret.frag as any)?.content;
             if ((ret.frag.size > 2 || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
-                smooth && (this._focusSpeed = 500);
+                !options.instant && (this._focusSpeed = 500);
                 let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
                 if (ret.frag.firstChild) {
                     selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index c04b79a1e..4073677f3 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -15,7 +15,7 @@ import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Ty
 import { AudioField } from '../../../../fields/URLField';
 import { emptyFunction, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs } from '../../../documents/Documents';
+import { Docs, DocumentOptions } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -35,7 +35,7 @@ import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import { map } from 'bluebird';
-import { OpenWhere, OpenWhereMod } from '../DocumentView';
+import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -313,6 +313,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
+            let focusSpeed = 500;
             if (activeItem.presActiveFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
                 const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
@@ -334,11 +335,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
                 this.startTempMedia(targetDoc, activeItem);
             }
-            if (targetDoc) {
-                //  Doc.linkFollowHighlight(targetDoc.annotationOn instanceof Doc ? [targetDoc, targetDoc.annotationOn] : targetDoc);
-                targetDoc && runInAction(() => (targetDoc.focusSpeed = activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500)));
-                setTimeout(() => (targetDoc.focusSpeed = undefined), NumCast(targetDoc.focusSpeed) + 10);
-            }
             if (targetDoc?.lastFrame !== undefined) {
                 targetDoc._currentFrame = 0;
             }
@@ -583,8 +579,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
         } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
             LightboxView.SetLightboxDoc(undefined);
-            const zooming = activeItem.presMovement !== PresMovement.Pan;
-            DocumentManager.Instance.jumpToDocument(targetDoc, zooming, openInTab, srcContext ? [srcContext] : [], undefined, undefined, activeItem, finished, undefined, true, NumCast(activeItem.presZoom, 1));
+            const options: DocFocusOptions = {
+                willZoom: activeItem.presMovement !== PresMovement.Pan,
+                zoomScale: NumCast(activeItem.presZoom, 1),
+                zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                noSelect: true,
+                originatingDoc: activeItem,
+            };
+            DocumentManager.Instance.jumpToDocument(targetDoc, options, openInTab, srcContext ? [srcContext] : [], finished);
         } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
         }
@@ -1199,7 +1201,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, hmargin?:number) => {
+    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,7 +1210,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)`}}
+                style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }}
                 className={`toolbar-slider ${active ? '' : 'none'}`}
                 onPointerDown={e => {
                     batch = UndoManager.StartBatch('pres slider');
diff --git a/src/client/views/pdf/Annotation.tsx b/src/client/views/pdf/Annotation.tsx
index 133b882f6..7069ff399 100644
--- a/src/client/views/pdf/Annotation.tsx
+++ b/src/client/views/pdf/Annotation.tsx
@@ -55,9 +55,9 @@ class RegionAnnotation extends React.Component<IRegionAnnotationProps> {
     pinToPres = () => this.props.pinToPres(this.annoTextRegion, {});
 
     @undoBatch
-    makePushpin = () => (this.annoTextRegion.isPushpin = !this.annoTextRegion.isPushpin);
+    makePushpin = () => (this.annoTextRegion.followLinkToggle = !this.annoTextRegion.followLinkToggle);
 
-    isPushpin = () => BoolCast(this.annoTextRegion.isPushpin);
+    isPushpin = () => BoolCast(this.annoTextRegion.followLinkToggle);
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5a5c63c3d..0703ca9b4 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -16,7 +16,7 @@ import { SnappingManager } from '../../util/SnappingManager';
 import { MarqueeOptionsMenu } from '../collections/collectionFreeForm';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
-import { DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { StyleProp } from '../StyleProvider';
@@ -166,7 +166,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     // scrolls to focus on a nested annotation document.  if this is part a link preview then it will jump to the scroll location,
     // otherwise it will scroll smoothly.
-    scrollFocus = (doc: Doc, scrollTop: number, smooth: boolean) => {
+    scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => {
         const mainCont = this._mainCont.current;
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
@@ -174,7 +174,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = scrollTo;
-                else if (smooth) smoothScroll((focusSpeed = NumCast(doc.focusSpeed, 500)), mainCont, scrollTo);
+                else if (!options.instant) smoothScroll((focusSpeed = options.zoomTime??500), mainCont, scrollTo);
                 else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
             }
         } else {
@@ -295,7 +295,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
         if (scrollToAnnotation) {
-            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), true);
+            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), {zoomTime: 500});
             Doc.linkFollowHighlight(scrollToAnnotation);
         }
     };
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index c5177de90..9c656e093 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -407,7 +407,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
      * or opening it in a new tab.
      */
     selectElement = async (doc: Doc, finishFunc: () => void) => {
-        await DocumentManager.Instance.jumpToDocument(doc, true, undefined, [], undefined, undefined, undefined, finishFunc);
+        await DocumentManager.Instance.jumpToDocument(doc, { willZoom: true }, undefined, [], finishFunc);
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 613daac016c367205ff1afddd81b7b9111c52d33 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 Dec 2022 22:45:22 -0500
Subject: cleaning up following links and pres item following so that view
 transitions don't interfere when clicking quickly (eg through animation
 frames).  changed animations to animate multi-level zooming parallel.

---
 src/client/util/CurrentUserUtils.ts                |   2 +-
 src/client/util/DocumentManager.ts                 | 145 ++++++------
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/views/LightboxView.tsx                  |   8 +-
 src/client/views/PropertiesButtons.tsx             |   8 +-
 src/client/views/PropertiesView.tsx                |   5 +-
 .../views/collections/CollectionDockingView.tsx    |   3 +-
 src/client/views/collections/CollectionMenu.tsx    |  11 +-
 src/client/views/collections/TabDocView.tsx        |  16 +-
 .../CollectionFreeFormLinkView.tsx                 |   6 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  39 ++--
 .../collections/collectionFreeForm/MarqueeView.tsx |  14 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |  57 ++---
 src/client/views/nodes/DocumentView.tsx            |  54 ++++-
 src/client/views/nodes/ImageBox.tsx                |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |   2 +-
 src/client/views/nodes/VideoBox.tsx                |   2 +-
 src/client/views/nodes/button/FontIconBox.tsx      |   8 +-
 src/client/views/nodes/trails/PresBox.tsx          | 253 ++++++---------------
 19 files changed, 280 insertions(+), 357 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index d13209c4f..9dfc91e3f 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -667,7 +667,7 @@ export class CurrentUserUtils {
               title: "Perspective",                    toolTip: "View",                 btnType: ButtonType.DropdownList,                                                                                    ignoreClick: true, width: 100, scripts: { script: 'setView(value, _readOnly_)'}},
             { title: "Pin",     icon: "map-pin",       toolTip: "Pin View to Trail",    btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "tab")'},                        width: 20,  scripts: { onClick: 'pinWithView(_readOnly_, altKey)'}},
             { title: "Back",    icon: "chevron-left",  toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'prevKeyFrame(_readOnly_)'}},
-            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20,  scripts: {}},
+            { title: "Num",     icon: "",              toolTip: "Frame Number",         btnType: ButtonType.TextButton,          funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20, buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()', scripts: { script: 'return curKeyFrame(_readOnly_)'}},
             { title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,         funcs: {hidden: '!SelectionManager_selectedDocType(undefined, "freeform") || IsNoviceMode()'}, width: 20,  scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
             { title: "Fill",    icon: "fill-drip",     toolTip: "Background Fill Color",btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true, width: 20,  scripts: { script:  'return setBackgroundColor(value, _readOnly_)'}}, // Only when a document is selected
             { title: "Header",  icon: "heading",       toolTip: "Header Color",         btnType: ButtonType.ColorButton,         funcs: {hidden: '!SelectionManager_selectedDocType()'},                     ignoreClick: true,             scripts: { script:  'return setHeaderColor(value, _readOnly_)'}},
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 1b63b615b..d2368f4f6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,19 +1,19 @@
 import { action, observable, runInAction } from 'mobx';
-import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { Cast, DocCast } from '../../fields/Types';
+import { listSpec } from '../../fields/Schema';
+import { Cast } from '../../fields/Types';
+import { AudioField } from '../../fields/URLField';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
-import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, 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';
 import { CollectionView } from '../views/collections/CollectionView';
+import { LightboxView } from '../views/LightboxView';
+import { DocFocusOptions, DocumentView, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
-import { listSpec } from '../../fields/Schema';
-import { AudioField } from '../../fields/URLField';
 const { Howl } = require('howler');
 
 export class DocumentManager {
@@ -31,6 +31,30 @@ export class DocumentManager {
     //private constructor so no other class can create a nodemanager
     private constructor() {}
 
+    private _viewRenderedCbs: { doc: Doc; func: (dv: DocumentView) => any }[] = [];
+    public AddViewRenderedCb = (doc: Doc, func: (dv: DocumentView) => any) => {
+        const dv = this.getDocumentViewById(doc[Id]);
+        this._viewRenderedCbs.push({ doc, func });
+        if (dv) {
+            this.callAddViewFuncs(dv);
+        }
+    };
+    callAddViewFuncs = (view: DocumentView) => {
+        const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
+        if (callFuncs.length) {
+            this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => callFuncs.includes(vc));
+            const intTimer = setInterval(
+                () => {
+                    if (!view.ComponentView?.incrementalRendering?.()) {
+                        callFuncs.forEach(cf => cf.func(view));
+                        clearInterval(intTimer);
+                    }
+                },
+                view.ComponentView?.incrementalRendering?.() ? 0 : 100
+            );
+        }
+    };
+
     @action
     public AddView = (view: DocumentView) => {
         //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
@@ -50,6 +74,7 @@ export class DocumentManager {
         } else {
             this.DocumentViews.add(view);
         }
+        this.callAddViewFuncs(view);
     };
     public RemoveView = action((view: DocumentView) => {
         this.LinkedDocumentViews.slice().forEach(
@@ -173,7 +198,7 @@ export class DocumentManager {
         targetDoc: Doc, // document to display
         options: DocFocusOptions, // options for how to navigate to target
         createViewFunc = DocumentManager.addView, // how to create a view of the doc if it doesn't exist
-        docContext: Doc[], // context to load that should contain the target
+        docContextPath: Doc[], // context to load that should contain the target
         finished?: () => void
     ): void => {
         const originalTarget = options.originalTarget ?? targetDoc;
@@ -201,29 +226,20 @@ export class DocumentManager {
             finished?.();
         };
         const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
-        const contextDocs = docContext.length ? DocListCast(docContext[0].data) : undefined;
-        const contextDoc = contextDocs?.find(doc => Doc.AreProtosEqual(doc, targetDoc) || Doc.AreProtosEqual(doc, annotatedDoc)) ? docContext.lastElement() : undefined;
-        const targetDocContext = contextDoc || annotatedDoc;
-        const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
-        const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (annoContainerView) {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
-                annoContainerView.iconify(() =>
-                    annoContainerView.focus(targetDoc, {
-                        ...options,
-                        originalTarget,
-                        afterFocus: (didFocus: boolean) =>
-                            new Promise<ViewAdjustment>(res => {
-                                focusAndFinish(true);
-                                res(ViewAdjustment.doNothing);
-                            }),
-                    })
-                );
-                return;
-            } else if (!docView && targetDoc.type !== DocumentType.MARKER) {
+                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
+            }
+            if (!docView && targetDoc.type !== DocumentType.MARKER) {
                 annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
             }
         }
+
+        const contextDoc = docContextPath.length ? docContextPath[0] : undefined;
+        const remainingDocContext = docContextPath.length ? docContextPath.slice(1) : [];
+        const targetDocContext = contextDoc || annotatedDoc;
+        const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
+        const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (focusView) {
             !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
@@ -252,56 +268,53 @@ export class DocumentManager {
                     // we found a context view and aren't forced to create a new one ... focus on the context first..
                     wasHidden = wasHidden || targetDocContextView.rootDoc.hidden;
                     targetDocContextView.rootDoc.hidden = false; // make sure context isn't hidden
-                    targetDocContext._viewTransition = 'transform 500ms';
+
+                    if (targetDocContext.layoutKey === 'layout_icon') {
+                        return targetDocContextView.iconify(
+                            () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)),
+                            30
+                        );
+                    }
+
+                    const contextFocusTime = options.zoomTime ? options.zoomTime / 2 : 500;
+                    const remainingFocustime = options.zoomTime ? options.zoomTime - contextFocusTime : undefined;
+                    targetDocContextView.setViewTransition('transform', contextFocusTime);
+                    // this makes focusing on contexts run in parallel -- jutmp to document below makes them run sequentially
+                    this.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished));
                     targetDocContextView.props.focus(targetDocContextView.rootDoc, {
                         ...options,
+                        zoomTime: contextFocusTime,
                         // originalTarget, // needed?
                         afterFocus: async () => {
-                            targetDocContext._viewTransition = undefined;
-                            if (targetDocContext.layoutKey === 'layout_icon') {
-                                targetDocContextView.iconify(() => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed?*/ }, createViewFunc, docContext, finished));
+                            // now find the target document within the context
+                            if (targetDoc._timecodeToShow) {
+                                // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
+                                targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
+                                finished?.();
+                            } else {
+                                // otherwise, just look for the target document in this context view now that we've focused the context view
+                                if (this.getFirstDocumentView(resolvedTarget)) {
+                                    // test again for the target view snce we presumably created the context above by focusing on it
+                                    this.jumpToDocument(targetDoc, { ...options, zoomTime: remainingFocustime }, createViewFunc, remainingDocContext, finished);
+                                } else if (targetDoc.layout) {
+                                    // there will no layout for a TEXTANCHOR type document
+                                    createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
+                                }
                             }
                             return ViewAdjustment.doNothing;
                         },
                     });
-
-                    // now find the target document within the context
-                    if (targetDoc._timecodeToShow) {
-                        // if the target has a timecode, it should show up once the (presumed) video context scrubs to the display timecode;
-                        targetDocContext._currentTimecode = targetDoc.anchorTimecodeToShow;
-                        finished?.();
-                    } else {
-                        // no timecode means we need to find the context view and focus on our target
-                        const retryDocView = this.getFirstDocumentView(resolvedTarget); // test again for the target view snce we presumably created the context above by focusing on it
-                        if (retryDocView) {
-                            // we found the target in the context.
-                            Doc.linkFollowHighlight(retryDocView.rootDoc);
-                            retryDocView.focus(targetDoc, {
-                                ...options,
-                                // originalTarget -- needed?
-                                afterFocus: (didFocus: boolean) =>
-                                    new Promise<ViewAdjustment>(res => {
-                                        !options.noSelect && focusAndFinish(true);
-                                        res(ViewAdjustment.doNothing);
-                                    }),
-                            }); // focus on the target in the context
-                        } else if (targetDoc.layout) {
-                            // there will no layout for a TEXTANCHOR type document
-                            createViewFunc(Doc.BrushDoc(targetDoc), finished); //  create a new view of the target
-                        }
-                    }
                 } else {
-                    if (docContext.length && docContext[0]?.layoutKey === 'layout_icon') {
-                        const docContextView = this.getFirstDocumentView(docContext[0]);
-                        if (docContextView) {
-                            return docContextView.iconify(() => this.jumpToDocument(targetDoc, { ...options, originalTarget }, createViewFunc, docContext.slice(1, docContext.length), finished));
-                        }
+                    if (docContextPath.length && docContextPath[0]?.layoutKey === 'layout_icon') {
+                        Doc.deiconifyView(docContextPath[0]);
+                        this.jumpToDocument(targetDoc, options, createViewFunc, docContextPath, finished);
+                    } else {
+                        // there's no context view so we need to create one first and try again when that finishes
+                        createViewFunc(
+                            targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
+                            () => this.jumpToDocument(targetDoc, { ...options }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), remainingDocContext, finished)
+                        );
                     }
-                    // there's no context view so we need to create one first and try again when that finishes
-                    createViewFunc(
-                        targetDocContext, // after creating the context, this calls the finish function that will retry looking for the target
-                        () => this.jumpToDocument(targetDoc, { ...options, originalTarget }, (doc: Doc, finished?: () => void) => doc !== targetDocContext && createViewFunc(doc, finished), docContext, finished)
-                    );
                 }
             }
         }
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 4f742817a..aec4db1df 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -48,7 +48,7 @@ export class LinkFollower {
                             });
                         } else {
                             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
+                            res(where !== OpenWhere.inPlace || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace'  resetting the initial focus&zoom would negate the zoom into the target
                         }
                     }, 100);
                 });
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 1f58763d1..cf0a2fcfb 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -50,7 +50,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this.LightboxDoc._panY = this._savedState.panY;
             this.LightboxDoc._scrollTop = this._savedState.scrollTop;
             this.LightboxDoc._viewScale = this._savedState.scale;
-            this.LightboxDoc._viewTransition = undefined;
         }
         if (!doc) {
             this._docFilters && (this._docFilters.length = 0);
@@ -167,7 +166,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (targetDocView && target) {
             const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
-            targetDocView.focus(target, { originalTarget: target, willZoom: true, scale: 0.9 });
+            targetDocView.focus(target, { originalTarget: target, willZoom: true, zoomScale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
         } else {
             if (!target && LightboxView.path.length) {
@@ -177,7 +176,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     LightboxView.LightboxDoc._panY = saved.panY;
                     LightboxView.LightboxDoc._viewScale = saved.scale;
                     LightboxView.LightboxDoc._scrollTop = saved.scrollTop;
-                    LightboxView.LightboxDoc._viewTransition = undefined;
                 }
                 const pop = LightboxView.path.pop();
                 if (pop) {
@@ -211,7 +209,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (docView) {
             LightboxView._docTarget = target;
             if (!target) docView.ComponentView?.shrinkWrap?.();
-            else docView.focus(target, { willZoom: true, scale: 0.9 });
+            else docView.focus(target, { willZoom: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
@@ -290,7 +288,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                                             const target = LightboxView._docTarget;
                                             const doc = LightboxView._doc;
                                             //const targetView = target && DocumentManager.Instance.getLightboxDocumentView(target);
-                                            if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
+                                            //if (doc === r.props.Document && (!target || target === doc)) r.ComponentView?.shrinkWrap?.();
                                             //else target?.focus(target, { willZoom: true, scale: 0.9, instant: true }); // bcz: why was this here?  it breaks smooth navigation in lightbox using 'next' button
                                         })
                                     );
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 656a56a15..66c3ed439 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -14,7 +14,7 @@ import { SelectionManager } from '../util/SelectionManager';
 import { undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { VideoBox } from './nodes/VideoBox';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
@@ -117,7 +117,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             'In Place',
             'isInPlaceContainer',
             on => `${on ? 'Make' : 'Remove'} in place container flag`,
-            on => 'window',
+            on => 'window-restore',
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
@@ -129,7 +129,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         containerDoc._forceActive =
                         containerDoc._isInPlaceContainer =
                             !containerDoc._isInPlaceContainer;
-                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? 'inPlace' : undefined;
+                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? OpenWhere.inPlace : undefined;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
                     const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
                     if (menuDoc) {
@@ -139,7 +139,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         }
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
                             menuItem.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true;
-                            menuItem._followLinkLocation = 'inPlace';
+                            menuItem._followLinkLocation = OpenWhere.inPlace;
                         });
                     }
                 });
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f7cc32cff..bc08e920a 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
-    changeFollowBehavior = action((follow: string) => this.selectedDoc && (this.selectedDoc.followLinkLocation = follow));
+    changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior));
@@ -1612,7 +1612,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <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="fullScreen">Overlaying current tab</option>
+                                                <option value="lightbox">Opening in lightbox</option>
                                                 <option value="add">Opening in new tab</option>
                                                 <option value="replace">Replacing current tab</option>
                                                 <option value="inPlace">Opening in place</option>
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 8cbe548c7..ffc004df6 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -191,7 +191,7 @@ export class CollectionDockingView extends CollectionSubView() {
 
         if (!pullSide && stack) {
             stack.addChild(docContentConfig, undefined);
-            stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]);
+            setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1]));
         } else {
             const newContentItem = () => {
                 const newItem = glayRoot.layoutManager.createContentItem({ type: 'stack', content: [docContentConfig] }, instance._goldenLayout);
@@ -389,6 +389,7 @@ export class CollectionDockingView extends CollectionSubView() {
                 const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : '';
                 if (!className.includes('lm_close') && !className.includes('lm_maximise')) {
                     this._flush = UndoManager.StartBatch('golden layout edit');
+                    DocServer.UPDATE_SERVER_CACHE();
                 }
             }
         }
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx
index db81f28f6..e2594b6ae 100644
--- a/src/client/views/collections/CollectionMenu.tsx
+++ b/src/client/views/collections/CollectionMenu.tsx
@@ -31,7 +31,7 @@ import { Colors } from '../global/globalEnums';
 import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from '../InkingStroke';
 import { LightboxView } from '../LightboxView';
 import { CollectionFreeFormDocumentView } from '../nodes/CollectionFreeFormDocumentView';
-import { DocumentView } from '../nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { RichTextMenu } from '../nodes/formattedText/RichTextMenu';
 import { DefaultStyleProvider } from '../StyleProvider';
@@ -569,7 +569,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu
     startRecording = () => {
         const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' });
         //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc);
-        CollectionDockingView.AddSplit(doc, 'right');
+        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
     };
 
     @computed
@@ -733,11 +733,12 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
                 doc._currentFrame = 0;
                 CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
             }
-            CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0);
+            CollectionFreeFormDocumentView.updateKeyframe(undefined, childDocs, currentFrame || 0);
             doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame);
         }
     }
 
+    _keyTimer: NodeJS.Timeout | undefined;
     @undoBatch
     @action
     nextKeyframe = (): void => {
@@ -746,7 +747,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+        this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
         this.document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
         this.document.lastFrame = Math.max(NumCast(this.document._currentFrame), NumCast(this.document.lastFrame));
     };
@@ -758,7 +759,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView
             this.document._currentFrame = 0;
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
-        CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
         this.document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
     };
 
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 13984171c..af1fb175a 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -196,11 +196,12 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 () => SelectionManager.Views().some(v => v.topMost && v.props.Document === doc),
                 action(selected => {
                     if (selected) this._activated = true;
-                    const toggle = tab.element[0].children[1].children[0] as HTMLInputElement;
+                    const toggle = tab.element[0].children[2].children[0] as HTMLInputElement;
                     selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch');
-                    // toggle.style.fontWeight = selected ? "bold" : "";
+                    toggle.style.fontWeight = selected ? 'bold' : '';
                     // toggle.style.textTransform = selected ? "uppercase" : "";
-                })
+                }),
+                { fireImmediately: true }
             );
 
             // highlight the tab when the tab document is brushed in any part of the UI
@@ -378,14 +379,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         if (options?.willZoom !== false && shrinkwrap && this._document) {
             const focusSpeed = options.zoomTime ?? 500;
             shrinkwrap();
-            this._document._viewTransition = `transform ${focusSpeed}ms`;
-            setTimeout(
-                action(() => {
-                    this._document!._viewTransition = undefined;
-                    options?.afterFocus?.(false);
-                }),
-                focusSpeed
-            );
+            this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false));
         } else {
             options?.afterFocus?.(false);
         }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index be20bf207..c57810a98 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -270,12 +270,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     </marker>
                     <filter id="outline">
                         <feMorphology in="SourceAlpha" result="expanded" operator="dilate" radius="1" />
-                        <feFlood flood-color={`${Colors.DARK_GRAY}`} />
+                        <feFlood floodColor={`${Colors.DARK_GRAY}`} />
                         <feComposite in2="expanded" operator="in" />
                         <feComposite in="SourceGraphic" />
                     </filter>
                     <filter x="0" y="0" width="1" height="1" id={`${link[Id] + 'background'}`}>
-                        <feFlood flood-color={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
+                        <feFlood floodColor={`${StrCast(link._backgroundColor, 'white')}`} result="bg" />
                         <feMerge>
                             <feMergeNode in="bg" />
                             <feMergeNode in="SourceGraphic" />
@@ -286,7 +286,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
                     filter={LinkManager.currentLink === link ? 'url(#outline)' : ''}
                     fill="pink"
                     stroke="antiquewhite"
-                    stroke-width="4"
+                    strokeWidth="4"
                     className="collectionfreeformlinkview-linkLine"
                     style={{ pointerEvents: 'all', opacity: this._opacity, stroke, strokeWidth }}
                     onClick={this.onClickLine}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 5fb3c1ac6..b8ff59a14 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,4 +1,5 @@
 import { Bezier } from 'bezier-js';
+import { Colors } from 'browndash-components';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
@@ -47,7 +48,7 @@ import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { TabDocView } from '../TabDocView';
-import { computePivotLayout, computePassLayout as computePassLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
+import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
@@ -108,7 +109,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
-    @observable _viewTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
+    @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _hLines: number[] | undefined;
     @observable _vLines: number[] | undefined;
     @observable _firstRender = true; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.
@@ -176,6 +177,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return this.getContainerTransform().translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
     }
 
+    _keyTimer: NodeJS.Timeout | undefined;
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
@@ -183,14 +185,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             CollectionFreeFormDocumentView.setupKeyframes(this.childDocs, 0);
         }
         if (back) {
-            CollectionFreeFormDocumentView.gotoKeyframe(this.childDocs.slice());
+            this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, this.childDocs.slice());
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) - 1);
         } else {
-            CollectionFreeFormDocumentView.updateKeyframe(this.childDocs, currentFrame || 0);
+            this._keyTimer = CollectionFreeFormDocumentView.updateKeyframe(this._keyTimer, this.childDocs, currentFrame || 0);
             this.Document._currentFrame = Math.max(0, (currentFrame || 0) + 1);
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
     };
+    @observable _keyframeEditing = false;
+    @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set);
+    getKeyFrameEditing = () => this._keyframeEditing;
     onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick);
     onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
@@ -712,6 +717,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     @action
     pan = (e: PointerEvent | React.Touch | { clientX: number; clientY: number }): void => {
+        this.props.DocumentView?.().clearViewTransition();
         const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY);
         this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
         this._lastX = e.clientX;
@@ -1079,7 +1085,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
-            this._viewTransition = panTime;
+            this._panZoomTransition = panTime;
             const scale = this.getLocalTransform().inverse().Scale;
             const minScale = NumCast(this.rootDoc._viewScaleMin, 1);
             const minPanX = NumCast(this.rootDoc._panXMin, 0);
@@ -1098,7 +1104,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.setPan(NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true);
             this._lastNudge && clearTimeout(this._lastNudge);
             this._lastNudge = setTimeout(
-                action(() => (this._viewTransition = 0)),
+                action(() => (this._panZoomTransition = 0)),
                 nudgeTime
             );
             return true;
@@ -1127,7 +1133,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) {
         if (this.Document._isGroup) return;
-        this._viewTransition = transitionTime;
+        this._panZoomTransition = transitionTime;
         const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         this.layoutDoc[this.scaleFieldKey] = scale;
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
@@ -1135,7 +1141,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y);
         this.layoutDoc._panX = NumCast(this.layoutDoc._panX) - newpan[0];
         this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];
-        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._viewTransition = 0))), this._viewTransition)); // set transition to be smooth, then reset
+        return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset
     }
 
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
@@ -1185,14 +1191,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 doc.hidden && Doc.UnHighlightDoc(doc);
                 const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing;
                 if (resetView) {
-                    const restoreState = (!LightboxView.LightboxDoc || LightboxView.LightboxDoc === this.props.Document) && savedState;
+                    const restoreState = savedState;
                     if (typeof restoreState !== 'boolean') {
                         this.Document._panX = restoreState.panX;
                         this.Document._panY = restoreState.panY;
                         this.Document[this.scaleFieldKey] = restoreState.scale;
                     }
                 }
-                runInAction(() => (this._viewTransition = 0));
+                runInAction(() => (this._panZoomTransition = 0));
                 return resetView;
             };
             const xf = !cantTransform
@@ -1364,7 +1370,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             z: Cast(z, 'number'),
             color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color),
             backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.props.styleProvider?.(childDoc, this.props, StyleProp.BackgroundColor),
-            opacity: Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
+            opacity: this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
             width: Cast(childDocLayout._width, 'number'),
             height: Cast(childDocLayout._height, 'number'),
@@ -1541,7 +1547,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
-            this.rootDoc, //
+            this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
             focusSpeed,
@@ -1869,6 +1875,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         DragManager.SetSnapLines(horizLines, vertLines);
     };
 
+    incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0;
+
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
             const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
@@ -1944,7 +1952,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         zoomScaling={this.zoomScaling}
                         presPaths={this.showPresPaths}
                         presPinView={BoolCast(this.Document.presPinView)}
-                        transition={this._viewTransition ? `transform ${this._viewTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
+                        transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', null)}
                         viewDefDivClick={this.props.viewDefDivClick}>
                         {this.children}
                     </CollectionFreeFormViewPannableContents>
@@ -2265,6 +2273,11 @@ ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
 ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true);
 });
+ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
+    const selView = SelectionManager.Views();
+    if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : undefined;
+    runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
+});
 ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) {
     !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }));
 });
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 9df3e195f..7c1137292 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,6 @@ import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
 import { OpenWhere } from '../../nodes/DocumentView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
-import { PresBox } from '../../nodes/trails/PresBox';
 import { VideoBox } from '../../nodes/VideoBox';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { PreviewCursor } from '../../PreviewCursor';
@@ -244,7 +243,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         if (!(e.nativeEvent as any).marqueeHit) {
             (e.nativeEvent as any).marqueeHit = true;
             // allow marquee if right click OR alt+left click OR in adding presentation slide & left key drag mode
-            if (e.button === 2 || (e.button === 0 && e.altKey) || (PresBox.startMarquee && e.button === 0)) {
+            if (e.button === 2 || (e.button === 0 && e.altKey)) {
                 // if (e.altKey || (MarqueeView.DragMarquee && this.props.active(true))) {
                 this.setPreviewCursor(e.clientX, e.clientY, true, false);
                 // (!e.altKey) && e.stopPropagation(); // bcz: removed so that you can alt-click on button in a collection to switch link following behaviors.
@@ -273,9 +272,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             this.cleanupInteractions(true); // stop listening for events if another lower-level handle (e.g. another Marquee) has stopPropagated this
         }
         e.altKey && e.preventDefault();
-        if (PresBox.startMarquee) {
-            e.stopPropagation();
-        }
     };
 
     @action
@@ -296,11 +292,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
             document.removeEventListener('pointerdown', hideMarquee, true);
             document.removeEventListener('wheel', hideMarquee, true);
         };
-        if (PresBox.startMarquee) {
-            this.pinWithView();
-            PresBox.startMarquee = false;
-        }
-        if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100 && !PresBox.startMarquee) {
+        if (!this._commandExecuted && Math.abs(this.Bounds.height * this.Bounds.width) > 100) {
             MarqueeOptionsMenu.Instance.createCollection = this.collection;
             MarqueeOptionsMenu.Instance.delete = this.delete;
             MarqueeOptionsMenu.Instance.summarize = this.summary;
@@ -686,7 +678,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 className="marqueeView"
                 style={{
                     overflow: StrCast(this.props.Document._overflow),
-                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible || PresBox.startMarquee ? 'crosshair' : 'pointer',
+                    cursor: [InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool) || this._visible ? 'crosshair' : 'pointer',
                 }}
                 onDragOver={e => e.preventDefault()}
                 onScroll={e => (e.currentTarget.scrollTop = e.currentTarget.scrollLeft = 0)}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 868822fbf..bf1f13a06 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -115,45 +115,30 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         });
     }
 
-    public static updateKeyframe(docs: Doc[], time: number, targetDoc?: Doc) {
+    public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number, targetDoc?: Doc) {
+        if (timer) clearTimeout(timer);
+        const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true);
         const timecode = Math.round(time);
-        docs.forEach(
-            action(doc => {
-                doc._viewTransition = doc.dataTransition = 'all 1s';
-                CollectionFreeFormDocumentView.animFields.forEach(val => {
-                    const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
-                });
-                CollectionFreeFormDocumentView.animStringFields.forEach(val => {
-                    const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
-                });
-                CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
-                    const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
-                    findexed?.length <= timecode + 1 && findexed.push(undefined as any);
-                });
-            })
-        );
-        setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    doc.dataTransition = 'inherit';
-                }),
-            1010
-        );
+        docs.forEach(doc => {
+            CollectionFreeFormDocumentView.animFields.forEach(val => {
+                const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as number);
+            });
+            CollectionFreeFormDocumentView.animStringFields.forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any as string);
+            });
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => {
+                const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null);
+                findexed?.length <= timecode + 1 && findexed.push(undefined as any);
+            });
+        });
+        return newTimer;
     }
 
-    public static gotoKeyframe(docs: Doc[], duration = 1000) {
-        docs.forEach(doc => (doc._viewTransition = doc.dataTransition = `all ${duration}ms`));
-        setTimeout(
-            () =>
-                docs.forEach(doc => {
-                    doc._viewTransition = undefined;
-                    doc.dataTransition = 'inherit';
-                }),
-            1010
-        );
+    public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration = 1000) {
+        if (timer) clearTimeout(timer);
+        return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true);
     }
 
     public static setupZoom(doc: Doc, targDoc: Doc) {
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index fb20887cb..4abfef563 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -127,6 +127,7 @@ export interface DocComponentView {
     Pause?: () => void;
     setFocus?: () => void;
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
+    incrementalRendering?: () => void;
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
@@ -238,7 +239,7 @@ 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.
-    _animateScaleTime = 300; // milliseconds;
+    @observable _animateScaleTime: Opt<number>; // milliseconds for animating between views.  defaults to 300 if not uset
     @observable _animateScalingTo = 0;
     @observable _pendingDoubleClick = false;
     private _disposers: { [name: string]: IReactionDisposer } = {};
@@ -259,6 +260,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     private get topMost() {
         return this.props.renderDepth === 0 && !LightboxView.LightboxDoc;
     }
+    public get animateScaleTime() {
+        return this._animateScaleTime ?? 300;
+    }
     public get displayName() {
         return 'DocumentView(' + this.props.Document.title + ')';
     } // this makes mobx trace() statements more descriptive
@@ -1396,7 +1400,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         fontFamily: StrCast(this.Document._fontFamily, 'inherit'),
                         fontSize: Cast(this.Document._fontSize, 'string', null),
                         transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined,
-                        transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this._animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
+                        transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`,
                     }}>
                     {this.innards}
                     {this.onClickHandler && this.props.ContainingCollectionView?.props.Document._viewType === CollectionViewType.Time ? <div className="documentView-contentBlocker" /> : null}
@@ -1476,7 +1480,37 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return 'DocumentView(' + this.props.Document?.title + ')';
     } // this makes mobx trace() statements more descriptive
     public ContentRef = React.createRef<HTMLDivElement>();
+    public ViewTimer: NodeJS.Timeout | undefined; // timer for res
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    public clearViewTransition = () => {
+        this.ViewTimer && clearTimeout(this.ViewTimer);
+        this.rootDoc._viewTransition = undefined;
+    };
+    public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => {
+        this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`;
+        if (dataTrans) this.rootDoc._dataTransition = `${transProp} ${timeInMs}ms`;
+        this.ViewTimer && clearTimeout(this.ViewTimer);
+        return (this.ViewTimer = setTimeout(() => {
+            this.rootDoc._viewTransition = undefined;
+            this.rootDoc._dataTransition = 'inherit';
+            afterTrans?.();
+        }, timeInMs + 10));
+    };
+    public static SetViewTransition(docs: Doc[], transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) {
+        docs.forEach(doc => {
+            doc._viewTransition = `${transProp} ${timeInMs}ms`;
+            dataTrans && (doc.dataTransition = `${transProp} ${timeInMs}ms`);
+        });
+        return setTimeout(
+            () =>
+                docs.forEach(doc => {
+                    doc._viewTransition = undefined;
+                    dataTrans && (doc.dataTransition = 'inherit');
+                    afterTrans?.();
+                }),
+            timeInMs + 10
+        );
+    }
 
     public static showBackLinks(linkSource: Doc) {
         const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
@@ -1617,15 +1651,21 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return { left, top, right, bottom, center: this.ComponentView?.getCenter?.(xf) };
     };
 
-    public iconify(finished?: () => void) {
+    public iconify(finished?: () => void, animateTime?: number) {
         this.ComponentView?.updateIcon?.();
+        const animTime = this.docView?._animateScaleTime;
+        runInAction(() => this.docView && animateTime !== undefined && (this.docView._animateScaleTime = animateTime));
+        const finalFinished = action(() => {
+            finished?.();
+            this.docView && (this.docView._animateScaleTime = animTime);
+        });
         const layoutKey = Cast(this.Document.layoutKey, 'string', null);
         if (layoutKey !== 'layout_icon') {
-            this.switchViews(true, 'icon', finished);
+            this.switchViews(true, 'icon', finalFinished);
             if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', '');
         } else {
             const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null);
-            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finished);
+            this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished);
             this.Document.deiconifyLayout = undefined;
             this.props.bringToFront(this.rootDoc);
         }
@@ -1651,10 +1691,10 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         this.docView && (this.docView._animateScalingTo = 0);
                         finished?.();
                     }),
-                    this.docView!._animateScaleTime - 10
+                    this.docView ? Math.max(0, this.docView.animateScaleTime - 10) : 0
                 );
             }),
-            this.docView!._animateScaleTime - 10
+            this.docView ? Math.max(0, this.docView?.animateScaleTime - 10) : 0
         );
     });
 
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 416107859..19f5e9e29 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -65,7 +65,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     scrollFocus = (anchor: Doc, options: DocFocusOptions) => {
         const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
         return PresBox.restoreTargetDocView(
-            this.rootDoc, //
+            this.props.DocumentView?.(), //
             { pinDocLayout: BoolCast(anchor.presPinDocLayout) },
             anchor,
             focusSpeed,
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index b19c4a9e2..88aac67a7 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -209,7 +209,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         if (this._sidebarRef?.current?.makeDocUnfiltered(doc)) return 1;
         this._initialScrollTarget = doc;
-        PresBox.restoreTargetDocView(this.rootDoc, {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
+        PresBox.restoreTargetDocView(this.props.DocumentView?.(), {}, doc, options.zoomTime ?? 500, { pannable: doc.presPinData ? true : false });
         return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 607cd6187..a8f78edd5 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -961,7 +961,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     // renders CollectionStackedTimeline
     @computed get renderTimeline() {
         return (
-            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%` }}>
+            <div className="videoBox-stackPanel" style={{ transition: this.transition, height: `${100 - this.heightPercent}%`, display: this.heightPercent === 100 ? 'none' : '' }}>
                 <CollectionStackedTimeline
                     ref={action((r: any) => (this._stackedTimeline = r))}
                     {...this.props}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index fe8c85e5e..e477d7ae2 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -510,8 +510,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
             case ButtonType.EditableText: return this.editableText;
             case ButtonType.DropdownButton: button = this.dropdownButton; break;
             case ButtonType.ToggleButton: button = this.toggleButton; break;
-            case ButtonType.TextButton: button = (
-                    <div className={`menuButton ${this.type}`} style={{ color, backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
+            case ButtonType.TextButton: 
+                    // Script for checking the outcome of the toggle
+                    const script = ScriptCast(this.rootDoc.script);
+                    const checkResult = script?.script.run({ _readOnly_: true }).result;
+                    button = (
+                    <div className={`menuButton ${this.type}`} style={{ color, backgroundColor:checkResult ?? backgroundColor, opacity: 1, gridAutoColumns: `${NumCast(this.rootDoc._height)} auto` }}>
                         {this.Icon(color)}
                         {StrCast(this.rootDoc.buttonText) ? <div className="button-text">{StrCast(this.rootDoc.buttonText)}</div> : null}
                         {label()}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 4073677f3..169f51dac 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -5,7 +5,7 @@ import { action, computed, IReactionDisposer, observable, ObservableSet, reactio
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
 import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, FieldResult, StrListCast } from '../../../../fields/Doc';
+import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -13,9 +13,9 @@ 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 { emptyFunction, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
-import { Docs, DocumentOptions } from '../../../documents/Documents';
+import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -30,12 +30,11 @@ import { ViewBoxBaseComponent } from '../../DocComponent';
 import { Colors } from '../../global/globalEnums';
 import { LightboxView } from '../../LightboxView';
 import { CollectionFreeFormDocumentView } from '../CollectionFreeFormDocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { map } from 'bluebird';
-import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../DocumentView';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -97,7 +96,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     public selectedArray = new ObservableSet<Doc>();
 
     @observable public static Instance: PresBox;
-    @observable static startMarquee: boolean = false; // onclick "+ new slide" in presentation mode, set as true, then when marquee selection finish, onPointerUp automatically triggers PinWithView
 
     @observable _isChildActive = false;
     @observable _moveOnFromAudio: boolean = true;
@@ -140,7 +138,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return Cast(this.activeItem?.presentationTargetDoc, Doc, null);
     }
     @computed get scrollable() {
-        if (this.targetDoc.type === DocumentType.PDF || this.targetDoc.type === DocumentType.WEB || this.targetDoc.type === DocumentType.RTF || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
+        if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true;
         return false;
     }
     @computed get panable() {
@@ -214,16 +212,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         PresBox.Instance = this;
     };
 
-    // There are still other internal frames and should go through all frames before going to next slide
-    nextInternalFrame = (targetDoc: Doc, activeItem: Doc) => {
-        const currentFrame = Cast(targetDoc?._currentFrame, 'number', null);
-        const childDocs = DocListCast(targetDoc[Doc.LayoutFieldKey(targetDoc)]);
-        targetDoc._viewTransition = 'all 1s';
-        setTimeout(() => (targetDoc._viewTransition = undefined), 1010);
-        this.nextKeyframe(targetDoc, activeItem);
-        targetDoc.keyFrameEditing = true;
-    };
-
     _mediaTimer!: [NodeJS.Timeout, Doc];
     // 'Play on next' for audio or video therefore first navigate to the audio/video before it should be played
     startTempMedia = (targetDoc: Doc, activeItem: Doc) => {
@@ -279,23 +267,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const targetDoc: Doc = this.targetDoc;
         const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
         const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null);
-        const lastFrame = Cast(targetDoc.lastFrame, 'number', null);
-        const curFrame = NumCast(targetDoc._currentFrame);
         let prevSelected = this.itemIndex;
         // Functionality for group with up
         let didZoom = activeItem.presMovement;
         for (; prevSelected > 0 && this.childDocs[Math.max(0, prevSelected - 1)].groupWithUp; prevSelected--) {
             didZoom = didZoom === 'none' ? this.childDocs[prevSelected].presMovement : didZoom;
         }
-        if (lastFrame !== undefined && curFrame >= 1) {
-            // Case 1: There are still other frames and should go through all frames before going to previous slide
-            this.prevKeyframe(targetDoc, activeItem);
-        } else if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
+        if (activeItem && this.childDocs[this.itemIndex - 1] !== undefined) {
             // Case 2: There are no other frames so it should go to the previous slide
             prevSelected = Math.max(0, prevSelected - 1);
             this.nextSlide(prevSelected);
             this.rootDoc._itemIndex = prevSelected;
-            if (NumCast(prevTargetDoc.lastFrame) > 0) prevTargetDoc._currentFrame = NumCast(prevTargetDoc.lastFrame);
         } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
             // Case 3: Pres loop is on so it should go to the last slide
             this.gotoDocument(this.childDocs.length - 1, activeItem);
@@ -313,14 +295,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
-            let focusSpeed = 500;
             if (activeItem.presActiveFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
                 const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
                 if (context) {
-                    const contextView = DocumentManager.Instance.getFirstDocumentView(context);
-                    if (contextView?.ComponentView) {
-                        CollectionFreeFormDocumentView.gotoKeyframe((contextView.ComponentView as CollectionFreeFormView).childDocs.slice(), transTime);
+                    const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
+                    if (ffview) {
+                        this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
                         context._currentFrame = NumCast(activeItem.presActiveFrame);
                     }
                 }
@@ -335,9 +316,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (this.layoutDoc.presStatus !== PresStatus.Edit && (targetDoc.type === DocumentType.AUDIO || targetDoc.type === DocumentType.VID) && activeItem.mediaStart === 'auto') {
                 this.startTempMedia(targetDoc, activeItem);
             }
-            if (targetDoc?.lastFrame !== undefined) {
-                targetDoc._currentFrame = 0;
-            }
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
             this.turnOffEdit();
@@ -345,14 +323,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.onHideDocument(); //Handles hide after/before
         }
     });
-    static pinDataTypes(target: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
-        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(target.type as any) || target._viewType === CollectionViewType.Stacking;
-        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(target.type as any) || (target.type === DocumentType.COL && target._viewType === CollectionViewType.Freeform);
-        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(target.type as any);
-        const clippable = [DocumentType.COMPARISON].includes(target.type as any);
-        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(target.type as any) && target.activeFrame === undefined;
-        const poslayoutview = [DocumentType.COL].includes(target.type as any) && target.activeFrame === undefined;
-        const textview = [DocumentType.RTF].includes(target.type as any) && target.activeFrame === undefined;
+    static pinDataTypes(target?: Doc): { scrollable?: boolean; pannable?: boolean; temporal?: boolean; clippable?: boolean; dataview?: boolean; textview?: boolean; poslayoutview?: boolean; dataannos?: boolean } {
+        const targetType = target?.type as any;
+        const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking;
+        const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform);
+        const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType);
+        const clippable = [DocumentType.COMPARISON].includes(targetType);
+        const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG].includes(targetType) && target?.activeFrame === undefined;
+        const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined;
+        const textview = [DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined;
         const dataannos = false;
         return { scrollable, pannable, temporal, clippable, dataview, textview, poslayoutview, dataannos };
     }
@@ -360,8 +339,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     playAnnotation = (anno: AudioField) => {};
     @action
-    static restoreTargetDocView(bestTarget: Doc, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTarget)) {
-        const presTransitionTime = `all ${transTime}ms`;
+    static restoreTargetDocView(bestTargetView: Opt<DocumentView>, pinProps: PinProps | undefined, activeItem: Doc, transTime: number, pinDataTypes = this.pinDataTypes(bestTargetView?.rootDoc)) {
+        if (!bestTargetView) return;
+        const bestTarget = bestTargetView.rootDoc;
         let changed = false;
         if (pinProps?.pinDocLayout) {
             if (
@@ -420,7 +400,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 .map(str => JSON.parse(str) as { id: string; x: number; y: number; w: number; h: number })
                 .forEach(data => {
                     const doc = DocServer.GetCachedRefField(data.id) as Doc;
-                    doc._dataTransition = presTransitionTime;
+                    doc._dataTransition = `all ${transTime}ms`;
                     doc.x = data.x;
                     doc.y = data.y;
                     doc._width = data.w;
@@ -456,8 +436,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
         }
         if (changed) {
-            bestTarget._viewTransition = presTransitionTime;
-            return setTimeout(() => (bestTarget._viewTransition = undefined), transTime + 10);
+            return bestTargetView.setViewTransition('all', transTime);
         }
     }
 
@@ -519,8 +498,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
         }
     }
-
-    static _navTimer: NodeJS.Timeout | undefined;
     /**
      * This method makes sure that cursor navigates to the element that
      * has the option open and last in the group.
@@ -549,55 +526,59 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const selViewCache = Array.from(this.selectedArray);
         const dragViewCache = Array.from(this._dragArray);
         const eleViewCache = Array.from(this._eleArray);
-        const self = this;
         const resetSelection = action(() => {
-            const presDocView = DocumentManager.Instance.getDocumentView(self.rootDoc);
+            const presDocView = DocumentManager.Instance.getDocumentView(this.rootDoc);
             if (presDocView) SelectionManager.SelectView(presDocView, false);
-            self.rootDoc.presStatus = presStatus;
-            self.clearSelectedArray();
-            selViewCache.forEach(doc => self.addToSelectedArray(doc));
-            self._dragArray.splice(0, self._dragArray.length, ...dragViewCache);
-            self._eleArray.splice(0, self._eleArray.length, ...eleViewCache);
+            this.rootDoc.presStatus = presStatus;
+            this.clearSelectedArray();
+            selViewCache.forEach(doc => this.addToSelectedArray(doc));
+            this._eleArray.splice(0, this._eleArray.length, ...eleViewCache);
         });
-        const openInTab = (doc: Doc, finished?: () => void) => {
-            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.add);
+        const createDocView = (doc: Doc, finished?: () => void) => {
+            DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
+            (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox);
             this.layoutDoc.presCollection = targetDoc;
-            // this still needs some fixing
-            setTimeout(resetSelection, 500);
-            if (doc !== targetDoc) {
-                setTimeout(finished ?? emptyFunction, 100); /// give it some time to create the targetDoc if we're opening up its context
-            } else {
-                finished?.();
-            }
         };
-        PresBox.NavigateToTarget(targetDoc, activeItem, openInTab, srcContext, includesDoc || tab ? undefined : resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? undefined : resetSelection);
     };
 
-    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, openInTab: any, srcContext: Doc, finished?: () => void) {
+    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) {
+        if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
+            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
+            return;
+        }
+        const restoreLayout = () => {
+            // After navigating to the document, if it is added as a presPinView then it will
+            // adjust the pan and scale to that of the pinView when it was added.
+            const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
+            if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
+                // targetDoc may or may not be displayed.  so get the first available document (or alias) view that matches targetDoc and use it
+                PresBox.restoreTargetDocView(DocumentManager.Instance.getFirstDocumentView(targetDoc), { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+            }
+        };
         // If openDocument is selected then it should open the document for the user
         if (activeItem.openDocument) {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
-        } else if (targetDoc && activeItem.presMovement !== PresMovement.None) {
-            LightboxView.SetLightboxDoc(undefined);
-            const options: DocFocusOptions = {
-                willZoom: activeItem.presMovement !== PresMovement.Pan,
-                zoomScale: NumCast(activeItem.presZoom, 1),
-                zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
-                noSelect: true,
-                originatingDoc: activeItem,
-            };
-            DocumentManager.Instance.jumpToDocument(targetDoc, options, openInTab, srcContext ? [srcContext] : [], finished);
-        } else if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
-            (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
-        }
-        // After navigating to the document, if it is added as a presPinView then it will
-        // adjust the pan and scale to that of the pinView when it was added.
-        const pinDocLayout = (BoolCast(activeItem.presPinLayout) || BoolCast(activeItem.presPinView)) && DocCast(targetDoc.context)?._currentFrame === undefined;
-        if (activeItem.presPinData || activeItem.presPinView || pinDocLayout) {
-            PresBox._navTimer && clearTimeout(PresBox._navTimer);
-            // targetDoc may or may not be displayed.  this gets the first available document (or alias) view that matches targetDoc
-            const bestTargetView = DocumentManager.Instance.getFirstDocumentView(targetDoc);
-            if (bestTargetView?.props.Document) PresBox._navTimer = PresBox.restoreTargetDocView(bestTargetView?.props.Document, { pinDocLayout }, activeItem, NumCast(activeItem.presTransition, 500));
+            setTimeout(restoreLayout);
+        } else {
+            if (targetDoc && activeItem.presMovement !== PresMovement.None) {
+                LightboxView.SetLightboxDoc(undefined);
+                const options: DocFocusOptions = {
+                    willZoom: activeItem.presMovement !== PresMovement.Pan,
+                    zoomScale: NumCast(activeItem.presZoom, 1),
+                    zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                    noSelect: true,
+                    originatingDoc: activeItem,
+                };
+
+                var containerDocContext = srcContext ? [srcContext] : [];
+                while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
+                    containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                }
+
+                DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finished);
+            }
+            restoreLayout();
         }
     }
 
@@ -1824,32 +1805,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return undefined;
     };
 
-    // Case in which the document has keyframes to navigate to next key frame
-    @action
-    nextKeyframe = (tagDoc: Doc, curDoc: Doc): void => {
-        const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
-        const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            tagDoc._currentFrame = 0;
-            // CollectionFreeFormDocumentView.setupScroll(tagDoc, 0);
-            // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.updateKeyframe(childDocs, currentFrame || 0, tagDoc);
-        tagDoc._currentFrame = Math.max(0, (currentFrame || 0) + 1);
-        tagDoc.lastFrame = Math.max(NumCast(tagDoc._currentFrame), NumCast(tagDoc.lastFrame));
-    };
-
-    @action
-    prevKeyframe = (tagDoc: Doc, actItem: Doc): void => {
-        const childDocs = DocListCast(tagDoc[Doc.LayoutFieldKey(tagDoc)]);
-        const currentFrame = Cast(tagDoc._currentFrame, 'number', null);
-        if (currentFrame === undefined) {
-            tagDoc._currentFrame = 0;
-            // CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0);
-        }
-        CollectionFreeFormDocumentView.gotoKeyframe(childDocs.slice());
-        tagDoc._currentFrame = Math.max(0, (currentFrame || 0) - 1);
-    };
+    _keyTimer: NodeJS.Timeout | undefined;
 
     /**
      * Returns the collection type as a string for headers
@@ -2022,76 +1978,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
-    @action
-    getList = (list: any): List<number> => list;
-
-    @action
-    updateList = (list: any): List<number> => {
-        const targetDoc: Doc = this.targetDoc;
-        const x: List<number> = list;
-        x[x.length - 1] = NumCast(targetDoc._scrollY);
-        return x;
-    };
-
-    @action
-    newFrame = () => {
-        const activeItem: Doc = this.activeItem;
-        const type: string = StrCast(this.targetDoc.type);
-        if (!activeItem.frameList) activeItem.frameList = new List<number>();
-        switch (type) {
-            case DocumentType.PDF || DocumentType.RTF || DocumentType.WEB:
-                this.updateList(activeItem.frameList);
-                break;
-        }
-    };
-
-    @computed get frameListHeader() {
-        return (
-            <div className="frameList-header">
-                &nbsp; Frames {this.panable ? <i>Panable</i> : this.scrollable ? <i>Scrollable</i> : null}
-                <div className={'frameList-headerButtons'}>
-                    <Tooltip title={<div className="dash-tooltip">{'Add frame by example'}</div>}>
-                        <div
-                            className={'headerButton'}
-                            onClick={e => {
-                                e.stopPropagation();
-                                this.newFrame();
-                            }}>
-                            <FontAwesomeIcon icon={'plus'} onPointerDown={e => e.stopPropagation()} />
-                        </div>
-                    </Tooltip>
-                    <Tooltip title={<div className="dash-tooltip">{'Edit in collection'}</div>}>
-                        <div className={'headerButton'} onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon icon={'edit'} onPointerDown={e => e.stopPropagation()} />
-                        </div>
-                    </Tooltip>
-                </div>
-            </div>
-        );
-    }
-
-    @computed get frameList() {
-        const frameList: List<number> = this.getList(this.activeItem.frameList);
-        return !frameList ? null : (
-            <div className="frameList-container">
-                {frameList.map(value => (
-                    <div className="framList-item" />
-                ))}
-            </div>
-        );
-    }
-
-    @computed get playButtonFrames() {
-        const targetDoc = this.targetDoc;
-        return !this.targetDoc ? null : (
-            <div className="presPanel-button-frame" style={{ display: targetDoc.lastFrame !== undefined && targetDoc.lastFrame >= 0 ? 'inline-flex' : 'none' }}>
-                <div>{NumCast(targetDoc._currentFrame)}</div>
-                <div className="presPanel-divider" style={{ border: 'solid 0.5px white', height: '60%' }}></div>
-                <div>{NumCast(targetDoc.lastFrame)}</div>
-            </div>
-        );
-    }
-
     @computed get playButtons() {
         const presEnd: boolean = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1;
         const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0;
@@ -2143,7 +2029,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 </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}
-                    {this.playButtonFrames}
                 </div>
                 <div className="presPanel-divider"></div>
                 {this.props.PanelWidth() > 250 ? (
@@ -2202,9 +2087,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         clearTimeout(this._presTimer);
     };
 
-    @action
-    startMarqueeCreateSlide = () => (PresBox.startMarquee = true);
-
     AddToMap = (treeViewDoc: Doc, index: number[]): Doc[] => {
         var indexNum = 0;
         for (let i = 0; i < index.length; i++) {
@@ -2271,7 +2153,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     </Tooltip>
                     <div className="presPanel-button-text">
                         Slide {this.itemIndex + 1} / {this.childDocs.length}
-                        {this.playButtonFrames}
                     </div>
                     <div className="presPanel-divider" />
                     <div className="presPanel-button-text" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, returnFalse, this.exitClicked, false, false)}>
-- 
cgit v1.2.3-70-g09d2


From abb815f3140ff3ded69aa6d9fd4505d6149eb29f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 5 Dec 2022 22:46:30 -0500
Subject: from last

---
 src/client/views/nodes/trails/PresBox.tsx | 1 +
 1 file changed, 1 insertion(+)

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 169f51dac..3b9a913f7 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -532,6 +532,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             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);
         });
         const createDocView = (doc: Doc, finished?: () => void) => {
-- 
cgit v1.2.3-70-g09d2


From 5d0094fb5305ed01cebd33cbee246433cd58b877 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 6 Dec 2022 17:42:26 -0500
Subject: stop playing media when switching to next slide

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

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 3b9a913f7..0a4423311 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -35,6 +35,7 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
+import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -234,6 +235,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
     // No more frames in current doc and next slide is defined, therefore move to next slide
     nextSlide = (slideNum?: number) => {
+        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
         let nextSelected = slideNum ?? this.itemIndex + 1;
         this.gotoDocument(nextSelected, this.activeItem);
         for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
-- 
cgit v1.2.3-70-g09d2


From 6ffb3b6879fbe8e12b79eb00a316b08bf1993149 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 Dec 2022 14:46:03 -0500
Subject: fixed presMovement.none so that no pan or zoom takes place.  fixed
 clicking on a presitem to stop audio playing other items.

---
 src/client/util/DocumentManager.ts                           |  2 +-
 src/client/util/Import & Export/DirectoryImportBox.tsx       |  2 +-
 src/client/util/LinkFollower.ts                              |  8 +++++---
 src/client/util/SharingManager.tsx                           |  2 +-
 src/client/views/LightboxView.tsx                            |  4 ++--
 src/client/views/MainView.tsx                                |  2 +-
 src/client/views/collections/TabDocView.tsx                  |  4 ++--
 .../collectionFreeForm/CollectionFreeFormView.tsx            |  6 +++---
 .../collections/collectionLinear/CollectionLinearView.tsx    |  2 +-
 .../collections/collectionSchema/CollectionSchemaCells.tsx   |  2 +-
 src/client/views/nodes/DocumentView.tsx                      |  3 ++-
 src/client/views/nodes/trails/PresBox.tsx                    | 12 ++++++------
 src/client/views/search/SearchBox.tsx                        |  2 +-
 13 files changed, 27 insertions(+), 24 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d2368f4f6..5b5848bf6 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -324,7 +324,7 @@ export function DocFocusOrOpen(doc: Doc, collectionDoc?: Doc) {
     const cv = collectionDoc && DocumentManager.Instance.getDocumentView(collectionDoc);
     const dv = DocumentManager.Instance.getDocumentView(doc, (cv?.ComponentView as CollectionFreeFormView)?.props.CollectionView);
     if (dv && Doc.AreProtosEqual(dv.props.Document, doc)) {
-        dv.props.focus(dv.props.Document, { willZoom: true });
+        dv.props.focus(dv.props.Document, { willPanZoom: true });
         Doc.linkFollowHighlight(dv?.props.Document, false);
     } else {
         const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 916eee4b7..7f0c8a3e8 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx	
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx	
@@ -168,7 +168,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> {
             await GooglePhotos.Export.CollectionToAlbum({ collection: importContainer });
             Doc.AddDocToList(Doc.GetProto(parent.props.Document), 'data', importContainer);
             !this.persistent && this.props.removeDocument && this.props.removeDocument(doc);
-            DocumentManager.Instance.jumpToDocument(importContainer, { willZoom: true }, undefined, []);
+            DocumentManager.Instance.jumpToDocument(importContainer, { willPanZoom: true }, undefined, []);
         }
 
         runInAction(() => {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 7615c2686..fe7fc7369 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -39,7 +39,8 @@ export class LinkFollower {
                         const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
                         if (targDocView) {
                             targDocView.props.focus(doc, {
-                                willZoom: BoolCast(sourceDoc.followLinkZoom, false),
+                                willPan: true,
+                                willPanZoom: BoolCast(sourceDoc.followLinkZoom, false),
                                 afterFocus: (didFocus: boolean) => {
                                     finished?.();
                                     res(ViewAdjustment.resetView);
@@ -57,7 +58,7 @@ export class LinkFollower {
                 createTabForTarget(false);
             } else {
                 // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
-                docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget });
+                docViewProps.focus(sourceDoc, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget });
             }
         };
         runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
@@ -100,7 +101,8 @@ export class LinkFollower {
                 const options: DocFocusOptions = {
                     playAudio: BoolCast(sourceDoc.followLinkAudio),
                     toggleTarget: BoolCast(sourceDoc.followLinkToggle),
-                    willZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
+                    willPan: true,
+                    willPanZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
                     zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500),
                     zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null),
                     effect: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffect) as PresEffect,
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 4cd45fc9c..6cbc76014 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -392,7 +392,7 @@ export class SharingManager extends React.Component<{}> {
                 onClick={() => {
                     let context: Opt<CollectionView>;
                     if (this.targetDoc && this.targetDocView && docs.length === 1 && (context = this.targetDocView.props.ContainingCollectionView)) {
-                        DocumentManager.Instance.jumpToDocument(this.targetDoc, { willZoom: true }, undefined, [context.props.Document]);
+                        DocumentManager.Instance.jumpToDocument(this.targetDoc, { willPanZoom: true }, undefined, [context.props.Document]);
                     }
                 }}
                 onPointerEnter={action(() => {
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index cf0a2fcfb..d79b696a3 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -166,7 +166,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (targetDocView && target) {
             const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.() || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
-            targetDocView.focus(target, { originalTarget: target, willZoom: true, zoomScale: 0.9 });
+            targetDocView.focus(target, { originalTarget: target, willPanZoom: true, zoomScale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
         } else {
             if (!target && LightboxView.path.length) {
@@ -209,7 +209,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (docView) {
             LightboxView._docTarget = target;
             if (!target) docView.ComponentView?.shrinkWrap?.();
-            else docView.focus(target, { willZoom: true, zoomScale: 0.9 });
+            else docView.focus(target, { willPanZoom: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 965cd560f..42e259eed 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -206,7 +206,7 @@ export class MainView extends React.Component {
         document.addEventListener('dash', (e: any) => {
             // event used by chrome plugin to tell Dash which document to focus on
             const id = FormattedTextBox.GetDocFromUrl(e.detail);
-            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, { willZoom: false }, undefined, []) : null));
+            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.jumpToDocument(doc, { willPan: false }, undefined, []) : null));
         });
         document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
         this.initEventListeners();
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index af1fb175a..a61ae680b 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -298,7 +298,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, OpenWhereMod.right);
-            setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), { willZoom: false }, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+            setTimeout(() => DocumentManager.Instance.jumpToDocument(docList.lastElement(), { willPan: true }, 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
     }
@@ -376,7 +376,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     @action
     focusFunc = (doc: Doc, options: DocFocusOptions) => {
         const shrinkwrap = options?.originalTarget === this._document && this.view?.ComponentView?.shrinkWrap;
-        if (options?.willZoom !== false && shrinkwrap && this._document) {
+        if (options?.willPanZoom !== false && shrinkwrap && this._document) {
             const focusSpeed = options.zoomTime ?? 500;
             shrinkwrap();
             this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false));
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index b8ff59a14..8a5e202e7 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1167,10 +1167,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.props.focus(doc, options);
         } else {
             const xfToCollection = options?.docTransform ?? Transform.Identity();
-            const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoom ? this.Document[this.scaleFieldKey] : undefined };
+            const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willPanZoom ? this.Document[this.scaleFieldKey] : undefined };
             const newState = HistoryUtil.getState();
             const cantTransform = (this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc;
-            const { panX, panY, scale } = cantTransform ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willZoom ? options?.zoomScale || 0.75 : undefined);
+            const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willPanZoom) ? savedState : this.calculatePanIntoView(doc, xfToCollection, options?.willPanZoom ? options?.zoomScale || 0.75 : undefined);
             if (!cantTransform) {
                 // only pan and zoom to focus on a document if the document is not an annotation in an annotation overlay collection
                 newState.initializers![this.Document[Id]] = { panX, panY };
@@ -2253,7 +2253,7 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
 export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) {
     SelectionManager.DeselectAll();
     dv.props.focus(dv.props.Document, {
-        willZoom: true,
+        willPanZoom: true,
         afterFocus: async didMove => {
             if (!didMove) {
                 const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index 924e07daa..ff263c4f3 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -150,7 +150,7 @@ export class CollectionLinearView extends CollectionSubView() {
                 <span className="bottomPopup-text">
                     Currently playing:
                     {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => (
-                        <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, { willZoom: true }, undefined, [])}>
+                        <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip, { willPanZoom: true }, undefined, [])}>
                             {clip.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '' : ',')}
                         </span>
                     ))}
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index c0dfeedfa..97a6c5c18 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
@@ -214,7 +214,7 @@ 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, { willZoom: false }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc));
+            DocumentManager.Instance.jumpToDocument(this._rowDoc, { willPan: true }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc));
         }
     };
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 778965553..81a9942c3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -95,7 +95,8 @@ export const ViewSpecPrefix = 'viewSpec'; // field prefix for anchor fields that
 
 export interface DocFocusOptions {
     originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
-    willZoom?: boolean; // determines whether to zoom in on target document
+    willPan?: boolean; // determines whether to pan to target document
+    willPanZoom?: boolean; // determines whether to zoom in on target document
     zoomScale?: number; // percent of containing frame to zoom into document
     zoomTime?: number;
     afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 0a4423311..ac8f01913 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -254,7 +254,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // Case 1: No more frames in current doc and next slide is defined, therefore move to next slide
             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.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);
@@ -266,9 +266,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     @action
     back = () => {
         const activeItem: Doc = this.activeItem;
-        const targetDoc: Doc = this.targetDoc;
         const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null);
-        const prevTargetDoc = Cast(prevItem.presentationTargetDoc, Doc, null);
         let prevSelected = this.itemIndex;
         // Functionality for group with up
         let didZoom = activeItem.presMovement;
@@ -282,7 +280,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = prevSelected;
         } else if (this.childDocs[this.itemIndex - 1] === undefined && this.layoutDoc.presLoop) {
             // Case 3: Pres loop is on so it should go to the last slide
-            this.gotoDocument(this.childDocs.length - 1, activeItem);
+            this.nextSlide(this.childDocs.length - 1);
         }
         return this.itemIndex;
     };
@@ -564,10 +562,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             LightboxView.SetLightboxDoc(targetDoc); // openInTab(targetDoc);
             setTimeout(restoreLayout);
         } else {
-            if (targetDoc && activeItem.presMovement !== PresMovement.None) {
+            if (targetDoc) {
                 LightboxView.SetLightboxDoc(undefined);
                 const options: DocFocusOptions = {
-                    willZoom: activeItem.presMovement !== PresMovement.Pan,
+                    willPan: activeItem.presMovement !== PresMovement.None,
+                    willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump,
                     zoomScale: NumCast(activeItem.presZoom, 1),
                     zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
                     noSelect: true,
@@ -857,6 +856,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //Regular click
     @action
     selectElement = async (doc: Doc) => {
+        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
         this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem);
         if (doc.presPinView || doc.presentationTargetDoc === this.layoutDoc.presCollection) setTimeout(() => this.updateCurrentPresentation(DocCast(doc.context)), 0);
         else this.updateCurrentPresentation(DocCast(doc.context));
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 9c656e093..b78c8654f 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -407,7 +407,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
      * or opening it in a new tab.
      */
     selectElement = async (doc: Doc, finishFunc: () => void) => {
-        await DocumentManager.Instance.jumpToDocument(doc, { willZoom: true }, undefined, [], finishFunc);
+        await DocumentManager.Instance.jumpToDocument(doc, { willPanZoom: true }, undefined, [], finishFunc);
     };
 
     /**
-- 
cgit v1.2.3-70-g09d2


From 7b8ca91984c41790de177e9d5275a230520cabc0 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 7 Dec 2022 18:40:51 -0500
Subject: stopped update _cache in ProxyField because it invalidates too much
 stuff during rendering.  moved some setContentViews() to constructors for
 efficiency

---
 .../collectionFreeForm/CollectionFreeFormView.tsx          |  6 +++++-
 src/client/views/nodes/AudioBox.tsx                        |  8 ++++++--
 src/client/views/nodes/ImageBox.tsx                        |  6 +++++-
 src/client/views/nodes/trails/PresBox.tsx                  | 14 +++++++-------
 src/fields/Proxy.ts                                        |  2 +-
 5 files changed, 24 insertions(+), 12 deletions(-)

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

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 8a5e202e7..50c427135 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -122,6 +122,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @observable ChildDrag: DocumentView | undefined; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
     @observable _brushedView = { width: 0, height: 0, panX: 0, panY: 0, opacity: 0 }; // highlighted region of freeform canvas used by presentations to indicate a region
 
+    constructor(props: any) {
+        super(props);
+        this.props.setContentView?.(this);
+    }
+
     @computed get views() {
         const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
         const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele);
@@ -1579,7 +1584,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     @action
     componentDidMount() {
         super.componentDidMount?.();
-        this.props.setContentView?.(this);
         this.props.setBrushViewer?.(this.brushView);
         setTimeout(
             action(() => {
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 497b4993c..d95668c89 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -70,6 +70,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @observable _muted: boolean = false;
     @observable _paused: boolean = false; // is recording paused
     // @observable rawDuration: number = 0; // computed from the length of the audio element when loaded
+
+    constructor(props: any) {
+        super(props);
+        this.props.setContentView?.(this);
+    }
+
     @computed get recordingStart() {
         return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime();
     }
@@ -111,8 +117,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     @action
     componentDidMount() {
-        this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document.  this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link.
-
         if (this.path) {
             this.mediaState = media_state.Paused;
             this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 19f5e9e29..ac953d13b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -56,6 +56,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @observable _curSuffix = '';
     @observable _uploadIcon = uploadIcons.idle;
 
+    constructor(props: any) {
+        super(props);
+        this.props.setContentView?.(this);
+    }
+
     protected createDropTarget = (ele: HTMLDivElement) => {
         this._dropDisposer?.();
         ele && (this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
@@ -94,7 +99,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     };
 
     componentDidMount() {
-        this.props.setContentView?.(this); // bcz: do not remove this.  without it, stepping into an image in the lightbox causes an infinite loop....
         this._disposers.sizer = reaction(
             () => ({
                 forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes,
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index ac8f01913..2694cb350 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -13,7 +13,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 { returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
+import { emptyPath, returnFalse, returnOne, setupMoveUpEvents, StopEvent } from '../../../../Utils';
 import { DocServer } from '../../../DocServer';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -69,7 +69,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * @param presEffectDoc presentation effects document that specifies the animation effect parameters
      * @returns a function that will wrap a JSX animation element wrapping any JSX element
      */
-    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Doc, root: Doc) {
+    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
         const effectProps = {
             left: presEffectDoc?.presEffectDirection === PresEffectDirection.Left,
             right: presEffectDoc?.presEffectDirection === PresEffectDirection.Right,
@@ -1214,7 +1214,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const isPresCollection: boolean = targetDoc === this.layoutDoc.presCollection;
         const isPinWithView: boolean = BoolCast(activeItem.presPinView);
         const presEffect = (effect: PresEffect) => (
-            <div className={`presBox-dropdownOption ${this.activeItem.presEffect === effect || (effect === PresEffect.None && !this.activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}>
+            <div className={`presBox-dropdownOption ${activeItem.presEffect === effect || (effect === PresEffect.None && !activeItem.presEffect) ? 'active' : ''}`} onPointerDown={StopEvent} onClick={() => this.updateEffect(effect)}>
                 {effect}
             </div>
         );
@@ -1224,7 +1224,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             </div>
         );
         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';
+            const color = activeItem.presEffectDirection === direction || (direction === PresEffectDirection.Center && !activeItem.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
             return (
                 <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                     <div
@@ -1241,7 +1241,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const zoom = NumCast(activeItem.presZoom, 1) * 100;
             let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
             if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
-            const effect = this.activeItem.presEffect ? this.activeItem.presEffect : PresMovement.None;
+            const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
             activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
             return (
                 <div
@@ -1376,7 +1376,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     this._openEffectDropdown = !this._openEffectDropdown;
                                 })}
                                 style={{ borderBottomLeftRadius: this._openEffectDropdown ? 0 : 5, border: this._openEffectDropdown ? `solid 2px ${Colors.MEDIUM_BLUE}` : 'solid 1px black' }}>
-                                {effect.toString()}
+                                {effect?.toString()}
                                 <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openEffectDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
                                 <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} style={{ display: this._openEffectDropdown ? 'grid' : 'none' }} onPointerDown={e => e.stopPropagation()}>
                                     {presEffect(PresEffect.None)}
@@ -2190,7 +2190,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 ScreenToLocalTransform={this.getTransform}
                                 AddToMap={this.AddToMap}
                                 RemFromMap={this.RemFromMap}
-                                hierarchyIndex={[]}
+                                hierarchyIndex={emptyPath}
                             />
                         ) : null}
                     </div>
diff --git a/src/fields/Proxy.ts b/src/fields/Proxy.ts
index 72ae13035..3aafacd96 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) {
-            setTimeout(action(() => (this.cache = cached)));
+            //setTimeout(action(() => (this.cache = cached))); // can't do this because it triggers too many invalidations while rendering.
         } else if (!this.promise) {
             this.promise = DocServer.GetRefField(this.fieldId).then(
                 action((field: any) => {
-- 
cgit v1.2.3-70-g09d2


From 52a435b09013619209b8bcc6758baeca47d5d350 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 8 Dec 2022 10:08:33 -0500
Subject: cleaned up animation effects to not reference presBox.  fixed anchors
 in text to have link properties set properly from properties view.

---
 src/client/util/DocumentManager.ts              |   2 +-
 src/client/util/LinkFollower.ts                 |   3 +-
 src/client/util/ReplayMovements.ts              | 104 +++---
 src/client/views/GlobalKeyHandler.ts            |   3 +-
 src/client/views/PropertiesView.tsx             |   2 +-
 src/client/views/animationtimeline/Keyframe.tsx | 426 +++++++++++++-----------
 src/client/views/linking/LinkMenuItem.tsx       |   5 +-
 src/client/views/nodes/DocumentView.tsx         |  46 ++-
 src/client/views/nodes/FunctionPlotBox.tsx      |   3 +-
 src/client/views/nodes/MapBox/MapBox.tsx        |   3 +-
 src/client/views/nodes/trails/PresBox.tsx       |  45 +--
 11 files changed, 359 insertions(+), 283 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 5b5848bf6..235b80cdd 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -241,7 +241,7 @@ export class DocumentManager {
         const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
         const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (focusView) {
-            !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, targetDoc); //TODO:glr make this a setting in PresBox
+            !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
                 focusView.focus(originalTarget, {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index fe7fc7369..0285803e8 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -105,8 +105,7 @@ export class LinkFollower {
                     willPanZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
                     zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500),
                     zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null),
-                    effect: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffect) as PresEffect,
-                    effectDirection: StrCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkEffectDirection) as PresEffectDirection,
+                    effect: sourceDoc,
                     originatingDoc: sourceDoc,
                 };
                 if (target.TourMap) {
diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts
index 86bc4c5de..d5bffc5e2 100644
--- a/src/client/util/ReplayMovements.ts
+++ b/src/client/util/ReplayMovements.ts
@@ -1,22 +1,24 @@
-import { CollectionFreeFormView } from "../views/collections/collectionFreeForm";
-import { IReactionDisposer, observable, observe, reaction } from "mobx";
-import { Doc } from "../../fields/Doc";
-import { VideoBox } from "../views/nodes/VideoBox";
-import { DocumentManager } from "./DocumentManager";
-import { CollectionDockingView } from "../views/collections/CollectionDockingView";
-import { DocServer } from "../DocServer";
-import { Movement, Presentation } from "./TrackMovements";
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
+import { IReactionDisposer, observable, observe, reaction } from 'mobx';
+import { Doc } from '../../fields/Doc';
+import { VideoBox } from '../views/nodes/VideoBox';
+import { DocumentManager } from './DocumentManager';
+import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { DocServer } from '../DocServer';
+import { Movement, Presentation } from './TrackMovements';
+import { OpenWhereMod } from '../views/nodes/DocumentView';
 
 export class ReplayMovements {
-    private timers: NodeJS.Timeout[] | null; 
+    private timers: NodeJS.Timeout[] | null;
     private videoBoxDisposeFunc: IReactionDisposer | null;
     private videoBox: VideoBox | null;
     private isPlaying: boolean;
 
-
     // create static instance and getter for global use
     @observable static _instance: ReplayMovements;
-    static get Instance(): ReplayMovements { return ReplayMovements._instance }
+    static get Instance(): ReplayMovements {
+        return ReplayMovements._instance;
+    }
     constructor() {
         // init the global instance
         ReplayMovements._instance = this;
@@ -37,20 +39,27 @@ export class ReplayMovements {
         }
         Doc.UserDoc().presentationMode = 'none';
 
-        this.isPlaying = false
+        this.isPlaying = false;
         // TODO: set userdoc presentMode to browsing
-        this.timers?.map(timer => clearTimeout(timer))
-    }
+        this.timers?.map(timer => clearTimeout(timer));
+    };
 
     setVideoBox = async (videoBox: VideoBox) => {
         // console.info('setVideoBox', videoBox);
-        if (this.videoBox !== null) { console.warn('setVideoBox on already videoBox'); }
-        if (this.videoBoxDisposeFunc !== null) { console.warn('setVideoBox on already videoBox dispose func'); this.videoBoxDisposeFunc(); }
-
+        if (this.videoBox !== null) {
+            console.warn('setVideoBox on already videoBox');
+        }
+        if (this.videoBoxDisposeFunc !== null) {
+            console.warn('setVideoBox on already videoBox dispose func');
+            this.videoBoxDisposeFunc();
+        }
 
         const { presentation } = videoBox;
-        if (presentation == null) { console.warn('setVideoBox on null videoBox presentation'); return; }
-        
+        if (presentation == null) {
+            console.warn('setVideoBox on null videoBox presentation');
+            return;
+        }
+
         let docIdtoDoc: Map<string, Doc> = new Map();
         try {
             docIdtoDoc = await this.loadPresentation(presentation);
@@ -59,29 +68,30 @@ export class ReplayMovements {
             throw 'error loading docs from server';
         }
 
-        
-        this.videoBoxDisposeFunc =
-            reaction(() => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
-                ({ playing, timeViewed }) => 
-                    playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements()
-            );
+        this.videoBoxDisposeFunc = reaction(
+            () => ({ playing: videoBox._playing, timeViewed: videoBox.player?.currentTime || 0 }),
+            ({ playing, timeViewed }) => (playing ? this.playMovements(presentation, docIdtoDoc, timeViewed) : this.pauseMovements())
+        );
         this.videoBox = videoBox;
-    }
+    };
 
     removeVideoBox = () => {
-        if (this.videoBoxDisposeFunc == null) { console.warn('removeVideoBox on null videoBox'); return; }
+        if (this.videoBoxDisposeFunc == null) {
+            console.warn('removeVideoBox on null videoBox');
+            return;
+        }
         this.videoBoxDisposeFunc();
 
         this.videoBox = null;
         this.videoBoxDisposeFunc = null;
-    }
+    };
 
     // should be called from interacting with the screen
     pauseFromInteraction = () => {
         this.videoBox?.Pause();
 
         this.pauseMovements();
-    }
+    };
 
     loadPresentation = async (presentation: Presentation) => {
         const { movements } = presentation;
@@ -91,7 +101,7 @@ export class ReplayMovements {
 
         // generate a set of all unique docIds
         const docIds = new Set<string>();
-        for (const {docId} of movements) {
+        for (const { docId } of movements) {
             if (!docIds.has(docId)) docIds.add(docId);
         }
 
@@ -107,27 +117,29 @@ export class ReplayMovements {
         // console.info('loadPresentation refFields', refFields, docIdtoDoc);
 
         return docIdtoDoc;
-    }
+    };
 
     // returns undefined if the docView isn't open on the screen
     getCollectionFFView = (docId: string) => {
         const isInView = DocumentManager.Instance.getDocumentViewById(docId);
-        if (isInView) { return isInView.ComponentView as CollectionFreeFormView; }
-    }
+        if (isInView) {
+            return isInView.ComponentView as CollectionFreeFormView;
+        }
+    };
 
     // will open the doc in a tab then return the CollectionFFView that holds it
     openTab = (docId: string, docIdtoDoc: Map<string, Doc>) => {
         const doc = docIdtoDoc.get(docId);
         if (doc == undefined) {
-            console.error(`docIdtoDoc did not contain docId ${docId}`)
+            console.error(`docIdtoDoc did not contain docId ${docId}`);
             return undefined;
         }
         // console.log('openTab', docId, doc);
-        CollectionDockingView.AddSplit(doc, 'right');
+        CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
         const docView = DocumentManager.Instance.getDocumentView(doc);
         // BUG - this returns undefined if the doc is already open
         return docView?.ComponentView as CollectionFreeFormView;
-    }
+    };
 
     // helper to replay a movement
     zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => {
@@ -135,7 +147,7 @@ export class ReplayMovements {
         scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0);
         document.Document._panX = panX;
         document.Document._panY = panY;
-    }
+    };
 
     getFirstMovements = (movements: Movement[]): Map<string, Movement> => {
         if (movements === null) return new Map();
@@ -146,18 +158,19 @@ export class ReplayMovements {
             if (!docIdtoFirstMove.has(docId)) docIdtoFirstMove.set(docId, move);
         }
         return docIdtoFirstMove;
-    }
+    };
 
     endPlayingPresentation = () => {
         this.isPlaying = false;
         Doc.UserDoc().presentationMode = 'none';
-    }
+    };
 
     public playMovements = (presentation: Presentation, docIdtoDoc: Map<string, Doc>, timeViewed: number = 0) => {
         // console.info('playMovements', presentation, timeViewed, docIdtoDoc);
 
-        if (presentation.movements === null || presentation.movements.length === 0) { //|| this.playFFView === null) {
-            return new Error('[recordingApi.ts] followMovements() failed: no presentation data')
+        if (presentation.movements === null || presentation.movements.length === 0) {
+            //|| this.playFFView === null) {
+            return new Error('[recordingApi.ts] followMovements() failed: no presentation data');
         }
         if (this.isPlaying) return;
 
@@ -165,7 +178,7 @@ export class ReplayMovements {
         Doc.UserDoc().presentationMode = 'watching';
 
         // only get the movements that are remaining in the video time left
-        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000)
+        const filteredMovements = presentation.movements.filter(movement => movement.time > timeViewed * 1000);
 
         const handleFirstMovements = () => {
             // if the first movement is a closed tab, open it
@@ -179,13 +192,12 @@ export class ReplayMovements {
                 const colFFView = this.getCollectionFFView(docId);
                 if (colFFView) this.zoomAndPan(firstMove, colFFView);
             }
-        }
+        };
         handleFirstMovements();
-        
 
         // make timers that will execute each movement at the correct replay time
         this.timers = filteredMovements.map(movement => {
-            const timeDiff = movement.time - timeViewed * 1000
+            const timeDiff = movement.time - timeViewed * 1000;
 
             return setTimeout(() => {
                 const collectionFFView = this.getCollectionFFView(movement.docId);
@@ -204,5 +216,5 @@ export class ReplayMovements {
                 }
             }, timeDiff);
         });
-    }
+    };
 }
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 4890d9624..5e700e281 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -26,6 +26,7 @@ import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
 import { MainView } from './MainView';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
+import { OpenWhereMod } from './nodes/DocumentView';
 import { AnchorMenu } from './pdf/AnchorMenu';
 
 const modifiers = ['control', 'meta', 'shift', 'alt'];
@@ -225,7 +226,7 @@ export class KeyManager {
                 if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
                     return { stopPropagation: false, preventDefault: false };
                 }
-                MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, 'right');
+                MainView.Instance.mainFreeform && CollectionDockingView.AddSplit(MainView.Instance.mainFreeform, OpenWhereMod.right);
                 break;
             case 'arrowleft':
                 if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 905f9e2d0..e43b160b8 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -306,7 +306,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     }
 
     @computed get links() {
-        const selAnchor = this.selectedDocumentView?.anchorViewDoc;
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
         return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />;
     }
 
diff --git a/src/client/views/animationtimeline/Keyframe.tsx b/src/client/views/animationtimeline/Keyframe.tsx
index 92d3e2bed..21a5af83f 100644
--- a/src/client/views/animationtimeline/Keyframe.tsx
+++ b/src/client/views/animationtimeline/Keyframe.tsx
@@ -1,38 +1,37 @@
-import { action, computed, observable, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import * as React from "react";
-import { Doc, DocListCast, Opt } from "../../../fields/Doc";
-import { List } from "../../../fields/List";
-import { createSchema, defaultSpec, listSpec, makeInterface } from "../../../fields/Schema";
-import { Cast, NumCast } from "../../../fields/Types";
-import { Docs } from "../../documents/Documents";
-import { Transform } from "../../util/Transform";
-import { CollectionDockingView } from "../collections/CollectionDockingView";
-import "../global/globalCssVariables.scss";
-import "./Keyframe.scss";
-import "./Timeline.scss";
-import { TimelineMenu } from "./TimelineMenu";
-
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { Doc, DocListCast, Opt } from '../../../fields/Doc';
+import { List } from '../../../fields/List';
+import { createSchema, defaultSpec, listSpec, makeInterface } from '../../../fields/Schema';
+import { Cast, NumCast } from '../../../fields/Types';
+import { Docs } from '../../documents/Documents';
+import { Transform } from '../../util/Transform';
+import { CollectionDockingView } from '../collections/CollectionDockingView';
+import '../global/globalCssVariables.scss';
+import { OpenWhereMod } from '../nodes/DocumentView';
+import './Keyframe.scss';
+import './Timeline.scss';
+import { TimelineMenu } from './TimelineMenu';
 
 /**
- * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also 
+ * Useful static functions that you can use. Mostly for logic, but you can also add UI logic here also
  */
 export namespace KeyframeFunc {
-
     export enum KeyframeType {
-        end = "end",
-        fade = "fade",
-        default = "default",
+        end = 'end',
+        fade = 'fade',
+        default = 'default',
     }
 
     export enum Direction {
-        left = "left",
-        right = "right"
+        left = 'left',
+        right = 'right',
     }
 
-    export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): (RegionData | undefined) => {
-        let leftMost: (RegionData | undefined) = undefined;
-        let rightMost: (RegionData | undefined) = undefined;
+    export const findAdjacentRegion = (dir: KeyframeFunc.Direction, currentRegion: Doc, regions: Doc[]): RegionData | undefined => {
+        let leftMost: RegionData | undefined = undefined;
+        let rightMost: RegionData | undefined = undefined;
         regions.forEach(region => {
             const neighbor = RegionData(region);
             if (currentRegion.position! > neighbor.position) {
@@ -52,11 +51,12 @@ export namespace KeyframeFunc {
         }
     };
 
-    export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closet keyframe to the left
+    export const calcMinLeft = (region: Doc, currentBarX: number, ref?: Doc) => {
+        //returns the time of the closet keyframe to the left
         let leftKf: Opt<Doc>;
         let time: number = 0;
         const keyframes = DocListCast(region.keyframes!);
-        keyframes.map((kf) => {
+        keyframes.map(kf => {
             let compTime = currentBarX;
             if (ref) compTime = NumCast(ref.time);
             if (NumCast(kf.time) < compTime && NumCast(kf.time) >= time) {
@@ -67,11 +67,11 @@ export namespace KeyframeFunc {
         return leftKf;
     };
 
-
-    export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => { //returns the time of the closest keyframe to the right 
+    export const calcMinRight = (region: Doc, currentBarX: number, ref?: Doc) => {
+        //returns the time of the closest keyframe to the right
         let rightKf: Opt<Doc>;
         let time: number = Infinity;
-        DocListCast(region.keyframes!).forEach((kf) => {
+        DocListCast(region.keyframes!).forEach(kf => {
             let compTime = currentBarX;
             if (ref) compTime = NumCast(ref.time);
             if (NumCast(kf.time) > compTime && NumCast(kf.time) <= NumCast(time)) {
@@ -93,27 +93,31 @@ export namespace KeyframeFunc {
         return regiondata;
     };
 
-
-    export const convertPixelTime = (pos: number, unit: "mili" | "sec" | "min" | "hr", dir: "pixel" | "time", tickSpacing: number, tickIncrement: number) => {
-        const time = dir === "pixel" ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
+    export const convertPixelTime = (pos: number, unit: 'mili' | 'sec' | 'min' | 'hr', dir: 'pixel' | 'time', tickSpacing: number, tickIncrement: number) => {
+        const time = dir === 'pixel' ? (pos * tickSpacing) / tickIncrement : (pos / tickSpacing) * tickIncrement;
         switch (unit) {
-            case "mili": return time;
-            case "sec": return dir === "pixel" ? time / 1000 : time * 1000;
-            case "min": return dir === "pixel" ? time / 60000 : time * 60000;
-            case "hr": return dir === "pixel" ? time / 3600000 : time * 3600000;
-            default: return time;
+            case 'mili':
+                return time;
+            case 'sec':
+                return dir === 'pixel' ? time / 1000 : time * 1000;
+            case 'min':
+                return dir === 'pixel' ? time / 60000 : time * 60000;
+            case 'hr':
+                return dir === 'pixel' ? time / 3600000 : time * 3600000;
+            default:
+                return time;
         }
     };
 }
 
 export const RegionDataSchema = createSchema({
-    position: defaultSpec("number", 0),
-    duration: defaultSpec("number", 0),
+    position: defaultSpec('number', 0),
+    duration: defaultSpec('number', 0),
     keyframes: listSpec(Doc),
-    fadeIn: defaultSpec("number", 0),
-    fadeOut: defaultSpec("number", 0),
+    fadeIn: defaultSpec('number', 0),
+    fadeOut: defaultSpec('number', 0),
     functions: listSpec(Doc),
-    hasData: defaultSpec("boolean", false)
+    hasData: defaultSpec('boolean', false),
 });
 export type RegionData = makeInterface<[typeof RegionDataSchema]>;
 export const RegionData = makeInterface(RegionDataSchema);
@@ -130,50 +134,63 @@ interface IProps {
     makeKeyData: (region: RegionData, pos: number, kftype: KeyframeFunc.KeyframeType) => Doc;
 }
 
-
 /**
- * 
+ *
  * This class handles the green region stuff
  * Key facts:
- * 
+ *
  * Structure looks like this
- * 
+ *
  * region as a whole
  *  <------------------------------REGION------------------------------->
- * 
- * region broken down 
- * 
+ *
+ * region broken down
+ *
  *  <|---------|############ MAIN CONTENT #################|-----------|>      .....followed by void.........
  *  (start)                                             (Fade 2)
  *            (fade 1)                                              (finish)
- * 
- * 
- * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked. 
- * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.  
- * If you want to know the exact location of the computer, message me. 
- * 
- * @author Andrew Kim 
+ *
+ *
+ * As you can see, this is different from After Effect and Premiere Pro, but this is how TAG worked.
+ * If you want to checkout TAG, it's in the lockers, and the password is the usual lab door password. It's the blue laptop.
+ * If you want to know the exact location of the computer, message me.
+ *
+ * @author Andrew Kim
  */
 @observer
 export class Keyframe extends React.Component<IProps> {
-
     @observable private _bar = React.createRef<HTMLDivElement>();
     @observable private _mouseToggled = false;
     @observable private _doubleClickEnabled = false;
 
-    @computed private get regiondata() { return RegionData(this.props.RegionData); }
-    @computed private get regions() { return DocListCast(this.props.node.regions); }
-    @computed private get keyframes() { return DocListCast(this.regiondata.keyframes); }
-    @computed private get pixelPosition() { return KeyframeFunc.convertPixelTime(this.regiondata.position, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
-    @computed private get pixelDuration() { return KeyframeFunc.convertPixelTime(this.regiondata.duration, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
-    @computed private get pixelFadeIn() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
-    @computed private get pixelFadeOut() { return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement); }
+    @computed private get regiondata() {
+        return RegionData(this.props.RegionData);
+    }
+    @computed private get regions() {
+        return DocListCast(this.props.node.regions);
+    }
+    @computed private get keyframes() {
+        return DocListCast(this.regiondata.keyframes);
+    }
+    @computed private get pixelPosition() {
+        return KeyframeFunc.convertPixelTime(this.regiondata.position, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+    }
+    @computed private get pixelDuration() {
+        return KeyframeFunc.convertPixelTime(this.regiondata.duration, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+    }
+    @computed private get pixelFadeIn() {
+        return KeyframeFunc.convertPixelTime(this.regiondata.fadeIn, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+    }
+    @computed private get pixelFadeOut() {
+        return KeyframeFunc.convertPixelTime(this.regiondata.fadeOut, 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+    }
 
     constructor(props: any) {
         super(props);
     }
     componentDidMount() {
-        setTimeout(() => {      //giving it a temporary 1sec delay... 
+        setTimeout(() => {
+            //giving it a temporary 1sec delay...
             if (!this.regiondata.keyframes) this.regiondata.keyframes = new List<Doc>();
             const start = this.props.makeKeyData(this.regiondata, this.regiondata.position, KeyframeFunc.KeyframeType.end);
             const fadeIn = this.props.makeKeyData(this.regiondata, this.regiondata.position + this.regiondata.fadeIn, KeyframeFunc.KeyframeType.fade);
@@ -202,12 +219,12 @@ export class Keyframe extends React.Component<IProps> {
                 this._doubleClickEnabled = false;
             }, 200);
             this._doubleClickEnabled = true;
-            document.addEventListener("pointermove", this.onBarPointerMove);
-            document.addEventListener("pointerup", (e: PointerEvent) => {
-                document.removeEventListener("pointermove", this.onBarPointerMove);
+            document.addEventListener('pointermove', this.onBarPointerMove);
+            document.addEventListener('pointerup', (e: PointerEvent) => {
+                document.removeEventListener('pointermove', this.onBarPointerMove);
             });
         }
-    }
+    };
 
     @action
     onBarPointerMove = (e: PointerEvent) => {
@@ -219,46 +236,46 @@ export class Keyframe extends React.Component<IProps> {
         const left = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions)!;
         const right = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions)!;
         const prevX = this.regiondata.position;
-        const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+        const futureX = this.regiondata.position + KeyframeFunc.convertPixelTime(e.movementX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
         if (futureX <= 0) {
             this.regiondata.position = 0;
-        } else if ((left && left.position + left.duration >= futureX)) {
+        } else if (left && left.position + left.duration >= futureX) {
             this.regiondata.position = left.position + left.duration;
-        } else if ((right && right.position <= futureX + this.regiondata.duration)) {
+        } else if (right && right.position <= futureX + this.regiondata.duration) {
             this.regiondata.position = right.position - this.regiondata.duration;
         } else {
             this.regiondata.position = futureX;
         }
         const movement = this.regiondata.position - prevX;
-        this.keyframes.forEach(kf => kf.time = NumCast(kf.time) + movement);
-    }
+        this.keyframes.forEach(kf => (kf.time = NumCast(kf.time) + movement));
+    };
 
     @action
     onResizeLeft = (e: React.PointerEvent) => {
         e.preventDefault();
         e.stopPropagation();
-        document.addEventListener("pointermove", this.onDragResizeLeft);
-        document.addEventListener("pointerup", () => {
-            document.removeEventListener("pointermove", this.onDragResizeLeft);
+        document.addEventListener('pointermove', this.onDragResizeLeft);
+        document.addEventListener('pointerup', () => {
+            document.removeEventListener('pointermove', this.onDragResizeLeft);
         });
-    }
+    };
 
     @action
     onResizeRight = (e: React.PointerEvent) => {
         e.preventDefault();
         e.stopPropagation();
-        document.addEventListener("pointermove", this.onDragResizeRight);
-        document.addEventListener("pointerup", () => {
-            document.removeEventListener("pointermove", this.onDragResizeRight);
+        document.addEventListener('pointermove', this.onDragResizeRight);
+        document.addEventListener('pointerup', () => {
+            document.removeEventListener('pointermove', this.onDragResizeRight);
         });
-    }
+    };
 
     @action
     onDragResizeLeft = (e: PointerEvent) => {
         e.preventDefault();
         e.stopPropagation();
         const bar = this._bar.current!;
-        const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+        const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
         const leftRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.left, this.regiondata, this.regions);
         if (leftRegion && this.regiondata.position + offset <= leftRegion.position + leftRegion.duration) {
             this.regiondata.position = leftRegion.position + leftRegion.duration;
@@ -275,90 +292,99 @@ export class Keyframe extends React.Component<IProps> {
         }
         this.keyframes[0].time = this.regiondata.position;
         this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
-    }
-
+    };
 
     @action
     onDragResizeRight = (e: PointerEvent) => {
         e.preventDefault();
         e.stopPropagation();
         const bar = this._bar.current!;
-        const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
+        const offset = KeyframeFunc.convertPixelTime(Math.round((e.clientX - bar.getBoundingClientRect().right) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
         const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, this.regiondata, this.regions);
         const fadeOutKeyframeTime = NumCast(this.keyframes[this.keyframes.length - 3].time);
-        if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) { //case 1: when third to last keyframe is in the way 
+        if (this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut + offset <= fadeOutKeyframeTime) {
+            //case 1: when third to last keyframe is in the way
             this.regiondata.duration = fadeOutKeyframeTime - this.regiondata.position + this.regiondata.fadeOut;
-        } else if (rightRegion && (this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position)) {
+        } else if (rightRegion && this.regiondata.position + this.regiondata.duration + offset >= rightRegion.position) {
             this.regiondata.duration = rightRegion.position - this.regiondata.position;
         } else {
             this.regiondata.duration += offset;
         }
         this.keyframes[this.keyframes.length - 2].time = this.regiondata.position + this.regiondata.duration - this.regiondata.fadeOut;
         this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
-    }
-
+    };
 
     @action
     createKeyframe = async (clientX: number) => {
         this._mouseToggled = true;
         const bar = this._bar.current!;
-        const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), "mili", "time", this.props.tickSpacing, this.props.tickIncrement);
-        if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) { //make sure keyframe is not created inbetween fades and ends
+        const offset = KeyframeFunc.convertPixelTime(Math.round((clientX - bar.getBoundingClientRect().left) * this.props.transform.Scale), 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement);
+        if (offset > this.regiondata.fadeIn && offset < this.regiondata.duration - this.regiondata.fadeOut) {
+            //make sure keyframe is not created inbetween fades and ends
             const position = this.regiondata.position;
             this.props.makeKeyData(this.regiondata, Math.round(position + offset), KeyframeFunc.KeyframeType.default);
             this.regiondata.hasData = true;
-            this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
-
+            this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(Math.round(position + offset), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement)); //first move the keyframe to the correct location and make a copy so the correct file gets coppied
         }
-    }
-
+    };
 
     @action
     moveKeyframe = async (e: React.MouseEvent, kf: Doc) => {
         e.preventDefault();
         e.stopPropagation();
-        this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement));
-    }
+        this.props.changeCurrentBarX(KeyframeFunc.convertPixelTime(NumCast(kf.time!), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement));
+    };
 
     /**
      * custom keyframe context menu items (when clicking on the keyframe circle)
      */
     @action
     makeKeyframeMenu = (kf: Doc, e: MouseEvent) => {
-        TimelineMenu.Instance.addItem("button", "Toggle Fade Only", () => {
+        TimelineMenu.Instance.addItem('button', 'Toggle Fade Only', () => {
             kf.type = kf.type === KeyframeFunc.KeyframeType.fade ? KeyframeFunc.KeyframeType.default : KeyframeFunc.KeyframeType.fade;
         }),
-            TimelineMenu.Instance.addItem("button", "Show Data", action(() => {
-                const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
-                CollectionDockingView.AddSplit(kvp, "right");
-            })),
-            TimelineMenu.Instance.addItem("button", "Delete", action(() => {
-                (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
-                this.forceUpdate();
-            })),
-            TimelineMenu.Instance.addItem("input", "Move", action((val) => {
-                let cannotMove: boolean = false;
-                const kfIndex: number = this.keyframes.indexOf(kf);
-                if (val < 0 || (val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time))) {
-                    cannotMove = true;
-                }
-                if (!cannotMove) {
-                    this.keyframes[kfIndex].time = parseInt(val, 10);
-                    this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
-                }
-            }));
-        TimelineMenu.Instance.addMenu("Keyframe");
+            TimelineMenu.Instance.addItem(
+                'button',
+                'Show Data',
+                action(() => {
+                    const kvp = Docs.Create.KVPDocument(kf, { _width: 300, _height: 300 });
+                    CollectionDockingView.AddSplit(kvp, OpenWhereMod.right);
+                })
+            ),
+            TimelineMenu.Instance.addItem(
+                'button',
+                'Delete',
+                action(() => {
+                    (this.regiondata.keyframes as List<Doc>).splice(this.keyframes.indexOf(kf), 1);
+                    this.forceUpdate();
+                })
+            ),
+            TimelineMenu.Instance.addItem(
+                'input',
+                'Move',
+                action(val => {
+                    let cannotMove: boolean = false;
+                    const kfIndex: number = this.keyframes.indexOf(kf);
+                    if (val < 0 || val < NumCast(this.keyframes[kfIndex - 1].time) || val > NumCast(this.keyframes[kfIndex + 1].time)) {
+                        cannotMove = true;
+                    }
+                    if (!cannotMove) {
+                        this.keyframes[kfIndex].time = parseInt(val, 10);
+                        this.keyframes[1].time = this.regiondata.position + this.regiondata.fadeIn;
+                    }
+                })
+            );
+        TimelineMenu.Instance.addMenu('Keyframe');
         TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
-    }
+    };
 
     /**
-     * context menu for region (anywhere on the green region). 
+     * context menu for region (anywhere on the green region).
      */
     @action
     makeRegionMenu = (kf: Doc, e: MouseEvent) => {
-        TimelineMenu.Instance.addItem("button", "Remove Region", () =>
-            Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
-            TimelineMenu.Instance.addItem("input", `fadeIn: ${this.regiondata.fadeIn}ms`, (val) => {
+        TimelineMenu.Instance.addItem('button', 'Remove Region', () => Cast(this.props.node.regions, listSpec(Doc))?.splice(this.regions.indexOf(this.props.RegionData), 1)),
+            TimelineMenu.Instance.addItem('input', `fadeIn: ${this.regiondata.fadeIn}ms`, val => {
                 runInAction(() => {
                     let cannotMove: boolean = false;
                     if (val < 0 || val > NumCast(this.keyframes[2].time) - this.regiondata.position) {
@@ -370,7 +396,7 @@ export class Keyframe extends React.Component<IProps> {
                     }
                 });
             }),
-            TimelineMenu.Instance.addItem("input", `fadeOut: ${this.regiondata.fadeOut}ms`, (val) => {
+            TimelineMenu.Instance.addItem('input', `fadeOut: ${this.regiondata.fadeOut}ms`, val => {
                 runInAction(() => {
                     let cannotMove: boolean = false;
                     if (val < 0 || val > this.regiondata.position + this.regiondata.duration - NumCast(this.keyframes[this.keyframes.length - 3].time)) {
@@ -382,34 +408,38 @@ export class Keyframe extends React.Component<IProps> {
                     }
                 });
             }),
-            TimelineMenu.Instance.addItem("input", `position: ${this.regiondata.position}ms`, (val) => {
+            TimelineMenu.Instance.addItem('input', `position: ${this.regiondata.position}ms`, val => {
                 runInAction(() => {
                     const prevPosition = this.regiondata.position;
                     let cannotMove: boolean = false;
-                    this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
-                        if (pos !== this.regiondata.position) {
-                            if ((val < 0) || (val > pos && val < pos + dur || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur))) {
-                                cannotMove = true;
+                    this.regions
+                        .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) }))
+                        .forEach(({ pos, dur }) => {
+                            if (pos !== this.regiondata.position) {
+                                if (val < 0 || (val > pos && val < pos + dur) || (this.regiondata.duration + val > pos && this.regiondata.duration + val < pos + dur)) {
+                                    cannotMove = true;
+                                }
                             }
-                        }
-                    });
+                        });
                     if (!cannotMove) {
                         this.regiondata.position = parseInt(val, 10);
                         this.updateKeyframes(this.regiondata.position - prevPosition);
                     }
                 });
             }),
-            TimelineMenu.Instance.addItem("input", `duration: ${this.regiondata.duration}ms`, (val) => {
+            TimelineMenu.Instance.addItem('input', `duration: ${this.regiondata.duration}ms`, val => {
                 runInAction(() => {
                     let cannotMove: boolean = false;
-                    this.regions.map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) })).forEach(({ pos, dur }) => {
-                        if (pos !== this.regiondata.position) {
-                            val += this.regiondata.position;
-                            if ((val < 0) || (val > pos && val < pos + dur)) {
-                                cannotMove = true;
+                    this.regions
+                        .map(region => ({ pos: NumCast(region.position), dur: NumCast(region.duration) }))
+                        .forEach(({ pos, dur }) => {
+                            if (pos !== this.regiondata.position) {
+                                val += this.regiondata.position;
+                                if (val < 0 || (val > pos && val < pos + dur)) {
+                                    cannotMove = true;
+                                }
                             }
-                        }
-                    });
+                        });
                     if (!cannotMove) {
                         this.regiondata.duration = parseInt(val, 10);
                         this.keyframes[this.keyframes.length - 1].time = this.regiondata.position + this.regiondata.duration;
@@ -417,9 +447,9 @@ export class Keyframe extends React.Component<IProps> {
                     }
                 });
             }),
-            TimelineMenu.Instance.addMenu("Region");
+            TimelineMenu.Instance.addMenu('Region');
         TimelineMenu.Instance.openMenu(e.clientX, e.clientY);
-    }
+    };
 
     @action
     updateKeyframes = (incr: number, filter: number[] = []) => {
@@ -428,7 +458,7 @@ export class Keyframe extends React.Component<IProps> {
                 kf.time = NumCast(kf.time) + incr;
             }
         });
-    }
+    };
 
     /**
      * hovering effect when hovered (hidden div darkens)
@@ -438,9 +468,9 @@ export class Keyframe extends React.Component<IProps> {
         e.preventDefault();
         e.stopPropagation();
         const div = ref.current!;
-        div.style.opacity = "1";
+        div.style.opacity = '1';
         Doc.BrushDoc(this.props.node);
-    }
+    };
 
     /**
      * hovering effect when hovered out (hidden div becomes invisible)
@@ -450,14 +480,12 @@ export class Keyframe extends React.Component<IProps> {
         e.preventDefault();
         e.stopPropagation();
         const div = ref.current!;
-        div.style.opacity = "0";
+        div.style.opacity = '0';
         Doc.UnBrushDoc(this.props.node);
-    }
-
+    };
 
     ///////////////////////UI STUFF /////////////////////////
 
-
     /**
      * drawing keyframe. Handles both keyframe with a circle (one that you create by double clicking) and one without circle (fades)
      * this probably needs biggest change, since everyone expected all keyframes to have a circle (and draggable)
@@ -465,32 +493,43 @@ export class Keyframe extends React.Component<IProps> {
     drawKeyframes = () => {
         const keyframeDivs: JSX.Element[] = [];
         return DocListCast(this.regiondata.keyframes).map(kf => {
-            if (kf.type as KeyframeFunc.KeyframeType !== KeyframeFunc.KeyframeType.end) {
-                return <>
-                    <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
-                        <div className="divider"></div>
-                        <div className="keyframeCircle keyframe-indicator"
-                            onPointerDown={(e) => { e.preventDefault(); e.stopPropagation(); this.moveKeyframe(e, kf); }}
-                            onContextMenu={(e: React.MouseEvent) => {
-                                e.preventDefault();
-                                e.stopPropagation();
-                                this.makeKeyframeMenu(kf, e.nativeEvent);
-                            }}
-                            onDoubleClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>
+            if ((kf.type as KeyframeFunc.KeyframeType) !== KeyframeFunc.KeyframeType.end) {
+                return (
+                    <>
+                        <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+                            <div className="divider"></div>
+                            <div
+                                className="keyframeCircle keyframe-indicator"
+                                onPointerDown={e => {
+                                    e.preventDefault();
+                                    e.stopPropagation();
+                                    this.moveKeyframe(e, kf);
+                                }}
+                                onContextMenu={(e: React.MouseEvent) => {
+                                    e.preventDefault();
+                                    e.stopPropagation();
+                                    this.makeKeyframeMenu(kf, e.nativeEvent);
+                                }}
+                                onDoubleClick={e => {
+                                    e.preventDefault();
+                                    e.stopPropagation();
+                                }}></div>
                         </div>
-                    </div>
-                    <div className="keyframe-information" />
-                </>;
+                        <div className="keyframe-information" />
+                    </>
+                );
             } else {
-                return <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
-                    <div className="divider" />
-                </div>;
+                return (
+                    <div className="keyframe" style={{ left: `${KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement) - this.pixelPosition}px` }}>
+                        <div className="divider" />
+                    </div>
+                );
             }
         });
-    }
+    };
 
     /**
-     * drawing the hidden divs that partition different intervals within a region. 
+     * drawing the hidden divs that partition different intervals within a region.
      */
     @action
     drawKeyframeDividers = () => {
@@ -500,26 +539,36 @@ export class Keyframe extends React.Component<IProps> {
             if (index !== this.keyframes.length - 1) {
                 const right = this.keyframes[index + 1];
                 const bodyRef = React.createRef<HTMLDivElement>();
-                const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
-                const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), "mili", "pixel", this.props.tickSpacing, this.props.tickIncrement);
+                const kfPos = KeyframeFunc.convertPixelTime(NumCast(kf.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
+                const rightPos = KeyframeFunc.convertPixelTime(NumCast(right.time), 'mili', 'pixel', this.props.tickSpacing, this.props.tickIncrement);
                 keyframeDividers.push(
-                    <div ref={bodyRef} className="body-container" style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
-                        onPointerOver={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOver(e, bodyRef); }}
-                        onPointerOut={(e) => { e.preventDefault(); e.stopPropagation(); this.onContainerOut(e, bodyRef); }}
-                        onContextMenu={(e) => {
+                    <div
+                        ref={bodyRef}
+                        className="body-container"
+                        style={{ left: `${kfPos - this.pixelPosition}px`, width: `${rightPos - kfPos}px` }}
+                        onPointerOver={e => {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            this.onContainerOver(e, bodyRef);
+                        }}
+                        onPointerOut={e => {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            this.onContainerOut(e, bodyRef);
+                        }}
+                        onContextMenu={e => {
                             e.preventDefault();
                             e.stopPropagation();
                             if (index !== 0 || index !== this.keyframes.length - 2) {
                                 this._mouseToggled = true;
                             }
                             this.makeRegionMenu(kf, e.nativeEvent);
-                        }}>
-                    </div>
+                        }}></div>
                 );
             }
         });
         return keyframeDividers;
-    }
+    };
 
     /**
      * rendering that green region
@@ -527,13 +576,18 @@ export class Keyframe extends React.Component<IProps> {
     //154, 206, 223
     render() {
         return (
-            <div className="bar" ref={this._bar} style={{
-                transform: `translate(${this.pixelPosition}px)`,
-                width: `${this.pixelDuration}px`,
-                background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${this.pixelFadeIn / this.pixelDuration * 100}%, rgba(154, 206, 223, 1) ${(this.pixelDuration - this.pixelFadeOut) / this.pixelDuration * 100}%, rgba(154, 206, 223, 0) 100% )`
-            }}
+            <div
+                className="bar"
+                ref={this._bar}
+                style={{
+                    transform: `translate(${this.pixelPosition}px)`,
+                    width: `${this.pixelDuration}px`,
+                    background: `linear-gradient(90deg, rgba(154, 206, 223, 0) 0%, rgba(154, 206, 223, 1) ${(this.pixelFadeIn / this.pixelDuration) * 100}%, rgba(154, 206, 223, 1) ${
+                        ((this.pixelDuration - this.pixelFadeOut) / this.pixelDuration) * 100
+                    }%, rgba(154, 206, 223, 0) 100% )`,
+                }}
                 onPointerDown={this.onBarPointerDown}>
-                <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft} ></div>
+                <div className="leftResize keyframe-indicator" onPointerDown={this.onResizeLeft}></div>
                 {/* <div className="keyframe-information"></div> */}
                 <div className="rightResize keyframe-indicator" onPointerDown={this.onResizeRight}></div>
                 {/* <div className="keyframe-information"></div> */}
@@ -542,4 +596,4 @@ export class Keyframe extends React.Component<IProps> {
             </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index fb4c6873e..e5a3200ca 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -132,7 +132,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                             ? Cast(this.props.linkDoc.anchor12, Doc, null)
                             : undefined;
 
-                    if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, true);
+                    if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, { instant: true });
                     LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false);
                 }
             }
@@ -146,9 +146,6 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
 
         const title = StrCast(this.props.destinationDoc.title).length > 18 ? StrCast(this.props.destinationDoc.title).substr(0, 14) + '...' : this.props.destinationDoc.title;
 
-        //  ...
-        // from anika to bob: here's where the text that is specifically linked would show up (linkDoc.storedText)
-        // ...
         const source =
             this.props.sourceDoc.type === DocumentType.RTF
                 ? this.props.linkDoc.storedText
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 81a9942c3..c9fbe7a98 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1,8 +1,9 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
-import { observer, renderReporter } from 'mobx-react';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
 import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
@@ -51,9 +52,9 @@ import { LinkAnchorBox } from './LinkAnchorBox';
 import { LinkDocPreview } from './LinkDocPreview';
 import { RadialMenu } from './RadialMenu';
 import { ScriptingBox } from './ScriptingBox';
-import { PinProps, PresBox } from './trails/PresBox';
-import React = require('react');
 import { PresEffect, PresEffectDirection } from './trails';
+import { PinProps } from './trails/PresBox';
+import React = require('react');
 const { Howl } = require('howler');
 
 interface Window {
@@ -102,8 +103,7 @@ export interface DocFocusOptions {
     afterFocus?: DocAfterFocusFunc; // function to call after focusing on a document
     docTransform?: Transform; // when a document can't be panned and zoomed within its own container (say a group), then we need to continue to move up the render hierarchy to find something that can pan and zoom.  when this happens the docTransform must accumulate all the transforms of each level of the hierarchy
     instant?: boolean; // whether focus should happen instantly (as opposed to smooth zoom)
-    effect?: PresEffect; // animation effect for focus
-    effectDirection?: PresEffectDirection;
+    effect?: Doc; // animation effect for focus
     noSelect?: boolean; // whether target should be selected after focusing
     playAudio?: boolean; // whether to play audio annotation on focus
     toggleTarget?: boolean; // whether to toggle target on and off
@@ -1428,6 +1428,36 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             )
         );
     };
+
+    /**
+     * returns an entrance animation effect function to wrap a JSX element
+     * @param presEffectDoc presentation effects document that specifies the animation effect parameters
+     * @returns a function that will wrap a JSX animation element wrapping any JSX element
+     */
+    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
+        const dir = presEffectDoc?.presEffectDirection ?? presEffectDoc?.linkAnimDirection;
+        const effectProps = {
+            left: dir === PresEffectDirection.Left,
+            right: dir === PresEffectDirection.Right,
+            top: dir === PresEffectDirection.Top,
+            bottom: dir === PresEffectDirection.Bottom,
+            opposite: true,
+            delay: 0,
+            duration: Cast(presEffectDoc?.presTransition, 'number', null),
+        };
+        //prettier-ignore
+        switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.linkAnimEffect))) {
+            default: 
+            case PresEffect.None:       return renderDoc;
+            case PresEffect.Zoom:       return <Zoom {...effectProps}>{renderDoc}</Zoom>;
+            case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>;
+            case PresEffect.Flip:       return <Flip {...effectProps}>{renderDoc}</Flip>;
+            case PresEffect.Rotate:     return <Rotate {...effectProps}>{renderDoc}</Rotate>;
+            case PresEffect.Bounce:     return <Bounce {...effectProps}>{renderDoc}</Bounce>;
+            case PresEffect.Roll:       return <Roll {...effectProps}>{renderDoc}</Roll>;
+            case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
+        }
+    }
     render() {
         TraceMobx();
         const highlighting = this.props.styleProvider?.(this.props.Document, this.props, StyleProp.Highlighting);
@@ -1446,7 +1476,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
-        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? this.rootDoc[AnimationSym], this.rootDoc) : renderDoc;
+        const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc);
         return (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
@@ -1720,7 +1750,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
 
     @computed get anchorViewDoc() {
-        return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : this.rootDoc;
+        return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined;
     }
     docViewPathFunc = () => this.docViewPath;
     isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index e09155ac2..24562ccbd 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -13,6 +13,7 @@ import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxBaseComponent } from '../DocComponent';
+import { DocFocusOptions } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 
 const EquationSchema = createSchema({});
@@ -47,7 +48,7 @@ export class FunctionPlotBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return anchor;
     };
     @action
-    scrollFocus = (doc: Doc, smooth: boolean) => {
+    scrollFocus = (doc: Doc, smooth: DocFocusOptions) => {
         this.dataDoc.xRange = new List<number>(Array.from(Cast(doc.xRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]))));
         this.dataDoc.yRange = new List<number>(Array.from(Cast(doc.yRange, listSpec('number'), Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]))));
         return 0;
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 6479e933e..b0f6f8358 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
@@ -638,6 +638,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
                             annotationLayer={this._annotationLayer.current}
+                            selectionText={returnEmptyString}
                             mainCont={this._mainCont.current}
                         />
                     )}
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2694cb350..e0f64e7aa 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1,11 +1,10 @@
 import React = require('react');
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, ObservableSet, observe, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
-import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
-import { Doc, DocListCast, FieldResult, Opt, StrListCast } from '../../../../fields/Doc';
+import { AnimationSym, Doc, DocListCast, FieldResult, HighlightSym, Opt, StrListCast } from '../../../../fields/Doc';
 import { Copy, Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -64,36 +63,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(PresBox, fieldKey);
     }
 
-    /**
-     * returns an entrance animation effect function to wrap a JSX element
-     * @param presEffectDoc presentation effects document that specifies the animation effect parameters
-     * @returns a function that will wrap a JSX animation element wrapping any JSX element
-     */
-    public static AnimationEffect(renderDoc: JSX.Element, presEffectDoc: Opt<Doc>, root: Doc) {
-        const effectProps = {
-            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),
-        };
-        //prettier-ignore
-        switch (StrCast(presEffectDoc?.presEffect)) {
-            default: 
-            case PresEffect.None:       return renderDoc;
-            case PresEffect.Zoom:       return <Zoom {...effectProps}>{renderDoc}</Zoom>;
-            case PresEffect.Fade:       return <Fade {...effectProps}>{renderDoc}</Fade>;
-            case PresEffect.Flip:       return <Flip {...effectProps}>{renderDoc}</Flip>;
-            case PresEffect.Rotate:     return <Rotate {...effectProps}>{renderDoc}</Rotate>;
-            case PresEffect.Bounce:     return <Bounce {...effectProps}>{renderDoc}</Bounce>;
-            case PresEffect.Roll:       return <Roll {...effectProps}>{renderDoc}</Roll>;
-            case PresEffect.Lightspeed: return <LightSpeed {...effectProps}>{renderDoc}</LightSpeed>;
-        }
-    }
-
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    private _obDisposers: { [name: string]: any } = {};
     public selectedArray = new ObservableSet<Doc>();
 
     @observable public static Instance: PresBox;
@@ -170,10 +141,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // Turn of progressivize editors
         this.turnOffEdit(true);
         Object.values(this._disposers).forEach(disposer => disposer?.());
+        Object.values(this._obDisposers).forEach(disposer => disposer?.());
     }
 
     @action
     componentDidMount() {
+        this._obDisposers.anim = observe(
+            this,
+            'activeItem',
+            change => {
+                change.oldValue && (DocCast((change.oldValue as Doc).presentationTargetDoc)[AnimationSym] = undefined);
+                change.newValue && (DocCast((change.newValue as Doc).presentationTargetDoc)[AnimationSym] = change.newValue as Doc);
+            },
+            true
+        );
         this._disposers.keyboard = reaction(
             () => this.selectedDoc,
             selected => {
-- 
cgit v1.2.3-70-g09d2


From 403d1c4fece9efa663e0fd7161afff9f27cf670c Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 16 Dec 2022 15:07:50 -0500
Subject: fixed problem with undo.  regularized all linkfollowing anchor fields
 to start with followLink<xxx>.   added ease vs linear flag for scroll
 transitions in link and preselmeent navigations. added link follow to move
 target to specified offset from source.  shifted from setting dropAction on
 items to setting childDropAction on collections

---
 src/Utils.ts                                       |  29 +++--
 src/client/documents/Documents.ts                  |   4 +-
 src/client/util/CurrentUserUtils.ts                |  35 +++---
 src/client/util/LinkFollower.ts                    |  78 ++++++++----
 src/client/views/DocumentButtonBar.scss            |  17 +++
 src/client/views/DocumentButtonBar.tsx             |  68 +++++++---
 src/client/views/GestureOverlay.tsx                |   1 -
 src/client/views/PropertiesView.tsx                |  88 +++++++++----
 .../views/collections/CollectionNoteTakingView.tsx |   4 +-
 .../views/collections/CollectionPileView.tsx       |   2 +-
 .../views/collections/CollectionStackingView.tsx   |   4 +-
 .../views/collections/CollectionTimeView.tsx       |   2 +-
 src/client/views/collections/TabDocView.tsx        |   5 +
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   6 +-
 .../collectionLinear/CollectionLinearView.tsx      |   3 +-
 src/client/views/linking/LinkPopup.scss            |   5 +-
 src/client/views/linking/LinkPopup.tsx             |  13 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  11 +-
 src/client/views/nodes/LabelBox.tsx                | 140 ++++++++++++---------
 src/client/views/nodes/WebBox.tsx                  |   8 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   5 +-
 src/client/views/nodes/trails/PresBox.tsx          |  37 ++++--
 src/client/views/nodes/trails/PresEnums.ts         |   1 +
 src/client/views/pdf/AnchorMenu.tsx                |   3 +-
 src/client/views/pdf/PDFViewer.tsx                 |  21 ++--
 src/client/views/search/SearchBox.tsx              |  29 ++++-
 src/fields/util.ts                                 |  10 +-
 29 files changed, 416 insertions(+), 217 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 5e0514bc6..9d3b9eb2b 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -558,9 +558,13 @@ export namespace JSONUtils {
     }
 }
 
-const easeInOutQuad = (currentTime: number, start: number, change: number, duration: number) => {
-    let newCurrentTime = currentTime / (duration / 2);
+const easeFunc = (transition: 'ease' | 'linear' | undefined, currentTime: number, start: number, change: number, duration: number) => {
+    if (transition === 'linear') {
+        let newCurrentTime = currentTime / duration; //  currentTime / (duration / 2);
+        return start + newCurrentTime * change;
+    }
 
+    let newCurrentTime = currentTime / (duration / 2);
     if (newCurrentTime < 1) {
         return (change / 2) * newCurrentTime * newCurrentTime + start;
     }
@@ -569,23 +573,28 @@ const easeInOutQuad = (currentTime: number, start: number, change: number, durat
     return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
 };
 
-export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number) {
+export function smoothScroll(duration: number, element: HTMLElement | HTMLElement[], to: number, transition: 'ease' | 'linear' | undefined, stopper?: () => void) {
+    stopper?.();
     const elements = element instanceof HTMLElement ? [element] : element;
     const starts = elements.map(element => element.scrollTop);
     const startDate = new Date().getTime();
-
+    let _stop = false;
+    const stop = () => (_stop = true);
     const animateScroll = () => {
         const currentDate = new Date().getTime();
         const currentTime = currentDate - startDate;
-        elements.map((element, i) => (element.scrollTop = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)));
+        elements.map((element, i) => (element.scrollTop = easeFunc(transition, currentTime, starts[i], to - starts[i], duration)));
 
-        if (currentTime < duration) {
-            requestAnimationFrame(animateScroll);
-        } else {
-            elements.forEach(element => (element.scrollTop = to));
+        if (!_stop) {
+            if (currentTime < duration) {
+                requestAnimationFrame(animateScroll);
+            } else {
+                elements.forEach(element => (element.scrollTop = to));
+            }
         }
     };
     animateScroll();
+    return stop;
 }
 
 export function smoothScrollHorizontal(duration: number, element: HTMLElement | HTMLElement[], to: number) {
@@ -596,7 +605,7 @@ export function smoothScrollHorizontal(duration: number, element: HTMLElement |
     const animateScroll = () => {
         const currentDate = new Date().getTime();
         const currentTime = currentDate - startDate;
-        elements.map((element, i) => (element.scrollLeft = easeInOutQuad(currentTime, starts[i], to - starts[i], duration)));
+        elements.map((element, i) => (element.scrollLeft = easeFunc('ease', currentTime, starts[i], to - starts[i], duration)));
 
         if (currentTime < duration) {
             requestAnimationFrame(animateScroll);
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d13d96dd3..634b14822 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -37,7 +37,7 @@ import { FontIconBox } from '../views/nodes/button/FontIconBox';
 import { ColorBox } from '../views/nodes/ColorBox';
 import { ComparisonBox } from '../views/nodes/ComparisonBox';
 import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox';
-import { DocFocusOptions, OpenWhereMod } from '../views/nodes/DocumentView';
+import { DocFocusOptions, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
 import { EquationBox } from '../views/nodes/EquationBox';
 import { FieldViewProps } from '../views/nodes/FieldView';
 import { FilterBox } from '../views/nodes/FilterBox';
@@ -1322,7 +1322,7 @@ export namespace DocUtils {
         return DocUtils.ActiveRecordings.map(audio => {
             const sourceDoc = getSourceDoc();
             const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor() || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
-            link && (link.followLinkLocation = 'add:right');
+            link && (link.followLinkLocation = OpenWhere.addRight);
             return link;
         });
     }
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 635980e03..5549769aa 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -22,7 +22,7 @@ import { Colors } from "../views/global/globalEnums";
 import { MainView } from "../views/MainView";
 import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox";
 import { OverlayView } from "../views/OverlayView";
-import { DragManager } from "./DragManager";
+import { DragManager, dropActionType } from "./DragManager";
 import { MakeTemplate } from "./DropConverter";
 import { LinkManager } from "./LinkManager";
 import { ScriptingGlobals } from "./ScriptingGlobals";
@@ -306,7 +306,7 @@ export class CurrentUserUtils {
         const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => {
             const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined;
             const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit,
-                _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, _dropAction: "alias",
+                _nativeWidth: 50, _nativeHeight: 50, _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, 
                 btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true,
                 _removeDropProperties: new List<string>(["_stayInCollection"]),
                 };
@@ -316,7 +316,7 @@ export class CurrentUserUtils {
         const reqdOpts:DocumentOptions = {
             title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true,
             _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true,
-            childDocumentsActive: true
+            childDocumentsActive: true, childDropAction: 'alias' 
         };
         const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
         return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts);
@@ -352,15 +352,15 @@ export class CurrentUserUtils {
             const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined;
             const reqdBtnOpts:DocumentOptions = {
                 title, icon, target, btnType: ButtonType.MenuButton, system: true, dontUndo: true,  dontRegisterView: true,
-                _width: 60, _height: 60,  _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, _dropAction: "alias",
-                _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+                _width: 60, _height: 60,  _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, 
+                _removeDropProperties: new List<string>(["_stayInCollection"]),
             };
             return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs);
         });
 
         const reqdStackOpts:DocumentOptions ={
-            title: "menuItemPanel", childDropAction: "alias", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)",  dontRegisterView: true, ignoreClick: true,
-            _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true
+            title: "menuItemPanel", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)",  dontRegisterView: true, ignoreClick: true,
+            _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true,
         };
         return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" });
     }
@@ -397,14 +397,14 @@ export class CurrentUserUtils {
     // sets up the main document for the mobile button
     static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, {
         ...opts,
-        _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
+         _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15,
         borderRounding: "5px", boxShadow: "0 0", system: true
     }) as any as Doc
 
     // sets up the text container for the information contained within the mobile button
     static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, {
         ...opts,
-        _removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
+        _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25,
         backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true
     }) as any as Doc
 
@@ -573,8 +573,8 @@ export class CurrentUserUtils {
     })
 
     static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({
-        btnType: ButtonType.ToolButton, _forceActive: true, _dropAction: "alias", _hideContextMenu: true, 
-        _removeDropProperties: new List<string>(["_dropAction", "_hideContextMenu", "stayInCollection"]), 
+        btnType: ButtonType.ToolButton, _forceActive: true, _hideContextMenu: true, 
+        _removeDropProperties: new List<string>([ "_hideContextMenu", "stayInCollection"]), 
         _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts,
     })
 
@@ -595,12 +595,15 @@ export class CurrentUserUtils {
             // { scripts: { onClick: 'nextKeyFrame(_readOnly_)'}, opts:{title: "Fwd",     icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton,  width: 20,}  },
              ];
         const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts));
-        const dockBtnsReqdOpts = {
-            title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard",
+        const dockBtnsReqdOpts:DocumentOptions = {
+            title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'alias',
             childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true
         };
         reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
-        reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
+        reaction(() => UndoManager.undoStack.slice(), () => {
+            console.log(UndoManager.undoStack)
+            Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4;
+        }, { fireImmediately: true });
         return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
     }
 
@@ -687,7 +690,7 @@ export class CurrentUserUtils {
             _nativeWidth: params.width ?? 30, _width: params.width ?? 30,
             _height: 30, _nativeHeight: 30, 
             _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true,
-            _dropAction: "alias", _removeDropProperties: new List<string>(["dropAction", "_stayInCollection"]),
+            _removeDropProperties: new List<string>([ "_stayInCollection"]),
         };
         const reqdFuncs:{[key:string]:any} = {
             ...params.funcs,
@@ -698,7 +701,7 @@ export class CurrentUserUtils {
 
     /// Initializes all the default buttons for the top bar context menu
     static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
-        const reqdCtxtOpts = { title: "context menu buttons", flexGap: 0, childDontRegisterViews: true,  linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
+        const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", flexGap: 0, childDropAction: 'alias', childDontRegisterViews: true,  linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 };
         const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
         const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => {
             const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title);
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 0285803e8..94badbb44 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,5 +1,6 @@
 import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt, WidthSym } from '../../fields/Doc';
+import { listSpec } from '../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
@@ -98,32 +99,59 @@ export class LinkFollower {
                     : linkDoc.anchor1
             ) as Doc;
             if (target) {
-                const options: DocFocusOptions = {
-                    playAudio: BoolCast(sourceDoc.followLinkAudio),
-                    toggleTarget: BoolCast(sourceDoc.followLinkToggle),
-                    willPan: true,
-                    willPanZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
-                    zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500),
-                    zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null),
-                    effect: sourceDoc,
-                    originatingDoc: sourceDoc,
+                const doFollow = (canToggle?: boolean) => {
+                    const options: DocFocusOptions = {
+                        playAudio: BoolCast(sourceDoc.followLinkAudio),
+                        toggleTarget: canToggle && BoolCast(sourceDoc.followLinkToggle),
+                        willPan: true,
+                        willPanZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
+                        zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkTransitionTime, 500),
+                        zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null),
+                        easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
+                        effect: sourceDoc,
+                        originatingDoc: sourceDoc,
+                    };
+                    if (target.TourMap) {
+                        const fieldKey = Doc.LayoutFieldKey(target);
+                        const tour = DocListCast(target[fieldKey]).reverse();
+                        LightboxView.SetLightboxDoc(currentContext, undefined, tour);
+                        setTimeout(LightboxView.Next);
+                        allFinished();
+                    } else {
+                        const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
+                        const containerDoc = containerAnnoDoc || target;
+                        var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
+                        while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
+                            containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                        }
+                        const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
+                        DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.inPlace), finished), targetContexts, allFinished);
+                    }
                 };
-                if (target.TourMap) {
-                    const fieldKey = Doc.LayoutFieldKey(target);
-                    const tour = DocListCast(target[fieldKey]).reverse();
-                    LightboxView.SetLightboxDoc(currentContext, undefined, tour);
-                    setTimeout(LightboxView.Next);
-                    allFinished();
-                } else {
-                    const containerAnnoDoc = Cast(target.annotationOn, Doc, null);
-                    const containerDoc = containerAnnoDoc || target;
-                    var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]);
-                    while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
-                        containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
+                let movedTarget = false;
+                if (sourceDoc.followLinkLocation === OpenWhere.inParent) {
+                    const sourceDocParent = DocCast(sourceDoc.context);
+                    if (target.context instanceof Doc && target.context !== sourceDocParent) {
+                        Doc.RemoveDocFromList(target.context, Doc.LayoutFieldKey(target.context), target);
+                        movedTarget = true;
+                    }
+                    if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) {
+                        Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
+                        movedTarget = true;
+                    }
+                    target.context = sourceDocParent;
+                    const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)];
+                    if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) {
+                        target.x = moveTo[0];
+                        movedTarget = true;
+                    }
+                    if (sourceDoc.followLinkYoffset !== undefined && moveTo[1] !== target.y) {
+                        target.y = moveTo[1];
+                        movedTarget = true;
                     }
-                    const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext;
-                    DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), targetContexts, allFinished);
-                }
+                    if (movedTarget) setTimeout(doFollow);
+                    else doFollow(true);
+                } else doFollow(true);
             } else {
                 allFinished();
             }
diff --git a/src/client/views/DocumentButtonBar.scss b/src/client/views/DocumentButtonBar.scss
index f9c988fdd..835d6c8bb 100644
--- a/src/client/views/DocumentButtonBar.scss
+++ b/src/client/views/DocumentButtonBar.scss
@@ -28,6 +28,15 @@ $linkGap: 3px;
     height: 20px;
     align-items: center;
 }
+.documentButtonBar-linkTypes {
+    position: absolute;
+    display: none;
+    width: 60px;
+    top: -14px;
+    background: black;
+    height: 20px;
+    align-items: center;
+}
 .documentButtonBar-followTypes {
     width: 20px;
     display: none;
@@ -55,6 +64,14 @@ $linkGap: 3px;
         }
     }
 }
+.documentButtonBar-link {
+    color: white;
+    &:hover {
+        .documentButtonBar-linkTypes {
+            display: flex;
+        }
+    }
+}
 
 .documentButtonBar-pinIcon {
     &:hover {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 5969d55e9..90c6c040c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -1,24 +1,24 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import { action, computed, observable, runInAction } from 'mobx';
+import { action, computed, observable, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../fields/Doc';
 import { RichTextField } from '../../fields/RichTextField';
-import { BoolCast, Cast, NumCast } from '../../fields/Types';
+import { Cast, NumCast } from '../../fields/Types';
 import { emptyFunction, returnFalse, setupMoveUpEvents, simulateMouseClick } from '../../Utils';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { Pulls, Pushes } from '../apis/google_docs/GoogleApiClientUtils';
 import { Docs } from '../documents/Documents';
 import { DragManager } from '../util/DragManager';
 import { SelectionManager } from '../util/SelectionManager';
-import { SettingsManager } from '../util/SettingsManager';
 import { SharingManager } from '../util/SharingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
 import { TabDocView } from './collections/TabDocView';
 import './DocumentButtonBar.scss';
 import { Colors } from './global/globalEnums';
+import { LinkPopup } from './linking/LinkPopup';
 import { MetadataEntryMenu } from './MetadataEntryMenu';
 import { DocumentLinksButton } from './nodes/DocumentLinksButton';
 import { DocumentView, DocumentViewInternal, OpenWhereMod } from './nodes/DocumentView';
@@ -26,6 +26,7 @@ import { DashFieldView } from './nodes/formattedText/DashFieldView';
 import { GoogleRef } from './nodes/formattedText/FormattedTextBox';
 import { TemplateMenu } from './TemplateMenu';
 import React = require('react');
+import { DocumentType } from '../documents/DocumentTypes';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -255,6 +256,28 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         );
     }
 
+    @observable subLink = '';
+    @computed get linkButton() {
+        const targetDoc = this.view0?.props.Document;
+        return !targetDoc || !this.view0 ? null : (
+            <div className="documentButtonBar-icon documentButtonBar-link">
+                <div className="documentButtonBar-linkTypes">
+                    <Tooltip title={<div>search for target</div>}>
+                        <div className="documentButtonBar-button">
+                            <button style={{ backgroundColor: 'transparent', width: 35, height: 35, display: 'flex', justifyContent: 'center', alignItems: 'center', position: 'relative' }} onPointerDown={this.toggleLinkSearch}>
+                                <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
+                                <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'center', top: 9, left: 2 }} icon={'link'} size="lg" />
+                            </button>
+                        </div>
+                    </Tooltip>
+                </div>
+                <div style={{ width: 25, height: 25 }}>
+                    <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
+                </div>
+            </div>
+        );
+    }
+
     @observable subPin = '';
     @computed
     get pinButton() {
@@ -284,7 +307,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                                     .views()
                                     .filter(v => v)
                                     .map(dv => dv!.rootDoc);
-                                TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null) });
+                                TabDocView.PinDoc(docs, { pinAudioPlay: true, pinDocLayout, pinDocContent, activeFrame: Cast(docs.lastElement()?.activeFrame, 'number', null), currentFrame: Cast(docs.lastElement()?.currentFrame, 'number', null) });
                                 e.stopPropagation();
                             }}
                         />
@@ -319,12 +342,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
     get shareButton() {
         const targetDoc = this.view0?.props.Document;
         return !targetDoc ? null : (
-            <Tooltip
-                title={
-                    <>
-                        <div className="dash-tooltip">{'Open Sharing Manager'}</div>
-                    </>
-                }>
+            <Tooltip title={<div className="dash-tooltip">{'Open Sharing Manager'}</div>}>
                 <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={e => SharingManager.Instance.open(this.view0, targetDoc)}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="users" />
                 </div>
@@ -347,12 +365,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
     get metadataButton() {
         const view0 = this.view0;
         return !view0 ? null : (
-            <Tooltip
-                title={
-                    <>
-                        <div className="dash-tooltip">Show metadata panel</div>
-                    </>
-                }>
+            <Tooltip title={<div className="dash-tooltip">Show metadata panel</div>}>
                 <div className="documentButtonBar-linkFlyout">
                     <Flyout
                         anchorPoint={anchorPoints.LEFT_TOP}
@@ -464,6 +477,12 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         simulateMouseClick(child, e.clientX, e.clientY - 30, e.screenX, e.screenY - 30);
     };
 
+    @observable _showLinkPopup = false;
+    @action
+    toggleLinkSearch = (e: React.PointerEvent) => {
+        this._showLinkPopup = !this._showLinkPopup;
+        e.stopPropagation();
+    };
     render() {
         if (!this.view0) return null;
 
@@ -476,9 +495,20 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                 <div className="documentButtonBar-button">
                     <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} ShowCount={true} />
                 </div>
-                <div className="documentButtonBar-button">
-                    <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={true} />
-                </div>
+                {this._showLinkPopup ? (
+                    <div style={{ position: 'absolute', zIndex: 1000 }}>
+                        <LinkPopup
+                            key="popup"
+                            showPopup={this._showLinkPopup}
+                            linkCreated={link => (link.linkDisplay = !this.props.views().lastElement()?.rootDoc.isLinkButton)}
+                            linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.()}
+                            linkFrom={() => this.props.views().lastElement()?.rootDoc}
+                        />
+                    </div>
+                ) : (
+                    <div className="documentButtonBar-button">{this.linkButton}</div>
+                )}
+
                 {(DocumentLinksButton.StartLink || Doc.UserDoc()['documentLinksButton-fullMenu']) && DocumentLinksButton.StartLink !== doc ? (
                     <div className="documentButtonBar-button">
                         <DocumentLinksButton View={this.view0} AlwaysOn={true} InMenu={true} StartLink={false} />
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index a29073f14..e3328fb4c 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -122,7 +122,6 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
                 onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined,
                 onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
                 backgroundColor: data.backgroundColor,
-                _removeDropProperties: new List<string>(['dropAction']),
                 dragFactory: data.dragFactory,
                 system: true,
             })
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e43b160b8..92c5708aa 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1414,7 +1414,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
 
     @undoBatch
-    changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.linkAnimEffect = behavior));
+    changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
 
     @undoBatch
     changeEffectDirection = action((effect: PresEffectDirection) => this.sourceAnchor && (this.sourceAnchor.linkAnimDirection = effect));
@@ -1474,8 +1474,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         return selAnchor ?? (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]))));
+    toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc, value: any = true, ovalue: any = false, cb: (val: any) => any = val => val) => {
+        anchor &&
+            setupMoveUpEvents(
+                this,
+                e,
+                returnFalse,
+                emptyFunction,
+                undoBatch(
+                    action(() => {
+                        anchor[prop] = anchor[prop] === value ? ovalue : value;
+                        this.selectedDoc && cb(anchor[prop]);
+                    })
+                )
+            );
     };
 
     @computed
@@ -1516,7 +1528,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         if (change) scale += change;
         if (scale < 0.01) scale = 0.01;
         if (scale > 1) scale = 1;
-        this.sourceAnchor && (this.sourceAnchor.linkZoomScale = scale);
+        this.sourceAnchor && (this.sourceAnchor.followLinkZoomScale = scale);
     };
 
     /**
@@ -1532,7 +1544,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     render() {
         const isNovice = Doc.noviceMode;
-        const zoom = Number((NumCast(this.sourceAnchor?.linkZoomScale, 1) * 100).toPrecision(3));
+        const zoom = Number((NumCast(this.sourceAnchor?.followLinkZoomScale, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
         const indent = 30;
         const hasSelectedAnchor = SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!));
@@ -1610,21 +1622,22 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <p>Follow by</p>
                                             <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkLocation, 'default')}>
                                                 <option value="default">Default</option>
-                                                <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">Overlaying current tab</option>
-                                                <option value="lightbox">Opening in lightbox</option>
-                                                <option value="add">Opening in new tab</option>
-                                                <option value="replace">Replacing current tab</option>
-                                                <option value="inPlace">Opening in place</option>
+                                                <option value={OpenWhere.addLeft}>Opening in new left pane</option>
+                                                <option value={OpenWhere.addRight}>Opening in new right pane</option>
+                                                <option value={OpenWhere.replaceLeft}>Replacing left tab</option>
+                                                <option value={OpenWhere.replaceRight}>Replacing right tab</option>
+                                                <option value={OpenWhere.fullScreen}>Overlaying current tab</option>
+                                                <option value={OpenWhere.lightbox}>Opening in lightbox</option>
+                                                <option value={OpenWhere.add}>Opening in new tab</option>
+                                                <option value={OpenWhere.replace}>Replacing current tab</option>
+                                                <option value={OpenWhere.inParent}>Opening in same collection</option>
+                                                <option value={OpenWhere.inPlace}>Opening in place</option>
                                                 {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
                                             </select>
                                         </div>
                                         <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?.linkAnimEffect, 'default')}>
+                                            <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, '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>
@@ -1642,9 +1655,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             '0.1',
                                             '0.1',
                                             '10',
-                                            NumCast(this.sourceAnchor?.linkTransitionTime) / 1000,
+                                            NumCast(this.sourceAnchor?.followLinkTransitionTime) / 1000,
                                             true,
-                                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.linkTransitionTime = timeInMS)),
+                                            (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => this.sourceAnchor && (this.sourceAnchor.followLinkTransitionTime = timeInMS)),
                                             indent
                                         )}{' '}
                                         <div
@@ -1681,9 +1694,42 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
                                             </button>
                                         </div>
+                                        <div className="propertiesView-input inline">
+                                            <p>Ease Transitions</p>
+                                            <button
+                                                style={{ background: this.sourceAnchor?.followLinkEase === 'linear' ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkEase', this.sourceAnchor, 'ease', 'linear')}
+                                                onClick={e => e.stopPropagation()}
+                                                className="propertiesButton">
+                                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                            </button>
+                                        </div>
+                                        <div className="propertiesView-input inline">
+                                            <p>Capture Offset to Target</p>
+                                            <button
+                                                style={{ background: this.sourceAnchor?.followLinkXoffset === undefined ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => {
+                                                    this.toggleAnchorProp(e, 'followLinkXoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.x) - NumCast(this.sourceAnchor?.x), undefined);
+                                                    this.toggleAnchorProp(e, 'followLinkYoffset', this.sourceAnchor, NumCast(this.destinationAnchor?.y) - NumCast(this.sourceAnchor?.y), undefined);
+                                                }}
+                                                onClick={e => e.stopPropagation()}
+                                                className="propertiesButton">
+                                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                            </button>
+                                        </div>
+                                        <div className="propertiesView-input inline">
+                                            <p>Center Target (no zoom)</p>
+                                            <button
+                                                style={{ background: this.sourceAnchor?.followLinkZoomScale !== 0 ? '' : '#4476f7', borderRadius: 3 }}
+                                                onPointerDown={e => this.toggleAnchorProp(e, 'followLinkZoomScale', this.sourceAnchor, 0, 0.5, val => !val && this.sourceAnchor && (this.sourceAnchor.followLinkZoom = true))}
+                                                onClick={e => e.stopPropagation()}
+                                                className="propertiesButton">
+                                                <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                            </button>
+                                        </div>
                                         <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
                                             <p>Zoom %</p>
-                                            <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
+                                            <div className="ribbon-property" style={{ display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '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))}>
@@ -1695,18 +1741,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 </div>
                                             </div>
                                             <button
-                                                style={{ background: !targZoom ? '' : '#4476f7', borderRadius: 3, gridColumn: 3 }}
+                                                style={{ background: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? '' : '#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>
-                                        {!targZoom ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
+                                        {!targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? null : PresBox.inputter('0', '1', '100', zoom, true, this.setZoom, 30)}
                                         <div
                                             className={'slider-headers'}
                                             style={{
-                                                display: !targZoom ? 'none' : 'grid',
+                                                display: !targZoom || this.sourceAnchor?.followLinkZoomScale === 0 ? 'none' : 'grid',
                                                 justifyContent: 'space-between',
                                                 width: `calc(100% - ${indent * 2}px)`,
                                                 marginLeft: indent,
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 5e389e17e..26c73438c 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -181,7 +181,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     }
 
     scrollToBottom = () => {
-        smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+        smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease');
     };
 
     // let's dive in and get the actual document we want to drag/move around
@@ -193,7 +193,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
             const top = found.getBoundingClientRect().top;
             const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
             }
         }
         const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx
index e95622630..ba90ed8cd 100644
--- a/src/client/views/collections/CollectionPileView.tsx
+++ b/src/client/views/collections/CollectionPileView.tsx
@@ -120,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, OpenWhere.inParent) && (this.props.removeDocument?.(doc) || false);
+                        this.props.addDocTab(doc, OpenWhere.inParentFromScreen) && (this.props.removeDocument?.(doc) || false);
                         dist = 0;
                     }
                 }
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 08aebc62d..acf59b5da 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -242,7 +242,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     }
 
     scrollToBottom = () => {
-        smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight);
+        smoothScroll(500, this._mainCont!, this._mainCont!.scrollHeight, 'ease');
     };
 
     // let's dive in and get the actual document we want to drag/move around
@@ -255,7 +255,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             const top = found.getBoundingClientRect().top;
             const localTop = this.props.ScreenToLocalTransform().transformPoint(0, top);
             if (Math.floor(localTop[1]) !== 0) {
-                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop);
+                smoothScroll((focusSpeed = options.zoomTime ?? 500), this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
             }
         }
         const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing;
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index ac896a8fd..a1466bcd0 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -57,7 +57,7 @@ export class CollectionTimeView extends CollectionSubView() {
     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); ";
+        ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; useRightSplit(alias, shiftKey); ";
         runInAction(() => {
             this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name });
             this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' });
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index a61ae680b..e45e2a1cb 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -281,6 +281,11 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 pinDoc.title = doc.title + ' (move)';
                 pinDoc.presMovement = PresMovement.Pan;
             }
+            if (pinProps?.currentFrame !== undefined) {
+                pinDoc.presCurrentFrame = pinProps?.currentFrame;
+                pinDoc.title = doc.title + ' (move)';
+                pinDoc.presMovement = PresMovement.Pan;
+            }
             if (pinDoc.isInkMask) {
                 pinDoc.presHideAfter = true;
                 pinDoc.presHideBefore = true;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 4818094de..6b9eb7916 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1186,7 +1186,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const focusSpeed = options?.instant ? 0 : didMove ? options.zoomTime ?? 500 : 0;
             // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
             if (didMove) {
-                scale && (this.Document[this.scaleFieldKey] = scale);
+                options.zoomScale && scale && (this.Document[this.scaleFieldKey] = scale);
                 this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             }
 
@@ -1343,6 +1343,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     addDocTab = action((doc: Doc, where: OpenWhere) => {
         switch (where) {
             case OpenWhere.inParent:
+                return this.props.addDocument?.(doc) || false;
+            case OpenWhere.inParentFromScreen:
                 return (
                     this.props.addDocument?.(
                         (doc instanceof Doc ? [doc] : doc).map(doc => {
@@ -1741,7 +1743,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             doc.x = scr?.[0];
             doc.y = scr?.[1];
         });
-        this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParent);
+        this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParentFromScreen);
         this.props.ContainingCollectionView?.removeDocument(this.props.Document);
     };
 
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index ff263c4f3..a2330c6b2 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -9,7 +9,7 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields
 import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
-import { DragManager } from '../../../util/DragManager';
+import { DragManager, dropActionType } from '../../../util/DragManager';
 import { Transform } from '../../../util/Transform';
 import { Colors, Shadows } from '../../global/globalEnums';
 import { DocumentLinksButton } from '../../nodes/DocumentLinksButton';
@@ -190,6 +190,7 @@ export class CollectionLinearView extends CollectionSubView() {
                     moveDocument={this.props.moveDocument}
                     addDocTab={this.props.addDocTab}
                     pinToPres={emptyFunction}
+                    dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType}
                     rootSelected={this.props.isSelected}
                     removeDocument={this.props.removeDocument}
                     ScreenToLocalTransform={docXf}
diff --git a/src/client/views/linking/LinkPopup.scss b/src/client/views/linking/LinkPopup.scss
index 60c9ebfcd..b20ad9476 100644
--- a/src/client/views/linking/LinkPopup.scss
+++ b/src/client/views/linking/LinkPopup.scss
@@ -1,7 +1,7 @@
 .linkPopup-container {
     background: white;
     box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
-    top: 35px;
+    top: 0;
     height: 200px;
     width: 200px;
     position: absolute;
@@ -38,8 +38,7 @@
         }
     }
 
-
     .searchBox-container {
         background: pink;
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx
index 0bcb68f82..7bdace2b6 100644
--- a/src/client/views/linking/LinkPopup.tsx
+++ b/src/client/views/linking/LinkPopup.tsx
@@ -11,10 +11,13 @@ import { SearchBox } from '../search/SearchBox';
 import { DefaultStyleProvider } from '../StyleProvider';
 import './LinkPopup.scss';
 import React = require('react');
+import { OpenWhere } from '../nodes/DocumentView';
 
 interface LinkPopupProps {
     showPopup: boolean;
     linkFrom?: () => Doc | undefined;
+    linkCreateAnchor?: () => Doc | undefined;
+    linkCreated?: (link: Doc) => void;
     // groupType: string;
     // linkDoc: Doc;
     // docView: DocumentView;
@@ -32,14 +35,10 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
 
     // TODO: should check for valid URL
     @undoBatch
-    makeLinkToURL = (target: string, lcoation: string) => {
-        ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, 'onRadd:rightight', target, target);
-    };
+    makeLinkToURL = (target: string, lcoation: string) => ((this.view as any)?.TextView as FormattedTextBox).makeLinkAnchor(undefined, OpenWhere.addRight, target, target);
 
     @action
-    onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.linkURL = e.target.value;
-    };
+    onLinkChange = (e: React.ChangeEvent<HTMLInputElement>) => (this.linkURL = e.target.value);
 
     getPWidth = () => 500;
     getPHeight = () => 500;
@@ -67,7 +66,9 @@ export class LinkPopup extends React.Component<LinkPopupProps> {
                         Document={Doc.MySearcher}
                         DataDoc={Doc.MySearcher}
                         linkFrom={linkDoc}
+                        linkCreateAnchor={this.props.linkCreateAnchor}
                         linkSearch={true}
+                        linkCreated={this.props.linkCreated}
                         fieldKey="data"
                         dropAction="move"
                         isSelected={returnTrue}
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index bf1f13a06..f8ef87fb1 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -184,7 +184,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
                 topDoc.x = spt[0];
                 topDoc.y = spt[1];
                 this.props.removeDocument?.(topDoc);
-                this.props.addDocTab(topDoc, OpenWhere.inParent);
+                this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen);
             } else {
                 const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0);
                 const fpt = screenXf.transformPoint(spt[0], spt[1]);
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 99fa62fa7..6f3152981 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -305,7 +305,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     top: 0,
                     pointerEvents: 'none',
                 }}>
-                {!DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>}
+                <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c9fbe7a98..76cc6800a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -75,6 +75,7 @@ export enum OpenWhere {
     inPlace = 'inPlace',
     lightbox = 'lightbox',
     add = 'add',
+    addLeft = 'add:left',
     addRight = 'add:right',
     addBottom = 'add:bottom',
     dashboard = 'dashboard',
@@ -82,7 +83,10 @@ export enum OpenWhere {
     fullScreen = 'fullScreen',
     toggle = 'toggle',
     replace = 'replace',
+    replaceRight = 'replace:right',
+    replaceLeft = 'replace:left',
     inParent = 'inParent',
+    inParentFromScreen = 'inParentFromScreen',
 }
 export enum OpenWhereMod {
     none = '',
@@ -108,6 +112,7 @@ export interface DocFocusOptions {
     playAudio?: boolean; // whether to play audio annotation on focus
     toggleTarget?: boolean; // whether to toggle target on and off
     originatingDoc?: Doc; // document that triggered the focus
+    easeFunc?: 'linear' | 'ease'; // transition method for scrolling
 }
 export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
 export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
@@ -737,7 +742,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     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));
+                    this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType));
                 }
             }
             e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document'  however it does mark the event as cancelBubble=true which we test for in the move event handlers
@@ -854,7 +859,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const portal = Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' });
             DocUtils.MakeLink({ doc: this.props.Document }, { doc: portal }, 'portal to:portal from');
         }
-        this.Document.followLinkLocation = 'inPlace';
+        this.Document.followLinkLocation = OpenWhere.inPlace;
         this.Document.followLinkZoom = true;
         this.Document._isLinkButton = true;
     };
@@ -1446,7 +1451,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             duration: Cast(presEffectDoc?.presTransition, 'number', null),
         };
         //prettier-ignore
-        switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.linkAnimEffect))) {
+        switch (StrCast(presEffectDoc?.presEffect, StrCast(presEffectDoc?.followLinkAnimEffect))) {
             default: 
             case PresEffect.None:       return renderDoc;
             case PresEffect.Zoom:       return <Zoom {...effectProps}>{renderDoc}</Zoom>;
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index b58a9affb..10897b48f 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -15,16 +15,17 @@ import { FieldView, FieldViewProps } from './FieldView';
 import BigText from './LabelBigText';
 import './LabelBox.scss';
 
-
 export interface LabelBoxProps {
     label?: string;
 }
 
 @observer
-export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxProps)>() {
-    public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LabelBox, fieldKey); }
+export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProps>() {
+    public static LayoutString(fieldKey: string) {
+        return FieldView.LayoutString(LabelBox, fieldKey);
+    }
     public static LayoutStringWithTitle(fieldStr: string, label?: string) {
-        return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`;  //e.g., "<ImageBox {...props} fieldKey={"data} />"
+        return !label ? LabelBox.LayoutString(fieldStr) : `<LabelBox fieldKey={'${fieldStr}'} label={'${label}'} {...props} />`; //e.g., "<ImageBox {...props} fieldKey={"data} />"
     }
     private dropDisposer?: DragManager.DragDropDisposer;
     private _timeout: any;
@@ -35,11 +36,12 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
         this._timeout && clearTimeout(this._timeout);
     }
 
+    getAnchor = () => {
+        return this.rootDoc;
+    };
+
     getTitle() {
-        return this.rootDoc["title-custom"] ? StrCast(this.rootDoc.title) :
-            this.props.label ? this.props.label :
-                typeof this.rootDoc[this.fieldKey] === "string" ? StrCast(this.rootDoc[this.fieldKey]) :
-                    StrCast(this.rootDoc.title);
+        return this.rootDoc['title-custom'] ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title);
     }
 
     protected createDropTarget = (ele: HTMLDivElement) => {
@@ -47,36 +49,42 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
         if (ele) {
             this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document);
         }
-    }
+    };
 
-    get paramsDoc() { return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc; }
+    get paramsDoc() {
+        return Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? this.dataDoc : this.layoutDoc;
+    }
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
-        !Doc.noviceMode && funcs.push({
-            description: "Clear Script Params", event: () => {
-                const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
-                params?.map(p => this.paramsDoc[p] = undefined);
-            }, icon: "trash"
-        });
+        !Doc.noviceMode &&
+            funcs.push({
+                description: 'Clear Script Params',
+                event: () => {
+                    const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
+                    params?.map(p => (this.paramsDoc[p] = undefined));
+                },
+                icon: 'trash',
+            });
 
-        funcs.length && ContextMenu.Instance.addItem({ description: "OnClick...", noexpand: true, subitems: funcs, icon: "mouse-pointer" });
-    }
+        funcs.length && ContextMenu.Instance.addItem({ description: 'OnClick...', noexpand: true, subitems: funcs, icon: 'mouse-pointer' });
+    };
 
     @undoBatch
     @action
     drop = (e: Event, de: DragManager.DropEvent) => {
         const docDragData = de.complete.docDragData;
-        const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
+        const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
         const missingParams = params?.filter(p => !this.paramsDoc[p]);
         if (docDragData && missingParams?.includes((e.target as any).textContent)) {
-            this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) =>
-                d.onDragStart ? docDragData.draggedDocuments[i] : d));
+            this.paramsDoc[(e.target as any).textContent] = new List<Doc>(docDragData.droppedDocuments.map((d, i) => (d.onDragStart ? docDragData.draggedDocuments[i] : d)));
             e.stopPropagation();
         }
-    }
+    };
 
     @observable _mouseOver = false;
-    @computed get hoverColor() { return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : "unset"; }
+    @computed get hoverColor() {
+        return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset';
+    }
 
     fitTextToBox = (r: any): any => {
         const singleLine = BoolCast(this.rootDoc._singleLine, true);
@@ -85,63 +93,73 @@ export class LabelBox extends ViewBoxBaseComponent<(FieldViewProps & LabelBoxPro
             fontSizeFactor: 1,
             minimumFontSize: NumCast(this.rootDoc._minFontSize, 8),
             maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000),
-            limitingDimension: "both",
-            horizontalAlign: "center",
-            verticalAlign: "center",
-            textAlign: "center",
+            limitingDimension: 'both',
+            horizontalAlign: 'center',
+            verticalAlign: 'center',
+            textAlign: 'center',
             singleLine,
-            whiteSpace: singleLine ? "nowrap" : "pre-wrap"
+            whiteSpace: singleLine ? 'nowrap' : 'pre-wrap',
         };
         this._timeout = undefined;
         if (!r) return params;
-        if (!r.offsetHeight || !r.offsetWidth) return this._timeout = setTimeout(() => this.fitTextToBox(r));
+        if (!r.offsetHeight || !r.offsetWidth) return (this._timeout = setTimeout(() => this.fitTextToBox(r)));
         const parent = r.parentNode;
         const parentStyle = parent.style;
-        parentStyle.display = "";
-        parentStyle.alignItems = "";
-        r.setAttribute("style", "");
-        r.style.width = singleLine ? "" : "100%";
+        parentStyle.display = '';
+        parentStyle.alignItems = '';
+        r.setAttribute('style', '');
+        r.style.width = singleLine ? '' : '100%';
 
-        r.style.textOverflow = "ellipsis";
-        r.style.overflow = "hidden";
+        r.style.textOverflow = 'ellipsis';
+        r.style.overflow = 'hidden';
         BigText(r, params);
         return params;
-    }
+    };
     // (!missingParams || !missingParams.length ? "" : "(" + missingParams.map(m => m + ":").join(" ") + ")")
     render() {
-        const boxParams = this.fitTextToBox(null);// this causes mobx to trigger re-render when data changes
-        const params = Cast(this.paramsDoc["onClick-paramFieldKeys"], listSpec("string"), []);
+        const boxParams = this.fitTextToBox(null); // this causes mobx to trigger re-render when data changes
+        const params = Cast(this.paramsDoc['onClick-paramFieldKeys'], listSpec('string'), []);
         const missingParams = params?.filter(p => !this.paramsDoc[p]);
         params?.map(p => DocListCast(this.paramsDoc[p])); // bcz: really hacky form of prefetching ...
         const label = this.getTitle();
         return (
-            <div className="labelBox-outerDiv"
-                onMouseLeave={action(() => this._mouseOver = false)}
-                onMouseOver={action(() => this._mouseOver = true)}
-                ref={this.createDropTarget} onContextMenu={this.specificContextMenu}
+            <div
+                className="labelBox-outerDiv"
+                onMouseLeave={action(() => (this._mouseOver = false))}
+                onMouseOver={action(() => (this._mouseOver = true))}
+                ref={this.createDropTarget}
+                onContextMenu={this.specificContextMenu}
                 style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}>
-                <div className="labelBox-mainButton" style={{
-                    backgroundColor: this.hoverColor,
-                    fontSize: StrCast(this.layoutDoc._fontSize),
-                    fontFamily: StrCast(this.layoutDoc._fontFamily) || "inherit",
-                    letterSpacing: StrCast(this.layoutDoc.letterSpacing),
-                    textTransform: StrCast(this.layoutDoc.textTransform) as any,
-                    paddingLeft: NumCast(this.rootDoc._xPadding),
-                    paddingRight: NumCast(this.rootDoc._xPadding),
-                    paddingTop: NumCast(this.rootDoc._yPadding),
-                    paddingBottom: NumCast(this.rootDoc._yPadding),
-                    width: this.props.PanelWidth(),
-                    height: this.props.PanelHeight(),
-                    whiteSpace: boxParams.singleLine ? "pre" : "pre-wrap"
-                }} >
-                    <span style={{ width: boxParams.singleLine ? "" : "100%" }} ref={action((r: any) => this.fitTextToBox(r))}>
-                        {label.startsWith("#") ? (null) : label.replace(/([^a-zA-Z])/g, "$1\u200b")}
+                <div
+                    className="labelBox-mainButton"
+                    style={{
+                        backgroundColor: this.hoverColor,
+                        fontSize: StrCast(this.layoutDoc._fontSize),
+                        fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit',
+                        letterSpacing: StrCast(this.layoutDoc.letterSpacing),
+                        textTransform: StrCast(this.layoutDoc.textTransform) as any,
+                        paddingLeft: NumCast(this.rootDoc._xPadding),
+                        paddingRight: NumCast(this.rootDoc._xPadding),
+                        paddingTop: NumCast(this.rootDoc._yPadding),
+                        paddingBottom: NumCast(this.rootDoc._yPadding),
+                        width: this.props.PanelWidth(),
+                        height: this.props.PanelHeight(),
+                        whiteSpace: boxParams.singleLine ? 'pre' : 'pre-wrap',
+                    }}>
+                    <span style={{ width: boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}>
+                        {label.startsWith('#') ? null : label.replace(/([^a-zA-Z])/g, '$1\u200b')}
                     </span>
                 </div>
-                <div className="labelBox-fieldKeyParams" >
-                    {!missingParams?.length ? (null) : missingParams.map(m => <div key={m} className="labelBox-missingParam">{m}</div>)}
+                <div className="labelBox-fieldKeyParams">
+                    {!missingParams?.length
+                        ? null
+                        : missingParams.map(m => (
+                              <div key={m} className="labelBox-missingParam">
+                                  {m}
+                              </div>
+                          ))}
                 </div>
             </div>
         );
     }
-}
\ No newline at end of file
+}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 64b186489..a3e83f047 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -241,7 +241,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
                 const durationSecStr = viewTrans.match(/([0-9.]*)s/);
                 const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
-                this.goTo(scrollTop, duration);
+                this.goTo(scrollTop, duration, 'ease');
             },
             { fireImmediately: true }
         );
@@ -295,7 +295,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             const scrollTo = Utils.scrollIntoView(NumCast(doc.y), doc[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(doc.y) + doc[HeightSym](), this.getScrollHeight()));
             if (scrollTo !== undefined && this._initialScroll === undefined) {
                 const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
-                this.goTo(scrollTo, focusSpeed);
+                this.goTo(scrollTo, focusSpeed, options.easeFunc);
                 return focusSpeed;
             } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
                 this._initialScroll = scrollTo;
@@ -497,11 +497,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         );
     };
 
-    goTo = (scrollTop: number, duration: number) => {
+    goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => {
         if (this._outerRef.current) {
             const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight());
             if (duration) {
-                smoothScroll(duration, [this._outerRef.current], scrollTop);
+                smoothScroll(duration, [this._outerRef.current], scrollTop, easeFunc);
                 this.setDashScrollTop(scrollTop, duration);
             } else {
                 this.setDashScrollTop(scrollTop);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 9e91f6c46..b895043de 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1120,7 +1120,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     const durationSecStr = viewTrans.match(/([0-9.]*)s/);
                     const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
                     if (duration) {
-                        smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0));
+                        this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper);
                     } else {
                         this._scrollRef.current.scrollTo({ top: pos });
                     }
@@ -1284,6 +1284,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         });
     }
     _didScroll = false;
+    _scrollStopper: undefined | (() => void);
     setupEditor(config: any, fieldKey: string) {
         const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]);
         const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
@@ -1302,7 +1303,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
                         const scrollPos = scrollRef.scrollTop + shift * self.props.ScreenToLocalTransform().Scale;
                         if (this._focusSpeed !== undefined) {
-                            scrollPos && smoothScroll(this._focusSpeed, scrollRef, scrollPos);
+                            scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper));
                         } else {
                             scrollRef.scrollTo({ top: scrollPos });
                         }
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index e0f64e7aa..39328ae4a 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -40,6 +40,7 @@ const { Howl } = require('howler');
 export interface PinProps {
     audioRange?: boolean;
     activeFrame?: number;
+    currentFrame?: number;
     hidePresBox?: boolean;
     pinViewport?: MarqueeViewBounds; // pin a specific viewport on a freeform view (use MarqueeView.CurViewBounds to compute if no region has been selected)
     pinDocLayout?: boolean; // pin layout info (width/height/x/y)
@@ -276,14 +277,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
             const targetDoc: Doc = this.targetDoc;
-            if (activeItem.presActiveFrame !== undefined) {
+            const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
+            if (activeFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
-                const context = DocCast(DocCast(activeItem.presentationTargetDoc).context);
+                const context = activeItem.presActiveFrame ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
                 if (context) {
                     const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
                     if (ffview) {
                         this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
-                        context._currentFrame = NumCast(activeItem.presActiveFrame);
+                        context._currentFrame = NumCast(activeFrame);
                     }
                 }
             }
@@ -547,11 +549,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 LightboxView.SetLightboxDoc(undefined);
                 const options: DocFocusOptions = {
                     willPan: activeItem.presMovement !== PresMovement.None,
-                    willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump,
-                    zoomScale: NumCast(activeItem.presZoom, 1),
+                    willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
+                    zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1),
                     zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
                     noSelect: true,
                     originatingDoc: activeItem,
+                    easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
                 };
 
                 var containerDocContext = srcContext ? [srcContext] : [];
@@ -747,8 +750,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     });
 
     movementName = action((activeItem: Doc) => {
-        if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
-            activeItem.presMovement = PresMovement.Zoom;
+        if (![PresMovement.Zoom, PresMovement.Pan, PresMovement.Center, PresMovement.Jump, PresMovement.None].includes(StrCast(activeItem.presMovement) as any)) {
+            return PresMovement.Zoom;
         }
         return StrCast(activeItem.presMovement);
     });
@@ -890,7 +893,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         else this.regularSelect(doc, ref, drag, focus);
     };
 
-    static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance.keyEvents(e);
+    static keyEventsWrapper = (e: KeyboardEvent) => PresBox.Instance?.keyEvents(e);
 
     // Key for when the presentaiton is active
     @action
@@ -1097,7 +1100,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         let timeInMS = Number(number) * 1000;
         if (change) timeInMS += change;
         if (timeInMS < 100) timeInMS = 100;
-        if (timeInMS > 10000) timeInMS = 10000;
+        if (timeInMS > 100000) timeInMS = 100000;
         setter(timeInMS);
     };
     setTransitionTime = (number: String, change?: number) => {
@@ -1155,6 +1158,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         activeItem.openDocument = !activeItem.openDocument;
         this.selectedArray.forEach(doc => (doc.openDocument = activeItem.openDocument));
     };
+    @undoBatch
+    @action
+    updateEaseFunc = (activeItem: Doc) => {
+        activeItem.presEaseFunc = activeItem.presEaseFunc === 'linear' ? 'ease' : 'linear';
+        this.selectedArray.forEach(doc => (doc.presEaseFunc = activeItem.presEaseFunc));
+    };
 
     @undoBatch
     @action
@@ -1223,7 +1232,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
             if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
             const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
-            activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
+            // activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
             return (
                 <div
                     className={`presBox-ribbon ${this._transitionTools && this.layoutDoc.presStatus === PresStatus.Edit ? 'active' : ''}`}
@@ -1247,6 +1256,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                             <FontAwesomeIcon className="presBox-dropdownIcon" style={{ gridColumn: 2, color: this._openMovementDropdown ? Colors.MEDIUM_BLUE : 'black' }} icon={'angle-down'} />
                             <div className={'presBox-dropdownOptions'} id={'presBoxMovementDropdown'} onPointerDown={StopEvent} style={{ display: this._openMovementDropdown ? 'grid' : 'none' }}>
                                 {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.None)}
+                                {presMovement(PresMovement.Center)}
                                 {presMovement(PresMovement.Zoom)}
                                 {presMovement(PresMovement.Pan)}
                                 {isPresCollection || (isPresCollection && isPinWithView) ? null : presMovement(PresMovement.Jump)}
@@ -1281,7 +1291,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 </div>
                             </div>
                         </div>
-                        {PresBox.inputter('0.1', '0.1', '10', transitionSpeed, true, this.setTransitionTime)}
+                        {PresBox.inputter('0.1', '0.1', '100', transitionSpeed, true, this.setTransitionTime)}
                         <div className={'slider-headers'}>
                             <div className="slider-text">Fast</div>
                             <div className="slider-text">Medium</div>
@@ -1317,6 +1327,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     Lightbox
                                 </div>
                             </Tooltip>
+                            <Tooltip title={<div className="dash-tooltip">{'Transition movement style'}</div>}>
+                                <div className="ribbon-toggle" onClick={() => this.updateEaseFunc(activeItem)}>
+                                    {`${StrCast(activeItem.presEaseFunc, 'ease')}`}
+                                </div>
+                            </Tooltip>
                         </div>
                         {type === DocumentType.AUDIO || type === DocumentType.VID ? null : (
                             <>
diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts
index 034f7588b..8c8b83fbf 100644
--- a/src/client/views/nodes/trails/PresEnums.ts
+++ b/src/client/views/nodes/trails/PresEnums.ts
@@ -1,6 +1,7 @@
 export enum PresMovement {
     Zoom = 'zoom',
     Pan = 'pan',
+    Center = 'center',
     Jump = 'jump',
     None = 'none',
 }
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index ee2ae10a7..265328036 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -210,14 +210,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
                               </button>
                           </Tooltip>
                       ),
-                      //NOTE: link popup is currently in progress
                       <Tooltip key="link" title={<div className="dash-tooltip">{'Find document to link to selected text'}</div>}>
                           <button className="antimodeMenu-button link" onPointerDown={this.toggleLinkPopup} style={{}}>
                               <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(1.5)' }} icon={'search'} size="lg" />
                               <FontAwesomeIcon style={{ position: 'absolute', transform: 'scale(0.5)', transformOrigin: 'top left', top: 12, left: 12 }} icon={'link'} size="lg" />
                           </button>
                       </Tooltip>,
-                      <LinkPopup key="popup" showPopup={this._showLinkPopup} linkFrom={this.onMakeAnchor} />,
+                      <LinkPopup key="popup" showPopup={this._showLinkPopup} linkCreateAnchor={this.onMakeAnchor} />,
                       AnchorMenu.Instance.StartCropDrag === unimplementedFunction ? (
                           <></>
                       ) : (
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 0703ca9b4..906dff377 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -76,7 +76,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _lastSearch = false;
     private _viewerIsSetup = false;
     private _ignoreScroll = false;
-    private _initialScroll: Opt<number>;
+    private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined;
     private _forcedScroll = true;
 
     selectionText = () => this._selectionText;
@@ -164,6 +164,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
     };
 
+    _scrollStopper: undefined | (() => void);
+
     // scrolls to focus on a nested annotation document.  if this is part a link preview then it will jump to the scroll location,
     // otherwise it will scroll smoothly.
     scrollFocus = (doc: Doc, scrollTop: number, options: DocFocusOptions) => {
@@ -173,12 +175,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
             const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, NumCast(this.props.Document.scrollHeight));
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
-                if (!this._pdfViewer) this._initialScroll = scrollTo;
-                else if (!options.instant) smoothScroll((focusSpeed = options.zoomTime??500), mainCont, scrollTo);
+                if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
+                else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
                 else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) });
             }
         } else {
-            this._initialScroll = NumCast(this.props.layoutDoc._scrollTop);
+            this._initialScroll = { loc: NumCast(this.props.layoutDoc._scrollTop), easeFunc: options.easeFunc };
         }
         return focusSpeed;
     };
@@ -202,7 +204,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             this.gotoPage(NumCast(this.props.Document._curPage, 1));
         }
         document.removeEventListener('pagesinit', this.pagesinit);
-        var quickScroll: string | undefined = this._initialScroll ? this._initialScroll.toString() : '';
+        var quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined };
         this._disposers.scale = reaction(
             () => NumCast(this.props.layoutDoc._viewScale, 1),
             scale => (this._pdfViewer.currentScaleValue = scale),
@@ -213,7 +215,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
             pos => {
                 if (!this._ignoreScroll) {
                     this._showWaiting && this.setupPdfJsViewer();
-                    const viewTrans = quickScroll ?? StrCast(this.props.Document._viewTransition);
+                    const viewTrans = quickScroll?.loc ?? StrCast(this.props.Document._viewTransition);
                     const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
                     const durationSecStr = viewTrans.match(/([0-9.]*)s/);
                     const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
@@ -221,7 +223,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     if (duration) {
                         setTimeout(
                             () => {
-                                this._mainCont.current && smoothScroll(duration, this._mainCont.current, pos);
+                                this._mainCont.current && (this._scrollStopper = smoothScroll(duration, this._mainCont.current, pos, this._initialScroll?.easeFunc ?? 'ease', this._scrollStopper));
                                 setTimeout(() => (this._forcedScroll = false), duration);
                             },
                             this._mainCont.current ? 0 : 250
@@ -236,7 +238,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         );
         quickScroll = undefined;
         if (this._initialScroll !== undefined && this._mainCont.current) {
-            this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll || 0) });
+            this._mainCont.current?.scrollTo({ top: Math.abs(this._initialScroll?.loc || 0) });
             this._initialScroll = undefined;
         }
     };
@@ -295,7 +297,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     @action
     scrollToAnnotation = (scrollToAnnotation: Doc) => {
         if (scrollToAnnotation) {
-            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), {zoomTime: 500});
+            this.scrollFocus(scrollToAnnotation, NumCast(scrollToAnnotation.y), { zoomTime: 500 });
             Doc.linkFollowHighlight(scrollToAnnotation);
         }
     };
@@ -452,6 +454,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
     };
 
     onClick = (e: React.MouseEvent) => {
+        this._scrollStopper?.();
         if (this._setPreviewCursor && e.button === 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             this._setPreviewCursor(e.clientX, e.clientY, false, false);
         }
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index b78c8654f..aac488559 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -4,10 +4,12 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { StrCast } from '../../../fields/Types';
+import { DocCast, StrCast } from '../../../fields/Types';
+import { StopEvent } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
+import { LinkManager } from '../../util/LinkManager';
 import { CollectionDockingView } from '../collections/CollectionDockingView';
 import { ViewBoxBaseComponent } from '../DocComponent';
 import { FieldView, FieldViewProps } from '../nodes/FieldView';
@@ -20,6 +22,8 @@ const ERROR = 0.03;
 export interface SearchBoxProps extends FieldViewProps {
     linkSearch: boolean;
     linkFrom?: (() => Doc | undefined) | undefined;
+    linkCreateAnchor?: () => Doc | undefined;
+    linkCreated?: (link: Doc) => void;
 }
 
 /**
@@ -111,10 +115,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
 
     // TODO: nda -- Change this method to change what happens when you click on the item.
     makeLink = action((linkTo: Doc) => {
-        if (this.props.linkFrom) {
-            const linkFrom = this.props.linkFrom();
+        if (this.props.linkCreateAnchor) {
+            const linkFrom = this.props.linkCreateAnchor();
             if (linkFrom) {
-                DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
+                const link = DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
+                link && this.props.linkCreated?.(link);
             }
         }
     });
@@ -380,7 +385,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
 
         const query = StrCast(this._searchString);
         Doc.SetSearchQuery(query);
-        Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true));
+        if (!this.props.linkSearch) Array.from(this._results.keys()).forEach(doc => DocumentManager.Instance.getFirstDocumentView(doc)?.ComponentView?.search?.(this._searchString, undefined, true));
         this._results.clear();
 
         if (query) {
@@ -437,6 +442,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
 
         const resultsJSX = Array();
 
+        const fromDoc = this.props.linkFrom?.();
+
         sortedResults.forEach(result => {
             var className = 'searchBox-results-scroll-view-result';
 
@@ -460,6 +467,13 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                                           e.stopPropagation();
                                       }
                             }
+                            style={{
+                                fontWeight: DocListCast(fromDoc?.links).find(
+                                    link => Doc.AreProtosEqual(LinkManager.getOppositeAnchor(link, fromDoc!), result[0] as Doc) || Doc.AreProtosEqual(DocCast(LinkManager.getOppositeAnchor(link, fromDoc!)?.annotationOn), result[0] as Doc)
+                                )
+                                    ? 'bold'
+                                    : '',
+                            }}
                             className={className}>
                             <div className="searchBox-result-title">{title as string}</div>
                             <div className="searchBox-result-type">{formattedType}</div>
@@ -482,7 +496,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                         defaultValue={''}
                         autoComplete="off"
                         onChange={this.onInputChange}
-                        onKeyPress={e => (e.key === 'Enter' ? this.submitSearch() : null)}
+                        onKeyPress={e => {
+                            e.key === 'Enter' ? this.submitSearch() : null;
+                            e.stopPropagation();
+                        }}
                         type="text"
                         placeholder="Search..."
                         id="search-input"
diff --git a/src/fields/util.ts b/src/fields/util.ts
index f222e4555..7f4892bd6 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -449,7 +449,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                     diff?.op === '$addToSet'
                         ? {
                               redo: () => {
-                                  receiver[prop].push(...diff.items.map((item: any) => (item.value ? item.value() : item)));
+                                  receiver[prop].push(...diff.items.map((item: any) => item.value ?? item));
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
                               },
                               undo: action(() => {
@@ -459,7 +459,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                           const ind = receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
                                           ind !== -1 && receiver[prop].splice(ind, 1);
                                       } else {
-                                          const ind = receiver[prop].indexOf(item.value ? item.value() : item);
+                                          const ind = receiver[prop].indexOf(item.value ?? item);
                                           ind !== -1 && receiver[prop].splice(ind, 1);
                                       }
                                   });
@@ -471,7 +471,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                         ? {
                               redo: action(() => {
                                   diff.items.forEach((item: any) => {
-                                      const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ? item.value() : item);
+                                      const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item);
                                       ind !== -1 && receiver[prop].splice(ind, 1);
                                   });
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
@@ -483,8 +483,8 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any
                                           const ind = (prevValue as List<any>).findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading);
                                           ind !== -1 && receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) === -1 && receiver[prop].splice(ind, 0, item);
                                       } else {
-                                          const ind = (prevValue as List<any>).indexOf(item.value ? item.value() : item);
-                                          ind !== -1 && receiver[prop].indexOf(item.value ? item.value() : item) === -1 && receiver[prop].splice(ind, 0, item);
+                                          const ind = (prevValue as List<any>).indexOf(item.value ?? item);
+                                          ind !== -1 && receiver[prop].indexOf(item.value ?? item) === -1 && receiver[prop].splice(ind, 0, item);
                                       }
                                   });
                                   lastValue = ObjectField.MakeCopy(receiver[prop]);
-- 
cgit v1.2.3-70-g09d2


From e373e66f8ed06f4501e00af8348f15ad113c7424 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 21 Dec 2022 11:11:39 -0500
Subject: cleaning up ACLs for performance and clarity

---
 src/client/documents/Documents.ts                  |   6 +-
 src/client/util/CurrentUserUtils.ts                |   1 -
 src/client/util/LinkManager.ts                     |   8 +-
 src/client/util/SharingManager.tsx                 |  49 ++++-----
 src/client/views/InkingStroke.tsx                  |   1 -
 src/client/views/PropertiesView.tsx                |  33 +++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |   3 -
 .../nodes/formattedText/FormattedTextBox.scss      |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          |   4 +-
 src/client/views/nodes/trails/PresEnums.ts         |   1 +
 src/fields/Doc.ts                                  |  42 +++++---
 src/fields/List.ts                                 |   5 +-
 src/fields/util.ts                                 | 110 ++++++++-------------
 13 files changed, 113 insertions(+), 152 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 634b14822..39dde6fb4 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -799,7 +799,7 @@ export namespace Docs {
             const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_"
             const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_');
 
-            dataProps['acl-Override'] = 'None';
+            // dataProps['acl-Override'] = SharingPermissions.Unset;
             dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
 
             dataProps.system = viewProps.system;
@@ -825,7 +825,7 @@ export namespace Docs {
 
             const viewFirstProps: { [id: string]: any } = {};
             viewFirstProps['acl-Public'] = options['_acl-Public'] ? options['_acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
-            viewFirstProps['acl-Override'] = 'None';
+            // viewFirstProps['acl-Override'] = SharingPermissions.Unset;
             viewFirstProps.author = Doc.CurrentUserEmail;
             let viewDoc: Doc;
             // determines whether viewDoc should be created using placeholder Doc or default
@@ -995,7 +995,7 @@ export namespace Docs {
             I.data = new InkField(points);
             I.creationDate = new DateField();
             I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
-            I['acl-Override'] = 'None';
+            //I['acl-Override'] = SharingPermissions.Unset;
             I.links = ComputedField.MakeFunction('links(self)');
             I[Initializing] = false;
             return I;
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5549769aa..afa9e7de3 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -601,7 +601,6 @@ export class CurrentUserUtils {
         };
         reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
         reaction(() => UndoManager.undoStack.slice(), () => {
-            console.log(UndoManager.undoStack)
             Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4;
         }, { fireImmediately: true });
         return DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), dockBtnsReqdOpts, btns);
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 588664dec..2b0ce1d3d 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -44,7 +44,6 @@ export class LinkManager {
                         if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
                             Doc.GetProto(a1)[DirectLinksSym].add(link);
                             Doc.GetProto(a2)[DirectLinksSym].add(link);
-                            //Doc.GetProto(link)[DirectLinksSym].add(link); // bcz: links are not linked to themself, so this was a hack
                         }
                     })
                 );
@@ -147,12 +146,7 @@ export class LinkManager {
         return this.relatedLinker(anchor);
     } // finds all links that contain the given anchor
     public getAllDirectLinks(anchor: Doc): Doc[] {
-        // FIXME:glr Why is Doc undefined?
-        if (Doc.GetProto(anchor)[DirectLinksSym]) {
-            return Array.from(Doc.GetProto(anchor)[DirectLinksSym]);
-        } else {
-            return [];
-        }
+        return Array.from(Doc.GetProto(anchor)[DirectLinksSym] ?? []);
     } // finds all links that contain the given anchor
 
     relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] {
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 6cbc76014..54ee691bc 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -5,7 +5,8 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import Select from 'react-select';
 import * as RequestPromise from 'request-promise';
-import { AclAugment, AclAdmin, AclEdit, AclPrivate, AclReadonly, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, Opt, AclSelfEdit } from '../../fields/Doc';
+import { AclAdmin, AclPrivate, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, HierarchyMapping, Opt } from '../../fields/Doc';
+import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { Cast, NumCast, StrCast } from '../../fields/Types';
 import { distributeAcls, GetEffectiveAcl, normalizeEmail, SharingPermissions, TraceMobx } from '../../fields/util';
@@ -20,10 +21,9 @@ import { SearchBox } from '../views/search/SearchBox';
 import { DocumentManager } from './DocumentManager';
 import { GroupManager, UserOptions } from './GroupManager';
 import { GroupMemberView } from './GroupMemberView';
+import { LinkManager } from './LinkManager';
 import { SelectionManager } from './SelectionManager';
 import './SharingManager.scss';
-import { LinkManager } from './LinkManager';
-import { Id } from '../../fields/FieldSymbols';
 
 export interface User {
     email: string;
@@ -82,16 +82,6 @@ export class SharingManager extends React.Component<{}> {
     @observable private layoutDocAcls: boolean = false; // whether the layout doc or data doc's acls are to be used
     @observable private myDocAcls: boolean = false; // whether the My Docs checkbox is selected or not
 
-    // maps acl symbols to SharingPermissions
-    private AclMap = new Map<symbol, string>([
-        [AclPrivate, SharingPermissions.None],
-        [AclReadonly, SharingPermissions.View],
-        [AclAugment, SharingPermissions.Augment],
-        [AclSelfEdit, SharingPermissions.SelfEdit],
-        [AclEdit, SharingPermissions.Edit],
-        [AclAdmin, SharingPermissions.Admin],
-    ]);
-
     // private get linkVisible() {
     //     return this.targetDoc ? this.targetDoc["acl-" + PublicKey] !== SharingPermissions.None : false;
     // }
@@ -184,6 +174,7 @@ export class SharingManager extends React.Component<{}> {
 
         const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
         return !docs
+            .map(doc => (this.layoutDocAcls ? doc : doc[DataSym]))
             .map(doc => {
                 doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
 
@@ -217,6 +208,7 @@ export class SharingManager extends React.Component<{}> {
 
         // ! ensures it returns true if document has been shared successfully, false otherwise
         return !docs
+            .map(doc => (this.layoutDocAcls ? doc : doc[DataSym]))
             .map(doc => {
                 doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard);
 
@@ -283,6 +275,7 @@ export class SharingManager extends React.Component<{}> {
             docs.forEach(doc => {
                 const isDashboard = dashboards.indexOf(doc) !== -1;
                 if (GetEffectiveAcl(doc) === AclAdmin) distributeAcls(`acl-${shareWith}`, permission, doc, undefined, undefined, isDashboard);
+                this.setDashboardBackground(doc, permission as SharingPermissions);
             });
         }
     };
@@ -290,14 +283,14 @@ export class SharingManager extends React.Component<{}> {
     /**
      * Sets the background of the Dashboard if it has been shared as a visual indicator
      */
-    setDashboardBackground = async (doc: Doc, permission: SharingPermissions) => {
+    setDashboardBackground = (doc: Doc, permission: SharingPermissions) => {
         if (Doc.IndexOf(doc, DocListCast(Doc.MyDashboards.data)) !== -1) {
             if (permission !== SharingPermissions.None) {
                 doc.isShared = true;
                 doc.backgroundColor = 'green';
             } else {
                 const acls = doc[DataSym][AclSym];
-                if (Object.keys(acls).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` ? true : [AclUnset, AclPrivate].includes(acls[key])))) {
+                if (Object.keys(acls).every(key => (key === `acl-${Doc.CurrentUserEmailNormalized}` || key === 'acl-Me' ? true : [AclUnset, AclPrivate].includes(acls[key])))) {
                     doc.isShared = undefined;
                     doc.backgroundColor = undefined;
                 }
@@ -372,7 +365,7 @@ export class SharingManager extends React.Component<{}> {
     private sharingOptions(uniform: boolean, override?: boolean) {
         const dropdownValues: string[] = Object.values(SharingPermissions);
         if (!uniform) dropdownValues.unshift('-multiple-');
-        if (override) dropdownValues.unshift('None');
+        if (!override) dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
         return dropdownValues
             .filter(permission => !Doc.noviceMode || ![SharingPermissions.SelfEdit].includes(permission as any))
             .map(permission => (
@@ -458,17 +451,6 @@ export class SharingManager extends React.Component<{}> {
         }
     };
 
-    // distributeOverCollection = (targetDoc?: Doc) => {
-    //     const target = targetDoc || this.targetDoc!;
-
-    //     const docs = SelectionManager.Views().length < 2 ? [target] : SelectionManager.Views().map(docView => docView.props.Document);
-    //     docs.forEach(doc => {
-    //         for (const [key, value] of Object.entries(doc[AclSym])) {
-    //             distributeAcls(key, this.AclMap.get(value)! as SharingPermissions, target);
-    //         }
-    //     });
-    // }
-
     /**
      * Sorting algorithm to sort users.
      */
@@ -491,6 +473,7 @@ export class SharingManager extends React.Component<{}> {
      * @returns the main interface of the SharingManager.
      */
     @computed get sharingInterface() {
+        if (!this.targetDoc) return null;
         TraceMobx();
         const groupList = GroupManager.Instance?.allGroups || [];
         const sortedUsers = this.users
@@ -523,21 +506,21 @@ export class SharingManager extends React.Component<{}> {
             docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin);
         }
 
-        const targetDoc = docs[0];
+        const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DataSym];
 
         // tslint:disable-next-line: no-unnecessary-callback-wrapper
         const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc));
         const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin);
 
         // users in common between all docs
-        const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym]?.[AclSym] && Object.keys(doc[DataSym][AclSym]))));
+        const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).map(doc => doc?.[AclSym] && Object.keys(doc[AclSym])));
 
         // the list of users shared with
         const userListContents: (JSX.Element | null)[] = users
             .filter(({ user }) => (docs.length > 1 ? commonKeys.includes(`acl-${normalizeEmail(user.email)}`) : docs[0]?.author !== user.email))
             .map(({ user, linkDatabase, sharingDoc, userColor }) => {
                 const userKey = `acl-${normalizeEmail(user.email)}`;
-                const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey] : doc?.[DataSym]?.[AclSym]?.[userKey] === docs[0]?.[DataSym]?.[AclSym]?.[userKey]));
+                const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]);
                 const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-';
 
                 return !permissions ? null : (
@@ -573,7 +556,7 @@ export class SharingManager extends React.Component<{}> {
                 <div key={'me'} className={'container'}>
                     <span className={'padding'}>Me</span>
                     <div className="edit-actions">
-                        <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? this.AclMap.get(effectiveAcls[0])! : '-multiple-'}</div>
+                        <div className={'permissions-dropdown'}>{effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-'}</div>
                     </div>
                 </div>
             ) : null
@@ -584,7 +567,9 @@ export class SharingManager extends React.Component<{}> {
         groupListMap.unshift({ title: 'Public' }); //, { title: "ALL" });
         const groupListContents = groupListMap.map(group => {
             const groupKey = `acl-${StrCast(group.title)}`;
-            const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]));
+            const uniform = docs
+                .map(doc => (this.layoutDocAcls ? doc : doc[DataSym]))
+                .every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey]));
             const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-';
 
             return !permissions ? null : (
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 36ea3ef18..a6fa2f04b 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -79,7 +79,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
     screenToLocal = () => this.props.ScreenToLocalTransform().scale(this.props.NativeDimScaling?.() || 1);
 
     getAnchor = () => {
-        console.log(document.activeElement);
         return this._subContentView?.getAnchor?.() || this.rootDoc;
     };
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 92c5708aa..8d495d286 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -7,7 +7,7 @@ import { intersection } from 'lodash';
 import { action, autorun, computed, Lambda, observable } 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';
+import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
@@ -364,7 +364,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
      */
     @undoBatch
     changePermissions = (e: any, user: string) => {
-        const docs = SelectionManager.Views().length < 2 ? (this.selectedDoc ? [this.selectedDoc] : []) : SelectionManager.Views().map(docView => docView.props.Document);
+        const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc : DocCast(doc)[DataSym]));
         SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs);
     };
 
@@ -374,7 +374,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     getPermissionsSelect(user: string, permission: string) {
         const dropdownValues: string[] = Object.values(SharingPermissions);
         if (permission === '-multiple-') dropdownValues.unshift(permission);
-        if (user === 'Override') dropdownValues.unshift('None');
+        if (user !== 'Override') dropdownValues.splice(dropdownValues.indexOf(SharingPermissions.Unset), 1);
         return (
             <select className="permissions-select" value={permission} onChange={e => this.changePermissions(e, user)}>
                 {dropdownValues
@@ -449,16 +449,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
      * @returns the sharing and permissions panel.
      */
     @computed get sharingTable() {
-        const AclMap = new Map<symbol, string>([
-            [AclUnset, 'None'],
-            [AclPrivate, SharingPermissions.None],
-            [AclReadonly, SharingPermissions.View],
-            [AclAugment, SharingPermissions.Augment],
-            [AclSelfEdit, SharingPermissions.SelfEdit],
-            [AclEdit, SharingPermissions.Edit],
-            [AclAdmin, SharingPermissions.Admin],
-        ]);
-
         // all selected docs
         const docs =
             SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym]));
@@ -470,7 +460,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         const showAdmin = effectiveAcls.every(acl => acl === AclAdmin);
 
         // users in common between all docs
-        const commonKeys: string[] = intersection(...docs.map(doc => (this.layoutDocAcls ? doc?.[AclSym] && Object.keys(doc[AclSym]) : doc?.[DataSym][AclSym] && Object.keys(doc[DataSym][AclSym]))));
+        const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me')));
 
         const tableEntries = [];
 
@@ -478,9 +468,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         if (commonKeys.length) {
             for (const key of commonKeys) {
                 const name = denormalizeEmail(key.substring(4));
-                const uniform = docs.every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key] : doc?.[DataSym]?.[AclSym]?.[key] === docs[0]?.[DataSym]?.[AclSym]?.[key]));
+                const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key]);
                 if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) {
-                    tableEntries.push(this.sharingItem(name, showAdmin, uniform ? AclMap.get(this.layoutDocAcls ? target[AclSym][key] : target[DataSym][AclSym][key])! : '-multiple-'));
+                    tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-'));
                 }
             }
         }
@@ -488,11 +478,16 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author);
         // shifts the current user, owner, public to the top of the doc.
         // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-"));
-        tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === docs[0]['acl-Public']) ? AclMap.get(target[AclSym]?.['acl-Public']) || SharingPermissions.None : '-multiple-'));
+        if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'));
+        tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-'));
         tableEntries.unshift(
-            this.sharingItem('Me', showAdmin, docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? AclMap.get(effectiveAcls[0])! : '-multiple-', !ownerSame)
+            this.sharingItem(
+                'Me',
+                showAdmin,
+                docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-',
+                !ownerSame
+            )
         );
-        if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner'));
 
         return <div className="propertiesView-sharingTable">{tableEntries}</div>;
     }
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 945614bcb..d59cec02d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -806,9 +806,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y },
                     });
                     const intersects = Array.from(new Set(rawIntersects as (number | string)[])); // convert to more manageable union array type
-                    if (intersects.length) {
-                        console.log();
-                    }
                     // return tuples of the inkingStroke intersected, and the t value of the intersection
                     intersections.push(...intersects.map(t => ({ inkView, t: +t + Math.floor(i / 4) }))); // convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
                 }
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 93dc0b2a1..cbe0a465d 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -15,7 +15,7 @@ audiotag {
     position: absolute;
     cursor: pointer;
     border-radius: 10px;
-    width: 10px;
+    width: 12px;
     margin-top: -2px;
     font-size: 4px;
     background: lightblue;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 39328ae4a..abaa3b373 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -1383,11 +1383,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                     {presEffect(PresEffect.Roll)}
                                 </div>
                             </div>
-                            <div className="ribbon-doubleButton" style={{ display: effect === 'None' ? 'none' : 'inline-flex' }}>
+                            <div className="ribbon-doubleButton" style={{ display: effect === PresEffectDirection.None ? 'none' : 'inline-flex' }}>
                                 <div className="presBox-subheading">Effect direction</div>
                                 <div className="ribbon-property">{StrCast(this.activeItem.presEffectDirection)}</div>
                             </div>
-                            <div className="effectDirection" style={{ display: effect === 'None' ? 'none' : 'grid', width: 40 }}>
+                            <div className="effectDirection" style={{ display: effect === PresEffectDirection.None ? 'none' : 'grid', width: 40 }}>
                                 {presDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
                                 {presDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
                                 {presDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
diff --git a/src/client/views/nodes/trails/PresEnums.ts b/src/client/views/nodes/trails/PresEnums.ts
index 8c8b83fbf..564829d54 100644
--- a/src/client/views/nodes/trails/PresEnums.ts
+++ b/src/client/views/nodes/trails/PresEnums.ts
@@ -23,6 +23,7 @@ export enum PresEffectDirection {
     Center = 'Enter from center',
     Top = 'Enter from Top',
     Bottom = 'Enter from bottom',
+    None = 'None',
 }
 
 export enum PresStatus {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 40152551e..51313a8df 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -113,28 +113,37 @@ export const Initializing = Symbol('Initializing');
 export const ForceServerWrite = Symbol('ForceServerWrite');
 export const CachedUpdates = Symbol('Cached updates');
 
-const AclMap = new Map<string, symbol>([
-    ['None', AclUnset],
-    [SharingPermissions.None, AclPrivate],
-    [SharingPermissions.View, AclReadonly],
-    [SharingPermissions.Augment, AclAugment],
-    [SharingPermissions.SelfEdit, AclSelfEdit],
-    [SharingPermissions.Edit, AclEdit],
-    [SharingPermissions.Admin, AclAdmin],
+export enum aclLevel {
+    unset = -1,
+    unshared = 0,
+    viewable = 1,
+    augmentable = 2,
+    selfEditable = 2.5,
+    editable = 3,
+    admin = 4,
+}
+// prettier-ignore
+export const HierarchyMapping: Map<symbol, { level:aclLevel; name: SharingPermissions }> = new Map([
+    [AclPrivate,  { level: aclLevel.unshared,     name: SharingPermissions.None }],
+    [AclReadonly, { level: aclLevel.viewable,     name: SharingPermissions.View }],
+    [AclAugment,  { level: aclLevel.augmentable,  name: SharingPermissions.Augment}],
+    [AclSelfEdit, { level: aclLevel.selfEditable, name: SharingPermissions.SelfEdit }],
+    [AclEdit,     { level: aclLevel.editable,     name: SharingPermissions.Edit }],
+    [AclAdmin,    { level: aclLevel.admin,        name: SharingPermissions.Admin }],
+    [AclUnset,    { level: aclLevel.unset,        name: SharingPermissions.Unset }],
 ]);
+export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> = new Map(Array.from(HierarchyMapping.entries()).map(value => [value[1].name, { level: value[1].level, acl: value[0] }]));
 
 // caches the document access permissions for the current user.
 // this recursively updates all protos as well.
 export function updateCachedAcls(doc: Doc) {
     if (!doc) return;
-    const permissions: { [key: string]: symbol } = {};
-
-    doc[UpdatingFromServer] = true;
-    Object.keys(doc).filter(key => key.startsWith('acl') && (permissions[key] = AclMap.get(StrCast(doc[key]))!));
-    doc[UpdatingFromServer] = false;
 
-    if (Object.keys(permissions).length) {
-        doc[AclSym] = permissions;
+    const target = (doc as any)?.__fields ?? doc;
+    const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {};
+    Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl));
+    if (Object.keys(permissions).length || doc[AclSym]?.length) {
+        runInAction(() => (doc[AclSym] = permissions));
     }
 
     if (doc.proto instanceof Promise) {
@@ -329,6 +338,7 @@ export class Doc extends RefField {
 
     @observable private ___fields: any = {};
     @observable private ___fieldKeys: any = {};
+    /// all of the raw acl's that have been set on this document.  Use GetEffectiveAcl to determine the actual ACL of the doc for editing
     @observable public [AclSym]: { [key: string]: symbol } = {};
     @observable public [DirectLinksSym]: Set<Doc> = new Set();
     @observable public [AnimationSym]: Opt<Doc>;
@@ -939,6 +949,7 @@ export namespace Doc {
 
     export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc {
         const copy = new Doc(copyProtoId, true);
+        updateCachedAcls(copy);
         const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []);
         Object.keys(doc).forEach(key => {
             if (exclude.includes(key)) return;
@@ -989,6 +1000,7 @@ export namespace Doc {
         if (doc) {
             const delegate = new Doc(id, true);
             delegate[Initializing] = true;
+            updateCachedAcls(delegate);
             delegate.proto = doc;
             delegate.author = Doc.CurrentUserEmail;
             Object.keys(doc)
diff --git a/src/fields/List.ts b/src/fields/List.ts
index 1e1adc7a8..809173ddf 100644
--- a/src/fields/List.ts
+++ b/src/fields/List.ts
@@ -127,6 +127,9 @@ const listHandlers: any = {
         this[Self].__realFields();
         return this[Self].__fields.map(toRealField).join(separator);
     },
+    lastElement() {
+        return this[Self].__fields.lastElement();
+    },
     lastIndexOf(valueToFind: any, fromIndex: number) {
         if (valueToFind instanceof RefField) {
             return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex);
@@ -213,7 +216,7 @@ function toRealField(field: Field) {
     return field instanceof ProxyField ? field.value : field;
 }
 
-function listGetter(target: any, prop: string | number | symbol, receiver: any): any {
+function listGetter(target: any, prop: string | symbol, receiver: any): any {
     if (listHandlers.hasOwnProperty(prop)) {
         return listHandlers[prop];
     }
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 7f4892bd6..51c76b19a 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -8,13 +8,11 @@ import { returnZero } from '../Utils';
 import CursorField from './CursorField';
 import {
     AclAdmin,
-    AclAugment,
     AclEdit,
+    aclLevel,
     AclPrivate,
-    AclReadonly,
     AclSelfEdit,
     AclSym,
-    AclUnset,
     DataSym,
     Doc,
     DocListCast,
@@ -22,8 +20,10 @@ import {
     FieldResult,
     ForceServerWrite,
     HeightSym,
+    HierarchyMapping,
     Initializing,
     LayoutSym,
+    ReverseHierarchyMap,
     updateCachedAcls,
     UpdatingFromServer,
     WidthSym,
@@ -118,6 +118,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
         if (writeToServer) {
             if (value === undefined) target[Update]({ $unset: { ['fields.' + prop]: '' } });
             else target[Update]({ $set: { ['fields.' + prop]: value instanceof ObjectField ? SerializationHelper.Serialize(value) : value === undefined ? null : value } });
+            if (prop === 'author' || prop.toString().startsWith('acl')) updateCachedAcls(target);
         } else {
             DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue);
         }
@@ -125,7 +126,12 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number
             (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) &&
             UndoManager.AddEvent({
                 redo: () => (receiver[prop] = value),
-                undo: () => (receiver[prop] = curValue),
+                undo: () => {
+                    const wasUpdate = receiver[UpdatingFromServer];
+                    receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable.
+                    receiver[prop] = curValue;
+                    receiver[UpdatingFromServer] = wasUpdate;
+                },
                 prop: prop?.toString(),
             });
         return true;
@@ -182,8 +188,11 @@ export function inheritParentAcls(parent: Doc, child: Doc) {
  * View: a user with view access to a document can only view it - they cannot add/remove/edit anything.
  *
  * None: the document is not shared with that user.
+ *
+ * Unset: Remove a sharing permission (eg., used )
  */
 export enum SharingPermissions {
+    Unset = 'None',
     Admin = 'Admin',
     Edit = 'Edit',
     SelfEdit = 'Self Edit',
@@ -203,22 +212,16 @@ const getEffectiveAclCache = computedFn(function (target: any, user?: string) {
 export function GetEffectiveAcl(target: any, user?: string): symbol {
     if (!target) return AclPrivate;
     if (target[UpdatingFromServer]) return AclAdmin;
-    // authored documents are private until an ACL is set.
-    if (!target[AclSym] && target.author && target.author !== Doc.CurrentUserEmail) return AclPrivate;
     return getEffectiveAclCache(target, user); // all changes received from the server must be processed as Admin.  return this directly so that the acls aren't cached (UpdatingFromServer is not observable)
 }
 
 function getPropAcl(target: any, prop: string | symbol | number) {
-    if (prop === UpdatingFromServer || prop === Initializing || target[UpdatingFromServer] || prop === AclSym) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
+    if (typeof prop === 'symbol' || target[UpdatingFromServer]) return AclAdmin; // requesting the UpdatingFromServer prop or AclSym must always go through to keep the local DB consistent
     if (prop && DocServer.IsPlaygroundField(prop.toString())) return AclEdit; // playground props are always editable
     return GetEffectiveAcl(target);
 }
 
-let HierarchyMapping: Map<symbol, number> | undefined;
-
 let cachedGroups = observable([] as string[]);
-/// bcz; argh!! TODO;   These do not belong here, but there were include order problems with leaving them in util.ts
-// need to investigate further what caused the mobx update problems and move to a better location.
 const getCachedGroupByNameCache = computedFn(function (name: string) {
     return cachedGroups.includes(name);
 }, true);
@@ -230,42 +233,32 @@ export function SetCachedGroups(groups: string[]) {
 }
 function getEffectiveAcl(target: any, user?: string): symbol {
     const targetAcls = target[AclSym];
-    const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
-    const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author
-    if (userChecked === targetAuthor || !targetAuthor) return AclAdmin;
-    if (GetCachedGroupByName('Admin')) return AclAdmin;
+    if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin;
 
+    const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group
     if (targetAcls && Object.keys(targetAcls).length) {
-        HierarchyMapping =
-            HierarchyMapping ||
-            new Map<symbol, number>([
-                [AclPrivate, 0],
-                [AclReadonly, 1],
-                [AclAugment, 2],
-                [AclSelfEdit, 2.5],
-                [AclEdit, 3],
-                [AclAdmin, 4],
-            ]);
-
         let effectiveAcl = AclPrivate;
         for (const [key, value] of Object.entries(targetAcls)) {
             // there are issues with storing fields with . in the name, so they are replaced with _ during creation
             // as a result we need to restore them again during this comparison.
             const entity = denormalizeEmail(key.substring(4)); // an individual or a group
-            if (HierarchyMapping.get(value as symbol)! > HierarchyMapping.get(effectiveAcl)!) {
-                if (GetCachedGroupByName(entity) || userChecked === entity) {
+            if (HierarchyMapping.get(value as symbol)!.level > HierarchyMapping.get(effectiveAcl)!.level) {
+                if (GetCachedGroupByName(entity) || userChecked === entity || entity === 'Me') {
                     effectiveAcl = value as symbol;
                 }
             }
         }
 
         // if there's an overriding acl set through the properties panel or sharing menu, that's what's returned if the user isn't an admin of the document
-        const override = targetAcls['acl-Override'];
-        if (override !== AclUnset && override !== undefined) effectiveAcl = override;
+        //const override = targetAcls['acl-Override'];
+        // if (override !== AclUnset && override !== undefined) effectiveAcl = override;
 
         // if we're in playground mode, return AclEdit (or AclAdmin if that's the user's effectiveAcl)
-        return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)! < 3 ? AclEdit : effectiveAcl;
+        return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl;
     }
+    // authored documents are private until an ACL is set.
+    const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author
+    if (targetAuthor && targetAuthor !== userChecked) return AclPrivate;
     return AclAdmin;
 }
 /**
@@ -290,21 +283,9 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
     }
     visited.push(target);
 
-    const HierarchyMapping = new Map<string, number>([
-        ['Not Shared', 0],
-        ['Can View', 1],
-        ['Can Augment', 2],
-        ['Self Edit', 2.5],
-        ['Can Edit', 3],
-        ['Admin', 4],
-    ]);
-
     let layoutDocChanged = false; // determines whether fetchProto should be called or not (i.e. is there a change that should be reflected in target[AclSym])
-    let dataDocChanged = false;
-    const dataDoc = target[DataSym];
-
     // if it is inheriting from a collection, it only inherits if A) the key doesn't already exist or B) the right being inherited is more restrictive
-    if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || HierarchyMapping.get(StrCast(target[key]))! > HierarchyMapping.get(acl)!)) {
+    if (GetEffectiveAcl(target) === AclAdmin && (!inheritingFromCollection || !target[key] || ReverseHierarchyMap.get(StrCast(target[key]))!.level > ReverseHierarchyMap.get(acl)!.level)) {
         target[key] = acl;
         layoutDocChanged = true;
 
@@ -315,15 +296,16 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
         }
     }
 
-    if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || HierarchyMapping.get(StrCast(dataDoc[key]))! > HierarchyMapping.get(acl)!)) {
+    let dataDocChanged = false;
+    const dataDoc = target[DataSym];
+    if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) {
         if (GetEffectiveAcl(dataDoc) === AclAdmin) {
             dataDoc[key] = acl;
             dataDocChanged = true;
         }
 
         // maps over the links of the document
-        const links = DocListCast(dataDoc.links);
-        links.forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+        DocListCast(dataDoc.links).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
 
         // maps over the children of the document
         DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => {
@@ -352,10 +334,10 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
 
 export function setter(target: any, in_prop: string | symbol | number, value: any, receiver: any): boolean {
     let prop = in_prop;
-    const effectiveAcl = getPropAcl(target, prop);
+    const effectiveAcl = in_prop === 'constructor' || typeof in_prop === 'symbol' ? AclAdmin : getPropAcl(target, prop);
     if (effectiveAcl !== AclEdit && effectiveAcl !== AclAdmin && !(effectiveAcl === AclSelfEdit && value instanceof RichTextField)) return true;
     // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't
-    if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined, 'None'].includes(value))) return true;
+    if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true;
     // if (typeof prop === "string" && prop.startsWith("acl") && !["Can Edit", "Can Augment", "Can View", "Not Shared", undefined].includes(value)) return true;
 
     if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) {
@@ -371,27 +353,21 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
     return _setter(target, prop, value, receiver);
 }
 
-export function getter(target: any, in_prop: string | symbol | number, receiver: any): any {
-    let prop = in_prop;
-
+export function getter(target: any, in_prop: string | symbol, receiver: any): any {
+    if (in_prop === 'constructor' || in_prop === 'toString' || in_prop === 'valueOf' || in_prop === 'factory' || in_prop === 'serializeInfo') return target[in_prop];
+    if (in_prop === 'then') return undefined; //If we're being awaited
     if (in_prop === AclSym) return target[AclSym];
-    if (in_prop === 'toString' || (in_prop !== HeightSym && in_prop !== WidthSym && in_prop !== LayoutSym && typeof prop === 'symbol')) return target.__fields[prop] || target[prop];
-    if (GetEffectiveAcl(target) === AclPrivate) return prop === HeightSym || prop === WidthSym ? returnZero : undefined;
-    if (prop === LayoutSym) return target.__LAYOUT__;
-    if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) {
-        if (!prop.startsWith('__')) prop = prop.substring(1);
-        if (target.__LAYOUT__) return target.__LAYOUT__[prop];
-    }
-    if (prop === 'then') {
-        //If we're being awaited
+    if (in_prop === LayoutSym) return target.__LAYOUT__;
+    if ((in_prop === HeightSym || in_prop === WidthSym) && GetEffectiveAcl(target) === AclPrivate) return returnZero;
+    if (typeof in_prop === 'symbol' || in_prop.startsWith('isMobX') || in_prop.startsWith('__')) return target.__fields[in_prop] || target[in_prop];
+    if (GetEffectiveAcl(target) === AclPrivate) {
+        if (in_prop === 'author') return target.__fields[in_prop] || target[in_prop];
         return undefined;
     }
-    if (typeof prop === 'symbol') {
-        return target.__fields[prop] || target[prop];
-    }
-    if (SerializationHelper.IsSerializing()) {
-        return target[prop];
-    }
+
+    const prop = in_prop.startsWith('_') ? in_prop.substring(1) : in_prop;
+    if (prop !== in_prop && target.__LAYOUT__) return target.__LAYOUT__[prop];
+    if (SerializationHelper.IsSerializing()) return target[prop];
     return getFieldImpl(target, prop, receiver);
 }
 
-- 
cgit v1.2.3-70-g09d2


From da9aef9e4cda036264801191361c1ecfacd9ba0b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 23 Dec 2022 18:05:02 -0500
Subject: fixed pinning items at frame 0 of an animated collection.

---
 src/client/views/ContextMenuItem.tsx      | 1 +
 src/client/views/nodes/trails/PresBox.tsx | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index dc9c2eb6c..e87d2046b 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -29,6 +29,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select
     @observable private _items: Array<ContextMenuProps> = [];
     @observable private overItem = false;
 
+    @action
     componentDidMount() {
         this._items.length = 0;
         if ((this.props as SubmenuProps)?.subitems) {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index abaa3b373..b26f485be 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -280,7 +280,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
             if (activeFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
-                const context = activeItem.presActiveFrame ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
+                const context = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
                 if (context) {
                     const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
                     if (ffview) {
-- 
cgit v1.2.3-70-g09d2


From 67316c700980fe653c48840407dc9d66a7ed8a2b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 6 Jan 2023 00:35:43 -0500
Subject: added zoom box highlighting of text anchors in pdf/web pages for
 link/pres following. Added serial/parallel option for presentation group with
 up.  Added direct pinning of text seletions to trails.  fixed 'hide' option
 for preselements to work with hidebefore/after

---
 src/client/util/DocumentManager.ts                 | 67 ++++++++++++-----
 src/client/util/LinkFollower.ts                    |  9 ++-
 src/client/views/MainView.tsx                      |  1 +
 src/client/views/MarqueeAnnotator.tsx              | 42 ++++++-----
 src/client/views/collections/CollectionSubView.tsx |  2 -
 src/client/views/collections/TabDocView.tsx        |  5 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  9 ++-
 src/client/views/nodes/DocumentContentsView.tsx    |  2 +-
 src/client/views/nodes/DocumentView.scss           | 16 +++++
 src/client/views/nodes/DocumentView.tsx            | 38 +++++++++-
 src/client/views/nodes/MapBox/MapBox.tsx           |  9 ---
 src/client/views/nodes/PDFBox.tsx                  |  7 ++
 src/client/views/nodes/WebBox.scss                 |  1 +
 src/client/views/nodes/WebBox.tsx                  | 27 +++++--
 src/client/views/nodes/trails/PresBox.tsx          | 83 ++++++++++++++--------
 src/client/views/nodes/trails/PresElementBox.tsx   |  3 +-
 src/client/views/pdf/PDFViewer.tsx                 | 10 ++-
 src/fields/Doc.ts                                  | 16 +++--
 18 files changed, 241 insertions(+), 106 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d2e9e17b4..70fe7f2c0 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,8 +1,8 @@
-import { action, observable, runInAction } from 'mobx';
-import { Doc, Opt } from '../../fields/Doc';
+import { action, observable, ObservableSet, runInAction } from 'mobx';
+import { AnimationSym, Doc, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { listSpec } from '../../fields/Schema';
-import { Cast, DocCast } from '../../fields/Types';
+import { Cast, DocCast, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -42,7 +42,7 @@ export class DocumentManager {
     callAddViewFuncs = (view: DocumentView) => {
         const callFuncs = this._viewRenderedCbs.filter(vc => vc.doc === view.rootDoc);
         if (callFuncs.length) {
-            this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => callFuncs.includes(vc));
+            this._viewRenderedCbs = this._viewRenderedCbs.filter(vc => !callFuncs.includes(vc));
             const intTimer = setInterval(
                 () => {
                     if (!view.ComponentView?.incrementalRendering?.()) {
@@ -147,14 +147,15 @@ export class DocumentManager {
 
     public getDocumentView(toFind: Doc, preferredCollection?: CollectionView): DocumentView | undefined {
         const found =
-            Array.from(DocumentManager.Instance.DocumentViews).find(
-                dv =>
-                    ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
-                    ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
-            )?.rootDoc ?? toFind;
+            // Array.from(DocumentManager.Instance.DocumentViews).find(
+            //     dv =>
+            //         ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFind.data as any)?.url?.href) ||
+            //         ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href)
+            // )?.rootDoc ??
+            toFind;
         return this.getDocumentViewById(found[Id], preferredCollection);
     }
-    
+
     public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => {
         const views: DocumentView[] = [];
         Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view));
@@ -167,11 +168,12 @@ export class DocumentManager {
     };
     public getDocumentViews(toFindIn: Doc): DocumentView[] {
         const toFind =
-            Array.from(DocumentManager.Instance.DocumentViews).find(
-                dv =>
-                    ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
-                    ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
-            )?.rootDoc ?? toFindIn;
+            // Array.from(DocumentManager.Instance.DocumentViews).find(
+            //     dv =>
+            //         ((dv.rootDoc.data as any)?.url?.href && (dv.rootDoc.data as any)?.url?.href === (toFindIn.data as any)?.url?.href) ||
+            //         ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFindIn.annotationOn)?.data as any)?.url?.href)
+            // )?.rootDoc ??
+            toFindIn;
 
         const toReturn: DocumentView[] = [];
         const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath));
@@ -203,6 +205,11 @@ export class DocumentManager {
         }
     }
 
+    public static removeOverlayViews() {
+        DocumentManager._overlayViews?.forEach(action(view => (view.textHtmlOverlay = undefined)));
+        DocumentManager._overlayViews?.clear();
+    }
+    static _overlayViews = new ObservableSet<DocumentView>();
     static addView = (doc: Doc, finished?: () => void) => {
         CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
         finished?.();
@@ -225,7 +232,7 @@ export class DocumentManager {
                 docView?.props.bringToFront(resolvedTarget);
             });
         }
-        const focusAndFinish = (didFocus: boolean) => {
+        const focusAndFinish = action((didFocus: boolean) => {
             const finalTargetDoc = resolvedTarget;
             if (options.toggleTarget) {
                 if (!didFocus && !wasHidden) {
@@ -236,12 +243,26 @@ export class DocumentManager {
                 finalTargetDoc.hidden && (finalTargetDoc.hidden = undefined);
                 !options.noSelect && docView?.select(false);
             }
+            if (targetDoc.textHtml && options.zoomTextSelections) {
+                const containerView = DocumentManager.Instance.getFirstDocumentView(finalTargetDoc);
+                if (containerView) {
+                    containerView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect));
+                    containerView.textHtmlOverlay = StrCast(targetDoc.textHtml);
+                    DocumentManager._overlayViews.add(containerView);
+                    if (Doc.UnhighlightTimer) {
+                        Doc.AddUnHighlightWatcher(() => {
+                            DocumentManager.removeOverlayViews();
+                            containerView.htmlOverlayEffect = '';
+                        });
+                    } else setTimeout(() => (containerView.htmlOverlayEffect = ''));
+                }
+            }
             finished?.();
-        };
+        });
         const annoContainerView = (!wasHidden || resolvedTarget !== annotatedDoc) && annotatedDoc && this.getFirstDocumentView(annotatedDoc);
         if (annoContainerView) {
             if (annoContainerView.props.Document.layoutKey === 'layout_icon') {
-                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
+                return annoContainerView.iconify(() => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
             }
             if (!docView && targetDoc.type !== DocumentType.MARKER) {
                 annoContainerView.focus(targetDoc, {}); // this allows something like a PDF view to remove its doc filters to expose the target so that it can be found in the retry code below
@@ -254,7 +275,15 @@ export class DocumentManager {
         const targetDocContextView = (targetDocContext && this.getFirstDocumentView(targetDocContext)) || (wasHidden && annoContainerView); // if we have an annotation container and the target was hidden, then try again because we just un-hid the document above
         const focusView = !docView && targetDoc.type === DocumentType.MARKER && annoContainerView ? annoContainerView : docView;
         if (focusView) {
-            !options.noSelect && Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
+            if (focusView.rootDoc === originalTarget) {
+                if (!options.noSelect) Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
+                else {
+                    focusView.rootDoc[AnimationSym] = options.effect;
+                    if (Doc.UnhighlightTimer) {
+                        Doc.AddUnHighlightWatcher(action(() => (focusView.rootDoc[AnimationSym] = undefined)));
+                    }
+                }
+            }
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
             const doFocus = (forceDidFocus: boolean) =>
                 focusView.focus(originalTarget, {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 94badbb44..d5ef9fab6 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,12 +1,10 @@
 import { action, runInAction } from 'mobx';
-import { Doc, DocListCast, Opt, WidthSym } from '../../fields/Doc';
-import { listSpec } from '../../fields/Schema';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
-import { PresEffect, PresEffectDirection } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { UndoManager } from './UndoManager';
@@ -59,7 +57,7 @@ export class LinkFollower {
                 createTabForTarget(false);
             } else {
                 // first focus & zoom onto this (the clicked document).  Then execute the function to focus on the target
-                docViewProps.focus(sourceDoc, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget });
+                docViewProps.focus(sourceDoc, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomTime: 1000, zoomScale: 1, afterFocus: createTabForTarget });
             }
         };
         runInAction(() => (DocumentDecorations.Instance.overrideBounds = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value
@@ -70,7 +68,7 @@ export class LinkFollower {
             docViewProps.ContainingCollectionDoc,
             action(() => {
                 batch.end();
-                Doc.AddUnlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
+                Doc.AddUnHighlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
             }),
             altKey ? true : undefined
         );
@@ -110,6 +108,7 @@ export class LinkFollower {
                         easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
                         effect: sourceDoc,
                         originatingDoc: sourceDoc,
+                        zoomTextSelections: false,
                     };
                     if (target.TourMap) {
                         const fieldKey = Doc.LayoutFieldKey(target);
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 42e259eed..39cc9ba8e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -482,6 +482,7 @@ export class MainView extends React.Component {
     }
 
     globalPointerDown = action((e: PointerEvent) => {
+        DocumentManager.removeOverlayViews();
         Doc.linkFollowUnhighlight();
         AudioBox.Enabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 2fdb59361..bf1242346 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -18,7 +18,7 @@ const _global = (window /* browser */ || global) /* node */ as any;
 
 export interface MarqueeAnnotatorProps {
     rootDoc: Doc;
-    down: number[];
+    down?: number[];
     iframe?: () => undefined | HTMLIFrameElement;
     scrollTop: number;
     scaling?: () => number;
@@ -45,6 +45,17 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
     @observable private _width: number = 0;
     @observable private _height: number = 0;
 
+    constructor(props: any) {
+        super(props);
+
+        AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true);
+        AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true));
+        AnchorMenu.Instance.OnAudio = unimplementedFunction;
+        AnchorMenu.Instance.Highlight = this.highlight;
+        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
+        AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
+    }
+
     @action
     static clearAnnotations(savedAnnotations: ObservableMap<number, HTMLDivElement[]>) {
         AnchorMenu.Instance.Status = 'marquee';
@@ -54,24 +65,21 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         savedAnnotations.clear();
     }
 
-    @action componentDidMount() {
-        // set marquee x and y positions to the spatially transformed position
-        const boundingRect = this.props.mainCont.getBoundingClientRect();
-        this._startX = this._left = (this.props.down[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
-        this._startY = this._top = (this.props.down[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
-        this._height = this._width = 0;
+    @action gotDownPoint() {
+        if (!this._width && !this._height) {
+            const downPt = this.props.down!;
+            // set marquee x and y positions to the spatially transformed position
+            const boundingRect = this.props.mainCont.getBoundingClientRect();
+            this._startX = this._left = (downPt[0] - boundingRect.left) * (this.props.mainCont.offsetWidth / boundingRect.width);
+            this._startY = this._top = (downPt[1] - boundingRect.top) * (this.props.mainCont.offsetHeight / boundingRect.height) + this.props.mainCont.scrollTop;
+        }
 
         const doc = this.props.iframe?.()?.contentDocument ?? document;
+        doc.removeEventListener('pointermove', this.onSelectMove);
+        doc.removeEventListener('pointerup', this.onSelectEnd);
         doc.addEventListener('pointermove', this.onSelectMove);
         doc.addEventListener('pointerup', this.onSelectEnd);
 
-        AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true);
-        AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true));
-        AnchorMenu.Instance.OnAudio = unimplementedFunction;
-        AnchorMenu.Instance.Highlight = this.highlight;
-        AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations);
-        AnchorMenu.Instance.onMakeAnchor = AnchorMenu.Instance.GetAnchor;
-
         /**
          * This function is used by the AnchorMenu to create an anchor highlight and a new linked text annotation.
          * It also initiates a Drag/Drop interaction to place the text annotation.
@@ -125,7 +133,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                   });
               });
     }
-    componentWillUnmount() {
+    releaseDownPt() {
         const doc = this.props.iframe?.()?.contentDocument ?? document;
         doc.removeEventListener('pointermove', this.onSelectMove);
         doc.removeEventListener('pointerup', this.onSelectEnd);
@@ -259,6 +267,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                 this.highlight('rgba(245, 230, 95, 0.75)', false); // yellowish highlight color for highlighted text (should match AnchorMenu's highlight color)
             }
             this.props.finishMarquee(undefined, undefined, e);
+            runInAction(() => (this._width = this._height = 0));
         } else {
             runInAction(() => (this._width = this._height = 0));
             this.props.finishMarquee(cliX, cliY, e);
@@ -266,8 +275,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
     };
 
     render() {
-        return (
+        return !this.props.down ? null : (
             <div
+                ref={r => (r ? this.gotDownPoint() : this.releaseDownPt())}
                 className="marqueeAnnotator-dragBox"
                 style={{
                     left: `${this._left}px`,
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index b66dc0aa2..4f3f21cea 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -172,8 +172,6 @@ export function CollectionSubView<X>(moreProps?: X) {
                 // The following conditional detects a recurring bug we've seen on the server
                 if (proto[Id] === Docs.Prototypes.get(DocumentType.COL)[Id]) {
                     alert('COLLECTION PROTO CURSOR ISSUE DETECTED! Check console for more info...');
-                    console.log(doc);
-                    console.log(proto);
                     throw new Error(`AHA! You were trying to set a cursor on a collection's proto, which is the original collection proto! Look at the two previously printed lines for document values!`);
                 }
                 let cursors = Cast(proto.cursors, listSpec(CursorField));
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index e45e2a1cb..cd58319cb 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -247,8 +247,9 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 alert('Cannot pin presentation document to itself');
                 return;
             }
-            const pinDoc = Doc.MakeAlias(doc);
-            pinDoc.presentationTargetDoc = doc;
+            const anchorDoc = DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.getAnchor?.();
+            const pinDoc = Doc.MakeAlias(anchorDoc ?? doc);
+            pinDoc.presentationTargetDoc = anchorDoc ?? doc;
             pinDoc.title = doc.title + ' - Slide';
             pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children.  the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
             pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 82b97dff0..dc0eb69f3 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1146,6 +1146,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return new Promise<number>(res => setTimeout(() => res(runInAction(() => (this._panZoomTransition = 0))), this._panZoomTransition)); // set transition to be smooth, then reset
     }
 
+    _focusCount = 0;
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
         const state = HistoryUtil.getState();
 
@@ -1187,6 +1188,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 this.setPan(panX, panY, focusSpeed, true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow
             }
 
+            const focusCount = ++this._focusCount;
             const startTime = Date.now();
             // focus on this collection within its parent view.  the parent view after focusing determines whether to reset the view change within the collection
             const endFocus = async (moved: boolean) => {
@@ -1200,7 +1202,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         this.Document[this.scaleFieldKey] = restoreState.scale;
                     }
                 }
-                runInAction(() => (this._panZoomTransition = 0));
+                this._focusCount === focusCount && didMove && runInAction(() => (this._panZoomTransition = 0));
                 return resetView;
             };
             const xf = !cantTransform
@@ -2284,5 +2286,8 @@ ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) {
     runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.()));
 });
 ScriptingGlobals.add(function pinWithView(readOnly: boolean, pinDocContent: boolean) {
-    !readOnly && SelectionManager.Views().forEach(view => TabDocView.PinDoc(view.rootDoc, { pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) }));
+    !readOnly &&
+        SelectionManager.Views().forEach(view =>
+            TabDocView.PinDoc(view.rootDoc, { currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), pinDocContent, pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()) })
+        );
 });
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index 3ab7cc0bc..569579996 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -59,7 +59,7 @@ class ObserverJsxParser1 extends JsxParser {
     }
 }
 
-const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
+export const ObserverJsxParser: typeof JsxParser = ObserverJsxParser1 as any;
 
 interface HTMLtagProps {
     Document: Doc;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index abf6e37ab..453bdac8e 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -51,6 +51,22 @@
         height: calc(100% - 20px);
     }
 
+    .documentView-htmlOverlay {
+        position: absolute;
+        display: flex;
+        top: 0;
+        height: 100%;
+        width: 100%;
+        .documentView-htmlOverlayInner {
+            box-shadow: black 0.2vw 0.2vw 0.8vw;
+            background: rgb(255, 255, 255);
+            overflow: auto;
+            position: relative;
+            margin: auto;
+            padding: 20px;
+        }
+    }
+
     .documentView-linkAnchorBoxAnchor {
         display: flex;
         overflow: hidden;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 76cc6800a..95cf08289 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
+import { ObserverJsxParser } from './DocumentContentsView';
 import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal';
 import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
@@ -110,6 +111,7 @@ export interface DocFocusOptions {
     effect?: Doc; // animation effect for focus
     noSelect?: boolean; // whether target should be selected after focusing
     playAudio?: boolean; // whether to play audio annotation on focus
+    zoomTextSelections?: boolean; // whether to display a zoomed overlay of anchor text selections
     toggleTarget?: boolean; // whether to toggle target on and off
     originatingDoc?: Doc; // document that triggered the focus
     easeFunc?: 'linear' | 'ease'; // transition method for scrolling
@@ -590,9 +592,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         setTimeout(() => this._componentView?.setViewSpec?.(anchor, LinkDocPreview.LinkInfo ? true : false));
         const focusSpeed = this._componentView?.scrollFocus?.(anchor, { ...options, instant: options?.instant || LinkDocPreview.LinkInfo ? true : false });
         const endFocus = focusSpeed === undefined ? options?.afterFocus : async (moved: boolean) => options?.afterFocus?.(true) ?? ViewAdjustment.doNothing;
+        const startTime = Date.now();
         this.props.focus(options?.docTransform ? anchor : this.rootDoc, {
             ...options,
-            afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), focusSpeed ?? 0)),
+            afterFocus: (didFocus: boolean) =>
+                new Promise<ViewAdjustment>(async res =>
+                    setTimeout(
+                        async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), //
+                        didFocus ? Math.max(0, (options.zoomTime ?? 500) - (Date.now() - startTime)) : 0
+                    )
+                ),
         });
     };
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
@@ -1098,11 +1107,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     };
     @observable _retryThumb = 1;
     thumbShown = () => {
+        const childHighlighted = () =>
+            Array.from(Doc.highlightedDocs.keys())
+                .concat(Array.from(Doc.brushManager.BrushedDoc.keys()))
+                .some(doc => Doc.AreProtosEqual(DocCast(doc.annotationOn), this.rootDoc));
+        const childOverlayed = () => Array.from(DocumentManager._overlayViews).some(view => Doc.AreProtosEqual(view.rootDoc, this.rootDoc));
         return !this.props.isSelected() &&
             LightboxView.LightboxDoc !== this.rootDoc &&
             this.thumb &&
             !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) &&
-            (!Doc.isBrushedHighlightedDegree(this.props.Document) || this.rootDoc._viewType === CollectionViewType.Docking) &&
+            ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) &&
             !this._componentView?.isAnyChildContentActive?.()
             ? true
             : false;
@@ -1754,6 +1768,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
 
+    @observable textHtmlOverlay: Opt<string>;
     @computed get anchorViewDoc() {
         return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined;
     }
@@ -1793,6 +1808,24 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     isHovering = () => this._isHovering;
     @observable _isHovering = false;
 
+    htmlOverlayEffect = '';
+    @computed get htmlOverlay() {
+        const effectProps = {
+            delay: 0,
+            duration: 500,
+        };
+        const highlight = (
+            <div className="webBox-textHighlight">
+                <ObserverJsxParser autoCloseVoidElements={true} key={42} onError={(e: any) => console.log('PARSE error', e)} renderInWrapper={false} jsx={StrCast(this.textHtmlOverlay)} />
+            </div>
+        );
+        return !this.textHtmlOverlay ? null : (
+            <div className="documentView-htmlOverlay">
+                <div className="documentView-htmlOverlayInner">{<Fade {...effectProps}>{DocumentViewInternal.AnimationEffect(highlight, { presEffect: this.htmlOverlayEffect ?? 'Zoom' } as any as Doc, this.rootDoc)} </Fade>}</div>
+            </div>
+        );
+    }
+
     render() {
         TraceMobx();
         const xshift = Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined;
@@ -1841,6 +1874,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                             focus={this.props.focus || emptyFunction}
                             ref={action((r: DocumentViewInternal | null) => r && (this.docView = r))}
                         />
+                        {this.htmlOverlay}
                     </div>
                 )}
 
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index b0f6f8358..c8d5b0154 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -322,7 +322,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
      */
     sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
         console.log('print all sidebar Docs');
-        console.log(this.allSidebarDocs);
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
         const docs = doc instanceof Doc ? [doc] : doc;
         docs.forEach(doc => {
@@ -337,8 +336,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 }
             }
         }); //add to annotation list
-        console.log('sidebaraddDocument');
-        console.log(doc);
 
         return this.addDocument(doc, sidebarKey); // add to sidebar list
     };
@@ -352,10 +349,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
         if (this.layoutDoc._showSidebar) this.toggleSidebar();
         const docs = doc instanceof Doc ? [doc] : doc;
-        docs.forEach(doc => {
-            console.log(this.allMapMarkers);
-            console.log(this.allSidebarDocs);
-        });
         return this.removeDocument(doc, sidebarKey);
     };
 
@@ -405,7 +398,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
      */
     @action
     private handlePlaceChanged = () => {
-        console.log(this.searchBox);
         const place = this.searchBox.getPlace();
 
         if (!place.geometry || !place.geometry.location) {
@@ -416,7 +408,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
         // zoom in on the location of the search result
         if (place.geometry.viewport) {
-            console.log(this._map);
             this._map.fitBounds(place.geometry.viewport);
         } else {
             this._map.setCenter(place.geometry.location);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 88aac67a7..e22ee5021 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -213,6 +213,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return this._pdfViewer?.scrollFocus(doc, NumCast(doc.presPinViewScroll, NumCast(doc.y)), options) ?? (didToggle ? 1 : undefined);
     };
     getAnchor = () => {
+        let ele: Opt<HTMLDivElement> = undefined;
+        if (this._pdfViewer?.selectionContent()) {
+            ele = document.createElement('div');
+            ele.append(this._pdfViewer.selectionContent()!);
+        }
         const docAnchor = () => {
             const anchor = Docs.Create.TextanchorDocument({
                 title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)),
@@ -222,6 +227,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             return anchor;
         };
         const anchor = this._pdfViewer?._getAnchor(this._pdfViewer.savedAnnotations()) ?? docAnchor();
+        anchor.text = ele?.textContent ?? '';
+        anchor.textHtml = ele?.innerHTML;
         this.addDocument(anchor);
         return anchor;
     };
diff --git a/src/client/views/nodes/WebBox.scss b/src/client/views/nodes/WebBox.scss
index a41f66ef0..6f578a9fc 100644
--- a/src/client/views/nodes/WebBox.scss
+++ b/src/client/views/nodes/WebBox.scss
@@ -7,6 +7,7 @@
     left: 0;
     position: relative;
     display: flex;
+    overflow: hidden;
 
     .webBox-sideResizer {
         position: absolute;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index a3e83f047..8be4884ce 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -29,7 +29,7 @@ import { AnchorMenu } from '../pdf/AnchorMenu';
 import { Annotation } from '../pdf/Annotation';
 import { SidebarAnnos } from '../SidebarAnnos';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewInternal, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
@@ -54,10 +54,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _keyInput = React.createRef<HTMLInputElement>();
     private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop));
-    private _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     private _sidebarRef = React.createRef<SidebarAnnos>();
     private _searchRef = React.createRef<HTMLInputElement>();
     private _searchString = '';
+
+    private get _getAnchor() {
+        return AnchorMenu.Instance?.GetAnchor;
+    }
     @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render.
     @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe.  so thisforces the ifrmae to be rebuilt when allowScripts is toggled
     @observable private _searching: boolean = false;
@@ -285,7 +288,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func);
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view);
     scrollFocus = (doc: Doc, options: DocFocusOptions) => {
-        if (StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant);
+        if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl), options.instant);
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
             this.toggleSidebar(options.instant);
         }
@@ -305,6 +308,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     getAnchor = () => {
+        let ele: Opt<HTMLDivElement> = undefined;
+        try {
+            const contents = this._iframe?.contentWindow?.getSelection()?.getRangeAt(0).cloneContents();
+            if (contents) {
+                ele = document.createElement('div');
+                ele.append(contents);
+            }
+        } catch (e) {}
         const anchor =
             this._getAnchor(this._savedAnnotations) ??
             Docs.Create.WebanchorDocument(this._url, {
@@ -312,6 +323,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 y: NumCast(this.layoutDoc._scrollTop),
                 unrendered: true,
             });
+        anchor.text = ele?.textContent ?? '';
+        anchor.textHtml = ele?.innerHTML;
         this.addDocumentWrapper(anchor);
         return anchor;
     };
@@ -553,8 +566,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     static urlHash = (s: string) => {
+        const split = s.split('');
         return Math.abs(
-            s.split('').reduce((a: any, b: any) => {
+            split.reduce((a: any, b: any) => {
                 a = (a << 5) - a + b.charCodeAt(0);
                 return a & a;
             }, 0)
@@ -686,7 +700,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
     };
     @action finishMarquee = (x?: number, y?: number, e?: PointerEvent) => {
-        this._getAnchor = AnchorMenu.Instance?.GetAnchor;
         this._marqueeing = undefined;
         this._isAnnotating = false;
         this._iframeClick = undefined;
@@ -1006,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 selectionText={returnEmptyString}
                                 annotationLayer={this._annotationLayer.current}
                                 mainCont={this._mainCont.current}
-                            />{' '}
+                            />
                         </div>
                     )}
                 </div>
@@ -1043,5 +1056,5 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     }
 }
 ScriptingGlobals.add(function urlHash(url: string) {
-    return WebBox.urlHash(url);
+    return url ? WebBox.urlHash(url) : 0;
 });
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index b26f485be..2e3135416 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -65,7 +65,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     private _disposers: { [name: string]: IReactionDisposer } = {};
-    private _obDisposers: { [name: string]: any } = {};
     public selectedArray = new ObservableSet<Doc>();
 
     @observable public static Instance: PresBox;
@@ -142,20 +141,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         // Turn of progressivize editors
         this.turnOffEdit(true);
         Object.values(this._disposers).forEach(disposer => disposer?.());
-        Object.values(this._obDisposers).forEach(disposer => disposer?.());
     }
 
     @action
     componentDidMount() {
-        this._obDisposers.anim = observe(
-            this,
-            'activeItem',
-            change => {
-                change.oldValue && (DocCast((change.oldValue as Doc).presentationTargetDoc)[AnimationSym] = undefined);
-                change.newValue && (DocCast((change.newValue as Doc).presentationTargetDoc)[AnimationSym] = change.newValue as Doc);
-            },
-            true
-        );
         this._disposers.keyboard = reaction(
             () => this.selectedDoc,
             selected => {
@@ -217,16 +206,37 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     // TODO: to handle child slides (entering into subtrail and exiting), also the next() and back() functions
     // No more frames in current doc and next slide is defined, therefore move to next slide
     nextSlide = (slideNum?: number) => {
-        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
-        let nextSelected = slideNum ?? this.itemIndex + 1;
-        this.gotoDocument(nextSelected, this.activeItem);
-        for (nextSelected = nextSelected + 1; nextSelected < this.childDocs.length; nextSelected++) {
-            if (this.childDocs[nextSelected].groupWithUp) {
-                this.gotoDocument(nextSelected, this.activeItem, true);
-            } else {
-                break;
+        const nextSlideInd = slideNum ?? this.itemIndex + 1;
+        let curSlideInd = nextSlideInd;
+        const resetSelection = action(() => {
+            this.clearSelectedArray();
+            for (let i = nextSlideInd; i <= curSlideInd; i++) {
+                this.addToSelectedArray(this.childDocs[i]);
             }
-        }
+        });
+        CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => DocumentManager.Instance.getDocumentView(clip)?.ComponentView?.Pause?.());
+        this.clearSelectedArray();
+        const doGroupWithUp =
+            (nextSelected: number, force = false) =>
+            () => {
+                if (nextSelected < this.childDocs.length) {
+                    if (force || this.childDocs[nextSelected].groupWithUp) {
+                        const serial = nextSelected + 1 < this.childDocs.length && NumCast(this.childDocs[nextSelected + 1].groupWithUp) > 1;
+                        if (serial) {
+                            this.gotoDocument(nextSelected, this.activeItem, true, async () => {
+                                const waitTime = NumCast(this.activeItem.presDuration) - NumCast(this.activeItem.presTransition);
+                                await new Promise<void>(res => setTimeout(() => res(), Math.max(0, waitTime)));
+                                doGroupWithUp(nextSelected + 1)();
+                            });
+                        } else {
+                            this.gotoDocument(nextSelected, this.activeItem, undefined, resetSelection);
+                            curSlideInd = this.itemIndex;
+                            doGroupWithUp(nextSelected + 1)();
+                        }
+                    }
+                }
+            };
+        doGroupWithUp(curSlideInd, true)();
     };
 
     // Called when the user activates 'next' - to move to the next part of the pres. trail
@@ -270,9 +280,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     //The function that is called when a document is clicked or reached through next or back.
     //it'll also execute the necessary actions if presentation is playing.
     @undoBatch
-    public gotoDocument = action((index: number, from?: Doc, group?: boolean) => {
+    public gotoDocument = action((index: number, from?: Doc, group?: boolean, finished?: () => void) => {
         Doc.UnBrushAllDocs();
-
         if (index >= 0 && index < this.childDocs.length) {
             this.rootDoc._itemIndex = index;
             const activeItem: Doc = this.activeItem;
@@ -280,10 +289,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame;
             if (activeFrame !== undefined) {
                 const transTime = NumCast(activeItem.presTransition, 500);
-                const context = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
+                const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc);
+                const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext;
                 if (context) {
                     const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.ComponentView as CollectionFreeFormView;
-                    if (ffview) {
+                    if (ffview?.childDocs) {
                         this._keyTimer = CollectionFreeFormDocumentView.gotoKeyframe(this._keyTimer, ffview.childDocs.slice(), transTime);
                         context._currentFrame = NumCast(activeFrame);
                     }
@@ -302,7 +312,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             if (!group) this.clearSelectedArray();
             this.childDocs[index] && this.addToSelectedArray(this.childDocs[index]); //Update selected array
             this.turnOffEdit();
-            this.navigateToActiveItem(); //Handles movement to element only when presTrail is list
+            this.navigateToActiveItem(finished); //Handles movement to element only when presTrail is list
             this.onHideDocument(); //Handles hide after/before
         }
     });
@@ -489,13 +499,18 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
      * a new tab. If presCollection is undefined it will open the document
      * on the right.
      */
-    navigateToActiveItem = () => {
+    navigateToActiveItem = (afterNav?: () => void) => {
         const activeItem: Doc = this.activeItem;
         const targetDoc: Doc = this.targetDoc;
+        const finished = () => {
+            afterNav?.();
+            console.log('Finish Slide Nav: ' + targetDoc.title);
+            targetDoc[AnimationSym] = undefined;
+        };
         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 = DocListCast(presCollection?.data).includes(targetDoc);
+        const includesDoc: boolean = 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
@@ -517,13 +532,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             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) => {
             DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
             (collectionDocView ?? this).props.addDocTab(doc, OpenWhere.lightbox);
             this.layoutDoc.presCollection = targetDoc;
         };
-        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? undefined : resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, srcContext, includesDoc || tab ? finished : resetSelection);
     };
 
     static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, srcContext: Doc, finished?: () => void) {
@@ -552,11 +568,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
                     zoomScale: activeItem.presMovement === PresMovement.Center ? 0 : NumCast(activeItem.presZoom, 1),
                     zoomTime: activeItem.presMovement === PresMovement.Jump ? 0 : NumCast(activeItem.presTransition, 500),
+                    effect: activeItem,
                     noSelect: true,
                     originatingDoc: activeItem,
                     easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
+                    zoomTextSelections: true
                 };
-
+                if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
                 var containerDocContext = srcContext ? [srcContext] : [];
                 while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) {
                     containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext];
@@ -577,12 +595,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         this.childDocs.forEach((doc, index) => {
             const curDoc = Cast(doc, Doc, null);
             const tagDoc = Cast(curDoc.presentationTargetDoc, Doc, null);
-            //if (tagDoc) tagDoc.opacity = 1;
             const itemIndexes: number[] = this.getAllIndexes(this.tagDocs, tagDoc);
-            const curInd: number = itemIndexes.indexOf(index);
             if (tagDoc === this.layoutDoc.presCollection) {
                 tagDoc.opacity = 1;
             } else {
+                if (curDoc.presHide) {
+                    if (index !== this.itemIndex) {
+                        tagDoc.opacity = 1;
+                    }
+                }
                 const hidingIndBef = itemIndexes.find(item => item >= this.itemIndex);
                 if (curDoc.presHideBefore && index === hidingIndBef) {
                     if (index > this.itemIndex) {
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index 4469be488..5e1474b89 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -507,11 +507,12 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 <Tooltip key="arrow" title={<div className="dash-tooltip">{activeItem.groupWithUp ? 'Ungroup' : 'Group with up'}</div>}>
                     <div
                         className="slideButton"
-                        onClick={() => (activeItem.groupWithUp = !activeItem.groupWithUp)}
+                        onClick={() => (activeItem.groupWithUp = (NumCast(activeItem.groupWithUp) + 1) % 3)}
                         style={{
                             zIndex: 1000 - this.indexInPres,
                             fontWeight: 700,
                             backgroundColor: activeItem.groupWithUp ? (presColorBool ? presBoxColor : Colors.MEDIUM_BLUE) : undefined,
+                            outline: NumCast(activeItem.groupWithUp) > 1 ? 'solid black 1px' : undefined,
                             height: activeItem.groupWithUp ? 53 : 18,
                             transform: activeItem.groupWithUp ? 'translate(0, -17px)' : undefined,
                         }}>
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 906dff377..f95d5ac2e 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -68,9 +68,9 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _viewer: React.RefObject<HTMLDivElement> = React.createRef();
-    public _getAnchor: (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>) => Opt<Doc> = () => undefined;
     _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _selectionText: string = '';
+    private _selectionContent: DocumentFragment | undefined;
     private _downX: number = 0;
     private _downY: number = 0;
     private _lastSearch = false;
@@ -78,8 +78,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _ignoreScroll = false;
     private _initialScroll: { loc: Opt<number>; easeFunc: 'linear' | 'ease' | undefined } | undefined;
     private _forcedScroll = true;
+    get _getAnchor() {
+        return AnchorMenu.Instance?.GetAnchor;
+    }
 
     selectionText = () => this._selectionText;
+    selectionContent = () => this._selectionContent;
 
     @observable isAnnotating = false;
     // key where data is stored
@@ -392,7 +396,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
 
     @action
     finishMarquee = (x?: number, y?: number) => {
-        this._getAnchor = AnchorMenu.Instance?.GetAnchor;
         this.isAnnotating = false;
         this._marqueeing = undefined;
         this._textSelecting = true;
@@ -437,7 +440,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 }
             }
         }
-        this._selectionText = selRange.cloneContents().textContent || '';
+        this._selectionContent = selRange.cloneContents();
+        this._selectionText = this._selectionContent?.textContent || '';
 
         // clear selection
         if (sel.empty) {
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 6762665a2..19ffc5005 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1,6 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { saveAs } from 'file-saver';
-import { action, computed, observable, ObservableMap, runInAction } from 'mobx';
+import { action, computed, observable, ObservableMap, ObservableSet, runInAction } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { alias, map, serializable } from 'serializr';
 import { DocServer } from '../client/DocServer';
@@ -343,6 +343,10 @@ export class Doc extends RefField {
     @observable public [DirectLinksSym]: Set<Doc> = new Set();
     @observable public [AnimationSym]: Opt<Doc>;
     @observable public [HighlightSym]: boolean = false;
+    static __Anim(Doc: Doc) {
+        // for debugging to print AnimationSym field easily.
+        return Doc[AnimationSym];
+    }
 
     private [UpdatingFromServer]: boolean = false;
     private [ForceServerWrite]: boolean = false;
@@ -1131,7 +1135,7 @@ export namespace Doc {
         BrushedDoc: ObservableMap<Doc, boolean> = new ObservableMap();
         SearchMatchDoc: ObservableMap<Doc, { searchMatch: number }> = new ObservableMap();
     }
-    const brushManager = new DocBrush();
+    export const brushManager = new DocBrush();
 
     export class DocData {
         @observable _user_doc: Doc = undefined!;
@@ -1284,8 +1288,8 @@ export namespace Doc {
     }
 
     let UnhighlightWatchers: (() => void)[] = [];
-    let UnhighlightTimer: any;
-    export function AddUnlightWatcher(watcher: () => void) {
+    export let UnhighlightTimer: any;
+    export function AddUnHighlightWatcher(watcher: () => void) {
         if (UnhighlightTimer) {
             UnhighlightWatchers.push(watcher);
         } else watcher();
@@ -1302,16 +1306,16 @@ export namespace Doc {
         }, 5000);
     }
 
-    var highlightedDocs = new Set<Doc>();
+    export var highlightedDocs = new ObservableSet<Doc>();
     export function IsHighlighted(doc: Doc) {
         if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
         return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
     }
     export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
         runInAction(() => {
-            doc[AnimationSym] = presEffect;
             highlightedDocs.add(doc);
             doc[HighlightSym] = true;
+            doc[AnimationSym] = presEffect;
             if (dataAndDisplayDocs) {
                 highlightedDocs.add(Doc.GetProto(doc));
                 Doc.GetProto(doc)[HighlightSym] = true;
-- 
cgit v1.2.3-70-g09d2


From 6d32fe60ce32d650a2ba0d5eb8e36dccb591521f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 7 Jan 2023 00:36:06 -0500
Subject: fixed pinning pdf documents without a selection to trail

---
 src/client/views/collections/CollectionSubView.tsx | 2 +-
 src/client/views/collections/CollectionView.tsx    | 1 +
 src/client/views/nodes/trails/PresBox.tsx          | 1 +
 3 files changed, 3 insertions(+), 1 deletion(-)

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

diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 4f3f21cea..d2074219a 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -111,7 +111,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                 rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : [];
             }
 
-            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && !d.unrendered).map(d => d as Doc);
+            const docs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc);
             const viewSpecScript = Cast(this.props.Document.viewSpecScript, ScriptField);
             const childDocs = viewSpecScript ? docs.filter(d => viewSpecScript.script.run({ doc: d }, console.log).result) : docs;
 
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 917d7618c..a28b1ca19 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -43,6 +43,7 @@ interface CollectionViewProps_ extends FieldViewProps {
     layoutEngine?: () => string;
     setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
     setBrushViewer?: (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => void;
+    ignoreUnrendered?: boolean;
 
     // property overrides for child documents
     childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 2e3135416..6f3dc5e82 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -2195,6 +2195,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 PanelHeight={this.panelHeight}
                                 childIgnoreNativeSize={true}
                                 moveDocument={returnFalse}
+                                ignoreUnrendered={true}
                                 //childFitWidth={returnTrue}
                                 childOpacity={returnOne}
                                 childLayoutTemplate={this.childLayoutTemplate}
-- 
cgit v1.2.3-70-g09d2


From 947e9e6634b734dc8cbfbd45db4ecbeef7f5d753 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 9 Jan 2023 15:52:20 -0500
Subject: changed default pres duration of pinned item to 2 secs.  if not
 defined, default is 0.

---
 src/client/views/collections/TabDocView.tsx | 1 +
 src/client/views/nodes/FilterBox.tsx        | 2 +-
 src/client/views/nodes/trails/PresBox.tsx   | 4 ++--
 3 files changed, 4 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index cd58319cb..5c05b5c13 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -253,6 +253,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
             pinDoc.title = doc.title + ' - Slide';
             pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children.  the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data
             pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom;
+            pinDoc.presDuration = 2000;
             pinDoc.groupWithUp = false;
             pinDoc.context = curPres;
             // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements.  That way the PresBox could configure all of its children at render time
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 04d252abe..78ba499ec 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -33,7 +33,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
     constructor(props: Readonly<FieldViewProps>) {
         super(props);
         const targetDoc = FilterBox.targetDoc;
-        if (targetDoc && !targetDoc.currentFilter) targetDoc.currentFilter = FilterBox.createFilterDoc();
+        if (targetDoc && !targetDoc.currentFilter) runInAction(() => (targetDoc.currentFilter = FilterBox.createFilterDoc()));
     }
 
     /// creates a new, empty filter doc
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 6f3dc5e82..c4935da35 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -572,7 +572,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     noSelect: true,
                     originatingDoc: activeItem,
                     easeFunc: StrCast(activeItem.presEaseFunc, 'ease') as any,
-                    zoomTextSelections: true
+                    zoomTextSelections: true,
                 };
                 if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
                 var containerDocContext = srcContext ? [srcContext] : [];
@@ -1250,7 +1250,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             const type = targetDoc.type;
             const transitionSpeed = activeItem.presTransition ? NumCast(activeItem.presTransition) / 1000 : 0.5;
             const zoom = NumCast(activeItem.presZoom, 1) * 100;
-            let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 2;
+            let duration = activeItem.presDuration ? NumCast(activeItem.presDuration) / 1000 : 0;
             if (activeItem.type === DocumentType.AUDIO) duration = NumCast(activeItem.duration);
             const effect = activeItem.presEffect ? activeItem.presEffect : PresMovement.None;
             // activeItem.presMovement = activeItem.presMovement ? activeItem.presMovement : PresMovement.Zoom;
-- 
cgit v1.2.3-70-g09d2


From de02143333177a39851f60c540f5a75a303a1c48 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 13 Jan 2023 11:36:17 -0500
Subject: changed starting presentations to not advance to next slide
 immediately but treat the first slide as if a transition had been made to it.

---
 src/client/views/nodes/trails/PresBox.tsx | 22 +++++++++-------------
 1 file changed, 9 insertions(+), 13 deletions(-)

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

diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index c4935da35..a609b46e4 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -638,14 +638,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     startAutoPres = async (startSlide: number) => {
         if (!this.childDocs.length) return;
         this.layoutDoc.presStatus = PresStatus.Autoplay;
-        this.startPresentation(startSlide + 1 === this.childDocs.length ? 0 : startSlide);
+        this.startPresentation(startSlide);
         clearTimeout(this._presTimer);
         const func = (itemIndex: number) => {
-            if (itemIndex === this.next()) this.layoutDoc.presStatus = PresStatus.Manual;
-            this._presTimer = setTimeout(
-                () => this.layoutDoc.presStatus !== PresStatus.Manual && func(this.itemIndex),
-                NumCast(this.activeItem.presDuration, this.activeItem.type === DocumentType.SCRIPTING ? 0 : 2500) + NumCast(this.activeItem.presTransition)
-            );
+            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));
         };
         func(this.itemIndex);
     };
@@ -1821,6 +1820,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
     }
 
     scrollFocus = () => {
+        this.gotoDocument(0);
         this.startOrPause(false);
         return undefined;
     };
@@ -2073,13 +2073,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
 
     @action
     startOrPause = (makeActive = true) => {
-        if (this.itemIndex + 1 === this.childDocs.length) {
-            this.gotoDocument(0);
-        } else {
-            makeActive && this.updateCurrentPresentation();
-            if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
-            else this.pauseAutoPres();
-        }
+        makeActive && this.updateCurrentPresentation();
+        if (this.layoutDoc.presStatus === PresStatus.Manual || this.layoutDoc.presStatus === PresStatus.Edit) this.startAutoPres(this.itemIndex);
+        else this.pauseAutoPres();
     };
 
     @action
-- 
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/trails/PresBox.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

