From 9e6e0242cccaaa4b683049599cd769ae649a4669 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 14 Nov 2022 22:34:17 -0500
Subject: added an inPlace property for setting up in place content menu
 patterns.

---
 src/client/views/PropertiesButtons.tsx | 44 +++++++++++++++++++++++++++++++---
 1 file changed, 41 insertions(+), 3 deletions(-)

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

diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 80c2c7705..aefc91751 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -2,7 +2,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, Opt } from '../../fields/Doc';
+import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { RichTextField } from '../../fields/RichTextField';
@@ -93,6 +93,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => 'lock'
         );
     }
+    @computed get forceActiveButton() {
+        return this.propertyToggleBtn(
+            'Active',
+            '_forceActive',
+            on => `${on ? 'Select to activate' : 'Contents always active'} `,
+            on => 'eye'
+        );
+    }
     @computed get fitContentButton() {
         return this.propertyToggleBtn(
             'View All',
@@ -101,6 +109,34 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => 'eye'
         );
     }
+    // this implments a container pattern by marking the targetDoc (collection) as an inPlace container,
+    // and then making the contained collection be a "menu" such that when any of its contents are clicked,
+    // they will open their targets in the outer container.  To get back to the "menu", you click on the main container.
+    @computed get inPlaceContainerButton() {
+        return this.propertyToggleBtn(
+            'In Place',
+            'isInPlaceContainer',
+            on => `${on ? 'Make' : 'Remove'} in place container flag`,
+            on => 'window',
+            onClick => {
+                SelectionManager.Views().forEach(dv => {
+                    const containerDoc = dv.rootDoc;
+                    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 = true;
+                        DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
+                        DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
+                            menuItem._isLinkButton = true;
+                            menuItem._followLinkLocation = 'inPlace';
+                        });
+                    }
+                });
+            }
+        );
+    }
     @computed get fitWidthButton() {
         return this.propertyToggleBtn(
             'Fit\xA0Width',
@@ -256,7 +292,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         docView.setToggleDetail();
                         break;
                     case 'linkInPlace':
-                        docView.toggleFollowLink('inPlace', true, false);
+                        docView.toggleFollowLink('inPlace', false, false);
                         break;
                     case 'linkOnRight':
                         docView.toggleFollowLink('add:right', false, false);
@@ -277,7 +313,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             ['nothing', 'Select Document'],
             ['enterPortal', 'Enter Portal'],
             ['toggleDetail', 'Toggle Detail'],
-            ['linkInPlace', 'Follow Link'],
+            ['linkInPlace', 'Open in Place'],
             ['linkOnRight', 'Open Link on Right'],
         ];
         const currentSelection = this.selectedDoc.onClickBehavior;
@@ -360,7 +396,9 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.onClickButton)}
                 {toggle(this.fitWidthButton)}
                 {toggle(this.freezeThumb)}
+                {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
+                {toggle(this.inPlaceContainerButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
                 {toggle(this.maskButton, { display: !isInk ? 'none' : '' })}
                 {toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })}
-- 
cgit v1.2.3-70-g09d2


From 6a6408b120c60a717efb90576e1f14d1ac2cde68 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 14 Nov 2022 22:54:33 -0500
Subject: add noHighlight and noShadow flags

---
 src/client/views/PropertiesButtons.tsx | 4 ++--
 src/client/views/StyleProvider.tsx     | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

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

diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index aefc91751..5992b3eb9 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -121,12 +121,12 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
-                    containerDoc._isLinkButton = containerDoc._fitContentsToBox = containerDoc._forceActive = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
+                    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 = true;
+                        menuDoc._forceActive = menuDoc._fitContentsToBox = menuDoc._isLinkButton = menuDoc._noShadow = menuDoc.noHighlighting = containerDoc._isInPlaceContainer;
                         DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
                             menuItem._isLinkButton = true;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index caee19e14..a04f4a4f4 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -109,7 +109,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
             return allSorts;
         case StyleProp.Highlighting:
-            if (doc) {
+            if (doc && !doc.noHighlighting) {
                 const highlightIndex = Doc.isBrushedHighlightedDegree(doc);
                 const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                 const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
@@ -250,7 +250,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             return docColor;
         }
         case StyleProp.BoxShadow: {
-            if (!doc || opacity() === 0) return undefined; // if it's not visible, then no shadow)
+            if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
             if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
             if (doc?.isLinkButton && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
             switch (doc?.type) {
-- 
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/PropertiesButtons.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 51a85bae74c3d8ccddb57f6063e93b237cdf1ebd Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 15 Nov 2022 14:24:11 -0500
Subject: added playback of audio annotation on inPlace container.  fixed image
 anchors to be marked un-rendered so link lines show up.

---
 src/client/util/DocumentManager.ts       |  2 +-
 src/client/views/PropertiesButtons.tsx   |  2 +-
 src/client/views/nodes/DocumentView.scss |  6 +++---
 src/client/views/nodes/ImageBox.tsx      | 11 +++++++----
 4 files changed, 12 insertions(+), 9 deletions(-)

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

diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index ef0a339ce..519f75dbd 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -252,7 +252,7 @@ export class DocumentManager {
         } else {
             if (!targetDocContext) {
                 // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
-                createViewFunc(Doc.BrushDoc(targetDoc), finished); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
+                createViewFunc(Doc.BrushDoc(targetDoc), () => focusAndFinish(true)); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
             } else {
                 // otherwise try to get a view of the context of the target
                 if (targetDocContextView) {
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index d27848a90..656a56a15 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -138,7 +138,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                             DocUtils.MakeLink({ doc: dv.rootDoc }, { doc: menuDoc }, 'back link to container');
                         }
                         DocListCast(menuDoc[Doc.LayoutFieldKey(menuDoc)]).forEach(menuItem => {
-                            menuItem.followAllLinks = menuItem._isLinkButton = true;
+                            menuItem.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true;
                             menuItem._followLinkLocation = 'inPlace';
                         });
                     }
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index e437d295e..01188d3fa 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-contentsView {
-            opacity: 0.5;
-        }
+        // > .documentView-contentsView {
+        //     opacity: 0.5;
+        // }
         > .documentView-captionWrapper {
             opacity: 1;
         }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index d67481b22..8ecd8104a 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -84,6 +84,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             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));
             }
@@ -93,9 +94,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     }; // 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 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);
+        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) });
         if (anchor) {
+            anchor._useAlt = Cast(this.layoutDoc[this.fieldKey + '-useAlt'], 'boolean', null);
             anchor._annotations = new List<Doc>(DocListCast(this.dataDoc[this.annotationKey]));
             this.addDocument(anchor);
             return anchor;
@@ -222,7 +225,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: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, 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' });
@@ -353,7 +356,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         TraceMobx();
 
         const srcpath = this.paths[0];
-        const fadepath = this.paths[Math.min(1, this.paths.length - 1)];
+        const fadepath = this.paths.lastElement();
         const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
         const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
         const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
-- 
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/PropertiesButtons.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 e17b1bdb09bfcadc717e687b09d2c18596341a10 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 9 Feb 2023 21:15:58 -0500
Subject: fixed childLayoutString to work.  made images capable of fitWidth.
 fixed animating data field pres changes.  fixed lightbox to ignore
 annotations on collections.  fixed double-click on icon to open in lightbox.
 added options for turning off ink labels, and opening ink in lightbox. fixed
 closing ink strokes by dragging. fixed drawing ink to use coord sys of
 starting point and to render ink the correct width and to honor
 GestureOverlay mode properly. .

---
 src/client/documents/Documents.ts                  |  4 +-
 src/client/util/CurrentUserUtils.ts                |  6 +-
 src/client/util/SettingsManager.tsx                |  4 ++
 src/client/views/DocumentDecorations.tsx           | 13 ++--
 src/client/views/GestureOverlay.tsx                | 22 ++++---
 src/client/views/GlobalKeyHandler.ts               | 29 ++++++++-
 src/client/views/InkTranscription.tsx              |  2 +-
 src/client/views/InkingStroke.tsx                  | 12 ++--
 src/client/views/LightboxView.tsx                  | 64 +++++++++----------
 src/client/views/PropertiesButtons.tsx             | 10 +++
 src/client/views/PropertiesView.tsx                |  2 +-
 src/client/views/StyleProvider.tsx                 |  4 +-
 src/client/views/collections/CollectionView.tsx    |  2 +-
 src/client/views/collections/TabDocView.tsx        | 14 ++---
 src/client/views/collections/TreeView.tsx          |  4 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 66 +++++++++++--------
 .../collections/collectionFreeForm/MarqueeView.tsx |  4 +-
 src/client/views/nodes/DocumentView.tsx            | 21 ++++---
 src/client/views/nodes/ImageBox.tsx                | 73 +++++++++++++++++-----
 src/client/views/nodes/LinkAnchorBox.tsx           |  1 +
 src/client/views/nodes/MapBox/MapBox.tsx           |  4 +-
 src/client/views/nodes/WebBox.tsx                  | 11 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  1 -
 src/client/views/nodes/trails/PresBox.tsx          |  4 ++
 src/client/views/pdf/AnchorMenu.tsx                |  1 +
 src/client/views/pdf/PDFViewer.tsx                 | 17 ++---
 src/fields/util.ts                                 |  2 +-
 27 files changed, 257 insertions(+), 140 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 692d09629..3faa6e11d 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -149,6 +149,8 @@ export class DocumentOptions {
     _height?: NUMt = new NumInfo('displayed height of document');
     _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)');
     _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)');
+    _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers');
+    _nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers');
     _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height");
     _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units");
     _fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)');
