From eb5f75785fd28acb50f1b30434e89223fff00185 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 7 Oct 2022 00:33:04 -0400
Subject: improvements to sizing webpages

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

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

diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 90d9c3c43..12f41394d 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -73,7 +73,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         this.allMetadata.map(tag => (target[tag] = tag));
         DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
         this.addDocument(target);
-        this._stackRef.current?.focusDocument(target);
+        this._stackRef.current?.focusDocument(target, {});
         return target;
     };
     makeDocUnfiltered = (doc: Doc) => {
-- 
cgit v1.2.3-70-g09d2


From dc942e6228e003caa3754a72c0e126d64332a004 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 1 Nov 2022 18:29:11 -0400
Subject: fixes for goldenlayout to initialize more cleanly.  updated all old
 ReactDOM.render() to ReactDom.createRoot().  fixes for PDF/Web sidebar
 sizing.  added text from pdf selection anchors to sidebar notes.   fixed  PDF
 text selection to align properly.

---
 src/client/goldenLayout.js                         |  11 +-
 src/client/util/DragManager.ts                     |   2 +-
 src/client/util/HypothesisUtils.ts                 | 160 ++++++++++++---------
 src/client/views/DocumentDecorations.scss          |   5 +-
 src/client/views/DocumentDecorations.tsx           |   9 +-
 src/client/views/GlobalKeyHandler.ts               |   2 +-
 src/client/views/Main.tsx                          |   7 +-
 src/client/views/MainView.scss                     |   9 --
 src/client/views/MainView.tsx                      |  94 +-----------
 src/client/views/MarqueeAnnotator.tsx              |   6 +-
 src/client/views/SidebarAnnos.tsx                  |  22 +++
 src/client/views/StyleProvider.tsx                 |   6 +-
 .../views/collections/CollectionDockingView.scss   |   4 +
 .../views/collections/CollectionDockingView.tsx    |  17 ++-
 src/client/views/collections/TabDocView.tsx        |   6 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |   3 -
 src/client/views/nodes/PDFBox.tsx                  |  13 +-
 src/client/views/nodes/WebBox.tsx                  |  14 +-
 src/client/views/nodes/button/FontIconBox.tsx      |  12 +-
 .../nodes/formattedText/DashDocCommentView.tsx     |   8 +-
 .../views/nodes/formattedText/DashDocView.tsx      |  12 +-
 .../views/nodes/formattedText/DashFieldView.tsx    |  53 +++++--
 .../views/nodes/formattedText/EquationView.tsx     |  10 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |   1 +
 .../views/nodes/formattedText/RichTextRules.ts     |   7 +-
 .../views/nodes/formattedText/SummaryView.tsx      |   9 +-
 src/client/views/nodes/formattedText/nodes_rts.ts  |   1 +
 src/client/views/pdf/PDFViewer.scss                |  12 +-
 src/client/views/pdf/PDFViewer.tsx                 |   6 +-
 src/debug/Repl.tsx                                 |  37 +++--
 30 files changed, 278 insertions(+), 280 deletions(-)

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

diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js
index dd11e6466..bc08b4d0b 100644
--- a/src/client/goldenLayout.js
+++ b/src/client/goldenLayout.js
@@ -459,7 +459,7 @@
                     this._bDragging = false;
                     this.emit('dragStop', oEvent, this._nOriginalX + this._nX);
                 } else  { // make title receive pointer events to allow setting insertion position or selecting texst range
-                    const classname = typeof oEvent.target?.className === "string" ? oEvent.target.className : "";
+                    const classname = oEvent.target ? (typeof oEvent.target.className === "string" ? oEvent.target.className : ""): "";
                     if (classname.includes("lm_title_wrap")) {
                         oEvent.target.children[0].style.pointerEvents = "all";
                         oEvent.target.children[0].focus();
@@ -5391,10 +5391,11 @@
          * @returns {void}
          */
         _render: function () {
-            this._reactComponent = ReactDOM.render(this._getReactComponent(), this._container.getElement()[0]);
-            this._originalComponentWillUpdate = this._reactComponent.componentWillUpdate || function () {
+            this._reactComponent = ReactDOM.createRoot(this._container.getElement()[0])
+            this._reactComponent.render(this._getReactComponent());
+            this._originalComponentWillUpdate = this._reactComponent.componentDidUpdate || function () {
             };
-            this._reactComponent.componentWillUpdate = this._onUpdate.bind(this);
+            this._reactComponent.componentDidUpdate = this._onUpdate.bind(this);
             if (this._container.getState()) {
                 this._reactComponent.setState(this._container.getState());
             }
@@ -5407,7 +5408,7 @@
          * @returns {void}
          */
         _destroy: function () {
-            ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
+          //  ReactDOM.unmountComponentAtNode(this._container.getElement()[0]);
             this._container.off('open', this._render, this);
             this._container.off('destroy', this._destroy, this);
         },
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index 36e5a65fb..d0690fa10 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -450,7 +450,7 @@ export namespace DragManager {
         const hideDragShowOriginalElements = (hide: boolean) => {
             dragLabel.style.display = hide ? '' : 'none';
             !hide && dragElements.map(dragElement => dragElement.parentNode === dragDiv && dragDiv.removeChild(dragElement));
-            eles.forEach(ele => (ele.hidden = hide));
+            setTimeout(() => eles.forEach(ele => (ele.hidden = hide)));
         };
         options?.hideSource && hideDragShowOriginalElements(true);
 
diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts
index e910a9118..297520d5d 100644
--- a/src/client/util/HypothesisUtils.ts
+++ b/src/client/util/HypothesisUtils.ts
@@ -1,30 +1,28 @@
-import { StrCast, Cast } from "../../fields/Types";
-import { SearchUtil } from "./SearchUtil";
-import { action, runInAction } from "mobx";
-import { Doc, Opt } from "../../fields/Doc";
-import { DocumentType } from "../documents/DocumentTypes";
-import { Docs } from "../documents/Documents";
-import { SelectionManager } from "./SelectionManager";
-import { WebField } from "../../fields/URLField";
-import { DocumentManager } from "./DocumentManager";
-import { DocumentLinksButton } from "../views/nodes/DocumentLinksButton";
-import { simulateMouseClick, Utils } from "../../Utils";
-import { DocumentView } from "../views/nodes/DocumentView";
-import { Id } from "../../fields/FieldSymbols";
+import { StrCast, Cast } from '../../fields/Types';
+import { SearchUtil } from './SearchUtil';
+import { action, runInAction } from 'mobx';
+import { Doc, Opt } from '../../fields/Doc';
+import { DocumentType } from '../documents/DocumentTypes';
+import { Docs } from '../documents/Documents';
+import { SelectionManager } from './SelectionManager';
+import { WebField } from '../../fields/URLField';
+import { DocumentManager } from './DocumentManager';
+import { DocumentLinksButton } from '../views/nodes/DocumentLinksButton';
+import { simulateMouseClick, Utils } from '../../Utils';
+import { DocumentView } from '../views/nodes/DocumentView';
+import { Id } from '../../fields/FieldSymbols';
 
 export namespace Hypothesis {
-
     /**
-     * Retrieve a WebDocument with the given url, prioritizing results that are on screen.  
+     * Retrieve a WebDocument with the given url, prioritizing results that are on screen.
      * If none exist, create and return a new WebDocument.
      */
     export const getSourceWebDoc = async (uri: string) => {
         const result = await findWebDoc(uri);
-        console.log(result ? "existing doc found" : "existing doc NOT found");
+        console.log(result ? 'existing doc found' : 'existing doc NOT found');
         return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found
     };
 
-
     /**
      * Search for a WebDocument whose url field matches the given uri, return undefined if not found
      */
@@ -33,18 +31,18 @@ export namespace Hypothesis {
         if (currentDoc && Cast(currentDoc.data, WebField)?.url.href === uri) return currentDoc; // always check first whether the currently selected doc is the annotation's source, only use Search otherwise
 
         const results: Doc[] = [];
-        await SearchUtil.Search("web", true).then(action(async (res: SearchUtil.DocSearchResult) => {
-            const docs = res.docs;
-            const filteredDocs = docs.filter(doc =>
-                doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data
-            );
-            filteredDocs.forEach(doc => {
-                uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history? 
-            });
-        }));
+        await SearchUtil.Search('web', true).then(
+            action(async (res: SearchUtil.DocSearchResult) => {
+                const docs = res.docs;
+                const filteredDocs = docs.filter(doc => doc.author === Doc.CurrentUserEmail && doc.type === DocumentType.WEB && doc.data);
+                filteredDocs.forEach(doc => {
+                    uri === Cast(doc.data, WebField)?.url.href && results.push(doc); // TODO check visited sites history?
+                });
+            })
+        );
 
         const onScreenResults = results.filter(doc => DocumentManager.Instance.getFirstDocumentView(doc));
-        return onScreenResults.length ? onScreenResults[0] : (results.length ? results[0] : undefined); // prioritize results that are currently on the screen
+        return onScreenResults.length ? onScreenResults[0] : results.length ? results[0] : undefined; // prioritize results that are currently on the screen
     };
 
     /**
@@ -52,17 +50,19 @@ export namespace Hypothesis {
      */
     export const linkListener = async (e: any) => {
         const annotationId: string = e.detail.id;
-        const annotationUri: string = StrCast(e.detail.uri).split("#annotations:")[0]; // clean hypothes.is URLs that reference a specific annotation 
+        const annotationUri: string = StrCast(e.detail.uri).split('#annotations:')[0]; // clean hypothes.is URLs that reference a specific annotation
         const sourceDoc: Doc = await getSourceWebDoc(annotationUri);
 
-        if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) { // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
+        if (!DocumentLinksButton.StartLink || sourceDoc === DocumentLinksButton.StartLink) {
+            // start new link if there were none already started, or if the old startLink came from the same web document (prevent links to itself)
             runInAction(() => {
                 DocumentLinksButton.AnnotationId = annotationId;
                 DocumentLinksButton.AnnotationUri = annotationUri;
                 DocumentLinksButton.StartLink = sourceDoc;
                 DocumentLinksButton.StartLinkView = undefined;
             });
-        } else { // if a link has already been started, complete the link to sourceDoc
+        } else {
+            // if a link has already been started, complete the link to sourceDoc
             runInAction(() => {
                 DocumentLinksButton.AnnotationId = annotationId;
                 DocumentLinksButton.AnnotationUri = annotationUri;
@@ -81,31 +81,41 @@ export namespace Hypothesis {
     export const makeLink = async (title: string, url: string, annotationId: string, annotationSourceDoc: Doc) => {
         // if the annotation's source webpage isn't currently loaded in Dash, we're not able to access and edit the annotation from the client
         // so we're loading the webpage and its annotations invisibly in a WebBox in MainView.tsx, until the editing is done
-        !DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
+        //!DocumentManager.Instance.getFirstDocumentView(annotationSourceDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = annotationSourceDoc);
 
         var success = false;
         const onSuccess = action(() => {
-            console.log("Edit success!!");
+            console.log('Edit success!!');
             success = true;
             clearTimeout(interval);
-            DocumentLinksButton.invisibleWebDoc = undefined;
-            document.removeEventListener("editSuccess", onSuccess);
+            //DocumentLinksButton.invisibleWebDoc = undefined;
+            document.removeEventListener('editSuccess', onSuccess);
         });
 
         const newHyperlink = `[${title}\n](${url})`;
-        const interval = setInterval(() => // keep trying to edit until annotations have loaded and editing is successful
-            !success && document.dispatchEvent(new CustomEvent<{ newHyperlink: string, id: string }>("addLink", {
-                detail: { newHyperlink: newHyperlink, id: annotationId },
-                bubbles: true
-            })), 300);
-
-        setTimeout(action(() => {
-            if (!success) {
-                clearInterval(interval);
-                DocumentLinksButton.invisibleWebDoc = undefined;
-            }
-        }), 10000); // give up if no success after 10s
-        document.addEventListener("editSuccess", onSuccess);
+        const interval = setInterval(
+            () =>
+                // keep trying to edit until annotations have loaded and editing is successful
+                !success &&
+                document.dispatchEvent(
+                    new CustomEvent<{ newHyperlink: string; id: string }>('addLink', {
+                        detail: { newHyperlink: newHyperlink, id: annotationId },
+                        bubbles: true,
+                    })
+                ),
+            300
+        );
+
+        setTimeout(
+            action(() => {
+                if (!success) {
+                    clearInterval(interval);
+                    //DocumentLinksButton.invisibleWebDoc = undefined;
+                }
+            }),
+            10000
+        ); // give up if no success after 10s
+        document.addEventListener('editSuccess', onSuccess);
     };
 
     /**
@@ -114,33 +124,40 @@ export namespace Hypothesis {
     export const deleteLink = async (linkDoc: Doc, sourceDoc: Doc, destinationDoc: Doc) => {
         if (Cast(destinationDoc.data, WebField)?.url.href !== StrCast(linkDoc.annotationUri)) return; // check that the destinationDoc is a WebDocument containing the target annotation
 
-        !DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
+        //!DocumentManager.Instance.getFirstDocumentView(destinationDoc) && runInAction(() => DocumentLinksButton.invisibleWebDoc = destinationDoc); // see note in makeLink
 
         var success = false;
         const onSuccess = action(() => {
-            console.log("Edit success!");
+            console.log('Edit success!');
             success = true;
             clearTimeout(interval);
-            DocumentLinksButton.invisibleWebDoc = undefined;
-            document.removeEventListener("editSuccess", onSuccess);
+            // DocumentLinksButton.invisibleWebDoc = undefined;
+            document.removeEventListener('editSuccess', onSuccess);
         });
 
         const annotationId = StrCast(linkDoc.annotationId);
         const linkUrl = Doc.globalServerPath(sourceDoc);
-        const interval = setInterval(() => {// keep trying to edit until annotations have loaded and editing is successful
-            !success && document.dispatchEvent(new CustomEvent<{ targetUrl: string, id: string }>("deleteLink", {
-                detail: { targetUrl: linkUrl, id: annotationId },
-                bubbles: true
-            }));
+        const interval = setInterval(() => {
+            // keep trying to edit until annotations have loaded and editing is successful
+            !success &&
+                document.dispatchEvent(
+                    new CustomEvent<{ targetUrl: string; id: string }>('deleteLink', {
+                        detail: { targetUrl: linkUrl, id: annotationId },
+                        bubbles: true,
+                    })
+                );
         }, 300);
 
-        setTimeout(action(() => {
-            if (!success) {
-                clearInterval(interval);
-                DocumentLinksButton.invisibleWebDoc = undefined;
-            }
-        }), 10000); // give up if no success after 10s
-        document.addEventListener("editSuccess", onSuccess);
+        setTimeout(
+            action(() => {
+                if (!success) {
+                    clearInterval(interval);
+                    //DocumentLinksButton.invisibleWebDoc = undefined;
+                }
+            }),
+            10000
+        ); // give up if no success after 10s
+        document.addEventListener('editSuccess', onSuccess);
     };
 
     /**
@@ -149,17 +166,20 @@ export namespace Hypothesis {
     export const scrollToAnnotation = (annotationId: string, target: Doc) => {
         var success = false;
         const onSuccess = () => {
-            console.log("Scroll success!!");
+            console.log('Scroll success!!');
             document.removeEventListener('scrollSuccess', onSuccess);
             clearInterval(interval);
             success = true;
         };
 
-        const interval = setInterval(() => { // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
-            document.dispatchEvent(new CustomEvent('scrollToAnnotation', {
-                detail: annotationId,
-                bubbles: true
-            }));
+        const interval = setInterval(() => {
+            // keep trying to scroll every 250ms until annotations have loaded and scrolling is successful
+            document.dispatchEvent(
+                new CustomEvent('scrollToAnnotation', {
+                    detail: annotationId,
+                    bubbles: true,
+                })
+            );
             const targetView: Opt<DocumentView> = DocumentManager.Instance.getFirstDocumentView(target);
             const position = targetView?.props.ScreenToLocalTransform().inverse().transformPoint(0, 0);
             targetView && position && simulateMouseClick(targetView.ContentDiv!, position[0], position[1], position[0], position[1], false);
@@ -168,4 +188,4 @@ export namespace Hypothesis {
         document.addEventListener('scrollSuccess', onSuccess); // listen for success message from client
         setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
     };
-}
\ No newline at end of file
+}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index 4e0b061a6..fe1190e27 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -250,8 +250,8 @@ $resizeHandler: 8px;
     .documentDecorations-lock {
         position: absolute;
         background: black;
-        right: 11;
-        top: 30px;
+        right: 23px;
+        top: 3px;
         color: gray;
         height: 14;
         width: 14;
@@ -260,6 +260,7 @@ $resizeHandler: 8px;
         display: flex;
         align-items: center;
         flex-direction: column;
+        border-radius: 15%;
     }
 
     .documentDecorations-rotationPath {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 06eb6c6d7..8e0686038 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -782,6 +782,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
         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 > 120;
         const useRotation = seldocview.rootDoc.type !== DocumentType.EQUATION; // when do we want an object to not rotate?
         const rotation = NumCast(seldocview.rootDoc._rotation);
 
@@ -856,9 +857,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                                     onContextMenu={e => e.preventDefault()}
                                 />
                             )}
-                            <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
-                                <FontAwesomeIcon size="sm" icon="lock" />
-                            </div>
+                            {!useLock ? null : (
+                                <div key="lock" className="documentDecorations-lock" style={{ color: seldocview.rootDoc._lockedPosition ? 'red' : undefined }} onPointerDown={this.onLockDown} onContextMenu={e => e.preventDefault()}>
+                                    <FontAwesomeIcon size="sm" icon="lock" />
+                                </div>
+                            )}
 
                             {hideDocumentButtonBar ? null : (
                                 <div
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index c19e6831f..5a6caf995 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -139,7 +139,7 @@ export class KeyManager {
                 document.body.focus();
                 break;
             case 'enter': {
-                DocumentDecorations.Instance.onCloseClick(false);
+                //DocumentDecorations.Instance.onCloseClick(false);
                 break;
             }
             case 'delete':
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index 3e6539c5e..f327f3184 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -3,9 +3,8 @@
 // }
 
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { AssignAllExtensions } from '../../extensions/General/Extensions';
-import { Utils } from '../../Utils';
 import { DocServer } from '../DocServer';
 import { Docs } from '../documents/Documents';
 import { CurrentUserUtils } from '../util/CurrentUserUtils';
@@ -14,7 +13,7 @@ import { ReplayMovements } from '../util/ReplayMovements';
 import { TrackMovements } from '../util/TrackMovements';
 import { CollectionView } from './collections/CollectionView';
 import { MainView } from './MainView';
-import * as dotenv from 'dotenv' // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
+import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
 dotenv.config();
 
 AssignAllExtensions();
@@ -45,6 +44,6 @@ AssignAllExtensions();
         new LinkManager();
         new TrackMovements();
         new ReplayMovements();
-        ReactDOM.render(<MainView />, document.getElementById('root'));
+        ReactDOM.createRoot(document.getElementById('root')!).render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MainView.scss b/src/client/views/MainView.scss
index c5ac1cf52..069206126 100644
--- a/src/client/views/MainView.scss
+++ b/src/client/views/MainView.scss
@@ -371,12 +371,3 @@ h1,
     width: 200px;
     height: 800px;
 }
-
-.mainVew-invisibleWebRef {
-    position: absolute;
-    left: 50;
-    top: 50;
-    display: block;
-    width: 500px;
-    height: 1000px;
-}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 052846e71..9648a7807 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -7,7 +7,7 @@ import { action, computed, configure, observable, reaction, runInAction } from '
 import { observer } from 'mobx-react';
 import 'normalize.css';
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc, DocListCast, Opt } from '../../fields/Doc';
 import { ScriptField } from '../../fields/ScriptField';
 import { DocCast, StrCast } from '../../fields/Types';
@@ -948,40 +948,6 @@ export class MainView extends React.Component {
         );
     }
 
-    @computed get invisibleWebBox() {
-        // see note under the makeLink method in HypothesisUtils.ts
-        return !DocumentLinksButton.invisibleWebDoc ? null : (
-            <div className="mainView-invisibleWebRef" ref={DocumentLinksButton.invisibleWebRef}>
-                <WebBox
-                    fieldKey={'data'}
-                    ContainingCollectionView={undefined}
-                    ContainingCollectionDoc={undefined}
-                    Document={DocumentLinksButton.invisibleWebDoc}
-                    dropAction={'move'}
-                    styleProvider={undefined}
-                    isSelected={returnFalse}
-                    select={returnFalse}
-                    setHeight={returnFalse}
-                    rootSelected={returnFalse}
-                    renderDepth={0}
-                    addDocTab={returnFalse}
-                    pinToPres={returnFalse}
-                    ScreenToLocalTransform={Transform.Identity}
-                    bringToFront={returnFalse}
-                    isContentActive={emptyFunction}
-                    whenChildContentsActiveChanged={returnFalse}
-                    focus={returnFalse}
-                    docViewPath={returnEmptyDoclist}
-                    PanelWidth={() => 500}
-                    PanelHeight={() => 800}
-                    docFilters={returnEmptyFilter}
-                    docRangeFilters={returnEmptyFilter}
-                    searchFilterDocs={returnEmptyDoclist}
-                />
-            </div>
-        );
-    }
-
     render() {
         return (
             <div
@@ -1037,68 +1003,10 @@ export class MainView extends React.Component {
                 <RichTextMenu />
                 <InkTranscription />
                 {this.snapLines}
-                <div className="mainView-webRef" ref={this.makeWebRef} />
                 <LightboxView key="lightbox" PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} maxBorder={[200, 50]} />
             </div>
         );
     }
-
-    makeWebRef = (ele: HTMLDivElement) => {
-        reaction(
-            () => DocumentLinksButton.invisibleWebDoc,
-            invisibleDoc => {
-                ReactDOM.unmountComponentAtNode(ele);
-                invisibleDoc &&
-                    ReactDOM.render(
-                        <span title="Drag as document" className="invisible-webbox">
-                            <div className="mainView-webRef" ref={DocumentLinksButton.invisibleWebRef}>
-                                <WebBox
-                                    fieldKey={'data'}
-                                    ContainingCollectionView={undefined}
-                                    ContainingCollectionDoc={undefined}
-                                    Document={invisibleDoc}
-                                    dropAction={'move'}
-                                    isSelected={returnFalse}
-                                    docViewPath={returnEmptyDoclist}
-                                    select={returnFalse}
-                                    rootSelected={returnFalse}
-                                    renderDepth={0}
-                                    setHeight={returnFalse}
-                                    styleProvider={undefined}
-                                    addDocTab={returnFalse}
-                                    pinToPres={returnFalse}
-                                    ScreenToLocalTransform={Transform.Identity}
-                                    bringToFront={returnFalse}
-                                    isContentActive={emptyFunction}
-                                    whenChildContentsActiveChanged={returnFalse}
-                                    focus={returnFalse}
-                                    PanelWidth={() => 500}
-                                    PanelHeight={() => 800}
-                                    docFilters={returnEmptyFilter}
-                                    docRangeFilters={returnEmptyFilter}
-                                    searchFilterDocs={returnEmptyDoclist}
-                                />
-                            </div>
-                            ;
-                        </span>,
-                        ele
-                    );
-
-                let success = false;
-                const onSuccess = () => {
-                    success = true;
-                    clearTimeout(interval);
-                    document.removeEventListener('editSuccess', onSuccess);
-                };
-
-                // For some reason, Hypothes.is annotations don't load until a click is registered on the page,
-                // so we keep simulating clicks until annotations have loaded and editing is successful
-                const interval = setInterval(() => !success && simulateMouseClick(ele, 50, 50, 50, 50), 500);
-                setTimeout(() => !success && clearInterval(interval), 10000); // give up if no success after 10s
-                document.addEventListener('editSuccess', onSuccess);
-            }
-        );
-    };
 }
 
 ScriptingGlobals.add(function selectMainMenu(doc: Doc, title: string) {
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index d9a989309..ce0e58d90 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -27,6 +27,7 @@ export interface MarqueeAnnotatorProps {
     mainCont: HTMLDivElement;
     docView: DocumentView;
     savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
+    selectionText: () => string;
     annotationLayer: HTMLDivElement;
     addDocument: (doc: Doc) => boolean;
     getPageFromScroll?: (top: number) => number;
@@ -147,7 +148,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
             return marqueeAnno;
         }
 
-        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
+        const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', title: 'Selection on ' + this.props.rootDoc.title });
         let minX = Number.MAX_VALUE;
         let maxX = -Number.MAX_VALUE;
         let minY = Number.MAX_VALUE;
@@ -271,7 +272,8 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
                     width: `${this._width}px`,
                     height: `${this._height}px`,
                     border: `${this._width === 0 ? '' : '2px dashed black'}`,
-                }}/>
+                }}
+            />
         );
     }
 }
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 12f41394d..df1eb72ce 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -15,6 +15,7 @@ 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;
@@ -72,6 +73,27 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         FormattedTextBox.DontSelectInitialText = true;
         this.allMetadata.map(tag => (target[tag] = tag));
         DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+
+        Doc.GetProto(target).text = new RichTextField(
+            JSON.stringify({
+                doc: {
+                    type: 'doc',
+                    content: [
+                        {
+                            type: 'paragraph',
+                            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                            content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }],
+                        },
+                        {
+                            type: 'paragraph',
+                            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                        },
+                    ],
+                },
+                selection: { type: 'text', anchor: 4, head: 4 },
+            }),
+            ''
+        );
         this.addDocument(target);
         this._stackRef.current?.focusDocument(target, {});
         return target;
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 869570a17..5abf4bde2 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -288,11 +288,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             return undefined;
         case StyleProp.Decorations:
             if (props?.ContainingCollectionDoc?._viewType === CollectionViewType.Freeform || doc?.x !== undefined || doc?.y !== undefined) {
-                return doc &&
-                    isBackground() &&
-                    !Doc.IsSystem(doc) &&
-                    (props?.renderDepth || 0) > 0 &&
-                    ((doc.type === DocumentType.COL && doc._viewType !== CollectionViewType.Pile) || [DocumentType.RTF, DocumentType.IMG, DocumentType.INK].includes(doc.type as DocumentType)) ? (
+                return doc && isBackground() && !Doc.IsSystem(doc) && (props?.renderDepth || 0) > 0 ? (
                     <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}>
                         <FontAwesomeIcon icon={isBackground() ? 'lock' : 'unlock'} style={{ color: isBackground() ? 'red' : undefined }} size="lg" />
                     </div>
diff --git a/src/client/views/collections/CollectionDockingView.scss b/src/client/views/collections/CollectionDockingView.scss
index ac3541f2c..78e44dfa2 100644
--- a/src/client/views/collections/CollectionDockingView.scss
+++ b/src/client/views/collections/CollectionDockingView.scss
@@ -89,6 +89,10 @@
     position: relative;
 }
 
+.lm_stack {
+    position: relative;
+}
+
 .lm_drag_tab {
     padding: 0;
     width: 15px !important;
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index e9b41de25..92319d080 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -1,6 +1,6 @@
 import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import * as GoldenLayout from '../../../client/goldenLayout';
 import { Doc, DocListCast, Opt } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
@@ -318,7 +318,7 @@ export class CollectionDockingView extends CollectionSubView() {
         }
     };
 
-    componentDidMount: () => void = () => {
+    componentDidMount: () => void = async () => {
         if (this._containerRef.current) {
             this._lightboxReactionDisposer = reaction(
                 () => LightboxView.LightboxDoc,
@@ -335,8 +335,11 @@ export class CollectionDockingView extends CollectionSubView() {
                     this._ignoreStateChange = '';
                 }
             );
-            setTimeout(this.setupGoldenLayout);
-            //window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window
+            reaction(
+                () => this.props.PanelWidth(),
+                width => !this._goldenLayout && width > 20 && setTimeout(() => this.setupGoldenLayout()), // need to wait for the collectiondockingview-container to have it's width/height since golden layout reads that to configure its windows
+                { fireImmediately: true }
+            );
         }
     };
 
@@ -460,7 +463,7 @@ export class CollectionDockingView extends CollectionSubView() {
     };
 
     tabDestroyed = (tab: any) => {
-        if (![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
+        if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) {
             Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc);
             Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true);
         }
@@ -470,8 +473,8 @@ export class CollectionDockingView extends CollectionSubView() {
             Doc.RemoveDocFromList(dview, fieldKey, tab.DashDoc);
             this.tabMap.delete(tab);
             tab._disposers && Object.values(tab._disposers).forEach((disposer: any) => disposer?.());
-            tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
-            setTimeout(this.stateChanged);
+            //tab.reactComponents?.forEach((ele: any) => ReactDOM.unmountComponentAtNode(ele));
+            this.stateChanged();
         }
     };
     tabCreated = (tab: any) => {
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index cde5132a3..31ed5a83b 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -4,7 +4,7 @@ import { Tooltip } from '@material-ui/core';
 import { clamp } from 'lodash';
 import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
@@ -143,8 +143,8 @@ export class TabDocView extends React.Component<TabDocViewProps> {
 
                 const docIcon = <FontAwesomeIcon onPointerDown={dragBtnDown} icon={iconType} />;
                 const closeIcon = <FontAwesomeIcon icon={'eye'} />;
-                ReactDOM.render(docIcon, iconWrap);
-                ReactDOM.render(closeIcon, closeWrap);
+                ReactDOM.createRoot(iconWrap).render(docIcon);
+                ReactDOM.createRoot(closeWrap).render(closeIcon);
                 tab.reactComponents = [iconWrap, closeWrap];
                 tab.element[0].prepend(iconWrap);
                 tab._disposers.layerDisposer = reaction(
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 1d455ad08..627487a9e 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -40,9 +40,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
     @observable public static AnnotationUri: string | undefined;
     @observable public static LinkEditorDocView: DocumentView | undefined;
 
-    @observable public static invisibleWebDoc: Opt<Doc>;
-    public static invisibleWebRef = React.createRef<HTMLDivElement>();
-
     @action
     @undoBatch
     onLinkButtonMoved = (e: PointerEvent) => {
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index c7001f846..fcb3ccb07 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -74,7 +74,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         if (oldDiv.className === 'pdfBox-ui' || oldDiv.className === 'pdfViewerDash-overlay-inking') {
             newDiv.style.display = 'none';
         }
-        if (newDiv && newDiv.style) newDiv.style.overflow = 'hidden';
+        if (newDiv?.style) newDiv.style.overflow = 'hidden';
         if (oldDiv instanceof HTMLCanvasElement) {
             const canvas = oldDiv;
             const img = document.createElement('img'); // create a Image Element
@@ -398,9 +398,12 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             </div>
         );
     }
-    sidebarWidth = () =>
-        !this.SidebarShown ? 0 : PDFBox.sidebarResizerWidth + (this._previewWidth ? PDFBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth));
-
+    sidebarWidth = () => {
+        if (!this.SidebarShown) return 0;
+        if (this._previewWidth) return PDFBox.sidebarResizerWidth + PDFBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target)
+        const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
+        return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
+    };
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
         funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
@@ -483,7 +486,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         crop={this.crop}
                     />
                 </div>
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}>
+                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
                         {...this.props}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index db493934a..5ce6a0eb1 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -836,9 +836,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             }
         }
     });
-    sidebarWidth = () =>
-        !this.SidebarShown ? 0 : WebBox.sidebarResizerWidth + (this._previewWidth ? WebBox.openSidebarWidth : ((NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc)) * this.props.PanelWidth()) / NumCast(this.layoutDoc.nativeWidth));
-
+    sidebarWidth = () => {
+        if (!this.SidebarShown) return 0;
+        if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target)
+        const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
+        return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
+    };
     @computed get content() {
         const interactive =
             !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
@@ -1000,6 +1003,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 docView={this.props.docViewPath().lastElement()}
                                 finishMarquee={this.finishMarquee}
                                 savedAnnotations={this.savedAnnotationsCreator}
+                                selectionText={returnEmptyString}
                                 annotationLayer={this._annotationLayer.current}
                                 mainCont={this._mainCont.current}
                             />{' '}
@@ -1015,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     }}
                     onPointerDown={e => this.sidebarBtnDown(e, false)}
                 />
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.layoutDoc[WidthSym]()}%` }}>
+                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
                     <SidebarAnnos
                         ref={this._sidebarRef}
                         {...this.props}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 2b83e9da8..42d20ba99 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -1,7 +1,6 @@
 import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
-import Color from 'color';
 import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
@@ -12,7 +11,7 @@ import { ScriptField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { WebField } from '../../../../fields/URLField';
 import { GestureUtils } from '../../../../pen-gestures/GestureUtils';
-import { aggregateBounds, Utils } from '../../../../Utils';
+import { aggregateBounds, StopEvent, Utils } from '../../../../Utils';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
 import { SelectionManager } from '../../../util/SelectionManager';
@@ -382,12 +381,15 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         //     style={{ borderBottomRightRadius: this.dropdown ? 0 : undefined }}>
         //     <FontAwesomeIcon icon={'caret-down'} color={color} size="sm" />
         // </div>;
-        setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
+        //setTimeout(() => this.colorPicker(curColor)); // cause an update to the color picker rendered in MainView
         return (
             <div
                 className={`menuButton ${this.type + (FontIconBox.GetShowLabels() ? 'Label' : '')} ${this.colorPickerClosed}`}
                 style={{ color: color, borderBottomLeftRadius: this.dropdown ? 0 : undefined }}
-                onClick={action(() => (this.colorPickerClosed = !this.colorPickerClosed))}
+                onClick={action(e => {
+                    this.colorPickerClosed = !this.colorPickerClosed;
+                    e.stopPropagation();
+                })}
                 onPointerDown={e => e.stopPropagation()}>
                 {this.Icon(color)}
                 <div className="colorButton-color" style={{ backgroundColor: curColor }} />
@@ -395,7 +397,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                 {/* {dropdownCaret} */}
                 {this.colorPickerClosed ? null : (
                     <div>
-                        <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
+                        <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onClick={e => e.stopPropagation()}>
                             {this.colorPicker(curColor)}
                         </div>
                         <div
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 40dd6fbc7..85523c176 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -1,5 +1,5 @@
 import { TextSelection } from 'prosemirror-state';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc } from '../../../../fields/Doc';
 import { DocServer } from '../../../DocServer';
 import React = require('react');
@@ -9,6 +9,7 @@ import React = require('react');
 // the comment can be toggled on/off with the '<-' text anchor.
 export class DashDocCommentView {
     dom: HTMLDivElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any) {
         this.dom = document.createElement('div');
@@ -30,12 +31,13 @@ export class DashDocCommentView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />, this.dom);
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
         (this as any).dom = this.dom;
     }
 
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
 
     selectNode() {}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 5f576be41..63ee7d1f3 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -1,7 +1,7 @@
 import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { NodeSelection } from 'prosemirror-state';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc';
 import { Cast, NumCast, StrCast } from '../../../../fields/Types';
 import { emptyFunction, returnFalse, Utils } from '../../../../Utils';
@@ -15,6 +15,7 @@ import React = require('react');
 
 export class DashDocView {
     dom: HTMLSpanElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this.dom = document.createElement('span');
@@ -38,14 +39,13 @@ export class DashDocView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(
-            <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />,
-            this.dom
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(
+            <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
         );
-        (this as any).dom = this.dom;
     }
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
     selectNode() {}
 }
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 35d919f38..f088b39d1 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -1,6 +1,6 @@
 import { action, computed, IReactionDisposer, observable } from 'mobx';
 import { observer } from 'mobx-react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc';
 import { List } from '../../../../fields/List';
 import { listSpec } from '../../../../fields/Schema';
@@ -19,6 +19,7 @@ import { CollectionViewType } from '../../../documents/DocumentTypes';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey);
@@ -43,11 +44,11 @@ export class DashFieldView {
             e.stopPropagation();
         };
 
-        setTimeout(() => ReactDOM.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} tbox={tbox} />, this.dom));
-        (this as any).dom = this.dom;
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />);
     }
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        //this.root.unmount();
     }
     selectNode() {}
 }
@@ -59,6 +60,7 @@ interface IDashFieldViewInternal {
     tbox: FormattedTextBox;
     width: number;
     height: number;
+    editable: boolean;
 }
 
 @observer
@@ -116,7 +118,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
                 return (
                     <span
                         className="dashFieldView-fieldSpan"
-                        contentEditable={true}
+                        contentEditable={this.props.editable}
                         style={{ display: strVal.length < 2 ? 'inline-block' : undefined }}
                         suppressContentEditableWarning={true}
                         defaultValue={strVal}
@@ -161,6 +163,41 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
         if (nodeText) {
             const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText;
 
+            // const json = {
+            //     doc: {
+            //         type: 'doc',
+            //         content: [
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: '7e02a420-8add-49b0-ad20-54680567575f', hideKey: true, editable: false }, marks: [{ type: 'strong' }] }],
+            //             },
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334077 } }] }],
+            //             },
+            //         ],
+            //     },
+            // };
+
+            // const json = {
+            //     doc: {
+            //         type: 'doc',
+            //         content: [
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'dashField', attrs: { fieldKey: 'hello', docid: '', hideKey: true, editable: true }, marks: [{ type: 'strong' }, { type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334006 } }] }],
+            //             },
+            //             {
+            //                 type: 'paragraph',
+            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            //                 content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334088 } }] }],
+            //             },
+            //         ],
+            //     },
+            // };
             // look for a document whose id === the fieldKey being displayed.  If there's a match, then that document
             // holds the different enumerated values for the field in the titles of its collected documents.
             // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
