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
|
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 { LightboxView } from '../views/LightboxView';
import { DocFocusOptions, DocumentViewSharedProps, OpenWhere, ViewAdjustment } from '../views/nodes/DocumentView';
import { PresEffect, PresEffectDirection } from '../views/nodes/trails';
import { DocumentManager } from './DocumentManager';
import { LinkManager } from './LinkManager';
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) => {
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 ? OpenWhere.inPlace : (StrCast(sourceDoc.followLinkLocation, followLoc) as OpenWhere);
docViewProps.addDocTab(doc, where);
setTimeout(() => {
const targDocView = DocumentManager.Instance.getFirstDocumentView(doc); // get first document view available within the lightbox if that's open, or anywhere otherwise.
if (targDocView) {
targDocView.props.focus(doc, {
willPan: true,
willPanZoom: BoolCast(sourceDoc.followLinkZoom, false),
afterFocus: (didFocus: boolean) => {
finished?.();
res(ViewAdjustment.resetView);
return new Promise<ViewAdjustment>(res2 => res2(ViewAdjustment.doNothing));
},
});
} else {
finished?.();
res(where !== OpenWhere.inPlace || BoolCast(sourceDoc.followLinkZoom) ? ViewAdjustment.resetView : ViewAdjustment.doNothing); // for 'inPlace' resetting the initial focus&zoom would negate the zoom into the target
}
}, 100);
});
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, { willPan: true, willPanZoom: BoolCast(sourceDoc.followLinkZoom, true), zoomScale: 1, afterFocus: createTabForTarget });
}
};
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,
createViewFunc,
docViewProps.ContainingCollectionDoc,
action(() => {
batch.end();
Doc.AddUnlightWatcher(action(() => (DocumentDecorations.Instance.overrideBounds = false)));
}),
altKey ? true : undefined
);
};
public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, createViewFunc: CreateViewFunc, 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).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 options: DocFocusOptions = {
playAudio: BoolCast(sourceDoc.followLinkAudio),
toggleTarget: BoolCast(sourceDoc.followLinkToggle),
willPan: true,
willPanZoom: BoolCast(LinkManager.getOppositeAnchor(linkDoc, target)?.followLinkZoom, false),
zoomTime: NumCast(LinkManager.getOppositeAnchor(linkDoc, target)?.linkTransitionTime, 500),
zoomScale: Cast(sourceDoc.linkZoomScale, 'number', null),
effect: sourceDoc,
originatingDoc: sourceDoc,
};
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, options, (doc, finished) => createViewFunc(doc, StrCast(linkDoc.followLinkLocation, 'inPlace'), finished), targetContexts, allFinished);
}
} else {
allFinished();
}
});
}
}
|