@@ -848,7 +850,7 @@ export namespace Docs {
 
         export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
             const imgField = url instanceof ImageField ? url : new ImageField(url);
-            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
+            return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { _nativeDimModifiable: false, _nativeHeightUnfrozen: false, title: basename(imgField.url.href), ...options }, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function PresDocument(options: DocumentOptions = {}) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 5f183cf91..f678c8936 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -183,7 +183,7 @@ export class CurrentUserUtils {
             const allopts = {system: true, ...opts};
             return DocUtils.AssignScripts(  (curIcon?.iconTemplate === opts.iconTemplate ? 
                       DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))),
-                {onClick:"deiconifyView(documentView)"});
+                {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView"});
         };
         const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({
             textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px",  _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts
@@ -803,7 +803,9 @@ export class CurrentUserUtils {
         doc._showLabel ?? (doc._showLabel = true);
         doc.textAlign ?? (doc.textAlign = "left");
         doc.activeTool = InkTool.None;
-        doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");;
+        doc.openInkInLightbox ?? (doc.openInkInLightbox = false);
+        doc.activeInkHideTextLabels ?? (doc.activeInkHideTextLabels = false);
+        doc.activeInkColor ?? (doc.activeInkColor = "rgb(0, 0, 0)");
         doc.activeInkWidth ?? (doc.activeInkWidth = 1);
         doc.activeInkBezier ?? (doc.activeInkBezier = "0");
         doc.activeFillColor ?? (doc.activeFillColor = "");
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index 179a1ac39..6c823e80a 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -199,6 +199,10 @@ export class SettingsManager extends React.Component<{}> {
                     <input type="checkbox" onChange={e => (Doc.UserDoc().activeInkHideTextLabels = !Doc.UserDoc().activeInkHideTextLabels)} checked={BoolCast(Doc.UserDoc().activeInkHideTextLabels)} />
                     <div className="preferences-check">Hide Labels In Ink Shapes</div>
                 </div>
+                <div>
+                    <input type="checkbox" onChange={e => (Doc.UserDoc().openInkInLightbox = !Doc.UserDoc().openInkInLightbox)} checked={BoolCast(Doc.UserDoc().openInkInLightbox)} />
+                    <div className="preferences-check">Open Ink Docs in Lightbox</div>
+                </div>
             </div>
         );
     }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index d1f0bf2ac..41f4a17fb 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -581,7 +581,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     }
                     let actualdW = Math.max(width + dW * scale, 20);
                     let actualdH = Math.max(height + dH * scale, 20);
-                    const fixedAspect = nwidth && nheight && (!doc._fitWidth || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
+                    const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false;
+                    const fixedAspect = nwidth && nheight && (!doc._fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable);
                     if (fixedAspect) {
                         if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) {
                             if (dragRight && modifyNativeDim) {
@@ -589,7 +590,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     doc._nativeWidth = (actualdW / (doc._width || 1)) * Doc.NativeWidth(doc);
                                 }
                             } else {
-                                if (!doc._fitWidth) {
+                                if (!doc._fitWidth || preserveNativeDim) {
                                     actualdH = (nheight / nwidth) * actualdW;
                                     doc._height = actualdH;
                                 } else if (!modifyNativeDim || dragBotRight) doc._height = actualdH;
@@ -597,11 +598,13 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                             doc._width = actualdW;
                         } else {
                             if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) {
-                                // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height.  But they are marked to allow their nativeHeight to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
+                                // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height.  But they are marked to allow their nativeHeight
+                                // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match
+                                // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used)
                                 doc._nativeHeight = (actualdH / (doc._height || 1)) * Doc.NativeHeight(doc);
                                 doc._autoHeight = false;
                             } else {
-                                if (!doc._fitWidth) {
+                                if (!doc._fitWidth || preserveNativeDim) {
                                     actualdW = (nwidth / nheight) * actualdH;
                                     doc._width = actualdW;
                                 } else if (!modifyNativeDim || dragBotRight) doc._width = actualdW;
@@ -615,7 +618,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         const rotCtr = [NumCast(doc._width) / 2, NumCast(doc._height) / 2];
                         const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI);
 
-                        const maxHeight = doc.nativHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
+                        const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling();
                         dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH);
                         dW && (doc._width = actualdW);
                         dH && (doc._autoHeight = false);
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e3328fb4c..6058eaaf9 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -1,9 +1,9 @@
 import React = require('react');
 import * as fitCurve from 'fit-curve';
-import { action, computed, observable, runInAction, trace } from 'mobx';
+import { action, computed, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../fields/Doc';
 import { InkData, InkTool } from '../../fields/InkField';
-import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
 import { Cast, FieldValue, NumCast } from '../../fields/Types';
 import MobileInkOverlay from '../../mobile/MobileInkOverlay';
@@ -14,7 +14,6 @@ import { CognitiveServices } from '../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { SelectionManager } from '../util/SelectionManager';
 import { Transform } from '../util/Transform';
 import './GestureOverlay.scss';
 import {
@@ -39,7 +38,6 @@ import { RadialMenu } from './nodes/RadialMenu';
 import HorizontalPalette from './Palette';
 import { Touchable } from './Touchable';
 import TouchScrollableMenu, { TouchScrollableMenuItem } from './TouchScrollableMenu';
-import { observer } from 'mobx-react';
 
 interface GestureOverlayProps {
     isActive: boolean;
@@ -47,6 +45,7 @@ interface GestureOverlayProps {
 @observer
 export class GestureOverlay extends Touchable<GestureOverlayProps> {
     static Instance: GestureOverlay;
+    static Instances: GestureOverlay[] = [];
 
     @observable public InkShape: Opt<GestureUtils.Gestures>;
     @observable public SavedColor?: string;
@@ -66,6 +65,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     @observable private _clipboardDoc?: JSX.Element;
     @observable private _possibilities: JSX.Element[] = [];
 
+    public static DownDocView: DocumentView | undefined;
+
     @computed private get height(): number {
         return 2 * Math.max(this._pointerY && this._thumbY ? this._thumbY - this._pointerY : 100, 100);
     }
@@ -89,7 +90,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     constructor(props: any) {
         super(props);
 
-        GestureOverlay.Instance = this;
+        GestureOverlay.Instances.push(this);
     }
 
     static setupThumbButtons(doc: Doc) {
@@ -154,7 +155,13 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
         }
         return Cast(userDoc.thumbDoc, Doc);
     }
+
+    componentWillUnmount() {
+        GestureOverlay.Instances.splice(GestureOverlay.Instances.indexOf(this), 1);
+        GestureOverlay.Instance = GestureOverlay.Instances.lastElement();
+    }
     componentDidMount = () => {
+        GestureOverlay.Instance = this;
         this._thumbDoc = FieldValue(Cast(GestureOverlay.setupThumbDoc(Doc.UserDoc()), Doc));
         this._inkToTextDoc = FieldValue(Cast(this._thumbDoc?.inkToTextDoc, Doc));
     };
@@ -627,6 +634,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     }
     @action
     onPointerUp = (e: PointerEvent) => {
+        GestureOverlay.DownDocView = undefined;
         if (this._points.length > 1) {
             const B = this.svgBounds;
             const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
@@ -906,8 +914,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> {
     }
 
     @computed get elements() {
-        const selView = SelectionManager.Views().lastElement();
-        const width = (Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1)) / (selView?.props.ScreenToLocalTransform().Scale || 1);
+        const selView = GestureOverlay.DownDocView;
+        const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1); // * (selView?.props.ScreenToLocalTransform().Scale || 1);
         const rect = this._overlayRef.current?.getBoundingClientRect();
         const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true);
         B.left = B.left - width / 2;
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 5e700e281..6c8a078ec 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -1,12 +1,12 @@
 import { random } from 'lodash';
-import { action, observable, runInAction } from 'mobx';
+import { action, runInAction } from 'mobx';
 import { DateField } from '../../fields/DateField';
 import { Doc, DocListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkTool } from '../../fields/InkField';
 import { List } from '../../fields/List';
 import { ScriptField } from '../../fields/ScriptField';
-import { Cast, DocCast, PromiseValue } from '../../fields/Types';
+import { Cast, PromiseValue } from '../../fields/Types';
 import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
 import { DocumentType } from '../documents/DocumentTypes';
@@ -18,6 +18,7 @@ import { SharingManager } from '../util/SharingManager';
 import { SnappingManager } from '../util/SnappingManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { CollectionDockingView } from './collections/CollectionDockingView';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
 import { CollectionFreeFormViewChrome } from './collections/CollectionMenu';
 import { CollectionStackedTimeline } from './collections/CollectionStackedTimeline';
 import { ContextMenu } from './ContextMenu';
@@ -103,6 +104,20 @@ export class KeyManager {
 
                 const groupings = SelectionManager.Views().slice();
                 const randomGroup = random(0, 1000);
+                const collectionView = groupings.reduce(
+                    (col, g) => (col === null || g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView === col ? g.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView : undefined),
+                    null as null | undefined | CollectionFreeFormView
+                );
+                if (collectionView) {
+                    UndoManager.RunInBatch(() => {
+                        collectionView._marqueeViewRef.current?.collection(
+                            e,
+                            true,
+                            groupings.map(g => g.rootDoc)
+                        );
+                    }, 'grouping');
+                    break;
+                }
                 UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group');
                 SelectionManager.DeselectAll();
                 break;
@@ -192,6 +207,16 @@ export class KeyManager {
             case 'arrowdown':
                 UndoManager.RunInBatch(() => SelectionManager.Views().map(dv => dv.props.CollectionFreeFormDocumentView?.().nudge?.(0, 10)), 'nudge down');
                 break;
+            case 'g':
+                if (document.activeElement?.tagName === 'INPUT' || document.activeElement?.tagName === 'TEXTAREA') {
+                    return { stopPropagation: false, preventDefault: false };
+                }
+
+                const groupings = SelectionManager.Views().slice();
+                const randomGroup = random(0, 1000);
+                UndoManager.RunInBatch(() => groupings.map(dv => (dv.layoutDoc.group = randomGroup)), 'group');
+                SelectionManager.DeselectAll();
+                break;
         }
 
         return {
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index bf0e8081d..246b887a6 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -67,7 +67,7 @@ export class InkTranscription extends React.Component {
                 : null;
         }
 
-        r.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
+        r?.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef));
 
         return (this._mathRef = r);
     };
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 7a5151634..d7e8b1c05 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -54,7 +54,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         return FieldView.LayoutString(InkingStroke, fieldStr);
     }
     public static IsClosed(inkData: InkData) {
-        return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
+        return inkData?.length && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
     }
     private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
     private _disposers: { [key: string]: IReactionDisposer } = {};
@@ -264,9 +264,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
             .map(p => ({ X: p[0], Y: p[1] }));
         const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
 
-        this._nearestT = nearestT;
-        this._nearestSeg = nearestSeg;
-        this._nearestScrPt = nearestPt;
+        if (distance < 40) {
+            this._nearestT = nearestT;
+            this._nearestSeg = nearestSeg;
+            this._nearestScrPt = nearestPt;
+        } else {
+            this._nearestT = this._nearestSeg = this._nearestScrPt = undefined;
+        }
     };
 
     /**
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 3627aa783..e531bf71c 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -27,6 +27,13 @@ interface LightboxViewProps {
     maxBorder: number[];
 }
 
+type LightboxSavedState = {
+    panX: Opt<number>;
+    panY: Opt<number>;
+    scale: Opt<number>;
+    scrollTop: Opt<number>;
+    layoutKey: Opt<string>;
+};
 @observer
 export class LightboxView extends React.Component<LightboxViewProps> {
     @computed public static get LightboxDoc() {
@@ -34,21 +41,22 @@ export class LightboxView extends React.Component<LightboxViewProps> {
     }
     private static LightboxDocTemplate = () => LightboxView._layoutTemplate;
     @observable private static _layoutTemplate: Opt<Doc>;
+    @observable private static _layoutTemplateString: Opt<string>;
     @observable private static _doc: Opt<Doc>;
     @observable private static _docTarget: Opt<Doc>;
     @observable private static _docFilters: string[] = []; // filters
-    private static _savedState: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }>;
+    private static _savedState: Opt<LightboxSavedState>;
     private static _history: Opt<{ doc: Doc; target?: Doc }[]> = [];
     @observable private static _future: Opt<Doc[]> = [];
     @observable private static _docView: Opt<DocumentView>;
-    private static openInTabFunc: any;
-    static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<{ panX: Opt<number>; panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number> }> }[] = [];
-    @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc) {
+    static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = [];
+    @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) {
         if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) {
-            this.LightboxDoc._panX = this._savedState.panX;
-            this.LightboxDoc._panY = this._savedState.panY;
-            this.LightboxDoc._scrollTop = this._savedState.scrollTop;
-            this.LightboxDoc._viewScale = this._savedState.scale;
+            if (this._savedState.panX !== undefined) this.LightboxDoc._panX = this._savedState.panX;
+            if (this._savedState.panY !== undefined) this.LightboxDoc._panY = this._savedState.panY;
+            if (this._savedState.scrollTop !== undefined) this.LightboxDoc._scrollTop = this._savedState.scrollTop;
+            if (this._savedState.scale !== undefined) this.LightboxDoc._viewScale = this._savedState.scale;
+            this.LightboxDoc.layoutKey = this._savedState.layoutKey;
         }
         if (!doc) {
             this._docFilters && (this._docFilters.length = 0);
@@ -69,10 +77,11 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]);
             if (doc !== LightboxView.LightboxDoc) {
                 this._savedState = {
-                    panX: Cast(doc._panX, 'number', null),
-                    panY: Cast(doc._panY, 'number', null),
-                    scale: Cast(doc._viewScale, 'number', null),
-                    scrollTop: Cast(doc._scrollTop, 'number', null),
+                    layoutKey: StrCast(doc.layoutKey),
+                    panX: Cast(doc.panX, 'number', null),
+                    panY: Cast(doc.panY, 'number', null),
+                    scale: Cast(doc.viewScale, 'number', null),
+                    scrollTop: Cast(doc.scrollTop, 'number', null),
                 };
             }
         }
@@ -87,7 +96,10 @@ export class LightboxView extends React.Component<LightboxViewProps> {
             ];
         }
         this._doc = doc;
-        this._layoutTemplate = layoutTemplate;
+        this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined;
+        if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) {
+            doc.layoutKey = layoutTemplate;
+        }
         this._docTarget = target || doc;
 
         return true;
@@ -132,7 +144,7 @@ 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: OpenWhere, layoutTemplate?: Doc, openInTabFunc?: any) => {
+    public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
         if (location !== OpenWhere.lightbox) {
             const inPlaceView = DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
             if (inPlaceView) {
@@ -140,12 +152,13 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 return true;
             }
         }
-        LightboxView.openInTabFunc = openInTabFunc;
         SelectionManager.DeselectAll();
         return LightboxView.SetLightboxDoc(
             doc,
             undefined,
-            [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '-annotations']), ...(LightboxView._future ?? [])].sort((a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)),
+            [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '-annotations']).filter(anno => anno.annotationOn !== doc), ...(LightboxView._future ?? [])].sort(
+                (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow)
+            ),
             layoutTemplate
         );
     };
@@ -193,8 +206,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
         if (docView) {
             LightboxView._docTarget = target;
-            if (!target) docView.ComponentView?.shrinkWrap?.();
-            else docView.focus(target, { willPanZoom: true, zoomScale: 0.9 });
+            target && docView.focus(target, { willPanZoom: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
@@ -218,7 +230,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 .filter(doc => doc)
                 .map(doc => doc!);
             LightboxView.SetLightboxDoc(coll, undefined, contents.length ? contents : links);
-            TabDocView.PinDoc(coll, { hidePresBox: true });
         }
     };
 
@@ -251,19 +262,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
 
                     <GestureOverlay isActive={true}>
                         <DocumentView
-                            ref={action((r: DocumentView | null) => {
-                                LightboxView._docView = r !== null ? r : undefined;
-                                r &&
-                                    setTimeout(
-                                        action(() => {
-                                            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?.();
-                                            //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
-                                        })
-                                    );
-                            })}
+                            ref={action((r: DocumentView | null) => (LightboxView._docView = r !== null ? r : undefined))}
                             Document={LightboxView.LightboxDoc}
                             DataDoc={undefined}
                             PanelWidth={this.lightboxWidth}
@@ -332,7 +331,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                     onClick={e => {
                         e.stopPropagation();
                         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/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 66c3ed439..65a950711 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -77,6 +77,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc)
         );
     }
+    @computed get hideImageButton() {
+        return this.propertyToggleBtn(
+            'Background',
+            '_hideImage',
+            on => (on ? 'Show Image' : 'Show Background'),
+            on => 'portrait'
+        );
+    }
     @computed get clustersButton() {
         return this.propertyToggleBtn(
             'Clusters',
@@ -383,6 +391,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
         const layoutField = this.selectedDoc?.[Doc.LayoutFieldKey(this.selectedDoc)];
         const isText = layoutField instanceof RichTextField;
         const isInk = layoutField instanceof InkField;
+        const isImage = layoutField instanceof ImageField;
         const isMap = this.selectedDoc?.type === DocumentType.MAP;
         const isCollection = this.selectedDoc?.type === DocumentType.COL;
         //TODO: will likely need to create separate note-taking view type here
@@ -410,6 +419,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.inPlaceContainerButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
                 {toggle(this.maskButton, { display: !isInk ? 'none' : '' })}
+                {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })}
                 {toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })}
                 {toggle(this.gridButton, { display: !isCollection ? 'none' : '' })}
                 {toggle(this.groupButton, { display: isTabView || !isCollection ? 'none' : '' })}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 411f51d84..a2bc37095 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -143,7 +143,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         : layoutDoc._fitWidth
                         ? !Doc.NativeHeight(this.dataDoc)
                             ? NumCast(this.props.height)
-                            : Math.min((this.docWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))
+                            : Math.min((this.docWidth() * Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height))
                         : NumCast(layoutDoc._height) || 50
                 )
             );
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index ece224c68..3cb920ba0 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -269,9 +269,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                 default:
                     return doc.z
                         ? `#9c9396  ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow
-                        : props?.ContainingCollectionDoc?._useClusters && doc.type !== DocumentType.INK
+                        : props?.ContainingCollectionDoc?._useClusters
                         ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
-                        : NumCast(doc.group, -1) !== -1 && doc.type !== DocumentType.INK
+                        : NumCast(doc.group, -1) !== -1
                         ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(isBackground() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent
                         : isBackground()
                         ? undefined // if it's a background & has a cluster color, make the shadow spread really big
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a28b1ca19..90406ed3c 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -256,7 +256,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
     childHideDecorationTitle = () => this.props.childHideDecorationTitle?.() ?? BoolCast(this.Document.childHideDecorationTitle);
     childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
     @computed get childLayoutString() {
-        return StrCast(this.rootDoc.childLayoutString);
+        return StrCast(this.rootDoc.childLayoutString, this.props.childLayoutString);
     }
 
     isContentActive = (outsideReaction?: boolean) => this.props.isContentActive();
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 25fccd89c..bf8d449ea 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -274,7 +274,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
                 pinDoc.presStartTime = NumCast(doc.clipStart);
                 pinDoc.presEndTime = NumCast(doc.clipEnd, duration);
             }
-            PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, pinDoc);
+            PresBox.pinDocView(pinDoc, pinProps.pinDocContent ? { ...pinProps, pinData: PresBox.pinDataTypes(doc) } : pinProps, DocCast(pinDoc.proto));
             pinDoc.onClick = ScriptField.MakeFunction('navigateToDoc(self.presentationTargetDoc, self)');
             Doc.AddDocToList(curPres, 'data', pinDoc, presSelected);
             //save position
@@ -359,7 +359,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         // prettier-ignore
         switch (whereFields[0]) {
             case OpenWhere.inPlace:  // fall through to lightbox
-            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location, undefined, this.addDocTab);
+            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
             case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
@@ -382,14 +382,8 @@ 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?.willPanZoom !== false && shrinkwrap && this._document) {
-            const focusSpeed = options.zoomTime ?? 500;
-            shrinkwrap();
-            this._view?.setViewTransition('transform', focusSpeed, () => options?.afterFocus?.(false));
-        } else {
-            options?.afterFocus?.(false);
-        }
+        options?.afterFocus?.(false);
+
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
             this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index bd326f917..2398d8f58 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -410,7 +410,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                 if (aspect) return this.embeddedPanelWidth() / (aspect || 1);
                 return layoutDoc._fitWidth
                     ? !Doc.NativeHeight(layoutDoc)
-                        ? NumCast(layoutDoc._height) //this.props.containerCollection._height)
+                        ? NumCast(layoutDoc._height)
                         : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.containerCollection._height)))
                     : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym]();
             })()
@@ -957,6 +957,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         );
     };
 
+    fitWidthFilter = (doc: Doc) => (doc.type === DocumentType.IMG ? false : undefined);
     renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
         return (
             <div style={{ height: this.embeddedPanelHeight(), width: this.embeddedPanelWidth() }}>
@@ -965,6 +966,7 @@ export class TreeView extends React.Component<TreeViewProps> {
                     ref={action((r: DocumentView | null) => (this._dref = r))}
                     Document={this.doc}
                     DataDoc={undefined}
+                    fitWidth={this.fitWidthFilter}
                     PanelWidth={this.embeddedPanelWidth}
                     PanelHeight={this.embeddedPanelHeight}
                     LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 51672578e..d6e95f97f 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -16,7 +16,7 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnTransparent, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
@@ -61,6 +61,7 @@ export type collectionFreeformViewProps = {
     scaleField?: string;
     noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale)
     engineProps?: any;
+    getScrollHeight?: () => number | undefined;
     dontScaleFilter?: (doc: Doc) => boolean; // whether this collection should scale documents to fit their panel vs just scrolling them
     dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not.
     // However, this screws up interactions since only the top layer gets events.  so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents.
@@ -363,10 +364,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 const grouping = this.props.Document._useClusters ? NumCast(cd.cluster, -1) : NumCast(cd.group, -1);
                 if (grouping !== -1) {
                     const layoutDoc = Doc.Layout(cd);
-                    const cx = NumCast(cd.x) - this._clusterDistance;
-                    const cy = NumCast(cd.y) - this._clusterDistance;
-                    const cw = NumCast(layoutDoc._width) + 2 * this._clusterDistance;
-                    const ch = NumCast(layoutDoc._height) + 2 * this._clusterDistance;
+                    const cx = NumCast(cd.x) - this._clusterDistance / 2;
+                    const cy = NumCast(cd.y) - this._clusterDistance / 2;
+                    const cw = NumCast(layoutDoc._width) + this._clusterDistance;
+                    const ch = NumCast(layoutDoc._height) + this._clusterDistance;
                     return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster;
                 }
                 return cluster;
@@ -545,7 +546,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     case InkTool.None:
                         if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
                             this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false);
                         }
                         break;
                 }
@@ -771,7 +772,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         } else if (!e.cancelBubble) {
             if (this.tryDragCluster(e, this._hitCluster)) {
                 return true;
-            } else this.pan(e);
+            }
+            this.pan(e);
         }
         return false;
     };
@@ -1004,6 +1006,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (deltaScale * invTransform.Scale > 20) {
             deltaScale = 20 / invTransform.Scale;
         }
+        if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
+            return;
+        }
         if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) {
             deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale;
         }
@@ -1012,29 +1017,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) {
             const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20);
             this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);
-            this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);
+            this.setPan(-localTransform.TranslateX / safeScale, NumCast(this.props.Document.scrollTop) * safeScale || -localTransform.TranslateY / safeScale);
         }
     };
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
+        if (this.Document._isGroup) return; // group style collections neither pan nor zoom
         PresBox.Instance?.pauseAutoPres();
         if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return;
         e.stopPropagation();
         e.preventDefault();
         switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) {
             case freeformScrollMode.Pan:
-                // if shift is selected then zoom
+                // if ctrl is selected then zoom
                 if (e.ctrlKey) {
-                    if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
-                        // things that can scroll vertically should do that instead of zooming
-                    } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
+                    if (this.props.isContentActive(true)) {
                         !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
                     }
-                    // otherwise pan
-                } else if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) {
-                    // things that can scroll vertically should do that instead of zooming
-                } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
+                } // otherwise pan
+                else if (this.props.isContentActive(true)) {
                     const dx = -e.deltaX;
                     const dy = -e.deltaY;
                     if (e.shiftKey) {
@@ -1046,9 +1048,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 break;
             default:
             case freeformScrollMode.Zoom:
-                if (!e.ctrlKey && (this.props.Document.scrollHeight ?? this.props.DocumentView?.().ComponentView?.getScrollHeight?.()) !== undefined) {
-                    // things that can scroll vertically should do that instead of zooming
-                } else if (this.props.isContentActive(true) && !this.Document._isGroup) {
+                if (this.props.isContentActive(true)) {
                     !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
                 }
                 break;
@@ -1096,7 +1096,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const minPanX = NumCast(this.rootDoc._panXMin, 0);
             const minPanY = NumCast(this.rootDoc._panYMin, 0);
             const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX));
-            const newPanY = Math.min(this.props.Document.scrollHeight !== undefined ? NumCast(this.Document.scrollHeight) : minPanY + (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, this.nativeHeight), Math.max(minPanY, panY));
+            const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale;
+            const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight;
+            const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight();
+            const maxPanY =
+                minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by fitWidth
+                (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) +
+                (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning
+            let newPanY = Math.max(minPanY, Math.min(maxPanY, panY));
+            if (NumCast(this.rootDoc.scrollTop) && NumCast(this.rootDoc._viewScale, minScale) !== minScale) {
+                const relTop = NumCast(this.rootDoc.scrollTop) / maxScrollTop;
+                this.rootDoc.scrollTop = undefined;
+                newPanY = minPanY + relTop * (maxPanY - minPanY);
+            } else if (fitYscroll && this.rootDoc.scrollTop === undefined && NumCast(this.rootDoc._viewScale, minScale) === minScale) {
+                const maxPanY = minPanY + fitYscroll;
+                const relTop = (panY - minPanY) / (maxPanY - minPanY);
+                setTimeout(() => {
+                    this.rootDoc.scrollTop = relTop * maxScrollTop;
+                }, 10);
+                newPanY = minPanY;
+            }
             !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
             !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
         }
@@ -1169,7 +1188,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 || doc.type === DocumentType.MARKER) {
+        if (this.props.getScrollHeight || 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();
@@ -2033,10 +2052,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             : (this.props.pointerEvents?.() as any),
                     transform: `scale(${this.nativeDimScaling || 1})`,
                     width: `${100 / (this.nativeDimScaling || 1)}%`,
-                    height:
-                        this.isAnnotationOverlay && (this.Document.scrollHeight || this.props.DocumentView?.().ComponentView?.getScrollHeight?.())
-                            ? NumCast(this.Document.scrollHeight || this.props.DocumentView?.().ComponentView?.getScrollHeight?.())
-                            : `${100 / (this.nativeDimScaling || 1)}%`, // : this.isAnnotationOverlay ? (this.Document.scrollHeight ? this.Document.scrollHeight : "100%") : this.props.PanelHeight()
+                    height: this.props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`,
                 }}>
                 {this._firstRender ? this.placeholder : this.marqueeView}
                 {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 0b7854926..bc3b17cd9 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -431,8 +431,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @undoBatch
     @action
-    collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => {
-        const selected = this.marqueeSelect(false);
+    collection = (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => {
+        const selected = selection ?? this.marqueeSelect(false);
         const activeFrame = selected.reduce((v, d) => v ?? Cast(d._activeFrame, 'number', null), undefined as number | undefined);
         if (e instanceof KeyboardEvent ? 'cg'.includes(e.key) : true) {
             this.props.removeDocument?.(selected);
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b94db2c6b..36c0240f1 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -56,6 +56,7 @@ import { ScriptingBox } from './ScriptingBox';
 import { PresEffect, PresEffectDirection } from './trails';
 import { PinProps } from './trails/PresBox';
 import React = require('react');
+import { GestureOverlay } from '../GestureOverlay';
 const { Howl } = require('howler');
 
 interface Window {
@@ -139,7 +140,6 @@ export interface DocComponentView {
     fieldKey?: string;
     annotationKey?: string;
     getTitle?: () => string;
-    getScrollHeight?: () => number;
     getCenter?: (xf: Transform) => { X: number; Y: number };
     ptToScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
     ptFromScreen?: (pt: { X: number; Y: number }) => { X: number; Y: number };
@@ -170,7 +170,7 @@ export interface DocumentViewSharedProps {
     dataTransition?: string; // specifies animation transition - used by collectionPile and potentially other layout engines when changing the size of documents so that the change won't be abrupt
     styleProvider: Opt<StyleProviderFunc>;
     focus: DocFocusFunc;
-    fitWidth?: (doc: Doc) => boolean;
+    fitWidth?: (doc: Doc) => boolean | undefined;
     docFilters: () => string[];
     docRangeFilters: () => string[];
     searchFilterDocs: () => Doc[];
@@ -637,8 +637,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                             console.log
                         );
                     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, OpenWhere.lightbox, this.props.LayoutTemplate?.(), this.props.addDocTab), 'double tap');
+                } else if (!Doc.IsSystem(this.rootDoc) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) {
+                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
@@ -669,7 +669,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     this._timeout = setTimeout(() => {
                         this._timeout = undefined;
                         clickFunc();
-                    }, 350);
+                    }, 150);
                 } else clickFunc();
             } else if (!this._longPress && this.allLinks.length && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
                 SelectionManager.DeselectAll();
@@ -698,6 +698,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        if (!(e.nativeEvent as any).DownDocView) (e.nativeEvent as any).DownDocView = GestureOverlay.DownDocView = this.props.DocumentView();
         if (this.rootDoc.type === DocumentType.INK && Doc.ActiveTool === InkTool.Eraser) return;
         // continue if the event hasn't been canceled AND we are using a mouse or this has an onClick or onDragStart function (meaning it is a button document)
         if (!(InteractionUtils.IsType(e, InteractionUtils.MOUSETYPE) || [InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool))) {
@@ -1640,7 +1641,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
         return this.docView?.LayoutFieldKey || 'layout';
     }
     get fitWidth() {
-        return this.props.fitWidth?.(this.rootDoc) || this.layoutDoc.fitWidth;
+        return this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth;
     }
 
     @computed get hideLinkButton() {
@@ -1688,8 +1689,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     }
     @computed get panelHeight() {
         if (this.effectiveNativeHeight && !this.layoutDoc.nativeHeightUnfrozen) {
-            const scrollHeight = this.fitWidth ? Math.max(this.ComponentView?.getScrollHeight?.() ?? NumCast(this.layoutDoc.scrollHeight)) : 0;
-            return Math.min(this.props.PanelHeight(), Math.max(scrollHeight, this.effectiveNativeHeight) * this.nativeScaling);
+            return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling);
         }
         return this.props.PanelHeight();
     }
@@ -1902,6 +1902,11 @@ ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
     documentView.select(false);
 });
 
+ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView) {
+    //documentView.iconify(() =>
+    LightboxView.AddDocTab(documentView.rootDoc, OpenWhere.lightbox, 'layout'); //, 0);
+});
+
 ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) {
     if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout');
     else dv.switchViews(true, detailLayoutKeySuffix, undefined, true);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index bdd99528b..540958941 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
 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';
+import { DashColor, 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 { Docs, DocUtils } from '../../documents/Documents';
@@ -26,13 +26,15 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, OpenWhere } from './DocumentView';
 import { FaceRectangles } from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import { PresBox } from './trails';
 import React = require('react');
 import Color = require('color');
+import { LinkDocPreview } from './LinkDocPreview';
+import { DocumentManager } from '../../util/DocumentManager';
 
 export const pageSchema = createSchema({
     googlePhotosUrl: 'string',
@@ -51,6 +53,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(ImageBox, fieldKey);
     }
+    private _ignoreScroll = false;
+    private _forcedScroll = false;
     private _dropDisposer?: DragManager.DragDropDisposer;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _getAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = () => undefined;
@@ -119,6 +123,16 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             },
             { fireImmediately: true }
         );
+        this._disposers.scroll = reaction(
+            () => this.layoutDoc._scrollTop,
+            s_top => {
+                this._forcedScroll = true;
+                !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top));
+                this._mainCont.current?.scrollTo({ top: NumCast(s_top) });
+                this._forcedScroll = false;
+            },
+            { fireImmediately: true }
+        );
     }
 
     componentWillUnmount() {
@@ -155,6 +169,21 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @undoBatch
     setUseAlt = () => (this.layoutDoc[this.fieldKey + '-useAlt'] = !this.layoutDoc[this.fieldKey + '-useAlt']);
 
+    @undoBatch
+    setNativeSize = action(() => {
+        const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._viewScale, 1);
+        const nscale = NumCast(this.props.PanelWidth()) / scaling;
+        const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']);
+        const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
+        this.dataDoc[this.fieldKey + '-nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']) * nh;
+        this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nw;
+        this.rootDoc._panX = nh * NumCast(this.rootDoc._panX);
+        this.rootDoc._panY = nw * NumCast(this.rootDoc._panY);
+        this.dataDoc._panXMax = this.dataDoc._panXMax ? nh * NumCast(this.dataDoc._panXMax) : undefined;
+        this.dataDoc._panXMin = this.dataDoc._panXMin ? nh * NumCast(this.dataDoc._panXMin) : undefined;
+        this.dataDoc._panYMax = this.dataDoc._panYMax ? nw * NumCast(this.dataDoc._panYMax) : undefined;
+        this.dataDoc._panYMin = this.dataDoc._panYMin ? nw * NumCast(this.dataDoc._panYMin) : undefined;
+    });
     @undoBatch
     rotate = action(() => {
         const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']);
@@ -189,6 +218,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         const croppingProto = Doc.GetProto(cropping);
         croppingProto.annotationOn = undefined;
         croppingProto.isPrototype = true;
+        croppingProto.backgroundColor = undefined;
         croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO
         croppingProto.type = DocumentType.IMG;
         croppingProto.layout = ImageBox.LayoutString('data');
@@ -205,7 +235,11 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.panYMax = anchh / viewScale;
         if (addCrop) {
             DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
+            cropping.y = NumCast(this.rootDoc.y);
+            this.props.addDocTab(cropping, OpenWhere.inParent);
         }
+        DocumentManager.Instance.AddViewRenderedCb(cropping, dv => setTimeout(() => (dv.ComponentView as ImageBox).setNativeSize(), 200));
         this.props.bringToFront(cropping);
         return cropping;
     };
@@ -216,6 +250,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: 'Set Native Pixel Size', event: this.setNativeSize, icon: 'expand-arrows-alt' });
             funcs.push({ description: `${this.layoutDoc[this.fieldKey + '-useAlt'] ? 'Show Alternate' : 'Show Primary'}`, 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) {
@@ -282,6 +317,8 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />;
     };
 
+    getScrollHeight = () => (this.props.fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._viewScale, 1) === NumCast(this.rootDoc._viewScaleMin, 1) ? this.nativeSize.nativeHeight : undefined);
+
     @computed
     private get considerDownloadIcon() {
         const data = this.dataDoc[this.fieldKey];
@@ -346,9 +383,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get content() {
         TraceMobx();
 
-        const col = Color(this.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
-        const srcpath = !col.alpha() ? '' : this.paths[0];
-        const fadepath = !col.alpha() ? '' : this.paths.lastElement();
+        const col = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
+        const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
+        const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
         const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
         const rotation = NumCast(this.dataDoc[this.fieldKey + '-rotation']);
         const aspect = rotation % 180 ? nativeHeight / nativeWidth : 1;
@@ -366,7 +403,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
         return (
             <div className="imageBox-cont" key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}>
-                <div className="imageBox-fader" style={{ opacity: col.alpha(), overflow: Array.from(this.props.docViewPath?.()).slice(-1)[0].fitWidth ? 'auto' : undefined }}>
+                <div className="imageBox-fader" style={{ opacity: col.alpha() }}>
                     <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                     {fadepath === srcpath ? null : (
                         <div
@@ -383,7 +420,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     }
 
-    screenToLocalTransform = this.props.ScreenToLocalTransform;
     contentFunc = () => [this.content];
 
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
@@ -394,6 +430,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         TraceMobx();
         return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />;
     }
+    screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop) * this.props.ScreenToLocalTransform().Scale);
     marqueeDown = (e: React.PointerEvent) => {
         if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) {
             setupMoveUpEvents(
@@ -410,7 +447,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             );
         }
     };
-    getScrollHeight = () => this.nativeSize.nativeHeight;
     @action
     finishMarquee = () => {
         this._getAnchor = AnchorMenu.Instance?.GetAnchor;
@@ -418,11 +454,6 @@ 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();
         const borderRad = this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BorderRounding);
@@ -432,25 +463,35 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 className="imageBox"
                 onContextMenu={this.specificContextMenu}
                 ref={this._mainCont}
+                onScroll={action(e => {
+                    if (!this._forcedScroll) {
+                        if (this.layoutDoc._scrollTop || this._mainCont.current?.scrollTop) {
+                            this._ignoreScroll = true;
+                            this.layoutDoc._scrollTop = this._mainCont.current?.scrollTop;
+                            this._ignoreScroll = false;
+                        }
+                    }
+                })}
                 style={{
                     width: this.props.PanelWidth() ? undefined : `100%`,
                     height: this.props.PanelWidth() ? undefined : `100%`,
                     pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined,
                     borderRadius,
-                    overflow: 'auto',
+                    overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined,
                 }}>
                 <CollectionFreeFormView
                     {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
                     renderDepth={this.props.renderDepth + 1}
                     fieldKey={this.annotationKey}
-                    styleProvider={this.styleProvider}
+                    styleProvider={this.props.styleProvider}
                     CollectionView={undefined}
                     isAnnotationOverlay={true}
                     annotationLayerHostsContent={true}
                     PanelWidth={this.props.PanelWidth}
-                    PanelHeight={this.props.fitWidth ? '100%' : this.props.PanelHeight}
+                    PanelHeight={this.props.PanelHeight}
                     ScreenToLocalTransform={this.screenToLocalTransform}
                     select={emptyFunction}
+                    getScrollHeight={this.getScrollHeight}
                     NativeDimScaling={returnOne}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     removeDocument={this.removeDocument}
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index eba9e281d..1b37dc7ab 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -60,6 +60,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
             } else {
                 this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100;
                 this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100;
+                this.rootDoc.linkAutoMove = false;
             }
         }
         return false;
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 95cb49037..5940fc075 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -542,8 +542,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         // }
     };
 
-    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
-    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth();
+    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
     scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
     opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()];
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d0d638e98..c4cca9679 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -295,12 +295,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         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()));
+            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._scrollHeight));
             if (scrollTo !== undefined && this._initialScroll === undefined) {
                 const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
                 this.goTo(scrollTo, focusSpeed, options.easeFunc);
                 return focusSpeed;
-            } else if (!this._webPageHasBeenRendered || !this.getScrollHeight() || this._initialScroll !== undefined) {
+            } else if (!this._webPageHasBeenRendered || !this._scrollHeight || this._initialScroll !== undefined) {
                 this._initialScroll = scrollTo;
             }
         }
@@ -376,9 +376,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             ContextMenu.Instance.setIgnoreEvents(true);
         }
     };
-
-    getScrollHeight = () => this._scrollHeight;
-
     isFirefox = () => {
         return 'InstallTrigger' in window; // navigator.userAgent.indexOf("Chrome") !== -1;
     };
@@ -969,8 +966,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value);
     showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
     setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func);
-    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
-    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth;
+    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
     scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop));
     anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick;
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 619c59f0e..80b18b8b9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -994,7 +994,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return this._didScroll ? this._focusSpeed : didToggle ? 1 : undefined; // if we actually scrolled, then return some focusSpeed
     };
 
-    getScrollHeight = () => this.scrollHeight;
     // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
     // Since we also monitor all component height changes, this will update the document's height.
     resetNativeHeight = (scrollHeight: number) => {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index 427911bd3..0a96297b7 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -35,6 +35,7 @@ import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
 import { CollectionStackedTimeline } from '../../collections/CollectionStackedTimeline';
+import { PresElementBox } from './PresElementBox';
 const { Howl } = require('howler');
 
 export interface PinProps {
@@ -383,9 +384,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             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) {
+            bestTarget._dataTransition = `all ${transTime}ms`;
             const fkey = Doc.LayoutFieldKey(bestTarget);
             Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
             bestTarget[fkey + '-useAlt'] = activeItem.presUseAlt;
+            setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10);
         }
         if (pinDataTypes.textview && activeItem.presData !== undefined) Doc.GetProto(bestTarget)[Doc.LayoutFieldKey(bestTarget)] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData;
         if (pinDataTypes.poslayoutview) {
@@ -2244,6 +2247,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 ignoreUnrendered={true}
                                 //childFitWidth={returnTrue}
                                 childOpacity={returnOne}
+                                //childLayoutString={PresElementBox.LayoutString('data')}
                                 childLayoutTemplate={this.childLayoutTemplate}
                                 childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined}
                                 filterAddDocument={this.addDocumentFilter}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index c53cc608c..4a7f5d038 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -11,6 +11,7 @@ import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
 import { LinkPopup } from '../linking/LinkPopup';
 import { ButtonDropdown } from '../nodes/formattedText/RichTextMenu';
 import './AnchorMenu.scss';
+import { LightboxView } from '../LightboxView';
 
 @observer
 export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b0b7816b8..9610a71ac 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -144,6 +144,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
     };
 
+    @observable _scrollHeight = 0;
+
     @action
     initialLoad = async () => {
         if (this._pageSizes.length === 0) {
@@ -164,8 +166,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                     )
                 )
             );
-            this.props.Document.scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72;
         }
+        runInAction(() => (this._scrollHeight = (this._pageSizes.reduce((size, page) => size + page.height, 0) * 96) / 72));
     };
 
     _scrollStopper: undefined | (() => void);
@@ -177,7 +179,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
             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));
+            const scrollTo = doc.unrendered ? scrollTop : Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, 0.1 * windowHeight, this._scrollHeight);
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 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);
@@ -453,10 +455,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
         }
     };
 
-    scrollXf = () => {
-        return this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform();
-    };
-
     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) {
@@ -504,10 +502,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
         );
     }
 
+    getScrollHeight = () => this._scrollHeight;
     showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno));
+    scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform());
     overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1));
-    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); // (this.Document.scrollHeight || Doc.NativeHeight(this.Document) || 0);
-    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); // () => this._pageSizes.length && this._pageSizes[0] ? this._pageSizes[0].width : Doc.NativeWidth(this.Document);
+    panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1);
+    panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
     transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()];
     opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])];
     childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => {
@@ -533,6 +533,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 isAnnotationOverlay={true}
                 fieldKey={this.props.fieldKey + '-annotations'}
                 CollectionView={undefined}
+                getScrollHeight={this.getScrollHeight}
                 setPreviewCursor={this.setPreviewCursor}
                 setBrushViewer={this.setBrushViewer}
                 PanelHeight={this.panelHeight}
diff --git a/src/fields/util.ts b/src/fields/util.ts
index dc0b41276..3a7484cfd 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -350,7 +350,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any {
                         return target[prop];
         case AclSym :   return target[AclSym];
         case $mobx:     return target.__fields[prop];
-        case LayoutSym: return target.__Layout__;
+        case LayoutSym: return target.__LAYOUT__;
         case HeightSym: case WidthSym: if (GetEffectiveAcl(target) === AclPrivate) return returnZero;
         default :
             if (typeof prop === 'symbol')  return target[prop];
-- 
cgit v1.2.3-70-g09d2


From 560d1d972b3dedb03f8af117f38b8bd836e9eccb Mon Sep 17 00:00:00 2001
From: srichman333 <sarah_n_richman@brown.edu>
Date: Mon, 20 Feb 2023 02:01:00 -0500
Subject: view all and snap lines button icons

---
 package-lock.json                      | 6 +++---
 src/client/views/PropertiesButtons.tsx | 4 ++--
 2 files changed, 5 insertions(+), 5 deletions(-)

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

diff --git a/package-lock.json b/package-lock.json
index d1ee7f9e3..c0c4e362d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4998,9 +4998,9 @@
             }
         },
         "core-js": {
-            "version": "1.2.7",
-            "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
-            "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
+            "version": "3.28.0",
+            "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.28.0.tgz",
+            "integrity": "sha512-GiZn9D4Z/rSYvTeg1ljAIsEqFm0LaN9gVtwDCrKL80zHtS31p9BAjmTxVqTQDMpwlMolJZOFntUG2uwyj7DAqw=="
         },
         "core-js-pure": {
             "version": "3.28.0",
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 65a950711..9bb21a662 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -114,7 +114,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             'View All',
             '_fitContentsToBox',
             on => `${on ? "Don't" : 'Do'} fit content to container visible area`,
-            on => 'eye'
+            on => 'object-group'
         );
     }
     // this implments a container pattern by marking the targetDoc (collection) as an inPlace container,
@@ -242,7 +242,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             'Snap\xA0Lines',
             'showSnapLines',
             on => `Display snapping lines when objects are dragged`,
-            on => 'border-all',
+            on => 'th',
             undefined,
             true
         );
-- 
cgit v1.2.3-70-g09d2


From 7cd43c51fe13b3fb0f76d026a16caee5bc320f4e Mon Sep 17 00:00:00 2001
From: srichman333 <sarah_n_richman@brown.edu>
Date: Mon, 20 Feb 2023 11:32:38 -0500
Subject: freeze icon

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

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

diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 9bb21a662..a152b4948 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -222,7 +222,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             'FreezeThumb',
             '_thumb-frozen',
             on => `${on ? 'Freeze' : 'Unfreeze'} thumbnail`,
-            on => 'arrows-alt-h',
+            on => 'snowflake',
             (dv, doc) => {
                 if (doc['thumb-frozen']) doc['thumb-frozen'] = undefined;
                 else {
-- 
cgit v1.2.3-70-g09d2


From 5e7989da274606638c96f649e97e9d1a979956f5 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 28 Feb 2023 20:57:01 -0500
Subject: cleaning up of following inPlace links to overlay current
 inplaceContainer contents instead of deleting them.

---
 src/client/util/LinkFollower.ts                    |  8 ++---
 src/client/views/DocumentButtonBar.tsx             |  2 +-
 src/client/views/LightboxView.tsx                  | 11 ++----
 src/client/views/PropertiesButtons.tsx             | 39 ++++++++++------------
 src/client/views/PropertiesView.tsx                |  6 ++--
 src/client/views/collections/CollectionSubView.tsx |  1 +
 src/client/views/collections/TabDocView.tsx        | 12 ++++++-
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 ++-
 src/client/views/nodes/DocumentView.tsx            |  9 ++---
 src/client/views/nodes/PDFBox.tsx                  | 10 +++++-
 src/client/views/nodes/WebBox.tsx                  |  9 ++++-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 +++++
 src/client/views/pdf/PDFViewer.tsx                 |  2 +-
 13 files changed, 72 insertions(+), 49 deletions(-)

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

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index bbfd516da..50bc89ed2 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,11 +1,11 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { List } from '../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
-import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
-import { CollectionDockingView } from '../views/collections/CollectionDockingView';
+import { DocumentType } from '../documents/DocumentTypes';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
 import { PresBox } from '../views/nodes/trails';
 import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
@@ -35,7 +35,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 ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
+                    const where = LightboxView.LightboxDoc ? OpenWhere.lightbox : (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/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 20c35739e..af868ba9c 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -243,7 +243,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
                 <div
                     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)))}>
+                    onClick={undoBatch(e => this.props.views().map(view => view?.docView?.toggleFollowLink(undefined, false)))}>
                     <div className="documentButtonBar-followTypes">
                         {followBtn(
                             true,
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index e531bf71c..c9b1dcfd8 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -145,13 +145,6 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         }
     }
     public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => {
-        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;
-            }
-        }
         SelectionManager.DeselectAll();
         return LightboxView.SetLightboxDoc(
             doc,
@@ -318,7 +311,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 <LightboxTourBtn navBtn={this.navBtn} future={this.future} stepInto={this.stepInto} />
                 <div
                     className="lightboxView-navBtn"
-                    title={'toggle fit width'}
+                    title="toggle fit width"
                     onClick={e => {
                         e.stopPropagation();
                         LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth;
@@ -327,7 +320,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 </div>
                 <div
                     className="lightboxView-tabBtn"
-                    title={'open in tab'}
+                    title="open in tab"
                     onClick={e => {
                         e.stopPropagation();
                         CollectionDockingView.AddSplit(LightboxView._docTarget || LightboxView._doc!, OpenWhereMod.none);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a152b4948..7e33ee495 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -6,6 +6,7 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { RichTextField } from '../../fields/RichTextField';
+import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { DocUtils } from '../documents/Documents';
@@ -129,27 +130,21 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             onClick => {
                 SelectionManager.Views().forEach(dv => {
                     const containerDoc = dv.rootDoc;
-                    containerDoc.followAllLinks =
-                        containerDoc.noShadow =
-                        containerDoc.noHighlighting =
-                        containerDoc._isLinkButton =
-                        containerDoc._fitContentsToBox =
-                        containerDoc._forceActive =
-                        containerDoc._isInPlaceContainer =
-                            !containerDoc._isInPlaceContainer;
-                    containerDoc.followLinkLocation = containerDoc._isInPlaceContainer ? OpenWhere.inPlace : undefined;
+                    //containerDoc.followAllLinks =
+                    // containerDoc.noShadow =
+                    // containerDoc.noHighlighting =
+                    // containerDoc._forceActive =
+                    containerDoc._fitContentsToBox = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
-                    const menuDoc = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]).lastElement();
-                    if (menuDoc) {
-                        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.followLinkAudio = menuItem.followAllLinks = menuItem._isLinkButton = true;
-                            menuItem._followLinkLocation = OpenWhere.inPlace;
+                    const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
+                    dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
+                    containerContents.forEach(doc => {
+                        DocListCast(doc.links).forEach(link => {
+                            doc.isLinkButton = true;
+                            //doc.followLinkLocation = OpenWhere.inPlace;
+                            link.linkDisplay = false;
                         });
-                    }
+                    });
                 });
             }
         );
@@ -309,10 +304,12 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         docView.setToggleDetail();
                         break;
                     case 'linkInPlace':
-                        docView.toggleFollowLink('inPlace', false, false);
+                        docView.toggleFollowLink(false, false);
+                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.inPlace : undefined;
                         break;
                     case 'linkOnRight':
-                        docView.toggleFollowLink('add:right', false, false);
+                        docView.toggleFollowLink(false, false);
+                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.addRight : undefined;
                         break;
                 }
             });
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f7708d801..e9d1433c3 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1406,7 +1406,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     });
 
     @undoBatch
-    changeFollowBehavior = action((follow: string) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
+    changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow));
 
     @undoBatch
     changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior));
@@ -1615,8 +1615,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                     <div className="propertiesView-section">
                                         <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(this.sourceAnchor?.followLinkLocation, 'default')}>
-                                                <option value="default">Default</option>
+                                            <select onChange={e => this.changeFollowBehavior(e.currentTarget.value === 'Default' ? undefined : e.currentTarget.value)} value={Cast(this.sourceAnchor?.followLinkLocation, 'string', null)}>
+                                                <option value={undefined}>Default</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>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 27ae3041f..c88ae314e 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -67,6 +67,7 @@ export function CollectionSubView<X>(moreProps?: X) {
         // to its children which may be templates.
         // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey'
         @computed get dataField() {
+            if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey];
             // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly
             // setTimeout changes it outside of the @computed section
             !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>()));
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index b75f315ca..01872df84 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -358,7 +358,17 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
         // prettier-ignore
         switch (whereFields[0]) {
-            case OpenWhere.inPlace:  // fall through to lightbox
+            case undefined:
+            case OpenWhere.inPlace:    {
+                if (this.layoutDoc?.isInPlaceContainer) {
+                    const inPlaceView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
+                    const data = inPlaceView?.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)];
+                    if (inPlaceView && (!data || data instanceof List)) {
+                        inPlaceView.layoutDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
+                        return true;
+                    }
+                }
+            }
             case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 0477c6a16..78804b070 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1395,6 +1395,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
     addDocTab = action((doc: Doc, where: OpenWhere) => {
+        if (this.props.isAnnotationOverlay) return this.props.addDocTab(doc, where);
         switch (where) {
             case OpenWhere.inParent:
                 return this.props.addDocument?.(doc) || false;
@@ -1412,9 +1413,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         (!docContext || this.props.removeDocument?.(docContext))) ||
                     false
                 );
+            case undefined:
             case OpenWhere.inPlace:
                 if (this.layoutDoc.isInPlaceContainer) {
-                    this.dataDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
+                    this.layoutDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
                     return true;
                 }
         }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 039a75a1b..beb0f8e3a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -792,7 +792,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    toggleFollowLink = (location: Opt<string>, zoom?: boolean, setTargetToggle?: boolean): void => {
+    toggleFollowLink = (zoom?: boolean, setTargetToggle?: boolean): void => {
         this.Document.ignoreClick = false;
         if (setTargetToggle) {
             this.Document.followLinkToggle = !this.Document.followLinkToggle;
@@ -802,7 +802,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
         if (this.Document._isLinkButton && !this.onClickHandler) {
             zoom !== undefined && (this.Document.followLinkZoom = zoom);
-            this.Document.followLinkLocation = location;
         } else if (this.Document._isLinkButton && this.onClickHandler) {
             this.Document._isLinkButton = false;
             this.dataDoc.onClick = this.Document.onClick = this.layoutDoc.onClick = undefined;
@@ -985,10 +984,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
 
                     onClicks.push({ description: this.Document.ignoreClick ? 'Select' : 'Do Nothing', event: () => (this.Document.ignoreClick = !this.Document.ignoreClick), icon: this.Document.ignoreClick ? 'unlock' : 'lock' });
-                    onClicks.push({ description: this.Document.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.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(undefined, false, true), icon: 'map-pin' });
+                    onClicks.push({ description: this.Document.isLinkButton || this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
+                    onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(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/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8d7ab2f0d..6b2f2246a 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -22,7 +22,7 @@ import { LightboxView } from '../LightboxView';
 import { CreateImage } from '../nodes/WebBoxRenderer';
 import { PDFViewer } from '../pdf/PDFViewer';
 import { SidebarAnnos } from '../SidebarAnnos';
-import { DocFocusOptions, DocumentView } from './DocumentView';
+import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
@@ -200,6 +200,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
 
+    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+            this.toggleSidebar(false);
+            return true;
+        }
+        return this.props.addDocTab(doc, where);
+    };
     scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
         let didToggle = false;
         if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(anchor) && !this.SidebarShown) {
@@ -487,6 +494,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     <PDFViewer
                         {...this.props}
                         rootDoc={this.rootDoc}
+                        addDocTab={this.sidebarAddDocTab}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
                         pdf={this._pdf!}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index caab18e0d..f5eab51c2 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, DocumentView, DocumentViewProps } from './DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
 import { VideoBox } from './VideoBox';
@@ -322,6 +322,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return undefined;
     };
 
+    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+            this.toggleSidebar(false);
+            return true;
+        }
+        return this.props.addDocTab(doc, where);
+    };
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         let ele: Opt<HTMLDivElement> = undefined;
         try {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 123499647..dc7a11e6e 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -230,6 +230,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
 
+    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
+        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
+            this.toggleSidebar(false);
+            return true;
+        }
+        return this.props.addDocTab(doc, where);
+    };
+
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         if (!pinProps) return this.rootDoc;
         const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 9877690f8..ffc49e4f3 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 { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps, OpenWhere } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { StyleProp } from '../StyleProvider';
-- 
cgit v1.2.3-70-g09d2


From 9c63f3833f15cc995c2255b83923384686127f3e Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 1 Mar 2023 11:57:05 -0500
Subject: changed inPlace link following to be lightbox.  allowed collections
 to be labeled as lightboxes.  lightbox collections display link targets in an
 overlay that hides the rest of their content

---
 src/client/documents/Documents.ts                  |  1 +
 src/client/util/DocumentManager.ts                 | 28 ++++++----
 src/client/util/LinkFollower.ts                    | 10 ++--
 src/client/views/DocumentDecorations.tsx           | 13 ++---
 src/client/views/MainView.tsx                      |  1 -
 src/client/views/PropertiesButtons.tsx             | 62 +++++++++++-----------
 src/client/views/PropertiesView.tsx                |  1 -
 src/client/views/collections/CollectionView.tsx    |  2 +-
 src/client/views/collections/TabDocView.tsx        | 22 ++++----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  4 +-
 src/client/views/nodes/DocumentView.tsx            | 17 +++---
 src/client/views/nodes/MapBox/MapBox.tsx           |  1 -
 src/fields/documentSchemas.ts                      |  4 +-
 13 files changed, 88 insertions(+), 78 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index d2ab67849..70c57e8fa 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -160,6 +160,7 @@ export class DocumentOptions {
     _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed
     _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)
+    _isLightbox?: boolean; // whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target
     _showCaption?: string; // which field to display in the caption area.  leave empty to have no caption
     _scrollTop?: number; // scroll location for pdfs
     _noAutoscroll?: boolean; // whether collections autoscroll when this item is dragged
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index d9273c2c9..0d10bed43 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -10,7 +10,7 @@ import { CollectionDockingView } from '../views/collections/CollectionDockingVie
 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 { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
@@ -307,16 +307,22 @@ export class DocumentManager {
             }
             //}
             if (options.playAudio) DocumentManager.playAudioAnno(focusView.rootDoc);
-            const doFocus = (forceDidFocus: boolean) =>
-                focusView.focus(originalTarget, {
-                    ...options,
-                    originalTarget,
-                    afterFocus: (didFocus: boolean) =>
-                        new Promise<ViewAdjustment>(res => {
-                            focusAndFinish(forceDidFocus || didFocus);
-                            res(ViewAdjustment.doNothing);
-                        }),
-                });
+            const doFocus = (forceDidFocus: boolean) => {
+                if (options.openInLightbox || (!options.originatingDoc?.followLinkLocation && DocCast(focusView.rootDoc.context)?._isLightbox)) {
+                    focusView.props.addDocTab(targetDoc, OpenWhere.lightbox);
+                    focusAndFinish(true);
+                } else {
+                    focusView.focus(originalTarget, {
+                        ...options,
+                        originalTarget,
+                        afterFocus: (didFocus: boolean) =>
+                            new Promise<ViewAdjustment>(res => {
+                                focusAndFinish(forceDidFocus || didFocus);
+                                res(ViewAdjustment.doNothing);
+                            }),
+                    });
+                }
+            };
             if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) {
                 focusView.iconify(() => doFocus(true));
             } else {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 50bc89ed2..e86e68c21 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -3,6 +3,7 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
@@ -36,7 +37,9 @@ export class LinkFollower {
             const createTabForTarget = (didFocus: boolean) =>
                 new Promise<ViewAdjustment>(res => {
                     const where = LightboxView.LightboxDoc ? OpenWhere.lightbox : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
-                    docViewProps.addDocTab(doc, where);
+                    const lightbox = where === OpenWhere.lightbox && DocumentManager.GetContextPath(doc).find(container => container.isLightbox && DocumentManager.Instance.getDocumentView(container));
+                    const addDocTab = lightbox ? DocumentManager.Instance.getDocumentView(lightbox)?.ComponentView?.addDocTab : undefined;
+                    (addDocTab ?? 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.
                         if (targDocView) {
@@ -51,7 +54,7 @@ export class LinkFollower {
                             });
                         } else {
                             finished?.();
-                            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
+                            res(BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing);
                         }
                     }, 100);
                 });
@@ -109,6 +112,7 @@ export class LinkFollower {
                         zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkTransitionTime, 500),
                         zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null),
                         easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
+                        openInLightbox: sourceDoc.followLinkLocation === OpenWhere.lightbox,
                         effect: sourceDoc,
                         originatingDoc: sourceDoc,
                         zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText),
@@ -123,7 +127,7 @@ export class LinkFollower {
                     } else {
                         const containerDocContext = DocumentManager.GetContextPath(target);
                         const targetContexts = !sourceDoc.followLinkToOuterContext && containerDocContext.length ? [containerDocContext.lastElement()] : containerDocContext;
-                        DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.inPlace), finished), targetContexts, allFinished);
+                        DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.lightbox), finished), targetContexts, allFinished);
                     }
                 };
                 let movedTarget = false;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 4e51f10a8..eeef01d17 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,7 +27,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
-import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
+import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
@@ -283,11 +283,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                     openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc);
                     Doc.deiconifyView(openDoc);
                 }
-                LightboxView.SetLightboxDoc(
-                    openDoc,
-                    undefined,
-                    selectedDocs.slice(1).map(view => view.props.Document)
-                );
+                selectedDocs[0].props.addDocTab(openDoc, OpenWhere.lightbox);
+                // LightboxView.SetLightboxDoc(
+                //     openDoc,
+                //     undefined,
+                //     selectedDocs.slice(1).map(view => view.props.Document)
+                // );
             }
         }
         SelectionManager.DeselectAll();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 1e9220e1e..95c0f3755 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -692,7 +692,6 @@ export class MainView extends React.Component {
         if (doc.dockingConfig) return DashboardView.openDashboard(doc);
         // prettier-ignore
         switch (whereFields[0]) {
-            case OpenWhere.inPlace:  // fall through to lightbox
             case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index 7e33ee495..a5c01490f 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -7,7 +7,7 @@ import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
-import { BoolCast, StrCast } from '../../fields/Types';
+import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
 import { DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
@@ -20,6 +20,7 @@ import { VideoBox } from './nodes/VideoBox';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
 import React = require('react');
+import { LinkManager } from '../util/LinkManager';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -118,14 +119,14 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             on => 'object-group'
         );
     }
-    // this implments a container pattern by marking the targetDoc (collection) as an inPlace container,
-    // and then making the contained collection be a "menu" such that when any of its contents are clicked,
-    // they will open their targets in the outer container.  To get back to the "menu", you click on the main container.
-    @computed get inPlaceContainerButton() {
+    // this implments a container pattern by marking the targetDoc (collection) as a lightbox
+    // that always fits its contents to its container and that hides all other documents when
+    // a link is followed that targets a 'lightbox' destination
+    @computed get isLightboxButton() {
         return this.propertyToggleBtn(
-            'In Place',
-            'isInPlaceContainer',
-            on => `${on ? 'Make' : 'Remove'} in place container flag`,
+            'Lightbox',
+            'isLightbox',
+            on => `${on ? 'Set' : 'Remove'} lightbox flag`,
             on => 'window-restore',
             onClick => {
                 SelectionManager.Views().forEach(dv => {
@@ -134,16 +135,12 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     // containerDoc.noShadow =
                     // containerDoc.noHighlighting =
                     // containerDoc._forceActive =
-                    containerDoc._fitContentsToBox = containerDoc._isInPlaceContainer = !containerDoc._isInPlaceContainer;
-                    containerDoc._xPadding = containerDoc._yPadding = containerDoc._isInPlaceContainer ? 10 : undefined;
+                    containerDoc._fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox;
+                    containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
                     dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
                     containerContents.forEach(doc => {
-                        DocListCast(doc.links).forEach(link => {
-                            doc.isLinkButton = true;
-                            //doc.followLinkLocation = OpenWhere.inPlace;
-                            link.linkDisplay = false;
-                        });
+                        DocListCast(doc.links).forEach(link => (link.linkDisplay = false));
                     });
                 });
             }
@@ -290,7 +287,6 @@ export class PropertiesButtons extends React.Component<{}, {}> {
     @undoBatch
     @action
     handleOptionChange = (onClick: string) => {
-        this.selectedDoc && (this.selectedDoc.onClickBehavior = onClick);
         SelectionManager.Views()
             .filter(dv => dv.docView)
             .map(dv => dv.docView!)
@@ -305,7 +301,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                         break;
                     case 'linkInPlace':
                         docView.toggleFollowLink(false, false);
-                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.inPlace : undefined;
+                        docView.props.Document.followLinkLocation = docView.props.Document._isLinkButton ? OpenWhere.lightbox : undefined;
                         break;
                     case 'linkOnRight':
                         docView.toggleFollowLink(false, false);
@@ -327,24 +323,26 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             ['nothing', 'Select Document'],
             ['enterPortal', 'Enter Portal'],
             ['toggleDetail', 'Toggle Detail'],
-            ['linkInPlace', 'Open in Place'],
+            ['linkInPlace', 'Open Link in Lightbox'],
             ['linkOnRight', 'Open Link on Right'],
         ];
-        const currentSelection = this.selectedDoc.onClickBehavior;
-        // Get items to place into the list
-
         const list = buttonList.map(value => {
-            const click = () => {
-                this.handleOptionChange(value[0]);
-            };
+            const click = () => this.handleOptionChange(value[0]);
+            const linkButton = BoolCast(this.selectedDoc._isLinkButton);
+            const followLoc = this.selectedDoc._followLinkLocation;
+            const linkedToLightboxView = () => DocListCast(this.selectedDoc.links).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+
+            let active = false;
+            // prettier-ignore
+            switch (value[0]) {
+                case 'linkInPlace': active = linkButton && followLoc === OpenWhere.lightbox && !linkedToLightboxView();  break;
+                case 'linkOnRight': active = linkButton && followLoc === OpenWhere.addRight; break;
+                case 'enterPortal': active = linkButton && this.selectedDoc._followLinkLocation === OpenWhere.lightbox && linkedToLightboxView(); break;
+                case 'toggleDetail':active = ScriptCast(this.selectedDoc.onClick)?.script.originalScript.includes('toggleDetail'); break;
+                case 'nothing':     active = !linkButton && this.selectedDoc.onClick === undefined;break;
+            }
             return (
-                <div
-                    className="list-item"
-                    key={`${value}`}
-                    style={{
-                        backgroundColor: value[0] === currentSelection ? Colors.LIGHT_BLUE : undefined,
-                    }}
-                    onClick={click}>
+                <div className="list-item" key={`${value}`} style={{ backgroundColor: active ? Colors.LIGHT_BLUE : undefined }} onClick={click}>
                     {value[1]}
                 </div>
             );
@@ -413,7 +411,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                 {toggle(this.freezeThumb)}
                 {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
-                {toggle(this.inPlaceContainerButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
+                {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })}
                 {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })}
                 {toggle(this.maskButton, { display: !isInk ? 'none' : '' })}
                 {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index e9d1433c3..0fe165783 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -1626,7 +1626,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <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>
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 90406ed3c..316f1e4e9 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -191,7 +191,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
             if (this.rootDoc.childClickedOpenTemplateView instanceof Doc) {
                 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' });
+            !Doc.noviceMode && optionItems.push({ description: `${this.rootDoc._isLightbox ? 'Unset' : 'Set'} is Lightbox`, event: () => (this.rootDoc._isLightbox = !this.rootDoc._isLightbox), icon: 'project-diagram' });
 
             if (!Doc.noviceMode && false) {
                 optionItems.push({
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 01872df84..042c2b71b 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -350,7 +350,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     //  replace[:{left,right,top,bottom,<any string>}] - e.g., "replace" will replace the current stack contents,
     //                                  "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
+    //  lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox
     addDocTab = (doc: Doc, location: OpenWhere) => {
         SelectionManager.DeselectAll();
         const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':');
@@ -359,17 +359,15 @@ export class TabDocView extends React.Component<TabDocViewProps> {
         // prettier-ignore
         switch (whereFields[0]) {
             case undefined:
-            case OpenWhere.inPlace:    {
-                if (this.layoutDoc?.isInPlaceContainer) {
-                    const inPlaceView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
-                    const data = inPlaceView?.dataDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)];
-                    if (inPlaceView && (!data || data instanceof List)) {
-                        inPlaceView.layoutDoc[Doc.LayoutFieldKey(inPlaceView.rootDoc)] = new List<Doc>([doc]);
-                        return true;
-                    }
-                }
-            }
-            case OpenWhere.lightbox:   return LightboxView.AddDocTab(doc, location);
+            case OpenWhere.lightbox:   if (this.layoutDoc?._isLightbox) {
+                                            const lightboxView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined;
+                                            const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)];
+                                            if (lightboxView && (!data || data instanceof List)) {
+                                                lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)] = new List<Doc>([doc]);
+                                                return true;
+                                            }
+                                       }
+                                       return LightboxView.AddDocTab(doc, location);
             case OpenWhere.dashboard:  return DashboardView.openDashboard(doc);
             case OpenWhere.fullScreen: return CollectionDockingView.OpenFullScreen(doc);
             case OpenWhere.close:      return CollectionDockingView.CloseSplit(doc, whereMods);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 78804b070..be8a72369 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1414,8 +1414,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     false
                 );
             case undefined:
-            case OpenWhere.inPlace:
-                if (this.layoutDoc.isInPlaceContainer) {
+            case OpenWhere.lightbox:
+                if (this.layoutDoc._isLightbox) {
                     this.layoutDoc[this.props.fieldKey] = new List<Doc>(doc instanceof Doc ? [doc] : doc);
                     return true;
                 }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index beb0f8e3a..8257c9c49 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -72,7 +72,6 @@ export enum ViewAdjustment {
 }
 
 export enum OpenWhere {
-    inPlace = 'inPlace',
     lightbox = 'lightbox',
     add = 'add',
     addLeft = 'add:left',
@@ -109,6 +108,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
+    openInLightbox?: boolean; // whether to open target in lightbox or just focus on it
     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
@@ -123,6 +123,7 @@ export interface DocComponentView {
     scrollPreview?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     scrollFocus?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
+    addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
     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.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
     menuControls?: () => JSX.Element; // controls to display in the top menu bar when the document is selected.
@@ -640,7 +641,8 @@ 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) && (![DocumentType.INK].includes(this.rootDoc.type as any) || Doc.UserDoc().openInkInLightbox) && !this.rootDoc.isLinkButton) {
-                    UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
+                    //UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox, this.props.LayoutTemplate?.()), 'double tap');
+                    UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap');
                     SelectionManager.DeselectAll();
                     Doc.UnBrushDoc(this.props.Document);
                 }
@@ -870,13 +872,16 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    makeIntoPortal = async () => {
+    makeIntoPortal = () => {
         const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document);
         if (!portalLink) {
-            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');
+            DocUtils.MakeLink(
+                { doc: this.props.Document },
+                { doc: Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }) },
+                'portal to:portal from'
+            );
         }
-        this.Document.followLinkLocation = OpenWhere.inPlace;
+        this.Document.followLinkLocation = OpenWhere.lightbox;
         this.Document.followLinkZoom = true;
         this.Document._isLinkButton = true;
     };
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index 039f11cd1..c182dfd19 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -331,7 +331,6 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         docs.forEach(doc => {
             if (doc.lat !== undefined && doc.lng !== undefined) {
                 const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng);
-                doc.onClickBehavior = 'enterPortal';
                 if (existingMarker) {
                     Doc.AddDocToList(existingMarker, 'data', doc);
                 } else {
diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts
index 10324449f..5b489a96c 100644
--- a/src/fields/documentSchemas.ts
+++ b/src/fields/documentSchemas.ts
@@ -84,11 +84,11 @@ export const documentSchema = createSchema({
     onPointerDown: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onPointerUp: ScriptField, // script to run when document is clicked (can be overriden by an onClick prop)
     onDragStart: ScriptField, // script to run when document is dragged (without being selected).  the script should return the Doc to be dropped.
-    followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, inPlace, default, )
+    followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, )
     hideLinkButton: 'boolean', // whether the blue link counter button should be hidden
     hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden
     linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints.
-    isInPlaceContainer: 'boolean', // whether the marked object will display addDocTab() calls that target "inPlace" destinations
+    isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations
     isLinkButton: 'boolean', // whether document functions as a link follow button to follow the first link on the document when clicked
     layers: listSpec('string'), // which layers the document is part of
     _lockedPosition: 'boolean', // whether the document can be moved (dragged)
-- 
cgit v1.2.3-70-g09d2


From 0c38e4dc096d6abf82ef11286616856b7119c6e1 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sun, 5 Mar 2023 21:24:09 -0500
Subject: replace jumpToDocument with showDocument.  restructure code to get
 rid of scrollFocus by adding getView() and fixing focus() and
 restoreTargetView

---
 src/Utils.ts                                       |  27 +++
 src/client/documents/Documents.ts                  |   2 +-
 src/client/util/DocumentManager.ts                 | 235 +++++++--------------
 .../util/Import & Export/DirectoryImportBox.tsx    |   2 +-
 src/client/util/LinkFollower.ts                    |  56 +----
 src/client/util/SharingManager.tsx                 |   2 +-
 src/client/views/InkingStroke.tsx                  |   8 +-
 src/client/views/LightboxView.tsx                  |   4 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/PropertiesButtons.tsx             |   4 +-
 src/client/views/PropertiesView.tsx                |   2 +-
 src/client/views/SidebarAnnos.tsx                  |   2 +-
 .../views/collections/CollectionNoteTakingView.tsx |  15 +-
 .../collections/CollectionStackedTimeline.tsx      |   4 +-
 .../views/collections/CollectionStackingView.tsx   |  13 +-
 .../views/collections/CollectionTimeView.tsx       |   6 +-
 src/client/views/collections/TabDocView.tsx        |  10 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 151 ++++---------
 .../collections/collectionFreeForm/MarqueeView.tsx |   3 +-
 .../collectionLinear/CollectionLinearView.tsx      |   2 +-
 .../CollectionMulticolumnView.tsx                  |   3 +-
 .../CollectionMultirowView.tsx                     |   1 -
 .../collectionSchema/CollectionSchemaCells.tsx     |   2 +-
 src/client/views/linking/LinkMenuItem.tsx          |   2 +-
 src/client/views/nodes/AudioBox.tsx                |   7 +-
 .../views/nodes/CollectionFreeFormDocumentView.tsx |   7 +-
 src/client/views/nodes/ComparisonBox.tsx           |  19 +-
 src/client/views/nodes/DocumentView.tsx            |  66 ++----
 src/client/views/nodes/FilterBox.tsx               |   2 +-
 src/client/views/nodes/FunctionPlotBox.tsx         |  20 +-
 src/client/views/nodes/ImageBox.tsx                |   7 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   2 +-
 src/client/views/nodes/PDFBox.tsx                  |  24 +--
 src/client/views/nodes/VideoBox.tsx                |  35 +--
 src/client/views/nodes/WebBox.tsx                  |  82 ++++---
 .../views/nodes/formattedText/FormattedTextBox.tsx |  28 +--
 src/client/views/nodes/trails/PresBox.scss         |   2 +-
 src/client/views/nodes/trails/PresBox.tsx          | 160 +++++++-------
 src/client/views/nodes/trails/PresElementBox.tsx   |   2 +-
 src/client/views/pdf/PDFViewer.tsx                 |   9 +-
 src/client/views/search/SearchBox.tsx              |   2 +-
 src/fields/ScriptField.ts                          |   7 +-
 src/fields/util.ts                                 |   6 +-
 43 files changed, 410 insertions(+), 635 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 22c8cb902..ae1478943 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,6 +1,7 @@
 import v4 = require('uuid/v4');
 import v5 = require('uuid/v5');
 import { ColorState } from 'react-color';
+import * as rp from 'request-promise';
 import { Socket } from 'socket.io';
 import { Colors } from './client/views/global/globalEnums';
 import { Message } from './server/Message';
@@ -34,6 +35,31 @@ export namespace Utils {
         return v5(seed, v5.URL);
     }
 
+    /**
+     * Uploads an image buffer to the server and stores with specified filename.  by default the image
+     * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
+     * @param imageUri the bytes of the image
+     * @param returnedFilename the base filename to store the image on the server
+     * @param nosuffix optionally suppress creating multiple resolution images
+     */
+    export async function convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
+        try {
+            const posting = Utils.prepend('/uploadURI');
+            const returnedUri = await rp.post(posting, {
+                body: {
+                    uri: imageUri,
+                    name: returnedFilename,
+                    nosuffix,
+                    replaceRootFilename,
+                },
+                json: true,
+            });
+            return returnedUri;
+        } catch (e) {
+            console.log('VideoBox :' + e);
+        }
+    }
+
     export function GetScreenTransform(ele?: HTMLElement): { scale: number; translateX: number; translateY: number } {
         if (!ele) {
             return { scale: 1, translateX: 1, translateY: 1 };
@@ -218,6 +244,7 @@ export namespace Utils {
     }
 
     export function scrollIntoView(targetY: number, targetHgt: number, scrollTop: number, contextHgt: number, minSpacing: number, scrollHeight: number) {
+        if (!targetHgt) return targetY; // if there's no height, then assume that
         if (scrollTop + contextHgt < Math.min(scrollHeight, targetY + minSpacing + targetHgt)) {
             return Math.ceil(targetY + minSpacing + targetHgt - contextHgt);
         }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index ebd06dbe2..98469a2f9 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1319,7 +1319,7 @@ export namespace DocUtils {
     }
 
     export function DefaultFocus(doc: Doc, options: DocFocusOptions) {
-        options?.afterFocus?.(false);
+        return undefined;
     }
 
     export let ActiveRecordings: { props: FieldViewProps; getAnchor: (addAsAnnotation: boolean) => Doc }[] = [];
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 0d10bed43..1a38e9aff 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,17 +1,22 @@
+import { loadAsync } from 'jszip';
 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, StrCast } from '../../fields/Types';
 import { AudioField } from '../../fields/URLField';
-import { returnFalse } from '../../Utils';
-import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { emptyFunction } from '../../Utils';
+import { CollectionViewType } from '../documents/DocumentTypes';
 import { CollectionDockingView } from '../views/collections/CollectionDockingView';
 import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { CollectionView } from '../views/collections/CollectionView';
+import { TabDocView } from '../views/collections/TabDocView';
 import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod, ViewAdjustment } from '../views/nodes/DocumentView';
+import { MainView } from '../views/MainView';
+import { DocFocusOptions, DocumentView, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView';
+import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox';
 import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox';
+import { PresBox } from '../views/nodes/trails';
 import { ScriptingGlobals } from './ScriptingGlobals';
 import { SelectionManager } from './SelectionManager';
 const { Howl } = require('howler');
@@ -165,12 +170,12 @@ export class DocumentManager {
     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));
-        return views?.find(view => view.ContentDiv?.getBoundingClientRect().width && view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse) || (views.length ? views[0] : undefined);
+        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);
+        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 getDocumentViews(toFindIn: Doc): DocumentView[] {
         const toFind =
@@ -198,7 +203,7 @@ export class DocumentManager {
 
     static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) {
         if (!doc) return [];
-        const srcContext = Cast(doc.context, Doc, null) ?? Cast(Cast(doc.annotationOn, Doc, null)?.context, Doc, null);
+        const srcContext = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null);
         var containerDocContext = srcContext ? [srcContext, doc] : [doc];
         while (
             containerDocContext.length &&
@@ -235,171 +240,91 @@ export class DocumentManager {
         CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
         finished?.();
     };
-    public jumpToDocument = (
+
+    // shows a documentView by:
+    // traverses down through the viewPath of contexts to the view:
+    //           focusing on each context
+    public showDocumentView = async (targetDocView: DocumentView, options: DocFocusOptions) => {
+        const docViewPath = targetDocView.docViewPath.slice();
+        let rootContextView = docViewPath.shift();
+        return rootContextView && this.focusViewsInPath(rootContextView, options, async () => ({ childDocView: docViewPath.shift(), viewSpec: undefined }));
+    };
+
+    // shows a document by first:
+    //           traversing down through the contexts that contain target until an existing view is found
+    //           if no container view is found, create one by: opening an existing tab that has the top-level view, or showing the top-level context in the lightbox.
+    // once a containing view is found, it then traverses back down through the contexts to the target document by:
+    //           focusing on each context
+    //           and finally restoring the targetDoc to the viewSpec specified by the last document which may either be the targetDoc, or a viewSpec that describes the targetDoc configuration
+    public showDocument = async (
         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
-        docContextPath: Doc[], // context to load that should contain the target
         finished?: () => void
-    ): void => {
-        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;
-        if (wasHidden) {
-            runInAction(() => {
-                resolvedTarget.hidden = false; // if the target is hidden, un-hide it here.
-                docView?.props.bringToFront(resolvedTarget);
-            });
-        }
-        const focusAndFinish = action((didFocus: boolean) => {
-            const finalTargetDoc = resolvedTarget;
-            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);
-                !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 docContextPath = DocumentManager.GetContextPath(targetDoc, true);
+        let rootContextView = await new Promise<DocumentView>(res => {
+            const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc));
+            if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!);
+            docContextPath.some(doc => TabDocView.Activate(doc)) || MainView.addDocTabFunc(docContextPath[0], options.openLocation as OpenWhere);
+            this.AddViewRenderedCb(docContextPath[0], dv => res(dv));
         });
-        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(targetDoc, { ...options, originalTarget, toggleTarget: false }, createViewFunc, docContextPath, finished)), 30);
-            }
-            if (!docView && targetDoc.type !== DocumentType.MARKER) {
-                annoContainerView.focus(targetDoc, options); // 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) {
-            // if (focusView.rootDoc === originalTarget) {
-            if (!options.noSelect) Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
-            else {
-                Doc.linkFollowHighlight(focusView.rootDoc, undefined, options.effect); //TODO:glr make this a setting in PresBox
-                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) => {
-                if (options.openInLightbox || (!options.originatingDoc?.followLinkLocation && DocCast(focusView.rootDoc.context)?._isLightbox)) {
-                    focusView.props.addDocTab(targetDoc, OpenWhere.lightbox);
-                    focusAndFinish(true);
-                } else {
-                    focusView.focus(originalTarget, {
-                        ...options,
-                        originalTarget,
-                        afterFocus: (didFocus: boolean) =>
-                            new Promise<ViewAdjustment>(res => {
-                                focusAndFinish(forceDidFocus || didFocus);
-                                res(ViewAdjustment.doNothing);
-                            }),
-                    });
-                }
-            };
-            if (focusView.props.Document.layoutKey === 'layout_icon' && focusView.rootDoc.type !== DocumentType.SCRIPTING) {
-                focusView.iconify(() => doFocus(true));
-            } else {
-                doFocus(false);
-            }
-        } else {
-            if (!targetDocContext) {
-                // we don't have a view and there's no context specified ... create a new view of the target using the dockFunc or default
-                createViewFunc(Doc.BrushDoc(targetDoc), () => focusAndFinish(true)); // bcz: should we use this?: Doc.MakeAlias(targetDoc)));
-            } else {
-                // otherwise try to get a view of the context of the target
-                if (targetDocContextView) {
-                    // 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
+        docContextPath.shift();
+        const childViewIterator = async () => {
+            const innerDoc = docContextPath.shift();
+            return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.unrendered ? (await rootContextView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined };
+        };
+        const target = await this.focusViewsInPath(rootContextView, options, childViewIterator);
+        this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc);
 
-                    if (targetDocContext.layoutKey === 'layout_icon') {
-                        return targetDocContextView.iconify(
-                            () => DocumentManager.Instance.AddViewRenderedCb(targetDoc, () => this.jumpToDocument(resolvedTarget ?? targetDoc, { ...options /* originalTarget - needed? */ }, createViewFunc, docContextPath, finished)),
-                            30
-                        );
-                    }
+        finished?.();
+    };
 
-                    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 () => {
-                            // 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;
-                        },
-                    });
-                } else {
-                    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)
-                        );
-                    }
-                }
-            }
+    focusViewsInPath = async (docView: DocumentView, options: DocFocusOptions, iterator: () => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView> }>) => {
+        let contextView: DocumentView | undefined; // view containing context that contains target
+        while (true) {
+            docView.rootDoc.layoutKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined;
+            docView.props.focus(docView.rootDoc, options); // focus the view within its container
+            const { childDocView, viewSpec } = await iterator();
+            if (!childDocView) return { viewSpec: viewSpec ?? docView.rootDoc, docView, contextView };
+            contextView = docView;
+            docView = childDocView;
         }
     };
