aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/LinkFollower.ts
blob: a4f54f1cfe08423bff6aa2587c843819770acc1a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
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<Doc>, 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<Doc>, 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 };
});