From 70c998562c8560283a7d6b9a1ae78b9207e3720f Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Mon, 31 Oct 2022 13:26:52 -0400
Subject: cleaning up link count icon to be part of documentLinks view and to
 appear on tab views that have links.

---
 src/client/views/nodes/DocumentLinksButton.tsx | 67 ++++++++++++--------------
 1 file changed, 31 insertions(+), 36 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 9ffbf8e37..1d455ad08 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -4,19 +4,18 @@ import { action, computed, observable, runInAction } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc, Opt } from '../../../fields/Doc';
 import { StrCast } from '../../../fields/Types';
-import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, returnFalse, setupMoveUpEvents, StopEvent } from '../../../Utils';
 import { DocUtils } from '../../documents/Documents';
 import { DragManager } from '../../util/DragManager';
 import { Hypothesis } from '../../util/HypothesisUtils';
 import { LinkManager } from '../../util/LinkManager';
 import { undoBatch, UndoManager } from '../../util/UndoManager';
-import { Colors } from '../global/globalEnums';
 import './DocumentLinksButton.scss';
 import { DocumentView } from './DocumentView';
 import { LinkDescriptionPopup } from './LinkDescriptionPopup';
 import { TaskCompletionBox } from './TaskCompletedBox';
 import React = require('react');
+import _ = require('lodash');
 
 const higflyout = require('@hig/flyout');
 export const { anchorPoints } = higflyout;
@@ -24,10 +23,12 @@ export const Flyout = higflyout.default;
 
 interface DocumentLinksButtonProps {
     View: DocumentView;
-    Offset?: (number | undefined)[];
+    Bottom?: boolean;
     AlwaysOn?: boolean;
     InMenu?: boolean;
+    OnHover?: boolean;
     StartLink?: boolean; //whether the link HAS been started (i.e. now needs to be completed)
+    ShowCount?: boolean;
     scaling?: () => number; // how uch doc is scaled so that link buttons can invert it
 }
 @observer
@@ -119,7 +120,6 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                 DocumentLinksButton.StartLink = this.props.View.props.Document;
                 DocumentLinksButton.StartLinkView = this.props.View;
             }
-            //action(() => Doc.BrushDoc(this.props.View.Document));
         }
     };
 
@@ -256,28 +256,32 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
      * todo:glr / anh seperate functionality such as onClick onPointerDown of link menu button
      */
     @computed get linkButtonInner() {
-        const btnDim = '30px';
-        const link = <img style={{ width: '22px', height: '16px' }} src={`/assets/${'link.png'}`} />;
+        const btnDim = 30;
         const isActive = DocumentLinksButton.StartLink === this.props.View.props.Document && this.props.StartLink;
-        return !this.props.InMenu ? (
-            <div className="documentLinksButton-cont" style={{ left: this.props.Offset?.[0], top: this.props.Offset?.[1], right: this.props.Offset?.[2], bottom: this.props.Offset?.[3] }}>
-                <div
-                    className={'documentLinksButton'}
-                    onPointerDown={this.onLinkMenuOpen}
-                    onClick={this.onLinkClick}
-                    style={{
-                        backgroundColor: Colors.LIGHT_BLUE,
-                        color: Colors.BLACK,
-                        fontSize: '20px',
-                        width: btnDim,
-                        height: btnDim,
-                    }}>
-                    {Array.from(this.filteredLinks).length}
-                </div>
+        const scaling = Math.min(1, this.props.scaling?.() || 1);
+        const showLinkCount = (onHover?: boolean, offset?: boolean) => (
+            <div
+                className="documentLinksButton-showCount"
+                onPointerDown={this.onLinkMenuOpen}
+                style={{
+                    fontSize: (onHover ? btnDim / 2 : 20) * scaling,
+                    width: (onHover ? btnDim / 2 : btnDim) * scaling,
+                    height: (onHover ? btnDim / 2 : btnDim) * scaling,
+                    bottom: offset ? 5 * scaling : onHover ? (-btnDim / 2) * scaling : undefined,
+                }}>
+                <span style={{ width: '100%', display: 'inline-block', textAlign: 'center' }}>{Array.from(this.filteredLinks).length}</span>
             </div>
+        );
+        return this.props.ShowCount ? (
+            showLinkCount(this.props.OnHover, this.props.Bottom)
         ) : (
             <div className="documentLinksButton-menu">
-                {this.props.InMenu && !this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node
+                {this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
+                    <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
+                        <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
+                    </div>
+                ) : null}
+                {!this.props.StartLink && DocumentLinksButton.StartLink !== this.props.View.props.Document ? ( //if the origin node is not this node
                     <div
                         className={'documentLinksButton-endLink'}
                         ref={this._linkButton}
@@ -286,34 +290,25 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                         <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
                     </div>
                 ) : null}
-                {this.props.InMenu && this.props.StartLink ? ( //if link has been started from current node, then set behavior of link button to deactivate linking when clicked again
-                    <div className={`documentLinksButton ${isActive ? `startLink` : ``}`} ref={this._linkButton} onPointerDown={isActive ? StopEvent : this.onLinkButtonDown} onClick={isActive ? this.clearLinks : this.onLinkClick}>
-                        <FontAwesomeIcon className="documentdecorations-icon" icon="link" />
-                    </div>
-                ) : null}
             </div>
         );
     }
 
     render() {
-        TraceMobx();
-
         const menuTitle = this.props.StartLink ? 'Drag or tap to start link' : 'Tap to complete link';
         const buttonTitle = 'Tap to view links; double tap to open link collection';
-        const title = this.props.InMenu ? menuTitle : buttonTitle;
+        const title = this.props.ShowCount ? buttonTitle : menuTitle;
 
         //render circular tooltip if it isn't set to invisible and show the number of doc links the node has, and render inner-menu link button for starting/stopping links if currently in menu
         return !Array.from(this.filteredLinks).length && !this.props.AlwaysOn ? null : (
             <div
                 className="documentLinksButton-wrapper"
                 style={{
-                    transform: `scale(${this.props.scaling?.() || 1})`,
+                    position: this.props.InMenu ? 'relative' : 'absolute',
+                    top: 0,
+                    pointerEvents: 'none',
                 }}>
-                {(this.props.InMenu && (DocumentLinksButton.StartLink || this.props.StartLink)) || (!DocumentLinksButton.LinkEditorDocView && !this.props.InMenu) ? (
-                    <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>
-                ) : (
-                    this.linkButtonInner
-                )}
+                <Tooltip title={!DocumentLinksButton.LinkEditorDocView ? <div className="dash-tooltip">{title}</div> : <></>}>{this.linkButtonInner}</Tooltip>
             </div>
         );
     }
-- 
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/nodes/DocumentLinksButton.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 c6d1059e24f362a167b9ac24e6f13d1e45361da9 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 29 Nov 2022 12:05:29 -0500
Subject: changes to streamline link editing UI (got rid of LinkEditor). 
 cleaned up link (un)highlighting.

---
 src/client/documents/Documents.ts                  |   1 -
 src/client/util/DocumentManager.ts                 |  19 +-
 src/client/util/LinkFollower.ts                    |   2 +-
 src/client/util/LinkManager.ts                     |   2 +-
 src/client/util/SelectionManager.ts                |   1 -
 src/client/views/DocumentDecorations.tsx           |  14 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/PropertiesView.scss               |  15 +-
 src/client/views/PropertiesView.tsx                |  94 +++--
 .../CollectionFreeFormLinksView.tsx                |   2 +-
 src/client/views/global/globalCssVariables.scss    |   1 +
 .../views/global/globalCssVariables.scss.d.ts      |   1 +
 src/client/views/linking/LinkEditor.scss           | 334 ---------------
 src/client/views/linking/LinkEditor.tsx            | 454 ---------------------
 src/client/views/linking/LinkMenu.tsx              |  37 +-
 src/client/views/linking/LinkMenuGroup.tsx         |  50 ++-
 src/client/views/linking/LinkMenuItem.tsx          |   4 +-
 src/client/views/nodes/DocumentLinksButton.tsx     |   2 +-
 src/client/views/nodes/DocumentView.tsx            |  16 +-
 src/client/views/nodes/LinkAnchorBox.tsx           | 110 ++---
 src/fields/Doc.ts                                  |  64 +--
 21 files changed, 194 insertions(+), 1031 deletions(-)
 delete mode 100644 src/client/views/linking/LinkEditor.scss
 delete mode 100644 src/client/views/linking/LinkEditor.tsx

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

diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index c5e08eeea..eed839520 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1375,7 +1375,6 @@ export namespace DocUtils {
                     'acl-Public': SharingPermissions.Augment,
                     '_acl-Public': SharingPermissions.Augment,
                     linkDisplay: true,
-                    _hidden: true,
                     _linkAutoMove: true,
                     linkRelationship,
                     _showCaption: 'description',
diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts
index 7120eeb88..4f02a8202 100644
--- a/src/client/util/DocumentManager.ts
+++ b/src/client/util/DocumentManager.ts
@@ -1,7 +1,7 @@
 import { action, observable, runInAction } from 'mobx';
 import { Doc, DocListCast, DocListCastAsync, Opt } from '../../fields/Doc';
 import { Id } from '../../fields/FieldSymbols';
-import { Cast } from '../../fields/Types';
+import { Cast, DocCast } from '../../fields/Types';
 import { returnFalse } from '../../Utils';
 import { DocumentType } from '../documents/DocumentTypes';
 import { LightboxView } from '../views/LightboxView';
@@ -36,15 +36,14 @@ export class DocumentManager {
         //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString);
         if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
             const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1';
-            DocListCast(view.rootDoc.links).forEach(link => {
-                this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
-                    this.LinkedDocumentViews.push({
-                        a: viewAnchorIndex === 'anchor2' ? otherView : view,
-                        b: viewAnchorIndex === 'anchor2' ? view : otherView,
-                        l: link,
-                    })
-                );
-            });
+            const link = view.rootDoc;
+            this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView =>
+                this.LinkedDocumentViews.push({
+                    a: viewAnchorIndex === 'anchor2' ? otherView : view,
+                    b: viewAnchorIndex === 'anchor2' ? view : otherView,
+                    l: link,
+                })
+            );
             this.LinkAnchorBoxViews.push(view);
             // this.LinkedDocumentViews.forEach(view => console.log("  LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " +
             //     view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString));
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts
index 5374bf44b..282116f1b 100644
--- a/src/client/util/LinkFollower.ts
+++ b/src/client/util/LinkFollower.ts
@@ -67,7 +67,7 @@ export class LinkFollower {
             docViewProps.ContainingCollectionDoc,
             action(() => {
                 batch.end();
-                DocumentDecorations.Instance.overrideBounds = false;
+                Doc.AddUnlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
             }),
             altKey ? true : undefined
         );
diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts
index 49cc3218d..01f4df723 100644
--- a/src/client/util/LinkManager.ts
+++ b/src/client/util/LinkManager.ts
@@ -44,7 +44,7 @@ export class LinkManager {
                         if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) {
                             Doc.GetProto(a1)[DirectLinksSym].add(link);
                             Doc.GetProto(a2)[DirectLinksSym].add(link);
-                            Doc.GetProto(link)[DirectLinksSym].add(link);
+                            //Doc.GetProto(link)[DirectLinksSym].add(link); // bcz: links are not linked to themself, so this was a hack
                         }
                     })
                 );
diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts
index 02d672a65..a3d6f5227 100644
--- a/src/client/util/SelectionManager.ts
+++ b/src/client/util/SelectionManager.ts
@@ -4,7 +4,6 @@ import { Doc, Opt } from '../../fields/Doc';
 import { DocCast } from '../../fields/Types';
 import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
 import { DocumentView } from '../views/nodes/DocumentView';
-import { LinkManager } from './LinkManager';
 import { ScriptingGlobals } from './ScriptingGlobals';
 
 export namespace SelectionManager {
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 3efb5fb37..d1f0bf2ac 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,7 +27,7 @@ import { Colors } from './global/globalEnums';
 import { InkingStroke } from './InkingStroke';
 import { InkStrokeProperties } from './InkStrokeProperties';
 import { LightboxView } from './LightboxView';
-import { DocumentView } from './nodes/DocumentView';
+import { DocumentView, OpenWhereMod } from './nodes/DocumentView';
 import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
 import { ImageBox } from './nodes/ImageBox';
 import React = require('react');
@@ -253,17 +253,17 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         if (selectedDocs.length) {
             if (e.ctrlKey) {
                 // open an alias in a new tab with Ctrl Key
-                CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), 'right');
+                CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), OpenWhereMod.right);
             } else if (e.shiftKey) {
                 // open centered in a new workspace with Shift Key
                 const alias = Doc.MakeAlias(selectedDocs[0].props.Document);
                 alias.context = undefined;
                 alias.x = -alias[WidthSym]() / 2;
                 alias.y = -alias[HeightSym]() / 2;
-                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), 'right');
+                CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), OpenWhereMod.right);
             } else if (e.altKey) {
                 // open same document in new tab
-                CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, 'right');
+                CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right);
             } else {
                 var openDoc = selectedDocs[0].props.Document;
                 if (openDoc.layoutKey === 'layout_icon') {
@@ -720,13 +720,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P
         const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating;
         const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating;
         // if multiple documents have been opened at the same time, then don't show open button
-        const hideOpenButton =hideDecorations || 
+        const hideOpenButton =
+            hideDecorations ||
             seldocview.props.hideOpenButton ||
             seldocview.rootDoc.hideOpenButton ||
             SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) ||
             this._isRounding ||
             this._isRotating;
-        const hideDeleteButton =hideDecorations || 
+        const hideDeleteButton =
+            hideDecorations ||
             this._isRounding ||
             this._isRotating ||
             seldocview.props.hideDeleteButton ||
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 98d0378be..09063901d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -484,7 +484,7 @@ export class MainView extends React.Component {
     }
 
     globalPointerDown = action((e: PointerEvent) => {
-        runInAction(() => (Doc.HighlightBrush.linkFollowEffect = undefined));
+        Doc.linkFollowUnhighlight();
         AudioBox.Enabled = true;
         const targets = document.elementsFromPoint(e.x, e.y);
         if (targets.length) {
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 30806f718..897be9a32 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -4,8 +4,13 @@
     height: 100%;
     width: 250;
     font-family: 'Roboto';
+    font-size: 12px;
     cursor: auto;
 
+    .slider-text {
+        font-size: 8px;
+    }
+
     overflow-x: hidden;
     overflow-y: auto;
 
@@ -865,7 +870,15 @@
     }
 
     .propertiesButton {
-        width: 4rem;
+        width: 2rem;
+        height: 2rem;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        > svg {
+            width: 15px;
+            height: 15px;
+        }
     }
 }
 
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ad3f62990..e8fd540a8 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -55,7 +55,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     }
 
     @computed get selectedDoc() {
-        return LinkManager.currentLink || SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard;
+        return SelectionManager.SelectedSchemaDoc() || this.selectedDocumentView?.rootDoc || Doc.ActiveDashboard;
     }
     @computed get selectedDocumentView() {
         if (SelectionManager.Views().length) return SelectionManager.Views()[0];
@@ -306,7 +306,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     }
 
     @computed get links() {
-        return !this.selectedDoc ? null : <PropertiesDocBacklinksSelector Document={this.selectedDoc} hideTitle={true} addDocTab={this.props.addDocTab} />;
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc;
+        return !selAnchor ? null : <PropertiesDocBacklinksSelector Document={selAnchor} hideTitle={true} addDocTab={this.props.addDocTab} />;
     }
 
     @computed get layoutPreview() {
@@ -1426,11 +1427,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
     @undoBatch
     animationDirection = (direction: PresEffectDirection, icon: string, gridColumn: number, gridRow: number, opts: object) => {
         const lanch = this.sourceAnchor;
-        const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.LIGHT_BLUE : 'black';
+        const color = lanch?.presEffectDirection === direction || (direction === PresEffectDirection.Center && !lanch?.presEffectDirection) ? Colors.MEDIUM_BLUE : '';
         return (
             <Tooltip title={<div className="dash-tooltip">{direction}</div>}>
                 <div
-                    style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '100%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', color }}
+                    style={{ ...opts, border: direction === PresEffectDirection.Center ? `solid 2px ${color}` : undefined, borderRadius: '20%', cursor: 'pointer', gridColumn, gridRow, justifySelf: 'center', background: color, color: 'black' }}
                     onClick={() => this.updateEffectDirection(direction)}>
                     {icon ? <FontAwesomeIcon icon={icon as any} /> : null}
                 </div>
@@ -1470,19 +1471,21 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         }
     };
 