+
+    @action
+    restoreDocView(viewSpec: Opt<Doc>, docView: DocumentView, options: DocFocusOptions, contextView: Opt<DocumentView>, targetDoc: Doc) {
+        if (viewSpec && docView) {
+            if (docView.ComponentView instanceof FormattedTextBox) docView.ComponentView?.focus(viewSpec, options);
+            PresBox.restoreTargetDocView(docView, viewSpec, options.zoomTime ?? 500);
+            Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect);
+            if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc);
+            if (options.toggleTarget) docView.rootDoc.hidden = !docView.rootDoc.hidden;
+            if (options.effect) docView.rootDoc[AnimationSym] = options.effect;
+
+            if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) {
+                // if the docView is a text anchor, the contextView is the PDF/Web/Text doc
+                contextView.htmlOverlayEffect = StrCast(options?.effect?.presEffect, StrCast(options?.effect?.followLinkAnimEffect));
+                contextView.textHtmlOverlay = StrCast(targetDoc.textHtml);
+                DocumentManager._overlayViews.add(contextView);
+            }
+            Doc.AddUnHighlightWatcher(() => {
+                docView.rootDoc[AnimationSym] = undefined;
+                DocumentManager.removeOverlayViews();
+                contextView && (contextView.htmlOverlayEffect = '');
+            });
+        }
+    }
 }
 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, { willPanZoom: true });
