aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/LinkFollower.ts
blob: df61ecece1864325349db2176496ad27a0f6f555 (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
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 { DocumentDecorations } from '../views/DocumentDecorations';
import { DocFocusOptions, DocumentViewSharedProps, OpenWhere } from '../views/nodes/DocumentView';
import { PresBox } from '../views/nodes/trails';
import { DocumentManager } from './DocumentManager';
import { LinkManager } from './LinkManager';
import { SelectionManager } from './SelectionManager';
import { UndoManager } from './UndoManager';
/*
 * link doc:
 * - anchor1: doc
 * - anchor2: 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 {
    // 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 click');
        runInAction(() => (DocumentDecorations.Instance.overrideBounds = 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(() => (DocumentDecorations.Instance.overrideBounds = false)));
            }),
            altKey ? true : undefined
        );
    };

    public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) {
        const linkDocs = link ? [link] : LinkManager.Links(sourceDoc);
        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).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0);
        const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0);
        const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView;
        const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs;
        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.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) {
                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),
                        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.context);
                    if (target.context instanceof Doc && target.context !== sourceDocParent) {
                        Doc.RemoveDocFromList(target.context, Doc.LayoutFieldKey(target.context), target);
                        movedTarget = true;
                    }
                    if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) {
                        Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target);
                        movedTarget = true;
                    }
                    target.context = 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();
            }
        });
    }
}