@@ -266,14 +303,12 @@ export class DashFieldViewMenu extends AntimodeMenu<AntimodeMenuProps> {
         document.addEventListener('pointerdown', hideMenu, true);
     };
     render() {
-        const buttons = [
+        return this.getElement([
             <Tooltip key="trash" title={<div className="dash-tooltip">{`Show Pivot Viewer for '${this._fieldKey}'`}</div>}>
                 <button className="antimodeMenu-button" onPointerDown={this.showFields}>
                     <FontAwesomeIcon icon="eye" size="lg" />
                 </button>
             </Tooltip>,
-        ];
-
-        return this.getElement(buttons);
+        ]);
     }
 }
diff --git a/src/client/views/nodes/formattedText/EquationView.tsx b/src/client/views/nodes/formattedText/EquationView.tsx
index 4895dcdc5..0fd2a7808 100644
--- a/src/client/views/nodes/formattedText/EquationView.tsx
+++ b/src/client/views/nodes/formattedText/EquationView.tsx
@@ -2,7 +2,7 @@ import EquationEditor from 'equation-editor-react';
 import { IReactionDisposer } from 'mobx';
 import { observer } from 'mobx-react';
 import { TextSelection } from 'prosemirror-state';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { Doc } from '../../../../fields/Doc';
 import { StrCast } from '../../../../fields/Types';
 import './DashFieldView.scss';
@@ -11,7 +11,7 @@ import React = require('react');
 
 export class EquationView {
     dom: HTMLDivElement; // container for label and value
-
+    root: any;
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
@@ -22,13 +22,13 @@ export class EquationView {
             e.stopPropagation();
         };
 
-        ReactDOM.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />, this.dom);
-        (this as any).dom = this.dom;
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<EquationViewInternal fieldKey={node.attrs.fieldKey} width={node.attrs.width} height={node.attrs.height} getPos={getPos} setEditor={this.setEditor} tbox={tbox} />);
     }
     _editor: EquationEditor | undefined;
     setEditor = (editor?: EquationEditor) => (this._editor = editor);
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
     setSelection() {
         this._editor?.mathField.focus();
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 096f9a92c..0e48dd93a 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1132,6 +1132,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         );
         quickScroll = undefined;
         this.tryUpdateScrollHeight();
+        setTimeout(this.tryUpdateScrollHeight, 250);
     }
 
     pushToGoogleDoc = async () => {
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 47833dd43..dc5d8ada8 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -248,8 +248,7 @@ export class RichTextRules {
             // [[fieldKey:Doc]] => show field of doc
             new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
                 const fieldKey = match[1];
-                const rawdocid = match[3];
-                const docid = rawdocid ? normalizeEmail(!rawdocid.includes('@') ? Doc.CurrentUserEmail + rawdocid : rawdocid.substring(1)) : undefined;
+                const docid = match[3]?.replace(':', '');
                 const value = match[2]?.substring(1);
                 if (!fieldKey) {
                     if (docid) {
@@ -259,7 +258,7 @@ export class RichTextRules {
                             if (rstate) {
                                 this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
                             }
-                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: rawdocid.replace(/^:/, ''), _width: 500, _height: 500 }, docid);
+                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
                             DocUtils.MakeLink({ doc: this.TextBox.getAnchor() }, { doc: target }, 'portal to:portal from', undefined);
 
                             const fstate = this.TextBox.EditorView?.state;
@@ -275,7 +274,7 @@ export class RichTextRules {
                     const num = value.match(/^[0-9.]$/);
                     this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
                 }
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
+                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: true });
                 return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
             }),
 
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
index 01acc3de9..1fe6d822b 100644
--- a/src/client/views/nodes/formattedText/SummaryView.tsx
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -1,6 +1,6 @@
 import { TextSelection } from 'prosemirror-state';
 import { Fragment, Node, Slice } from 'prosemirror-model';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import React = require('react');
 
 // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked.
@@ -9,6 +9,7 @@ import React = require('react');
 // method instead of changing prosemirror's text when the expand/elide buttons are clicked.
 export class SummaryView {
     dom: HTMLSpanElement; // container for label and value
+    root: any;
 
     constructor(node: any, view: any, getPos: any) {
         const self = this;
@@ -35,13 +36,13 @@ export class SummaryView {
             return js.apply(this, arguments);
         };
 
-        ReactDOM.render(<SummaryViewInternal />, this.dom);
-        (this as any).dom = this.dom;
+        this.root = ReactDOM.createRoot(this.dom);
+        this.root.render(<SummaryViewInternal />);
     }
 
     className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed');
     destroy() {
-        ReactDOM.unmountComponentAtNode(this.dom);
+        // ReactDOM.unmountComponentAtNode(this.dom);
     }
     selectNode() {}
 
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index 66d747bf7..aa2475dca 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -263,6 +263,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             fieldKey: { default: '' },
             docid: { default: '' },
             hideKey: { default: false },
+            editable: { default: true },
         },
         group: 'inline',
         draggable: false,
diff --git a/src/client/views/pdf/PDFViewer.scss b/src/client/views/pdf/PDFViewer.scss
index 822af6a68..470aa3eb1 100644
--- a/src/client/views/pdf/PDFViewer.scss
+++ b/src/client/views/pdf/PDFViewer.scss
@@ -27,14 +27,14 @@
         opacity: unset;
         mix-blend-mode: multiply; // bcz: makes text fuzzy!
 
-        span {
-            padding-right: 5px;
-            padding-bottom: 4px;
-        }
+        // span {
+        //     padding-right: 5px;
+        //     padding-bottom: 4px;
+        // }
     }
 
     .textLayer ::selection {
-        background: #ACCEF7;
+        background: #accef7;
     }
 
     // should match the backgroundColor in createAnnotation()
@@ -111,4 +111,4 @@
 
 .pdfViewerDash-interactive {
     pointer-events: all;
-}
\ No newline at end of file
+}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 5c10c7cef..abc7336bd 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -79,6 +79,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
     private _initialScroll: Opt<number>;
     private _forcedScroll = true;
 
+    selectionText = () => this._selectionText;
+
     @observable isAnnotating = false;
     // key where data is stored
     @computed get allAnnotations() {
@@ -358,7 +360,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
             MarqueeAnnotator.clearAnnotations(this._savedAnnotations);
             this._marqueeing = [e.clientX, e.clientY];
             this.isAnnotating = true;
-            if (e.target && ((e.target as any).className.includes('endOfContent') || (e.target as any).parentElement.className !== 'textLayer')) {
+            const target = e.target as any;
+            if (e.target && (target.className.includes('endOfContent') || (target.parentElement.className !== 'textLayer' && target.parentElement.parentElement?.className !== 'textLayer'))) {
                 this._textSelecting = false;
                 document.addEventListener('pointermove', this.onSelectMove); // need this to prevent document from being dragged if stopPropagation doesn't get called
             } else {
@@ -574,6 +577,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
                             docView={this.props.docViewPath().lastElement()}
                             finishMarquee={this.finishMarquee}
                             savedAnnotations={this.savedAnnotations}
+                            selectionText={this.selectionText}
                             annotationLayer={this._annotationLayer.current}
                             mainCont={this._mainCont.current}
                             anchorMenuCrop={this._textSelecting ? undefined : this.crop}
diff --git a/src/debug/Repl.tsx b/src/debug/Repl.tsx
index 1b12e208c..b8081648f 100644
--- a/src/debug/Repl.tsx
+++ b/src/debug/Repl.tsx
@@ -1,5 +1,5 @@
 import * as React from 'react';
-import * as ReactDOM from 'react-dom';
+import * as ReactDOM from 'react-dom/client';
 import { observer } from 'mobx-react';
 import { observable, computed } from 'mobx';
 import { CompileScript } from '../client/util/Scripting';
@@ -11,39 +11,40 @@ import { resolvedPorts } from '../client/util/CurrentUserUtils';
 
 @observer
 class Repl extends React.Component {
-    @observable text: string = "";
+    @observable text: string = '';
 
-    @observable executedCommands: { command: string, result: any }[] = [];
+    @observable executedCommands: { command: string; result: any }[] = [];
 
     onChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
         this.text = e.target.value;
-    }
+    };
 
     onKeyDown = (e: React.KeyboardEvent) => {
-        if (!e.ctrlKey && e.key === "Enter") {
+        if (!e.ctrlKey && e.key === 'Enter') {
             e.preventDefault();
             const script = CompileScript(this.text, {
-                addReturn: true, typecheck: false,
-                params: { makeInterface: "any" }
+                addReturn: true,
+                typecheck: false,
+                params: { makeInterface: 'any' },
             });
             if (!script.compiled) {
-                this.executedCommands.push({ command: this.text, result: "Compile Error" });
+                this.executedCommands.push({ command: this.text, result: 'Compile Error' });
             } else {
                 const result = script.run({ makeInterface }, e => this.executedCommands.push({ command: this.text, result: e.message || e }));
                 result.success && this.executedCommands.push({ command: this.text, result: result.result });
             }
-            this.text = "";
+            this.text = '';
         }
-    }
+    };
 
     @computed
     get commands() {
         return this.executedCommands.map(command => {
             return (
-                <div style={{ marginTop: "5px" }}>
+                <div style={{ marginTop: '5px' }}>
                     <p>{command.command}</p>
                     {/* <pre>{JSON.stringify(command.result, null, 2)}</pre> */}
-                    <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? "object" : String(command.result)}</pre>
+                    <pre>{command.result instanceof RefField || command.result instanceof ObjectField ? 'object' : String(command.result)}</pre>
                 </div>
             );
         });
@@ -52,16 +53,14 @@ class Repl extends React.Component {
     render() {
         return (
             <div>
-                <div style={{ verticalAlign: "bottom" }}>
-                    {this.commands}
-                </div>
-                <textarea style={{ width: "100%", position: "absolute", bottom: "0px" }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} />
+                <div style={{ verticalAlign: 'bottom' }}>{this.commands}</div>
+                <textarea style={{ width: '100%', position: 'absolute', bottom: '0px' }} value={this.text} onChange={this.onChange} onKeyDown={this.onKeyDown} />
             </div>
         );
     }
 }
 
 (async function () {
-    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, "repl");
-    ReactDOM.render(<Repl />, document.getElementById("root"));
-})();
\ No newline at end of file
+    DocServer.init(window.location.protocol, window.location.hostname, resolvedPorts.socket, 'repl');
+    ReactDOM.createRoot(document.getElementById('root')!).render(<Repl />);
+})();
-- 
cgit v1.2.3-70-g09d2


From 1a8024dce302acc1637cd1f7d74b0238c5979c15 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 2 Nov 2022 10:42:25 -0400
Subject: select docs when title header is edited. fixed doc decoration top
 border.  sort sidebar annos by y coord

---
 src/client/views/DocumentDecorations.scss               |  4 +++-
 src/client/views/SidebarAnnos.tsx                       |  9 ++++++++-
 src/client/views/collections/CollectionStackingView.tsx | 12 ++++++++----
 src/client/views/nodes/DocumentView.tsx                 |  5 ++++-
 4 files changed, 23 insertions(+), 7 deletions(-)

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

diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index b41962c73..ccac5ffe4 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -1,7 +1,7 @@
 @import 'global/globalCssVariables';
 
 $linkGap: 3px;
-$headerHeight: 25px;
+$headerHeight: 20px;
 $resizeHandler: 8px;
 
 .documentDecorations-Dark,
@@ -424,7 +424,9 @@ $resizeHandler: 8px;
     background: $light-gray;
     opacity: 0.2;
     pointer-events: all;
+    transition: opacity 1s;
     &:hover {
+        transition: unset;
         opacity: 1;
     }
 }
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index df1eb72ce..869caabd1 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -3,7 +3,7 @@ import { observer } from 'mobx-react';
 import { Doc, DocListCast, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
-import { NumCast, StrCast } from '../../fields/Types';
+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';
@@ -66,6 +66,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             _height: 50,
             _fitWidth: true,
             _autoHeight: true,
+            _isLinkButton: true,
             _fontSize: StrCast(Doc.UserDoc().fontSize),
             _fontFamily: StrCast(Doc.UserDoc().fontFamily),
         });
@@ -133,6 +134,11 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])];
     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;
