From dd08c20ec6df3fad6ecd6b16c787f10b0c23feb4 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 2 May 2024 00:39:31 -0400
Subject: lots more dependency cycle unwinding.

---
 src/client/views/nodes/DocumentView.tsx | 203 +++++++++++++++-----------------
 1 file changed, 96 insertions(+), 107 deletions(-)

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

diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 1e7f1015a..f8ce50c98 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -7,10 +7,10 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { Bounce, Fade, Flip, JackInTheBox, Roll, Rotate, Zoom } from 'react-awesome-reveal';
-import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick } from '../../../ClientUtils';
+import { ClientUtils, DivWidth, isTargetChildOf as isParentOf, lightOrDark, returnFalse, returnVal, simulateMouseClick } from '../../../ClientUtils';
 import { Utils, emptyFunction, emptyPath } from '../../../Utils';
 import { Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../fields/Doc';
-import { AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
+import { AclAdmin, AclEdit, AclPrivate, Animation, AudioPlay, DocData, DocViews } from '../../../fields/DocSymbols';
 import { Id } from '../../../fields/FieldSymbols';
 import { InkTool } from '../../../fields/InkField';
 import { List } from '../../../fields/List';
@@ -20,20 +20,17 @@ import { ScriptField } from '../../../fields/ScriptField';
 import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
 import { AudioField } from '../../../fields/URLField';
 import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
+import { AudioAnnoState } from '../../../server/SharedMediaTypes';
 import { DocServer } from '../../DocServer';
-import { Networking } from '../../Network';
 import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
 import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
 import { Docs } from '../../documents/Documents';
-import { DictationManager } from '../../util/DictationManager';
-import { DocumentManager } from '../../util/DocumentManager';
 import { DragManager } from '../../util/DragManager';
 import { dropActionType } from '../../util/DropActionTypes';
 import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
-import { LinkManager, UPDATE_SERVER_CACHE } from '../../util/LinkManager';
+import { UPDATE_SERVER_CACHE } from '../../util/LinkManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SearchUtil } from '../../util/SearchUtil';
-import { SelectionManager } from '../../util/SelectionManager';
 import { SharingManager } from '../../util/SharingManager';
 import { SnappingManager } from '../../util/SnappingManager';
 import { UndoManager, undoBatch, undoable } from '../../util/UndoManager';
@@ -42,16 +39,15 @@ import { ContextMenuProps } from '../ContextMenuItem';
 import { DocComponent, ViewBoxInterface } from '../DocComponent';
 import { EditableView } from '../EditableView';
 import { FieldsDropdown } from '../FieldsDropdown';
-import { GestureOverlay } from '../GestureOverlay';
 import { LightboxView } from '../LightboxView';
-import { AudioAnnoState, StyleProp } from '../StyleProvider';
+import { PinProps } from '../PinFuncs';
+import { StyleProp } from '../StyleProp';
 import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView';
 import { DocumentLinksButton } from './DocumentLinksButton';
 import './DocumentView.scss';
 import { FieldViewProps, FieldViewSharedProps } from './FieldView';
 import { FocusViewOptions } from './FocusViewOptions';
 import { KeyValueBox } from './KeyValueBox';
-import { LinkAnchorBox } from './LinkAnchorBox';
 import { OpenWhere } from './OpenWhere';
 import { FormattedTextBox } from './formattedText/FormattedTextBox';
 import { PresEffect, PresEffectDirection } from './trails';
@@ -90,6 +86,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     // this makes mobx trace() statements more descriptive
     public get displayName()  { return 'DocumentViewInternal(' + this.Document.title + ')'; } // prettier-ignore
     public static SelectAfterContextMenu = true; //  whether a document should be selected after it's contextmenu is triggered.
+
     /**
      * This function is filled in by MainView to allow non-viewBox views to add Docs as tabs without
      * needing to know about/reference MainView
@@ -183,11 +180,10 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document).  e.g.,
     //     - PDF text regions are rendered as an Annotations without generating a DocumentView, '
     //     - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link
-    //     - and links to PDF/Web docs at a certain scroll location never create an explicit view.
-    // For each of these, we create LinkAnchorBox's on the border of the DocumentView.
+    //     - and links to PDF/Web docs at a certain scroll location never create an explicit anchor view.
     @computed get directLinks() {
         TraceMobx();
-        return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(
+        return Doc.Links(this.Document).filter(
             link =>
                 (link.link_matchEmbeddings ? link.link_anchor_1 === this.Document : Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.Document)) ||
                 (link.link_matchEmbeddings ? link.link_anchor_2 === this.Document : Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.Document)) ||
@@ -197,11 +193,11 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }
     @computed get _allLinks(): Doc[] {
         TraceMobx();
-        return LinkManager.Instance.getAllRelatedLinks(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
+        return Doc.Links(this.Document).filter(link => !link.link_matchEmbeddings || link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document);
     }
 
     @computed get filteredLinks() {
-        return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
+        return DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []);
     }
 
     componentWillUnmount() {
@@ -259,7 +255,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     startDragging(x: number, y: number, dropAction: dropActionType, hideSource = false) {
         const docView = this._docView;
         if (this._mainCont.current && docView) {
-            const views = SelectionManager.Views.filter(dv => dv.ContentDiv);
+            const views = DocumentView.Selected().filter(dv => dv.ContentDiv);
             const selected = views.length > 1 && views.some(dv => dv.Document === this.Document) ? views : [docView];
             const dragData = new DragManager.DocumentDragData(selected.map(dv => dv.Document));
             const screenXf = docView.screenToViewTransform();
@@ -289,8 +285,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     };
     onBrowseClick = (e: React.MouseEvent) => {
         const browseTransitionTime = 500;
-        SelectionManager.DeselectAll();
-        DocumentManager.Instance.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
+        DocumentView.DeselectAll();
+        DocumentView.showDocument(this.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => {
             const options: FocusViewOptions = { pointFocus: { X: e.clientX, Y: e.clientY }, zoomTime: browseTransitionTime };
             if (!focused && this._docView) {
                 this._docView
@@ -327,7 +323,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     UndoManager.RunInBatch(() => this.onDoubleClickHandler.script.run(scriptProps, console.log).result?.select && this._props.select(false), 'on double click: ' + this.Document.title);
                 } else if (!Doc.IsSystem(this.Document) && defaultDblclick !== 'ignore') {
                     UndoManager.RunInBatch(() => LightboxView.Instance.AddDocTab(this.Document, OpenWhere.lightbox), 'double tap');
-                    SelectionManager.DeselectAll();
+                    DocumentView.DeselectAll();
                     Doc.UnBrushDoc(this.Document);
                 } else {
                     this._singleClickFunc?.();
@@ -377,7 +373,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (this._props.isGroupActive?.() === 'child' && !this._props.isDocumentActive?.()) return;
         // eslint-disable-next-line no-use-before-define
         this._longPressSelector = setTimeout(() => DocumentView.LongPress && this._props.select(false), 1000);
-        if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this._docView;
+        if (!DocumentView.DownDocView) DocumentView.DownDocView = this._docView;
 
         this._downX = e.clientX;
         this._downY = e.clientY;
@@ -463,7 +459,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
     }, 'set toggle detail');
 
     drop = undoable((e: Event, de: DragManager.DropEvent) => {
-        if (this._props.dontRegisterView || this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) return false;
+        if (this._props.dontRegisterView) return false;
         if (this.Document === Doc.ActiveDashboard) {
             e.stopPropagation();
             e.preventDefault();
@@ -484,7 +480,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     if (linkDoc) {
                         de.complete.linkDocument = linkDoc;
                         linkDoc.layout_isSvg = true;
-                        DocumentManager.LinkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
+                        DocumentView.linkCommonAncestor(linkDoc)?.ComponentView?.addDocument?.(linkDoc);
                     }
                 }
                 e.stopPropagation();
@@ -516,7 +512,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         if (e && this.layoutDoc.layout_hideContextMenu && Doc.noviceMode) {
             e.preventDefault();
             e.stopPropagation();
-            // !this._props.isSelected(true) && SelectionManager.SelectView(this.DocumentView(), false);
+            // !this._props.isSelected(true) && DocumentView.SelectView(this.DocumentView(), false);
         }
         // the touch onContextMenu is button 0, the pointer onContextMenu is button 2
         if (e) {
@@ -576,8 +572,8 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             if (this._props.bringToFront) {
                 const zorders = cm.findByDescription('ZOrder...');
                 const zorderItems: ContextMenuProps[] = zorders && 'subitems' in zorders ? zorders.subitems : [];
-                zorderItems.push({ description: 'Bring to Front', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
-                zorderItems.push({ description: 'Send to Back', event: () => SelectionManager.Views.forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' });
+                zorderItems.push({ description: 'Bring to Front', event: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, false)), icon: 'arrow-up' });
+                zorderItems.push({ description: 'Send to Back', event: () => DocumentView.Selected().forEach(dv => dv._props.bringToFront?.(dv.Document, true)), icon: 'arrow-down' });
                 zorderItems.push({
                     description: !this.layoutDoc._keepZDragged ? 'Keep ZIndex when dragged' : 'Allow ZIndex to change when dragged',
                     event: undoBatch(
@@ -601,7 +597,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' });
                     !Doc.noviceMode && onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' });
-                } else if (LinkManager.Links(this.Document).length) {
+                } else if (Doc.Links(this.Document).length) {
                     onClicks.push({ description: 'Restore On Click default', event: () => this.noOnClick(), icon: 'link' });
                     onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' });
                     !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' });
@@ -718,7 +714,7 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             case StyleProp.PointerEvents: return 'none';
             case StyleProp.Highlighting:  return undefined;
             case StyleProp.Opacity:       {
-                const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []).filter(d => d.link_displayLine || Doc.UserDoc().showLinkLines);
+                const filtered = DocUtils.FilterDocs(this.directLinks, this._props.childFilters?.() ?? [], []);
                 return filtered.some(link => link._link_displayArrow) ? 0 : undefined;
             }
             default:
@@ -726,34 +722,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
         return this._props.styleProvider?.(doc, props, property);
     };
 
-    // eslint-disable-next-line no-return-assign
-    removeLinkByHiding = (link: Doc) => () => link.link_displayLine = false; // prettier-ignore
-    @computed get allLinkEndpoints() {
-        // the small blue dots that mark the endpoints of links
-        if (this._componentView instanceof KeyValueBox || this._props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this._props.dontRegisterView || this.layoutDoc.layout_unrendered) return null;
-        return this.filteredLinks.map(link => (
-            <div className="documentView-anchorCont" key={link[Id]}>
-                <DocumentView
-                    {...this._props}
-                    isContentActive={returnFalse}
-                    Document={link}
-                    containerViewPath={this._props.docViewPath}
-                    PanelWidth={this.anchorPanelWidth}
-                    PanelHeight={this.anchorPanelHeight}
-                    dontRegisterView={false}
-                    layout_showTitle={returnEmptyString}
-                    hideCaptions
-                    hideLinkAnchors
-                    layout_fitWidth={returnTrue}
-                    removeDocument={this.removeLinkByHiding(link)}
-                    styleProvider={this.anchorStyleProvider}
-                    LayoutTemplate={undefined}
-                    LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${LinkManager.anchorIndex(link, this.Document)}`)}
-                />
-            </div>
-        ));
-    }
-
     @computed get viewBoxContents() {
         TraceMobx();
         const isInk = this.layoutDoc._layout_isSvg && !this._props.LayoutTemplateString;
@@ -780,7 +748,6 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
                     setTitleFocus={this.setTitleFocus}
                     hideClickBehaviors={BoolCast(this.Document.hideClickBehaviors)}
                 />
-                {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints}
             </div>
         );
     }
@@ -1008,50 +975,45 @@ export class DocumentViewInternal extends DocComponent<FieldViewProps & Document
             default:                    return renderDoc;
         }
     }
-    public static recordAudioAnnotation(dataDoc: Doc, field: string, onRecording?: (stop: () => void) => void, onEnd?: () => void) {
-        let gumStream: any;
-        let recorder: any;
-        navigator.mediaDevices.getUserMedia({ audio: true }).then(stream => {
-            let audioTextAnnos = Cast(dataDoc[field + '_audioAnnotations_text'], listSpec('string'), null);
-            if (audioTextAnnos) audioTextAnnos.push('');
-            else audioTextAnnos = dataDoc[field + '_audioAnnotations_text'] = new List<string>(['']);
-            DictationManager.Controls.listen({
-                interimHandler: value => { audioTextAnnos[audioTextAnnos.length - 1] = value; }, // prettier-ignore
-                continuous: { indefinite: false },
-            }).then(results => {
-                if (results && [DictationManager.Controls.Infringed].includes(results)) {
-                    DictationManager.Controls.stop();
-                }
-                onEnd?.();
-            });
-
-            gumStream = stream;
-            recorder = new MediaRecorder(stream);
-            recorder.ondataavailable = async (e: any) => {
-                const [{ result }] = await Networking.UploadFilesToServer({ file: e.data });
-                if (!(result instanceof Error)) {
-                    const audioField = new AudioField(result.accessPaths.agnostic.client);
-                    const audioAnnos = Cast(dataDoc[field + '_audioAnnotations'], listSpec(AudioField), null);
-                    if (audioAnnos) audioAnnos.push(audioField);
-                    else dataDoc[field + '_audioAnnotations'] = new List([audioField]);
-                }
-            };
-            recorder.start();
-            const stopFunc = () => {
-                recorder.stop();
-                DictationManager.Controls.stop(/* false */);
-                dataDoc.audioAnnoState = AudioAnnoState.stopped;
-                gumStream.getAudioTracks()[0].stop();
-            };
-            if (onRecording) onRecording(stopFunc);
-            else setTimeout(stopFunc, 5000);
-        });
-    }
 }
 
 @observer
 export class DocumentView extends DocComponent<DocumentViewProps>() {
     public static ROOT_DIV = 'documentView-effectsWrapper';
+    // LinkFollower
+    public static FollowLink: (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => boolean;
+    // selection funcs
+    public static DeselectAll: (except?: Doc) => void | undefined;
+    public static DeselectView: (dv: DocumentView | undefined) => void | undefined;
+    public static SelectView: (dv: DocumentView | undefined, extendSelection: boolean) => void | undefined;
+    public static Selected: () => DocumentView[];
+    public static SelectedDocs: () => Doc[];
+    public static SelectSchemaDoc: (doc: Doc, deselectAllFirst?: boolean) => void;
+    public static SelectedSchemaDoc: () => Opt<Doc>;
+    // view mgr funcs
+    public static activateTabView: (tabDoc: Doc) => boolean;
+    public static allViews: () => DocumentView[];
+    public static addView: (dv: DocumentView) => void | undefined;
+    public static removeView: (dv: DocumentView) => void | undefined;
+    public static addViewRenderedCb: (doc: Opt<Doc>, func: (dv: DocumentView) => any) => boolean;
+    public static getFirstDocumentView: (toFind: Doc) => DocumentView | undefined;
+    public static getDocumentView: (target: Doc | undefined, preferredCollection?: DocumentView) => Opt<DocumentView>;
+    public static getContextPath: (doc: Opt<Doc>, includeExistingViews?: boolean) => Doc[];
+    public static getLightboxDocumentView: (toFind: Doc) => Opt<DocumentView>;
+    public static showDocumentView: (targetDocView: DocumentView, options: FocusViewOptions) => Promise<void>;
+    public static showDocument: (
+        targetDoc: Doc, // document to display
+        optionsIn: FocusViewOptions, // options for how to navigate to target
+        finished?: (changed: boolean) => void // func called after focusing on target with flag indicating whether anything needed to be done.
+    ) => Promise<void>;
+    public static linkCommonAncestor: (link: Doc) => DocumentView | undefined;
+    // pin func
+    public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void;
+    // gesture
+    public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event.  used by GestureOverlay to determine the doc a gesture should apply to.
+    // media playing
+    @observable public static CurrentlyPlaying: DocumentView[] = [];
+
     public get displayName() {  return 'DocumentView(' + (this.Document?.title??"") + ')';  } // prettier-ignore
     public ContentRef = React.createRef<HTMLDivElement>();
     private _htmlOverlayEffect: Opt<Doc>;
@@ -1138,14 +1100,14 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     componentDidMount() {
         runInAction(() => this.Document[DocViews].add(this));
         this._disposers.onViewMounted = reaction(() => ScriptCast(this.Document.onViewMounted)?.script?.run({ this: this.Document }).result, emptyFunction);
-        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.AddView(this);
+        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.addView(this);
     }
 
     componentWillUnmount() {
         this._viewTimer && clearTimeout(this._viewTimer);
         runInAction(() => this.Document[DocViews].delete(this));
         Object.values(this._disposers).forEach(disposer => disposer?.());
-        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentManager.Instance.RemoveView(this);
+        !BoolCast(this.Document.dontRegisterView, this._props.dontRegisterView) && DocumentView.removeView(this);
     }
 
     public set IsSelected(val)  { runInAction(() => { this._selected = val; }); } // prettier-ignore
@@ -1176,10 +1138,6 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         const xf = this.screenToContentsTransform().scale(this.nativeScaling).inverse();
         const [[left, top], [right, bottom]] = [xf.transformPoint(0, 0), xf.transformPoint(this.panelWidth, this.panelHeight)];
 
-        if (this._props.LayoutTemplateString?.includes(LinkAnchorBox.name)) {
-            const docuBox = this.ContentDiv.getElementsByClassName('linkAnchorBox-cont');
-            if (docuBox.length) return { ...docuBox[0].getBoundingClientRect(), transition: undefined };
-        }
         // transition is returned so that the bounds will 'update' at the end of an animated transition.  This is needed by xAnchor in LinkBox
         const transition = this.docViewPath().find((parent: DocumentView) => parent.DataTransition?.() || parent.ComponentView?.viewTransition?.());
         return { left, top, right, bottom, transition: transition?.DataTransition?.() || transition?.ComponentView?.viewTransition?.() };
@@ -1289,7 +1247,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
         if (checkResult) {
             return Doc.UserDoc().defaultTextLayout;
         }
-        const view = SelectionManager.Views[0]?._props.renderDepth > 0 ? SelectionManager.Views[0] : undefined;
+        const view = DocumentView.Selected()[0]?._props.renderDepth > 0 ? DocumentView.Selected()[0] : undefined;
         undoable(() => {
             let tempDoc: Opt<Doc>;
             if (view) {
@@ -1359,11 +1317,11 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
     screenToLocalScale = () => this._props.ScreenToLocalTransform().Scale;
     isSelected = () => this.IsSelected;
     select = (extendSelection: boolean, focusSelection?: boolean) => {
-        if (this.IsSelected && SelectionManager.Views.length > 1) SelectionManager.DeselectView(this);
+        if (this.IsSelected && DocumentView.Selected().length > 1) DocumentView.DeselectView(this);
         else {
-            SelectionManager.SelectView(this, extendSelection);
+            DocumentView.SelectView(this, extendSelection);
             if (focusSelection) {
-                DocumentManager.Instance.showDocument(this.Document, {
+                DocumentView.showDocument(this.Document, {
                     willZoomCentered: true,
                     zoomScale: 0.9,
                     zoomTime: 500,
@@ -1506,6 +1464,37 @@ export function returnEmptyDocViewList() {
     return emptyPath;
 }
 
+// eslint-disable-next-line default-param-last
+export function DocFocusOrOpen(docIn: Doc, optionsIn: FocusViewOptions = { willZoomCentered: true, zoomScale: 0, openLocation: OpenWhere.toggleRight }, containingDoc?: Doc) {
+    let doc = docIn;
+    const options = optionsIn;
+    const func = () => {
+        const cv = DocumentView.getDocumentView(containingDoc);
+        const dv = DocumentView.getDocumentView(doc, cv);
+        if (dv && (!containingDoc || dv.containerViewPath?.().lastElement()?.Document === containingDoc)) {
+            DocumentView.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.Document));
+        } else {
+            const container = DocCast(containingDoc ?? doc.embedContainer ?? Doc.BestEmbedding(doc));
+            const showDoc = !Doc.IsSystem(container) && !cv ? container : doc;
+            options.toggleTarget = undefined;
+            DocumentView.showDocument(showDoc, options, () => DocumentView.showDocument(doc, { ...options, openLocation: undefined })).then(() => {
+                const cvFound = DocumentView.getDocumentView(containingDoc);
+                const dvFound = DocumentView.getDocumentView(doc, cvFound);
+                dvFound && Doc.linkFollowHighlight(dvFound.Document);
+            });
+        }
+    };
+    if (Doc.IsDataProto(doc) && Doc.GetEmbeddings(doc).some(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))) {
+        doc = Doc.GetEmbeddings(doc).find(embed => embed.hidden && [AclAdmin, AclEdit].includes(GetEffectiveAcl(embed)))!;
+    }
+    if (doc.hidden) {
+        doc.hidden = false;
+        options.toggleTarget = false;
+        setTimeout(func);
+    } else func();
+}
+ScriptingGlobals.add(DocFocusOrOpen);
+
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function deiconifyView(documentView: DocumentView) {
     documentView.iconify();
@@ -1527,9 +1516,9 @@ ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSour
     const collectedLinks = DocListCast(linkCollection[DocData].data);
     let wid = NumCast(linkSource._width);
     let embedding: Doc | undefined;
-    const links = LinkManager.Links(linkSource);
+    const links = Doc.Links(linkSource);
     links.forEach(link => {
-        const other = LinkManager.getOppositeAnchor(link, linkSource);
+        const other = Doc.getOppositeAnchor(link, linkSource);
         const otherdoc = DocCast(other?.annotationOn ?? other);
         if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) {
             embedding = Doc.MakeEmbedding(otherdoc);
-- 
cgit v1.2.3-70-g09d2