-        Doc.linkFollowHighlight(dv?.props.Document, false);
+    if (dv) {
+        DocumentManager.Instance.showDocumentView(dv, { willZoomCentered: true });
     } else {
         const context = doc.context !== Doc.MyFilesystem && Cast(doc.context, Doc, null);
         const showDoc = context || doc;
-        CollectionDockingView.AddSplit(Doc.BestAlias(showDoc), OpenWhereMod.right) && context && setTimeout(() => DocumentManager.Instance.getDocumentView(Doc.GetProto(doc))?.focus(doc, {}));
+        DocumentManager.Instance.showDocument(Doc.BestAlias(showDoc), { openLocation: OpenWhere.addRight }, () => DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }));
     }
 }
 ScriptingGlobals.add(DocFocusOrOpen);
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 7f0c8a3e8..559958c2b 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, { willPanZoom: true }, undefined, []);
+            DocumentManager.Instance.showDocument(importContainer, { willZoomCentered: true });
         }
 
         runInAction(() => {
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index b40c7bdf9..c9a178db7 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,15 +1,11 @@
 import { action, runInAction } from 'mobx';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { List } from '../../fields/List';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
 import { DocumentType } from '../documents/DocumentTypes';
-import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
 import { DocumentDecorations } from '../views/DocumentDecorations';
-import { LightboxView } from '../views/LightboxView';
-import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
+import { DocFocusOptions, DocumentViewSharedProps, OpenWhere } 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';
 
@@ -32,46 +28,10 @@ export class LinkFollower {
     // 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) => {
         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>) => {
-            const createTabForTarget = (didFocus: boolean) =>
-                new Promise<ViewAdjustment>(res => {
-                    const where = LightboxView.LightboxDoc ? OpenWhere.lightbox : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
-                    const lightbox = where === OpenWhere.lightbox && DocumentManager.GetContextPath(doc).find(container => container.isLightbox && DocumentManager.Instance.getDocumentView(container));
-                    const addDocTab = lightbox ? DocumentManager.Instance.getDocumentView(lightbox)?.ComponentView?.addDocTab : undefined;
-                    (addDocTab ?? 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.
-                        if (targDocView) {
-                            targDocView.props.focus(doc, {
-                                willPan: true,
-                                willPanZoom: BoolCast(sourceDoc.followLinkZoom, false),
-                                afterFocus: (didFocus: boolean) => {
-                                    finished?.();
-                                    res(ViewAdjustment.resetView);
-                                    return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing));
-                                },
-                            });
-                        } else {
-                            finished?.();
-                            res(BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing);
-                        }
-                    }, 100);
-                });
-
-            if (!sourceDoc.followLinkZoom) {
-                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), 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
         LinkFollower.traverseLink(
             linkDoc,
             sourceDoc,
-            createViewFunc,
-            docViewProps.ContainingCollectionDoc,
             action(() => {
                 batch.end();
                 Doc.AddUnHighlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
@@ -80,7 +40,7 @@ export class LinkFollower {
         );
     };
 
-    public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
+    public static traverseLink(link: Opt<Doc>, sourceDoc: 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
@@ -104,15 +64,17 @@ export class LinkFollower {
             ) as Doc;
             if (target) {
                 const doFollow = (canToggle?: boolean) => {
+                    const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle);
                     const options: DocFocusOptions = {
                         playAudio: BoolCast(sourceDoc.followLinkAudio),
-                        toggleTarget: canToggle && BoolCast(sourceDoc.followLinkToggle),
+                        toggleTarget,
+                        noSelect: true,
                         willPan: true,
-                        willPanZoom: BoolCast(sourceDoc.followLinkZoom, false),
+                        willZoomCentered: BoolCast(sourceDoc.followLinkZoom, false),
                         zoomTime: NumCast(sourceDoc.followLinkTransitionTime, 500),
                         zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null),
                         easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any,
-                        openInLightbox: sourceDoc.followLinkLocation === OpenWhere.lightbox,
+                        openLocation: StrCast(sourceDoc.followLinkLocation, OpenWhere.lightbox),
                         effect: sourceDoc,
                         originatingDoc: sourceDoc,
                         zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText),
@@ -125,9 +87,7 @@ export class LinkFollower {
                         }
                         finished?.();
                     } else {
-                        const containerDocContext = DocumentManager.GetContextPath(target);
-                        const targetContexts = !sourceDoc.followLinkToOuterContext && containerDocContext.length ? [containerDocContext.lastElement()] : containerDocContext;
-                        DocumentManager.Instance.jumpToDocument(target, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, OpenWhere.lightbox), finished), targetContexts, allFinished);
+                        DocumentManager.Instance.showDocument(target, options, allFinished);
                     }
                 };
                 let movedTarget = false;
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 36095700c..3c05af4bb 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -375,7 +375,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, { willPanZoom: true }, undefined, [context.props.Document]);
+                        DocumentManager.Instance.showDocument(this.targetDoc, { willZoomCentered: true });
                     }
                 }}
                 onPointerEnter={action(() => {
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 73e46a553..4f08a8e22 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -91,18 +91,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
         if (anchor) {
             anchor.backgroundColor = 'transparent';
             // /* addAsAnnotation &&*/ this.addDocument(anchor);
-            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: true, inkable: true } }, this.rootDoc);
+            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), inkable: true } }, this.rootDoc);
             return anchor;
         }
         return this.rootDoc;
     };
 