+        return NumCast(ay) - NumCast(by);
+    };
     render() {
         const renderTag = (tag: string) => {
             const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`);
@@ -184,6 +190,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                         PanelWidth={this.panelWidth}
                         docFilters={this.docFilters}
                         scaleField={this.sidebarKey + '-scale'}
+                        sortFunc={this.sortByLinkAnchorY}
                         setHeight={this.setHeightCallback}
                         isAnnotationOverlay={false}
                         select={emptyFunction}
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 77b47ed82..175051d5c 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -3,12 +3,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CursorProperty } from 'csstype';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -33,6 +33,7 @@ import { CollectionSubView } from './CollectionSubView';
 const _global = (window /* browser */ || global) /* node */ as any;
 
 export type collectionStackingViewProps = {
+    sortFunc?: (a: Doc, b: Doc) => number;
     chromeHidden?: boolean;
     // view type is stacking
     viewType?: CollectionViewType;
@@ -73,7 +74,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     }
     // filteredChildren is what you want to work with. It's the list of things that you're currently displaying
     @computed get filteredChildren() {
-        return this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout);
+        const children = this.childLayoutPairs.filter(pair => pair.layout instanceof Doc && !pair.layout.hidden).map(pair => pair.layout);
+        if (this.props.sortFunc) children.sort(this.props.sortFunc);
+        return children;
     }
     // how much margin we give the header
     @computed get headerMargin() {
@@ -305,6 +308,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
 
     isChildContentActive = () =>
         this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
+    isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined);
     // this is what renders the document that you see on the screen
     // called in Children: this actually adds a document to our children list
     getDisplayDoc(doc: Doc, width: () => number) {
@@ -326,7 +330,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                 styleProvider={this.styleProvider}
                 docViewPath={this.props.docViewPath}
                 fitWidth={this.props.childFitWidth}
-                isContentActive={this.isChildContentActive}
+                isContentActive={doc.isLinkButton ? this.isChildButtonContentActive : this.isChildContentActive}
                 onKey={this.onKeyDown}
                 isDocumentActive={this.isContentActive}
                 LayoutTemplate={this.props.childLayoutTemplate}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 50b76896e..595fc3bca 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1313,7 +1313,10 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         .join('\\')}
                     display={'block'}
                     fontSize={10}
-                    GetValue={() => (showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle)}
+                    GetValue={() => {
+                        this.props.select(false);
+                        return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle;
+                    }}
                     SetValue={undoBatch((input: string) => {
                         if (input?.startsWith('#')) {
                             if (this.props.showTitle) {
-- 
cgit v1.2.3-70-g09d2


From b171b939743de6126f3fb5ca1e2d34bba7cbe428 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 2 Nov 2022 12:54:18 -0400
Subject: fix sidebar so active filters add proper metadata to newly created
 documents

---
 src/client/views/SidebarAnnos.tsx                  | 72 ++++++++++++++--------
 src/client/views/nodes/DocumentView.tsx            |  1 +
 .../views/nodes/formattedText/FormattedTextBox.tsx |  5 +-
 3 files changed, 52 insertions(+), 26 deletions(-)

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

diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 869caabd1..78480da03 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -1,6 +1,6 @@
 import { computed } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, DocListCast, StrListCast } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, StrListCast } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
 import { List } from '../../fields/List';
 import { DocCast, NumCast, StrCast } from '../../fields/Types';
@@ -39,12 +39,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
     }
     _stackRef = React.createRef<CollectionStackingView>();
     @computed get allMetadata() {
-        const keys = new Set<string>();
-        DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => SearchBox.documentKeys(doc).forEach(key => keys.add(key)));
-        return Array.from(keys.keys())
-            .filter(key => key[0])
-            .filter(key => key[0] !== '_' && key[0] === key[0].toUpperCase())
-            .sort();
+        const keys = new Map<string, FieldResult<Field>>();
+        DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc =>
+            SearchBox.documentKeys(doc)
+                .filter(key => key[0] && key[0] !== '_' && key[0] === key[0].toUpperCase())
+                .map(key => keys.set(key, doc[key]))
+        );
+        return keys;
     }
     @computed get allUsers() {
         const keys = new Set<string>();
@@ -72,26 +73,47 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         });
         FormattedTextBox.SelectOnLoad = target[Id];
         FormattedTextBox.DontSelectInitialText = true;
-        this.allMetadata.map(tag => (target[tag] = tag));
         DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
 
+        const taggedContent = this.docFilters()
+            .filter(data => data.split(':')[0])
+            .map(data => {
+                const key = data.split(':')[0];
+                const val = Field.Copy(this.allMetadata.get(key));
+                Doc.GetProto(target)[key] = val;
+                return {
+                    type: 'dashField',
+                    attrs: { fieldKey: key, docid: '', hideKey: false, editable: true },
+                    marks: [
+                        { type: 'pFontSize', attrs: { fontSize: '12px' } },
+                        { type: 'pFontFamily', attrs: { family: 'Arial' } },
+                        { type: 'pFontColor', attrs: { color: 'black' } },
+                        { type: 'strong' },
+                        { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } },
+                    ],
+                };
+            });
+        const textLines = [
+            {
+                type: 'paragraph',
+                attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+                content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }],
+            },
+            { type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null } },
+        ];
+        const metadatatext = {
+            type: 'paragraph',
+            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
+            content: taggedContent,
+        };
+        if (taggedContent.length) textLines.push(metadatatext);
         Doc.GetProto(target).text = new RichTextField(
             JSON.stringify({
                 doc: {
                     type: 'doc',
-                    content: [
-                        {
-                            type: 'paragraph',
-                            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-                            content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }],
-                        },
-                        {
-                            type: 'paragraph',
-                            attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-                        },
-                    ],
+                    content: textLines,
                 },
-                selection: { type: 'text', anchor: 4, head: 4 },
+                selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
             }),
             ''
         );
@@ -148,10 +170,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 </div>
             );
         };
-        const renderMeta = (tag: string) => {
-            const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:exists`);
+        const renderMeta = (tag: string, dflt: FieldResult<Field>) => {
+            const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${dflt}:exists`);
             return (
-                <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, 'exists', true, this.sidebarKey, e.shiftKey)}>
+                <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, dflt, 'exists', true, this.sidebarKey, e.shiftKey)}>
                     {tag}
                 </div>
             );
@@ -178,7 +200,9 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 }}>
                 <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}>
                     {this.allUsers.map(renderUsers)}
-                    {this.allMetadata.map(renderMeta)}
+                    {Array.from(this.allMetadata.keys())
+                        .sort()
+                        .map(key => renderMeta(key, this.allMetadata.get(key)))}
                 </div>
                 <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}>
                     <CollectionStackingView
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 595fc3bca..a35400e72 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -619,6 +619,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     }, 350);
                 } else clickFunc();
             } else if (this.allLinks && this.Document.type !== DocumentType.LINK && !isScriptBox() && this.Document.isLinkButton && !e.shiftKey && !e.ctrlKey) {
+                SelectionManager.DeselectAll();
                 this.allLinks.length && LinkFollower.FollowLink(undefined, this.props.Document, this.props, e.altKey);
             } else {
                 if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 0e48dd93a..ee97e5e62 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1398,9 +1398,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             } else if (this._editorView) {
                 this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
             }
-            FormattedTextBox.DontSelectInitialText = false;
         }
-        selectOnLoad && this._editorView!.focus();
+        if (FormattedTextBox.DontSelectInitialText) setTimeout(() => selectOnLoad && this._editorView!.focus(), 0);
+        else selectOnLoad && this._editorView!.focus();
+        FormattedTextBox.DontSelectInitialText = false;
         // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
         if (this._editorView) {
             this._editorView.state.storedMarks = [
-- 
cgit v1.2.3-70-g09d2


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

---
 src/client/views/SidebarAnnos.tsx                  | 34 +++++++----
 src/client/views/linking/LinkMenuItem.tsx          |  1 +
 src/client/views/nodes/LinkAnchorBox.tsx           |  1 +
 src/client/views/nodes/LinkDocPreview.tsx          |  2 +
 .../views/nodes/formattedText/DashFieldView.scss   |  9 ++-
 .../views/nodes/formattedText/DashFieldView.tsx    | 65 ++++++++--------------
 .../views/nodes/formattedText/FormattedTextBox.tsx |  7 ++-
 .../formattedText/FormattedTextBoxComment.tsx      | 14 +++--
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 src/client/views/nodes/formattedText/marks_rts.ts  |  4 +-
 10 files changed, 71 insertions(+), 68 deletions(-)

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

diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 78480da03..c285b2e34 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -67,7 +67,6 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             _height: 50,
             _fitWidth: true,
             _autoHeight: true,
-            _isLinkButton: true,
             _fontSize: StrCast(Doc.UserDoc().fontSize),
             _fontFamily: StrCast(Doc.UserDoc().fontFamily),
         });
@@ -84,20 +83,33 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 return {
                     type: 'dashField',
                     attrs: { fieldKey: key, docid: '', hideKey: false, editable: true },
-                    marks: [
-                        { type: 'pFontSize', attrs: { fontSize: '12px' } },
-                        { type: 'pFontFamily', attrs: { family: 'Arial' } },
-                        { type: 'pFontColor', attrs: { color: 'black' } },
-                        { type: 'strong' },
-                        { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } },
-                    ],
+                    marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }],
                 };
             });
-        const textLines = [
+
+        const textLines: any = [
             {
                 type: 'paragraph',
                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-                content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false } }],
+                content: [
+                    {
+                        type: 'dashField',
+                        marks: [
+                            {
+                                type: 'linkAnchor',
+                                attrs: {
+                                    allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', noPreview: true, anchorId: `${target[Id]}` }],
+                                    location: 'add:right',
+                                    title: 'Anchored Selection',
+                                    docref: false,
+                                },
+                            },
+                            { type: 'pFontSize', attrs: { fontSize: '8px' } },
+                            { type: 'em' },
+                        ],
+                        attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false },
+                    },
+                ],
             },
             { type: 'paragraph', attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null } },
         ];
@@ -118,7 +130,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             ''
         );
         this.addDocument(target);
-        this._stackRef.current?.focusDocument(target, {});
+        setTimeout(() => this._stackRef.current?.focusDocument(target, {}));
         return target;
     };
     makeDocUnfiltered = (doc: Doc) => {
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index 3f9db2612..387e0e3d5 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -155,6 +155,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
                                 linkDoc: this.props.linkDoc,
                                 showHeader: false,
                                 location: [this._drag.current?.getBoundingClientRect().right ?? 100, this._drag.current?.getBoundingClientRect().top ?? e.clientY],
+                                noPreview: false,
                             })
                         }
                         onPointerDown={this.onLinkButtonDown}>
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index 5102eae51..d6cf79f87 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -144,6 +144,7 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
                         linkDoc: this.rootDoc,
                         showHeader: true,
                         location: [e.clientX, e.clientY + 20],
+                        noPreview: false,
                     })
                 }
                 onPointerDown={this.onPointerDown}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 27e79a83b..0e66214d1 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -26,6 +26,7 @@ interface LinkDocPreviewProps {
     location: number[];
     hrefs?: string[];
     showHeader?: boolean;
+    noPreview?: boolean;
 }
 @observer
 export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
@@ -111,6 +112,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
                                     this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget;
                                 }
                                 this._toolTipText = '';
+                                if (LinkDocPreview.LinkInfo?.noPreview) this.followLink();
                             }
                         })
                     );
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
index c36e6804b..f17579853 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.scss
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -1,3 +1,5 @@
+@import '../../global/globalCssVariables';
+
 .dashFieldView {
     position: relative;
     display: inline-flex;
@@ -22,7 +24,7 @@
         position: relative;
         display: inline-block;
         font-weight: normal;
-        background: rgba(0,0,0,0.1);
+        background: rgba(0, 0, 0, 0.1);
     }
     .dashFieldView-fieldSpan {
         min-width: 8px;
@@ -31,11 +33,12 @@
         padding-left: 2px;
         display: inline-block;
         background-color: rgba(155, 155, 155, 0.24);
-        font-weight: bold;
         span {
             min-width: 100%;
             display: inline-block;
         }
     }
 }
-        
\ No newline at end of file
+.ProseMirror-selectedNode {
+    outline: solid 1px $light-blue !important;
+}
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index f088b39d1..fbcf25215 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -16,21 +16,20 @@ import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
 import { Tooltip } from '@material-ui/core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
+import { NodeSelection } from 'prosemirror-state';
+import { FormattedTextBoxComment } from './FormattedTextBoxComment';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
     root: any;
 
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
-        const { boolVal, strVal } = DashFieldViewInternal.fieldContent(tbox.props.Document, tbox.rootDoc, node.attrs.fieldKey);
-
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
         this.dom.style.height = node.attrs.height;
-        this.dom.style.fontWeight = 'bold';
         this.dom.style.position = 'relative';
         this.dom.style.display = 'inline-block';
-        this.dom.textContent = node.attrs.fieldKey.startsWith('#') ? node.attrs.fieldKey : node.attrs.fieldKey + ' ' + strVal;
+        //this.dom.contentEditable = 'true';
         this.dom.onkeypress = function (e: any) {
             e.stopPropagation();
         };
@@ -45,12 +44,20 @@ export class DashFieldView {
         };
 
         this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(<DashFieldViewInternal fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />);
+        this.root.render(
+            <DashFieldViewInternal node={node} getPos={getPos} fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />
+        );
     }
     destroy() {
         //this.root.unmount();
     }
-    selectNode() {}
+    deselectNode() {
+        this.dom.classList.remove('ProseMirror-selectednode');
+    }
+    selectNode() {
+        this.dom.classList.add('ProseMirror-selectednode');
+        console.log('SELECT');
+    }
 }
 
 interface IDashFieldViewInternal {
@@ -61,6 +68,8 @@ interface IDashFieldViewInternal {
     width: number;
     height: number;
     editable: boolean;
+    node: any;
+    getPos: any;
 }
 
 @observer
@@ -127,7 +136,13 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
                             r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false));
                             r?.addEventListener(
                                 'pointerdown',
-                                action(e => e.stopPropagation())
+                                action(e => {
+                                    // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
+                                    // while (target && !target.dataset?.targethrefs) target = target.parentElement;
+                                    this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos()))));
+                                    // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
+                                    // e.stopPropagation();
+                                })
                             );
                         }}>
                         {strVal}
@@ -162,42 +177,6 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
     updateText = (nodeText: string, forceMatch: boolean) => {
         if (nodeText) {
             const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText;
-
-            // const json = {
-            //     doc: {
-            //         type: 'doc',
-            //         content: [
-            //             {
-            //                 type: 'paragraph',
-            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-            //                 content: [{ type: 'dashField', attrs: { fieldKey: 'text', docid: '7e02a420-8add-49b0-ad20-54680567575f', hideKey: true, editable: false }, marks: [{ type: 'strong' }] }],
-            //             },
-            //             {
-            //                 type: 'paragraph',
-            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-            //                 content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334077 } }] }],
-            //             },
-            //         ],
-            //     },
-            // };
-
-            // const json = {
-            //     doc: {
-            //         type: 'doc',
-            //         content: [
-            //             {
-            //                 type: 'paragraph',
-            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-            //                 content: [{ type: 'dashField', attrs: { fieldKey: 'hello', docid: '', hideKey: true, editable: true }, marks: [{ type: 'strong' }, { type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334006 } }] }],
-            //             },
-            //             {
-            //                 type: 'paragraph',
-            //                 attrs: { align: null, color: null, id: null, indent: null, inset: null, lineSpacing: null, paddingBottom: null, paddingTop: null },
-            //                 content: [{ type: 'text', marks: [{ type: 'user_mark', attrs: { userid: 'mart@bar.com', modified: 1667334088 } }] }],
-            //             },
-            //         ],
-            //     },
-            // };
             // look for a document whose id === the fieldKey being displayed.  If there's a match, then that document
             // holds the different enumerated values for the field in the titles of its collected documents.
             // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index e9f59f17d..4378c10f7 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -393,13 +393,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         const newAutoLinks = new Set<Doc>();
         const oldAutoLinks = DocListCast(this.props.Document.links).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;
             const t = this._editorView.state.selection.to;
             var tr = this._editorView.state.tr as any;
             const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
             tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
             DocListCast(Doc.MyPublishedDocs.data).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
-            tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
+            tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
             this._editorView?.dispatch(tr);
         }
         oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink);
@@ -1485,7 +1486,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         document.removeEventListener('pointermove', this.onSelectMove);
     };
     onPointerUp = (e: React.PointerEvent): void => {
-        if (!this._editorView?.state.selection.empty && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
+        if (!this._editorView?.state.selection.empty && !(this._editorView?.state.selection instanceof NodeSelection) && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
         if (!this._downEvent) return;
         this._downEvent = false;
         if (this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
@@ -1494,7 +1495,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
-            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc);
+            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.noPreview);
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index bdf59863b..e7ca26d5c 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -1,5 +1,5 @@
 import { Mark, ResolvedPos } from 'prosemirror-model';
-import { EditorState } from 'prosemirror-state';
+import { EditorState, NodeSelection } from 'prosemirror-state';
 import { EditorView } from 'prosemirror-view';
 import { Doc } from '../../../../fields/Doc';
 import { DocServer } from '../../../DocServer';
@@ -92,7 +92,7 @@ export class FormattedTextBoxComment {
         FormattedTextBoxComment.tooltip.style.display = '';
     }
 
-    static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '') {
+    static update(textBox: FormattedTextBox, view: EditorView, lastState?: EditorState, hrefs: string = '', linkDoc: string = '', noPreview: boolean = false) {
         FormattedTextBoxComment.textBox = textBox;
         if (hrefs || !lastState?.doc.eq(view.state.doc) || !lastState?.selection.eq(view.state.selection)) {
             FormattedTextBoxComment.setupPreview(
@@ -102,12 +102,13 @@ export class FormattedTextBoxComment {
                     ?.trim()
                     .split(' ')
                     .filter(h => h),
-                linkDoc
+                linkDoc,
+                noPreview
             );
         }
     }
 
-    static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string) {
+    static setupPreview(view: EditorView, textBox: FormattedTextBox, hrefs?: string[], linkDoc?: string, noPreview?: boolean) {
         const state = view.state;
         // this section checks to see if the insertion point is over text entered by a different user.  If so, it sets ths comment text to indicate the user and the modification date
         if (state.selection.$from) {
@@ -130,8 +131,8 @@ export class FormattedTextBoxComment {
         if (state.selection.$from && hrefs?.length) {
             const nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
             const naft = findEndOfMark(state.selection.$from, view, findLinkMark) || nbef;
-            nbef &&
-                naft &&
+            //nbef &&
+            naft &&
                 LinkDocPreview.SetLinkInfo({
                     docProps: textBox.props,
                     linkSrc: textBox.rootDoc,
@@ -139,6 +140,7 @@ export class FormattedTextBoxComment {
                     location: (pos => [pos.left, pos.top + 25])(view.coordsAtPos(state.selection.from - Math.max(0, nbef - 1))),
                     hrefs,
                     showHeader: true,
+                    noPreview,
                 });
         }
     }
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index dc5d8ada8..e3c67ad2e 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -274,7 +274,7 @@ export class RichTextRules {
                     const num = value.match(/^[0-9.]$/);
                     this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
                 }
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: true });
+                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false });
                 return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
             }),
 
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 00c41e187..4206a1006 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -75,6 +75,7 @@ export const marks: { [index: string]: MarkSpec } = {
             allAnchors: { default: [] as { href: string; title: string; anchorId: string }[] },
             location: { default: null },
             title: { default: null },
+            noPreview: { default: false },
             docref: { default: false }, // flags whether the linked text comes from a document within Dash.  If so, an attribution label is appended after the text
         },
         inclusive: false,
@@ -85,6 +86,7 @@ export const marks: { [index: string]: MarkSpec } = {
                     return {
                         location: dom.getAttribute('location'),
                         title: dom.getAttribute('title'),
+                        noPreview: dom.getAttribute('noPreview'),
                     };
                 },
             },
@@ -111,7 +113,7 @@ export const marks: { [index: string]: MarkSpec } = {
                       ['br'],
                   ]
                 : //node.attrs.allLinks.length === 1 ?
-                  ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, location: node.attrs.location, style: `text-decoration: underline` }, 0];
+                  ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, noPreview: node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0];
             // ["div", { class: "prosemirror-anchor" },
             //     ["span", { class: "prosemirror-linkBtn" },
             //         ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0],
-- 
cgit v1.2.3-70-g09d2


From 31a51e9dda07e48c88166bffbc8f1ad7166cd624 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Fri, 4 Nov 2022 14:34:46 -0400
Subject: more fixes to sidebar annotations of pdfs to improve link following.

---
 src/client/util/LinkFollower.ts                         | 17 +++++++++++++++--
 src/client/views/DocumentDecorations.tsx                |  2 ++
 src/client/views/SidebarAnnos.tsx                       |  3 ++-
 .../collectionFreeForm/CollectionFreeFormView.tsx       |  4 +++-
 .../views/nodes/formattedText/DashDocCommentView.tsx    |  9 +++++----
 src/client/views/nodes/formattedText/DashFieldView.tsx  |  3 ---
 .../views/nodes/formattedText/FormattedTextBox.tsx      |  2 +-
 src/client/views/nodes/formattedText/marks_rts.ts       |  2 +-
 8 files changed, 29 insertions(+), 13 deletions(-)

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

diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index f75ac24f5..ea0531fa2 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -1,9 +1,10 @@
-import { action, observable, observe } from 'mobx';
+import { action, observable, observe, runInAction } from 'mobx';
 import { computedFn } from 'mobx-utils';
 import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc';
 import { List } from '../../fields/List';
 import { ProxyField } from '../../fields/Proxy';
 import { BoolCast, Cast, StrCast } from '../../fields/Types';
+import { DocumentDecorations } from '../views/DocumentDecorations';
 import { LightboxView } from '../views/LightboxView';
 import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView';
 import { DocumentManager } from './DocumentManager';
@@ -59,7 +60,19 @@ export class LinkFollower {
                 docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget });
             }
         };
-        LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined);
+        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,
+            BoolCast(sourceDoc.followLinkZoom, zoom),
+            docViewProps.ContainingCollectionDoc,
+            action(() => {
+                batch.end();
+                DocumentDecorations.Instance.overrideBounds = false;
+            }),
+            altKey ? true : undefined
+        );
     };
 
     public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 47347284c..cec03c991 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -70,8 +70,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         );
     }
 
+    @observable overrideBounds = false;
     @computed
     get Bounds() {
+        if (this.overrideBounds) return { x: 0, y: 0, r: 0, b: 0 };
         const views = SelectionManager.Views();
         return views
             .filter(dv => dv.props.renderDepth > 0)
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index c285b2e34..ec68a6b36 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -98,9 +98,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                             {
                                 type: 'linkAnchor',
                                 attrs: {
-                                    allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', noPreview: true, anchorId: `${target[Id]}` }],
+                                    allAnchors: [{ href: `/doc/${target[Id]}`, title: 'Anchored Selection', anchorId: `${target[Id]}` }],
                                     location: 'add:right',
                                     title: 'Anchored Selection',
+                                    noPreview: true,
                                     docref: false,
                                 },
                             },
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 2f246e74f..ac3777aa6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1229,6 +1229,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
         const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
         const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
+        const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top));
+        const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0;
         if (screen.right - screen.left < bounds.right - bounds.left || screen.bot - screen.top < bounds.bot - bounds.top) {
             return {
                 panX: (bounds.left + bounds.right) / 2,
@@ -1238,7 +1240,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         return {
             panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
-            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - ph / 10 - screen.top) + Math.max(0, bounds.bot + ph / 10 - screen.bot),
+            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
         };
     };
 
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index 85523c176..4bff57842 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -36,11 +36,12 @@ export class DashDocCommentView {
         (this as any).dom = this.dom;
     }
 
-    destroy() {
-        // ReactDOM.unmountComponentAtNode(this.dom);
+    deselectNode() {
+        this.dom.classList.remove('ProseMirror-selectednode');
+    }
+    selectNode() {
+        this.dom.classList.add('ProseMirror-selectednode');
     }
-
-    selectNode() {}
 }
 
 interface IDashDocCommentViewInternal {
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index fbcf25215..afae59733 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -17,7 +17,6 @@ import { Tooltip } from '@material-ui/core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { NodeSelection } from 'prosemirror-state';
-import { FormattedTextBoxComment } from './FormattedTextBoxComment';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
@@ -29,7 +28,6 @@ export class DashFieldView {
         this.dom.style.height = node.attrs.height;
         this.dom.style.position = 'relative';
         this.dom.style.display = 'inline-block';
-        //this.dom.contentEditable = 'true';
         this.dom.onkeypress = function (e: any) {
             e.stopPropagation();
         };
@@ -56,7 +54,6 @@ export class DashFieldView {
     }
     selectNode() {
         this.dom.classList.add('ProseMirror-selectednode');
-        console.log('SELECT');
     }
 }
 
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 4378c10f7..63435eea8 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -1495,7 +1495,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
-            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.noPreview);
+            FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview);
         }
 
         if (e.button === 0 && this.props.isSelected(true) && !e.altKey) {
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
index 4206a1006..97549830c 100644
--- a/src/client/views/nodes/formattedText/marks_rts.ts
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -113,7 +113,7 @@ export const marks: { [index: string]: MarkSpec } = {
                       ['br'],
                   ]
                 : //node.attrs.allLinks.length === 1 ?
-                  ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, noPreview: node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0];
+                  ['a', { class: anchorids, 'data-targethrefs': targethrefs, title: node.attrs.title, 'data-noPreview': node.attrs.noPreview, location: node.attrs.location, style: `text-decoration: underline` }, 0];
             // ["div", { class: "prosemirror-anchor" },
             //     ["span", { class: "prosemirror-linkBtn" },
             //         ["a", { ...node.attrs, class: linkids, "data-targetids": targetids, title: `${node.attrs.title}` }, 0],
-- 
cgit v1.2.3-70-g09d2


From ae8e487fbf7a5e662a04c14e805041bbdf49c87d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 30 Nov 2022 12:58:47 -0500
Subject: show link count for isLinkButton docs, don't show link links for
 sidebaranno links by default.

---
 src/client/views/SidebarAnnos.tsx       | 3 ++-
 src/client/views/StyleProvider.tsx      | 2 +-
 src/client/views/nodes/DocumentView.tsx | 5 ++---
 3 files changed, 5 insertions(+), 5 deletions(-)

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

diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index ec68a6b36..6d06bbbf6 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -72,7 +72,8 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         });
         FormattedTextBox.SelectOnLoad = target[Id];
         FormattedTextBox.DontSelectInitialText = true;
-        DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+        const link = DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+        link && (link.linkDisplay = false);
 
         const taggedContent = this.docFilters()
             .filter(data => data.split(':')[0])
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 8ee5ebdb6..dc7a4e047 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -129,7 +129,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
         case StyleProp.Opacity:
             return Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null));
         case StyleProp.HideLinkButton:
-            return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
+            return props?.hideLinkButton || (!selected && doc?.hideLinkButton);
         case StyleProp.FontSize:
             return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize)));
         case StyleProp.FontFamily:
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index a8bea61c9..48e32c071 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1535,9 +1535,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     linkButtonInverseScaling = () => (this.props.NativeDimScaling?.() || 1) * this.screenToLocalTransform().Scale;
 
     @computed get linkCountView() {
-        return this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton ? null : (
-            <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />
-        );
+        const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton;
+        return hideCount ? null : <DocumentLinksButton View={this} scaling={this.linkButtonInverseScaling} OnHover={true} Bottom={this.topMost} ShowCount={true} />;
     }
 
     @computed get docViewPath(): DocumentView[] {
-- 
cgit v1.2.3-70-g09d2


From f303131b40fac4909ab2730b369ef3bdb8e00b1d Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 21 Feb 2023 09:53:18 -0500
Subject: fixed explore mode zooming.  don't show doc decorations until you
 move outside of the document, fixed dragging radius button over webbox to
 still get pointer events. fixed selection text in coments for web boxes..
 fixed setting default background color for notes.  fixed mode buttons to
 trigger click behavior before double click behavior.  fixed events on nested
 text boxes that are linkAnchors (like text quotes in sidebar comments),

---
 src/Utils.ts                                       |  9 +++---
 src/client/documents/Documents.ts                  |  5 ++--
 src/client/util/CurrentUserUtils.ts                |  7 +++--
 src/client/views/DocumentDecorations.tsx           | 20 +++++++------
 src/client/views/Main.tsx                          |  5 ++--
 src/client/views/MarqueeAnnotator.tsx              |  2 +-
 src/client/views/SidebarAnnos.tsx                  | 25 ++++++++--------
 src/client/views/StyleProvider.tsx                 |  3 +-
 .../views/collections/CollectionStackingView.tsx   |  8 ++++--
 .../views/collections/CollectionTreeView.tsx       |  5 +++-
 src/client/views/collections/TreeView.tsx          | 33 +++++++++++++++++-----
 .../collectionFreeForm/CollectionFreeFormView.tsx  |  6 +++-
 src/client/views/nodes/DocumentView.tsx            |  6 ++--
 src/client/views/nodes/ImageBox.tsx                |  4 +--
 src/client/views/nodes/WebBox.tsx                  | 11 ++++++--
 src/client/views/nodes/button/FontIconBox.tsx      | 15 +++-------
 .../views/nodes/formattedText/DashFieldView.tsx    | 22 +++++++++++++--
 .../views/nodes/formattedText/FormattedTextBox.tsx | 22 ++++++---------
 .../views/nodes/formattedText/RichTextMenu.tsx     | 21 ++++++++------
 src/client/views/topbar/TopBar.tsx                 |  2 +-
 20 files changed, 145 insertions(+), 86 deletions(-)

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

diff --git a/src/Utils.ts b/src/Utils.ts
index 9d3b9eb2b..22c8cb902 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -686,8 +686,9 @@ export function DashColor(color: string) {
     try {
         return color ? Color(color.toLowerCase()) : Color('transparent');
     } catch (e) {
-        console.log('COLOR error:', e);
-        return Color('red');
+        if (color.includes('gradient')) console.log("using color 'white' in place of :" + color);
+        else console.log('COLOR error:', e);
+        return Color('white');
     }
 }
 
@@ -816,7 +817,7 @@ export function setupMoveUpEvents(
             (target as any)._noClick = clickEvent(e, (target as any)._doubleTap);
         }
         document.removeEventListener('pointermove', _moveEvent);
-        document.removeEventListener('pointerup', _upEvent);
+        document.removeEventListener('pointerup', _upEvent, true);
     };
     const _clickEvent = (e: MouseEvent): void => {
         if ((target as any)._noClick) e.stopPropagation();
@@ -827,6 +828,6 @@ export function setupMoveUpEvents(
         e.preventDefault();
     }
     document.addEventListener('pointermove', _moveEvent);
-    document.addEventListener('pointerup', _upEvent);
+    document.addEventListener('pointerup', _upEvent, true);
     document.addEventListener('click', _clickEvent, true);
 }
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 3faa6e11d..debb11066 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -224,6 +224,7 @@ export class DocumentOptions {
     contextMenuScripts?: List<ScriptField>;
     contextMenuLabels?: List<string>;
     contextMenuIcons?: List<string>;
+    allowClickBeforeDoubleClick?: boolean; // whether a click function can fire before the timeout for a double click has expired
     dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel)
     description?: string; // added for links
     layout?: string | Doc; // default layout string for a document
@@ -583,7 +584,7 @@ export namespace Docs {
                 DocumentType.FONTICON,
                 {
                     layout: { view: FontIconBox, dataField: defaultDataKey },
-                    options: { hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
+                    options: { allowClickBeforeDoubleClick: true, hideLinkButton: true, _width: 40, _height: 40, borderRounding: '100%', links: '@links(self)' },
                 },
             ],
             [
@@ -1808,7 +1809,7 @@ export namespace DocUtils {
             _yMargin: noMargins ? 0 : undefined,
             annotationOn,
             docMaxAutoHeight: maxHeight,
-            backgroundColor: backgroundColor,
+            backgroundColor,
             _width: width || 200,
             _height: 35,
             x: x,
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index f678c8936..3d95cb947 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -612,6 +612,7 @@ export class CurrentUserUtils {
               btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) },
             { title: "Size",    toolTip: "Font size", width: 75, btnType: ButtonType.NumberButton,               ignoreClick: true, scripts: {script:  '{ return setFontSize(value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions },
             { title: "Color",   toolTip: "Font color",           btnType: ButtonType.ColorButton,  icon: "font", ignoreClick: true, scripts: {script:  '{ return setFontColor(value, _readOnly_);}'}},
+            { title: "Highlight",toolTip:"Font highlight",       btnType: ButtonType.ColorButton,  icon: "highlighter", ignoreClick: true, scripts: {script:  '{ return setFontHighlight(value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} },
             { title: "Bold",    toolTip: "Bold (Ctrl+B)",        btnType: ButtonType.ToggleButton, icon: "bold",                    scripts: {onClick: '{ return toggleBold(_readOnly_); }'} },
             { title: "Italic",  toolTip: "Italic (Ctrl+I)",      btnType: ButtonType.ToggleButton, icon: "italic",                  scripts: {onClick: '{ return toggleItalic(_readOnly_);}'} },
             { title: "Under",   toolTip: "Underline (Ctrl+U)",   btnType: ButtonType.ToggleButton, icon: "underline",               scripts: {onClick: '{ return toggleUnderline(_readOnly_);}'} },
@@ -635,9 +636,9 @@ export class CurrentUserUtils {
             { title: "Write",  toolTip: "Write (Ctrl+Shift+P)",  btnType: ButtonType.ToggleButton, icon: "pen",                         scripts: {onClick:'{ return setActiveTool("write", false, _readOnly_);}'} },
             { title: "Eraser", toolTip: "Eraser (Ctrl+E)",       btnType: ButtonType.ToggleButton, icon: "eraser",                      scripts: {onClick:'{ return setActiveTool("eraser", false, _readOnly_);}' }},
             // { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", scripts:{onClick: 'setActiveTool("highlighter")'} },
-            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
-            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",                      scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
-            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",                       scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
+            { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle",           scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Circle}", true, _readOnly_);}`} },
+            { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square",           scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Rectangle}", true, _readOnly_);}`} },
+            { title: "Line",   toolTip: "Line (double tap to lock mode)",   btnType: ButtonType.ToggleButton, icon: "minus",            scripts: {onClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool("${GestureUtils.Gestures.Line}", true, _readOnly_);}`} },
             { title: "Mask",   toolTip: "Mask",                  btnType: ButtonType.ToggleButton, icon: "user-circle",                 scripts: {onClick:'{ return setIsInkMask(_readOnly_);}'} },
             { title: "Fill",   toolTip: "Fill color",            btnType: ButtonType.ColorButton,  icon: "fill-drip",ignoreClick: true, scripts: {script: '{ return setFillColor(value, _readOnly_);}'} },
             { title: "Width",  toolTip: "Stroke width",          btnType: ButtonType.NumberButton,                   ignoreClick: true, scripts: {script: '{ return setStrokeWidth(value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1},
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 41f4a17fb..21f63ada4 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { IconButton } from 'browndash-components';
-import { action, computed, observable, reaction } from 'mobx';
+import { action, computed, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { FaUndo } from 'react-icons/fa';
 import { DateField } from '../../fields/DateField';
@@ -182,7 +182,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         dragData.removeDocument = dragDocView.props.removeDocument;
         dragData.isDocDecorationMove = true;
         dragData.canEmbed = dragTitle;
-        this._hidden = this.Interacting = true;
+        this._hidden = true;
         DragManager.StartDocumentDrag(
             SelectionManager.Views().map(dv => dv.ContentDiv!),
             dragData,
@@ -191,7 +191,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             {
                 dragComplete: action(e => {
                     dragData.canEmbed && SelectionManager.DeselectAll();
-                    this._hidden = this.Interacting = false;
+                    this._hidden = false;
                 }),
                 hideSource: true,
             }
@@ -293,7 +293,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
      */
     @action
     onRadiusDown = (e: React.PointerEvent): void => {
-        this._isRounding = true;
+        this._isRounding = DocumentDecorations.Instance.Interacting = true;
         this._resizeUndo = UndoManager.StartBatch('DocDecs set radius');
         // Call util move event function
         setupMoveUpEvents(
@@ -316,10 +316,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                 return false;
             }, // moveEvent
             action(e => {
-                this._isRounding = false;
+                DocumentDecorations.Instance.Interacting = this._isRounding = false;
                 this._resizeUndo?.end();
             }), // upEvent
-            e => {} // clickEvent
+            e => {}, // clickEvent,
+            true
         );
     };
 
@@ -710,11 +711,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         return this._rotCenter;
     }
 
+    @observable _showNothing = true;
+
     render() {
         const { b, r, x, y } = this.Bounds;
         const bounds = { b, r, x, y };
-        const seldocview = SelectionManager.Views().slice(-1)[0];
+        const seldocview = SelectionManager.Views().lastElement();
         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)) {
+            setTimeout(action(() => (this._showNothing = true)));
             return null;
         }
         // hide the decorations if the parent chooses to hide it or if the document itself hides it
@@ -812,7 +816,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
             </div>
         );
         return (
-            <div className={`documentDecorations${colorScheme}`}>
+            <div className={`documentDecorations${colorScheme}`} style={{ opacity: this._showNothing ? 0.1 : undefined }} onPointerOver={action(e => (this._showNothing = false))}>
                 <div
                     className="documentDecorations-background"
                     style={{
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index e0b4c5159..6b18caed0 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -22,7 +22,8 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
 
 (async () => {
     MainView.Live = window.location.search.includes('live');
-    ReactDOM.createRoot(document.getElementById('root')!).render(<FieldLoader />);
+    const root = ReactDOM.createRoot(document.getElementById('root')!);
+    root.render(<FieldLoader />);
     window.location.search.includes('safe') && CollectionView.SetSafeMode(true);
     const info = await CurrentUserUtils.loadCurrentUser();
     if (info.email === 'guest') DocServer.Control.makeReadOnly();
@@ -47,6 +48,6 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0 }; // bcz: not sure
         new LinkManager();
         new TrackMovements();
         new ReplayMovements();
-        ReactDOM.createRoot(document.getElementById('root')!).render(<MainView />);
+        root.render(<MainView />);
     }, 0);
 })();
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 5ab91dd70..3bdf65d01 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -49,7 +49,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> {
         super(props);
 
         AnchorMenu.Instance.OnCrop = (e: PointerEvent) => this.props.anchorMenuCrop?.(this.highlight('', true), true);
-        AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true));
+        AnchorMenu.Instance.OnClick = (e: PointerEvent) => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true));
         AnchorMenu.Instance.OnAudio = unimplementedFunction;
         AnchorMenu.Instance.Highlight = this.highlight;
         AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 6d06bbbf6..b9af28413 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -88,6 +88,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 };
             });
 
+        if (!anchor.text) Doc.GetProto(anchor).text = '-selection-';
         const textLines: any = [
             {
                 type: 'paragraph',
@@ -121,16 +122,18 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             content: taggedContent,
         };
         if (taggedContent.length) textLines.push(metadatatext);
-        Doc.GetProto(target).text = new RichTextField(
-            JSON.stringify({
-                doc: {
-                    type: 'doc',
-                    content: textLines,
-                },
-                selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
-            }),
-            ''
-        );
+        if (textLines.length) {
+            Doc.GetProto(target).text = new RichTextField(
+                JSON.stringify({
+                    doc: {
+                        type: 'doc',
+                        content: textLines,
+                    },
+                    selection: { type: 'text', anchor: 4, head: 4 }, // set selection to middle paragraph
+                }),
+                ''
+            );
+        }
         this.addDocument(target);
         setTimeout(() => this._stackRef.current?.focusDocument(target, {}));
         return target;
@@ -233,7 +236,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                         isAnnotationOverlay={false}
                         select={emptyFunction}
                         NativeDimScaling={returnOne}
-                        childShowTitle={this.showTitle}
+                        //childShowTitle={this.showTitle}
                         childDocumentsActive={this.props.isContentActive}
                         whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                         childHideDecorationTitle={returnTrue}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 3cb920ba0..817baeae6 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -19,6 +19,7 @@ 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',
@@ -116,7 +117,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
                 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]') {
-                    return { highlightStyle, highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK };
+                    return { highlightStyle, highlightColor: SelectionManager.Views().some(dv => dv.rootDoc === doc) ? 'black' : highlightColor, highlightIndex, highlightStroke: doc.type === DocumentType.INK };
                 }
             }
             return undefined;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 2f495d55c..6314b4529 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -129,6 +129,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         TraceMobx();
         // appears that we are going to reset the _docXfs. TODO: what is Xfs?
         this._docXfs.length = 0;