-    toggleProp = (e: React.PointerEvent, prop: string) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => this.selectedDoc && (this.selectedDoc[prop] = !this.selectedDoc[prop]))));
+    toggleLinkProp = (e: React.PointerEvent, prop: string) => {
+        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.currentLink && (LinkManager.currentLink[prop] = !LinkManager.currentLink[prop]))));
     };
 
     @computed get destinationAnchor() {
         const ldoc = LinkManager.currentLink;
-        const lanch = LinkManager.currentLinkAnchor;
+        const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
         if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch;
         return ldoc ? DocCast(ldoc.anchor2) : ldoc;
     }
 
     @computed get sourceAnchor() {
-        return LinkManager.currentLinkAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
+        const selAnchor = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor;
+
+        return selAnchor ?? (LinkManager.currentLink && this.destinationAnchor ? LinkManager.getOppositeAnchor(LinkManager.currentLink, this.destinationAnchor) : LinkManager.currentLink);
     }
 
     toggleAnchorProp = (e: React.PointerEvent, prop: string, anchor?: Doc) => {
@@ -1545,9 +1548,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
         const isNovice = Doc.noviceMode;
         const zoom = Number((NumCast(this.sourceAnchor?.presZoom, 1) * 100).toPrecision(3));
         const targZoom = this.sourceAnchor?.followLinkZoom;
-        const selectedDoc = this.selectedDoc;
         const indent = 30;
-        if (!selectedDoc && !this.isPres) {
+        if (!this.selectedDoc && !this.isPres) {
             return (
                 <div className="propertiesView" style={{ width: this.props.width }}>
                     <div className="propertiesView-title" style={{ width: this.props.width }}>
@@ -1556,7 +1558,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                 </div>
             );
         } else {
-            if (selectedDoc && !this.isPres) {
+            if (this.selectedDoc && !this.isPres) {
                 return (
                     <div
                         className="propertiesView"
@@ -1573,9 +1575,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                         {this.contextsSubMenu}
 
                         {this.linksSubMenu}
-                        {!selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(dv.rootDoc.links).includes(LinkManager.currentLink!)) ? null : (
+                        {!this.selectedDoc || !LinkManager.currentLink || !SelectionManager.Views().some(dv => DocListCast(this.sourceAnchor?.links).includes(LinkManager.currentLink!)) ? null : (
                             <>
-                                <div className="propertiesView-section">
+                                <div className="propertiesView-section" style={{ background: 'darkgray' }}>
                                     <div className="propertiesView-input first" style={{ display: 'grid', gridTemplateColumns: '84px auto' }}>
                                         <p>Relationship</p>
                                         {this.editRelationship}
@@ -1584,11 +1586,41 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                         <p>Description</p>
                                         {this.editDescription}
                                     </div>
+                                    <div className="propertiesView-input inline">
+                                        <p>Show link</p>
+                                        <button
+                                            style={{ background: !LinkManager.currentLink?.linkDisplay ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleLinkProp(e, 'linkDisplay')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
+                                        <p>Auto-move anchors</p>
+                                        <button
+                                            style={{ background: !LinkManager.currentLink?.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleLinkProp(e, 'linkAutoMove')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
+                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
+                                        <p>Display arrow</p>
+                                        <button
+                                            style={{ background: !LinkManager.currentLink?.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
+                                            onPointerDown={e => this.toggleLinkProp(e, 'linkDisplayArrow')}
+                                            onClick={e => e.stopPropagation()}
+                                            className="propertiesButton">
+                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
+                                        </button>
+                                    </div>
                                 </div>
                                 <div className="propertiesView-section">
                                     <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 84px)' }}>
                                         <p>Follow by</p>
-                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(selectedDoc.followLinkLocation, 'default')}>
+                                        <select onChange={e => this.changeFollowBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkLocation, 'default')}>
                                             <option value="default">Default</option>
                                             <option value="add:left">Opening in new left pane</option>
                                             <option value="add:right">Opening in new right pane</option>
@@ -1598,7 +1630,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <option value="add">Opening in new tab</option>
                                             <option value="replace">Replacing current tab</option>
                                             <option value="inPlace">Opening in place</option>
-                                            {selectedDoc.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
+                                            {LinkManager.currentLink?.linksToAnnotation ? <option value="openExternal">Open in external page</option> : null}
                                         </select>
                                     </div>
                                     <div className="propertiesView-input inline first" style={{ display: 'grid', gridTemplateColumns: '84px calc(100% - 134px) 50px' }}>
@@ -1609,7 +1641,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                                 <option value={effect.toString()}>{effect.toString()}</option>
                                             ))}
                                         </select>
-                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, gridColumn: 3 }}>
+                                        <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}>
                                             {this.animationDirection(PresEffectDirection.Left, 'angle-right', 1, 2, {})}
                                             {this.animationDirection(PresEffectDirection.Right, 'angle-left', 3, 2, {})}
                                             {this.animationDirection(PresEffectDirection.Top, 'angle-down', 2, 1, {})}
@@ -1650,34 +1682,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
                                             <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
                                         </button>
                                     </div>
-                                    <div className="propertiesView-input inline">
-                                        <p>Show link</p>
-                                        <button style={{ background: !selectedDoc.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} onPointerDown={e => this.toggleProp(e, 'linkDisplay')} onClick={e => e.stopPropagation()} className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
-                                        <p>Auto-move anchor</p>
-                                        <button
-                                            style={{ background: !selectedDoc.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }}
-                                            onPointerDown={e => this.toggleProp(e, 'linkAutoMove')}
-                                            onClick={e => e.stopPropagation()}
-                                            className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline" style={{ marginLeft: 10 }}>
-                                        <p>Display arrow</p>
-                                        <button
-                                            style={{ background: !selectedDoc.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }}
-                                            onPointerDown={e => this.toggleProp(e, 'linkDisplayArrow')}
-                                            onClick={e => e.stopPropagation()}
-                                            className="propertiesButton">
-                                            <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" />
-                                        </button>
-                                    </div>
-                                    <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 128px) 50px' }}>
-                                        <p>Zoom % screen</p>
+                                    <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}>
+                                        <p>Zoom %</p>
                                         <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}>
                                             <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} />
                                             <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}>
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
index b8344dc0c..9e360f557 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinksView.tsx
@@ -2,10 +2,10 @@ import { computed } from 'mobx';
 import { observer } from 'mobx-react';
 import { Id } from '../../../../fields/FieldSymbols';
 import { DocumentManager } from '../../../util/DocumentManager';
+import { LightboxView } from '../../LightboxView';
 import './CollectionFreeFormLinksView.scss';
 import { CollectionFreeFormLinkView } from './CollectionFreeFormLinkView';
 import React = require('react');
-import { LightboxView } from '../../LightboxView';
 
 @observer
 export class CollectionFreeFormLinksView extends React.Component<React.PropsWithChildren<{}>> {
diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss
index e68f9abe3..430a36dce 100644
--- a/src/client/views/global/globalCssVariables.scss
+++ b/src/client/views/global/globalCssVariables.scss
@@ -84,4 +84,5 @@ $TREE_BULLET_WIDTH: 20px;
     LEFT_MENU_WIDTH: $LEFT_MENU_WIDTH;
     TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH;
     INK_MASK_SIZE: $INK_MASK_SIZE;
+    MEDIUM_GRAY: $medium-gray;
 }
diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts
index 76259113c..3375579d6 100644
--- a/src/client/views/global/globalCssVariables.scss.d.ts
+++ b/src/client/views/global/globalCssVariables.scss.d.ts
@@ -11,6 +11,7 @@ interface IGlobalScss {
     LEFT_MENU_WIDTH: string;
     TREE_BULLET_WIDTH: string;
     INK_MASK_SIZE: number;
+    MEDIUM_GRAY: string;
 }
 declare const globalCssVariables: IGlobalScss;
 
diff --git a/src/client/views/linking/LinkEditor.scss b/src/client/views/linking/LinkEditor.scss
deleted file mode 100644
index b0ee4e46d..000000000
--- a/src/client/views/linking/LinkEditor.scss
+++ /dev/null
@@ -1,334 +0,0 @@
-@import '../global/globalCssVariables';
-
-.linkEditor {
-    width: 100%;
-    height: auto;
-    font-size: 13px; // TODO
-    user-select: none;
-    max-width: 280px;
-}
-
-.linkEditor-button-back {
-    //margin-bottom: 6px;
-    border-radius: 10px;
-    width: 18px;
-    height: 18px;
-    padding: 0;
-
-    &:hover {
-        cursor: pointer;
-    }
-}
-
-.linkEditor-info {
-    padding-top: 12px;
-    padding-left: 5px;
-    padding-bottom: 3px;
-    //margin-bottom: 6px;
-    display: flex;
-    justify-content: space-between;
-    color: black;
-
-    .linkEditor-linkedTo {
-        width: calc(100% - 46px);
-        overflow: hidden;
-        position: relative;
-        text-overflow: ellipsis;
-        white-space: pre;
-
-        .linkEditor-downArrow {
-            &:hover {
-                cursor: pointer;
-            }
-        }
-    }
-}
-
-.linkEditor-moreInfo {
-    margin-left: 12px;
-    padding-left: 13px;
-    padding-right: 6.5px;
-    padding-bottom: 4px;
-    font-size: 9px;
-    //font-style: italic;
-    text-decoration-color: grey;
-
-    .button {
-        color: black;
-
-        &:hover {
-            cursor: pointer;
-        }
-    }
-}
-
-.linkEditor-zoomFollow {
-    padding-left: 26px;
-    padding-right: 6.5px;
-    padding-bottom: 3.5px;
-    display: flex;
-
-    .linkEditor-zoomFollow-label {
-        text-decoration-color: black;
-        color: black;
-        line-height: 1.7;
-    }
-
-    .linkEditor-zoomFollow-input {
-        display: block;
-        width: 20px;
-    }
-}
-.linkEditor-deleteBtn {
-    padding-left: 3px;
-}
-
-.linkEditor-description {
-    padding-left: 26px;
-    padding-bottom: 3.5px;
-    display: flex;
-
-    .linkEditor-description-label {
-        text-decoration-color: black;
-        color: black;
-    }
-
-    .linkEditor-description-input {
-        display: flex;
-
-        .linkEditor-description-editing {
-            min-width: 85%;
-            //border: 1px solid grey;
-            //border-radius: 4px;
-            padding-left: 2px;
-            //margin-right: 4px;
-            color: black;
-            text-decoration-color: grey;
-        }
-
-        .linkEditor-description-add-button {
-            display: inline;
-            border-radius: 7px;
-            font-size: 9px;
-            background: black;
-            height: 80%;
-            color: white;
-            padding: 3px;
-            margin-left: 3px;
-
-            &:hover {
-                cursor: pointer;
-                background: grey;
-            }
-        }
-    }
-}
-
-.linkEditor-relationship-dropdown {
-    position: absolute;
-    width: 154px;
-    max-height: 90px;
-    overflow: auto;
-    background: white;
-
-    p {
-        padding: 3px;
-        cursor: pointer;
-        border: 1px solid $medium-gray;
-    }
-
-    p:hover {
-        background: $light-blue;
-    }
-}
-
-.linkEditor-followingDropdown {
-    padding-left: 26px;
-    padding-right: 6.5px;
-    padding-bottom: 15px;
-    display: flex;
-
-    &:hover {
-        cursor: pointer;
-    }
-
-    .linkEditor-followingDropdown-label {
-        color: black;
-        padding-right: 3px;
-    }
-
-    .linkEditor-followingDropdown-dropdown {
-        .linkEditor-followingDropdown-header {
-            border: 1px solid grey;
-            border-radius: 4px;
-            //background-color: rgb(236, 236, 236);
-            padding-left: 2px;
-            padding-right: 2px;
-            text-decoration-color: black;
-            color: rgb(94, 94, 94);
-
-            .linkEditor-followingDropdown-icon {
-                float: right;
-                color: black;
-            }
-        }
-
-        .linkEditor-followingDropdown-optionsList {
-            padding-left: 3px;
-            padding-right: 3px;
-
-            &:last-child {
-                border-bottom: none;
-            }
-
-            .linkEditor-followingDropdown-option {
-                border: 0.25px solid grey;
-                //background-color: rgb(236, 236, 236);
-                padding-left: 2px;
-                padding-right: 2px;
-                color: grey;
-                text-decoration-color: grey;
-                font-size: 9px;
-                border-top: none;
-
-                &:hover {
-                    background-color: rgb(187, 220, 231);
-                }
-            }
-        }
-    }
-}
-
-.linkEditor-button,
-.linkEditor-addbutton {
-    width: 15%;
-    border-radius: 7px;
-    font-size: 9px;
-    background: black;
-    padding: 3px;
-    height: 80%;
-    color: white;
-    text-align: center;
-    margin: auto;
-    margin-left: 3px;
-    > svg {
-        margin: auto;
-    }
-    &:disabled {
-        background-color: gray;
-    }
-}
-
-.linkEditor-addbutton {
-    margin-left: 0px;
-}
-
-.linkEditor-groupsLabel {
-    display: flex;
-    justify-content: space-between;
-}
-
-.linkEditor-group {
-    background-color: $light-gray;
-    padding: 6px;
-    margin: 3px 0;
-    border-radius: 3px;
-
-    .linkEditor-group-row {
-        display: flex;
-        margin-bottom: 3px;
-    }
-
-    .linkEditor-group-row-label {
-        margin-right: 6px;
-        display: inline-block;
-    }
-
-    .linkEditor-metadata-row {
-        display: flex;
-        justify-content: space-between;
-        margin-bottom: 6px;
-
-        .linkEditor-error {
-            border-color: red;
-        }
-
-        input {
-            width: calc(50% - 16px);
-            height: 20px;
-        }
-
-        button {
-            width: 20px;
-            height: 20px;
-            margin-left: 3px;
-            padding: 0;
-            font-size: 10px;
-        }
-    }
-}
-
-.linkEditor-dropdown {
-    width: 100%;
-    position: relative;
-    z-index: 999;
-
-    input {
-        width: 100%;
-    }
-
-    .linkEditor-options-wrapper {
-        width: 100%;
-        position: absolute;
-        top: 19px;
-        left: 0;
-        display: flex;
-        flex-direction: column;
-    }
-
-    .linkEditor-option {
-        background-color: $light-gray;
-        border: 1px solid $medium-gray;
-        border-top: 0;
-        padding: 3px;
-        cursor: pointer;
-
-        &:hover {
-            background-color: lightgray;
-        }
-
-        &.onDown {
-            background-color: gray;
-        }
-    }
-}
-
-.linkEditor-typeButton {
-    background-color: transparent;
-    color: $dark-gray;
-    height: 20px;
-    padding: 0 3px;
-    padding-bottom: 2px;
-    text-align: left;
-    text-transform: none;
-    letter-spacing: normal;
-    font-size: 12px;
-    font-weight: bold;
-    display: inline-block;
-    width: calc(100% - 40px);
-
-    &:hover {
-        background-color: $white;
-    }
-}
-
-.linkEditor-group-buttons {
-    height: 20px;
-    display: flex;
-    justify-content: flex-end;
-    margin-top: 5px;
-
-    .linkEditor-button {
-        margin-left: 3px;
-    }
-}
diff --git a/src/client/views/linking/LinkEditor.tsx b/src/client/views/linking/LinkEditor.tsx
deleted file mode 100644
index 01e33708a..000000000
--- a/src/client/views/linking/LinkEditor.tsx
+++ /dev/null
@@ -1,454 +0,0 @@
-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, NumListCast, StrListCast, Field } from '../../../fields/Doc';
-import { DateCast, StrCast, Cast, BoolCast, DocCast, NumCast } from '../../../fields/Types';
-import { LinkManager } from '../../util/LinkManager';
-import { undoBatch } from '../../util/UndoManager';
-import './LinkEditor.scss';
-import { LinkRelationshipSearch } from './LinkRelationshipSearch';
-import React = require('react');
-import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils';
-import { PresBox, PresEffect } from '../nodes/trails';
-
-interface LinkEditorProps {
-    sourceDoc: Doc;
-    linkDoc: Doc;
-    showLinks?: () => void;
-    hideback?: boolean;
-}
-@observer
-export class LinkEditor extends React.Component<LinkEditorProps> {
-    @observable description = Field.toString(LinkManager.currentLink?.description as any as Field);
-    @observable relationship = StrCast(LinkManager.currentLink?.linkRelationship);
-    @observable zoomFollow = BoolCast(this.props.sourceDoc.followLinkZoom);
-    @observable audioFollow = BoolCast(this.props.sourceDoc.followLinkAudio);
-    @observable openDropdown: boolean = false;
-    @observable openEffectDropdown: boolean = false;
-    @observable private buttonColor: string = '';
-    @observable private relationshipButtonColor: string = '';
-    @observable private relationshipSearchVisibility: string = 'none';
-    @observable private searchIsActive: boolean = false;
-
-    //@observable description = this.props.linkDoc.description ? StrCast(this.props.linkDoc.description) : "DESCRIPTION";
-
-    @undoBatch
-    setRelationshipValue = action((value: string) => {
-        if (LinkManager.currentLink) {
-            const prevRelationship = LinkManager.currentLink.linkRelationship as string;
-            LinkManager.currentLink.linkRelationship = value;
-            Doc.GetProto(LinkManager.currentLink).linkRelationship = value;
-            const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
-            const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes);
-            const linkColorList = StrListCast(Doc.UserDoc().linkColorList);
-
-            // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color
-            if (!linkRelationshipList?.includes(value)) {
-                linkRelationshipList.push(value);
-                linkRelationshipSizes.push(1);
-                const randColor = 'rgb(' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ',' + Math.floor(Math.random() * 255) + ')';
-                linkColorList.push(randColor);
-                // if the relationship is already in the list AND the new rel is different from the prev rel, update the rel sizes
-            } else if (linkRelationshipList && value !== prevRelationship) {
-                const index = linkRelationshipList.indexOf(value);
-                //increment size of new relationship size
-                if (index !== -1 && index < linkRelationshipSizes.length) {
-                    const pvalue = linkRelationshipSizes[index];
-                    linkRelationshipSizes[index] = pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue + 1;
-                }
-                //decrement the size of the previous relationship if it already exists (i.e. not default 'link' relationship upon link creation)
-                if (linkRelationshipList.includes(prevRelationship)) {
-                    const pindex = linkRelationshipList.indexOf(prevRelationship);
-                    if (pindex !== -1 && pindex < linkRelationshipSizes.length) {
-                        const pvalue = linkRelationshipSizes[pindex];
-                        linkRelationshipSizes[pindex] = Math.max(0, pvalue === undefined || !Number.isFinite(pvalue) ? 1 : pvalue - 1);
-                    }
-                }
-            }
-            this.relationshipButtonColor = 'rgb(62, 133, 55)';
-            setTimeout(
-                action(() => (this.relationshipButtonColor = '')),
-                750
-            );
-            return true;
-        }
-    });
-
-    /**
-     * returns list of strings with possible existing relationships that contain what is currently in the input field
-     */
-    @action
-    getRelationshipResults = () => {
-        const query = this.relationship; //current content in input box
-        const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList);
-        if (linkRelationshipList) {
-            return linkRelationshipList.filter(rel => rel.includes(query));
-        }
-    };
-
-    /**
-     * toggles visibility of the relationship search results when the input field is focused on
-     */
-    @action
-    toggleRelationshipResults = () => {
-        this.relationshipSearchVisibility = this.relationshipSearchVisibility === 'none' ? 'block' : 'none';
-    };
-
-    @undoBatch
-    setDescripValue = action((value: string) => {
-        if (LinkManager.currentLink) {
-            Doc.GetProto(LinkManager.currentLink).description = value;
-            this.buttonColor = 'rgb(62, 133, 55)';
-            setTimeout(
-                action(() => (this.buttonColor = '')),
-                750
-            );
-            return true;
-        }
-    });
-
-    onDescriptionKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
-        if (e.key === 'Enter') {
-            this.setDescripValue(this.description);
-            document.getElementById('input')?.blur();
-        }
-        e.stopPropagation();
-    };
-
-    onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => {
-        if (e.key === 'Enter') {
-            this.setRelationshipValue(this.relationship);
-            document.getElementById('input')?.blur();
-        }
-        e.stopPropagation();
-    };
-
-    onDescriptionDown = () => this.setDescripValue(this.description);
-    onRelationshipDown = () => this.setRelationshipValue(this.relationship);
-
-    onBlur = () => {
-        //only hide the search results if the user clicks out of the input AND not on any of the search results
-        // i.e. if search is not active
-        if (!this.searchIsActive) {
-            this.toggleRelationshipResults();
-        }
-    };
-    onFocus = () => {
-        this.toggleRelationshipResults();
-    };
-    toggleSearchIsActive = () => {
-        this.searchIsActive = !this.searchIsActive;
-    };
-
-    @action
-    handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.description = e.target.value;
-    };
-    @action
-    handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        this.relationship = e.target.value;
-    };
-    @action
-    handleZoomFollowChange = () => {
-        this.props.sourceDoc.followLinkZoom = !this.props.sourceDoc.followLinkZoom;
-    };
-    @action
-    handleAudioFollowChange = () => {
-        this.props.sourceDoc.followLinkAudio = !this.props.sourceDoc.followLinkAudio;
-    };
-    @action
-    handleRelationshipSearchChange = (result: string) => {
-        this.setRelationshipValue(result);
-        this.toggleRelationshipResults();
-        this.relationship = result;
-    };
-    @computed
-    get editRelationship() {
-        //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        return (
-            <div className="linkEditor-description">
-                <div className="linkEditor-description-label">Relationship:</div>
-                <div className="linkEditor-description-input">
-                    <div className="linkEditor-description-editing">
-                        <input
-                            style={{ width: '100%' }}
-                            id="input"
-                            value={this.relationship}
-                            autoComplete={'off'}
-                            placeholder={'Enter link relationship'}
-                            onKeyDown={this.onRelationshipKey}
-                            onChange={this.handleRelationshipChange}
-                            onFocus={this.onFocus}
-                            onBlur={this.onBlur}></input>
-                        <LinkRelationshipSearch results={this.getRelationshipResults()} display={this.relationshipSearchVisibility} handleRelationshipSearchChange={this.handleRelationshipSearchChange} toggleSearch={this.toggleSearchIsActive} />
-                    </div>
-                    <div className="linkEditor-description-add-button" style={{ background: this.relationshipButtonColor }} onPointerDown={this.onRelationshipDown}>
-                        Set
-                    </div>
-                </div>
-            </div>
-        );
-    }
-    @computed
-    get editZoomFollow() {
-        //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        return (
-            <div className="linkEditor-zoomFollow">
-                <div className="linkEditor-zoomFollow-label">Zoom To Link Target:</div>
-                <div className="linkEditor-zoomFollow-input">
-                    <div className="linkEditor-zoomFollow-editing">
-                        <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleZoomFollowChange)} defaultChecked={this.zoomFollow} />
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @computed
-    get editAudioFollow() {
-        //NOTE: confusingly, the classnames for the following relationship JSX elements are the same as the for the description elements for shared CSS
-        return (
-            <div className="linkEditor-zoomFollow">
-                <div className="linkEditor-zoomFollow-label">Play Target Audio:</div>
-                <div className="linkEditor-zoomFollow-input">
-                    <div className="linkEditor-zoomFollow-editing">
-                        <input type="checkbox" onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, this.handleAudioFollowChange)} defaultChecked={this.audioFollow} />
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @computed
-    get editDescription() {
-        return (
-            <div className="linkEditor-description">
-                <div className="linkEditor-description-label">Description:</div>
-                <div className="linkEditor-description-input">
-                    <div className="linkEditor-description-editing">
-                        <input style={{ width: '100%' }} autoComplete={'off'} id="input" value={this.description} placeholder={'Enter link description'} onKeyDown={this.onDescriptionKey} onChange={this.handleDescriptionChange}></input>
-                    </div>
-                    <div className="linkEditor-description-add-button" style={{ background: this.buttonColor }} onPointerDown={this.onDescriptionDown}>
-                        Set
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @action
-    changeDropdown = () => {
-        this.openDropdown = !this.openDropdown;
-    };
-
-    @undoBatch
-    changeFollowBehavior = action((follow: string) => {
-        this.openDropdown = false;
-        Doc.GetProto(this.props.linkDoc).followLinkLocation = follow;
-    });
-
-    @computed
-    get followingDropdown() {
-        return (
-            <div className="linkEditor-followingDropdown">
-                <div className="linkEditor-followingDropdown-label">Follow by:</div>
-                <div className="linkEditor-followingDropdown-dropdown">
-                    <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeDropdown}>
-                        {StrCast(this.props.linkDoc.followLinkLocation, 'default')}
-                        <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
-                    </div>
-                    <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openDropdown ? '' : 'none' }}>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('default')}>
-                            Default
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:left')}>
-                            Always opening in new left pane
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add:right')}>
-                            Always opening in new right pane
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:right')}>
-                            Always replacing right tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace:left')}>
-                            Always replacing left tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('fullScreen')}>
-                            Always opening full screen
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('add')}>
-                            Always opening in a new tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('replace')}>
-                            Replacing Tab
-                        </div>
-                        <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('inPlace')}>
-                            Opening in Place
-                        </div>
-                        {this.props.linkDoc.linksToAnnotation ? (
-                            <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeFollowBehavior('openExternal')}>
-                                Always open in external page
-                            </div>
-                        ) : null}
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    @computed get destinationAnchor() {
-        const ldoc = this.props.linkDoc;
-        if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) {
-            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2);
-            if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1);
-        }
-        return LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc) ?? this.props.sourceDoc;
-    }
-    @action
-    changeEffectDropdown = () => {
-        this.openEffectDropdown = !this.openEffectDropdown;
-    };
-
-    @undoBatch
-    changeEffect = action((follow: string) => {
-        this.openEffectDropdown = false;
-        this.destinationAnchor.presEffect = follow;
-    });
-
-    @computed
-    get effectDropdown() {
-        return (
-            <div className="linkEditor-followingDropdown">
-                <div className="linkEditor-followingDropdown-label">Animation:</div>
-                <div className="linkEditor-followingDropdown-dropdown">
-                    <div className="linkEditor-followingDropdown-header" onPointerDown={this.changeEffectDropdown}>
-                        {StrCast(this.destinationAnchor.presEffect, 'default')}
-                        <FontAwesomeIcon className="linkEditor-followingDropdown-icon" icon={this.openEffectDropdown ? 'chevron-up' : 'chevron-down'} size={'lg'} />
-                    </div>
-                    <div className="linkEditor-followingDropdown-optionsList" style={{ display: this.openEffectDropdown ? '' : 'none' }}>
-                        {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => (
-                            <div className="linkEditor-followingDropdown-option" onPointerDown={() => this.changeEffect(effect.toString())}>
-                                {effect.toString()}
-                            </div>
-                        ))}
-                    </div>
-                </div>
-            </div>
-        );
-    }
-
-    autoMove = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkAutoMove = !this.props.linkDoc.linkAutoMove))));
-    };
-
-    showAnchor = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.hidden = !this.props.linkDoc.hidden))));
-    };
-
-    showLink = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => (this.props.linkDoc.linkDisplay = !this.props.linkDoc.linkDisplay))));
-    };
-
-    deleteLink = (e: React.PointerEvent): void => {
-        setupMoveUpEvents(this, e, returnFalse, emptyFunction, undoBatch(action(() => LinkManager.Instance.deleteLink(this.props.linkDoc))));
-    };
-
-    render() {
-        const destination = LinkManager.getOppositeAnchor(this.props.linkDoc, this.props.sourceDoc);
-
-        return !destination ? null : (
-            <div className="linkEditor" tabIndex={0} onKeyDown={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}>
-                <div className="linkEditor-info">
-                    {!this.props.showLinks ? null : (
-                        <Tooltip title={<div className="dash-tooltip">Return to link menu</div>} placement="top">
-                            <button className="linkEditor-button-back" style={{ display: this.props.hideback ? 'none' : '' }} onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this.props.showLinks?.())}>
-                                <FontAwesomeIcon icon="arrow-left" size="sm" />{' '}
-                            </button>
-                        </Tooltip>
-                    )}
-                    <p className="linkEditor-linkedTo">
-                        Editing Link to: <b>{StrCast(destination.proto?.title, StrCast(destination.title, 'untitled'))}</b>
-                    </p>
-                    <Tooltip title={<div className="dash-tooltip">Delete Link</div>}>
-                        <div className="linkEditor-deleteBtn" onPointerDown={this.deleteLink} onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon="trash" size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                <div className="linkEditor-moreInfo">
-                    {this.props.linkDoc.author ? (
-                        <>
-                            {' '}
-                            <b>Author:</b> {StrCast(this.props.linkDoc.author)}
-                        </>
-                    ) : null}
-                    {this.props.linkDoc.creationDate ? (
-                        <>
-                            {' '}
-                            <b>Creation Date:</b>
-                            {DateCast(this.props.linkDoc.creationDate).toString()}
-                        </>
-                    ) : null}
-                </div>
-                {this.editDescription}
-                {this.editRelationship}
-                {this.editZoomFollow}
-                {this.editAudioFollow}
-                <div className="linkEditor-description">
-                    Show Anchor:
-                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.hidden ? 'Show Link Anchor' : 'Hide Link Anchor'}</div>}>
-                        <div
-                            className="linkEditor-button"
-                            style={{ background: this.props.linkDoc.hidden ? 'gray' : '#4476f7' /* $medium-blue                                       */ }}
-                            onPointerDown={this.showAnchor}
-                            onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon={'eye'} size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                <div className="linkEditor-description">
-                    Show Link Line:
-                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkDisplay ? 'Hide Link Line' : 'Show Link Line'}</div>}>
-                        <div
-                            className="linkEditor-button"
-                            style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkDisplay ? '#4476f7' /* $medium-blue */ : '' }}
-                            onPointerDown={this.showLink}
-                            onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon={'project-diagram'} size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                <div className="linkEditor-description">
-                    Freeze Anchor:
-                    <Tooltip title={<div className="dash-tooltip">{this.props.linkDoc.linkAutoMove ? 'Click to freeze link anchor position' : 'Click to auto move link anchor'}</div>}>
-                        <div
-                            className="linkEditor-button"
-                            style={{ background: this.props.linkDoc.hidden ? 'gray' : this.props.linkDoc.linkAutoMove ? '' : '#4476f7' /* $medium-blue */ }}
-                            onPointerDown={this.autoMove}
-                            onClick={e => e.stopPropagation()}>
-                            <FontAwesomeIcon className="fa-icon" icon={'play'} size="sm" />
-                        </div>
-                    </Tooltip>
-                </div>
-                {this.followingDropdown}
-                {this.effectDropdown}
-                {PresBox.inputter('0.1', '0.1', '10', NumCast(this.destinationAnchor.presTransition) / 1000, true, (val: string) => PresBox.SetTransitionTime(val, (timeInMS: number) => (this.destinationAnchor.presTransition = timeInMS)))}
-                <div
-                    className={'slider-headers'}
-                    style={{
-                        marginLeft: 30,
-                        marginRight: 30,
-                        display: 'grid',
-                        justifyContent: 'space-between',
-                        width: '100%',
-                        gridTemplateColumns: 'auto auto auto',
-                    }}>
-                    <div className="slider-text">Fast</div>
-                    <div className="slider-text">Medium</div>
-                    <div className="slider-text">Slow</div>
-                </div>{' '}
-            </div>
-        );
-    }
-}
diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 0c46a6d96..c9112eec3 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -1,14 +1,13 @@
-import { action, computed, observable } from 'mobx';
+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';
-import { LinkEditor } from './LinkEditor';
 import './LinkMenu.scss';
 import { LinkMenuGroup } from './LinkMenuGroup';
 import React = require('react');