-    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
-        if (this._subContentView) return this._subContentView?.scrollFocus?.(docView, anchor, options);
-
-        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
-        return PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
-    };
     /**
      * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
      * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 8d60da0dd..2567d44bb 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -160,7 +160,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (targetDocView && target) {
             const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement();
             l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen');
-            targetDocView.focus(target, { originalTarget: target, willPanZoom: true, zoomScale: 0.9 });
+            DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
             if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target });
         } else {
             if (!target && LightboxView.path.length) {
@@ -195,7 +195,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         const docView = DocumentManager.Instance.getLightboxDocumentView(target || doc);
         if (docView) {
             LightboxView._docTarget = target;
-            target && docView.focus(target, { willPanZoom: true, zoomScale: 0.9 });
+            target && DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 });
         } else {
             LightboxView.SetLightboxDoc(doc, target);
         }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 95c0f3755..e1ba5943d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -205,7 +205,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, { willPan: false }, undefined, []) : null));
+            DocServer.GetRefField(id).then(doc => (doc instanceof Doc ? DocumentManager.Instance.showDocument(doc, { willPan: false }) : null));
         });
         document.addEventListener('linkAnnotationToDash', Hypothesis.linkListener);
         this.initEventListeners();
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index a5c01490f..ac1b66013 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -16,11 +16,11 @@ import { undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { DocumentView, OpenWhere } from './nodes/DocumentView';
-import { VideoBox } from './nodes/VideoBox';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
 import React = require('react');
 import { LinkManager } from '../util/LinkManager';
+import { Utils } from '../../Utils';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -222,7 +222,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     setTimeout(() =>
                         pasteImageBitmap((data_url: any, error: any) => {
                             error && console.log(error);
-                            data_url && VideoBox.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
+                            data_url && Utils.convertDataUri(data_url, doc[Id] + '-thumb-frozen', true).then(returnedfilename => (doc['thumb-frozen'] = new ImageField(returnedfilename)));
                         })
                     );
                 }
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 5105cc615..e750dc43a 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -335,7 +335,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
                         PanelWidth={panelWidth}
                         PanelHeight={panelHeight}
-                        focus={returnFalse}
+                        focus={emptyFunction}
                         ScreenToLocalTransform={this.getTransform}
                         docFilters={returnEmptyFilter}
                         docRangeFilters={returnEmptyFilter}
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index b9af28413..74ea624a6 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -139,7 +139,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         return target;
     };
     makeDocUnfiltered = (doc: Doc) => {
-        if (DocListCast(this.props.rootDoc[this.sidebarKey]).includes(doc)) {
+        if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
             if (this.props.layoutDoc[this.filtersKey]) {
                 this.props.layoutDoc[this.filtersKey] = new List<string>();
             }
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 0e1601f35..6035871cd 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -3,7 +3,7 @@ import { CursorProperty } from 'csstype';
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
-import { Copy, Id } from '../../../fields/FieldSymbols';
+import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
@@ -11,7 +11,6 @@ import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Ty
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { Transform } from '../../util/Transform';
@@ -19,7 +18,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { LightboxView } from '../LightboxView';
-import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
@@ -187,20 +186,16 @@ export class CollectionNoteTakingView extends CollectionSubView() {
     // let's dive in and get the actual document we want to drag/move around
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
         Doc.BrushDoc(doc);
-        let focusSpeed = 0;
         const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
         if (found) {
             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, options.easeFunc);
+                let focusSpeed = options.zoomTime ?? 500;
+                smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
+                return focusSpeed;
             }
         }
-        const endFocus = async (moved: boolean) => (options?.afterFocus ? options?.afterFocus(moved) : ViewAdjustment.doNothing);
-        this.props.focus(this.rootDoc, {
-            ...options,
-            afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
-        });
     };
 
     styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 6fd4c8c0a..572fbfec2 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -765,7 +765,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
 
     // starting the drag event for anchor resizing
     onAnchorDown = (e: React.PointerEvent, anchor: Doc, left: boolean): void => {
-        this.props._timeline?.setPointerCapture(e.pointerId);
+        //this.props._timeline?.setPointerCapture(e.pointerId);
         const newTime = (e: PointerEvent) => {
             const rect = (e.target as any).getBoundingClientRect();
             return this.props.toTimeline(e.clientX - rect.x, rect.width);
@@ -793,7 +793,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
             },
             e => {
                 this.props.setTime(newTime(e));
-                this.props._timeline?.releasePointerCapture(e.pointerId);
+                // this.props._timeline?.releasePointerCapture(e.pointerId);
                 undo?.end();
             },
             emptyFunction
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 6314b4529..4805a748b 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, OpenWhere, ViewAdjustment } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
 import { StyleProp } from '../StyleProvider';
@@ -250,20 +250,17 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     focusDocument = (doc: Doc, options: DocFocusOptions) => {
         Doc.BrushDoc(doc);
 
-        let focusSpeed = 0;
         const found = this._mainCont && Array.from(this._mainCont.getElementsByClassName('documentView-node')).find((node: any) => node.id === doc[Id]);
         if (found) {
             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, options.easeFunc);
+                let focusSpeed = options.zoomTime ?? 500;
+                smoothScroll(focusSpeed, this._mainCont!, localTop[1] + this._mainCont!.scrollTop, options.easeFunc);
+                return focusSpeed;
             }
         }
-        const endFocus = async (moved: boolean) => options?.afterFocus?.(moved) ?? ViewAdjustment.doNothing;
-        this.props.focus(this.rootDoc, {
-            ...options,
-            afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didFocus)), focusSpeed)),
-        });
+        return undefined;
     };
 
     styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx
index 437b22040..89b2fbfe3 100644
--- a/src/client/views/collections/CollectionTimeView.tsx
+++ b/src/client/views/collections/CollectionTimeView.tsx
@@ -66,11 +66,11 @@ export class CollectionTimeView extends CollectionSubView() {
     };
 
     @action
-    scrollPreview = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
+    scrollPreview = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
         // if in preview, then override document's fields with view spec
-        this._focusFilters = StrListCast(anchor.presPinDocFilters);
+        this._focusFilters = StrListCast(anchor.presDocFilters);
         this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
-        this._focusPivotField = StrCast(anchor.presPinPivotField);
+        this._focusPivotField = StrCast(anchor.presPivotField);
         return undefined;
     };
 
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 042c2b71b..99283996e 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -305,7 +305,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(), { willPan: true }, undefined, []), 100); // keeps the pinned doc in view since the sidebar shifts things
+            setTimeout(() => DocumentManager.Instance.showDocument(docList.lastElement(), { willPan: true }), 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
     }
@@ -388,13 +388,17 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     getCurrentFrame = () => {
         return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame);
     };
+    static Activate = (tabDoc: Doc) => {
+        const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc);
+        tab?.header.parent.setActiveContentItem(tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
+        return tab !== undefined;
+    };
     @action
     focusFunc = (doc: Doc, options: DocFocusOptions) => {
-        options?.afterFocus?.(false);
-
         if (!this.tab.header.parent._activeContentItem || this.tab.header.parent._activeContentItem !== this.tab.contentItem) {
             this.tab.header.parent.setActiveContentItem(this.tab.contentItem); // glr: Panning does not work when this is set - (this line is for trying to make a tab that is not topmost become topmost)
         }
+        return undefined;
     };
     active = () => this._isActive;
     @observable _forceInvalidateScreenToLocal = 0;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 79832eb39..63b566f28 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -4,7 +4,7 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import { DateField } from '../../../../fields/DateField';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../../fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField';
 import { List } from '../../../../fields/List';
@@ -15,13 +15,12 @@ import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } fro
 import { ImageField } from '../../../../fields/URLField';
 import { TraceMobx } from '../../../../fields/util';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, emptyFunction, intersectRect, returnFalse, returnTransparent, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { aggregateBounds, emptyFunction, intersectRect, returnFalse, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { CognitiveServices } from '../../../cognitive_services/CognitiveServices';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../../util/DragManager';
-import { HistoryUtil } from '../../../util/History';
 import { InteractionUtils } from '../../../util/InteractionUtils';
 import { ReplayMovements } from '../../../util/ReplayMovements';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
@@ -33,15 +32,15 @@ import { undoBatch, UndoManager } from '../../../util/UndoManager';
 import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariables.scss';
 import { Timeline } from '../../animationtimeline/Timeline';
 import { ContextMenu } from '../../ContextMenu';
+import { DocumentDecorations } from '../../DocumentDecorations';
 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, OpenWhere, ViewAdjustment } from '../../nodes/DocumentView';
+import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PinProps, PresBox } from '../../nodes/trails/PresBox';
-import { VideoBox } from '../../nodes/VideoBox';
 import { CreateImage } from '../../nodes/WebBoxRenderer';
 import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
@@ -52,8 +51,6 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import React = require('react');
-import { DocumentDecorations } from '../../DocumentDecorations';
-import { PresEffect } from '../../nodes/trails';
 
 export type collectionFreeformViewProps = {
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -300,6 +297,30 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
     }
 
+    focus = (anchor: Doc, options: DocFocusOptions) => {
+        const xfToCollection = options?.docTransform ?? Transform.Identity();
+        const savedState = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined };
+        const cantTransform = this.fitContentsToBox || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
+        const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale || 0.75 : undefined);
+
+        // focus on the document in the collection
+        const didMove = !cantTransform && !anchor.z && (panX !== savedState.panX || panY !== savedState.panY || scale !== savedState.scale);
+        // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...
+        if (didMove) {
+            const focusTime = options?.instant ? 0 : options.zoomTime ?? 500;
+            options.zoomScale && scale && (this.Document[this.scaleFieldKey] = scale);
+            this.setPan(panX, panY, focusTime, 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
+            return focusTime;
+        }
+    };
+
+    getView = async (doc: Doc): Promise<Opt<DocumentView>> => {
+        return new Promise<Opt<DocumentView>>(res => {
+            const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv));
+            findDoc(dv => res(dv));
+        });
+    };
+
     @action
     internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) {
         if (!de.embedKey && !this.ChildDrag && this.rootDoc._isGroup) return false;
@@ -1199,79 +1220,6 @@ 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();
-
-        // TODO This technically isn't correct if type !== "doc", as
-        // currently nothing is done, but we should probably push a new state
-        if (state.type === 'doc' && this.Document._panX !== undefined && this.Document._panY !== undefined) {
-            const init = state.initializers![this.Document[Id]];
-            if (!init) {
-                state.initializers![this.Document[Id]] = { panX: NumCast(this.Document._panX), panY: NumCast(this.Document._panY) };
-                HistoryUtil.pushState(state);
-            } else if (init.panX !== this.Document._panX || init.panY !== this.Document._panY) {
-                init.panX = NumCast(this.Document._panX);
-                init.panY = NumCast(this.Document._panY);
-                HistoryUtil.pushState(state);
-            }
-        }
-        // if (SelectionManager.Views().length !== 1 || SelectionManager.Views()[0].Document !== doc) {
-        //     SelectionManager.DeselectAll();
-        // }
-        if (this.props.getScrollHeight || this.props.Document.scrollTop !== undefined || this.props.Document.currentTimecode !== undefined) {
-            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?.willPanZoom ? this.Document[this.scaleFieldKey] : undefined };
-            const newState = HistoryUtil.getState();
-            const cantTransform = this.fitContentsToBox || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc);
-            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 };
-                HistoryUtil.pushState(newState);
-            }
-            // 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 ? 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) {
-                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
-            }
-
-            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) => {
-                doc.hidden && Doc.UnHighlightDoc(doc);
-                const resetView = options?.afterFocus ? await options?.afterFocus(moved) : ViewAdjustment.doNothing;
-                if (resetView) {
-                    const restoreState = savedState;
-                    if (typeof restoreState !== 'boolean') {
-                        this.Document._panX = restoreState.panX;
-                        this.Document._panY = restoreState.panY;
-                        this.Document[this.scaleFieldKey] = restoreState.scale;
-                    }
-                }
-                this._focusCount === focusCount && didMove && runInAction(() => (this._panZoomTransition = 0));
-                return resetView;
-            };
-            const xf = !cantTransform
-                ? Transform.Identity()
-                : this.props.isAnnotationOverlay
-                ? new Transform(NumCast(this.rootDoc.x), NumCast(this.rootDoc.y), this.rootDoc[WidthSym]() / Doc.NativeWidth(this.rootDoc))
-                : new Transform(NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]() / 2 - NumCast(this.rootDoc._panX), NumCast(this.rootDoc.y) + this.rootDoc[HeightSym]() / 2 - NumCast(this.rootDoc._panY), 1);
-
-            this.props.focus(!cantTransform ? this.rootDoc : doc, {
-                ...options,
-                docTransform: xf,
-                afterFocus: (didFocus: boolean) => new Promise<ViewAdjustment>(res => setTimeout(async () => res(await endFocus(didMove || didFocus)), Math.max(0, focusSpeed - (Date.now() - startTime)))),
-            });
-        }
-    };
-
     calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
         const layoutdoc = Doc.Layout(doc);
         const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y));
@@ -1371,7 +1319,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 searchFilterDocs={this.searchFilterDocs}
                 isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive}
                 isContentActive={emptyFunction}
-                focus={this.focusDocument}
+                focus={this.isAnnotationOverlay ? this.props.focus : this.focus}
                 addDocTab={this.addDocTab}
                 addDocument={this.props.addDocument}
                 removeDocument={this.props.removeDocument}
@@ -1602,17 +1550,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return elements;
     }
 
-    @action
-    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
-        const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
-        if (options.preview) {
-            this._focusFilters = StrListCast(anchor.presPinDocFilters);
-            this._focusRangeFilters = StrListCast(anchor.presPinDocRangeFilters);
-            return undefined;
-        }
-        return PresBox.restoreTargetDocView(docView, anchor, 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 = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type)
         const anchor = Docs.Create.CollectionAnchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), unrendered: true, presTransition: 500, annotationOn: this.rootDoc });
@@ -1744,7 +1681,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const nativeHeight = height;
         return CreateImage(Utils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight)
             .then(async (data_url: any) => {
-                const returnedFilename = await VideoBox.convertDataUri(data_url, filename, noSuffix, replaceRootFilename);
+                const returnedFilename = await Utils.convertDataUri(data_url, filename, noSuffix, replaceRootFilename);
                 cb(returnedFilename as string, nativeWidth, nativeHeight);
             })
             .catch(function (error: any) {
@@ -2303,21 +2240,21 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
 }
 
 export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY: number) {
+    const browseTransitionTime = 500;
     SelectionManager.DeselectAll();
-    dv.props.focus(dv.props.Document, {
-        willPanZoom: true,
-        zoomScale: 0.8,
-        afterFocus: async didMove => {
-            if (!didMove) {
-                const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
-                const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
-                const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
-                await ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5);
-            }
-            return ViewAdjustment.doNothing;
-        },
-    });
-    Doc.linkFollowHighlight(dv?.props.Document, false);
+    if (
+        dv.props.focus(dv.props.Document, {
+            willZoomCentered: true,
+            zoomScale: 0.8,
+            zoomTime: browseTransitionTime,
+        }) === undefined
+    ) {
+        const selfFfview = dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
+        const parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
+        const ffview = selfFfview && selfFfview.rootDoc[selfFfview.props.scaleField || '_viewScale'] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it.  Otherwise, zoom out on the parent ffview
+        ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), 0.5, browseTransitionTime);
+        Doc.linkFollowHighlight(dv?.props.Document, false);
+    }
 }
 ScriptingGlobals.add(CollectionBrowseClick);
 ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index bd33c4d80..a73627165 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 { VideoBox } from '../../nodes/VideoBox';
 import { pasteImageBitmap } from '../../nodes/WebBoxRenderer';
 import { PreviewCursor } from '../../PreviewCursor';
 import { SubCollectionViewProps } from '../CollectionSubView';
@@ -167,7 +166,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
                 pasteImageBitmap((data: any, error: any) => {
                     error && console.log(error);
                     data &&
-                        VideoBox.convertDataUri(data, this.props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
+                        Utils.convertDataUri(data, this.props.Document[Id] + '-thumb-frozen').then(returnedfilename => {
                             this.props.Document['thumb-frozen'] = new ImageField(returnedfilename);
                         });
                 })
diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
index b9cda7130..d54e8ce98 100644
--- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
+++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx
@@ -152,7 +152,7 @@ export class CollectionLinearView extends CollectionSubView() {
                     Currently playing:
                     {CollectionStackedTimeline.CurrentlyPlaying.map((clip, i) => (
                         <>
-                            <span className="audio-title" onPointerDown={() => DocumentManager.Instance.jumpToDocument(clip.rootDoc, { willPanZoom: true }, undefined, [])}>
+                            <span className="audio-title" onPointerDown={() => DocumentManager.Instance.showDocument(clip.rootDoc, { willZoomCentered: true })}>
                                 {clip.rootDoc.title + (i === CollectionStackedTimeline.CurrentlyPlaying.length - 1 ? '  ' : ',')}
                             </span>
                             <FontAwesomeIcon icon={!clip.ComponentView?.IsPlaying?.() ? 'play' : 'pause'} size="lg" onPointerDown={() => clip.ComponentView?.TogglePause?.()} />{' '}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index b88da5191..88d045fa7 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -234,7 +234,6 @@ export class CollectionMulticolumnView extends CollectionSubView() {
     onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
 
-    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) => {
@@ -260,7 +259,7 @@ export class CollectionMulticolumnView extends CollectionSubView() {
                 hideResizeHandles={this.props.childHideResizeHandles?.()}
                 hideDecorationTitle={this.props.childHideDecorationTitle?.()}
                 fitContentsToBox={this.props.fitContentsToBox}
-                focus={this.focusDocument}
+                focus={this.props.focus}
                 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 407deaabd..f18917bef 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -234,7 +234,6 @@ export class CollectionMultirowView extends CollectionSubView() {
     onChildClickHandler = () => ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => ScriptCast(this.Document.onChildDoubleClick);
 
-    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) => {
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaCells.tsx
index 97a6c5c18..18ddd881b 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, { willPan: true }, emptyFunction, targetContext ? [targetContext] : [], () => this.props.setPreviewDoc(this._rowDoc));
+            DocumentManager.Instance.showDocument(this._rowDoc, { willPan: true }, () => this.props.setPreviewDoc(this._rowDoc));
         }
     };
 
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index a2a2255e6..cdac91a62 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -139,7 +139,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                             ? Cast(this.props.linkDoc.anchor12, Doc, null)
                             : undefined;
 
-                    if (focusDoc) this.props.docView.ComponentView?.scrollFocus?.(focusDoc, { instant: true });
+                    if (focusDoc) this.props.docView.props.focus(focusDoc, { instant: true });
                     LinkFollower.FollowLink(this.props.linkDoc, this.props.sourceDoc, this.props.docView.props, false);
                 }
             }
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 3966aecf6..4c26468b9 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -71,12 +71,6 @@ 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();
     }
@@ -118,6 +112,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     @action
     componentDidMount() {
+        this.props.setContentView?.(this);
         if (this.path) {
             this.mediaState = media_state.Paused;
             this.setPlayheadTime(NumCast(this.layoutDoc.clipStart));
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index 9a32556f8..901415d1e 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -152,9 +152,9 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
             }
             CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val)));
             CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode)));
-            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (Doc.GetProto(doc)[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', Doc.GetProto(doc), currTimecode)));
-            const targetDoc = doc.type === DocumentType.RTF ? Doc.GetProto(doc) : doc; // data fields, like rtf 'text' exist on the data doc, so
-            doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
+            CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode)));
+            const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so
+            //doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!)
             targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0');
             targetDoc.dataTransition = 'inherit';
         });
@@ -190,7 +190,6 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.();
     panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.();
     screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y);
-    focusDoc = (doc: Doc) => this.props.focus(doc, {});
     returnThis = () => this;
     render() {
         TraceMobx();
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index dd03b9b99..ecffe6c4f 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -4,6 +4,7 @@ import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { emptyFunction, OmitKeys, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { undoBatch } from '../../util/UndoManager';
@@ -12,6 +13,7 @@ import { StyleProp } from '../StyleProvider';
 import './ComparisonBox.scss';
 import { DocumentView, DocumentViewProps } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
 import React = require('react');
 
 @observer
@@ -24,6 +26,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
 
     @observable _animating = '';
 
+    componentDidMount() {
+        this.props.setContentView?.(this);
+    }
+
     protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
         this._disposers[disposerId]?.();
         if (ele) {
@@ -72,6 +78,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
         return false;
     };
 
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
+        const anchor = Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc });
+        if (anchor) {
+            if (!addAsAnnotation) anchor.backgroundColor = 'transparent';
+            /* addAsAnnotation &&*/ this.addDocument(anchor);
+            PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), clippable: true } }, this.rootDoc);
+            return anchor;
+        }
+        return this.rootDoc;
+    };
+
     @undoBatch
     clearDoc = (e: React.MouseEvent, fieldKey: string) => {
         e.stopPropagation; // prevent click event action (slider movement) in registerSliding
@@ -103,7 +120,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                 <>
                     <DocumentView
                         ref={r => {
-                            whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
+                            //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 f2aaa5cbc..dcccd1143 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -66,11 +66,6 @@ declare class MediaRecorder {
     constructor(e: any);
 }
 
-export enum ViewAdjustment {
-    resetView = 1,
-    doNothing = 0,
-}
-
 export enum OpenWhere {
     lightbox = 'lightbox',
     add = 'add',
@@ -96,33 +91,30 @@ export enum OpenWhereMod {
 }
 
 export interface DocFocusOptions {
-    originalTarget?: Doc; // set in JumpToDocument, used by TabDocView to determine whether to fit contents to tab
     willPan?: boolean; // determines whether to pan to target document
-    willPanZoom?: boolean; // determines whether to zoom in on target document
+    willZoomCentered?: 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
     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)
     preview?: boolean; // whether changes should be previewed by the componentView or written to the document
     effect?: Doc; // animation effect for focus
     noSelect?: boolean; // whether target should be selected after focusing
     playAudio?: boolean; // whether to play audio annotation on focus
-    openInLightbox?: boolean; // whether to open target in lightbox or just focus on it
+    openLocation?: string; // where to open a missing document
     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
 }
-export type DocAfterFocusFunc = (notFocused: boolean) => Promise<ViewAdjustment>;
-export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => void;
+export type DocFocusFunc = (doc: Doc, options: DocFocusOptions) => Opt<number>;
 export type StyleProviderFunc = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => any;
 export interface DocComponentView {
     updateIcon?: () => void; // updates the icon representation of the document
     getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => 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)
-    scrollPreview?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
-    scrollFocus?: (docView: DocumentView, doc: Doc, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
+    scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: DocFocusOptions) => Opt<number>; // returns the duration of the focus
     brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void;
+    getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined
     addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox
     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.
     shrinkWrap?: () => void; // requests a document to display all of its contents with no white space.  currently only implemented (needed?) for freeform views
@@ -131,10 +123,10 @@ export interface DocComponentView {
     getKeyFrameEditing?: () => boolean; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
     setKeyFrameEditing?: (set: boolean) => void; // whether the document is in keyframe editing mode (if it is, then all hidden documents that are not active at the keyframe time will still be shown)
     playFrom?: (time: number, endTime?: number) => void;
-    Pause?: () => void;
-    IsPlaying?: () => boolean;
-    TogglePause?: (keep?: boolean) => void;
-    setFocus?: () => void;
+    Pause?: () => void; // pause a media document (eg, audio/video)
+    IsPlaying?: () => boolean; // is a media document playing
+    TogglePause?: (keep?: boolean) => void; // toggle media document playing state
+    setFocus?: () => void; // sets input focus to the componentView
     componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null;
     incrementalRendering?: () => void;
     fieldKey?: string;
@@ -583,32 +575,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     };
 
-    focus = (anchor: Doc, options: DocFocusOptions) => {
-        LightboxView.SetCookie(StrCast(anchor['cookies-set']));
-
-        // Restore viewing specification of target by reading them out of the anchor and applying to the target doc.
-        const presItem = DocCast(options.originatingDoc); // if originating doc was a presItem, then anchor will be its proto.  use presItem instead
-        const targetMatch = Doc.AreProtosEqual(anchor, this.rootDoc) || (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) ? true : false;
-        const scrollFocus =
-            (LinkDocPreview.LinkInfo ? this._componentView?.scrollPreview : undefined) ??
-            this._componentView?.scrollFocus ??
-            ((docView: DocumentView, anchor: Doc, options: DocFocusOptions) => (focusSpeed => (PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined))(options.instant ? 0 : options.zoomTime ?? 500));
-        const focusSpeed = targetMatch && scrollFocus?.(this.props.DocumentView(), presItem?.proto === anchor ? presItem : anchor, options);
-
-        // FOCUS: navigate through the display hierarchy making sure the target is in view
-        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>(async res =>
-                    setTimeout(
-                        async () => res(endFocus ? await endFocus(didFocus || focusSpeed !== undefined) : ViewAdjustment.doNothing), //
-                        didFocus ? Math.max(0, (options.zoomTime ?? 500) - (Date.now() - startTime)) : 0
-                    )
-                ),
-        });
+    defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => {
+        const targetMatch =
+            Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document
+            (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties applie to this document
+                ? true
+                : false;
+        return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
     };
+
     onClick = action((e: React.MouseEvent | React.PointerEvent) => {
         if (!this.Document.ignoreClick && this.props.renderDepth >= 0 && Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             let stopPropagate = true;
@@ -1201,7 +1176,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     ScreenToLocalTransform={this.screenToLocal}
                     rootSelected={this.rootSelected}
                     onClick={this.onClickFunc}
-                    focus={this.focus}
+                    focus={emptyFunction}
                     layoutKey={this.finalLayoutKey}
                 />
                 {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints}
@@ -1242,8 +1217,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             link =>
                 Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) ||
                 Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) ||
-                ((link.anchor1 as Doc).unrendered && Doc.AreProtosEqual((link.anchor1 as Doc).annotationOn as Doc, this.rootDoc)) ||
-                ((link.anchor2 as Doc).unrendered && Doc.AreProtosEqual((link.anchor2 as Doc).annotationOn as Doc, this.rootDoc))
+                ((link.anchor1 as Doc)?.unrendered && Doc.AreProtosEqual((link.anchor1 as Doc)?.annotationOn as Doc, this.rootDoc)) ||
+                ((link.anchor2 as Doc)?.unrendered && Doc.AreProtosEqual((link.anchor2 as Doc)?.annotationOn as Doc, this.rootDoc))
         );
     }
     @computed get allLinks() {
@@ -1721,7 +1696,6 @@ 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.type === DocumentType.PRES || this.props.treeViewDoc || Doc.AreProtosEqual(this.props.Document, Doc.UserDoc())) {
             return undefined;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 78ba499ec..d41a4bb82 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -465,7 +465,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         isContentActive={returnTrue}
                         whenChildContentsActiveChanged={returnFalse}
                         treeViewHideTitle={true}
-                        focus={returnFalse}
+                        focus={emptyFunction}
                         onCheckedClick={this.scriptField}
                         treeViewHideHeaderFields={false}
                         dontRegisterView={true}
diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx
index 8fa01c97a..75b4907cd 100644
--- a/src/client/views/nodes/FunctionPlotBox.tsx
+++ b/src/client/views/nodes/FunctionPlotBox.tsx
@@ -15,6 +15,7 @@ import { undoBatch } from '../../util/UndoManager';
 import { ViewBoxAnnotatableComponent } from '../DocComponent';
 import { DocFocusOptions, DocumentView } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
+import { PinProps, PresBox } from './trails';
 
 const EquationSchema = createSchema({});
 
@@ -37,23 +38,18 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
     componentDidMount() {
         this.props.setContentView?.(this);
         reaction(
-            () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.dataDoc.xRange, this.dataDoc.yRange],
+            () => [DocListCast(this.dataDoc[this.fieldKey]).map(doc => doc?.text), this.layoutDoc.width, this.layoutDoc.height, this.rootDoc.xRange, this.rootDoc.yRange],
             () => this.createGraph()
         );
     }
-    getAnchor = (addAsAnnotation: boolean) => {
+    getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
-        anchor.xRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
-        anchor.yRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
+        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc);
+        anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain));
+        anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain));
         if (addAsAnnotation) this.addDocument(anchor);
         return anchor;
     };