+        this._renderCount < docs.length && setTimeout(action(() => (this._renderCount = Math.min(docs.length, this._renderCount + 5))));
         return docs.map((d, i) => {
             const height = () => this.getDocHeight(d);
             const width = () => this.getDocWidth(d);
@@ -139,7 +140,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
             // So we're choosing whether we're going to render a column or a masonry doc
             return (
                 <div className={`collectionStackingView-${this.isStackingView ? 'columnDoc' : 'masonryDoc'}`} key={d[Id]} style={style}>
-                    {this.getDisplayDoc(d, width)}
+                    {this.getDisplayDoc(d, width, i)}
                 </div>
             );
         });
@@ -297,12 +298,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     };
     isContentActive = () => (this.props.isSelected() || this.props.isContentActive() ? true : this.props.isSelected() === false || this.props.isContentActive() === false ? false : undefined);
 
+    @observable _renderCount = 5;
     isChildContentActive = () =>
         this.props.isDocumentActive?.() && (this.props.childDocumentsActive?.() || BoolCast(this.rootDoc.childDocumentsActive)) ? true : this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined;
     isChildButtonContentActive = () => (this.props.childDocumentsActive?.() === false || this.rootDoc.childDocumentsActive === false ? false : undefined);
     // this is what renders the document that you see on the screen
     // called in Children: this actually adds a document to our children list