-import { emptyFunction } from '../../../Utils';
 
 interface Props {
     docView: DocumentView;
@@ -23,15 +22,9 @@ interface Props {
 @observer
 export class LinkMenu extends React.Component<Props> {
     _editorRef = React.createRef<HTMLDivElement>();
-    @observable _editingLink?: Doc;
     @observable _linkMenuRef = React.createRef<HTMLDivElement>();
 
-    clear = !this.props.clearLinkEditor
-        ? undefined
-        : action(() => {
-              this.props.clearLinkEditor?.();
-              this._editingLink = undefined;
-          });
+    clear = () => this.props.clearLinkEditor?.();
 
     componentDidMount() {
         this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true);
@@ -43,7 +36,7 @@ export class LinkMenu extends React.Component<Props> {
     onPointerDown = action((e: PointerEvent) => {
         LinkDocPreview.Clear();
         if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) {
-            this.clear?.();
+            this.clear();
         }
     });
 
@@ -54,34 +47,20 @@ export class LinkMenu extends React.Component<Props> {
      */
     renderAllGroups = (groups: Map<string, Array<Doc>>): Array<JSX.Element> => {
         const linkItems = Array.from(groups.entries()).map(group => (
-            <LinkMenuGroup
-                key={group[0]}
-                itemHandler={this.props.itemHandler}
-                docView={this.props.docView}
-                sourceDoc={this.props.docView.props.Document}
-                group={group[1]}
-                groupType={group[0]}
-                clearLinkEditor={this.clear}
-                showEditor={action(linkDoc => (this._editingLink = linkDoc))}
-            />
+            <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} />
         ));
 
         return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
     };
 
     render() {
-        const sourceDoc = this.props.docView.props.Document;
+        const sourceDoc = this.props.docView.rootDoc;
+        const sourceAnchor = this.props.docView.anchorViewDoc ?? sourceDoc;
         const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds());
 
         return (
             <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}>
-                {this._editingLink ? (
-                    <div className="linkMenu-listEditor">
-                        <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} />
-                    </div>
-                ) : (
-                    <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceDoc))}</div>
-                )}
+                <div className="linkMenu-list">{this.renderAllGroups(LinkManager.Instance.getRelatedGroupedLinks(sourceAnchor))}</div>
             </div>
         );
     }
diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx
index 9d2082e21..d02a1c4eb 100644
--- a/src/client/views/linking/LinkMenuGroup.tsx
+++ b/src/client/views/linking/LinkMenuGroup.tsx
@@ -2,19 +2,19 @@ import { observer } from 'mobx-react';
 import { observable, action } from 'mobx';
 import { Doc, StrListCast } from '../../../fields/Doc';
 import { Id } from '../../../fields/FieldSymbols';
-import { Cast } from '../../../fields/Types';
+import { Cast, DocCast } from '../../../fields/Types';
 import { LinkManager } from '../../util/LinkManager';
 import { DocumentView } from '../nodes/DocumentView';
 import './LinkMenu.scss';
 import { LinkMenuItem } from './LinkMenuItem';
 import React = require('react');
+import { DocumentType } from '../../documents/DocumentTypes';
 
 interface LinkMenuGroupProps {
     sourceDoc: Doc;
     group: Doc[];
     groupType: string;
     clearLinkEditor?: () => void;
-    showEditor: (linkDoc: Doc) => void;
     docView: DocumentView;
     itemHandler?: (doc: Doc) => void;
 }
@@ -44,25 +44,33 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> {
     render() {
         const set = new Set<Doc>(this.props.group);
         const groupItems = Array.from(set.keys()).map(linkDoc => {
-            const destination =
-                LinkManager.getOppositeAnchor(linkDoc, this.props.sourceDoc) ||
-                LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === this.props.sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
-            if (destination && this.props.sourceDoc) {
-                return (
-                    <LinkMenuItem
-                        key={linkDoc[Id]}
-                        itemHandler={this.props.itemHandler}
-                        groupType={this.props.groupType}
-                        docView={this.props.docView}
-                        linkDoc={linkDoc}
-                        sourceDoc={this.props.sourceDoc}
-                        destinationDoc={destination}
-                        clearLinkEditor={this.props.clearLinkEditor}
-                        showEditor={this.props.showEditor}
-                        menuRef={this._menuRef}
-                    />
-                );
-            }
+            const sourceDoc =
+                this.props.docView.anchorViewDoc ??
+                (this.props.docView.rootDoc.type === DocumentType.LINK //
+                    ? this.props.docView.props.LayoutTemplateString?.includes('anchor1')
+                        ? DocCast(linkDoc.anchor1)
+                        : DocCast(linkDoc.anchor2)
+                    : this.props.sourceDoc);
+            const destDoc = !sourceDoc
+                ? undefined
+                : this.props.docView.rootDoc.type === DocumentType.LINK
+                ? this.props.docView.props.LayoutTemplateString?.includes('anchor1')
+                    ? DocCast(linkDoc.anchor2)
+                    : DocCast(linkDoc.anchor1)
+                : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) || LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null));
+            return !destDoc || !sourceDoc ? null : (
+                <LinkMenuItem
+                    key={linkDoc[Id]}
+                    itemHandler={this.props.itemHandler}
+                    groupType={this.props.groupType}
+                    docView={this.props.docView}
+                    linkDoc={linkDoc}
+                    sourceDoc={sourceDoc}
+                    destinationDoc={destDoc}
+                    clearLinkEditor={this.props.clearLinkEditor}
+                    menuRef={this._menuRef}
+                />
+            );
         });
 
         return (
diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx
index c3705b0e1..fb4c6873e 100644
--- a/src/client/views/linking/LinkMenuItem.tsx
+++ b/src/client/views/linking/LinkMenuItem.tsx
@@ -18,6 +18,7 @@ import { DocumentView } from '../nodes/DocumentView';
 import { LinkDocPreview } from '../nodes/LinkDocPreview';
 import './LinkMenuItem.scss';
 import React = require('react');
+import { SelectionManager } from '../../util/SelectionManager';
 
 interface LinkMenuItemProps {
     groupType: string;
@@ -26,7 +27,6 @@ interface LinkMenuItemProps {
     sourceDoc: Doc;
     destinationDoc: Doc;
     clearLinkEditor?: () => void;
-    showEditor: (linkDoc: Doc) => void;
     menuRef: React.Ref<HTMLDivElement>;
     itemHandler?: (doc: Doc) => void;
 }
@@ -100,10 +100,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> {
             },
             emptyFunction,
             action(() => {
+                SelectionManager.SelectView(this.props.docView, false);
                 if ((SettingsManager.propertiesWidth ?? 0) < 100) {
                     SettingsManager.propertiesWidth = 250;
                 }
-                //this.props.showEditor(this.props.linkDoc);
             })
         );
     };
diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx
index 627487a9e..99fa62fa7 100644
--- a/src/client/views/nodes/DocumentLinksButton.tsx
+++ b/src/client/views/nodes/DocumentLinksButton.tsx
@@ -305,7 +305,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp
                     top: 0,
                     pointerEvents: 'none',
                 }}>
-                <Tooltip title={!DocumentLinksButton.LinkEditorDocView ? <div className="dash-tooltip">{title}</div> : <></>}>{this.linkButtonInner}</Tooltip>
+                {!DocumentLinksButton.LinkEditorDocView ? this.linkButtonInner : <Tooltip title={<div className="dash-tooltip">{title}</div>}>{this.linkButtonInner}</Tooltip>}
             </div>
         );
     }
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 2729a5047..a8bea61c9 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -3,7 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { Tooltip } from '@material-ui/core';
 import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from 'mobx';
 import { observer, renderReporter } from 'mobx-react';
-import { AclAdmin, AclEdit, AclPrivate, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
+import { AclAdmin, AclEdit, AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
 import { Document } from '../../../fields/documentSchemas';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
@@ -11,7 +11,7 @@ import { List } from '../../../fields/List';
 import { ObjectField } from '../../../fields/ObjectField';
 import { listSpec } from '../../../fields/Schema';
 import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, SharingPermissions, TraceMobx } from '../../../fields/util';
 import { MobileInterface } from '../../../mobile/MobileInterface';
@@ -1156,7 +1156,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
     anchorPanelHeight = () => this.props.PanelHeight() || 1;
     anchorStyleProvider = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any => {
         // prettier-ignore
-        switch (property) {
+        switch (property.split(':')[0]) {
             case StyleProp.ShowTitle:     return '';
             case StyleProp.PointerEvents: return 'none';
             case StyleProp.LinkSource:    return this.props.Document; // pass the LinkSource to the LinkAnchorBox
@@ -1188,7 +1188,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
         TraceMobx();
         if (this.layoutDoc.unrendered || this.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return null;
         if (this.rootDoc.type === DocumentType.PRES || this.rootDoc.type === DocumentType.LINK || this.props.dontRegisterView) return null;
-        const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => !d.hidden);
+        const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay);
         return filtered.map((link, i) => (
             <div className="documentView-anchorCont" key={link[Id]}>
                 <DocumentView
@@ -1415,7 +1415,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
             clipPath: borderPath.path ? `path('${borderPath.path}')` : undefined,
         });
 
-        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? Doc.HighlightBrush.linkFollowEffect, this.rootDoc) : renderDoc;
+        const animRenderDoc = Doc.IsHighlighted(this.rootDoc) || PresBox.Instance?.isActiveItemTarget(this.layoutDoc) ? PresBox.AnimationEffect(renderDoc, PresBox.Instance?.activeItem ?? this.rootDoc[AnimationSym], this.rootDoc) : renderDoc;
         return (
             <div
                 className={`${DocumentView.ROOT_DIV} docView-hack`}
@@ -1535,8 +1535,7 @@ 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) &&
-            DocumentLinksButton.LinkEditorDocView?.rootDoc !== this.rootDoc ? null : (
+        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} />
         );
     }