-    @action
-    scrollFocus = (docView: DocumentView, doc: Doc, options: 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;
-    };
     createGraph = (ele?: HTMLDivElement) => {
         this._plotEle = ele || this._plotEle;
         const width = this.props.PanelWidth();
@@ -65,8 +61,8 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps>
                 target: '#' + this._plotEle.id,
                 width,
                 height,
-                xAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-10, 10]) },
-                yAxis: { domain: Cast(this.dataDoc.xRange, listSpec('number'), [-1, 9]) },
+                xAxis: { domain: Cast(this.rootDoc.xRange, listSpec('number'), [-10, 10]) },
+                yAxis: { domain: Cast(this.rootDoc.yRange, listSpec('number'), [-1, 9]) },
                 grid: true,
                 data: fns.map(fn => ({
                     fn,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 363cd1d94..c6d4cb694 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 { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
@@ -17,6 +17,7 @@ import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_ser
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
+import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { undoBatch } from '../../util/UndoManager';
 import { ContextMenu } from '../../views/ContextMenu';
@@ -26,15 +27,13 @@ import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComp
 import { MarqueeAnnotator } from '../MarqueeAnnotator';
 import { AnchorMenu } from '../pdf/AnchorMenu';
 import { StyleProp } from '../StyleProvider';
-import { DocFocusOptions, DocumentView, OpenWhere } from './DocumentView';
+import { OpenWhere } from './DocumentView';
 import { FaceRectangles } from './FaceRectangles';
 import { FieldView, FieldViewProps } from './FieldView';
 import './ImageBox.scss';
 import { PinProps, PresBox } from './trails';
 import React = require('react');
 import Color = require('color');
-import { LinkDocPreview } from './LinkDocPreview';
-import { DocumentManager } from '../../util/DocumentManager';
 
 export const pageSchema = createSchema({
     googlePhotosUrl: 'string',
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 1eab06381..bbe9b4323 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -253,7 +253,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                         <DocumentView
                             ref={r => {
                                 const targetanchor = this._linkDoc && this._linkSrc && LinkManager.getOppositeAnchor(this._linkDoc, this._linkSrc);
-                                targetanchor && this._targetDoc !== targetanchor && r?.focus(targetanchor, {});
+                                targetanchor && this._targetDoc !== targetanchor && r?.props.focus?.(targetanchor, {});
                             }}
                             Document={this._targetDoc!}
                             moveDocument={returnFalse}
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 6b2f2246a..5f207cc0d 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -11,6 +11,7 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
+import { DocumentManager } from '../../util/DocumentManager';
 import { KeyCodes } from '../../util/KeyCodes';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
@@ -27,7 +28,6 @@ import { FieldView, FieldViewProps } from './FieldView';
 import { ImageBox } from './ImageBox';
 import './PDFBox.scss';
 import { PinProps, PresBox } from './trails';
-import { VideoBox } from './VideoBox';
 import React = require('react');
 
 @observer
@@ -141,7 +141,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             4
         )
             .then((data_url: any) => {
-                VideoBox.convertDataUri(data_url, region[Id]).then(returnedfilename =>
+                Utils.convertDataUri(data_url, region[Id]).then(returnedfilename =>
                     setTimeout(
                         action(() => {
                             croppingProto.data = new ImageField(returnedfilename);
@@ -207,17 +207,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         }
         return this.props.addDocTab(doc, where);
     };
-    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
-        let didToggle = false;
-        if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(anchor) && !this.SidebarShown) {
-            this.toggleSidebar(options.preview);
-            didToggle = true;
-        }
-        if (this._sidebarRef?.current?.makeDocUnfiltered(anchor)) return 1;
+    focus = (anchor: Doc, options: DocFocusOptions) => {
         this._initialScrollTarget = anchor;
-        PresBox.restoreTargetDocView(docView, anchor, options.zoomTime ?? 500);
-        return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.presPinViewScroll, NumCast(anchor.y)), options) ?? (didToggle ? 1 : undefined);
+        return this._pdfViewer?.scrollFocus(anchor, NumCast(anchor.y, NumCast(anchor.presViewScroll)), options);
+    };
+
+    getView = async (doc: Doc) => {
+        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
+        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
     };
+
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
         let ele: Opt<HTMLDivElement> = undefined;
         if (this._pdfViewer?.selectionContent()) {
@@ -296,7 +295,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         this._pdfViewer = pdfViewer;
         const docView = this.props.DocumentView?.();
         if (this._initialScrollTarget && docView) {
-            this.scrollFocus(docView, this._initialScrollTarget, { instant: true });
+            this.focus(this._initialScrollTarget, { instant: true });
             this._initialScrollTarget = undefined;
         }
     };
@@ -498,6 +497,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
                         pdf={this._pdf!}
+                        focus={this.focus}
                         url={this.pdfUrl!.url.pathname}
                         isContentActive={this.isPdfContentActive}
                         anchorMenuClick={this.anchorMenuClick}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 1f2eee086..7df3d5f24 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,7 +3,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
 import { observer } from 'mobx-react';
 import { basename } from 'path';
-import * as rp from 'request-promise';
 import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
@@ -52,30 +51,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     public static LayoutString(fieldKey: string) {
         return FieldView.LayoutString(VideoBox, fieldKey);
     }
-    /**
-     * Uploads an image buffer to the server and stores with specified filename.  by default the image
-     * is stored at multiple resolutions each retrieved by using the filename appended with _o, _s, _m, _l (indicating original, small, medium, or large)
-     * @param imageUri the bytes of the image
-     * @param returnedFilename the base filename to store the image on the server
-     * @param nosuffix optionally suppress creating multiple resolution images
-     */
-    public static async convertDataUri(imageUri: string, returnedFilename: string, nosuffix = false, replaceRootFilename?: string) {
-        try {
-            const posting = Utils.prepend('/uploadURI');
-            const returnedUri = await rp.post(posting, {
-                body: {
-                    uri: imageUri,
-                    name: returnedFilename,
-                    nosuffix,
-                    replaceRootFilename,
-                },
-                json: true,
-            });
-            return returnedUri;
-        } catch (e) {
-            console.log('VideoBox :' + e);
-        }
-    }
 
     static _youtubeIframeCounter: number = 0;
     static heightPercent = 80; // height of video relative to videoBox when timeline is open
@@ -243,8 +218,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     }
 
+    _keepCurrentlyPlaying = false; // flag to prevent document when paused from being removed from global 'currentlyPlaying' list
     IsPlaying = () => this._playing;
-    _keepCurrentlyPlaying = false;
     TogglePause = () => {
         if (!this._playing) this.Play();
         else {
@@ -369,7 +344,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
             const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_'));
             const filename = basename(encodedFilename);
-            VideoBox.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
+            Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createRealSummaryLink)(returnedFilename, downX, downY));
         }
     };
 
@@ -439,6 +414,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         }
     };
 
+    // getView = async (doc: Doc) => {
+    //     return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+    // };
+
     // extracts video thumbnails and saves them as field of doc
     getVideoThumbnails = () => {
         if (this.dataDoc.thumbnails !== undefined) return;
@@ -455,7 +434,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
             canvas.getContext('2d')?.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, 100, 100);
             const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, '');
             const encodedFilename = encodeURIComponent('thumbnail' + retitled + '_' + video.currentTime.toString().replace(/\./, '_'));
-            thumbnailPromises?.push(VideoBox.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
+            thumbnailPromises?.push(Utils.convertDataUri(canvas.toDataURL(), basename(encodedFilename), true));
             const newTime = video.currentTime + video.duration / (VideoBox.numThumbnails - 1);
             if (newTime < video.duration) {
                 video.currentTime = newTime;
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index f5eab51c2..b1acef44a 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -13,6 +13,7 @@ import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
+import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -32,10 +33,9 @@ import { StyleProp } from '../StyleProvider';
 import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import { FieldView, FieldViewProps } from './FieldView';
 import { LinkDocPreview } from './LinkDocPreview';
-import { VideoBox } from './VideoBox';
+import { PinProps, PresBox } from './trails';
 import './WebBox.scss';
 import React = require('react');
-import { PinProps, PresBox } from './trails';
 const { CreateImage } = require('./WebBoxRenderer');
 const _global = (window /* browser */ || global) /* node */ as any;
 const htmlToText = require('html-to-text');
@@ -90,7 +90,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return this.allAnnotations.filter(a => a.textInlineAnnotations);
     }
     @computed get webField() {
-        return Cast(this.dataDoc[this.props.fieldKey], WebField)?.url;
+        return Cast(this.rootDoc[this.props.fieldKey], WebField)?.url;
     }
     @computed get webThumb() {
         return (
@@ -163,7 +163,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         console.log('BAD DATA IN THUMB CREATION');
                         return;
                     }
-                    VideoBox.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
+                    Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
                         setTimeout(
                             action(() => {
                                 this.rootDoc.thumbLockout = false;
@@ -188,10 +188,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             this._annotationKeySuffix = () => this._urlHash + '-annotations';
             const reqdFuncs: { [key: string]: string } = {};
             // bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created)
-            reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"`;
+            reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"])`;
             reqdFuncs[this.fieldKey + '-annotations-setter'] = `this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"] = value`;
-            reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"`;
-            DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs);
+            reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"])`;
+            DocUtils.AssignScripts(this.rootDoc, {}, reqdFuncs);
         });
         reaction(
             () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document),
@@ -294,32 +294,25 @@ 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);
-    focus = (doc: Doc, options: DocFocusOptions) => {
-        !doc.unrendered && this.props.DocumentView?.() && this.scrollFocus(this.props.DocumentView?.(), doc, {});
-        this.props.focus(doc, options);
-    };
-    scrollFocus = (docView: DocumentView, anchor: Doc, options: DocFocusOptions) => {
-        if (this._url && StrCast(anchor.webUrl) !== this._url) this.submitURL(StrCast(anchor.webUrl), options.preview);
-        if (DocListCast(this.props.Document[this.fieldKey + '-sidebar']).includes(anchor) && !this.SidebarShown) {
-            this.toggleSidebar(options.preview);
-        }
-        if (this._sidebarRef?.current?.makeDocUnfiltered(anchor)) return 1;
+    focus = (anchor: Doc, options: DocFocusOptions) => {
         if (anchor !== this.rootDoc && this._outerRef.current) {
             const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1);
-            const scrollTo = !anchor[HeightSym]()
-                ? NumCast(anchor.y)
-                : Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
-            const focusSpeed = options.instant ? 0 : options.zoomTime ?? 500;
-            if (scrollTo !== undefined && this._initialScroll === undefined) {
-                this.goTo(scrollTo, focusSpeed, options.easeFunc);
-                PresBox.restoreTargetDocView(docView, anchor, focusSpeed);
-                return focusSpeed;
-            } else if (!this._webPageHasBeenRendered || !this._scrollHeight || this._initialScroll !== undefined) {
-                this._initialScroll = scrollTo;
-                return PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined;
+            const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight));
+            if (scrollTo !== undefined) {
+                if (this._initialScroll === undefined) {
+                    this.goTo(scrollTo, options.zoomTime ?? 500, options.easeFunc);
+                } else {
+                    this._initialScroll = scrollTo;
+                }
             }
         }
-        return undefined;
+    };
+
+    getView = async (doc: Doc) => {
+        if (this.rootDoc.layoutKey === 'layout_icon') this.props.DocumentView?.().iconify();
+        if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl));
+        if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false);
+        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
     };
 
     sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
@@ -545,14 +538,14 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     forward = (checkAvailable?: boolean) => {
-        const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'), []);
-        const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []);
+        const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string'), []);
+        const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'), []);
         if (checkAvailable) return future.length;
         runInAction(() => {
             if (future.length) {
                 const curUrl = this._url;
-                this.dataDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]);
-                this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
+                this.rootDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]);
+                this.rootDoc[this.fieldKey] = new WebField(new URL(future.pop()!));
                 if (this._webUrl === this._url) {
                     this._webUrl = curUrl;
                     setTimeout(action(() => (this._webUrl = this._url)));
@@ -566,15 +559,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     back = (checkAvailable?: boolean) => {
-        const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'));
-        const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'), []);
+        const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string'));
+        const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'), []);
         if (checkAvailable) return history.length;
         runInAction(() => {
             if (history.length) {
                 const curUrl = this._url;
-                if (future === undefined) this.dataDoc[this.fieldKey + '-future'] = new List<string>([this._url]);
-                else this.dataDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]);
-                this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
+                if (future === undefined) this.rootDoc[this.fieldKey + '-future'] = new List<string>([this._url]);
+                else this.rootDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]);
+                this.layoutDoc[this.fieldKey] = new WebField(new URL(history.pop()!));
                 if (this._webUrl === this._url) {
                     this._webUrl = curUrl;
                     setTimeout(action(() => (this._webUrl = this._url)));
@@ -601,11 +594,11 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         if (!newUrl) return;
         if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl;
         try {
-            const future = Cast(this.dataDoc[this.fieldKey + '-future'], listSpec('string'));
-            const history = Cast(this.dataDoc[this.fieldKey + '-history'], listSpec('string'));
+            const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string'));
+            const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'));
             const url = this.webField?.toString();
             if (url && !preview) {
-                this.dataDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]);
+                this.rootDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]);
                 this.layoutDoc._scrollTop = 0;
                 if (this._webPageHasBeenRendered) {
                     this.layoutDoc.thumb = undefined;
@@ -616,7 +609,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 future && (future.length = 0);
             }
             if (!preview) {
-                this.dataDoc[this.fieldKey] = new WebField(new URL(newUrl));
+                this.layoutDoc[this.fieldKey] = new WebField(new URL(newUrl));
                 !dontUpdateIframe && (this._webUrl = this._url);
             }
         } catch (e) {
@@ -742,7 +735,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     @computed get urlContent() {
         if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null;
         this.props.thumbShown?.();
-        const field = this.dataDoc[this.props.fieldKey];
+        const field = this.rootDoc[this.props.fieldKey];
         let view;
         if (field instanceof HtmlField) {
             view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />;
@@ -849,7 +842,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     toggleSidebar = action((preview: boolean = false) => {
         var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
         if (!nativeWidth) {
-            const defaultNativeWidth = this.dataDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym]();
+            const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym]();
             Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth);
             Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[HeightSym]() / this.Document[WidthSym]()) * defaultNativeWidth);
             nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
@@ -1006,6 +999,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none');
     annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none');
     render() {
+        setTimeout(() => DocListCast(this.rootDoc[this.annotationKey]).forEach(doc => (doc.webUrl = this._url)));
         const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
         const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any);
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index dc7a11e6e..d857b150c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -230,16 +230,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
     }
 
-    sidebarAddDocTab = (doc: Doc, where: OpenWhere) => {
-        if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) {
-            this.toggleSidebar(false);
-            return true;
-        }
-        return this.props.addDocTab(doc, where);
-    };
-
     getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => {
-        if (!pinProps) return this.rootDoc;
+        if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc;
         const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true });
         this.addDocument(anchor);
         this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
@@ -948,12 +940,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return anchorDoc ?? this.rootDoc;
     }
 
-    scrollFocus = (docView: DocumentView, textAnchor: Doc, options: DocFocusOptions) => {
-        let didToggle = false;
-        if (DocListCast(this.Document[this.fieldKey + '-sidebar']).includes(textAnchor) && !this.SidebarShown) {
-            this.toggleSidebar(options.preview);
-            didToggle = true;
+    getView = async (doc: Doc) => {
+        if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
+            !this.SidebarShown && this.toggleSidebar(false);
+            setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
         }
+        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
+    };
+    focus = (textAnchor: Doc, options: DocFocusOptions) => {
+        const focusSpeed = options.zoomTime ?? 500;
         const textAnchorId = textAnchor[Id];
         const findAnchorFrag = (frag: Fragment, editor: EditorView) => {
             const nodes: Node[] = [];
@@ -992,7 +987,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) {
-                !options.instant && (this._focusSpeed = 500);
+                !options.instant && (this._focusSpeed = focusSpeed);
                 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
@@ -1004,9 +999,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000));
             }
         }
-        const finalFocusSpeed = this._didScroll ? this._focusSpeed : didToggle ? 1 : undefined;
-        PresBox.restoreTargetDocView(docView, textAnchor, finalFocusSpeed ?? 0);
-        return finalFocusSpeed; // if we actually scrolled, then return some focusSpeed
     };
 
     // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc.
diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss
index fd202590e..eb91c82f3 100644
--- a/src/client/views/nodes/trails/PresBox.scss
+++ b/src/client/views/nodes/trails/PresBox.scss
@@ -12,7 +12,7 @@
     height: 100%;
     min-height: 35px;
     letter-spacing: 2px;
-    overflow: hidden;
+    //overflow: hidden;
     transition: 0.7s opacity ease;
 
     .presBox-listCont {
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index ab4822117..94962650a 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -46,6 +46,7 @@ export interface pinDataTypes {
     pivot?: boolean;
     temporal?: boolean;
     clippable?: boolean;
+    datarange?: boolean;
     dataview?: boolean;
     textview?: boolean;
     poslayoutview?: boolean;
@@ -329,6 +330,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         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 datarange = [DocumentType.FUNCPLOT].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;
@@ -350,23 +352,33 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             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.rotation !== NumCast(activeItem.presRotation, 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.rotation = NumCast(activeItem.presRotation, 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 || (!pinDataTypes && activeItem.presPinClipWidth !== undefined)) {
-            if (bestTarget._clipWidth !== activeItem.presPinClipWidth) {
-                bestTarget._clipWidth = activeItem.presPinClipWidth;
+        if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) {
+            if (bestTarget.xRange !== activeItem.presXRange) {
+                bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy]();
+                changed = true;
+            }
+            if (bestTarget.yRange !== activeItem.presYRange) {
+                bestTarget.yRange = (activeItem.presYRange as ObjectField)?.[Copy]();
+                changed = true;
+            }
+        }
+        if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presClipWidth !== undefined)) {
+            if (bestTarget._clipWidth !== activeItem.presClipWidth) {
+                bestTarget._clipWidth = activeItem.presClipWidth;
                 changed = true;
             }
         }
@@ -381,8 +393,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 Doc.GetProto(bestTarget).fillColor = activeItem.presFillColor;
                 changed = true;
             }
-            if (bestTarget.color !== activeItem.color) {
-                Doc.GetProto(bestTarget).color = activeItem.color;
+            if (bestTarget.color !== activeItem.presColor) {
+                Doc.GetProto(bestTarget).color = activeItem.presColor;
                 changed = true;
             }
             if (bestTarget.width !== activeItem.width) {
@@ -394,31 +406,31 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 changed = true;
             }
         }
-        if ((pinDataTypes?.viewType && activeItem.presPinViewType !== undefined) || (!pinDataTypes && activeItem.presPinViewType !== undefined)) {
-            if (bestTarget._viewType !== activeItem.presPinViewType) {
-                bestTarget._viewType = activeItem.presPinViewType;
+        if ((pinDataTypes?.viewType && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) {
+            if (bestTarget._viewType !== activeItem.presViewType) {
+                bestTarget._viewType = activeItem.presViewType;
                 changed = true;
             }
         }
 
-        if ((pinDataTypes?.filters && activeItem.presPinDocFilters !== undefined) || (!pinDataTypes && activeItem.presPinDocFilters !== undefined)) {
-            if (bestTarget.docFilters !== activeItem.presPinDocFilters) {
-                bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presPinDocFilters as ObjectField) || new List<string>([]);
+        if ((pinDataTypes?.filters && activeItem.presDocFilters !== undefined) || (!pinDataTypes && activeItem.presDocFilters !== undefined)) {
+            if (bestTarget.docFilters !== activeItem.presDocFilters) {
+                bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]);
                 changed = true;
             }
         }
 
-        if ((pinDataTypes?.pivot && activeItem.presPinPvitoField !== undefined) || (!pinDataTypes && activeItem.presPinPivotField !== undefined)) {
-            if (bestTarget.pivotField !== activeItem.presPinPivotField) {
-                bestTarget.pivotField = activeItem.presPinPivotField;
+        if ((pinDataTypes?.pivot && activeItem.presPivotField !== undefined) || (!pinDataTypes && activeItem.presPivotField !== undefined)) {
+            if (bestTarget.pivotField !== activeItem.presPivotField) {
+                bestTarget.pivotField = activeItem.presPivotField;
                 bestTarget._prevFilterIndex = 1; // need to revisit this...see CollectionTimeView
                 changed = true;
             }
         }
 
-        if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presPinViewScroll !== undefined)) {
-            if (bestTarget._scrollTop !== activeItem.presPinViewScroll) {
-                bestTarget._scrollTop = activeItem.presPinViewScroll;
+        if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) {
+            if (bestTarget._scrollTop !== activeItem.presViewScroll) {
+                bestTarget._scrollTop = activeItem.presViewScroll;
                 changed = true;
                 const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number'));
                 if (contentBounds) {
@@ -479,7 +491,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 });
             setTimeout(() => Array.from(transitioned).forEach(action(doc => (doc._dataTransition = undefined))), transTime + 10);
         }
-        if (pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPinViewX !== undefined || activeItem.presPinViewScale !== undefined))) {
+        if (pinDataTypes?.pannable || (!pinDataTypes && (activeItem.presPinViewBounds !== undefined || activeItem.presPanX !== undefined || activeItem.presViewScale !== undefined))) {
             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] };
@@ -492,10 +504,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     dv.ComponentView?.brushView?.(viewport);
                 }
             } else {
-                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;
+                if (bestTarget._panX !== activeItem.presPanX || bestTarget._panY !== activeItem.presPanY || bestTarget._viewScale !== activeItem.presViewScale) {
+                    bestTarget._panX = activeItem.presPanX;
+                    bestTarget._panY = activeItem.presPanY;
+                    bestTarget._viewScale = activeItem.presViewScale;
                     changed = true;
                 }
             }
@@ -514,7 +526,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             pinDoc.presPinLayout = true;
             pinDoc.presX = NumCast(targetDoc.x);
             pinDoc.presY = NumCast(targetDoc.y);
-            pinDoc.presRot = NumCast(targetDoc.rotation);
+            pinDoc.presRotation = NumCast(targetDoc.rotation);
             pinDoc.presWidth = NumCast(targetDoc.width);
             pinDoc.presHeight = NumCast(targetDoc.height);
         }
@@ -526,6 +538,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 pinProps.pinData.pannable ||
                 pinProps.pinData.viewType ||
                 pinProps.pinData.clippable ||
+                pinProps.pinData.datarange ||
                 pinProps.pinData.dataview ||
                 pinProps.pinData.textview ||
                 pinProps.pinData.poslayoutview ||
@@ -546,8 +559,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 pinDoc.presWidth = targetDoc._width;
                 pinDoc.presHeight = targetDoc._height;
             }
-            if (pinProps.pinData.scrollable) pinDoc.presPinViewScroll = targetDoc._scrollTop;
-            if (pinProps.pinData.clippable) pinDoc.presPinClipWidth = targetDoc._clipWidth;
+            if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._scrollTop;
+            if (pinProps.pinData.clippable) pinDoc.presClipWidth = targetDoc._clipWidth;
+            if (pinProps.pinData.datarange) {
+                pinDoc.presXRange = undefined; //targetDoc?.xrange;
+                pinDoc.presYRange = undefined; //targetDoc?.yrange;
+            }
             if (pinProps.pinData.poslayoutview)
                 pinDoc.presPinLayoutData = new List<string>(
                     DocListCast(targetDoc[fkey] as ObjectField).map(d =>
@@ -564,13 +581,13 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         })
                     )
                 );
-            if (pinProps.pinData.viewType) pinDoc.presPinViewType = targetDoc._viewType;
-            if (pinProps.pinData.filters) pinDoc.presPinDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
-            if (pinProps.pinData.pivot) pinDoc.presPinPivotField = targetDoc._pivotField;
+            if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType;
+            if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField);
+            if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField;
             if (pinProps.pinData.pannable) {
-                pinDoc.presPinViewX = NumCast(targetDoc._panX);
-                pinDoc.presPinViewY = NumCast(targetDoc._panY);
-                pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1);
+                pinDoc.presPanX = NumCast(targetDoc._panX);
+                pinDoc.presPanY = NumCast(targetDoc._panY);
+                pinDoc.presViewScale = NumCast(targetDoc._viewScale, 1);
             }
             if (pinProps.pinData.temporal) {
                 pinDoc.presStartTime = targetDoc._currentTimecode;
@@ -582,9 +599,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             // If pinWithView option set then update scale and x / y props of slide
             const bounds = pinProps.pinViewport;
             pinDoc.presPinView = true;
-            pinDoc.presPinViewScale = NumCast(targetDoc._viewScale, 1);
-            pinDoc.presPinViewX = bounds.left + bounds.width / 2;
-            pinDoc.presPinViewY = bounds.top + bounds.height / 2;
+            pinDoc.presViewScale = NumCast(targetDoc._viewScale, 1);
+            pinDoc.presPanX = bounds.left + bounds.width / 2;
+            pinDoc.presPanY = bounds.top + bounds.height / 2;
             pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]);
         }
     }
@@ -618,14 +635,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             }
             finished();
         });
-        const createDocView = (doc: Doc, finished?: () => void) => {
-            DocumentManager.Instance.AddViewRenderedCb(doc, () => finished?.());
-            LightboxView.AddDocTab(doc, OpenWhere.lightbox);
-        };
-        PresBox.NavigateToTarget(targetDoc, activeItem, createDocView, resetSelection);
+        PresBox.NavigateToTarget(targetDoc, activeItem, resetSelection);
     };
 
-    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, createDocView: any, finished?: () => void) {
+    static NavigateToTarget(targetDoc: Doc, activeItem: Doc, finished?: () => void) {
         if (activeItem.presMovement === PresMovement.None && targetDoc.type === DocumentType.SCRIPTING) {
             (DocumentManager.Instance.getFirstDocumentView(targetDoc)?.ComponentView as ScriptingBox)?.onRun?.();
             return;
@@ -634,7 +647,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const presTime = NumCast(activeItem.presTransition, effect ? 750 : 500);
         const options: DocFocusOptions = {
             willPan: activeItem.presMovement !== PresMovement.None,
-            willPanZoom: activeItem.presMovement === PresMovement.Zoom || activeItem.presMovement === PresMovement.Jump || activeItem.presMovement === PresMovement.Center,
+            willZoomCentered: 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 : Math.min(Math.max(effect ? 750 : 500, (effect ? 0.2 : 1) * presTime), presTime),
             effect: activeItem,
@@ -644,38 +657,23 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
             zoomTextSelections: BoolCast(activeItem.presZoomText),
             playAudio: BoolCast(activeItem.presPlayAudio),
         };
-        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), undefined, targetDoc);
-            }
-        };
-        const finishAndRestoreLayout = () => {
-            finished?.();
-            restoreLayout();
-        };
-        const containerDocContext = DocumentManager.GetContextPath(targetDoc);
-
-        let context = containerDocContext.length ? containerDocContext[0] : targetDoc;
         if (activeItem.presOpenInLightbox) {
-            if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(DocCast(targetDoc.annotationOn) ?? targetDoc))) {
-                context = DocCast(targetDoc.annotationOn) ?? targetDoc;
-                LightboxView.SetLightboxDoc(context); // openInTab(targetDoc);
+            const context = DocCast(targetDoc.annotationOn) ?? targetDoc;
+            if (!DocumentManager.Instance.getLightboxDocumentView(context)) {
+                LightboxView.SetLightboxDoc(context);
             }
         }
         if (targetDoc) {
             if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined;
 
             DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => {
-                if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(context.annotationOn) ?? context)) {
+                // if target or the doc it annotates is not in the lightbox, then close the lightbox
+                if (!DocumentManager.Instance.getLightboxDocumentView(DocCast(targetDoc.annotationOn) ?? targetDoc)) {
                     LightboxView.SetLightboxDoc(undefined);
                 }
-                DocumentManager.Instance.jumpToDocument(targetDoc, options, createDocView, containerDocContext, finishAndRestoreLayout);
+                DocumentManager.Instance.showDocument(targetDoc, options, finished);
             });