-    getDisplayDoc(doc: Doc, width: () => number) {
+    getDisplayDoc(doc: Doc, width: () => number, count: number) {
         const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField && !doc.PARAMS ? undefined : this.props.DataDoc;
         const height = () => this.getDocHeight(doc);
 
@@ -310,7 +312,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         const stackedDocTransform = () => this.getDocTransform(doc, dref);
         this._docXfs.push({ stackedDocTransform, width, height });
         //DocumentView is how the node will be rendered
-        return (
+        return count > this._renderCount ? null : (
             <DocumentView
                 ref={r => (dref = r || undefined)}
                 Document={doc}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 1a265af4a..456f2a13d 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -259,11 +259,13 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         return StrListCast(this.doc.childContextMenuLabels).map((label, i) => ({ script: customScripts[i], filter: customFilters[i], icon: icons[i], label }));
     };
     headerFields = () => this.props.treeViewHideHeaderFields || BoolCast(this.doc.treeViewHideHeaderFields);
+    @observable _renderCount = 1;
     @computed get treeViewElements() {
         TraceMobx();
         const dropAction = StrCast(this.doc.childDropAction) as dropActionType;
         const addDoc = (doc: Doc | Doc[], relativeTo?: Doc, before?: boolean) => this.addDoc(doc, relativeTo, before);
         const moveDoc = (d: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument?.(d, target, addDoc) || false;
+        if (this.treeChildren.length < this._renderCount) setTimeout(action(() => (this._renderCount = Math.min(this.treeChildren.length, this._renderCount + 20))));
         return TreeView.GetChildElements(
             this.treeChildren,
             this,
@@ -296,7 +298,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             //TODO: [AL] add these
             this.props.AddToMap,
             this.props.RemFromMap,
-            this.props.hierarchyIndex
+            this.props.hierarchyIndex,
+            this._renderCount
         );
     }
     @computed get titleBar() {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 2398d8f58..0d2968ba1 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -32,6 +32,7 @@ import { CollectionTreeView, TreeViewType } from './CollectionTreeView';
 import { CollectionView } from './CollectionView';
 import './TreeView.scss';
 import React = require('react');
+import { render } from 'react-dom';
 
 export interface TreeViewProps {
     treeView: CollectionTreeView;
@@ -95,7 +96,7 @@ export class TreeView extends React.Component<TreeViewProps> {
     private _header: React.RefObject<HTMLDivElement> = React.createRef();
     private _tref = React.createRef<HTMLDivElement>();
     @observable _docRef: Opt<DocumentView>;
-    private _selDisposer: Opt<IReactionDisposer>;
+    private _disposers: { [name: string]: IReactionDisposer } = {};
     private _editTitleScript: (() => ScriptField) | undefined;
     private _openScript: (() => ScriptField) | undefined;
     private _treedropDisposer?: DragManager.DragDropDisposer;
@@ -212,14 +213,14 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
 
     @action setEditTitle = (docView?: DocumentView) => {
-        this._selDisposer?.();
+        this._disposers.selection?.();
         if (!docView) {
             this._editTitle = false;
         } else if (docView.isSelected()) {
             const doc = docView.Document;
             SelectionManager.SelectSchemaViewDoc(doc);
             this._editTitle = true;
-            this._selDisposer = reaction(
+            this._disposers.selection = reaction(
                 () => SelectionManager.SelectedSchemaDoc(),
                 seldoc => seldoc !== doc && this.setEditTitle(undefined)
             );
@@ -259,7 +260,8 @@ export class TreeView extends React.Component<TreeViewProps> {
     };
 
     componentWillUnmount() {
-        this._selDisposer?.();
+        this._renderTimer && clearTimeout(this._renderTimer);
+        Object.values(this._disposers).forEach(disposer => disposer?.());
         this._treeEle && this.props.unobserveHeight(this._treeEle);
         document.removeEventListener('pointermove', this.onDragMove, true);
         document.removeEventListener('pointermove', this.onDragUp, true);
@@ -268,6 +270,10 @@ export class TreeView extends React.Component<TreeViewProps> {
     }
 
     componentDidUpdate() {
+        this._disposers.opening = reaction(
+            () => this.treeViewOpen,
+            open => !open && (this._renderCount = 20)
+        );
         this.props.hierarchyIndex !== undefined && this.props.AddToMap?.(this.doc, this.props.hierarchyIndex);
     }
 
@@ -512,6 +518,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         return rows;
     }
 
+    _renderTimer: any;
+    @observable _renderCount = 1;
     @computed get renderContent() {
         TraceMobx();
         const expandKey = this.treeViewExpandedView;
@@ -543,6 +551,14 @@ export class TreeView extends React.Component<TreeViewProps> {
             const docs = expandKey === 'aliases' ? this.childAliases : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs;
             let downX = 0,
                 downY = 0;
+            if (docs?.length && this._renderCount < docs?.length) {
+                this._renderTimer && clearTimeout(this._renderTimer);
+                this._renderTimer = setTimeout(
+                    action(() => {
+                        this._renderCount = Math.min(docs!.length, this._renderCount + 20);
+                    })
+                );
+            }
             return (
                 <>
                     {!docs?.length || this.props.AddToMap /* hack to identify pres box trees */ ? null : (
@@ -599,7 +615,8 @@ export class TreeView extends React.Component<TreeViewProps> {
                                   // TODO: [AL] add these
                                   this.props.AddToMap,
                                   this.props.RemFromMap,
-                                  this.props.hierarchyIndex
+                                  this.props.hierarchyIndex,
+                                  this._renderCount
                               )}
                     </ul>
                 </>
@@ -651,7 +668,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         return (
             <div
                 className={`bullet${this.props.treeView.outlineMode ? '-outline' : ''}`}
-                key={'bullet'}
+                key="bullet"
                 title={this.childDocs?.length ? `click to see ${this.childDocs?.length} items` : 'view fields'}
                 onClick={this.bulletClick}
                 style={
@@ -1131,7 +1148,8 @@ export class TreeView extends React.Component<TreeViewProps> {
         // TODO: [AL] add these
         AddToMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
         RemFromMap?: (treeViewDoc: Doc, index: number[]) => Doc[],
-        hierarchyIndex?: number[]
+        hierarchyIndex?: number[],
+        renderCount?: number
     ) {
         const viewSpecScript = Cast(containerCollection.viewSpecScript, ScriptField);
         if (viewSpecScript) {
@@ -1144,6 +1162,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         return docs
             .filter(child => child instanceof Doc)
             .map((child, i) => {
+                if (renderCount && i > renderCount) return null;
                 const pair = Doc.GetLayoutDataDocPair(containerCollection, dataDoc, child);
                 if (!pair.layout || pair.data instanceof Promise) {
                     return null;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d6e95f97f..4d6e0dff2 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -53,6 +53,7 @@ import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCurso
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 import React = require('react');
+import { DocumentDecorations } from '../../DocumentDecorations';
 
 export type collectionFreeformViewProps = {
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
@@ -1305,7 +1306,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     pointerEvents = () => {
         const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);
         const pointerEvents =
-            this.props.isContentActive() === false ? 'none' : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
+            this.props.isContentActive() === false || DocumentDecorations.Instance.Interacting
+                ? 'none'
+                : this.props.childPointerEvents ?? (this.props.viewDefDivClick || (engine === computePassLayout.name && !this.props.isSelected(true)) ? 'none' : this.props.pointerEvents?.());
         return pointerEvents;
     };
     getChildDocView(entry: PoolData) {
@@ -2287,6 +2290,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
     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;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 36c0240f1..3d89566ee 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -664,7 +664,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                         ? this.props.select(false)
                         : '';
                 const clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click'));
-                if (this.onDoubleClickHandler) {
+                if (this.onDoubleClickHandler && !this.props.Document.allowClickBeforeDoubleClick) {
                     runInAction(() => (this._pendingDoubleClick = true));
                     this._timeout = setTimeout(() => {
                         this._timeout = undefined;
@@ -780,7 +780,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         if (this.onPointerUpHandler?.script && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
             this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log);
         } else {
-            this._doubleTap = Date.now() - this._lastTap < 300 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
+            const now = Date.now();
+            console.log(now - this._lastTap);
+            this._doubleTap = now - this._lastTap < 600 && e.button === 0 && Math.abs(e.clientX - this._downX) < 2 && Math.abs(e.clientY - this._downY) < 2;
             // bcz: this is a placeholder.  documents, when selected, should stopPropagation on doubleClicks if they want to keep the DocumentView from getting them
             if (!this.props.isSelected(true) || ![DocumentType.PDF, DocumentType.RTF].includes(StrCast(this.rootDoc.type) as any)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected
         }
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 540958941..c66f2a67b 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -383,7 +383,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
     @computed get content() {
         TraceMobx();
 
-        const col = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor));
+        const backAlpha = DashColor(this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor)).alpha();
         const srcpath = this.layoutDoc.hideImage ? '' : this.paths[0];
         const fadepath = this.layoutDoc.hideImage ? '' : this.paths.lastElement();
         const { nativeWidth, nativeHeight, nativeOrientation } = this.nativeSize;
@@ -403,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() }}>
+                <div className="imageBox-fader" style={{ opacity: backAlpha }}>
                     <img key="paths" src={srcpath} style={{ transform, transformOrigin }} draggable={false} width={nativeWidth} />
                     {fadepath === srcpath ? null : (
                         <div
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index c392f3577..5bbe521a7 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -257,6 +257,10 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         // this._iframe?.contentDocument?.removeEventListener("pointerup", this.iframeUp);
     }
 
+    private _selectionText: string = '';
+    private _selectionContent: DocumentFragment | undefined;
+    selectionText = () => this._selectionText;
+    selectionContent = () => this._selectionContent;
     @action
     createTextAnnotation = (sel: Selection, selRange: Range | undefined) => {
         if (this._mainCont.current && selRange) {
@@ -276,6 +280,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             }
         }
         //this._selectionText = selRange.cloneContents().textContent || "";
+        this._selectionContent = selRange?.cloneContents();
+        this._selectionText = this._selectionContent?.textContent || '';
 
         // clear selection
         if (sel.empty) sel.empty(); // Chrome
@@ -854,8 +860,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
     };
     @computed get content() {
-        const interactive =
-            !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None && !DocumentDecorations.Instance?.Interacting;
+        const interactive = !this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None;
         return (
             <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}>
                 {this.urlContent}
@@ -1014,7 +1019,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 docView={this.props.docViewPath().lastElement()}
                                 finishMarquee={this.finishMarquee}
                                 savedAnnotations={this.savedAnnotationsCreator}
-                                selectionText={returnEmptyString}
+                                selectionText={this.selectionText}
                                 annotationLayer={this._annotationLayer.current}
                                 mainCont={this._mainCont.current}
                             />
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 9d67283a0..b352f3790 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -136,7 +136,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
         const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ value, _readOnly_: false }), 'set num value');
 
         // Script for checking the outcome of the toggle
-        const checkResult: number = numScript?.script.run({ value: 0, _readOnly_: true }).result || 0;
+        const checkResult = Number(numScript?.script.run({ value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3));
 
         const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>;
 
@@ -150,7 +150,6 @@ export class FontIconBox extends DocComponent<ButtonProps>() {
                         max={NumCast(this.rootDoc.numBtnMax, 100)}
                         value={checkResult}
                         className={'menu-slider'}
-                        id="slider"
                         onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))}
                         onPointerUp={() => this._batch?.end()}
                         onChange={e => {
@@ -642,13 +641,7 @@ ScriptingGlobals.add(function setFontHighlight(color?: string, checkResult?: boo
     if (checkResult) {
         return (selected ?? Doc.UserDoc())._fontHighlight;
     }
-    if (selected) {
-        selected._fontColor = color;
-        if (color) {
-            editorView?.state && RichTextMenu.Instance.setHighlight(color, editorView, editorView?.dispatch);
-        }
-    }
-    Doc.UserDoc()._fontHighlight = color;
+    color && RichTextMenu.Instance.setHighlight(color);
 });
 
 // toggle: Set overlay status of selected document
@@ -795,7 +788,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
             GestureOverlay.Instance.KeepPrimitiveMode = keepPrim;
         }
         if (Object.values(GestureUtils.Gestures).includes(tool as any)) {
-            if (GestureOverlay.Instance.InkShape === tool) {
+            if (GestureOverlay.Instance.InkShape === tool && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
                 GestureOverlay.Instance.InkShape = undefined;
             } else {
@@ -804,7 +797,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean,
             }
         } else if (tool) {
             // pen or eraser
-            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape) {
+            if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) {
                 Doc.ActiveTool = InkTool.None;
             } else {
                 Doc.ActiveTool = tool as any;
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index 39005a18b..b33529aeb 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -18,12 +18,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CollectionViewType } from '../../../documents/DocumentTypes';
 import { NodeSelection } from 'prosemirror-state';
 import { OpenWhere } from '../DocumentView';
+import { FormattedTextBoxComment } from './FormattedTextBoxComment';
 
 export class DashFieldView {
     dom: HTMLDivElement; // container for label and value
     root: any;
+    node: any;
+    tbox: FormattedTextBox;
 
+    unclickable = () => !this.tbox.props.isSelected() && this.node.marks.some((m: any) => m.type === this.tbox.EditorView?.state.schema.marks.linkAnchor && m.attrs.noPreview);
     constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+        this.node = node;
+        this.tbox = tbox;
         this.dom = document.createElement('div');
         this.dom.style.width = node.attrs.width;
         this.dom.style.height = node.attrs.height;
@@ -44,7 +50,18 @@ export class DashFieldView {
 
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
-            <DashFieldViewInternal node={node} getPos={getPos} fieldKey={node.attrs.fieldKey} docid={node.attrs.docid} width={node.attrs.width} height={node.attrs.height} hideKey={node.attrs.hideKey} editable={node.attrs.editable} tbox={tbox} />
+            <DashFieldViewInternal
+                node={node}
+                unclickable={this.unclickable}
+                getPos={getPos}
+                fieldKey={node.attrs.fieldKey}
+                docid={node.attrs.docid}
+                width={node.attrs.width}
+                height={node.attrs.height}
+                hideKey={node.attrs.hideKey}
+                editable={node.attrs.editable}
+                tbox={tbox}
+            />
         );
     }
     destroy() {
@@ -68,6 +85,7 @@ interface IDashFieldViewInternal {
     editable: boolean;
     node: any;
     getPos: any;
+    unclickable: () => boolean;
 }
 
 @observer
@@ -125,7 +143,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
                 return (
                     <span
                         className="dashFieldView-fieldSpan"
-                        contentEditable={true}
+                        contentEditable={!this.props.unclickable()}
                         style={{ display: strVal.length < 2 ? 'inline-block' : undefined }}
                         suppressContentEditableWarning={true}
                         defaultValue={strVal}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 49acd8a57..0ff09a0ca 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -81,7 +81,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     static _highlightStyleSheet: any = addStyleSheet();
     static _bulletStyleSheet: any = addStyleSheet();
     static _userStyleSheet: any = addStyleSheet();
-    static _canAnnotate = true;
     static _hadSelection: boolean = false;
     private _sidebarRef = React.createRef<SidebarAnnos>();
     private _ref: React.RefObject<HTMLDivElement> = React.createRef();
@@ -102,7 +101,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     private _rules: RichTextRules | undefined;
     private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason.  This flags the 2nd click as happening on a selection which allows bullet points to toggle
     private _forceDownNode: Node | undefined;
-    private _downEvent: any;
     private _downX = 0;
     private _downY = 0;
     private _break = true;
@@ -118,7 +116,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     }
 
     @computed get noSidebar() {
-        return this.props.docViewPath?.()[this.props.docViewPath().length - 2]?.rootDoc.type === DocumentType.RTF || this.props.noSidebar || this.Document._noSidebar;
+        return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._noSidebar;
     }
     @computed get sidebarWidthPercent() {
         return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%');
@@ -254,7 +252,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             });
         };
         AnchorMenu.Instance.Highlight = action((color: string, isLinkButton: boolean) => {
-            this._editorView?.state && RichTextMenu.Instance.setHighlight(color, this._editorView, this._editorView?.dispatch);
+            this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
             return undefined;
         });
         AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true);
@@ -788,7 +786,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         );
 
         const uicontrols: ContextMenuProps[] = [];
-        !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ''} Show Menu on Selections`, event: () => (FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate), icon: 'expand-arrows-alt' });
         uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: 'expand-arrows-alt' });
         uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' });
         !Doc.noviceMode &&
@@ -1455,7 +1452,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         }
         this._downX = e.clientX;
         this._downY = e.clientY;
-        this._downEvent = true;
         FormattedTextBoxComment.textBox = this;
         if (e.button === 0 && (this.props.rootSelected(true) || this.props.isSelected(true)) && !e.altKey && !e.ctrlKey && !e.metaKey) {
             if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
@@ -1477,17 +1473,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         document.removeEventListener('pointermove', this.onSelectMove);
     };
     onPointerUp = (e: React.PointerEvent): void => {
-        if (!this._editorView?.state.selection.empty && !(this._editorView?.state.selection instanceof NodeSelection) && FormattedTextBox._canAnnotate && !(e.nativeEvent as any).dash) this.setupAnchorMenu();
-        if (!this._downEvent) return;
-        this._downEvent = false;
-        if (this._editorView?.state.selection.empty && this.props.isContentActive(true) && !(e.nativeEvent as any).dash) {
-            const editor = this._editorView!;
+        const editor = this._editorView!;
+        const state = editor?.state;
+        if (!state || !editor) return;
+        if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
+        else if (this.props.isContentActive(true)) {
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
-            !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0))));
+            !this.props.isSelected(true) && editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0))));
             let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
             while (target && !target.dataset?.targethrefs) target = target.parentElement;
             FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
-            if (pcords && pcords.inside > 0 && this._editorView.state.doc.nodeAt(pcords.inside)?.type === this._editorView.state.schema.nodes.dashDoc) {
+            if (pcords && pcords.inside > 0 && state.doc.nodeAt(pcords.inside)?.type === state.schema.nodes.dashDoc) {
                 return;
             }
         }
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index b70da2e5e..f0caa1f4f 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -54,7 +54,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @observable private _activeFontColor: string = 'black';
     @observable private showColorDropdown: boolean = false;
 
-    @observable private activeHighlightColor: string = 'transparent';
+    @observable private _activeHighlightColor: string = 'transparent';
     @observable private showHighlightDropdown: boolean = false;
 
     @observable private currentLink: string | undefined = '';
@@ -89,6 +89,9 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     @computed get fontColor() {
         return this._activeFontColor;
     }
+    @computed get fontHighlight() {
+        return this._activeHighlightColor;
+    }
     @computed get fontFamily() {
         return this._activeFontFamily;
     }
@@ -138,7 +141,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various';
         this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0];
         this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...';
-        this.activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
+        this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...';
 
         // update link in current selection
         this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle));
@@ -356,11 +359,13 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
         this.updateMenu(this.view, undefined, this.props);
     };
 
-    setHighlight(color: String, view: EditorView, dispatch: any) {
-        const highlightMark = view.state.schema.mark(view.state.schema.marks.marker, { highlight: color });
-        if (view.state.selection.empty) return false;
-        view.focus();
-        this.setMark(highlightMark, view.state, dispatch, false);
+    setHighlight(color: string) {
+        if (this.view) {
+            const highlightMark = this.view.state.schema.mark(this.view.state.schema.marks.marker, { highlight: color });
+            this.setMark(highlightMark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(highlightMark)), true);
+            this.view.focus();
+        } else Doc.UserDoc()._fontHighlight = color;
+        this.updateMenu(this.view, undefined, this.props);
     }
 
     setColor(color: string) {
@@ -563,7 +568,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
     }
 
     @action setActiveHighlight(color: string) {
-        this.activeHighlightColor = color;
+        this._activeHighlightColor = color;
     }
 
     @action setCurrentLink(link: string) {
diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx
index 7e728306c..f2e9be61d 100644
--- a/src/client/views/topbar/TopBar.tsx
+++ b/src/client/views/topbar/TopBar.tsx
@@ -54,7 +54,7 @@ export class TopBar extends React.Component {
                         <span style={{ color: Colors.LIGHT_BLUE, fontWeight: 500 }}>dash</span>
                     </div>
                 )}
-                {Doc.ActiveDashboard && !Doc.noviceMode && (
+                {Doc.ActiveDashboard && (
                     <Button
                         text="Explore"
                         tooltip="Browsing mode for directly navigating to documents"
-- 
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/SidebarAnnos.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/SidebarAnnos.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 b25f333713706a7456ab418960082bcabe830f11 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 20 Mar 2023 20:48:38 -0400
Subject: fixed copying of docs with template layout docs.  fixed clone to
 search through RTFs for referenced documents to clone.  renamed nested
 documents docId instead of docid for consistency with other nested field ids.

---
 src/client/util/CurrentUserUtils.ts                |  6 +--
 src/client/views/GlobalKeyHandler.ts               |  2 +-
 src/client/views/SidebarAnnos.tsx                  |  4 +-
 src/client/views/collections/CollectionSubView.tsx | 10 ++---
 src/client/views/nodes/DocumentView.tsx            |  6 +--
 .../nodes/formattedText/DashDocCommentView.tsx     | 16 ++++----
 .../views/nodes/formattedText/DashDocView.tsx      | 16 ++++----
 .../views/nodes/formattedText/DashFieldView.tsx    |  8 ++--
 .../views/nodes/formattedText/FormattedTextBox.tsx |  8 ++--
 .../views/nodes/formattedText/RichTextRules.ts     | 26 ++++++------
 src/client/views/nodes/formattedText/nodes_rts.ts  |  8 ++--
 src/fields/Doc.ts                                  | 46 ++++++++++++++--------
 src/fields/RichTextField.ts                        | 26 ++++++------
 src/fields/RichTextUtils.ts                        | 10 ++---
 14 files changed, 103 insertions(+), 89 deletions(-)

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

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 2820c66ee..c038fd6ab 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -148,7 +148,7 @@ export class CurrentUserUtils {
             { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" },
             { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }];
         const reqdNoteList = reqdTempOpts.map(opts => {
-            const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true, system: true};
+            const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true};
             const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined;
             return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note");
         });
@@ -224,11 +224,11 @@ export class CurrentUserUtils {
                     {
                         type: "paragraph", attrs: {}, content: [{
                             type: "dashField",
-                            attrs: { fieldKey: "author", docid: "", hideKey: false },
+                            attrs: { fieldKey: "author", docId: "", hideKey: false },
                             marks: [{ type: "strong" }]
                         }, {
                             type: "dashField",
-                            attrs: { fieldKey: "creationDate", docid: "", hideKey: false },
+                            attrs: { fieldKey: "creationDate", docId: "", hideKey: false },
                             marks: [{ type: "strong" }]
                         }]
                     }]
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index f849b21e3..0f8b46dbe 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -372,7 +372,7 @@ export class KeyManager {
                                 list.push(doc);
                             }
                             if (count === docids.length) {
-                                const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, ['links'])).clone : d)));
+                                const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d)));
                                 if (added.length) {
                                     added.map(doc => (doc.context = targetDataDoc));
                                     undoBatch(() => {
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 7519cbb05..02dd15960 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -84,7 +84,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 Doc.GetProto(target)[key] = val;
                 return {
                     type: 'dashField',
-                    attrs: { fieldKey: key, docid: '', hideKey: false, editable: true },
+                    attrs: { fieldKey: key, docId: '', hideKey: false, editable: true },
                     marks: [{ type: 'pFontSize', attrs: { fontSize: '12px' } }, { type: 'strong' }, { type: 'user_mark', attrs: { userid: Doc.CurrentUserEmail, modified: 0 } }],
                 };
             });
@@ -111,7 +111,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                             { type: 'pFontSize', attrs: { fontSize: '8px' } },
                             { type: 'em' },
                         ],
-                        attrs: { fieldKey: 'text', docid: anchor[Id], hideKey: true, editable: false },
+                        attrs: { fieldKey: 'text', docId: anchor[Id], hideKey: true, editable: false },
                     },
                 ],
             },
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index e46220f02..bd74c9399 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -269,10 +269,10 @@ export function CollectionSubView<X>(moreProps?: X) {
                 if (FormattedTextBox.IsFragment(html)) {
                     const href = FormattedTextBox.GetHref(html);
                     if (href) {
-                        const docid = FormattedTextBox.GetDocFromUrl(href);
-                        if (docid) {
+                        const docId = FormattedTextBox.GetDocFromUrl(href);
+                        if (docId) {
                             // prosemirror text containing link to dash document
-                            DocServer.GetRefField(docid).then(f => {
+                            DocServer.GetRefField(docId).then(f => {
                                 if (f instanceof Doc) {
                                     if (options.x || options.y) {
                                         f.x = options.x as number;
@@ -311,8 +311,8 @@ export function CollectionSubView<X>(moreProps?: X) {
                     } else {
                         const path = window.location.origin + '/doc/';
                         if (text.startsWith(path)) {
-                            const docid = text.replace(Doc.globalServerPath(), '').split('?')[0];
-                            DocServer.GetRefField(docid).then(f => {
+                            const docId = text.replace(Doc.globalServerPath(), '').split('?')[0];
+                            DocServer.GetRefField(docId).then(f => {
                                 if (f instanceof Doc) {
                                     if (options.x || options.y) {
                                         f.x = options.x as number;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 36e8facf5..edf508c95 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -1578,8 +1578,8 @@ export class DocumentView extends React.Component<DocumentViewProps> {
     }
 
     public static showBackLinks(linkSource: Doc) {
-        const docid = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
-        DocServer.GetRefField(docid).then(docx => {
+        const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish';
+        DocServer.GetRefField(docId).then(docx => {
             const rootAlias = () => {
                 const rootAlias = Doc.MakeAlias(linkSource);
                 rootAlias.x = rootAlias.y = 0;
@@ -1592,7 +1592,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
                         /*rootAlias()*/
                     ],
                     { title: linkSource.title + '-pivot', _width: 500, _height: 500 },
-                    docid
+                    docId
                 );
             linkCollection.linkSource = linkSource;
             if (!linkCollection.reactionScript) linkCollection.reactionScript = ScriptField.MakeScript('updateLinkCollection(self)');
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
index fcd6e0c55..aa269d8d6 100644
--- a/src/client/views/nodes/formattedText/DashDocCommentView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -32,7 +32,7 @@ export class DashDocCommentView {
         };
 
         this.root = ReactDOM.createRoot(this.dom);
-        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docid={node.attrs.docid} />);
+        this.root.render(<DashDocCommentViewInternal view={view} getPos={getPos} docId={node.attrs.docId} />);
         (this as any).dom = this.dom;
     }
 
@@ -48,7 +48,7 @@ export class DashDocCommentView {
 }
 
 interface IDashDocCommentViewInternal {
-    docid: string;
+    docId: string;
     view: any;
     getPos: any;
 }
@@ -63,13 +63,13 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
     }
 
     onPointerLeaveCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
         e.preventDefault();
         e.stopPropagation();
     }
 
     onPointerEnterCollapsed(e: any) {
-        DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+        DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
         e.preventDefault();
         e.stopPropagation();
     }
@@ -82,7 +82,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
             const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
             this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
             setTimeout(() => {
-                expand && DocServer.GetRefField(this.props.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+                expand && DocServer.GetRefField(this.props.docId).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
                 try {
                     this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1))));
                 } catch (e) {}
@@ -100,12 +100,12 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         const state = this.props.view.state;
         for (let i = this.props.getPos() + 1; i < state.doc.content.size; i++) {
             const m = state.doc.nodeAt(i);
-            if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docid === this.props.docid) {
+            if (m && m.type === state.schema.nodes.dashDoc && m.attrs.docId === this.props.docId) {
                 return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any; pos: number; hidden: boolean };
             }
         }
 
-        const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: this.props.docid, float: 'right' });
+        const dashDoc = state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: this.props.docId, float: 'right' });
         this.props.view.dispatch(state.tr.insert(this.props.getPos() + 1, dashDoc));
         setTimeout(() => {
             try {
@@ -119,7 +119,7 @@ export class DashDocCommentViewInternal extends React.Component<IDashDocCommentV
         return (
             <span
                 className="formattedTextBox-inlineComment"
-                id={'DashDocCommentView-' + this.props.docid}
+                id={'DashDocCommentView-' + this.props.docId}
                 onPointerLeave={this.onPointerLeaveCollapsed}
                 onPointerEnter={this.onPointerEnterCollapsed}
                 onPointerUp={this.onPointerUpCollapsed}
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
index 19ee1fc7b..61345f891 100644
--- a/src/client/views/nodes/formattedText/DashDocView.tsx
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -42,7 +42,7 @@ export class DashDocView {
 
         this.root = ReactDOM.createRoot(this.dom);
         this.root.render(
-            <DashDocViewInternal docid={node.attrs.docid} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
+            <DashDocViewInternal docId={node.attrs.docId} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} />
         );
     }
     destroy() {
@@ -53,7 +53,7 @@ export class DashDocView {
 }
 
 interface IDashDocViewInternal {
-    docid: string;
+    docId: string;
     alias: string;
     tbox: FormattedTextBox;
     width: string;
@@ -77,7 +77,7 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
 
     updateDoc = action((dashDoc: Doc) => {
         this._dashDoc = dashDoc;
-        this._finalLayout = this.props.docid ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
+        this._finalLayout = this.props.docId ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc, this.props.fieldKey);
 
         if (this._finalLayout) {
             if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) {
@@ -107,12 +107,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
         super(props);
         this._textBox = this.props.tbox;
 
-        DocServer.GetRefField(this.props.docid + this.props.alias).then(async dashDoc => {
+        DocServer.GetRefField(this.props.docId + this.props.alias).then(async dashDoc => {
             if (!(dashDoc instanceof Doc)) {
                 this.props.alias &&
-                    DocServer.GetRefField(this.props.docid).then(async dashDocBase => {
+                    DocServer.GetRefField(this.props.docId).then(async dashDocBase => {
                         if (dashDocBase instanceof Doc) {
-                            const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docid + this.props.alias);
+                            const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docId + this.props.alias);
                             aliasedDoc.layoutKey = 'layout';
                             this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined);
                             this.updateDoc(aliasedDoc);
@@ -161,12 +161,12 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> {
     };
 
     onPointerLeave = () => {
-        const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+        const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
         ele && (ele.style.backgroundColor = '');
     };
 
     onPointerEnter = () => {
-        const ele = document.getElementById('DashDocCommentView-' + this.props.docid) as HTMLDivElement;
+        const ele = document.getElementById('DashDocCommentView-' + this.props.docId) as HTMLDivElement;
         ele && (ele.style.backgroundColor = 'orange');
     };
 
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
index a89e8b4ed..21ccf3bc7 100644
--- a/src/client/views/nodes/formattedText/DashFieldView.tsx
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -54,7 +54,7 @@ export class DashFieldView {
                 unclickable={this.unclickable}
                 getPos={getPos}
                 fieldKey={node.attrs.fieldKey}
-                docid={node.attrs.docid}
+                docId={node.attrs.docId}
                 width={node.attrs.width}
                 height={node.attrs.height}
                 hideKey={node.attrs.hideKey}
@@ -76,7 +76,7 @@ export class DashFieldView {
 
 interface IDashFieldViewInternal {
     fieldKey: string;
-    docid: string;
+    docId: string;
     hideKey: boolean;
     tbox: FormattedTextBox;
     width: number;
@@ -100,8 +100,8 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna
         this._fieldKey = this.props.fieldKey;
         this._textBoxDoc = this.props.tbox.props.Document;
 
-        if (this.props.docid) {
-            DocServer.GetRefField(this.props.docid).then(
+        if (this.props.docId) {
+            DocServer.GetRefField(this.props.docId).then(
                 action(async dashDoc => {
                     dashDoc instanceof Doc && (this._dashDoc = dashDoc);
                 })
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index a76f9623a..da10bb0dc 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -186,7 +186,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
         return '';
     }
     public static GetDocFromUrl(url: string) {
-        return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docid
+        return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
     }
 
     constructor(props: any) {
@@ -545,7 +545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                     width: target[WidthSym](),
                     height: target[HeightSym](),
                     title: 'dashDoc',
-                    docid: target[Id],
+                    docId: target[Id],
                     float: 'unset',
                 });
                 if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) {
@@ -1260,7 +1260,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => {
                 if (pdfAnchor instanceof Doc) {
                     const dashField = view.state.schema.nodes.paragraph.create({}, [
-                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docid: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
+                        view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, editable: false }, undefined, [
                             view.state.schema.marks.linkAnchor.create({
                                 allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }],
                                 location: 'add:right',
@@ -1485,7 +1485,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
     onPointerUp = (e: React.PointerEvent): void => {
         const editor = this._editorView!;
         const state = editor?.state;
-        if (!state || !editor) return;
+        if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
         if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
         else if (this.props.isContentActive(true)) {
             const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 5675776fb..9e9b61db3 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -89,8 +89,8 @@ export class RichTextRules {
                 textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
                 textDoc[inlineFieldKey] = ''; // set a default value for the annotation
                 const node = (state.doc.resolve(start) as any).nodeAfter;
-                const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
-                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docid: textDocInline[Id], float: 'right' });
+                const newNode = schema.nodes.dashComment.create({ docId: textDocInline[Id] });
+                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: 'dashDoc', docId: textDocInline[Id], float: 'right' });
                 const sm = state.storedMarks || undefined;
                 const replaced = node
                     ? state.tr
@@ -248,17 +248,17 @@ export class RichTextRules {
             // [[fieldKey:Doc]] => show field of doc
             new InputRule(new RegExp(/\[\[([a-zA-Z_\? \-0-9]*)(=[a-zA-Z_@\? /\-0-9]*)?(:[a-zA-Z_@:\.\? \-0-9]+)?\]\]$/), (state, match, start, end) => {
                 const fieldKey = match[1];
-                const docid = match[3]?.replace(':', '');
+                const docId = match[3]?.replace(':', '');
                 const value = match[2]?.substring(1);
                 if (!fieldKey) {
-                    if (docid) {
-                        DocServer.GetRefField(docid).then(docx => {
+                    if (docId) {
+                        DocServer.GetRefField(docId).then(docx => {
                             const rstate = this.TextBox.EditorView?.state;
                             const selection = rstate?.selection.$from.pos;
                             if (rstate) {
                                 this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
                             }
-                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500 }, docid);
+                            const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId);
                             DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
 
                             const fstate = this.TextBox.EditorView?.state;
@@ -274,7 +274,7 @@ export class RichTextRules {
                     const num = value.match(/^[0-9.]$/);
                     this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value;
                 }
-                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid, hideKey: false });
+                const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false });
                 return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true);
             }),
 
@@ -311,16 +311,16 @@ export class RichTextRules {
                 const fieldKey = match[1] || '';
                 const fieldParam = match[2]?.replace('…', '...') || '';
                 const rawdocid = match[3]?.substring(1);
-                const docid = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
-                if (!fieldKey && !docid) return state.tr;
-                docid &&
-                    DocServer.GetRefField(docid).then(docx => {
+                const docId = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined;
+                if (!fieldKey && !docId) return state.tr;
+                docId &&
+                    DocServer.GetRefField(docId).then(docx => {
                         if (!(docx instanceof Doc && docx)) {
-                            Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docid);
+                            Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docId);
                         }
                     });
                 const node = (state.doc.resolve(start) as any).nodeAfter;
-                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docid, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
+                const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docId, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() });
                 const sm = state.storedMarks || undefined;
                 return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
             }),
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
index aa2475dca..6c9d5d31a 100644
--- a/src/client/views/nodes/formattedText/nodes_rts.ts
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -176,7 +176,7 @@ export const nodes: { [index: string]: NodeSpec } = {
 
     dashComment: {
         attrs: {
-            docid: { default: '' },
+            docId: { default: '' },
         },
         inline: true,
         group: 'inline',
@@ -213,7 +213,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             title: { default: null },
             float: { default: 'left' },
             location: { default: 'add:right' },
-            docid: { default: '' },
+            docId: { default: '' },
         },
         group: 'inline',
         draggable: true,
@@ -246,7 +246,7 @@ export const nodes: { [index: string]: NodeSpec } = {
             float: { default: 'right' },
             hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off
             fieldKey: { default: '' },
-            docid: { default: '' },
+            docId: { default: '' },
             alias: { default: '' },
         },
         group: 'inline',
@@ -261,7 +261,7 @@ export const nodes: { [index: string]: NodeSpec } = {
         inline: true,
         attrs: {
             fieldKey: { default: '' },
-            docid: { default: '' },
+            docId: { default: '' },
             hideKey: { default: false },
             editable: { default: true },
         },
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 40ef67f92..40da482d2 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -695,6 +695,9 @@ export namespace Doc {
         return bestAlias ?? Doc.MakeAlias(doc);
     }
 
+    // this lists out all the tag ids that can be  in a RichTextField that might contain document ids.
+    // if a document is cloned, we need to make sure to clone all of these referenced documents as well;
+    export const DocsInTextFieldIds = ['audioId', 'textId', 'anchorId', 'docId'];
     export async function makeClone(doc: Doc, cloneMap: Map<string, Doc>, linkMap: Map<string, Doc>, rtfs: { copy: Doc; key: string; field: RichTextField }[], exclusions: string[], cloneLinks: boolean, asBranch: boolean): Promise<Doc> {
         if (Doc.IsBaseProto(doc)) return doc;
         if (cloneMap.get(doc[Id])) return cloneMap.get(doc[Id])!;
@@ -713,12 +716,22 @@ export namespace Doc {
                     if (docs !== undefined && docs.length) {
                         const clones = await Promise.all(docs.map(async d => Doc.makeClone(d, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch)));
                         assignKey(new List<Doc>(clones));
-                    } else if (doc[key] instanceof Doc) {
-                        assignKey(key.includes('layout[') ? undefined : key.startsWith('layout') ? (doc[key] as Doc) : await Doc.makeClone(doc[key] as Doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch)); // reference documents except copy documents that are expanded template fields
                     } else {
                         assignKey(ObjectField.MakeCopy(field));
                         if (field instanceof RichTextField) {
-                            if (field.Data.includes('"audioId":') || field.Data.includes('"textId":') || field.Data.includes('"anchorId":')) {
+                            if (DocsInTextFieldIds.some(id => field.Data.includes(`"${id}":`))) {
+                                const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => '(' + exp + ')').join('|') + ')":"([a-z-A-Z0-9_]*)"', 'g');
+                                const rawdocids = field.Data.match(docidsearch);
+                                const docids = rawdocids?.map((str: string) =>
+                                    DocsInTextFieldIds.reduce((output, exp) => {
+                                        return output.replace(new RegExp(`${exp}":`, 'g'), '');
+                                    }, str)
+                                        .replace(/"/g, '')
+                                        .trim()
+                                );
+                                const results = docids && (await DocServer.GetRefFields(docids));
+                                const docs = results && Array.from(Object.keys(results)).map(key => DocCast(results[key]));
+                                docs && docs.map(doc => Doc.makeClone(doc, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
                                 rtfs.push({ copy, key, field });
                             }
                         }
@@ -726,24 +739,22 @@ export namespace Doc {
                 };
                 const docAtKey = doc[key];
                 if (docAtKey instanceof Doc) {
-                    if (!Doc.IsSystem(docAtKey) && (key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
+                    if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || key === 'annotationOn' || key === 'proto' || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) {
                         assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, cloneLinks, asBranch));
                     } else {
                         assignKey(docAtKey);
                     }
+                } else if (field instanceof RefField) {
+                    assignKey(field);
+                } else if (cfield instanceof ComputedField) {
+                    assignKey(cfield[Copy]());
+                    //   ComputedField.MakeFunction(cfield.script.originalScript));
+                } else if (field instanceof ObjectField) {
+                    await copyObjectField(field);
+                } else if (field instanceof Promise) {
+                    debugger; //This shouldn't happen...
                 } else {
-                    if (field instanceof RefField) {
-                        assignKey(field);
-                    } else if (cfield instanceof ComputedField) {
-                        assignKey(cfield[Copy]());
-                        //   ComputedField.MakeFunction(cfield.script.originalScript));
-                    } else if (field instanceof ObjectField) {
-                        await copyObjectField(field);
-                    } else if (field instanceof Promise) {
-                        debugger; //This shouldn't happen...
-                    } else {
-                        assignKey(field);
-                    }
+                    assignKey(field);
                 }
             })
         );
@@ -807,7 +818,8 @@ export namespace Doc {
             };
             const regex = `(${Doc.localServerPath()})([^"]*)`;
             const re = new RegExp(regex, 'g');
-            copy[key] = new RichTextField(field.Data.replace(/("textId":|"audioId":|"anchorId":)"([^"]+)"/g, replacer).replace(re, replacer2), field.Text);
+            const docidsearch = new RegExp('(' + DocsInTextFieldIds.map(exp => `"${exp}":`).join('|') + ')"([^"]+)"', 'g');
+            copy[key] = new RichTextField(field.Data.replace(docidsearch, replacer).replace(re, replacer2), field.Text);
         });
         return { clone: copy, map: cloneMap, linkMap };
     }
diff --git a/src/fields/RichTextField.ts b/src/fields/RichTextField.ts
index d7edd4266..3e75a071f 100644
--- a/src/fields/RichTextField.ts
+++ b/src/fields/RichTextField.ts
@@ -1,11 +1,11 @@
-import { serializable } from "serializr";
-import { scriptingGlobal } from "../client/util/ScriptingGlobals";
-import { Deserializable } from "../client/util/SerializationHelper";
-import { Copy, ToScriptString, ToString } from "./FieldSymbols";
-import { ObjectField } from "./ObjectField";
+import { serializable } from 'serializr';
+import { scriptingGlobal } from '../client/util/ScriptingGlobals';
+import { Deserializable } from '../client/util/SerializationHelper';
+import { Copy, ToScriptString, ToString } from './FieldSymbols';
+import { ObjectField } from './ObjectField';
 
 @scriptingGlobal
-@Deserializable("RichTextField")
+@Deserializable('RichTextField')
 export class RichTextField extends ObjectField {
     @serializable(true)
     readonly Data: string;
@@ -13,14 +13,14 @@ export class RichTextField extends ObjectField {
     @serializable(true)
     readonly Text: string;
 
-    constructor(data: string, text: string = "") {
+    constructor(data: string, text: string = '') {
         super();
         this.Data = data;
         this.Text = text;
     }
 
     Empty() {
-        return !(this.Text || this.Data.toString().includes("dashField") || this.Data.toString().includes("align"));
+        return !(this.Text || this.Data.toString().includes('dashField') || this.Data.toString().includes('align'));
     }
 
     [Copy]() {
@@ -28,14 +28,16 @@ export class RichTextField extends ObjectField {
     }
 
     [ToScriptString]() {
-        return `new RichTextField("${this.Data.replace(/"/g, "\\\"")}", "${this.Text}")`;
+        return `new RichTextField("${this.Data.replace(/"/g, '\\"')}", "${this.Text}")`;
     }
     [ToString]() {
         return this.Text;
     }
 
     public static DashField(fieldKey: string) {
-        return new RichTextField(`{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`, "");
+        return new RichTextField(
+            `{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"${fieldKey}","docId":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}`,
+            ''
+        );
     }
-
-}
\ No newline at end of file
+}
diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts
index bf055cd8b..239b59e83 100644
--- a/src/fields/RichTextUtils.ts
+++ b/src/fields/RichTextUtils.ts
@@ -264,18 +264,18 @@ export namespace RichTextUtils {
 
         const imageNode = (schema: any, image: ImageTemplate, textNote: Doc) => {
             const { url: src, width, agnostic } = image;
-            let docid: string;
+            let docId: string;
             const guid = Utils.GenerateDeterministicGuid(agnostic);
             const backingDocId = StrCast(textNote[guid]);
             if (!backingDocId) {
                 const backingDoc = Docs.Create.ImageDocument(agnostic, { _width: 300, _height: 300 });
                 DocUtils.makeCustomViewClicked(backingDoc, Docs.Create.FreeformDocument);
-                docid = backingDoc[Id];
-                textNote[guid] = docid;
+                docId = backingDoc[Id];
+                textNote[guid] = docId;
             } else {
-                docid = backingDocId;
+                docId = backingDocId;
             }
-            return schema.node('image', { src, agnostic, width, docid, float: null, location: 'add:right' });
+            return schema.node('image', { src, agnostic, width, docId, float: null, location: 'add:right' });
         };
 
         const textNode = (schema: any, run: docs_v1.Schema$TextRun) => {
-- 
cgit v1.2.3-70-g09d2


From c5c2c309cd88bbeb2f1b668cb040cffaac9c8470 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 4 Apr 2023 20:12:16 -0400
Subject: fixed using freeformview in sidebar of pdfs. fixed issues with
 dragging items out of schema and with selecting the schema view by clicking
 on headers.  Fixed a lot of errors caused by using OmitKeys which masks type
 checking.  fixed some pointerevent problems with treeview and freeformview

---
 src/client/documents/Documents.ts                  |  22 ++--
 src/client/views/DocumentDecorations.tsx           |  40 +++---
 src/client/views/EditableView.tsx                  |   4 +-
 src/client/views/InkStrokeProperties.ts            |   1 -
 src/client/views/InkingStroke.tsx                  |  13 +-
 src/client/views/MainView.tsx                      |   1 +
 src/client/views/PropertiesView.tsx                |  73 ++---------
 src/client/views/SidebarAnnos.tsx                  |  15 +--
 src/client/views/StyleProvider.tsx                 |   2 +-
 src/client/views/TemplateMenu.tsx                  |   2 +-
 .../views/collections/CollectionCarousel3DView.tsx |   7 +-
 .../views/collections/CollectionCarouselView.tsx   |   8 +-
 .../collections/CollectionStackedTimeline.tsx      |  54 +++-----
 .../views/collections/CollectionTreeView.tsx       |  24 ++--
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 139 +++++++++++---------
 .../collections/collectionFreeForm/MarqueeView.tsx |   2 +
 .../collectionGrid/CollectionGridView.tsx          |   9 +-
 .../collectionSchema/CollectionSchemaView.tsx      |  99 +++------------
 .../collectionSchema/SchemaColumnHeader.tsx        |  10 +-
 src/client/views/nodes/AudioBox.tsx                |   6 +-
 src/client/views/nodes/ComparisonBox.tsx           |   6 +-
 src/client/views/nodes/DocumentView.tsx            |  26 +---
 src/client/views/nodes/FieldView.tsx               |   2 +-
 src/client/views/nodes/ImageBox.tsx                |  12 +-
 src/client/views/nodes/MapBox/MapBox.tsx           |   6 +-
 src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx |   5 +-
 src/client/views/nodes/PDFBox.tsx                  | 141 ++++++++++++++++-----
 src/client/views/nodes/ScreenshotBox.tsx           |  18 ++-
 src/client/views/nodes/VideoBox.tsx                |  12 +-
 src/client/views/nodes/WebBox.tsx                  |  14 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  27 ++--
 src/client/views/pdf/PDFViewer.tsx                 |  15 ++-
 src/fields/Doc.ts                                  |   9 +-
 34 files changed, 389 insertions(+), 437 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9b8b9c877..9cb480c4a 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1420,41 +1420,39 @@ export namespace DocUtils {
 
     export function DocumentFromField(target: Doc, fieldKey: string, proto?: Doc, options?: DocumentOptions): Doc | undefined {
         let created: Doc | undefined;
-        let layout: ((fieldKey: string) => string) | undefined;
         const field = target[fieldKey];
-        const resolved = options || {};
+        const resolved = options ?? {};
         if (field instanceof ImageField) {
             created = Docs.Create.ImageDocument(field.url.href, resolved);
-            layout = ImageBox.LayoutString;
+            created.layout = ImageBox.LayoutString(fieldKey);
         } else if (field instanceof Doc) {
             created = field;
         } else if (field instanceof VideoField) {
             created = Docs.Create.VideoDocument(field.url.href, resolved);
-            layout = VideoBox.LayoutString;
+            created.layout = VideoBox.LayoutString(fieldKey);
         } else if (field instanceof PdfField) {
             created = Docs.Create.PdfDocument(field.url.href, resolved);
-            layout = PDFBox.LayoutString;
+            created.layout = PDFBox.LayoutString(fieldKey);
         } else if (field instanceof AudioField) {
             created = Docs.Create.AudioDocument(field.url.href, resolved);
-            layout = AudioBox.LayoutString;
+            created.layout = AudioBox.LayoutString(fieldKey);
         } else if (field instanceof RecordingField) {
             created = Docs.Create.RecordingDocument(field.url.href, resolved);
-            layout = RecordingBox.LayoutString;
+            created.layout = RecordingBox.LayoutString(fieldKey);
         } else if (field instanceof InkField) {
             created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved);
-            layout = InkingStroke.LayoutString;
+            created.layout = InkingStroke.LayoutString(fieldKey);
         } else if (field instanceof List && field[0] instanceof Doc) {
             created = Docs.Create.StackingDocument(DocListCast(field), resolved);
-            layout = CollectionView.LayoutString;
+            created.layout = CollectionView.LayoutString(fieldKey);
         } else if (field instanceof MapField) {
             created = Docs.Create.MapDocument(DocListCast(field), resolved);
-            layout = MapBox.LayoutString;
+            created.layout = MapBox.LayoutString(fieldKey);
         } else {
             created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved });
-            layout = FormattedTextBox.LayoutString;
+            created.layout = FormattedTextBox.LayoutString(fieldKey);
         }
         if (created) {
-            created.layout = layout?.(fieldKey);
             created.title = fieldKey;
             proto && created.proto && (created.proto = Doc.GetProto(proto));
         }
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 985e6f88f..9bc583ce5 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -31,6 +31,7 @@ import { DocumentView, OpenWhere, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
+import { RichTextField } from '../../fields/RichTextField';
 
 @observer
 export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> {
@@ -127,19 +128,23 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
                         }
                         //@ts-ignore
                         const titleField = +this._accumulatedTitle == this._accumulatedTitle ? +this._accumulatedTitle : this._accumulatedTitle;
-                        Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
 
-                        if (d.rootDoc.syncLayoutFieldWithTitle) {
-                            const title = titleField.toString();
+                        if (titleField.toString().startsWith('<this>')) {
+                            const title = titleField.toString().replace(/<this>\.?/, '');
                             const curKey = Doc.LayoutFieldKey(d.rootDoc);
-                            if (curKey !== title && d.dataDoc[title] === undefined) {
-                                d.rootDoc.layout = FormattedTextBox.LayoutString(title);
-                                setTimeout(() => {
-                                    const val = d.dataDoc[curKey];
-                                    d.dataDoc[curKey] = undefined;
-                                    d.dataDoc[title] = val;
-                                });
+                            if (curKey !== title) {
+                                if (title) {
+                                    if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') {
+                                        d.rootDoc.layoutKey = `layout_${title}`;
+                                        d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title);
+                                        d.rootDoc[`${title}-nativeWidth`] = d.rootDoc[`${title}-nativeHeight`] = 0;
+                                    }
+                                } else {
+                                    d.rootDoc.layoutKey = undefined;
+                                }
                             }
+                        } else {
+                            Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true);
                         }
                     }),
                 'title blur'
@@ -453,17 +458,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
-        const views = SelectionManager.Views().map(dv => dv.rootDoc);
-        this._inkDragDocs = views
-            .filter(doc => doc.type === DocumentType.INK)
-            .map(doc => {
-                if (InkStrokeProperties.Instance._lock) {
-                    Doc.SetNativeHeight(doc, NumCast(doc._height));
-                    Doc.SetNativeWidth(doc, NumCast(doc._width));
-                }
-                return { doc, x: NumCast(doc.x), y: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) };
-            });
-
         setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction);
         this.Interacting = true; // turns off pointer events on things like youtube videos and web pages so that dragging doesn't get "stuck" when cursor moves over them
         this._resizeHdlId = e.currentTarget.className;
@@ -487,10 +481,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         if (!first) return false;
         let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
         var fixedAspect = Doc.NativeAspect(first.layoutDoc);
-        InkStrokeProperties.Instance._lock &&
-            SelectionManager.Views()
-                .filter(dv => dv.rootDoc.type === DocumentType.INK)
-                .forEach(dv => (fixedAspect = Doc.NativeAspect(dv.rootDoc)));
 
         const resizeHdl = this._resizeHdlId.split(' ')[0];
         if (fixedAspect && (resizeHdl === 'documentDecorations-bottomRightResizer' || resizeHdl === 'documentDecorations-topLeftResizer')) {
diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx
index bb190e93b..164b6c57a 100644
--- a/src/client/views/EditableView.tsx
+++ b/src/client/views/EditableView.tsx
@@ -129,7 +129,7 @@ export class EditableView extends React.Component<EditableProps> {
                 this._editing = true;
                 this.props.isEditingCallback?.(true);
             }
-            e.stopPropagation();
+            // e.stopPropagation();
         }
     };
 
@@ -215,7 +215,7 @@ export class EditableView extends React.Component<EditableProps> {
                 className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`}
                 ref={this._ref}
                 style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }}
-                onPointerDown={e => e.stopPropagation()}
+                //onPointerDown={this.stopPropagation}
                 onClick={this.onClick}
                 placeholder={this.props.placeholder}>
                 <span style={{ fontStyle: this.props.fontStyle, fontSize: this.props.fontSize }}>{this.props.contents ? this.props.contents?.valueOf() : this.props.placeholder?.valueOf()}</span>
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 1d8d52425..e6df0801c 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -19,7 +19,6 @@ export class InkStrokeProperties {
         return this._Instance || new InkStrokeProperties();
     }
 
-    @observable _lock = false;
     @observable _controlButton = false;
     @observable _currentPoint = -1;
 
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index e3642fdaf..2fd6cc4d6 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -25,10 +25,11 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, HeightSym, WidthSym } from '../../fields/Doc';
 import { InkData, InkField } from '../../fields/InkField';
-import { BoolCast, Cast, DocCast, NumCast, RTFCast, StrCast } from '../../fields/Types';
+import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
 import { TraceMobx } from '../../fields/util';
-import { DashColor, OmitKeys, returnFalse, setupMoveUpEvents } from '../../Utils';
+import { DashColor, returnFalse, setupMoveUpEvents } from '../../Utils';
 import { CognitiveServices } from '../cognitive_services/CognitiveServices';
+import { Docs } from '../documents/Documents';
 import { InteractionUtils } from '../util/InteractionUtils';
 import { SnappingManager } from '../util/SnappingManager';
 import { Transform } from '../util/Transform';
@@ -41,13 +42,12 @@ import { InkControlPtHandles, InkEndPtHandles } from './InkControlPtHandles';
 import './InkStroke.scss';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { InkTangentHandles } from './InkTangentHandles';
-import { DocComponentView, DocFocusOptions, DocumentView } from './nodes/DocumentView';
+import { DocComponentView } from './nodes/DocumentView';
 import { FieldView, FieldViewProps } from './nodes/FieldView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
+import { PinProps, PresBox } from './nodes/trails';
 import { StyleProp } from './StyleProvider';
 import Color = require('color');
-import { Docs } from '../documents/Documents';
-import { PinProps, PresBox } from './nodes/trails';
 
 @observer
 export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -464,7 +464,8 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() {
                             //top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2,
                         }}>
                         <FormattedTextBox
-                            {...OmitKeys(this.props, ['children']).omit}
+                            {...this.props}
+                            setHeight={undefined}
                             setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
                             yPadding={10}
                             xPadding={10}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f5adc17d0..befb0200d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -259,6 +259,7 @@ export class MainView extends React.Component {
                 fa.faConciergeBell,
                 fa.faWindowRestore,
                 fa.faFolder,
+                fa.faFolderOpen,
                 fa.faMapPin,
                 fa.faMapMarker,
                 fa.faFingerprint,
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index f3a5a5393..fbc7d7696 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -102,7 +102,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
 
     @observable inOptions: boolean = false;
     @observable _controlButton: boolean = false;
-    @observable _lock: boolean = false;
 
     componentDidMount() {
         this.selectedDocListenerDisposer?.();
@@ -586,16 +585,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         <FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
                     </div>
                 </Tooltip>
-                <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? 'Unlock ratio' : 'Lock ratio'}</div>}>
-                    <div className="inking-button-lock" onPointerDown={action(() => (InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock))}>
-                        <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? 'lock' : 'unlock'} color="white" size="lg" />
-                    </div>
-                </Tooltip>
-                <Tooltip title={<div className="dash-tooltip">{'Rotate 90˚'}</div>}>
-                    <div className="inking-button-rotate" onPointerDown={action(() => this.rotate(Math.PI / 2))}>
-                        <FontAwesomeIcon icon="undo" color="white" size="lg" />
-                    </div>
-                </Tooltip>
             </div>
         );
     }
@@ -644,9 +633,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @action
     upDownButtons = (dirs: string, field: string) => {
         switch (field) {
-            case 'rot':
-                this.rotate(dirs === 'up' ? 0.1 : -0.1);
-                break;
             case 'Xps':
                 this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10));
                 break;
@@ -662,7 +648,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 const oldX = NumCast(this.selectedDoc?.x);
                 const oldY = NumCast(this.selectedDoc?.y);
                 this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === 'up' ? 10 : -10));
-                InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth) * NumCast(this.selectedDoc?._height));
                 const doc = this.selectedDoc;
                 if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
                     const ink = Cast(doc.data, InkField)?.inkData;
@@ -684,7 +669,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 const oX = NumCast(this.selectedDoc?.x);
                 const oY = NumCast(this.selectedDoc?.y);
                 this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === 'up' ? 10 : -10));
-                InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight) * NumCast(this.selectedDoc?._width));
                 const docu = this.selectedDoc;
                 if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
                     const ink = Cast(docu.data, InkField)?.inkData;
@@ -704,11 +688,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     };
 
     getField(key: string) {
-        //if (this.selectedDoc) {
         return Field.toString(this.selectedDoc?.[key] as Field);
-        // } else {
-        //     return undefined as Opt<string>;
-        // }
     }
 
     @computed get shapeXps() {
@@ -717,9 +697,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @computed get shapeYps() {
         return this.getField('y');
     }
-    @computed get shapeRot() {
-        return this.getField('rotation');
-    }
     @computed get shapeHgt() {
         return this.getField('_height');
     }
@@ -732,18 +709,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     set shapeYps(value) {
         this.selectedDoc && (this.selectedDoc.y = Number(value));
     }
-    set shapeRot(value) {
-        this.selectedDoc && (this.selectedDoc.rotation = Number(value));
-    }
     set shapeWid(value) {
-        const oldWidth = NumCast(this.selectedDoc?._width);
         this.selectedDoc && (this.selectedDoc._width = Number(value));
-        InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
     }
     set shapeHgt(value) {
-        const oldHeight = NumCast(this.selectedDoc?._height);
         this.selectedDoc && (this.selectedDoc._height = Number(value));
-        InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
     }
 
     @computed get hgtInput() {
@@ -790,30 +760,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
             'Y:'
         );
     }
-    @computed get rotInput() {
-        return this.inputBoxDuo(
-            'rot',
-            this.shapeRot,
-            (val: string) => {
-                if (!isNaN(Number(val))) {
-                    this.rotate(Number(val) - Number(this.shapeRot));
-                    this.shapeRot = val;
-                }
-                return true;
-            },
-            '∠:',
-            'rot',
-            this.shapeRot,
-            (val: string) => {
-                if (!isNaN(Number(val))) {
-                    this.rotate(Number(val) - Number(this.shapeRot));
-                    this.shapeRot = val;
-                }
-                return true;
-            },
-            ''
-        );
-    }
 
     @observable private _fillBtn = false;
     @observable private _lineBtn = false;
@@ -1080,10 +1026,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @computed get transformEditor() {
         return (
             <div className="transform-editor">
-                {this.controlPointsButton}
+                {this.isInk ? this.controlPointsButton : null}
                 {this.hgtInput}
                 {this.XpsInput}
-                {this.rotInput}
             </div>
         );
     }
@@ -1194,17 +1139,15 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                     </div>
                 )}
 
-                {this.isInk ? (
-                    <div className="propertiesView-transform">
-                        <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
-                            Transform
-                            <div className="propertiesView-transform-title-icon">
-                                <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
-                            </div>
+                <div className="propertiesView-transform">
+                    <div className="propertiesView-transform-title" onPointerDown={action(() => (this.openTransform = !this.openTransform))} style={{ backgroundColor: this.openTransform ? 'black' : '' }}>
+                        Transform
+                        <div className="propertiesView-transform-title-icon">
+                            <FontAwesomeIcon icon={this.openTransform ? 'caret-down' : 'caret-right'} size="lg" color="white" />
                         </div>
-                        {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
                     </div>
-                ) : null}
+                    {this.openTransform ? <div className="propertiesView-transform-content">{this.transformEditor}</div> : null}
+                </div>
             </>
         );
     }
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 02dd15960..48c0150e6 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -5,7 +5,7 @@ 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 { emptyFunction, returnAll, returnFalse, returnOne, returnTrue, returnZero } from '../../Utils';
 import { Docs, DocUtils } from '../documents/Documents';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { LinkManager } from '../util/LinkManager';
@@ -26,6 +26,7 @@ interface ExtraProps {
     // usePanelWidth: boolean;
     showSidebar: boolean;
     nativeWidth: number;
+    usePanelWidth?: boolean;
     whenChildContentsActiveChanged: (isActive: boolean) => void;
     ScreenToLocalTransform: () => Transform;
     sidebarAddDocument: (doc: Doc | Doc[], suffix: string) => boolean;
@@ -158,13 +159,10 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
             .ScreenToLocalTransform()
             .translate(Doc.NativeWidth(this.props.dataDoc), 0)
             .scale(this.props.NativeDimScaling?.() || 1);
-    // panelWidth = () => !this.props.layoutDoc._showSidebar ? 0 :
-    //     this.props.usePanelWidth ? this.props.PanelWidth() :
-    //         (NumCast(this.props.layoutDoc.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth() / NumCast(this.props.layoutDoc.nativeWidth);
     panelWidth = () =>
         !this.props.showSidebar
             ? 0
-            : this.props.layoutDoc.type === DocumentType.RTF || this.props.layoutDoc.type === DocumentType.MAP
+            : this.props.usePanelWidth // [DocumentType.RTF, DocumentType.MAP].includes(this.props.layoutDoc.type as any)
             ? this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1)
             : ((NumCast(this.props.nativeWidth) - Doc.NativeWidth(this.props.dataDoc)) * this.props.PanelWidth()) / NumCast(this.props.nativeWidth);
     panelHeight = () => this.props.PanelHeight() - this.filtersHeight();
@@ -224,20 +222,21 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
                 </div>
                 <div style={{ width: '100%', height: `calc(100% - 38px)`, position: 'relative' }}>
                     <CollectionStackingView
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                        ref={this._stackRef}
+                        {...this.props}
+                        setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
+                        ref={this._stackRef}
                         PanelHeight={this.panelHeight}
                         PanelWidth={this.panelWidth}
                         docFilters={this.docFilters}
-                        scaleField={this.sidebarKey + '-scale'}
                         sortFunc={this.sortByLinkAnchorY}
                         setHeight={this.setHeightCallback}
                         isAnnotationOverlay={false}
                         select={emptyFunction}
                         NativeDimScaling={returnOne}
                         //childShowTitle={this.showTitle}
+                        isAnyChildContentActive={returnFalse}
                         childDocumentsActive={this.props.isContentActive}
                         whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                         childHideDecorationTitle={returnTrue}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index 50cf8eba7..f7b9420a7 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -170,7 +170,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
             const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color));
             if (docColor) return docColor;
             const docView = props?.DocumentView?.();
-            const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, 'backgroundColor');
+            const backColor = backgroundCol() || docView?.props.styleProvider?.(docView.props.treeViewDoc, docView.props, StyleProp.BackgroundColor);
             if (!backColor) return undefined;
             return lightOrDark(backColor);
         case StyleProp.Hidden:
diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx
index 681ff66e0..45db240a9 100644
--- a/src/client/views/TemplateMenu.tsx
+++ b/src/client/views/TemplateMenu.tsx
@@ -79,7 +79,7 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> {
     };
     componentDidMount() {
         !this._addedKeys && (this._addedKeys = new ObservableSet());
-        Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document)))
+        [...Array.from(Object.keys(Doc.GetProto(this.props.docViews[0].props.Document))), ...Array.from(Object.keys(this.props.docViews[0].props.Document))]
             .filter(key => key.startsWith('layout_'))
             .map(key => runInAction(() => this._addedKeys.add(key.replace('layout_', ''))));
     }
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 57ff1b292..a266c9207 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -5,7 +5,7 @@ import * as React from 'react';
 import { Doc } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse, Utils } from '../../../Utils';
+import { returnFalse, returnZero, Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { DocumentView } from '../nodes/DocumentView';
 import { StyleProp } from '../StyleProvider';
@@ -43,7 +43,10 @@ export class CollectionCarousel3DView extends CollectionSubView() {
         const displayDoc = (childPair: { layout: Doc; data: Doc }) => {
             return (
                 <DocumentView
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'childLayoutTemplate', 'childLayoutString']).omit}
+                    {...this.props}
+                    NativeWidth={returnZero}
+                    NativeHeight={returnZero}
+                    suppressSetHeight={true}
                     onDoubleClick={this.onChildDoubleClick}
                     renderDepth={this.props.renderDepth + 1}
                     LayoutTemplate={this.props.childLayoutTemplate}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 32f6207ed..c0220f804 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { NumCast, ScriptCast, StrCast } from '../../../fields/Types';
-import { OmitKeys, returnFalse } from '../../../Utils';
+import { emptyFunction, returnFalse, returnZero } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
@@ -47,7 +47,7 @@ export class CollectionCarouselView extends CollectionSubView() {
     @computed get content() {
         const index = NumCast(this.layoutDoc._itemIndex);
         const curDoc = this.childLayoutPairs?.[index];
-        const captionProps = { ...OmitKeys(this.props, ['setHeight']).omit, fieldKey: 'caption' };
+        const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined };
         const marginX = NumCast(this.layoutDoc['caption-xMargin']);
         const marginY = NumCast(this.layoutDoc['caption-yMargin']);
         const showCaptions = StrCast(this.layoutDoc._showCaption);
@@ -55,7 +55,9 @@ export class CollectionCarouselView extends CollectionSubView() {
             <>
                 <div className="collectionCarouselView-image" key="image">
                     <DocumentView
-                        {...OmitKeys(this.props, ['setHeight', 'NativeWidth', 'NativeHeight', 'childLayoutTemplate', 'childLayoutString']).omit}
+                        {...this.props}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
                         onDoubleClick={this.onContentDoubleClick}
                         onClick={this.onContentClick}
                         hideCaptions={showCaptions ? true : false}
diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx
index 4941bc722..642d29d2a 100644
--- a/src/client/views/collections/CollectionStackedTimeline.tsx
+++ b/src/client/views/collections/CollectionStackedTimeline.tsx
@@ -9,7 +9,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ComputedField, ScriptField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { ImageField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, returnNone, returnOne, returnTrue, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents, smoothScrollHorizontal, StopEvent } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
@@ -23,7 +23,6 @@ import { Transform } from '../../util/Transform';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { AudioWaveform } from '../AudioWaveform';
 import { CollectionSubView } from '../collections/CollectionSubView';
-import { Colors } from '../global/globalEnums';
 import { LightboxView } from '../LightboxView';
 import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView';
 import { LabelBox } from '../nodes/LabelBox';
@@ -512,37 +511,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
 
     currentTimecode = () => this.currentTime;
 
-    @computed get renderDictation() {
-        const dictation = Cast(this.dataDoc[this.props.dictationKey], Doc, null);
-        return !dictation ? null : (
-            <div
-                style={{
-                    position: 'absolute',
-                    height: '100%',
-                    top: this.timelineContentHeight,
-                    background: Colors.LIGHT_BLUE,
-                }}>
-                <DocumentView
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
-                    Document={dictation}
-                    PanelHeight={this.dictationHeight}
-                    isAnnotationOverlay={true}
-                    isDocumentActive={returnFalse}
-                    select={emptyFunction}
-                    NativeDimScaling={returnOne}
-                    xMargin={25}
-                    yMargin={10}
-                    ScreenToLocalTransform={this.dictationScreenToLocalTransform}
-                    whenChildContentsActiveChanged={emptyFunction}
-                    removeDocument={returnFalse}
-                    moveDocument={returnFalse}
-                    addDocument={returnFalse}
-                    CollectionView={undefined}
-                    renderDepth={this.props.renderDepth + 1}></DocumentView>
-            </div>
-        );
-    }
-
     // renders selection region on timeline
     @computed get selectionContainer() {
         const markerEnd = CollectionStackedTimeline.SelectingRegion === this ? this.currentTime : this._markerEnd;
@@ -638,7 +606,6 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack
                                 PanelWidth={this.timelineContentWidth}
                             />
                         )}
-                        {/* {this.renderDictation} */}
 
                         <div
                             className="collectionStackedTimeline-hover"
@@ -817,16 +784,22 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
     // renders anchor LabelBox
     renderInner = computedFn(function (this: StackedTimelineAnchor, mark: Doc, script: undefined | (() => ScriptField), doublescript: undefined | (() => ScriptField), screenXf: () => Transform, width: () => number, height: () => number) {
         const anchor = observable({ view: undefined as any });
-        const focusFunc = (doc: Doc, options: DocFocusOptions) => this.props.playLink(mark);
+        const focusFunc = (doc: Doc, options: DocFocusOptions): number | undefined => {
+            this.props.playLink(mark);
+            return undefined;
+        };
         return {
             anchor,
             view: (
                 <DocumentView
                     key="view"
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                    {...this.props}
+                    NativeWidth={returnZero}
+                    NativeHeight={returnZero}
                     ref={action((r: DocumentView | null) => (anchor.view = r))}
                     Document={mark}
                     DataDoc={undefined}
+                    docViewPath={returnEmptyDoclist}
                     pointerEvents={this.noEvents ? returnNone : undefined}
                     styleProvider={this.props.styleProvider}
                     renderDepth={this.props.renderDepth + 1}
@@ -837,7 +810,16 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps>
                     PanelHeight={height}
                     fitWidth={returnTrue}
                     ScreenToLocalTransform={screenXf}
+                    addDocTab={returnFalse}
+                    pinToPres={emptyFunction}
+                    whenChildContentsActiveChanged={emptyFunction}
                     focus={focusFunc}
+                    isContentActive={returnFalse}
+                    ContainingCollectionView={undefined}
+                    ContainingCollectionDoc={undefined}
+                    searchFilterDocs={returnEmptyDoclist}
+                    docFilters={returnEmptyFilter}
+                    docRangeFilters={returnEmptyFilter}
                     rootSelected={returnFalse}
                     onClick={script}
                     onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript}
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 553967b95..4a11e8f0b 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -7,7 +7,7 @@ import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue } from '../../../Utils';
+import { emptyFunction, returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnNone, returnOne, returnTrue, returnZero } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -384,12 +384,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
         this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false;
 
     @observable _headerHeight = 0;
-    contentFunc = () => {
+    @computed get content() {
         const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
         const pointerEvents = () => (!this.props.isContentActive() && !SnappingManager.GetIsDragging() ? 'none' : undefined);
         const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? null : this.titleBar;
-        return [
-            <div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
+        return (
+            <div style={{ display: 'flex', flexDirection: 'column', height: '100%', pointerEvents: 'all' }}>
                 {!this.buttonMenu && !this.noviceExplainer ? null : (
                     <div className="documentButtonMenu" ref={action((r: HTMLDivElement | null) => r && (this._headerHeight = Number(getComputedStyle(r).height.replace(/px/, ''))))}>
                         {this.buttonMenu}
@@ -428,9 +428,9 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                         </div>
                     </div>
                 </div>
-            </div>,
-        ];
-    };
+            </div>
+        );
+    }
     render() {
         TraceMobx();
 
@@ -439,7 +439,11 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
             <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}>
                 {!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeViewHasOverlay ? (
                     <CollectionFreeFormView
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                        {...this.props}
+                        setContentView={emptyFunction}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
+                        pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
                         isAnnotationOverlay={true}
                         isAnnotationOverlayScrollable={true}
                         childDocumentsActive={this.props.isDocumentActive}
@@ -451,10 +455,10 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree
                         moveDocument={this.moveAnnotationDocument}
                         bringToFront={emptyFunction}
                         renderDepth={this.props.renderDepth + 1}>
-                        {this.contentFunc}
+                        {this.content}
                     </CollectionFreeFormView>
                 ) : (
-                    this.contentFunc()
+                    this.content
                 )}
             </div>
         );
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 51624689e..bc25ad43a 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -45,7 +45,7 @@ interface CollectionViewProps_ extends FieldViewProps {
 
     // property overrides for child documents
     childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox)
-    childDocumentsActive?: () => boolean; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
+    childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode)
     childFitWidth?: (child: Doc) => boolean;
     childShowTitle?: () => string;
     childOpacity?: () => number;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index aed3683d4..d39668a5d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,6 +1,6 @@
 import { Bezier } from 'bezier-js';
 import { Colors } from 'browndash-components';
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import { DateField } from '../../../../fields/DateField';
@@ -53,10 +53,14 @@ import { MarqueeView } from './MarqueeView';
 import React = require('react');
 
 export type collectionFreeformViewProps = {
+    noPointerWheel?: () => boolean; // turn off pointerwheel interactions (see PDFViewer)
+    NativeWidth?: () => number;
+    NativeHeight?: () => number;
+    originTopLeft?: boolean;
     annotationLayerHostsContent?: boolean; // whether to force scaling of content (needed by ImageBox)
     viewDefDivClick?: ScriptField;
     childPointerEvents?: string;
-    scaleField?: string;
+    viewField?: 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;
@@ -97,8 +101,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private get isAnnotationOverlay() {
         return this.props.isAnnotationOverlay;
     }
-    private get scaleFieldKey() {
-        return this.props.scaleField || '_viewScale';
+    public get scaleFieldKey() {
+        return this.props.viewField ? this.props.viewField + '-viewScale' : '_viewScale';
+    }
+    private get panXFieldKey() {
+        return this.props.viewField ? this.props.viewField + '-panX' : '_panX';
+    }
+    private get panYFieldKey() {
+        return this.props.viewField ? this.props.viewField + '-panY' : '_panY';
     }
     private get borderWidth() {
         return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH;
@@ -149,21 +159,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                   );
     }
     @computed get nativeWidth() {
-        return this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+        return this.props.NativeWidth?.() || (this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
     }
     @computed get nativeHeight() {
-        return this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
+        return this.props.NativeHeight?.() || (this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)));
     }
     @computed get cachedCenteringShiftX(): number {
         const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
-        return this.props.isAnnotationOverlay ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+        return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedCenteringShiftY(): number {
         const dv = this.props.DocumentView?.();
         const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling;
         // if freeform has a native aspect, then the panel height needs to be adjusted to match it
         const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth();
-        return this.props.isAnnotationOverlay ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
+        return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections
     }
     @computed get cachedGetLocalTransform(): Transform {
         return Transform.Identity()
@@ -236,13 +246,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     reverseNativeScaling = () => (this.fitContentsToBox ? true : false);
     // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document.
     // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image
-    panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
-    panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY, NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
+    panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1));
+    panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1));
     zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1));
     contentTransform = () =>
-        !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1
-            ? ''
-            : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+        this.props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
     getTransform = () => this.cachedGetTransform.copy();
     getLocalTransform = () => this.cachedGetLocalTransform.copy();
     getContainerTransform = () => this.cachedGetContainerTransform.copy();
@@ -293,7 +301,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     }
 
     groupFocus = (anchor: Doc, options: DocFocusOptions) => {
-        options.docTransform = new Transform(-NumCast(this.rootDoc.panX) + NumCast(anchor.x), -NumCast(this.rootDoc.panY) + NumCast(anchor.y), 1);
+        options.docTransform = new Transform(-NumCast(this.rootDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.rootDoc[this.panYFieldKey]) + NumCast(anchor.y), 1);
         const res = this.props.focus(this.rootDoc, options);
         options.docTransform = undefined;
         return res;
@@ -301,7 +309,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     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 savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), 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);
 
@@ -772,7 +780,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         PresBox.Instance?.pauseAutoPres();
         const dx = e.deltaX;
         const dy = e.deltaY;
-        this.setPan(NumCast(this.Document._panX) - dx, NumCast(this.Document._panY) - dy, 0, true);
+        this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
     };
 
     @action
@@ -780,7 +788,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         PresBox.Instance?.pauseAutoPres();
         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.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true);
         this._lastX = e.clientX;
         this._lastY = e.clientY;
     };
@@ -979,13 +987,13 @@ 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, NumCast(this.props.Document.scrollTop) * safeScale || -localTransform.TranslateY / safeScale);
+            this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.scrollTop) * safeScale) || -localTransform.TranslateY / safeScale);
         }
     };
 
     @action
     onPointerWheel = (e: React.WheelEvent): void => {
-        if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom
+        if (this.props.noPointerWheel?.() || this.Document._isGroup || !this.isContentActive()) 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();
@@ -995,7 +1003,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // if ctrl is selected then zoom
                 if (e.ctrlKey) {
                     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?
+                        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 (this.props.isContentActive(true)) {
@@ -1039,16 +1047,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) },
                     }),
                     {
-                        xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
-                        yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE },
+                        xrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
+                        yrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE },
                     }
                 );
 
-                const panelDim = [this.props.PanelWidth() / this.zoomScaling(), this.props.PanelHeight() / this.zoomScaling()];
-                if (ranges.xrange.min >= panX + panelDim[0] / 2) panX = ranges.xrange.max + panelDim[0] / 2; // snaps pan position of range of content goes out of bounds
-                else if (ranges.xrange.max <= panX - panelDim[0] / 2) panX = ranges.xrange.min - panelDim[0] / 2;
-                if (ranges.yrange.min >= panY + panelDim[1] / 2) panY = ranges.yrange.max + panelDim[1] / 2;
-                else if (ranges.yrange.max <= panY - panelDim[1] / 2) panY = ranges.yrange.min - panelDim[1] / 2;
+                const panelWidMax = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+                const panelWidMin = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+                const panelHgtMax = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1);
+                const panelHgtMin = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1);
+                if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this.props.originTopLeft ? 0 : panelWidMax / 2);
+                else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this.props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2);
+                if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this.props.originTopLeft ? 0 : panelHgtMax / 2);
+                else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2);
             }
         }
         if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) {
@@ -1078,8 +1089,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 }, 10);
                 newPanY = minPanY;
             }
-            !this.Document._verticalScroll && (this.Document._panX = this.isAnnotationOverlay ? newPanX : panX);
-            !this.Document._horizontalScroll && (this.Document._panY = this.isAnnotationOverlay ? newPanY : panY);
+            !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX);
+            !this.Document._horizontalScroll && (this.Document[this.panYFieldKey] = this.isAnnotationOverlay ? newPanY : panY);
         }
     }
 
@@ -1087,8 +1098,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     nudge = (x: number, y: number, nudgeTime: number = 500) => {
         if (this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Freeform || this.props.ContainingCollectionDoc._panX !== undefined) {
             this.setPan(
-                NumCast(this.layoutDoc._panX) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
-                NumCast(this.layoutDoc._panY) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
+                NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale
+                NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(),
                 nudgeTime,
                 true
             );
@@ -1106,12 +1117,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         } else {
             const docs = this.childLayoutPairs.map(pair => pair.layout).slice();
             docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));
-            let zlast = docs.length ? Math.max(docs.length, NumCast(docs[docs.length - 1].zIndex)) : 1;
-            if (zlast - docs.length > 100) {
-                for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
-                zlast = docs.length + 1;
+            let zlast = docs.length ? Math.max(docs.length, NumCast(docs.lastElement().zIndex)) : 1;
+            if (docs.lastElement() !== doc) {
+                if (zlast - docs.length > 100) {
+                    for (let i = 0; i < docs.length; i++) doc.zIndex = i + 1;
+                    zlast = docs.length + 1;
+                }
+                doc.zIndex = zlast + 1;
             }
-            doc.zIndex = zlast + 1;
         }
     };
 
@@ -1134,8 +1147,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]);
         const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] };
         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];
+        this.layoutDoc[this.panXFieldKey] = NumCast(this.layoutDoc[this.panXFieldKey]) - newpan[0];
+        this.layoutDoc[this.panYFieldKey] = NumCast(this.layoutDoc[this.panYFieldKey]) - newpan[1];
     }
 
     calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => {
@@ -1158,8 +1171,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight();
         const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1);
         const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1);
-        const cx = NumCast(this.layoutDoc._panX) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
-        const cy = NumCast(this.layoutDoc._panY) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
+        const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this.props.isAnnotationOverlay ? pw / 2 : 0);
+        const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this.props.isAnnotationOverlay ? ph / 2 : 0);
         const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 };
         const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top));
         const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0;
@@ -1171,8 +1184,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             };
         }
         return {
-            panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panX) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
-            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc._panY) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
+            panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panXFieldKey]) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right),
+            panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panYFieldKey]) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot),
         };
     };
 
@@ -1459,7 +1472,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
         if (this.props.isAnnotationOverlay && this.props.Document[this.scaleFieldKey]) {
             // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar
-            if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
+            if (this.props.viewField) this.props.Document[this.scaleFieldKey] = Math.min(1, this.zoomScaling());
             else this.props.Document[this.scaleFieldKey] = Math.max(1, this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey]));
         }
 
@@ -1501,7 +1514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                     cbounds => {
                         if (cbounds) {
                             const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2];
-                            const p = [NumCast(this.layoutDoc._panX), NumCast(this.layoutDoc._panY)];
+                            const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])];
                             const pbounds = {
                                 x: cbounds.x - p[0] + c[0],
                                 y: cbounds.y - p[1] + c[1],
@@ -1511,8 +1524,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                             if (Number.isFinite(pbounds.r - pbounds.x) && Number.isFinite(pbounds.b - pbounds.y)) {
                                 this.layoutDoc._width = pbounds.r - pbounds.x;
                                 this.layoutDoc._height = pbounds.b - pbounds.y;
-                                this.layoutDoc._panX = (cbounds.r + cbounds.x) / 2;
-                                this.layoutDoc._panY = (cbounds.b + cbounds.y) / 2;
+                                this.layoutDoc[this.panXFieldKey] = (cbounds.r + cbounds.x) / 2;
+                                this.layoutDoc[this.panYFieldKey] = (cbounds.b + cbounds.y) / 2;
                                 this.layoutDoc.x = pbounds.x;
                                 this.layoutDoc.y = pbounds.y;
                             }
@@ -1634,8 +1647,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0;
             const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0;
             if (deltaX !== 0 || deltaY !== 0) {
-                this.Document._panY = NumCast(this.Document._panY) + deltaY / 2;
-                this.Document._panX = NumCast(this.Document._panX) + deltaX / 2;
+                this.Document[this.panYFieldKey] = NumCast(this.Document[this.panYFieldKey]) + deltaY / 2;
+                this.Document[this.panXFieldKey] = NumCast(this.Document[this.panXFieldKey]) + deltaX / 2;
             }
         }
         e.stopPropagation();
@@ -1659,8 +1672,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const height = Math.max(...docs.map(doc => NumCast(doc._height))) + 20;
         const dim = Math.ceil(Math.sqrt(docs.length));
         docs.forEach((doc, i) => {
-            doc.x = NumCast(this.Document._panX) + (i % dim) * width - (width * dim) / 2;
-            doc.y = NumCast(this.Document._panY) + Math.floor(i / dim) * height - (height * dim) / 2;
+            doc.x = NumCast(this.Document[this.panXFieldKey]) + (i % dim) * width - (width * dim) / 2;
+            doc.y = NumCast(this.Document[this.panYFieldKey]) + Math.floor(i / dim) * height - (height * dim) / 2;
         });
     };
 
@@ -1675,7 +1688,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         appearanceItems.push({
             description: 'Reset View',
             event: () => {
-                this.props.Document._panX = this.props.Document._panY = 0;
+                this.props.Document[this.panXFieldKey] = this.props.Document[this.panYFieldKey] = 0;
                 this.props.Document[this.scaleFieldKey] = 1;
             },
             icon: 'compress-arrows-alt',
@@ -1795,11 +1808,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
 
-    children = () => {
+    get children() {
         this.incrementalRender();
-        const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : [];
+        const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : this.props.children ? [this.props.children] : [];
         return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />];
-    };
+    }
 
     @computed get placeholder() {
         return (
@@ -1843,6 +1856,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                                 PanelHeight={this.props.PanelHeight}
                                 panX={this.panX}
                                 panY={this.panY}
+                                nativeDimScaling={this.nativeDim}
                                 zoomScaling={this.zoomScaling}
                                 layoutDoc={this.layoutDoc}
                                 isAnnotationOverlay={this.isAnnotationOverlay}
@@ -1877,6 +1891,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const wscale = nw ? this.props.PanelWidth() / nw : 1;
         return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale;
     }
+    nativeDim = () => this.nativeDimScaling;
 
     private groupDropDisposer?: DragManager.DragDropDisposer;
     protected createGroupEventsTarget = (ele: HTMLDivElement) => {
@@ -1912,7 +1927,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ref={r => {
                     this.createDashEventsTarget(r);
                     // prevent wheel events from passivly propagating up through containers
-                    r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
+                    !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false });
                 }}
                 onWheel={this.onPointerWheel}
                 onClick={this.onClick}
@@ -1985,7 +2000,8 @@ interface CollectionFreeFormViewPannableContentsProps {
     transform: () => string;
     zoomScaling: () => number;
     viewDefDivClick?: ScriptField;
-    children: () => JSX.Element[];
+    children?: React.ReactNode | undefined;
+    //children: () => JSX.Element[];
     transition?: string;
     presPaths: () => JSX.Element | null;
     presPinView?: boolean;
@@ -2079,7 +2095,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
                     width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable.  This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection
                     //willChange: "transform"
                 }}>
-                {this.props.children()}
+                {this.props.children}
                 {!this.props.brushView.width ? null : (
                     <div
                         className="collectionFreeFormView-brushView"
@@ -2107,6 +2123,7 @@ interface CollectionFreeFormViewBackgroundGridProps {
     PanelWidth: () => number;
     PanelHeight: () => number;
     isAnnotationOverlay?: boolean;
+    nativeDimScaling: () => number;
     zoomScaling: () => number;
     layoutDoc: Doc;
     cachedCenteringShiftX: number;
@@ -2124,10 +2141,10 @@ class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFor
         const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling();
         const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling();
         const renderGridSpace = gridSpace * this.props.zoomScaling();
-        const w = this.props.PanelWidth() + 2 * renderGridSpace;
-        const h = this.props.PanelHeight() + 2 * renderGridSpace;
+        const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace;
+        const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace;
         const strokeStyle = Doc.ActiveDashboard?.colorScheme === ColorScheme.Dark ? 'rgba(255,255,255,0.5)' : 'rgba(0, 0,0,0.5)';
-        return (
+        return !this.props.nativeDimScaling() ? null : (
             <canvas
                 className="collectionFreeFormView-grid"
                 width={w}
@@ -2174,7 +2191,7 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY
         const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined;
         let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView;
         while (parFfview?.rootDoc._isGroup) parFfview = parFfview.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
+        const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 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);
     } else {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index f16371592..043fe0bcf 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -237,6 +237,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     onPointerDown = (e: React.PointerEvent): void => {
+        // if (this.props.pointerEvents?.() === 'none') return;
         this._downX = this._lastX = e.clientX;
         this._downY = this._lastY = e.clientY;
         if (!(e.nativeEvent as any).marqueeHit) {
@@ -345,6 +346,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
 
     @action
     onClick = (e: React.MouseEvent): void => {
+        if (this.props.pointerEvents?.() === 'none') return;
         if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) {
             if (Doc.ActiveTool === InkTool.None) {
                 if (!(e.nativeEvent as any).marqueeHit) {
diff --git a/src/client/views/collections/collectionGrid/CollectionGridView.tsx b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
index 9468c5f06..e8ae88ae5 100644
--- a/src/client/views/collections/collectionGrid/CollectionGridView.tsx
+++ b/src/client/views/collections/collectionGrid/CollectionGridView.tsx
@@ -4,7 +4,7 @@ import * as React from 'react';
 import { Doc, Opt } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
@@ -186,7 +186,10 @@ export class CollectionGridView extends CollectionSubView() {
     getDisplayDoc(layout: Doc, dxf: () => Transform, width: () => number, height: () => number) {
         return (
             <DocumentView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                {...this.props}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                setContentView={emptyFunction}
                 Document={layout}
                 DataDoc={layout.resolvedDataDoc as Doc}
                 isContentActive={this.isChildContentActive}
@@ -196,7 +199,7 @@ export class CollectionGridView extends CollectionSubView() {
                 whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged}
                 onClick={this.onChildClickHandler}
                 renderDepth={this.props.renderDepth + 1}
-                dontCenter={this.props.Document.centerY ? '' : 'y'}
+                dontCenter={this.props.Document.centerY ? undefined : 'y'}
             />
         );
     }
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index d47c9762c..fd9bcf681 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -1,23 +1,20 @@
 import React = require('react');
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, observable, ObservableMap, trace, untracked } from 'mobx';
+import { action, computed, observable, ObservableMap, untracked } from 'mobx';
 import { observer } from 'mobx-react';
-import { Doc, DocListCast, Field, StrListCast } from '../../../../fields/Doc';
+import { computedFn } from 'mobx-utils';
+import { Doc, Field, StrListCast } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { List } from '../../../../fields/List';
-import { RichTextField } from '../../../../fields/RichTextField';
 import { listSpec } from '../../../../fields/Schema';
 import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types';
-import { ImageField } from '../../../../fields/URLField';
 import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils';
 import { Docs, DocUtils } from '../../../documents/Documents';
 import { DocumentManager } from '../../../util/DocumentManager';
 import { DragManager } from '../../../util/DragManager';
 import { SelectionManager } from '../../../util/SelectionManager';
-import { Transform } from '../../../util/Transform';
 import { undoBatch } from '../../../util/UndoManager';
 import { ContextMenu } from '../../ContextMenu';
-import { ContextMenuProps } from '../../ContextMenuItem';
 import { EditableView } from '../../EditableView';
 import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
@@ -215,9 +212,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    addNewKey = (key: string, defaultVal: any) => {
-        this.childDocs.forEach(doc => (doc[key] = defaultVal));
-    };
+    addNewKey = (key: string, defaultVal: any) => this.childDocs.forEach(doc => (doc[key] = defaultVal));
 
     @undoBatch
     @action
@@ -303,9 +298,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    addRowRef = (doc: Doc, ref: HTMLDivElement) => {
-        this._rowEles.set(doc, ref);
-    };
+    addRowRef = (doc: Doc, ref: HTMLDivElement) => this._rowEles.set(doc, ref);
 
     @action
     setColRef = (index: number, ref: HTMLDivElement) => {
@@ -405,68 +398,12 @@ export class CollectionSchemaView extends CollectionSubView() {
 
     menuCallback = (x: number, y: number) => {
         ContextMenu.Instance.clearItems();
-        const layoutItems: ContextMenuProps[] = [];
-        const docItems: ContextMenuProps[] = [];
-        const dataDoc = this.props.DataDoc || this.props.Document;
-
-        DocUtils.addDocumentCreatorMenuItems(
-            doc => {
-                FormattedTextBox.SelectOnLoad = StrCast(doc[Id]);
-                return this.addRow(doc);
-            },
-            this.addRow,
-            x,
-            y,
-            true
-        );
 
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => dataDoc[fieldKey] instanceof RichTextField || dataDoc[fieldKey] instanceof ImageField || typeof dataDoc[fieldKey] === 'string')
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = DocUtils.DocumentFromField(dataDoc, fieldKey, Doc.GetProto(this.props.Document));
-                        if (created) {
-                            if (this.props.Document.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, this.props.Document);
-                            }
-                            return this.addRow(created);
-                        }
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        Array.from(Object.keys(Doc.GetProto(dataDoc)))
-            .filter(fieldKey => DocListCast(dataDoc[fieldKey]).length)
-            .map(fieldKey =>
-                docItems.push({
-                    description: ':' + fieldKey,
-                    event: () => {
-                        const created = Docs.Create.CarouselDocument([], { _width: 400, _height: 200, title: fieldKey });
-                        if (created) {
-                            const container = this.props.Document.resolvedDataDoc ? Doc.GetProto(this.props.Document) : this.props.Document;
-                            if (container.isTemplateDoc) {
-                                Doc.MakeMetadataFieldTemplate(created, container);
-                                return Doc.AddDocToList(container, Doc.LayoutFieldKey(container), created);
-                            }
-                            return this.addRow(created) || false;
-                        }
-                    },
-                    icon: 'compress-arrows-alt',
-                })
-            );
-        !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Doc Fields ...', subitems: docItems, icon: 'eye' });
-        !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' });
+        DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true);
+
         ContextMenu.Instance.setDefaultItem('::', (name: string): void => {
             Doc.GetProto(this.props.Document)[name] = '';
-            const created = Docs.Create.TextDocument('', { title: name, _autoHeight: true });
-            if (created) {
-                if (this.props.Document.isTemplateDoc) {
-                    Doc.MakeMetadataFieldTemplate(created, this.props.Document);
-                }
-                this.addRow(created);
-            }
+            this.addRow(Docs.Create.TextDocument('', { title: name, _autoHeight: true }));
         });
         ContextMenu.Instance.displayMenu(x, y, undefined, true);
     };
@@ -543,9 +480,7 @@ export class CollectionSchemaView extends CollectionSubView() {
     };
 
     @action
-    closeColumnMenu = () => {
-        this._columnMenuIndex = undefined;
-    };
+    closeColumnMenu = () => (this._columnMenuIndex = undefined);
 
     @action
     openFilterMenu = (index: number) => {
@@ -596,19 +531,14 @@ export class CollectionSchemaView extends CollectionSubView() {
     getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field);
 
     removeFieldFilters = (field: string) => {
-        this.getFieldFilters(field).forEach(filter => {
-            Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove');
-        });
+        this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove'));
     };
 
     onFilterKeyDown = (e: React.KeyboardEvent) => {
+        //prettier-ignore
         switch (e.key) {
-            case 'Enter':
-                this.closeFilterMenu(true);
-                break;
-            case 'Escape':
-                this.closeFilterMenu(false);
-                break;
+            case 'Enter' : this.closeFilterMenu(true); break;
+            case 'Escape': this.closeFilterMenu(false);break;
         }
     };
 
@@ -903,6 +833,7 @@ interface CollectionSchemaViewDocsProps {
 class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> {
     tableWidthFunc = () => this.props.schema.tableWidth;
     rowHeightFunc = () => CollectionSchemaView._rowHeight;
+    childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -CollectionSchemaView._rowHeight - index * this.rowHeightFunc()));
     render() {
         return (
             <div className="schema-table-content">
@@ -932,7 +863,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP
                                 docRangeFilters={this.props.schema.childDocRangeFilters}
                                 searchFilterDocs={this.props.schema.searchFilterDocs}
                                 rootSelected={this.props.schema.rootSelected}
-                                ScreenToLocalTransform={Transform.Identity}
+                                ScreenToLocalTransform={this.childScreenToLocal(index)}
                                 bringToFront={emptyFunction}
                                 isDocumentActive={this.props.schema.props.childDocumentsActive?.() ? this.props.schema.props.isDocumentActive : this.props.schema.isContentActive}
                                 isContentActive={emptyFunction}
diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
index cbdaeb933..d88d67c94 100644
--- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx
@@ -39,18 +39,12 @@ export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps>
 
     @action
     onPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction);
+        setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false);
     };
 
     render() {
         return (
-            <div
-                className="schema-column-header"
-                style={{ width: this.props.columnWidths[this.props.columnIndex] }}
-                onPointerDown={this.onPointerDown}
-                ref={(col: HTMLDivElement | null) => {
-                    col && this.props.setColRef(this.props.columnIndex, col);
-                }}>
+            <div className="schema-column-header" style={{ width: this.props.columnWidths[this.props.columnIndex] }} onPointerDown={this.onPointerDown} ref={col => col && this.props.setColRef(this.props.columnIndex, col)}>
                 <div className="schema-column-resizer left" onPointerDown={e => this.props.resizeColumn(e, this.props.columnIndex)}></div>
                 <div className="schema-column-title">{this.fieldKey}</div>
 
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 384975b45..a6acf882c 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -7,7 +7,7 @@ import { Doc, DocListCast } from '../../../fields/Doc';
 import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, DateCast, NumCast } from '../../../fields/Types';
 import { AudioField, nullAudio } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnFalse, setupMoveUpEvents } from '../../../Utils';
+import { emptyFunction, formatTime, returnFalse, setupMoveUpEvents } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { Networking } from '../../Network';
 import { DragManager } from '../../util/DragManager';
@@ -19,7 +19,6 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
 import './AudioBox.scss';
 import { FieldView, FieldViewProps } from './FieldView';
-import { SelectionManager } from '../../util/SelectionManager';
 import { PinProps, PresBox } from './trails';
 
 /**
@@ -655,7 +654,8 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         return (
             <CollectionStackedTimeline
                 ref={action((r: any) => (this._stackedTimeline = r))}
-                {...OmitKeys(this.props, ['CollectionFreeFormDocumentView']).omit}
+                {...this.props}
+                CollectionFreeFormDocumentView={undefined}
                 fieldKey={this.annotationKey}
                 dictationKey={this.fieldKey + '-dictation'}
                 mediaPath={this.path}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index ecffe6c4f..ace388c57 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -3,7 +3,7 @@ import { action, observable } from 'mobx';
 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 { emptyFunction, returnFalse, returnNone, returnZero, setupMoveUpEvents } from '../../../Utils';
 import { Docs } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { SnappingManager } from '../../util/SnappingManager';
@@ -122,7 +122,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                         ref={r => {
                             //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true });
                         }}
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                        {...this.props}
+                        NativeWidth={returnZero}
+                        NativeHeight={returnZero}
                         isContentActive={returnFalse}
                         isDocumentActive={returnFalse}
                         styleProvider={this.docStyleProvider}
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 091a6b050..c7bf37a45 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -12,7 +12,7 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
-import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, OmitKeys, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
+import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils';
 import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils';
 import { DocServer } from '../../DocServer';
 import { Docs, DocUtils } from '../../documents/Documents';
@@ -512,7 +512,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 this.props.isDocumentActive?.() &&
                 !this.props.onBrowseClick?.() &&
                 !this.Document.ignoreClick &&
-                !e.ctrlKey &&
                 e.button === 0 &&
                 this.pointerEvents !== 'none' &&
                 !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)
@@ -705,22 +704,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const appearance = cm.findByDescription('UI Controls...');
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
-            !Doc.noviceMode &&
-                appearanceItems.push({
-                    description: 'Add a Field',
-                    event: () => {
-                        const alias = Doc.MakeAlias(this.rootDoc);
-                        alias.layout = FormattedTextBox.LayoutString('newfield');
-                        alias.title = 'newfield';
-                        alias._height = 35;
-                        alias._width = 100;
-                        alias.syncLayoutFieldWithTitle = true;
-                        alias.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc.width);
-                        alias.y = NumCast(this.rootDoc.y);
-                        this.props.addDocument?.(alias);
-                    },
-                    icon: 'eye',
-                });
             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' });
@@ -863,7 +846,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     onClickFunc: any = () => (this.disableClickScriptFunc ? undefined : this.onClickHandler);
     setHeight = (height: number) => (this.layoutDoc._height = height);
     setContentView = action((view: { getAnchor?: (addAsAnnotation: boolean) => Doc; forward?: () => boolean; back?: () => boolean }) => (this._componentView = view));
-    isContentActive = (outsideReaction?: boolean) => {
+    isContentActive = (outsideReaction?: boolean): boolean | undefined => {
         return this.props.isContentActive() === false
             ? false
             : Doc.ActiveTool !== InkTool.None ||
@@ -1064,7 +1047,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         }
     };
 
-    captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewInternalProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
+    captionStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => this.props?.styleProvider?.(doc, props, property + ':caption');
     @computed get innards() {
         TraceMobx();
         const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1;
@@ -1079,7 +1062,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     maxHeight: `max(100%, ${20 * ffscale()}px)`,
                 }}>
                 <FormattedTextBox
-                    {...OmitKeys(this.props, ['children']).omit}
+                    {...this.props}
                     yPadding={10}
                     xPadding={10}
                     fieldKey={this.showCaption}
@@ -1090,7 +1073,6 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     dontScale={true}
                     renderDepth={this.props.renderDepth}
                     isContentActive={this.isContentActive}
-                    onClick={this.onClickFunc}
                 />
             </div>
         );
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index e53422ab7..8d3534a5c 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -20,7 +20,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
 
     select: (isCtrlPressed: boolean) => void;
     isContentActive: (outsideReaction?: boolean) => boolean | undefined;
-    isDocumentActive?: () => boolean;
+    isDocumentActive?: () => boolean | undefined;
     isSelected: (outsideReaction?: boolean) => boolean;
     setHeight?: (height: number) => void;
     NativeDimScaling?: () => number; // scaling the DocumentView does to transform its contents into its panel & needed by ScreenToLocal NOTE: Must also be added to DocumentViewInternalsProps
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 48e54b722..b5193cd20 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 { DashColor, emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { DashColor, emptyFunction, returnEmptyString, returnFalse, returnOne, returnZero, 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';
@@ -409,8 +409,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         );
     }
 
-    contentFunc = () => [this.content];
-
     private _mainCont: React.RefObject<HTMLDivElement> = React.createRef();
     private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef();
     @observable _marqueeing: number[] | undefined;
@@ -473,7 +471,10 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 }}>
                 <CollectionFreeFormView
                     ref={this._ffref}
-                    {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                    {...this.props}
+                    setContentView={emptyFunction}
+                    NativeWidth={returnZero}
+                    NativeHeight={returnZero}
                     renderDepth={this.props.renderDepth + 1}
                     fieldKey={this.annotationKey}
                     styleProvider={this.props.styleProvider}
@@ -487,11 +488,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                     focus={this.focus}
                     getScrollHeight={this.getScrollHeight}
                     NativeDimScaling={returnOne}
+                    isAnyChildContentActive={returnFalse}
                     whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     removeDocument={this.removeDocument}
                     moveDocument={this.moveDocument}
                     addDocument={this.addDocument}>
-                    {this.contentFunc}
+                    {this.content}
                 </CollectionFreeFormView>
                 {this.annotationLayer}
                 {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : (
diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx
index ea8f47b39..36be7d257 100644
--- a/src/client/views/nodes/MapBox/MapBox.tsx
+++ b/src/client/views/nodes/MapBox/MapBox.tsx
@@ -8,7 +8,7 @@ import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
 import { InkTool } from '../../../../fields/InkField';
 import { NumCast, StrCast } from '../../../../fields/Types';
-import { emptyFunction, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { DragManager } from '../../../util/DragManager';
 import { SnappingManager } from '../../../util/SnappingManager';
@@ -635,7 +635,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                                 .map(marker => (
                                     <MapBoxInfoWindow
                                         key={marker[Id]}
-                                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                                        {...this.props}
+                                        setContentView={emptyFunction}
                                         place={marker}
                                         markerMap={this.markerMap}
                                         PanelWidth={this.infoWidth}
@@ -674,6 +675,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         rootDoc={this.rootDoc}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
+                        usePanelWidth={true}
                         showSidebar={this.SidebarShown}
                         nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
index 00bedafbe..d65ef9c4c 100644
--- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
+++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx
@@ -4,7 +4,7 @@ import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Doc } from '../../../../fields/Doc';
 import { Id } from '../../../../fields/FieldSymbols';
-import { emptyFunction, OmitKeys, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
+import { emptyFunction, returnAll, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero, setupMoveUpEvents } from '../../../../Utils';
 import { Docs } from '../../../documents/Documents';
 import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
 import { CollectionNoteTakingView } from '../../collections/CollectionNoteTakingView';
@@ -52,7 +52,8 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi
                     <div style={{ width: this.props.PanelWidth(), height: this.props.PanelHeight() }}>
                         <CollectionStackingView
                             ref={r => (this._stack = r)}
-                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                            {...this.props}
+                            setContentView={emptyFunction}
                             Document={this.props.place}
                             DataDoc={undefined}
                             fieldKey="data"
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 5f207cc0d..a254edd87 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -5,16 +5,20 @@ import * as Pdfjs from 'pdfjs-dist';
 import 'pdfjs-dist/web/pdf_viewer.css';
 import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { InkTool } from '../../../fields/InkField';
+import { ComputedField } from '../../../fields/ScriptField';
+import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, PdfField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
-import { DocumentType } from '../../documents/DocumentTypes';
+import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { DocumentManager } from '../../util/DocumentManager';
 import { KeyCodes } from '../../util/KeyCodes';
+import { SelectionManager } from '../../util/SelectionManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../DocComponent';
@@ -41,7 +45,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     private _initialScrollTarget: Opt<Doc>;
     private _pdfViewer: PDFViewer | undefined;
     private _searchRef = React.createRef<HTMLInputElement>();
-    private _selectReactionDisposer: IReactionDisposer | undefined;
+    private _disposers: { [name: string]: IReactionDisposer } = {};
     private _sidebarRef = React.createRef<SidebarAnnos>();
 
     @observable private _searching: boolean = false;
@@ -184,11 +188,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
 
     componentWillUnmount() {
-        this._selectReactionDisposer?.();
+        Object.values(this._disposers).forEach(disposer => disposer?.());
     }
     componentDidMount() {
         this.props.setContentView?.(this);
-        this._selectReactionDisposer = reaction(
+        this._disposers.select = reaction(
             () => this.props.isSelected(),
             () => {
                 document.removeEventListener('keydown', this.onKeyDown);
@@ -196,6 +200,16 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             },
             { fireImmediately: true }
         );
+        this._disposers.scroll = reaction(
+            () => this.rootDoc.scrollTop,
+            () => {
+                if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '-panY'])) instanceof ComputedField)) {
+                    this.props.Document[this.SidebarKey + '-panY'] = ComputedField.MakeFunction('this.scrollTop');
+                }
+                this.props.Document[this.SidebarKey + '-viewScale'] = 1;
+                this.props.Document[this.SidebarKey + '-panX'] = 0;
+            }
+        );
     }
 
     brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view);
@@ -303,7 +317,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
 
     // adding external documents; to sidebar key
     // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation")
-    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => {
+    sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
         return this.addDocument(doc, sidebarKey);
     };
@@ -428,6 +442,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     };
     specificContextMenu = (e: React.MouseEvent): void => {
         const funcs: ContextMenuProps[] = [];
+        funcs.push({
+            description: 'Toggle Sidebar Type',
+            event: () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform),
+            icon: 'expand-arrows-alt',
+        });
         funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
         funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
         //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
@@ -467,12 +486,87 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         );
     }
 
+    public get SidebarKey() {
+        return this.fieldKey + '-sidebar';
+    }
+    @computed get pdfScale() {
+        const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']);
+        const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth);
+        const pdfRatio = pdfNativeWidth / nativeWidth;
+        return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth;
+    }
+    @computed get sidebarNativeWidth() {
+        return this.sidebarWidth() / this.pdfScale;
+    }
+    @computed get sidebarNativeHeight() {
+        return this.props.PanelHeight() / this.pdfScale;
+    }
+    sidebarNativeWidthFunc = () => this.sidebarNativeWidth;
+    sidebarNativeHeightFunc = () => this.sidebarNativeHeight;
+    sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
+    sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey);
+    sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate((this.sidebarWidth() - this.props.PanelWidth()) / this.pdfScale, 0);
+    @computed get sidebarCollection() {
+        const renderComponent = (tag: string) => {
+            const ComponentTag = tag === CollectionViewType.Freeform ? CollectionFreeFormView : CollectionStackingView;
+            return ComponentTag === CollectionStackingView ? (
+                <SidebarAnnos
+                    ref={this._sidebarRef}
+                    {...this.props}
+                    rootDoc={this.rootDoc}
+                    layoutDoc={this.layoutDoc}
+                    dataDoc={this.dataDoc}
+                    setHeight={emptyFunction}
+                    nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
+                    showSidebar={this.SidebarShown}
+                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                    sidebarAddDocument={this.sidebarAddDocument}
+                    moveDocument={this.moveDocument}
+                    removeDocument={this.removeDocument}
+                    //PanelWidth={this.sidebarWidth}
+                    //ScreenToLocalTransform={this.sidebarScreenToLocal}
+                />
+            ) : (
+                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
+                    <ComponentTag
+                        {...this.props}
+                        setContentView={emptyFunction} // override setContentView to do nothing
+                        NativeWidth={this.sidebarNativeWidthFunc}
+                        NativeHeight={this.sidebarNativeHeightFunc}
+                        PanelHeight={this.props.PanelHeight}
+                        PanelWidth={this.sidebarWidth}
+                        xPadding={0}
+                        yPadding={0}
+                        viewField={this.SidebarKey}
+                        isAnnotationOverlay={false}
+                        originTopLeft={true}
+                        isAnyChildContentActive={this.isAnyChildContentActive}
+                        select={emptyFunction}
+                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+                        removeDocument={this.sidebarRemDocument}
+                        moveDocument={this.sidebarMoveDocument}
+                        addDocument={this.sidebarAddDocument}
+                        CollectionView={undefined}
+                        ScreenToLocalTransform={this.sidebarScreenToLocal}
+                        renderDepth={this.props.renderDepth + 1}
+                        noSidebar={true}
+                        fieldKey={this.SidebarKey}
+                    />
+                </div>
+            );
+        };
+        return (
+            <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}>
+                {renderComponent(StrCast(this.layoutDoc.sidebarViewType))}
+            </div>
+        );
+    }
     isPdfContentActive = () => this.isAnyChildContentActive() || this.props.isSelected() || (this.props.renderDepth === 0 && LightboxView.IsLightboxDocView(this.props.docViewPath()));
     @computed get renderPdfView() {
         TraceMobx();
         const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1;
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
-        return (
+        return !this._pdf ? null : (
             <div
                 className={'pdfBox'}
                 onContextMenu={this.specificContextMenu}
@@ -496,7 +590,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         addDocTab={this.sidebarAddDocTab}
                         layoutDoc={this.layoutDoc}
                         dataDoc={this.dataDoc}
-                        pdf={this._pdf!}
+                        pdf={this._pdf}
                         focus={this.focus}
                         url={this.pdfUrl!.url.pathname}
                         isContentActive={this.isPdfContentActive}
@@ -510,22 +604,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                         crop={this.crop}
                     />
                 </div>
-                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>
-                    <SidebarAnnos
-                        ref={this._sidebarRef}
-                        {...this.props}
-                        rootDoc={this.rootDoc}
-                        layoutDoc={this.layoutDoc}
-                        dataDoc={this.dataDoc}
-                        setHeight={emptyFunction}
-                        nativeWidth={this._previewNativeWidth ?? NumCast(this.layoutDoc._nativeWidth)}
-                        showSidebar={this.SidebarShown}
-                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
-                        sidebarAddDocument={this.sidebarAddDocument}
-                        moveDocument={this.moveDocument}
-                        removeDocument={this.removeDocument}
-                    />
-                </div>
+                <div style={{ position: 'absolute', height: '100%', right: 0, top: 0, width: `calc(100 * ${this.sidebarWidth() / this.props.PanelWidth()}%` }}>{this.sidebarCollection}</div>
                 {this.settingsPanel()}
             </div>
         );
@@ -535,21 +614,17 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
     static pdfpromise = new Map<string, Promise<Pdfjs.PDFDocumentProxy>>();
     render() {
         TraceMobx();
-        if (this._pdf) {
-            if (!this.props.thumbShown?.()) {
-                return this.renderPdfView;
-            }
-            return null;
-        }
+        if (this.props.thumbShown?.()) return null;
+        const pdfView = this.renderPdfView;
 
         const href = this.pdfUrl?.url.href;
-        if (href) {
+        if (!pdfView && href) {
             if (PDFBox.pdfcache.get(href)) setTimeout(action(() => (this._pdf = PDFBox.pdfcache.get(href))));
             else {
                 if (!PDFBox.pdfpromise.get(href)) PDFBox.pdfpromise.set(href, Pdfjs.getDocument(href).promise);
                 PDFBox.pdfpromise.get(href)?.then(action((pdf: any) => PDFBox.pdfcache.set(href, (this._pdf = pdf))));
             }
         }
-        return this.renderTitleBox;
+        return pdfView ?? this.renderTitleBox;
     }
 }
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 61e4894f0..db11a7776 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -11,7 +11,7 @@ import { ComputedField } from '../../../fields/ScriptField';
 import { Cast, NumCast } from '../../../fields/Types';
 import { AudioField, VideoField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, OmitKeys, returnFalse, returnOne } from '../../../Utils';
+import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
@@ -277,7 +277,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
         dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState');
         this.dataDoc[this.fieldKey + '-dictation'] = dictationText;
     };
-    contentFunc = () => [this.threed, this.content];
     videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth();
     formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight());
     render() {
@@ -287,7 +286,10 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                 <div className="videoBox-viewer">
                     <div style={{ position: 'relative', height: this.videoPanelHeight() }}>
                         <CollectionFreeFormView
-                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                            {...this.props}
+                            setContentView={emptyFunction}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
                             PanelHeight={this.videoPanelHeight}
                             PanelWidth={this.props.PanelWidth}
                             focus={this.props.focus}
@@ -296,6 +298,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                             select={emptyFunction}
                             isContentActive={returnFalse}
                             NativeDimScaling={returnOne}
+                            isAnyChildContentActive={returnFalse}
                             whenChildContentsActiveChanged={emptyFunction}
                             removeDocument={returnFalse}
                             moveDocument={returnFalse}
@@ -304,17 +307,19 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                             ScreenToLocalTransform={this.props.ScreenToLocalTransform}
                             renderDepth={this.props.renderDepth + 1}
                             ContainingCollectionDoc={this.props.ContainingCollectionDoc}>
-                            {this.contentFunc}
+                            <>
+                                {this.threed}
+                                {this.content}
+                            </>
                         </CollectionFreeFormView>
                     </div>
                     <div style={{ position: 'relative', height: this.formattedPanelHeight() }}>
                         {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : (
                             <FormattedTextBox
-                                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight']).omit}
+                                {...this.props}
                                 Document={this.dataDoc[this.fieldKey + '-dictation']}
                                 fieldKey={'text'}
                                 PanelHeight={this.formattedPanelHeight}
-                                isAnnotationOverlay={true}
                                 select={emptyFunction}
                                 isContentActive={emptyFunction}
                                 NativeDimScaling={returnOne}
@@ -324,7 +329,6 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
                                 removeDocument={returnFalse}
                                 moveDocument={returnFalse}
                                 addDocument={returnFalse}
-                                CollectionView={undefined}
                                 renderDepth={this.props.renderDepth + 1}
                                 ContainingCollectionDoc={this.props.ContainingCollectionDoc}></FormattedTextBox>
                         )}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0b88f5fe3..0570c7fcb 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -9,7 +9,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { AudioField, ImageField, VideoField } from '../../../fields/URLField';
-import { emptyFunction, formatTime, OmitKeys, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../Utils';
+import { emptyFunction, formatTime, returnEmptyString, returnFalse, returnOne, returnZero, setupMoveUpEvents, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentType } from '../../documents/DocumentTypes';
 import { Networking } from '../../Network';
@@ -894,8 +894,6 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
 
     playing = () => this._playing;
 
-    contentFunc = () => [this.youtubeVideoId ? this.youtubeContent : this.content];
-
     scaling = () => this.props.NativeDimScaling?.() || 1;
 
     panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100;
@@ -1068,7 +1066,10 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             left: (this.props.PanelWidth() - this.panelWidth()) / 2,
                         }}>
                         <CollectionFreeFormView
-                            {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                            {...this.props}
+                            setContentView={emptyFunction}
+                            NativeWidth={returnZero}
+                            NativeHeight={returnZero}
                             renderDepth={this.props.renderDepth + 1}
                             fieldKey={this.annotationKey}
                             CollectionView={undefined}
@@ -1076,6 +1077,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             annotationLayerHostsContent={true}
                             PanelWidth={this.panelWidth}
                             PanelHeight={this.panelHeight}
+                            isAnyChildContentActive={returnFalse}
                             ScreenToLocalTransform={this.screenToLocalTransform}
                             docFilters={this.timelineDocFilter}
                             select={emptyFunction}
@@ -1084,7 +1086,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                             removeDocument={this.removeDocument}
                             moveDocument={this.moveDocument}
                             addDocument={this.addDocWithTimecode}>
-                            {this.contentFunc}
+                            {this.youtubeVideoId ? this.youtubeContent : this.content}
                         </CollectionFreeFormView>
                     </div>
                     {this.annotationLayer}
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index d57518a8d..66d0fd385 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -11,7 +11,7 @@ import { listSpec } from '../../../fields/Schema';
 import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
 import { ImageField, WebField } from '../../../fields/URLField';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, getWordAtPoint, OmitKeys, returnFalse, returnOne, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
+import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
@@ -305,7 +305,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             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);
+                    const focusTime = options.zoomTime ?? 500;
+                    this.goTo(scrollTo, focusTime, options.easeFunc);
+                    return focusTime;
                 } else {
                     this._initialScroll = scrollTo;
                 }
@@ -904,9 +906,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         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);
-        const renderAnnotations = (docFilters?: () => string[]) => (
+        const renderAnnotations = (docFilters: () => string[]) => (
             <CollectionFreeFormView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                {...this.props}
+                setContentView={emptyFunction}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
                 renderDepth={this.props.renderDepth + 1}
                 isAnnotationOverlay={true}
                 fieldKey={this.annotationKey}
@@ -921,6 +926,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                 dropAction={'alias'}
                 docFilters={docFilters}
                 select={emptyFunction}
+                isAnyChildContentActive={returnFalse}
                 bringToFront={emptyFunction}
                 styleProvider={this.childStyleProvider}
                 whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index ee4249b02..d8afc3227 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -20,7 +20,7 @@ import { RichTextUtils } from '../../../../fields/RichTextUtils';
 import { ComputedField } from '../../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
 import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
 import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils';
 import { DocServer } from '../../../DocServer';
 import { Docs, DocUtils } from '../../../documents/Documents';
@@ -1739,12 +1739,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
             }
         }
     };
-    fitContentsToBox = () => this.props.Document._fitContentsToBox;
+    fitContentsToBox = () => BoolCast(this.props.Document._fitContentsToBox);
     sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1);
     sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => {
         if (!this.layoutDoc._showSidebar) this.toggleSidebar();
-        // console.log("printting allSideBarDocs");
-        // console.log(this.allSidebarDocs);
         return this.addDocument(doc, sidebarKey);
     };
     sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey);
@@ -1785,8 +1783,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 className="formattedTextBox-sidebar-handle"
                 onPointerDown={this.sidebarDown}
                 style={{
-                    backgroundColor: backgroundColor,
-                    color: color,
+                    backgroundColor,
+                    color,
                     opacity: annotated ? 1 : undefined,
                 }}>
                 <FontAwesomeIcon icon={'comment-alt'} />
@@ -1800,33 +1798,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                 <SidebarAnnos
                     ref={this._sidebarRef}
                     {...this.props}
-                    fieldKey={this.fieldKey}
                     rootDoc={this.rootDoc}
                     layoutDoc={this.layoutDoc}
                     dataDoc={this.dataDoc}
-                    ScreenToLocalTransform={this.sidebarScreenToLocal}
+                    usePanelWidth={true}
                     nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
-                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     showSidebar={this.SidebarShown}
-                    PanelWidth={this.sidebarWidth}
-                    setHeight={this.setSidebarHeight}
+                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                     sidebarAddDocument={this.sidebarAddDocument}
                     moveDocument={this.moveDocument}
                     removeDocument={this.removeDocument}
+                    ScreenToLocalTransform={this.sidebarScreenToLocal}
+                    fieldKey={this.fieldKey}
+                    PanelWidth={this.sidebarWidth}
+                    setHeight={this.setSidebarHeight}
                 />
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
                     <ComponentTag
-                        {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                        {...this.props}
+                        setContentView={emptyFunction}
                         NativeWidth={returnZero}
                         NativeHeight={returnZero}
                         PanelHeight={this.props.PanelHeight}
                         PanelWidth={this.sidebarWidth}
                         xPadding={0}
                         yPadding={0}
-                        scaleField={this.SidebarKey + '-scale'}
+                        viewField={this.SidebarKey}
                         isAnnotationOverlay={false}
                         select={emptyFunction}
+                        isAnyChildContentActive={returnFalse}
                         NativeDimScaling={this.sidebarContentScaling}
                         whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                         removeDocument={this.sidebarRemDocument}
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index c5060a2c2..6ab541207 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -7,7 +7,7 @@ import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { Cast, NumCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, OmitKeys, returnNone, smoothScroll, Utils } from '../../../Utils';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnAll, returnFalse, returnNone, returnTrue, returnZero, smoothScroll, Utils } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { SelectionManager } from '../../util/SelectionManager';
@@ -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, OpenWhere } from '../nodes/DocumentView';
+import { DocFocusOptions, DocumentViewProps } from '../nodes/DocumentView';
 import { FieldViewProps } from '../nodes/FieldView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import { StyleProp } from '../StyleProvider';
@@ -512,7 +512,7 @@ export class PDFViewer extends React.Component<IViewerProps> {
         return this.props.styleProvider?.(doc, props, property);
     };
 
-    renderAnnotations = (docFilters?: () => string[], mixBlendMode?: any, display?: string) => (
+    renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => (
         <div
             className="pdfViewerDash-overlay"
             style={{
@@ -522,9 +522,12 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
             }}>
             <CollectionFreeFormView
-                {...OmitKeys(this.props, ['NativeWidth', 'NativeHeight', 'setContentView']).omit}
+                {...this.props}
+                NativeWidth={returnZero}
+                NativeHeight={returnZero}
+                setContentView={emptyFunction} // override setContentView to do nothing
+                pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone}
                 renderDepth={this.props.renderDepth + 1}
-                pointerEvents={returnNone}
                 isAnnotationOverlay={true}
                 fieldKey={this.props.fieldKey + '-annotations'}
                 CollectionView={undefined}
@@ -534,6 +537,8 @@ export class PDFViewer extends React.Component<IViewerProps> {
                 PanelHeight={this.panelHeight}
                 PanelWidth={this.panelWidth}
                 ScreenToLocalTransform={this.overlayTransform}
+                isAnyChildContentActive={returnFalse}
+                isAnnotationOverlayScrollable={true}
                 dropAction={'alias'}
                 docFilters={docFilters}
                 select={emptyFunction}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 553c4525c..cc024d83a 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -1213,7 +1213,7 @@ export namespace Doc {
         return doc[StrCast(doc.layoutKey, 'layout')];
     }
     export function LayoutFieldKey(doc: Doc): string {
-        return StrCast(Doc.Layout(doc).layout).split("'")[1];
+        return StrCast(Doc.Layout(doc)[StrCast(doc.layoutKey, 'layout')]).split("'")[1]; // bcz: TODO check on this .  used to always reference 'layout', now it uses the layout speicfied by the current layoutKey
     }
     export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) {
         return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1);
@@ -1222,9 +1222,10 @@ export namespace Doc {
         return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeWidth'], useWidth ? doc[WidthSym]() : 0));
     }
     export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) {
-        const dheight = doc ? NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0) : 0;
-        const nheight = doc ? (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]() : 0;
-        return !doc ? 0 : NumCast(doc._nativeHeight, nheight || dheight);
+        if (!doc) return 0;
+        const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym]();
+        const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0);
+        return NumCast(doc._nativeHeight, nheight || dheight);
     }
     export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) {
         doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width;
-- 
cgit v1.2.3-70-g09d2


From e4b76f6c1dee63c49408c48d7b2bc98e62dc813a Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Wed, 5 Apr 2023 12:19:01 -0400
Subject: adjusted general context menu items to always appear last.  cleaned
 up makeLink api,

---
 src/client/documents/Documents.ts                  | 32 +++++-----
 src/client/util/DragManager.ts                     |  1 -
 src/client/util/LinkFollower.ts                    |  2 -
 src/client/views/ContextMenu.tsx                   | 12 ++--
 src/client/views/DocumentButtonBar.tsx             |  4 +-
 src/client/views/MainView.tsx                      |  1 +
 src/client/views/SidebarAnnos.tsx                  |  2 +-
 .../views/collections/CollectionNoteTakingView.tsx |  2 +-
 .../views/collections/CollectionStackingView.tsx   | 28 ++++-----
 src/client/views/collections/CollectionSubView.tsx |  2 +-
 src/client/views/collections/TreeView.tsx          |  2 +-
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 26 ++------
 .../collections/collectionFreeForm/MarqueeView.tsx |  2 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |  2 +-
 src/client/views/nodes/DocumentView.tsx            | 70 +++++++++++++++-------
 src/client/views/nodes/ImageBox.tsx                |  2 +-
 src/client/views/nodes/PDFBox.tsx                  | 31 +++++-----
 src/client/views/nodes/VideoBox.tsx                |  6 +-
 .../views/nodes/formattedText/FormattedTextBox.tsx |  4 +-
 .../views/nodes/formattedText/RichTextRules.ts     |  2 +-
 src/client/views/search/SearchBox.tsx              |  2 +-
 21 files changed, 117 insertions(+), 118 deletions(-)

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 9cb480c4a..5623ca218 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -470,7 +470,7 @@ export namespace Docs {
                 DocumentType.AUDIO,
                 {
                     layout: { view: AudioBox, dataField: defaultDataKey },
-                    options: { _height: 100, forceReflow: true, nativeDimModifiable: true },
+                    options: { _height: 100, fitWidth: true, forceReflow: true, nativeDimModifiable: true },
                 },
             ],
             [
@@ -934,13 +934,13 @@ export namespace Docs {
             return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
         }
 
-        export function LinkDocument(source: { doc: Doc; ctx?: Doc }, target: { doc: Doc; ctx?: Doc }, options: DocumentOptions = {}, id?: string) {
+        export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
             const linkDoc = InstanceFromProto(
                 Prototypes.get(DocumentType.LINK),
                 undefined,
                 {
-                    anchor1: source.doc,
-                    anchor2: target.doc,
+                    anchor1: source,
+                    anchor2: target,
                     ...options,
                 },
                 id
@@ -1294,16 +1294,14 @@ export namespace DocUtils {
         broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1));
         return DocUtils.ActiveRecordings.map(audio => {
             const sourceDoc = getSourceDoc();
-            const link = sourceDoc && DocUtils.MakeLink({ doc: sourceDoc }, { doc: audio.getAnchor(true) || audio.props.Document }, 'recording annotation:linked recording', 'recording timeline');
-            link && (link.followLinkLocation = OpenWhere.addRight);
-            return link;
+            return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { linkDisplay: false, linkRelationship: 'recording annotation:linked recording', description: 'recording timeline' });
         });
     }
 
-    export function MakeLink(source: { doc: Doc }, target: { doc: Doc }, linkRelationship: string = '', description: string = '', id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
-        if (!linkRelationship) linkRelationship = target.doc.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
-        const sv = DocumentManager.Instance.getDocumentView(source.doc);
-        if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target.doc) return;
+    export function MakeLink(source: Doc, target: Doc, linkSettings: { linkRelationship?: string; description?: string; linkDisplay?: boolean }, id?: string, allowParCollectionLink?: boolean, showPopup?: number[]) {
+        if (!linkSettings.linkRelationship) linkSettings.linkRelationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link';
+        const sv = DocumentManager.Instance.getDocumentView(source);
+        if (!allowParCollectionLink && sv?.props.ContainingCollectionDoc === target) return;
         if (target.doc === Doc.UserDoc()) return undefined;
 
         const makeLink = action((linkDoc: Doc, showPopup?: number[]) => {
@@ -1343,16 +1341,16 @@ export namespace DocUtils {
                 target,
                 {
                     title: ComputedField.MakeFunction('generateLinkTitle(self)') as any,
-                    'anchor1-useLinkSmallAnchor': source.doc.useLinkSmallAnchor ? true : undefined,
-                    'anchor2-useLinkSmallAnchor': target.doc.useLinkSmallAnchor ? true : undefined,
+                    'anchor1-useLinkSmallAnchor': source.useLinkSmallAnchor ? true : undefined,
+                    'anchor2-useLinkSmallAnchor': target.useLinkSmallAnchor ? true : undefined,
                     'acl-Public': SharingPermissions.Augment,
                     '_acl-Public': SharingPermissions.Augment,
-                    linkDisplay: true,
+                    linkDisplay: linkSettings.linkDisplay,
                     _linkAutoMove: true,
-                    linkRelationship,
+                    linkRelationship: linkSettings.linkRelationship,
                     _showCaption: 'description',
                     _showTitle: 'linkRelationship',
-                    description,
+                    description: linkSettings.description,
                 },
                 id
             ),
@@ -1700,7 +1698,7 @@ export namespace DocUtils {
                 _timecodeToShow: Cast(doc._timecodeToShow, 'number', null),
             });
             Doc.AddDocToList(context, annotationField, pushpin);
-            const pushpinLink = DocUtils.MakeLink({ doc: pushpin }, { doc: doc }, 'pushpin', '');
+            const pushpinLink = DocUtils.MakeLink(pushpin, doc, { linkRelationship: 'pushpin' }, '');
             doc._timecodeToShow = undefined;
             return pushpin;
         }
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index cc116fb46..7d2aa813f 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -168,7 +168,6 @@ export namespace DragManager {
             this.dropDocCreator = dropDocCreator;
             this.offset = [0, 0];
         }
-        linkSourceDoc?: Doc;
         dropDocCreator: (annotationOn: Doc | undefined) => Doc;
         dropDocument?: Doc;
         offset: number[];
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 785018990..df61ecece 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -9,8 +9,6 @@ import { DocumentManager } from './DocumentManager';
 import { LinkManager } from './LinkManager';
 import { SelectionManager } from './SelectionManager';
 import { UndoManager } from './UndoManager';
-
-type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
 /*
  * link doc:
  * - anchor1: doc
diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 6a530e3ae..e4c3e864b 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -102,13 +102,11 @@ export class ContextMenu extends React.Component {
         }
     }
     @action
-    moveAfter(item: ContextMenuProps, after: ContextMenuProps) {
-        if (after && this.findByDescription(after.description)) {
-            const curInd = this._items.findIndex(i => i.description === item.description);
-            this._items.splice(curInd, 1);
-            const afterInd = this._items.findIndex(i => i.description === after.description);
-            this._items.splice(afterInd + 1, 0, item);
-        }
+    moveAfter(item: ContextMenuProps, after?: ContextMenuProps) {
+        const curInd = this._items.findIndex(i => i.description === item.description);
+        this._items.splice(curInd, 1);
+        const afterInd = after && this.findByDescription(after.description) ? this._items.findIndex(i => i.description === after.description) : this._items.length;
+        this._items.splice(afterInd, 0, item);
     }
     @action
     setDefaultItem(prefix: string, item: (name: string) => void) {
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 9389fed01..3e1a0f265 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -416,7 +416,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
         const targetDoc = this.view0?.props.Document;
         return !targetDoc ? null : (
             <Tooltip title={<div className="dash-tooltip">{`Open Context Menu`}</div>}>
-                <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={e => this.openContextMenu(e)}>
+                <div className="documentButtonBar-icon" style={{ color: 'white', cursor: 'pointer' }} onClick={this.openContextMenu}>
                     <FontAwesomeIcon className="documentdecorations-icon" icon="bars" />
                 </div>
             </Tooltip>
@@ -569,7 +569,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV
             const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc;
             const trail = DocCast(anchor.presTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true);
             if (trail !== anchor.presTrail) {
-                DocUtils.MakeLink({ doc: anchor }, { doc: trail }, 'link trail');
+                DocUtils.MakeLink(anchor, trail, { linkRelationship: 'link trail' });
                 anchor.presTrail = trail;
             }
             Doc.ActivePresentation = trail;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index befb0200d..b0888df68 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -270,6 +270,7 @@ export class MainView extends React.Component {
                 fa.faLaptopCode,
                 fa.faMale,
                 fa.faCopy,
+                fa.faHome,
                 fa.faHandPointLeft,
                 fa.faHandPointRight,
                 fa.faCompass,
diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx
index 48c0150e6..2d2b0f83e 100644
--- a/src/client/views/SidebarAnnos.tsx
+++ b/src/client/views/SidebarAnnos.tsx
@@ -74,7 +74,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> {
         });
         FormattedTextBox.SelectOnLoad = target[Id];
         FormattedTextBox.DontSelectInitialText = true;
-        const link = DocUtils.MakeLink({ doc: anchor }, { doc: target }, 'inline comment:comment on');
+        const link = DocUtils.MakeLink(anchor, target, { linkRelationship: 'inline comment:comment on' });
         link && (link.linkDisplay = false);
 
         const taggedContent = this.docFilters()
diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx
index 121260680..cb5be990d 100644
--- a/src/client/views/collections/CollectionNoteTakingView.tsx
+++ b/src/client/views/collections/CollectionNoteTakingView.tsx
@@ -441,7 +441,7 @@ export class CollectionNoteTakingView extends CollectionSubView() {
         } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
             this.props.addDocument?.(source);
-            de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+            de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed
             e.stopPropagation();
         } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
         return false;
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 64ec419ec..1e02fc9d4 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -3,14 +3,14 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { CursorProperty } from 'csstype';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
-import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
+import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
 import { List } from '../../../fields/List';
 import { listSpec } from '../../../fields/Schema';
 import { SchemaHeaderField } from '../../../fields/SchemaHeaderField';
-import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { TraceMobx } from '../../../fields/util';
-import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
+import { emptyFunction, returnEmptyDoclist, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils';
 import { Docs, DocUtils } from '../../documents/Documents';
 import { CollectionViewType } from '../../documents/DocumentTypes';
 import { DragManager, dropActionType } from '../../util/DragManager';
@@ -460,7 +460,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
         } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) {
             const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' });
             this.props.addDocument?.(source);
-            de.complete.linkDocument = DocUtils.MakeLink({ doc: source }, { doc: de.complete.linkDragData.linkSourceGetAnchor() }, 'doc annotation', ''); // TODODO this is where in text links get passed
+            de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed
             e.stopPropagation();
         } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData);
         return false;
@@ -623,11 +623,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
     onContextMenu = (e: React.MouseEvent): void => {
         // need to test if propagation has stopped because GoldenLayout forces a parallel react hierarchy to be created for its top-level layout
         if (!e.isPropagationStopped()) {
-            const subItems: ContextMenuProps[] = [];
-            subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
-            subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
-            subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' });
-            ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' });
+            const cm = ContextMenu.Instance;
+            const options = cm.findByDescription('Options...');
+            const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+            optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' });
+            optionItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' });
+            optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' });
+            !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' });
         }
     };
 
@@ -735,14 +737,6 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection
                                 <EditableView {...editableViewProps} />
                             </div>
                         )}
-                        {/* {this.chromeHidden || !this.props.isSelected() ? (null) :
-                            <Switch
-                                onChange={this.onToggle}
-                                onClick={this.onToggle}
-                                defaultChecked={true}
-                                checkedChildren="edit"
-                                unCheckedChildren="view"
-                            />} */}
                     </div>
                 </div>
             </>
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5100d8d67..132ed6fb6 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -335,7 +335,7 @@ export function CollectionSubView<X>(moreProps?: X) {
                                 const focusNode = iframe?.contentDocument?.getSelection()?.focusNode as any;
                                 if (focusNode) {
                                     const anchor = srcWeb?.ComponentView?.getAnchor?.(true);
-                                    anchor && DocUtils.MakeLink({ doc: htmlDoc }, { doc: anchor });
+                                    anchor && DocUtils.MakeLink(htmlDoc, anchor, {});
                                 }
                             }
                         }
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 99b7549c0..75e76019e 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -359,7 +359,7 @@ export class TreeView extends React.Component<TreeViewProps> {
         if (de.complete.linkDragData) {
             const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor();
             const destDoc = this.doc;
-            DocUtils.MakeLink({ doc: sourceDoc }, { doc: destDoc }, 'tree link', '');
+            DocUtils.MakeLink(sourceDoc, destDoc, { linkRelationship: 'tree link' });
             e.stopPropagation();
         }
         const docDragData = de.complete.docDragData;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d39668a5d..fb0bd2a19 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -397,7 +397,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document
                 const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' });
                 this.props.addDocument?.(source);
-                de.complete.linkDocument = DocUtils.MakeLink({ doc: linkDragData.linkSourceGetAnchor() }, { doc: source }, 'annotated by:annotation of', ''); // TODODO this is where in text links get passed
+                de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { linkRelationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed
             }
             e.stopPropagation(); //  do nothing if link is dropped into any freeform view parent of dragged document
             return true;
@@ -1693,6 +1693,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             },
             icon: 'compress-arrows-alt',
         });
+        if (this.props.setContentView === emptyFunction) {
+            !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
+            return;
+        }
         !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' });
         appearanceItems.push({
             description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`,
@@ -1700,7 +1704,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt',
         });
         appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' });
-        //appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: "compress-arrows-alt" });
+        !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
         appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
 
         this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' });
@@ -1730,27 +1734,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
         const mores = ContextMenu.Instance.findByDescription('More...');
         const moreItems = mores && 'subitems' in mores ? mores.subitems : [];
-        moreItems.push({ description: 'Import exported collection', icon: 'upload', event: ({ x, y }) => this.importDocument(e.clientX, e.clientY) });
         !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
     };
 
-    importDocument = (x: number, y: number) => {
-        const input = document.createElement('input');
-        input.type = 'file';
-        input.accept = '.zip';
-        input.onchange = _e => {
-            input.files &&
-                Doc.importDocument(input.files[0]).then(doc => {
-                    if (doc instanceof Doc) {
-                        const [xx, yy] = this.getTransform().transformPoint(x, y);
-                        (doc.x = xx), (doc.y = yy);
-                        this.props.addDocument?.(doc);
-                    }
-                });
-        };
-        input.click();
-    };
-
     @undoBatch
     @action
     transcribeStrokes = (math: boolean) => {
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index 043fe0bcf..0b4f76fa1 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -527,7 +527,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque
         });
         const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' });
         const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' });
-        DocUtils.MakeLink({ doc: summary }, { doc: portal }, 'summary of:summarized by', '');
+        DocUtils.MakeLink(summary, portal, { linkRelationship: 'summary of:summarized by' });
 
         portal.hidden = true;
         this.props.addDocument?.(portal);
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index d6679b46d..df3299eef 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -142,7 +142,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
             } else if (startLink !== endLink) {
                 endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink;
                 startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink;
-                const linkDoc = DocUtils.MakeLink({ doc: startLink }, { doc: endLink }, DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined, undefined, undefined, true);
+                const linkDoc = DocUtils.MakeLink(startLink, endLink, { linkRelationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }, undefined, true);
 
                 LinkManager.currentLink = linkDoc;
 
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index c7bf37a45..159aa026a 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -617,22 +617,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
 
     @undoBatch
     @action
-    drop = async (e: Event, de: DragManager.DropEvent) => {
+    drop = (e: Event, de: DragManager.DropEvent) => {
         if (this.props.dontRegisterView || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return;
         if (this.props.Document === Doc.ActiveDashboard) {
             alert((e.target as any)?.closest?.('*.lm_content') ? "You can't perform this move most likely because you don't have permission to modify the destination." : 'Linking to document tabs not yet supported. Drop link on document content.');
             return;
         }
         const linkdrag = de.complete.annoDragData ?? de.complete.linkDragData;
-        if (linkdrag) linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
-        if (linkdrag?.linkSourceDoc) {
-            e.stopPropagation();
-            if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
-                de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
-            }
-            if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
-                const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.props.Document;
-                de.complete.linkDocument = DocUtils.MakeLink({ doc: linkdrag.linkSourceDoc }, { doc: dropDoc }, undefined, undefined, undefined, undefined, [de.x, de.y - 50]);
+        if (linkdrag) {
+            linkdrag.linkSourceDoc = linkdrag.linkSourceGetAnchor();
+            if (linkdrag.linkSourceDoc) {
+                e.stopPropagation();
+                if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) {
+                    de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined);
+                }
+                if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) {
+                    const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc;
+                    de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, undefined, [de.x, de.y - 50]);
+                }
             }
         }
     };
@@ -643,9 +645,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document && d.linkRelationship === 'portal to:portal from');
         if (!portalLink) {
             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.props.Document,
+                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]' }),
+                { linkRelationship: 'portal to:portal from' }
             );
         }
         this.Document.followLinkLocation = OpenWhere.lightbox;
