aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/LinkFollower.ts
blob: f75ac24f5a859158ac69cd8f8320b47d8d29651a (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
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 { DocumentManager } from './DocumentManager';
import { UndoManager } from './UndoManager';

type CreateViewFunc = (doc: Doc, followLinkLocation: string, finished?: () => void) => void;
/*
 * 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, 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 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);
        var count = 0;
        const allFinished = () => ++count === 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 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);
                    }
                } else {
                    allFinished();
                }
            } else {
                allFinished();
            }
        });
    }
}