-        } else finishAndRestoreLayout();
+        } else finished?.();
     }
 
     /**
@@ -992,11 +990,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         presDocView && SelectionManager.SelectView(presDocView, false);
     };
 
-    focusElement = (doc: Doc, options: DocFocusOptions) => this.selectElement(doc);
+    focusElement = (doc: Doc, options: DocFocusOptions) => {
+        this.selectElement(doc);
+        return undefined;
+    };
 
     //Regular click
     @action
-    selectElement = async (doc: Doc, noNav = false) => {
+    selectElement = (doc: Doc, noNav = false) => {
         CollectionStackedTimeline.CurrentlyPlaying?.map((clip, i) => clip?.ComponentView?.Pause?.());
         if (noNav) {
             const index = this.childDocs.indexOf(doc);
@@ -1192,15 +1193,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     }
                 } else if (doc.presPinView && presCollection === tagDoc && dv) {
                     // Case B: Document is presPinView and is presCollection
-                    const scale: number = 1 / NumCast(doc.presPinViewScale);
+                    const scale: number = 1 / NumCast(doc.presViewScale);
                     const height: number = dv.props.PanelHeight() * scale;
                     const width: number = dv.props.PanelWidth() * scale;
                     const indWidth = width / 10;
                     const indHeight = Math.max(height / 10, 15);
                     const indEdge = Math.max(indWidth, indHeight);
                     const indFontSize = indEdge * 0.8;
-                    const xLoc: number = NumCast(doc.presPinViewX) - width / 2;
-                    const yLoc: number = NumCast(doc.presPinViewY) - height / 2;
+                    const xLoc: number = NumCast(doc.presPanX) - width / 2;
+                    const yLoc: number = NumCast(doc.presPanY) - height / 2;
                     docs.push(tagDoc);
                     order.push(
                         <>
@@ -1233,8 +1234,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                 if ((index = 0)) pathPoints = n1x + ',' + n1y;
                 else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
             } else if (doc.presPinView) {
-                const n1x = NumCast(doc.presPinViewX);
-                const n1y = NumCast(doc.presPinViewY);
+                const n1x = NumCast(doc.presPanX);
+                const n1y = NumCast(doc.presPanY);
                 if ((index = 0)) pathPoints = n1x + ',' + n1y;
                 else pathPoints = pathPoints + ' ' + n1x + ',' + n1y;
             }
@@ -1960,12 +1961,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
 
-    scrollFocus = (docView: DocumentView, doc: Doc, options: DocFocusOptions) => {
-        // this.gotoDocument(0);
-        // this.startOrPause(false);
-        return undefined;
-    };
-
     _keyTimer: NodeJS.Timeout | undefined;
 
     /**
@@ -2041,9 +2036,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel';
         const mode = StrCast(this.rootDoc._viewType) as CollectionViewType;
         const isMini: boolean = this.toolbarWidth <= 100;
+        const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc);
         const activeColor = Colors.LIGHT_BLUE;
         const inactiveColor = Colors.WHITE;
-        return mode === CollectionViewType.Carousel3D ? null : (
+        return mode === CollectionViewType.Carousel3D || inOverlay ? null : (
             <div id="toolbarContainer" className={'presBox-toolbar'}>
                 {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}>
                     <FontAwesomeIcon icon={"plus"} />
@@ -2404,11 +2400,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
         );
     }
     static NavigateToDoc(bestTarget: Doc, activeItem: Doc) {
-        const openInTab = (doc: Doc, finished?: () => void) => {
-            CollectionDockingView.AddSplit(doc, OpenWhereMod.right);
-            finished?.();
-        };
-        PresBox.NavigateToTarget(bestTarget, activeItem, openInTab);
+        PresBox.NavigateToTarget(bestTarget, activeItem);
     }
 }
 
diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx
index d19b78dbc..698a2817e 100644
--- a/src/client/views/nodes/trails/PresElementBox.tsx
+++ b/src/client/views/nodes/trails/PresElementBox.tsx
@@ -297,7 +297,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const targetDoc = DocCast(presTargetDoc.annotationOn) ?? presTargetDoc;
         activeItem.presX = NumCast(targetDoc.x);
         activeItem.presY = NumCast(targetDoc.y);
-        activeItem.presRot = NumCast(targetDoc.rotation);
+        activeItem.presRotation = NumCast(targetDoc.rotation);
         activeItem.presWidth = NumCast(targetDoc.width);
         activeItem.presHeight = NumCast(targetDoc.height);
         activeItem.presPinLayout = true;
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index ffc49e4f3..86d14c22e 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -179,10 +179,10 @@ export class PDFViewer extends React.Component<IViewerProps> {
         let focusSpeed: Opt<number>;
         if (doc !== this.props.rootDoc && mainCont) {
             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, windowHeight * 0.1, this._scrollHeight);
+            const scrollTo = Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight);
             if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) {
                 if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc };
-                else if (!options.instant && !options.preview) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper);
+                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 {
@@ -518,10 +518,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this.props.styleProvider?.(doc, props, property);
     };
 
-    focus = (doc: Doc, options: DocFocusOptions) => {
-        !doc.unrendered && this.props.DocumentView?.() && this.scrollToAnnotation(doc);
-        this.props.focus(doc, options);
-    };
     renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
         <div
             className="pdfViewerDash-overlay"
@@ -544,7 +540,6 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 PanelWidth={this.panelWidth}
                 ScreenToLocalTransform={this.overlayTransform}
                 dropAction={'alias'}
-                focus={this.focus}
                 docFilters={docFilters}
                 select={emptyFunction}
                 bringToFront={emptyFunction}
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 4c4275ce7..3ba60edd7 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -414,7 +414,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, { willPanZoom: true }, undefined, [], finishFunc);
+        await DocumentManager.Instance.showDocument(doc, { willZoomCentered: true }, finishFunc);
     };
 
     /**
diff --git a/src/fields/ScriptField.ts b/src/fields/ScriptField.ts
index 5f02bd73d..16da0f9e2 100644
--- a/src/fields/ScriptField.ts
+++ b/src/fields/ScriptField.ts
@@ -218,7 +218,12 @@ export class ComputedField extends ScriptField {
             doc[`${fieldKey}-indexed`] = flist;
         }
         const getField = ScriptField.CompileScript(`getIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey})`, {}, true, {});
-        const setField = ScriptField.CompileScript(`setIndexVal(self['${fieldKey}-indexed'], self.${interpolatorKey}, value)`, { value: 'any' }, true, {});
+        const setField = ScriptField.CompileScript(
+            `{setIndexVal (self['${fieldKey}-indexed'], self.${interpolatorKey}, value); console.log(self["data-indexed"][self.${interpolatorKey}],self.data,self["data-indexed"]))}`,
+            { value: 'any' },
+            false,
+            {}
+        );
         return getField.compiled ? new ComputedField(getField, setField?.compiled ? setField : undefined) : undefined;
     }
 }
diff --git a/src/fields/util.ts b/src/fields/util.ts
index 6024705ec..e517e7604 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -335,8 +335,10 @@ export function setter(target: any, in_prop: string | symbol | number, value: an
             return true;
         }
     }
-    if (target.__fields[prop] instanceof ComputedField && target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
-        return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
+    if (target.__fields[prop] instanceof ComputedField) {
+        if (target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) {
+            return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false;
+        }
     }
     return _setter(target, prop, value, receiver);
 }
-- 
cgit v1.2.3-70-g09d2


From b599afe5fdcf8afc38432e34b1e175f82499d25b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 11 Mar 2023 14:05:18 -0500
Subject: changed links from being a computed field on documents to being
 accessed by a function call on LinkManager

---
 src/client/documents/Documents.ts                  | 101 ++++++---------------
 src/client/util/CaptureManager.tsx                 |  23 +++--
 src/client/util/CurrentUserUtils.ts                |   3 +-
 src/client/util/LinkFollower.ts                    |   3 +-
 src/client/util/LinkManager.ts                     |  17 +++-
 src/client/util/SelectionManager.ts                |   4 +-
 src/client/views/LightboxView.tsx                  |   9 +-
 src/client/views/PropertiesButtons.tsx             |  10 +-
 src/client/views/PropertiesView.tsx                |   8 +-
 src/client/views/SidebarAnnos.tsx                  |   7 +-
 src/client/views/StyleProvider.tsx                 |  11 ++-
 .../collections/CollectionStackedTimeline.tsx      |   7 +-
 src/client/views/collections/CollectionSubView.tsx |   1 -
 src/client/views/collections/TreeView.tsx          |   3 +-
 src/client/views/linking/LinkMenu.tsx              |   1 -
 src/client/views/linking/LinkMenuItem.tsx          |   6 +-
 src/client/views/nodes/AudioBox.tsx                |   3 +-
 src/client/views/nodes/DocumentView.tsx            |   6 +-
 src/client/views/nodes/LinkDocPreview.tsx          |   9 +-
 src/client/views/nodes/VideoBox.tsx                |   5 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  18 ++--
 src/client/views/nodes/trails/PresBox.tsx          |   3 +-
 src/client/views/search/SearchBox.tsx              |   3 +-
 src/fields/util.ts                                 |  23 ++---
 24 files changed, 121 insertions(+), 163 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 4dc1c3b71..251e432ef 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { action, runInAction } from 'mobx';
 import { basename } from 'path';
 import { DateField } from '../../fields/DateField';
-import { Doc, DocListCast, DocListCastAsync, Field, Initializing, Opt, updateCachedAcls } from '../../fields/Doc';
+import { Doc, DocListCast, Field, Initializing, Opt, updateCachedAcls } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { HtmlField } from '../../fields/HtmlField';
 import { InkField, PointData } from '../../fields/InkField';
@@ -403,7 +403,6 @@ export namespace Docs {
                         nativeDimModifiable: true,
                         nativeHeightUnfrozen: true,
                         forceReflow: true,
-                        links: '@links(self)',
                     },
                 },
             ],
@@ -411,35 +410,35 @@ export namespace Docs {
                 DocumentType.SEARCH,
                 {
                     layout: { view: SearchBox, dataField: defaultDataKey },
-                    options: { _width: 400, links: '@links(self)' },
+                    options: { _width: 400 },
                 },
             ],
             [
                 DocumentType.COLOR,
                 {
                     layout: { view: ColorBox, dataField: defaultDataKey },
-                    options: { _nativeWidth: 220, _nativeHeight: 300, links: '@links(self)' },
+                    options: { _nativeWidth: 220, _nativeHeight: 300 },
                 },
             ],
             [
                 DocumentType.IMG,
                 {
                     layout: { view: ImageBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.WEB,
                 {
                     layout: { view: WebBox, dataField: defaultDataKey },
-                    options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: '@links(self)' },
+                    options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
                 },
             ],
             [
                 DocumentType.COL,
                 {
                     layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1, links: '@links(self)' },
+                    options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1 },
                 },
             ],
             [
@@ -453,35 +452,35 @@ export namespace Docs {
                 DocumentType.VID,
                 {
                     layout: { view: VideoBox, dataField: defaultDataKey },
-                    options: { _currentTimecode: 0, links: '@links(self)' },
+                    options: { _currentTimecode: 0 },
                 },
             ],
             [
                 DocumentType.AUDIO,
                 {
                     layout: { view: AudioBox, dataField: defaultDataKey },
-                    options: { _height: 100, backgroundColor: 'lightGray', _fitWidth: true, forceReflow: true, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _height: 100, forceReflow: true, nativeDimModifiable: true },
                 },
             ],
             [
                 DocumentType.REC,
                 {
                     layout: { view: VideoBox, dataField: defaultDataKey },
-                    options: { _height: 100, backgroundColor: 'pink', links: '@links(self)' },
+                    options: { _height: 100, backgroundColor: 'pink' },
                 },
             ],
             [
                 DocumentType.PDF,
                 {
                     layout: { view: PDFBox, dataField: defaultDataKey },
-                    options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, links: '@links(self)' },
+                    options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true },
                 },
             ],
             [
                 DocumentType.MAP,
                 {
                     layout: { view: MapBox, dataField: defaultDataKey },
-                    options: { _height: 600, _width: 800, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _height: 600, _width: 800, nativeDimModifiable: true },
                 },
             ],
             [
@@ -502,7 +501,6 @@ export namespace Docs {
                         description: '',
                         showCaption: 'description',
                         backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area
-                        links: '@links(self)',
                         _removeDropProperties: new List(['isLinkButton']),
                     },
                 },
@@ -527,7 +525,7 @@ export namespace Docs {
                 DocumentType.SCRIPTING,
                 {
                     layout: { view: ScriptingBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
@@ -540,56 +538,56 @@ export namespace Docs {
                 DocumentType.LABEL,
                 {
                     layout: { view: LabelBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)', _singleLine: true },
+                    options: { _singleLine: true },
                 },
             ],
             [
                 DocumentType.EQUATION,
                 {
                     layout: { view: EquationBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)', nativeDimModifiable: true, fontSize: '14px', hideResizeHandles: true, hideDecorationTitle: true },
+                    options: { nativeDimModifiable: true, fontSize: '14px', hideResizeHandles: true, hideDecorationTitle: true },
                 },
             ],
             [
                 DocumentType.FUNCPLOT,
                 {
                     layout: { view: FunctionPlotBox, dataField: defaultDataKey },
-                    options: { nativeDimModifiable: true, links: '@links(self)' },
+                    options: { nativeDimModifiable: true },
                 },
             ],
             [
                 DocumentType.BUTTON,
                 {
                     layout: { view: LabelBox, dataField: 'onClick' },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.SLIDER,
                 {
                     layout: { view: SliderBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.PRES,
                 {
                     layout: { view: PresBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: defaultDataKey },
-                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
+                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%' },
                 },
             ],
             [
                 DocumentType.WEBCAM,
                 {
                     layout: { view: RecordingBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
@@ -603,7 +601,7 @@ export namespace Docs {
                 DocumentType.MARKER,
                 {
                     layout: { view: CollectionView, dataField: defaultDataKey },
-                    options: { links: '@links(self)', hideLinkButton: true, pointerEvents: 'none' },
+                    options: { hideLinkButton: true, pointerEvents: 'none' },
                 },
             ],
             [
@@ -611,21 +609,21 @@ export namespace Docs {
                 {
                     // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method
                     layout: { view: InkingStroke, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.SCREENSHOT,
                 {
                     layout: { view: ScreenshotBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.COMPARISON,
                 {
                     layout: { view: ComparisonBox, dataField: defaultDataKey },
-                    options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'alias', links: '@links(self)' },
+                    options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'alias' },
                 },
             ],
             [
@@ -640,21 +638,21 @@ export namespace Docs {
                 DocumentType.GROUP,
                 {
                     layout: { view: EmptyBox, dataField: defaultDataKey },
-                    options: { links: '@links(self)' },
+                    options: {},
                 },
             ],
             [
                 DocumentType.DATAVIZ,
                 {
                     layout: { view: DataVizBox, dataField: defaultDataKey },
-                    options: { _fitWidth: true, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _fitWidth: true, nativeDimModifiable: true },
                 },
             ],
             [
                 DocumentType.LOADING,
                 {
                     layout: { view: LoadingBox, dataField: '' },
-                    options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true, links: '@links(self)' },
+                    options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true },
                 },
             ],
         ]);
@@ -881,15 +879,7 @@ export namespace Docs {
         }
 
         export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
-            return InstanceFromProto(
-                Prototypes.get(DocumentType.AUDIO),
-                new AudioField(url),
-                { ...options, backgroundColor: ComputedField.MakeFunction("this._mediaState === 'playing' ? 'green':'gray'") as any },
-                undefined,
-                undefined,
-                undefined,
-                overwriteDoc
-            );
+            return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc);
         }
 
         export function RecordingDocument(url: string, options: DocumentOptions = {}) {
@@ -992,7 +982,6 @@ export namespace Docs {
             I.creationDate = new DateField();
             I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment;
             //I['acl-Override'] = SharingPermissions.Unset;
-            I.links = ComputedField.MakeFunction('links(self)');
             I[Initializing] = false;
             return I;
         }
@@ -1282,38 +1271,6 @@ export namespace DocUtils {
         return rangeFilteredDocs;
     }
 
-    export function Publish(promoteDoc: Doc, targetID: string, addDoc: any, remDoc: any) {
-        targetID = targetID.replace(/^-/, '').replace(/\([0-9]*\)$/, '');
-        DocServer.GetRefField(targetID).then(doc => {
-            if (promoteDoc !== doc) {
-                let copy = doc as Doc;
-                if (copy) {
-                    Doc.Overwrite(promoteDoc, copy, true);
-                } else {
-                    copy = Doc.MakeCopy(promoteDoc, true, targetID);
-                }
-                !doc && (copy.title = undefined) && (Doc.GetProto(copy).title = targetID);
-                addDoc && addDoc(copy);
-                remDoc && remDoc(promoteDoc);
-                if (!doc) {
-                    DocListCastAsync(promoteDoc.links).then(links => {
-                        links &&
-                            links.map(async link => {
-                                if (link) {
-                                    const a1 = await Cast(link.anchor1, Doc);
-                                    if (a1 && Doc.AreProtosEqual(a1, promoteDoc)) link.anchor1 = copy;
-                                    const a2 = await Cast(link.anchor2, Doc);
-                                    if (a2 && Doc.AreProtosEqual(a2, promoteDoc)) link.anchor2 = copy;
-                                    LinkManager.Instance.deleteLink(link);
-                                    LinkManager.Instance.addLink(link);
-                                }
-                            });
-                    });
-                }
-            }
-        });
-    }
-
     export function DefaultFocus(doc: Doc, options: DocFocusOptions) {
         return undefined;
     }
@@ -1711,7 +1668,7 @@ export namespace DocUtils {
     export function LeavePushpin(doc: Doc, annotationField: string) {
         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));
+        const hasContextAnchor = LinkManager.Links(doc).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)) {
             const pushpin = Docs.Create.FontIconDocument({
                 title: 'pushpin',
diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx
index 735b06f6d..c9fcc84a3 100644
--- a/src/client/util/CaptureManager.tsx
+++ b/src/client/util/CaptureManager.tsx
@@ -2,12 +2,13 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
-import { Doc, DocListCast } from '../../fields/Doc';
-import { StrCast } from '../../fields/Types';
+import { Doc } from '../../fields/Doc';
+import { DocCast, StrCast } from '../../fields/Types';
 import { addStyleSheet } from '../../Utils';
 import { LightboxView } from '../views/LightboxView';
 import { MainViewModal } from '../views/MainViewModal';
 import './CaptureManager.scss';
+import { LinkManager } from './LinkManager';
 import { SelectionManager } from './SelectionManager';
 
 @observer
@@ -48,16 +49,14 @@ export class CaptureManager extends React.Component<{}> {
         const doc = this._document;
         const order: JSX.Element[] = [];
         if (doc) {
-            DocListCast(doc.links).forEach((l, i) => {
-                if (l) {
-                    order.push(
-                        <div className="list-item">
-                            <div className="number">{i}</div>
-                            {StrCast((l.anchor1 as Doc).title)}
-                        </div>
-                    );
-                }
-            });
+            LinkManager.Links(doc).forEach((l, i) =>
+                order.push(
+                    <div className="list-item">
+                        <div className="number">{i}</div>
+                        {StrCast(DocCast(l.anchor1)?.title)}
+                    </div>
+                )
+            );
         }
 
         return (
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2b0a2cdac..c4fb4788c 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, dropActionType } from "./DragManager";
+import { DragManager } from "./DragManager";
 import { MakeTemplate } from "./DropConverter";
 import { LinkManager } from "./LinkManager";
 import { ScriptingGlobals } from "./ScriptingGlobals";
@@ -949,6 +949,5 @@ ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Do
 ScriptingGlobals.add(function createNewPresentation() { return MainView.Instance.createNewPresentation(); }, "creates a new presentation when called");
 ScriptingGlobals.add(function openPresentation(pres:Doc) { return MainView.Instance.openPresentation(pres); }, "creates a new presentation when called");
 ScriptingGlobals.add(function createNewFolder() { return MainView.Instance.createNewFolder(); }, "creates a new folder in myFiles when called");
-ScriptingGlobals.add(function links(doc: any) { return new List(LinkManager.Instance.getAllRelatedLinks(doc)); }, "returns all the links to the document or its annotations", "(doc: any)");
 ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar");
 ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; });
\ No newline at end of file
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index e28687561..eacbcc0e3 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -6,6 +6,7 @@ import { DocumentDecorations } from '../views/DocumentDecorations';
 import { DocFocusOptions, DocumentViewSharedProps, OpenWhere } 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';
 
@@ -41,7 +42,7 @@ export class LinkFollower {
     };
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
-        const linkDocs = link ? [link] : DocListCast(sourceDoc.links);
+        const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
         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).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0);
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 46d44fea4..555215417 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -4,6 +4,7 @@ import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '
 import { List } from '../../fields/List';
 import { ProxyField } from '../../fields/Proxy';
 import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types';
+import { ScriptingGlobals } from './ScriptingGlobals';
 /*
  * link doc:
  * - anchor1: doc
@@ -24,6 +25,10 @@ export class LinkManager {
     public static get Instance() {
         return LinkManager._instance;
     }
+
+    public static Links(doc: Doc | undefined) {
+        return doc ? LinkManager.Instance.getAllRelatedLinks(doc) : [];
+    }
     public static addLinkDB = async (linkDb: any) => {
         await Promise.all(
             ((await DocListCastAsync(linkDb.data)) ?? []).map(link =>
@@ -34,7 +39,7 @@ export class LinkManager {
         LinkManager.userLinkDBs.push(linkDb);
     };
     public static AutoKeywords = 'keywords:Usages';
-    static links: Doc[] = [];
+    static _links: Doc[] = [];
     constructor() {
         LinkManager._instance = this;
         this.createLinkrelationshipLists();
@@ -72,7 +77,7 @@ export class LinkManager {
             );
         };
         const watchUserLinkDB = (userLinkDBDoc: Doc) => {
-            LinkManager.links.push(...DocListCast(userLinkDBDoc.data));
+            LinkManager._links.push(...DocListCast(userLinkDBDoc.data));
             const toRealField = (field: Field) => (field instanceof ProxyField ? field.value : field); // see List.ts.  data structure is not a simple list of Docs, but a list of ProxyField/Fields
             if (userLinkDBDoc.data) {
                 observe(
@@ -193,3 +198,11 @@ export class LinkManager {
         if (Doc.AreProtosEqual(anchor, linkDoc)) return linkDoc;
     }
 }
+
+ScriptingGlobals.add(
+    function links(doc: any) {
+        return new List(LinkManager.Links(doc));
+    },
+    'returns all the links to the document or its annotations',
+    '(doc: any)'
+);
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 646942569..c0fc25376 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -1,6 +1,6 @@
 import { action, observable, ObservableMap } from 'mobx';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
+import { Doc, Opt } from '../../fields/Doc';
 import { DocCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocumentView } from '../views/nodes/DocumentView';
@@ -22,7 +22,7 @@ export namespace SelectionManager {
             // if doc is not in SelectedDocuments, add it
             if (!manager.SelectedViews.get(docView) && docView.props.Document.type !== DocumentType.MARKER) {
                 if (!ctrlPressed) {
-                    if (LinkManager.currentLink && !DocListCast(docView.rootDoc.links).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
+                    if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) {
                         LinkManager.currentLink = undefined;
                     }
                     this.DeselectAll();
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index 2567d44bb..976c8763e 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -4,9 +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 { List } from '../../fields/List';
-import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types';
-import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../Utils';
+import { Cast, NumCast, StrCast } from '../../fields/Types';
+import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnTrue } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
 import { DocumentManager } from '../util/DocumentManager';
 import { LinkManager } from '../util/LinkManager';
@@ -88,7 +87,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
                 ...future
                     .slice()
                     .sort((a, b) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow))
-                    .sort((a, b) => DocListCast(a.links).length - DocListCast(b.links).length),
+                    .sort((a, b) => LinkManager.Links(a).length - LinkManager.Links(b).length),
             ];
         }
         this._doc = doc;
@@ -214,7 +213,7 @@ export class LightboxView extends React.Component<LightboxViewProps> {
         if (coll) {
             const fieldKey = Doc.LayoutFieldKey(coll);
             const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])];
-            const links = DocListCast(coll.links)
+            const links = LinkManager.Links(coll)
                 .map(link => LinkManager.getOppositeAnchor(link, coll))
                 .filter(doc => doc)
                 .map(doc => doc!);
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ac1b66013..ebbe20077 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -9,8 +9,10 @@ import { RichTextField } from '../../fields/RichTextField';
 import { ScriptField } from '../../fields/ScriptField';
 import { BoolCast, ScriptCast, StrCast } from '../../fields/Types';
 import { ImageField } from '../../fields/URLField';
+import { Utils } from '../../Utils';
 import { DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
 import { SelectionManager } from '../util/SelectionManager';
 import { undoBatch } from '../util/UndoManager';
 import { Colors } from './global/globalEnums';
@@ -19,8 +21,6 @@ import { DocumentView, OpenWhere } from './nodes/DocumentView';
 import { pasteImageBitmap } from './nodes/WebBoxRenderer';
 import './PropertiesButtons.scss';
 import React = require('react');
-import { LinkManager } from '../util/LinkManager';
-import { Utils } from '../../Utils';
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
 export const Flyout = higflyout.default;
@@ -139,9 +139,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
                     const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]);
                     dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' });
-                    containerContents.forEach(doc => {
-                        DocListCast(doc.links).forEach(link => (link.linkDisplay = false));
-                    });
+                    containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.linkDisplay = false)));
                 });
             }
         );
@@ -330,7 +328,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
             const click = () => this.handleOptionChange(value[0]);
             const linkButton = BoolCast(this.selectedDoc._isLinkButton);
             const followLoc = this.selectedDoc._followLinkLocation;
-            const linkedToLightboxView = () => DocListCast(this.selectedDoc.links).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
+            const linkedToLightboxView = () => LinkManager.Links(this.selectedDoc).some(link => LinkManager.getOppositeAnchor(link, this.selectedDoc)?._isLightbox);
 
             let active = false;
             // prettier-ignore
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index 7a985628f..03b4100a7 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -4,10 +4,10 @@ import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-soli
 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, computed, Lambda, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { ColorState, SketchPicker } from 'react-color';
-import { AclAdmin, AclSym, HierarchyMapping, DataSym, Doc, DocListCast, Field, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
+import { AclAdmin, AclSym, DataSym, Doc, Field, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { InkField } from '../../fields/InkField';
 import { List } from '../../fields/List';
@@ -23,10 +23,10 @@ import { SharingManager } from '../util/SharingManager';
 import { Transform } from '../util/Transform';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { EditableView } from './EditableView';
+import { FilterPanel } from './FilterPanel';
 import { Colors } from './global/globalEnums';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { DocumentView, OpenWhere, StyleProviderFunc } from './nodes/DocumentView';
-import { FilterPanel } from './FilterPanel';
 import { KeyValueBox } from './nodes/KeyValueBox';
 import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
 import { PropertiesButtons } from './PropertiesButtons';
@@ -1486,7 +1486,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         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!));
+        const hasSelectedAnchor = LinkManager.Links(this.sourceAnchor).includes(LinkManager.currentLink!);
         if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this.props.width }}>
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 74ea624a6..7519cbb05 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,10 +3,12 @@ import { observer } from 'mobx-react';
 import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
+import { RichTextField } from '../../fields/RichTextField';
 import { DocCast, NumCast, StrCast } from '../../fields/Types';
 import { emptyFunction, OmitKeys, returnAll, returnOne, returnTrue, returnZero } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
+import { LinkManager } from '../util/LinkManager';
 import { Transform } from '../util/Transform';
 import { CollectionStackingView } from './collections/CollectionStackingView';
 import { FieldViewProps } from './nodes/FieldView';
@@ -15,7 +17,6 @@ import { SearchBox } from './search/SearchBox';
 import './SidebarAnnos.scss';
 import { StyleProp } from './StyleProvider';
 import React = require('react');
-import { RichTextField } from '../../fields/RichTextField';
 
 interface ExtraProps {
     fieldKey: string;
@@ -174,8 +175,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     showTitle = () => 'title';
     setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight());
     sortByLinkAnchorY = (a: Doc, b: Doc) => {
-        const ay = DocListCast(a.links).length && DocCast(DocListCast(a.links)[0].anchor1).y;
-        const by = DocListCast(b.links).length && DocCast(DocListCast(b.links)[0].anchor1).y;
+        const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].anchor1).y;
+        const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].anchor1).y;
         return NumCast(ay) - NumCast(by);
     };
     render() {
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 153c30052..ce764c7bf 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -1,12 +1,15 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Shadows } from 'browndash-components';
 import { action, runInAction } from 'mobx';
 import { extname } from 'path';
-import { Doc, DocListCast, Opt } from '../../fields/Doc';
-import { BoolCast, Cast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types';
+import { Doc, Opt } from '../../fields/Doc';
+import { BoolCast, Cast, ImageCast, NumCast, StrCast } from '../../fields/Types';
 import { DashColor, emptyFunction, lightOrDark } from '../../Utils';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocFocusOrOpen } from '../util/DocumentManager';
+import { LinkManager } from '../util/LinkManager';
+import { SelectionManager } from '../util/SelectionManager';
 import { ColorScheme } from '../util/SettingsManager';
 import { undoBatch, UndoManager } from '../util/UndoManager';
 import { TreeSort } from './collections/TreeView';
@@ -18,8 +21,6 @@ import { FieldViewProps } from './nodes/FieldView';
 import { SliderBox } from './nodes/SliderBox';
 import './StyleProvider.scss';
 import React = require('react');
-import { Shadows } from 'browndash-components';
-import { SelectionManager } from '../util/SelectionManager';
 
 export enum StyleProp {
     TreeViewIcon = 'treeViewIcon',
@@ -261,7 +262,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.BoxShadow: {
             if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow)
             if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW;
-            if (doc?.isLinkButton && DocListCast(doc?.links).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
+            if (doc?.isLinkButton && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em');
             switch (doc?.type) {
                 case DocumentType.COL:
                     return StrCast(
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 572fbfec2..5bdff347c 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -2,7 +2,7 @@ import React = require('react');
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
-import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
+import { Doc, Opt, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
@@ -15,6 +15,7 @@ import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
+import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -740,14 +741,14 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
             () => this.props.currentTimecode(),
             time => {
                 const dictationDoc = Cast(this.props.layoutDoc['data-dictation'], Doc, null);
-                const isDictation = dictationDoc && DocListCast(this.props.mark.links).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
+                const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc);
                 if (
                     !LightboxView.LightboxDoc &&
                     // bcz: when should links be followed?  we don't want to move away from the video to follow a link but we can open it in a sidebar/etc.  But we don't know that upfront.
                     // for now, we won't follow any links when the lightbox is oepn to avoid "losing" the video.
                     /*(isDictation || !Doc.AreProtosEqual(LightboxView.LightboxDoc, this.props.layoutDoc))*/
                     !this.props.layoutDoc.dontAutoFollowLinks &&
