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

---
 src/client/views/linking/LinkMenu.tsx | 31 ++++++++++++++++---------------
 1 file changed, 16 insertions(+), 15 deletions(-)

(limited to 'src/client/views/linking/LinkMenu.tsx')

diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx
index 0096a58bd..0c46a6d96 100644
--- a/src/client/views/linking/LinkMenu.tsx
+++ b/src/client/views/linking/LinkMenu.tsx
@@ -8,12 +8,13 @@ import { LinkEditor } from './LinkEditor';
 import './LinkMenu.scss';
 import { LinkMenuGroup } from './LinkMenuGroup';
 import React = require('react');
+import { emptyFunction } from '../../../Utils';
 
 interface Props {
     docView: DocumentView;
-    position?: { x?: number; y?: number };
+    style?: { left: number; top: number };
     itemHandler?: (doc: Doc) => void;
-    clearLinkEditor: () => void;
+    clearLinkEditor?: () => void;
 }
 
 /**
@@ -25,26 +26,24 @@ export class LinkMenu extends React.Component<Props> {
     @observable _editingLink?: Doc;
     @observable _linkMenuRef = React.createRef<HTMLDivElement>();
 
-    @computed get position() {
-        return this.props.position ?? (dv => ({ x: dv?.left || 0, y: (dv?.bottom || 0) + 15 }))(this.props.docView.getBounds());
-    }
-
-    clear = action(() => {
-        this.props.clearLinkEditor();
-        this._editingLink = undefined;
-    });
+    clear = !this.props.clearLinkEditor
+        ? undefined
+        : action(() => {
+              this.props.clearLinkEditor?.();
+              this._editingLink = undefined;
+          });
 
     componentDidMount() {
-        document.addEventListener('pointerdown', this.onPointerDown, true);
+        this.props.clearLinkEditor && document.addEventListener('pointerdown', this.onPointerDown, true);
     }
     componentWillUnmount() {
-        document.removeEventListener('pointerdown', this.onPointerDown, true);
+        this.props.clearLinkEditor && document.removeEventListener('pointerdown', this.onPointerDown, true);
     }
 
     onPointerDown = action((e: PointerEvent) => {
         LinkDocPreview.Clear();
         if (!this._linkMenuRef.current?.contains(e.target as any) && !this._editorRef.current?.contains(e.target as any)) {
-            this.clear();
+            this.clear?.();
         }
     });
 
@@ -67,13 +66,15 @@ export class LinkMenu extends React.Component<Props> {
             />
         ));
 
-        return linkItems.length ? linkItems : this.props.position ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
+        return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>];
     };
 
     render() {
         const sourceDoc = this.props.docView.props.Document;
+        const style = this.props.style ?? (dv => ({ left: dv?.left || 0, top: this.props.docView.topMost ? undefined : (dv?.bottom || 0) + 15, bottom: this.props.docView.topMost ? 20 : undefined, maxWidth: 200 }))(this.props.docView.getBounds());
+
         return (
-            <div className="linkMenu" ref={this._linkMenuRef} style={{ left: this.position.x, top: this.props.docView.topMost ? undefined : this.position.y, bottom: this.props.docView.topMost ? 20 : undefined }}>
+            <div className="linkMenu" ref={this._linkMenuRef} style={{ ...style }}>
                 {this._editingLink ? (
                     <div className="linkMenu-listEditor">
                         <LinkEditor sourceDoc={sourceDoc} linkDoc={this._editingLink} showLinks={action(() => (this._editingLink = undefined))} />
-- 
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/linking/LinkMenu.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 b599afe5fdcf8afc38432e34b1e175f82499d25b Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Sat, 11 Mar 2023 14:05:18 -0500
Subject: changed links from being a computed field on documents to being
 accessed by a function call on LinkManager

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

(limited to 'src/client/views/linking/LinkMenu.tsx')

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