@@ -1654,6 +1653,9 @@ export class DocumentView extends React.Component<DocumentViewProps> {
 
     startDragging = (x: number, y: number, dropAction: dropActionType, hideSource = false) => this.docView?.startDragging(x, y, dropAction, hideSource);
 
+    @computed get anchorViewDoc() {
+        return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : this.rootDoc;
+    }
     docViewPathFunc = () => this.docViewPath;
     isSelected = (outsideReaction?: boolean) => SelectionManager.IsSelected(this, outsideReaction);
     select = (extendSelection: boolean) => SelectionManager.SelectView(this, !SelectionManager.Views().some(v => v.props.Document === this.props.ContainingCollectionDoc) && extendSelection);
diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx
index be9565452..e89076c1f 100644
--- a/src/client/views/nodes/LinkAnchorBox.tsx
+++ b/src/client/views/nodes/LinkAnchorBox.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import { action, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import { Doc } from '../../../fields/Doc';
@@ -7,20 +6,17 @@ import { TraceMobx } from '../../../fields/util';
 import { emptyFunction, setupMoveUpEvents, Utils } from '../../../Utils';
 import { DragManager } from '../../util/DragManager';
 import { LinkFollower } from '../../util/LinkFollower';
-import { SelectionManager } from '../../util/SelectionManager';
 import { ContextMenu } from '../ContextMenu';
 import { ContextMenuProps } from '../ContextMenuItem';
 import { ViewBoxBaseComponent } from '../DocComponent';
-import { LinkEditor } from '../linking/LinkEditor';
 import { StyleProp } from '../StyleProvider';
 import { FieldView, FieldViewProps } from './FieldView';
 import './LinkAnchorBox.scss';
 import { LinkDocPreview } from './LinkDocPreview';
 import React = require('react');
-import { OpenWhere } from './DocumentView';
-const higflyout = require('@hig/flyout');
-export const { anchorPoints } = higflyout;
-export const Flyout = higflyout.default;
+import { LinkManager } from '../../util/LinkManager';
+import globalCssVariables = require('../global/globalCssVariables.scss');
+import { SelectionManager } from '../../util/SelectionManager';
 
 @observer
 export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
@@ -34,15 +30,23 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
     _timeout: NodeJS.Timeout | undefined;
     @observable _x = 0;
     @observable _y = 0;
-    @observable _selected = false;
-    @observable _editing = false;
-    @observable _forceOpen = false;
 
     onPointerDown = (e: React.PointerEvent) => {
-        setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, false);
+        const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
+        setupMoveUpEvents(
+            this,
+            e,
+            this.onPointerMove,
+            emptyFunction,
+            (e, doubleTap) => {
+                if (doubleTap) LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
+                else this.props.select(false);
+            },
+            false
+        );
     };
     onPointerMove = action((e: PointerEvent, down: number[], delta: number[]) => {
-        const cdiv = this._ref && this._ref.current && this._ref.current.parentElement;
+        const cdiv = this._ref?.current?.parentElement;
         if (!this._isOpen && cdiv) {
             const bounds = cdiv.getBoundingClientRect();
             const pt = Utils.getNearestPointInPerimeter(bounds.left, bounds.top, bounds.width, bounds.height, e.clientX, e.clientY);
@@ -60,58 +64,8 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         }
         return false;
     });
-    @action
-    onClick = (e: React.MouseEvent) => {
-        if (e.button === 2 || e.ctrlKey || !this.layoutDoc.isLinkButton) {
-            this.props.select(false);
-        }
-        if (!this._doubleTap && !e.ctrlKey && e.button < 2) {
-            const anchorContainerDoc = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.LinkSource);
-            this._editing = true;
-            anchorContainerDoc && this.props.bringToFront(anchorContainerDoc, false);
-            if (anchorContainerDoc && !this.layoutDoc.onClick && !this._isOpen) {
-                this._timeout = setTimeout(
-                    action(() => {
-                        LinkFollower.FollowLink(this.rootDoc, anchorContainerDoc, this.props, false);
-                        this._editing = false;
-                    }),
-                    300 - (Date.now() - this._lastTap)
-                );
-                e.stopPropagation();
-            }
-        } else {
-            this._timeout && clearTimeout(this._timeout);
-            this._timeout = undefined;
-            this._doubleTap = false;
-            this.openLinkEditor(e);
-            e.stopPropagation();
-        }
-    };
 