@@ -653,6 +655,24 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         this.Document._isLinkButton = true;
     };
 
+    importDocument = () => {
+        const input = document.createElement('input');
+        input.type = 'file';
+        input.accept = '.zip';
+        input.onchange = _e => {
+            if (input.files) {
+                const batch = UndoManager.StartBatch('importing');
+                Doc.importDocument(input.files[0]).then(doc => {
+                    if (doc instanceof Doc) {
+                        this.props.addDocTab(doc, OpenWhere.addRight);
+                        batch.end();
+                    }
+                });
+            }
+        };
+        input.click();
+    };
+
     @action
     onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => {
         if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) {
@@ -704,9 +724,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             const appearance = cm.findByDescription('UI Controls...');
             const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];
             !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template   ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' });
-            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' });
+            !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' });
 
             if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && this.props.ContainingCollectionDoc?._viewType !== CollectionViewType.Tree) {
                 const existingOnClick = cm.findByDescription('OnClick...');
@@ -774,16 +792,21 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                     }
                     moreItems.push({ description: 'Copy ID', event: () => Utils.CopyText(Doc.globalServerPath(this.props.Document)), icon: 'fingerprint' });
                 }
-                moreItems.push({ description: 'Export collection', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
-
-                (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && moreItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
             }
 
-            if (this.props.removeDocument && !Doc.IsSystem(this.rootDoc) && Doc.ActiveDashboard !== this.props.Document) {
+            !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+        }
+        const constantItems: ContextMenuProps[] = [];
+
+        if (!Doc.IsSystem(this.rootDoc)) {
+            constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) });
+            constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() });
+            (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' });
+            if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) {
                 // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions)
-                moreItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
+                constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' });
             }