-                    DocListCast(this.props.mark.links).length &&
+                    LinkManager.Links(this.props.mark).length &&
                     time > NumCast(this.props.mark[this.props.startTag]) &&
                     time < NumCast(this.props.mark[this.props.endTag]) &&
                     this._lastTimecode < NumCast(this.props.mark[this.props.startTag]) - 1e-5
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index c88ae314e..e46220f02 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -1,5 +1,4 @@
 import { action, computed, observable } from 'mobx';
-import ReactLoading from 'react-loading';
 import * as rp from 'request-promise';
 import CursorField from '../../../fields/CursorField';
 import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc';
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 08ffa1466..2bdcf472f 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -15,6 +15,7 @@ import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -708,7 +709,7 @@ export class TreeView extends React.Component<TreeViewProps> {
 
     @computed get validExpandViewTypes() {
         const annos = () => (DocListCast(this.doc[this.fieldKey + '-annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : '');
-        const links = () => (DocListCast(this.doc.links).length && !this.props.treeView.dashboardMode ? 'links' : '');
+        const links = () => (LinkManager.Links(this.doc).length && !this.props.treeView.dashboardMode ? 'links' : '');
         const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : '');
         const aliases = () => (this.props.treeView.dashboardMode ? '' : 'aliases');
         const fields = () => (Doc.noviceMode ? '' : 'fields');
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index c9112eec3..3f6369898 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,7 +1,6 @@
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../../fields/Doc';
-import { DocCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index cdac91a62..4741fc6f2 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, DocListCast } from '../../../fields/Doc';
+import { Doc } from '../../../fields/Doc';
 import { Cast, DocCast, StrCast } from '../../../fields/Types';
 import { WebField } from '../../../fields/URLField';
 import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
@@ -15,11 +15,11 @@ import { LinkManager } from '../../util/LinkManager';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { undoBatch } from '../../util/UndoManager';
+import { MainView } from '../MainView';
 import { DocumentView, OpenWhere } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
 import React = require('react');
-import { MainView } from '../MainView';
 
 interface LinkMenuItemProps {
     groupType: string;
@@ -34,7 +34,7 @@ interface LinkMenuItemProps {
 
 // drag links and drop link targets (aliasing them if needed)
 export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) {
-    const draggedDocs = (specificLinks ? specificLinks : DocListCast(sourceDoc.links)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
+    const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[];
 
     if (draggedDocs.length) {
         const moddrag: Doc[] = [];
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 4c26468b9..890ecc1b2 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -11,6 +11,7 @@ import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } f
 import { DocUtils } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
+import { LinkManager } from '../../util/LinkManager';
 import { undoBatch } from '../../util/UndoManager';
 import { CollectionStackedTimeline, TrimScope } from '../collections/CollectionStackedTimeline';
 import { ContextMenu } from '../ContextMenu';
@@ -84,7 +85,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return this.props.PanelHeight() < 50;
     } // used to collapse timeline when node is shrunk
     @computed get links() {
-        return DocListCast(this.dataDoc.links);
+        return LinkManager.Links(this.dataDoc);
     }
     @computed get mediaState() {
         return this.dataDoc.mediaState as media_state;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2b6629913..964231e9d 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -929,7 +929,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     },
                     icon: 'eye',
                 });
-            DocListCast(this.Document.links).length &&
+            LinkManager.Links(this.Document).length &&
                 appearanceItems.splice(0, 0, { description: `${this.layoutDoc.hideLinkButton ? 'Show' : 'Hide'} Link Button`, event: action(() => (this.layoutDoc.hideLinkButton = !this.layoutDoc.hideLinkButton)), icon: 'eye' });
             !appearance && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
@@ -969,7 +969,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     onClicks.push({ description: (this.Document.followLinkToggle ? 'Remove' : 'Make') + ' Target Visibility Toggle', event: () => this.toggleFollowLink(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) {
+                } else if (LinkManager.Links(this.Document).length) {
                     onClicks.push({ description: 'Select on Click', event: () => this.selectOnClick(), icon: 'link' });
                     onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(undefined, false), icon: 'link' });
                     onClicks.push({ description: 'Toggle Link Target on Click', event: () => this.toggleTargetOnClick(), icon: 'map-pin' });
@@ -1907,7 +1907,7 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) {
     const linkSource = Cast(linkCollection.linkSource, Doc, null);
     const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data);
     let wid = linkSource[WidthSym]();
-    const links = DocListCast(linkSource.links);
+    const links = LinkManager.Links(linkSource);
     links.forEach(link => {
         const other = LinkManager.getOppositeAnchor(link, linkSource);
         const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other;
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index bbe9b4323..16b352e4f 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -3,7 +3,7 @@ import { Tooltip } from '@material-ui/core';
 import { action, computed, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import wiki from 'wikijs';
-import { Doc, DocCastAsync, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types';
 import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils';
 import { DocServer } from '../../DocServer';
@@ -14,11 +14,10 @@ import { LinkFollower } from '../../util/LinkFollower';
 import { LinkManager } from '../../util/LinkManager';
 import { SettingsManager } from '../../util/SettingsManager';
 import { Transform } from '../../util/Transform';
+import { SearchBox } from '../search/SearchBox';
 import { DocumentView, DocumentViewSharedProps, OpenWhere } from './DocumentView';
 import './LinkDocPreview.scss';
 import React = require('react');
-import { SearchUtil } from '../../util/SearchUtil';
-import { SearchBox } from '../search/SearchBox';
 
 interface LinkDocPreviewProps {
     linkDoc?: Doc;
@@ -110,8 +109,8 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                 const anchorDoc = anchorDocId ? PromiseValue(DocCast(DocServer.GetCachedRefField(anchorDocId) ?? DocServer.GetRefField(anchorDocId))) : undefined;
                 anchorDoc?.then?.(
                     action(anchor => {
-                        if (anchor instanceof Doc && DocListCast(anchor.links).length) {
-                            this._linkDoc = this._linkDoc ?? DocListCast(anchor.links)[0];
+                        if (anchor instanceof Doc && LinkManager.Links(anchor).length) {
+                            this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0];
                             const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords;
                             if (automaticLink) {
                                 // automatic links specify the target in the link info, not the source
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 7df3d5f24..dc6bf6f9e 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx';
 import { observer } from 'mobx-react';
 import { basename } from 'path';
-import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc';
+import { Doc, HeightSym, WidthSym } from '../../../fields/Doc';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
@@ -14,6 +14,7 @@ import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
 import { DocumentManager } from '../../util/DocumentManager';
+import { LinkManager } from '../../util/LinkManager';
 import { ReplayMovements } from '../../util/ReplayMovements';
 import { SelectionManager } from '../../util/SelectionManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -85,7 +86,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @observable _scrubbing: boolean = false;
 
     @computed get links() {
-        return DocListCast(this.dataDoc.links);
+        return LinkManager.Links(this.dataDoc);
     }
     @computed get heightPercent() {
         return NumCast(this.layoutDoc._timelineHeightPercent, 100);
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d9ce9a0b0..a76f9623a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1,7 +1,7 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { isEqual } from 'lodash';
-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 { baseKeymap, selectAll } from 'prosemirror-commands';
 import { history } from 'prosemirror-history';
@@ -11,7 +11,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model';
 import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state';
 import { EditorView } from 'prosemirror-view';
 import { DateField } from '../../../../fields/DateField';
-import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
+import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { PrefetchProxy } from '../../../../fields/Proxy';
@@ -48,6 +48,7 @@ import { StyleProp } from '../../StyleProvider';
 import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
 import { FieldView, FieldViewProps } from '../FieldView';
 import { LinkDocPreview } from '../LinkDocPreview';
+import { PinProps, PresBox } from '../trails';
 import { DashDocCommentView } from './DashDocCommentView';
 import { DashDocView } from './DashDocView';
 import { DashFieldView } from './DashFieldView';
@@ -63,7 +64,6 @@ import { schema } from './schema_rts';
 import { SummaryView } from './SummaryView';
 import applyDevTools = require('prosemirror-dev-tools');
 import React = require('react');
-import { PinProps, PresBox } from '../trails';
 const translateGoogleApi = require('translate-google-api');
 
 export const GoogleRef = 'googleDocId';
@@ -363,7 +363,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         let linkTime;
         let linkAnchor;
         let link;
-        DocListCast(this.dataDoc.links).forEach((l, i) => {
+        LinkManager.Links(this.dataDoc).forEach((l, i) => {
             const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined;
             if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') {
                 linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
@@ -400,7 +400,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
     autoLink = () => {
         const newAutoLinks = new Set<Doc>();
-        const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
+        const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.linkRelationship === LinkManager.AutoKeywords);
         if (this._editorView?.state.doc.textContent) {
             const isNodeSel = this._editorView.state.selection instanceof NodeSelection;
             const f = this._editorView.state.selection.from;
@@ -453,7 +453,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                             alink =
                                 alink ??
-                                (DocListCast(this.Document.links).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
+                                (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) ||
                                     DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, LinkManager.AutoKeywords)!);
                             newAutoLinks.add(alink);
                             const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
@@ -687,7 +687,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
 
     @undoBatch
     deleteAnnotation = (anchor: Doc) => {
-        LinkManager.Instance.deleteLink(DocListCast(anchor.links)[0]);
+        LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
         // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]);
         // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
         // AnchorMenu.Instance.fadeOut(true);
@@ -1014,7 +1014,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     }
     componentDidMount() {
         !this.props.dontSelectOnLoad && 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.
-        this._cachedLinks = DocListCast(this.Document.links);
+        this._cachedLinks = LinkManager.Links(this.Document);
         this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation);
         this._disposers.autoHeight = reaction(
             () => this.autoHeight,
@@ -1041,7 +1041,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             { fireImmediately: true }
         );
         this._disposers.links = reaction(
-            () => DocListCast(this.dataDoc.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
+            () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
             newLinks => {
                 this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l));
                 this._cachedLinks = newLinks;
diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx
index dda96d8e7..be40b3592 100644
--- a/src/client/views/nodes/trails/PresBox.tsx
+++ b/src/client/views/nodes/trails/PresBox.tsx
@@ -35,7 +35,6 @@ import { FieldView, FieldViewProps } from '../FieldView';
 import { ScriptingBox } from '../ScriptingBox';
 import './PresBox.scss';
 import { PresEffect, PresEffectDirection, PresMovement, PresStatus } from './PresEnums';
-import { tag } from 'xregexp/types';
 const { Howl } = require('howler');
 
 export interface pinDataTypes {
@@ -1551,7 +1550,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() {
                                 const type = DocCast(tagDoc?.annotationOn)?.type ?? tagDoc.type;
                                 activeItem.presIndexedStart = type === DocumentType.COL ? 1 : 0;
                                 // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized.
-                                // to avoid creating a new slide to correspond to each of the target's data list, we simply reference the target's data list.
+                                // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list.
                                 let dataField = Doc.LayoutFieldKey(tagDoc);
                                 if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '-annotations';
 
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 3ba60edd7..fe9b13fbe 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -5,7 +5,6 @@ import * as React from 'react';
 import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 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';
@@ -470,7 +469,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
                                       }
                             }
                             style={{
-                                fontWeight: DocListCast(fromDoc?.links).find(
+                                fontWeight: LinkManager.Links(fromDoc).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'
diff --git a/src/fields/util.ts b/src/fields/util.ts
index e517e7604..6ff13d5d3 100644
--- a/src/fields/util.ts
+++ b/src/fields/util.ts
@@ -1,8 +1,8 @@
-import { forEach } from 'lodash';
 import { $mobx, action, observable, runInAction, trace } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { DocServer } from '../client/DocServer';
 import { CollectionViewType } from '../client/documents/DocumentTypes';
+import { LinkManager } from '../client/util/LinkManager';
 import { SerializationHelper } from '../client/util/SerializationHelper';
 import { UndoManager } from '../client/util/UndoManager';
 import { returnZero } from '../Utils';
@@ -18,7 +18,6 @@ import {
     Doc,
     DocListCast,
     DocListCastAsync,
-    FieldResult,
     ForceServerWrite,
     HeightSym,
     HierarchyMapping,
@@ -259,7 +258,7 @@ function getEffectiveAcl(target: any, user?: string): symbol {
  */
 export function distributeAcls(key: string, acl: SharingPermissions, target: Doc, inheritingFromCollection?: boolean, visited?: Doc[], isDashboard?: boolean) {
     if (!visited) visited = [] as Doc[];
-    if (visited.includes(target)) return;
+    if (!target || visited.includes(target)) return;
 
     if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) {
         target[key] = acl;
@@ -293,26 +292,18 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc
         }
 
         // maps over the links of the document
-        DocListCast(dataDoc.links).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
+        LinkManager.Links(dataDoc).forEach(link => distributeAcls(key, acl, link, inheritingFromCollection, visited));
 
         // maps over the children of the document
-        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + (isDashboard ? '-all' : '')]).map(d => {
+        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => {
             distributeAcls(key, acl, d, inheritingFromCollection, visited);
-            // }
-            const data = d[DataSym];
-            if (data) {
-                distributeAcls(key, acl, data, inheritingFromCollection, visited);
-            }
+            distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited);
         });
 
         // maps over the annotations of the document
-        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).map(d => {
+        DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).forEach(d => {
             distributeAcls(key, acl, d, inheritingFromCollection, visited);
-            // }
-            const data = d[DataSym];
-            if (data) {
-                distributeAcls(key, acl, data, inheritingFromCollection, visited);
-            }
+            distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited);
         });
     }
 
-- 
cgit v1.2.3-70-g09d2


From bdccbc7a37216ffc88920ec48f9119a8ada0be60 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 17 Mar 2023 10:54:30 -0400
Subject: cleaned up highlighting styles a bit.  fixed stackedTimeline screen
 to local xf.

---
 src/client/documents/Documents.ts                    |  3 ++-
 src/client/views/PropertiesButtons.tsx               |  2 +-
 src/client/views/StyleProvider.tsx                   | 15 +++++++--------
 .../views/collections/CollectionStackedTimeline.scss |  2 +-
 .../views/collections/CollectionStackedTimeline.tsx  |  3 ++-
 src/client/views/collections/TabDocView.tsx          |  2 +-
 src/client/views/nodes/AudioBox.tsx                  |  2 +-
 src/client/views/nodes/DocumentView.tsx              |  4 ++++
 src/client/views/nodes/LabelBox.tsx                  |  2 +-
 src/client/views/nodes/button/FontIconBox.scss       | 20 --------------------
 10 files changed, 20 insertions(+), 35 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 457811e26..ff6c8d440 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -245,6 +245,7 @@ export class DocumentOptions {
     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
+    disableDocBrushing?: boolean; // whether to suppress border highlighting
     hideDecorationTitle?: boolean;
     hideOpenButton?: boolean;
     hideResizeHandles?: boolean;
@@ -584,7 +585,7 @@ export namespace Docs {
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: defaultDataKey },
-                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%' },
+                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40 },
                 },
             ],
             [
diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx
index ebbe20077..11e9dd9c9 100644
--- a/src/client/views/PropertiesButtons.tsx
+++ b/src/client/views/PropertiesButtons.tsx
@@ -133,7 +133,7 @@ export class PropertiesButtons extends React.Component<{}, {}> {
                     const containerDoc = dv.rootDoc;
                     //containerDoc.followAllLinks =
                     // containerDoc.noShadow =
-                    // containerDoc.noHighlighting =
+                    // containerDoc.disableDocBrushing =
                     // containerDoc._forceActive =
                     containerDoc._fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox;
                     containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index ce764c7bf..dba5d703f 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -112,16 +112,15 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' };
             return allSorts;
         case StyleProp.Highlighting:
-            if (doc && !doc.noHighlighting) {
-                const highlightIndex = Doc.isBrushedHighlightedDegree(doc);
-                const highlightColor = ['transparent', 'rgb(68, 118, 247)', 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
+            if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) {
+                const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc);
+                const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0);
+                const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex];
                 const highlightStyle = ['solid', 'dashed', 'solid', 'solid', 'solid'][highlightIndex];
-                const excludeTypes = [DocumentType.FONTICON];
-                let highlighting = !props?.disableDocBrushing && highlightIndex && !excludeTypes.includes(doc.type as any) && doc._viewType !== CollectionViewType.Linear; // bcz: hack to turn off highlighting onsidebar panel documents.  need to flag a document as not highlightable in a more direct way
-                if (highlighting && props?.focus !== emptyFunction && StrCast(doc.title) !== '[pres element template]') {
+                if (highlightIndex) {
                     return {
                         highlightStyle,
-                        highlightColor: highlightIndex !== Doc.DocBrushStatus.highlighted && SelectionManager.Views().some(dv => dv.rootDoc === doc) ? 'black' : highlightColor,
+                        highlightColor,
                         highlightIndex,
                         highlightStroke: doc.type === DocumentType.INK,
                     };
@@ -218,7 +217,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                     docColor = docColor || 'transparent';
                     break;
                 case DocumentType.LABEL:
-                    docColor = docColor || (doc.annotationOn !== undefined ? 'rgba(128, 128, 128, 0.18)' : undefined) || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
+                    docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
                     break;
                 case DocumentType.BUTTON:
                     docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY);
diff --git a/src/client/views/collections/CollectionStackedTimeline.scss b/src/client/views/collections/CollectionStackedTimeline.scss
index 5a107d2ca..a55b70e22 100644
--- a/src/client/views/collections/CollectionStackedTimeline.scss
+++ b/src/client/views/collections/CollectionStackedTimeline.scss
@@ -97,7 +97,7 @@
         top: 2.5%;
         height: 95%;
         border-radius: 4px;
-        background: $light-gray;
+        //background: $light-gray;
         &:hover {
             opacity: 1;
         }
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 5bdff347c..f9249e7d5 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -399,8 +399,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                 title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any,
                 _minFontSize: 12,
                 _maxFontSize: 24,
-                _singleLine: false,
+                _singleLine: true,
                 _stayInCollection: true,
+                backgroundColor: 'rgba(128, 128, 128, 0.5)',
                 useLinkSmallAnchor: true,
                 hideLinkButton: true,
                 _isLinkButton: true,
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 9459aaf1e..0ab94e2e3 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -57,7 +57,7 @@ export class TabDocView extends React.Component<TabDocViewProps> {
     }
     @computed get tabBorderColor() {
         const highlight = DefaultStyleProvider(this._document, undefined, StyleProp.Highlighting);
-        if (highlight?.highlightIndex >= Doc.DocBrushStatus.highlighted) return highlight.highlightColor;
+        if (highlight?.highlightIndex === Doc.DocBrushStatus.highlighted) return highlight.highlightColor;
         return 'transparent';
     }
     @computed get tabColor() {
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 890ecc1b2..0b95d712c 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -445,7 +445,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @action
     timelineWhenChildContentsActiveChanged = (isActive: boolean) => this.props.whenChildContentsActiveChanged((this._isAnyChildContentActive = isActive));
 
-    timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.bottomControlsHeight);
+    timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight);
 
     setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time);
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b7a760c1e..6ae102a0c 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1489,6 +1489,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
+        if (this.rootDoc.title === 'Italic') {
+            console.log(highlighting, this.borderRounding, highlighting?.highlightStroke);
+        }
+
         const animRenderDoc = DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc);
         return (
             <div
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 33fab6873..6e0b4be37 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -79,7 +79,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp
 
     @observable _mouseOver = false;
     @computed get hoverColor() {
-        return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : 'unset';
+        return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor);
     }
 
     fitTextToBox = (r: any): any => {
diff --git a/src/client/views/nodes/button/FontIconBox.scss b/src/client/views/nodes/button/FontIconBox.scss
index a1ca777b3..7fe1436c7 100644
--- a/src/client/views/nodes/button/FontIconBox.scss
+++ b/src/client/views/nodes/button/FontIconBox.scss
@@ -43,10 +43,6 @@
         cursor: pointer;
         flex-direction: column;
 
-        &:hover {
-            background-color: rgba(0, 0, 0, 0.3) !important;
-        }
-
         svg {
             width: 50% !important;
             height: 50%;
@@ -68,10 +64,6 @@
         justify-content: center;
         align-items: center;
         justify-items: center;
-
-        &:hover {
-            filter: brightness(0.85) !important;
-        }
     }
 
     &.tglBtn,
@@ -220,10 +212,6 @@
             box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3);
             border-radius: 3px;
         }
-
-        &:hover {
-            background-color: rgba(0, 0, 0, 0.3) !important;
-        }
     }
 
     &.colorBtnLabel {
@@ -248,10 +236,6 @@
         align-content: center;
         align-items: center;
 
-        &:hover {
-            background-color: rgba(0, 0, 0, 0.3) !important;
-        }
-
         .menuButton-dropdownList {
             position: absolute;
             width: 150px;
@@ -283,10 +267,6 @@
         cursor: pointer;
         background: transparent;
 
-        &:hover {
-            background-color: rgba(0, 0, 0, 0.3) !important;
-        }
-
         &.slider {
             color: $white;
             cursor: pointer;
-- 
cgit v1.2.3-70-g09d2

