import { action, runInAction } from 'mobx'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DocumentView } from '../views/nodes/DocumentView'; import { FocusViewOptions } from '../views/nodes/FocusViewOptions'; import { OpenWhere } from '../views/nodes/OpenWhere'; import { PresBox } from '../views/nodes/trails'; import { ScriptingGlobals } from './ScriptingGlobals'; import { SnappingManager } from './SnappingManager'; 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 { public static Init() { DocumentView.FollowLink = LinkFollower.FollowLink; } // 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(() => SnappingManager.SetIsLinkFollowing(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 return LinkFollower.traverseLink( linkDoc, sourceDoc, action(() => { batch.end(); Doc.AddUnHighlightWatcher(() => SnappingManager.SetIsLinkFollowing(false)); }), altKey ? true : undefined ); }; public static traverseLink(link: Opt, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) { const getView = (doc: Doc) => DocumentView.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc)!); const isAnchor = (source?: Doc, anchor?: Doc) => Doc.AreProtosEqual(anchor, source) || Doc.AreProtosEqual(DocCast(anchor?.annotationOn), source); const linkDocs = link ? [link] : Doc.Links(sourceDoc); const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, DocCast(l.link_anchor_1))); // link docs where 'sourceDoc' is link_anchor_1 const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, DocCast(l.link_anchor_2))); // link docs where 'sourceDoc' is link_anchor_2 const fwdLinkWithoutTargetView = fwdLinks.find(l => !DocCast(l.link_anchor_2) || !getView(DocCast(l.link_anchor_2)!)); const backLinkWithoutTargetView = backLinks.find(l => !DocCast(l.link_anchor_1) || !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); let count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); if (!followLinks.length) { finished?.(); return false; } followLinks.forEach(async linkDoc => { const target = DocCast( sourceDoc === linkDoc.link_anchor_1 ? linkDoc.link_anchor_2 : sourceDoc === linkDoc.link_anchor_2 ? linkDoc.link_anchor_1 : Doc.AreProtosEqual(sourceDoc, DocCast(linkDoc.link_anchor_1)) || Doc.AreProtosEqual(DocCast(DocCast(linkDoc.link_anchor_1)?.annotationOn), sourceDoc) ? linkDoc.link_anchor_2 : linkDoc.link_anchor_1 ); if (target) { const srcAnchor = Doc.getOppositeAnchor(linkDoc, target) ?? sourceDoc; const doFollow = (canToggle?: boolean) => { const toggleTarget = canToggle && BoolCast(sourceDoc.followLinkToggle); const options: FocusViewOptions = { playAudio: BoolCast(srcAnchor.followLinkAudio), playMedia: BoolCast(srcAnchor.followLinkVideo), toggleTarget, noSelect: true, willPan: true, willZoomCentered: BoolCast(srcAnchor.followLinkZoom, false), zoomTime: NumCast(srcAnchor.followLinkTransitionTime, 500), zoomScale: Cast(srcAnchor.followLinkZoomScale, 'number', null), easeFunc: StrCast(srcAnchor.followLinkEase, 'ease') as 'ease' | 'linear', openLocation: StrCast(srcAnchor.followLinkLocation, OpenWhere.lightbox) as OpenWhere, effect: srcAnchor, zoomTextSelections: BoolCast(srcAnchor.followLinkZoomText), }; if (target.type === DocumentType.PRES) { const containerDocContext = DocumentView.getContextPath(sourceDoc, true); // gather all views that affect layout of sourceDoc so we can revert them after playing the rail DocumentView.DeselectAll(); if (!DocumentView.addViewRenderedCb(target, dv => containerDocContext.length && dv.ComponentView?.playTrail?.(containerDocContext))) { PresBox.OpenPresMinimized(target, [0, 0]); } finished?.(); } else { DocumentView.showDocument(target, options, allFinished); } }; let movedTarget = false; if (srcAnchor.followLinkLocation === OpenWhere.inParent) { const sourceDocParent = DocCast(sourceDoc.embedContainer); if (target.embedContainer instanceof Doc && target.embedContainer !== sourceDocParent) { Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutDataKey(target.embedContainer), target); movedTarget = true; } if (sourceDocParent) { if (!DocListCast(sourceDocParent[Doc.LayoutDataKey(sourceDocParent)]).includes(target)) { Doc.AddDocToList(sourceDocParent, Doc.LayoutDataKey(sourceDocParent), target); movedTarget = true; } Doc.SetContainer(target, sourceDocParent); } } const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (srcAnchor.followLinkXoffset !== undefined && moveTo[0] !== target.x) { target.x = moveTo[0]; movedTarget = true; } if (srcAnchor.followLinkYoffset !== undefined && moveTo[1] !== target.y) { target.y = moveTo[1]; movedTarget = true; } if (movedTarget) setTimeout(doFollow); else doFollow(true); } else { allFinished(); } }); return true; } } // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function followLink(doc: Doc, altKey: boolean) { // DocumentView.DeselectAll(); return LinkFollower.FollowLink(undefined, doc, altKey) ? undefined : { select: true }; });