diff options
| author | bobzel <zzzman@gmail.com> | 2023-06-23 21:44:01 -0400 |
|---|---|---|
| committer | bobzel <zzzman@gmail.com> | 2023-06-23 21:44:01 -0400 |
| commit | 85c017527f209c9d007d67ac70958843ab45e729 (patch) | |
| tree | e2649860002e0c60e98d84439a67235002ddd9a4 /src/client/util/LinkFollower.ts | |
| parent | e9d5dbeef2bf1dab9dfb863d970b70b3074e3d0a (diff) | |
| parent | 1429ab79eac9aa316082f52c14c576f6b3a97111 (diff) | |
Merge branch 'master' into heartbeat
Diffstat (limited to 'src/client/util/LinkFollower.ts')
| -rw-r--r-- | src/client/util/LinkFollower.ts | 190 |
1 files changed, 107 insertions, 83 deletions
diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index f75ac24f5..f74409e42 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -1,19 +1,19 @@ -import { action, observable, observe } from 'mobx'; -import { computedFn } from 'mobx-utils'; -import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; -import { List } from '../../fields/List'; -import { ProxyField } from '../../fields/Proxy'; -import { BoolCast, Cast, StrCast } from '../../fields/Types'; -import { LightboxView } from '../views/LightboxView'; -import { DocumentViewSharedProps, ViewAdjustment } from '../views/nodes/DocumentView'; +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'; - -type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void; /* * link doc: - * - anchor1: doc - * - anchor2: doc + * - link_anchor_1: doc + * - link_anchor_2: doc * * group doc: * - type: string representing the group type/name/category @@ -23,90 +23,114 @@ type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => vo * - 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<Doc>, sourceDoc: Doc, docViewProps: DocumentViewSharedProps, altKey: boolean, zoom: boolean = false) => { - const batch = UndoManager.StartBatch('follow link click'); - // open up target if it's not already in view ... - const createViewFunc = (doc: Doc, followLoc: string, finished?: Opt<() => void>) => { - const createTabForTarget = (didFocus: boolean) => - new Promise<ViewAdjustment>(res => { - const where = LightboxView.LightboxDoc ? 'lightbox' : StrCast(sourceDoc.followLinkLocation, followLoc); - docViewProps.addDocTab(doc, where); - setTimeout(() => { - const getFirstDocView = LightboxView.LightboxDoc ? DocumentManager.Instance.getLightboxDocumentView : DocumentManager.Instance.getFirstDocumentView; - const targDocView = getFirstDocView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise. - if (targDocView) { - targDocView.props.focus(doc, { - willZoom: BoolCast(sourceDoc.followLinkZoom, false), - afterFocus: (didFocus: boolean) => { - finished?.(); - res(ViewAdjustment.resetView); - return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing)); - }, - }); - } else { - res(where !== 'inPlace' ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target - } - }); - }); - - if (!sourceDoc.followLinkZoom) { - createTabForTarget(false); - } else { - // first focus & zoom onto this (the clicked document). Then execute the function to focus on the target - docViewProps.focus(sourceDoc, { willZoom: BoolCast(sourceDoc.followLinkZoom, true), scale: 1, afterFocus: createTabForTarget }); - } - }; - LinkFollower.traverseLink(linkDoc, sourceDoc, createViewFunc, BoolCast(sourceDoc.followLinkZoom, zoom), docViewProps.ContainingCollectionDoc, batch.end, altKey ? true : undefined); + public static FollowLink = (linkDoc: Opt<Doc>, 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<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, zoom = false, currentContext?: Doc, finished?: () => void, traverseBacklink?: boolean) { - const linkDocs = link ? [link] : DocListCast(sourceDoc.links); - 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).length === 0); - const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews(d.anchor1 as Doc).length === 0); - const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; - const linkDocList = linkWithoutTargetDoc ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs; - const followLinks = sourceDoc.isPushpin ? linkDocList : linkDocList.slice(0, 1); + public static traverseLink(link: Opt<Doc>, 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 => { - if (linkDoc) { - const target = ( - sourceDoc === linkDoc.anchor1 - ? linkDoc.anchor2 - : sourceDoc === linkDoc.anchor2 - ? linkDoc.anchor1 - : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) - ? linkDoc.anchor2 - : linkDoc.anchor1 - ) as Doc; - if (target) { - if (target.TourMap) { - const fieldKey = Doc.LayoutFieldKey(target); - const tour = DocListCast(target[fieldKey]).reverse(); - LightboxView.SetLightboxDoc(currentContext, undefined, tour); - setTimeout(LightboxView.Next); - allFinished(); - } else { - const containerAnnoDoc = Cast(target.annotationOn, Doc, null); - const containerDoc = containerAnnoDoc || target; - var containerDocContext = containerDoc?.context ? [Cast(containerDoc?.context, Doc, null)] : ([] as Doc[]); - while (containerDocContext.length && !DocumentManager.Instance.getDocumentView(containerDocContext[0]) && containerDocContext[0].context) { - containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + const 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]); } - const targetContexts = LightboxView.LightboxDoc ? [containerAnnoDoc || containerDocContext[0]].filter(a => a) : containerDocContext; - DocumentManager.Instance.jumpToDocument(target, zoom, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'lightbox'), finished), targetContexts, linkDoc, undefined, sourceDoc, allFinished); + 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; } - } else { - allFinished(); - } + 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<Field>) { + return ScriptCast(field)?.script.originalScript.includes('followLink('); +} |