-    openLinkDocOnRight = (e: React.MouseEvent) => {
-        this.props.addDocTab(this.rootDoc, OpenWhere.addRight);
-    };
-    openLinkTargetOnRight = (e: React.MouseEvent) => {
-        const alias = Doc.MakeAlias(Cast(this.layoutDoc[this.fieldKey], Doc, null));
-        alias._isLinkButton = undefined;
-        alias.layoutKey = 'layout';
-        this.props.addDocTab(alias, OpenWhere.addRight);
-    };
-    @action
-    openLinkEditor = action((e: React.MouseEvent) => {
-        SelectionManager.DeselectAll();
-        this._editing = this._forceOpen = true;
-    });
-
-    specificContextMenu = (e: React.MouseEvent): void => {
-        const funcs: ContextMenuProps[] = [];
-        funcs.push({ description: 'Open Link Target on Right', event: () => this.openLinkTargetOnRight(e), icon: 'eye' });
-        funcs.push({ description: 'Open Link on Right', event: () => this.openLinkDocOnRight(e), icon: 'eye' });
-        funcs.push({ description: 'Open Link Editor', event: () => this.openLinkEditor(e), icon: 'eye' });
-        funcs.push({ description: 'Toggle Always Show Link', event: () => (this.props.Document.linkDisplay = !this.props.Document.linkDisplay), icon: 'eye' });
-
-        ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' });
-    };
+    specificContextMenu = (e: React.MouseEvent): void => {};
 
     render() {
         TraceMobx();
@@ -122,22 +76,13 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
         const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor');
         const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1';
         const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25;
-
         const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title);
-        const flyout = (
-            <div className="linkAnchorBoxBox-flyout" title=" " onPointerOver={() => Doc.UnBrushDoc(this.rootDoc)}>
-                <LinkEditor sourceDoc={Cast(this.dataDoc[this.fieldKey], Doc, null)} hideback={true} linkDoc={this.rootDoc} showLinks={action(() => {})} />
-                {!this._forceOpen ? null : (
-                    <div className="linkAnchorBox-linkCloser" onPointerDown={action(() => (this._isOpen = this._editing = this._forceOpen = false))}>
-                        <FontAwesomeIcon color="dimgray" icon={'times'} size={'sm'} />
-                    </div>
-                )}
-            </div>
-        );
+        const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor1') ? 'anchor1' : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor2') ? 'anchor2' : '';
         return (
             <div
+                ref={this._ref}
+                title={targetTitle}
                 className={`linkAnchorBox-cont${small ? '-small' : ''}`}
-                //onPointerLeave={} //LinkDocPreview.Clear}
                 onPointerEnter={e =>
                     LinkDocPreview.SetLinkInfo({
                         docProps: this.props,
@@ -149,24 +94,15 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() {
                     })
                 }
                 onPointerDown={this.onPointerDown}
-                onClick={this.onClick}
-                title={targetTitle}
                 onContextMenu={this.specificContextMenu}
-                ref={this._ref}
                 style={{
+                    border: selView && this.rootDoc[selView] === this.rootDoc[this.fieldKey] ? `solid ${globalCssVariables.MEDIUM_GRAY} 2px` : undefined,
                     background,
                     left: `calc(${x}% - ${small ? 2.5 : 7.5}px)`,
                     top: `calc(${y}% - ${small ? 2.5 : 7.5}px)`,
                     transform: `scale(${anchorScale})`,
-                }}>
-                {!this._editing && !this._forceOpen ? null : (
-                    <Flyout anchorPoint={anchorPoints.LEFT_TOP} content={flyout} open={this._forceOpen ? true : undefined} onOpen={() => (this._isOpen = true)} onClose={action(() => (this._isOpen = this._forceOpen = this._editing = false))}>
-                        <span className="linkAnchorBox-button">
-                            <FontAwesomeIcon icon={'eye'} size={'lg'} />
-                        </span>
-                    </Flyout>
-                )}
-            </div>
+                }}
+            />
         );
     }
 }
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index c10751698..c6cabe269 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -94,6 +94,8 @@ export function DocListCastOrNull(field: FieldResult) {
 
 export const WidthSym = Symbol('Width');
 export const HeightSym = Symbol('Height');
+export const AnimationSym = Symbol('Animation');
+export const HighlightSym = Symbol('Highlight');
 export const DataSym = Symbol('Data');
 export const LayoutSym = Symbol('Layout');
 export const FieldsSym = Symbol('Fields');
@@ -329,6 +331,8 @@ export class Doc extends RefField {
     @observable private ___fieldKeys: any = {};
     @observable public [AclSym]: { [key: string]: symbol } = {};
     @observable public [DirectLinksSym]: Set<Doc> = new Set();
+    @observable public [AnimationSym]: Opt<Doc>;
+    @observable public [HighlightSym]: boolean = false;
 
     private [UpdatingFromServer]: boolean = false;
     private [ForceServerWrite]: boolean = false;
@@ -1261,53 +1265,55 @@ export namespace Doc {
     }
 
     export function linkFollowUnhighlight() {
-        Doc.UnhighlightAll();
+        UnhighlightWatchers.length = 0;
+        highlightedDocs.forEach(doc => Doc.UnHighlightDoc(doc));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
-        runInAction(() => (HighlightBrush.linkFollowEffect = undefined));
     }
 
-    let _lastDate = 0;
+    let UnhighlightWatchers: (() => void)[] = [];
+    let UnhighlightTimer: any;
+    export function AddUnlightWatcher(watcher: () => void) {
+        if (UnhighlightTimer) {
+            UnhighlightWatchers.push(watcher);
+        } else watcher();
+    }
     export function linkFollowHighlight(destDoc: Doc | Doc[], dataAndDisplayDocs = true, presEffect?: Doc) {
-        //linkFollowUnhighlight();
-        // runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = undefined));
-        // setTimeout(() => runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect)));
-        runInAction(() => presEffect && (HighlightBrush.linkFollowEffect = presEffect));
-        (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs));
+        linkFollowUnhighlight();
+        (destDoc instanceof Doc ? [destDoc] : destDoc).forEach(doc => Doc.HighlightDoc(doc, dataAndDisplayDocs, presEffect));
         document.removeEventListener('pointerdown', linkFollowUnhighlight);
         document.addEventListener('pointerdown', linkFollowUnhighlight);
-        const lastDate = (_lastDate = Date.now());
-        window.setTimeout(() => _lastDate === lastDate && linkFollowUnhighlight(), 5000);
+        if (UnhighlightTimer) clearTimeout(UnhighlightTimer);
+        UnhighlightTimer = window.setTimeout(() => {
+            UnhighlightWatchers.forEach(watcher => watcher());
+            linkFollowUnhighlight();
+            UnhighlightTimer = 0;
+        }, 5000);
     }
 
-    export class HighlightBrush {
-        @observable HighlightedDoc: Map<Doc, boolean> = new Map();
-        @observable static linkFollowEffect: Doc | undefined;
-    }
-    const highlightManager = new HighlightBrush();
+    var highlightedDocs = new Set<Doc>();
     export function IsHighlighted(doc: Doc) {
         if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false;
-        return highlightManager.HighlightedDoc.get(doc) || highlightManager.HighlightedDoc.get(Doc.GetProto(doc));
+        return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym];
     }
-    export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true) {
+    export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) {
         runInAction(() => {
-            highlightManager.HighlightedDoc.set(doc, true);
-            dataAndDisplayDocs && highlightManager.HighlightedDoc.set(Doc.GetProto(doc), true);
+            doc[AnimationSym] = presEffect;
+            highlightedDocs.add(doc);
+            doc[HighlightSym] = true;
+            if (dataAndDisplayDocs) {
+                highlightedDocs.add(Doc.GetProto(doc));
+                Doc.GetProto(doc)[HighlightSym] = true;
+            }
         });
     }
     export function UnHighlightDoc(doc: Doc) {
         runInAction(() => {
-            highlightManager.HighlightedDoc.set(doc, false);
-            highlightManager.HighlightedDoc.set(Doc.GetProto(doc), false);
+            highlightedDocs.delete(doc);
+            highlightedDocs.delete(Doc.GetProto(doc));
+            doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false;
+            doc[AnimationSym] = undefined;
         });
     }
-    export function UnhighlightAll() {
-        const mapEntries = highlightManager.HighlightedDoc.keys();
-        let docEntry: IteratorResult<Doc>;
-        while (!(docEntry = mapEntries.next()).done) {
-            const targetDoc = docEntry.value;
-            targetDoc && Doc.UnHighlightDoc(targetDoc);
-        }
-    }
     export function UnBrushAllDocs() {
         brushManager.BrushedDoc.clear();
     }
-- 
cgit v1.2.3-70-g09d2


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

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

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

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

