-
Notifications
You must be signed in to change notification settings - Fork 3
/
TransitionalViewManager.ts
307 lines (251 loc) · 12.7 KB
/
TransitionalViewManager.ts
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
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
import { type AnnotationMaskCoordinates } from "../pdf/PDFPageRenderer";
import { Messenger } from "../Messenger";
import { TransitionalPopup } from "./TransitionalPopup";
import { CoreToWebviewMessageType, UpdateTransitionalContentMessage, UpdateTransitionalMetadataMessage } from "../../shared/messenger/messages";
import { TransitionalMetadata } from "../../shared/transitionals/types";
import { TaskQueuer } from "../../shared/tasks/TaskQueuer";
import { PDFManager } from "../pdf/PDFManager";
import { TransitionalViewContext } from "./TransitionalViewContext";
import { TransitionalViewProvider } from "./TransitionalViewProvider";
import { TransitionalView } from "./TransitionalView";
import { TRANSITIONAL_VIEW_PROVIDERS } from "../../transitionals/view-providers";
export interface TransitionalDisplayRequest {
codeMappingId: number;
annotationMaskCoordinates: AnnotationMaskCoordinates;
pdfPageDetail: {
pageNumber: number;
width: number;
height: number;
scale: number;
}
}
interface TransitionalData {
contentNode?: HTMLElement;
metadata?: TransitionalMetadata;
}
export interface TransitionalAvailabilityData {
codeMappingId: number;
isAvailable: boolean;
}
export class TransitionalViewManager {
static readonly REQUEST_TRANSITIONAL_DISPLAY_EVENT = "request-transitional-display";
static readonly TRANSITIONAL_AVAILABILITY_CHANGE_EVENT = "transitional-availability-change";
static readonly TRANSITIONALS_ARE_UNAVAILABLE_BODY_CLASS = "transitionals-unavailable";
static readonly TRANSITIONALS_ARE_DISABLED_BODY_CLASS = "transitionals-disabled";
private messenger: Messenger;
private transitionalNamesToViewProviders: Map<string, TransitionalViewProvider>;
private codeMappingIdsToTransitionalData: Map<number, TransitionalData>;
private transitionalDataUpdateTaskQueuer: TaskQueuer;
private currentlyDisplayedTransitionalPopup: TransitionalPopup | null;
constructor(messenger: Messenger) {
this.messenger = messenger;
this.transitionalNamesToViewProviders = new Map(
TRANSITIONAL_VIEW_PROVIDERS
.map(provider => [provider.transitionalName, provider])
);
this.codeMappingIdsToTransitionalData = new Map();
this.transitionalDataUpdateTaskQueuer = new TaskQueuer();
this.currentlyDisplayedTransitionalPopup = null;
this.startHandlingTransitionalDisplayRequests();
this.startHandlingPdfEvents();
this.startHandlingWebviewMessages();
}
get currentlyDisplayedTransitionalView(): TransitionalView | null {
return this.currentlyDisplayedTransitionalPopup?.transitionalView ?? null;
}
get hasCurrentlyDisplayedTransitionalView(): boolean {
return this.currentlyDisplayedTransitionalPopup !== null;
}
setTransitionalsGloballyEnabled(enable: boolean): void {
// If transitionals are globally disabled, tag the body element with a dedicated class
// and hide the currently displayed transitional (if any)
document.body.classList.toggle(
TransitionalViewManager.TRANSITIONALS_ARE_DISABLED_BODY_CLASS,
!enable
);
// If transitionals have possibly become globally disabled, hide the currently displayed transitional (if any)
if (!enable) {
this.hideCurrentlyDisplayedTransitional();
}
// Signal that the availability of the transitional may have changed
this.emitTransitionalAvailabilityChangeEvent();
}
private ensureTransitionalDataExistsForCodeMappingId(codeMappingId: number): void {
if (!this.codeMappingIdsToTransitionalData.has(codeMappingId)) {
this.codeMappingIdsToTransitionalData.set(codeMappingId, {});
}
}
private getTransitionalDataWithCodeMappingId(codeMappingId: number): TransitionalData | null {
return this.codeMappingIdsToTransitionalData.get(codeMappingId) ?? null;
}
private processTransitionalDisplayRequest(request: TransitionalDisplayRequest): void {
// Get the transitional data for the requeted code mapping ID and ensure that
// 1. both the content and the metadata exist in this manager;
// 1. the transitional is currently available;
// 2. there is a view factory for this type of transitional.
const data = this.getTransitionalDataWithCodeMappingId(request.codeMappingId);
if (!data || !data.contentNode || !data.metadata) {
console.warn(`Transitional data (content or metadata) is missing for code mapping ID "${request.codeMappingId}".`);
return;
}
if (!data.metadata.available) {
console.warn(`Transitional with code mapping ID "${request.codeMappingId}" is not available: it cannot be displayed.`);
return;
}
const transitionalName = data.metadata.name;
if (!this.transitionalNamesToViewProviders.has(transitionalName)) {
console.warn(`There is no view factory for transitionals named "${transitionalName}": it cannot be displayed.`);
return;
}
// Clone the content node and display a new view in a transitional popup
const clonedContentNode = TransitionalViewManager.cloneTransitionalContentNode(data.contentNode);
const factory = this.transitionalNamesToViewProviders.get(transitionalName)!;
const view = factory.createView(clonedContentNode, data.metadata, new TransitionalViewContext(
request.codeMappingId,
this.messenger,
request.annotationMaskCoordinates,
request.pdfPageDetail
));
const popup = new TransitionalPopup(view, () => {
this.currentlyDisplayedTransitionalPopup = null;
});
this.currentlyDisplayedTransitionalPopup = popup;
}
private hideCurrentlyDisplayedTransitional(): void {
if (!this.hasCurrentlyDisplayedTransitionalView) {
return;
}
this.currentlyDisplayedTransitionalPopup!.close();
}
private handleTransitionalDisplayRequest(request: TransitionalDisplayRequest): void {
// Ensure no transitional is displayed
this.hideCurrentlyDisplayedTransitional();
// Process the display request
this.processTransitionalDisplayRequest(request);
}
private updateCurrentlyDisplayedTransitionalContent(): void {
if (!this.hasCurrentlyDisplayedTransitionalView) {
return;
}
const transitionalPopup = this.currentlyDisplayedTransitionalPopup!;
const transitionalView = this.currentlyDisplayedTransitionalView!;
const codeMappingId = transitionalView.codeMappingId;
const data = this.getTransitionalDataWithCodeMappingId(codeMappingId);
if (!data || !data.contentNode) {
console.warn(`Transitional content is missing for the currently displayed transitional (with code mapping ID "${codeMappingId}"): the view cannot be updated.`);
return;
}
const clonedContentNode = TransitionalViewManager.cloneTransitionalContentNode(data.contentNode);
transitionalView.updateContentWith(clonedContentNode);
transitionalPopup.onAfterVisualationContentUpdate();
}
private updateCurrentlyDisplayedTransitionalMetadata(): void {
if (!this.hasCurrentlyDisplayedTransitionalView) {
return;
}
const transitionalPopup = this.currentlyDisplayedTransitionalPopup!;
const transitionalView = this.currentlyDisplayedTransitionalView!;
const codeMappingId = transitionalView.codeMappingId;
const data = this.getTransitionalDataWithCodeMappingId(codeMappingId);
if (!data || !data.metadata) {
console.warn(`Transitional metadata is missing for the currently displayed transitional (with code mapping ID "${codeMappingId}"): the view cannot be updated.`);
return;
}
transitionalView.updateMetadataWith(data.metadata);
transitionalPopup.onAfterTransitionalMetadataUpdate();
}
private updateTransitionalContent(codeMappingId: number, newContentAsHtml: string): void {
// Get the current data for the given code mapping ID, or create it if needed
this.ensureTransitionalDataExistsForCodeMappingId(codeMappingId);
const data = this.getTransitionalDataWithCodeMappingId(codeMappingId)!;
// Create an HTML element from the given content string and update the data with it
const temporaryContainerNode = document.createElement("template");
temporaryContainerNode.innerHTML = newContentAsHtml;
data.contentNode = temporaryContainerNode.content.firstElementChild as HTMLElement;
// Possibly update the currently displayed transitional (if there is one)
if (this.currentlyDisplayedTransitionalView?.codeMappingId === codeMappingId) {
this.updateCurrentlyDisplayedTransitionalContent();
}
}
private updateTransitionalMetadata(codeMappingId: number, newMetadata: TransitionalMetadata): void {
// Get the current data for the given code mapping ID, or create it if needed
this.ensureTransitionalDataExistsForCodeMappingId(codeMappingId);
const data = this.getTransitionalDataWithCodeMappingId(codeMappingId)!;
// Update the data with the new metadata
data.metadata = newMetadata;
// Possibly update the currently displayed transitional (if there is one)
if (this.currentlyDisplayedTransitionalView?.codeMappingId === codeMappingId) {
this.updateCurrentlyDisplayedTransitionalMetadata();
}
// Signal that the availability of the transitional may have changed
this.emitTransitionalAvailabilityChangeEvent([codeMappingId]);
}
private emitTransitionalAvailabilityChangeEvent(onlyCodeMappingToInclude?: number[]): void {
let dataOfAllIncludedTransitionals = [...this.codeMappingIdsToTransitionalData.entries()];
if (onlyCodeMappingToInclude) {
dataOfAllIncludedTransitionals = dataOfAllIncludedTransitionals
.filter(([codeMappingId, data]) => onlyCodeMappingToInclude.includes(codeMappingId));
}
window.dispatchEvent(new CustomEvent<TransitionalAvailabilityData[]>(
TransitionalViewManager.TRANSITIONAL_AVAILABILITY_CHANGE_EVENT,
{
detail: dataOfAllIncludedTransitionals.map(([codeMappingId, data]) => {
return {
codeMappingId: codeMappingId,
isAvailable: (data.metadata !== undefined && data.metadata.available)
};
})
}
));
}
private startHandlingTransitionalDisplayRequests(): void {
window.addEventListener(
TransitionalViewManager.REQUEST_TRANSITIONAL_DISPLAY_EVENT,
(event: Event) => {
const customEvent = event as CustomEvent<TransitionalDisplayRequest>;
this.handleTransitionalDisplayRequest(customEvent.detail);
}
);
}
private startHandlingPdfEvents(): void {
window.addEventListener(
PDFManager.PDF_COMPILATION_STARTED_EVENT,
(event: Event) => {
this.hideCurrentlyDisplayedTransitional();
}
);
window.addEventListener(
PDFManager.PDF_WILL_RESIZE_EVENT,
(event: Event) => {
this.currentlyDisplayedTransitionalPopup?.onBeforePdfResize();
}
);
window.addEventListener(
PDFManager.PDF_DID_RESIZE_EVENT,
(event: Event) => {
this.currentlyDisplayedTransitionalPopup?.onAfterPdfResize();
}
);
}
private startHandlingWebviewMessages(): void {
this.messenger.setHandlerFor(
CoreToWebviewMessageType.UpdateTransitionalContent,
async (message: UpdateTransitionalContentMessage) => {
this.transitionalDataUpdateTaskQueuer.add(async () => {
this.updateTransitionalContent(message.codeMappingId, message.contentAsHtml);
});
}
);
this.messenger.setHandlerFor(
CoreToWebviewMessageType.UpdateTransitionalMetadata,
async (message: UpdateTransitionalMetadataMessage) => {
this.transitionalDataUpdateTaskQueuer.add(async () => {
this.updateTransitionalMetadata(message.codeMappingId, message.metadata);
});
}
);
}
private static cloneTransitionalContentNode(node: HTMLElement): HTMLElement {
return node.cloneNode(true) as HTMLElement;
}
}