-            !more && moreItems.length && cm.addItem({ description: 'More...', subitems: moreItems, icon: 'compass' });
+            cm.addItem({ description: 'General...', noexpand: false, subitems: constantItems, icon: 'question' });
         }
         const help = cm.findByDescription('Help...');
         const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
@@ -834,7 +857,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
                 icon: 'book',
             });
         }
-        cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+        if (!help) cm.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'question' });
+        else cm.moveAfter(help);
 
         e?.stopPropagation(); // DocumentViews should stop propagation of this event
         cm.displayMenu((e?.pageX || pageX || 0) - 15, (e?.pageY || pageY || 0) - 15);
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index b5193cd20..f38ebba27 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -222,7 +222,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.panYMin = anchy / viewScale;
         croppingProto.panYMax = anchh / viewScale;
         if (addCrop) {
-            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
             cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym]();
             cropping.y = NumCast(this.rootDoc.y);
             this.props.addDocTab(cropping, OpenWhere.inParent);
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index a254edd87..e38d7e2a8 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -130,7 +130,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         croppingProto['data-nativeWidth'] = anchw;
         croppingProto['data-nativeHeight'] = anchh;
         if (addCrop) {
-            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
         }
         this.props.bringToFront(cropping);
 
@@ -344,7 +344,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
             },
             (e, movement, isClick) => !isClick && batch.end(),
             () => {
-                this.toggleSidebar();
+                onButton && this.toggleSidebar();
                 batch.end();
             }
         );
