import { action, observable, runInAction } from 'mobx'; import { Doc, DocListCast, Field, FieldResult, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DocFocusOptions, OpenWhere } from '../views/nodes/DocumentView'; import { PresBox } from '../views/nodes/trails'; import { DocumentManager } from './DocumentManager'; import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SelectionManager } from './SelectionManager'; import { UndoManager } from './UndoManager'; /* * link doc: * - link_anchor_1: doc * - link_anchor_2: doc * * group doc: * - type: string representing the group type/name/category * - metadata: doc representing the metadata kvps * * metadata doc: * - user defined kvps */ export class LinkFollower { @observable public static IsFollowing = false; // follows a link - if the target is on screen, it highlights/pans to it. // if the target isn't onscreen, then it will open up the target in the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); public static FollowLink = (linkDoc: Opt, sourceDoc: Doc, altKey: boolean) => { const batch = UndoManager.StartBatch('Follow Link'); runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value LinkFollower.traverseLink( linkDoc, sourceDoc, action(() => { batch.end(); Doc.AddUnHighlightWatcher(action(() => (LinkFollower.IsFollowing = false))); }), altKey ? true : undefined ); }; public static traverseLink(link: Opt, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) { const getView = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc)); const isAnchor = (sourceDoc: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, sourceDoc) || Doc.AreProtosEqual(anchor.annotationOn as Doc, sourceDoc); const linkDocs = link ? [link] : LinkManager.Links(sourceDoc); const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1 const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2 const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2))); const backLinkWithoutTargetView = backLinks.find(l => !getView(DocCast(l.link_anchor_1))); const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks; const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); var count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); if (!followLinks.length) finished?.(); followLinks.forEach(async linkDoc => { const target = ( sourceDoc === linkDoc.link_anchor_1 ? linkDoc.link_anchor_2 : sourceDoc === linkDoc.link_anchor_2 ? linkDoc.link_anchor_1 : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc) ? linkDoc.link_anchor_2 : linkDoc.link_anchor_1 ) as Doc; if (target) { const doFollow = (canToggle?: boolean) => { const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle); const options: DocFocusOptions = { playAudio: BoolCast(sourceDoc.followLinkAudio), toggleTarget, noSelect: true, willPan: true, willZoomCentered: BoolCast(sourceDoc.followLinkZoom, false), zoomTime: NumCast(sourceDoc.followLinkTransitionTime, 500), zoomScale: Cast(sourceDoc.followLinkZoomScale, 'number', null), easeFunc: StrCast(sourceDoc.followLinkEase, 'ease') as any, openLocation: StrCast(sourceDoc.followLinkLocation, OpenWhere.lightbox) as OpenWhere, effect: sourceDoc, zoomTextSelections: BoolCast(sourceDoc.followLinkZoomText), }; if (target.type === DocumentType.PRES) { const containerDocContext = DocumentManager.GetContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail SelectionManager.DeselectAll(); if (!DocumentManager.Instance.AddViewRenderedCb(target, dv => containerDocContext.length && (dv.ComponentView as PresBox).PlayTrail(containerDocContext))) { PresBox.OpenPresMinimized(target, [0, 0]); } finished?.(); } else { DocumentManager.Instance.showDocument(target, options, allFinished); } }; let movedTarget = false; if (sourceDoc.followLinkLocation === OpenWhere.inParent) { const sourceDocParent = DocCast(sourceDoc.embedContainer); if (target.embedContainer instanceof Doc && target.embedContainer !== sourceDocParent) { Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutFieldKey(target.embedContainer), target); movedTarget = true; } if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) { Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target); movedTarget = true; } target.embedContainer = sourceDocParent; const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) { target.x = moveTo[0]; movedTarget = true; } if (sourceDoc.followLinkYoffset !== undefined && moveTo[1] !== target.y) { target.y = moveTo[1]; movedTarget = true; } if (movedTarget) setTimeout(doFollow); else doFollow(true); } else doFollow(true); } else { allFinished(); } }); } } ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) { SelectionManager.DeselectAll(); LinkFollower.FollowLink(undefined, doc, altKey); }); export function FollowLinkScript() { return ScriptField.MakeScript('followLink(this,altKey)', { altKey: 'boolean' }); } export function IsFollowLinkScript(field: FieldResult) { return ScriptCast(field)?.script.originalScript.includes('followLink('); }