@@ -440,17 +440,20 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc);
         return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1);
     };
+    @undoBatch
+    toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform);
     specificContextMenu = (e: React.MouseEvent): void => {
-        const funcs: ContextMenuProps[] = [];
-        funcs.push({
-            description: 'Toggle Sidebar Type',
-            event: () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform),
-            icon: 'expand-arrows-alt',
-        });
-        funcs.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
-        funcs.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
-        //funcs.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
-        ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
+        const cm = ContextMenu.Instance;
+        const options = cm.findByDescription('Options...');
+        const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : [];
+        optionItems.push({ description: 'Toggle Sidebar Type', event: this.toggleSidebarType, icon: 'expand-arrows-alt' });
+        !Doc.noviceMode && optionItems.push({ description: 'update icon', event: () => this.pdfUrl && this.updateIcon(), icon: 'expand-arrows-alt' });
+        //optionItems.push({ description: "Toggle Sidebar ", event: () => this.toggleSidebar(), icon: "expand-arrows-alt" });
+        !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'asterisk' });
+        const help = cm.findByDescription('Help...');
+        const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : [];
+        helpItems.push({ description: 'Copy path', event: () => this.pdfUrl && Utils.CopyText(Utils.prepend('') + this.pdfUrl.url.pathname), icon: 'expand-arrows-alt' });
+        !help && ContextMenu.Instance.addItem({ description: 'Help...', noexpand: true, subitems: helpItems, icon: 'asterisk' });
     };
 
     @computed get renderTitleBox() {
@@ -523,8 +526,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
                     sidebarAddDocument={this.sidebarAddDocument}
                     moveDocument={this.moveDocument}
                     removeDocument={this.removeDocument}
-                    //PanelWidth={this.sidebarWidth}
-                    //ScreenToLocalTransform={this.sidebarScreenToLocal}
                 />
             ) : (
                 <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.props.DocumentView?.()!, false), true)}>
@@ -568,7 +569,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps
         const scale = previewScale * (this.props.NativeDimScaling?.() || 1);
         return !this._pdf ? null : (
             <div
-                className={'pdfBox'}
+                className="pdfBox"
                 onContextMenu={this.specificContextMenu}
                 style={{
                     display: this.props.thumbShown?.() ? 'none' : undefined,
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 0570c7fcb..e14ad4b05 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -327,7 +327,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
                 _isLinkButton: true,
             });
             this.props.addDocument?.(b);
-            DocUtils.MakeLink({ doc: b }, { doc: this.rootDoc }, 'video snapshot');
+            DocUtils.MakeLink(b, this.rootDoc, { linkRelationship: 'video snapshot' });
             Networking.PostToServer('/youtubeScreenshot', {
                 id: this.youtubeVideoId,
                 timecode: this.layoutDoc._currentTimecode,
@@ -376,7 +376,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc));
         Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc));
         this.props.addDocument?.(imageSnapshot);
-        const link = DocUtils.MakeLink({ doc: imageSnapshot }, { doc: this.getAnchor(true) }, 'video snapshot');
+        const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { linkRelationship: 'video snapshot' });
         link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3);
         setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true));
     };
@@ -1031,7 +1031,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
         croppingProto.panYMin = anchy / viewScale;
         croppingProto.panYMax = anchh / viewScale;
         if (addCrop) {
-            DocUtils.MakeLink({ doc: region }, { doc: cropping }, 'cropped image', '');
+            DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' });
         }
         this.props.bringToFront(cropping);
         return cropping;
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index d8afc3227..361e000f9 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -454,7 +454,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                             alink =
                                 alink ??
                                 (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)!);
+                                    DocUtils.MakeLink(this.props.Document, target, { linkRelationship: LinkManager.AutoKeywords })!);
                             newAutoLinks.add(alink);
                             const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }];
                             allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? []));
@@ -1273,7 +1273,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps
                         ]),
                     ]);
 
-                    const link = DocUtils.MakeLink({ doc: pdfAnchor }, { doc: this.rootDoc }, 'PDF pasted');
+                    const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { linkRelationship: 'PDF pasted' });
                     if (link) {
                         view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste'));
                     }
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
index 9e9b61db3..e691869cc 100644
--- a/src/client/views/nodes/formattedText/RichTextRules.ts
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -259,7 +259,7 @@ export class RichTextRules {
                                 this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3))));
                             }
                             const target = (docx instanceof Doc && docx) || Docs.Create.FreeformDocument([], { title: docId, _width: 500, _height: 500 }, docId);
-                            DocUtils.MakeLink({ doc: this.TextBox.getAnchor(true) }, { doc: target }, 'portal to:portal from', undefined);
+                            DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' });
 
                             const fstate = this.TextBox.EditorView?.state;
                             if (fstate && selection) {
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index fe9b13fbe..8f93f1150 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -117,7 +117,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() {
     makeLink = action((linkTo: Doc) => {
         const linkFrom = this.props.linkCreateAnchor?.();
         if (linkFrom) {
-            const link = DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo });
+            const link = DocUtils.MakeLink(linkFrom, linkTo, {});
             link && this.props.linkCreated?.(link);
         }
     });
-- 
cgit v1.2.3-70-g09d2

