@readium/navigator 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +28 -0
  2. package/README.MD +11 -0
  3. package/dist/assets/AccessibleDfA.otf +0 -0
  4. package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
  5. package/dist/index.js +6263 -0
  6. package/dist/index.umd.cjs +107 -0
  7. package/package.json +65 -0
  8. package/src/Navigator.ts +66 -0
  9. package/src/audio/engine/AudioEngine.ts +136 -0
  10. package/src/audio/engine/WebAudioEngine.ts +286 -0
  11. package/src/audio/engine/index.ts +2 -0
  12. package/src/audio/index.ts +1 -0
  13. package/src/epub/EpubNavigator.ts +507 -0
  14. package/src/epub/frame/FrameBlobBuilder.ts +211 -0
  15. package/src/epub/frame/FrameComms.ts +142 -0
  16. package/src/epub/frame/FrameManager.ts +134 -0
  17. package/src/epub/frame/FramePoolManager.ts +179 -0
  18. package/src/epub/frame/index.ts +3 -0
  19. package/src/epub/fxl/FXLCoordinator.ts +152 -0
  20. package/src/epub/fxl/FXLFrameManager.ts +286 -0
  21. package/src/epub/fxl/FXLFramePoolManager.ts +632 -0
  22. package/src/epub/fxl/FXLPeripherals.ts +587 -0
  23. package/src/epub/fxl/FXLPeripheralsDebug.ts +46 -0
  24. package/src/epub/fxl/FXLSpreader.ts +95 -0
  25. package/src/epub/fxl/index.ts +5 -0
  26. package/src/epub/index.ts +3 -0
  27. package/src/helpers/sML.ts +120 -0
  28. package/src/index.ts +3 -0
  29. package/types/src/Navigator.d.ts +41 -0
  30. package/types/src/audio/engine/AudioEngine.d.ts +114 -0
  31. package/types/src/audio/engine/WebAudioEngine.d.ts +107 -0
  32. package/types/src/audio/engine/index.d.ts +2 -0
  33. package/types/src/audio/index.d.ts +1 -0
  34. package/types/src/epub/EpubNavigator.d.ts +66 -0
  35. package/types/src/epub/frame/FrameBlobBuilder.d.ts +13 -0
  36. package/types/src/epub/frame/FrameComms.d.ts +26 -0
  37. package/types/src/epub/frame/FrameManager.d.ts +21 -0
  38. package/types/src/epub/frame/FramePoolManager.d.ts +17 -0
  39. package/types/src/epub/frame/index.d.ts +3 -0
  40. package/types/src/epub/fxl/FXLCoordinator.d.ts +37 -0
  41. package/types/src/epub/fxl/FXLFrameManager.d.ts +41 -0
  42. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +93 -0
  43. package/types/src/epub/fxl/FXLPeripherals.d.ts +97 -0
  44. package/types/src/epub/fxl/FXLPeripheralsDebug.d.ts +13 -0
  45. package/types/src/epub/fxl/FXLSpreader.d.ts +12 -0
  46. package/types/src/epub/fxl/index.d.ts +5 -0
  47. package/types/src/epub/index.d.ts +3 -0
  48. package/types/src/helpers/sML.d.ts +51 -0
  49. package/types/src/index.d.ts +3 -0
@@ -0,0 +1,632 @@
1
+ import { ModuleName } from "@readium/navigator-html-injectables";
2
+ import { Locator, Publication, ReadingProgression, Orientation, Page, Link, Spread } from "@readium/shared";
3
+ import { FrameCommsListener } from "../frame";
4
+ import FrameBlobBuider from "../frame/FrameBlobBuilder";
5
+ import { FXLFrameManager } from "./FXLFrameManager";
6
+ import { FXLPeripherals } from "./FXLPeripherals";
7
+ import { FXLSpreader } from "./FXLSpreader";
8
+
9
+ const UPPER_BOUNDARY = 8;
10
+ const LOWER_BOUNDARY = 5;
11
+
12
+ const OFFSCREEN_LOAD_DELAY = 300;
13
+ const OFFSCREEN_LOAD_TIMEOUT = 15000;
14
+ const RESIZE_UPDATE_TIMEOUT = 250;
15
+ const SLIDE_FAST = 150;
16
+ const SLIDE_SLOW = 500;
17
+
18
+ export class FXLFramePoolManager {
19
+ private readonly container: HTMLElement;
20
+ private readonly positions: Locator[];
21
+ private readonly pool: Map<string, FXLFrameManager> = new Map();
22
+ private readonly blobs: Map<string, string> = new Map();
23
+ private readonly inprogress: Map<string, Promise<void>> = new Map();
24
+ private readonly delayedShow: Map<string, Promise<void>> = new Map();
25
+ private readonly delayedTimeout: Map<string, number> = new Map();
26
+ private currentBaseURL: string | undefined;
27
+ private previousFrames: FXLFrameManager[] = [];
28
+
29
+ // NEW
30
+ private readonly bookElement: HTMLDivElement;
31
+ public readonly spineElement: HTMLDivElement;
32
+ private readonly pub: Publication;
33
+ public width: number = 0;
34
+ public height: number = 0;
35
+ private transform: string = "";
36
+ public currentSlide: number = 0;
37
+ private spreader: FXLSpreader;
38
+ private spread = true; // TODO
39
+ private readonly spreadPresentation: Spread;
40
+ private orientationInternal = -1; // Portrait = 1, Landscape = 0, Unknown = -1
41
+ private containerHeightCached: number;
42
+ private readonly resizeBoundHandler: EventListenerOrEventListenerObject;
43
+ private resizeTimeout: number | undefined;
44
+ // private readonly pages: FXLFrameManager[] = [];
45
+ public readonly peripherals: FXLPeripherals;
46
+
47
+ constructor(container: HTMLElement, positions: Locator[], pub: Publication) {
48
+ this.container = container;
49
+ this.positions = positions;
50
+ this.pub = pub;
51
+ this.spreadPresentation = pub.metadata.getPresentation()?.spread || Spread.auto;
52
+
53
+ if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
54
+ // TODO support TTB and BTT
55
+ throw Error("Unsupported reading progression for EPUB");
56
+
57
+ // NEW
58
+ this.spreader = new FXLSpreader(this.pub);
59
+ this.containerHeightCached = container.clientHeight;
60
+ this.resizeBoundHandler = this.nativeResizeHandler.bind(this);
61
+
62
+ this.ownerWindow.addEventListener("resize", this.resizeBoundHandler);
63
+ this.ownerWindow.addEventListener("orientationchange", this.resizeBoundHandler);
64
+
65
+ this.bookElement = document.createElement("div");
66
+ this.bookElement.ariaLabel = "Book";
67
+ this.bookElement.tabIndex = -1;
68
+ this.updateBookStyle(true);
69
+
70
+ this.spineElement = document.createElement("div");
71
+ this.spineElement.ariaLabel = "Spine";
72
+
73
+ this.bookElement.appendChild(this.spineElement);
74
+ this.container.appendChild(this.bookElement);
75
+ this.updateSpineStyle(true);
76
+
77
+ this.peripherals = new FXLPeripherals(this);
78
+
79
+ this.pub.readingOrder.items.forEach((link) => {
80
+ // Create <iframe>
81
+ const fm = new FXLFrameManager(this.peripherals, this.pub.metadata.effectiveReadingProgression, link.href);
82
+ this.spineElement.appendChild(fm.element);
83
+
84
+ // this.pages.push(fm);
85
+ this.pool.set(link.href, fm);
86
+ fm.width = 100 / this.length * (link.properties?.getOrientation() === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
87
+ fm.height = this.height;
88
+ });
89
+ }
90
+
91
+ private _listener!: FrameCommsListener;
92
+ public set listener(listener: FrameCommsListener) {
93
+ this._listener = listener;
94
+ }
95
+ public get listener() {
96
+ return this._listener;
97
+ }
98
+
99
+ public get doNotDisturb() {
100
+ // TODO other situations
101
+ return this.peripherals.pan.touchID > 0;
102
+ }
103
+
104
+ private nativeResizeHandler(_: Event) {
105
+ this.resizeHandler(true);
106
+ }
107
+
108
+ /**
109
+ * When window resizes, resize slider components as well
110
+ */
111
+ resizeHandler(slide = true, fast = true) {
112
+ // relcalculate currentSlide
113
+ // prevent hiding items when browser width increases
114
+
115
+ if (this.currentSlide + this.perPage > this.length) {
116
+ this.currentSlide = this.length <= this.perPage ? 0 : this.length - 1;
117
+ }
118
+
119
+ this.containerHeightCached = this.container.clientHeight;
120
+
121
+ this.orientationInternal = -1;
122
+
123
+ this.updateSpineStyle(true);
124
+ if(slide/* && !sML.Mobile*/) {
125
+ this.currentSlide = this.reAlign();
126
+ this.slideToCurrent(!fast, fast);
127
+ }
128
+
129
+ clearTimeout(this.resizeTimeout);
130
+ this.resizeTimeout = window.setTimeout(() => {
131
+ // TODO optimize this expensive set of loops and operations
132
+ this.pool.forEach((frm, linkHref) => {
133
+ let i = this.pub.readingOrder.items.findIndex(l => l.href === linkHref);
134
+ const link = this.pub.readingOrder.items[i];
135
+ frm.width = 100 / this.length * (link.properties?.getOrientation() === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
136
+ frm.height = this.height;
137
+ if(!frm.loaded) return;
138
+ const spread = this.spreader.findByLink(link)!;
139
+ frm.update(this.spreadPosition(spread, link));
140
+ });
141
+ }, RESIZE_UPDATE_TIMEOUT);
142
+ }
143
+
144
+ /**
145
+ * It is important that these values be cached to avoid spamming them on redraws, they are expensive.
146
+ */
147
+ private updateDimensions() {
148
+ this.width = this.bookElement.clientWidth;
149
+ this.height = this.bookElement.clientHeight;
150
+ // this.containerHeightCached = r.height;
151
+ }
152
+
153
+ public get rtl() {
154
+ return this.pub.metadata.effectiveReadingProgression === ReadingProgression.rtl;
155
+ }
156
+
157
+ private get single() {
158
+ return !this.spread || this.portrait;
159
+ }
160
+
161
+ public get perPage() {
162
+ return (this.spread && !this.portrait) ? 2 : 1;
163
+ }
164
+
165
+ get threshold(): number {
166
+ return 50;
167
+ }
168
+
169
+ get portrait(): boolean {
170
+ if(this.spreadPresentation === Spread.none) return true; // No spreads
171
+ if(this.orientationInternal === -1) {
172
+ this.orientationInternal = this.containerHeightCached > this.container.clientWidth ? 1 : 0;
173
+ }
174
+ return this.orientationInternal === 1;
175
+ }
176
+
177
+ public updateSpineStyle(animate: boolean, fast = true) {
178
+ let margin = "0";
179
+ this.updateDimensions();
180
+ if(this.perPage > 1 && true) // this.shift
181
+ margin = `${this.width / 2}px`;
182
+
183
+ const spineStyle = {
184
+ transition: animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out",
185
+ marginRight: this.rtl ? margin : "0",
186
+ marginLeft: this.rtl ? "0" : margin,
187
+ width: `${(this.width / this.perPage) * this.length}px`,
188
+ transform: this.transform,
189
+
190
+ // Static (should be moved to CSS)
191
+ contain: "content"
192
+ } as CSSStyleDeclaration;
193
+
194
+ Object.assign(this.spineElement.style, spineStyle);
195
+ }
196
+
197
+ public updateBookStyle(initial=false) {
198
+ if(initial) {
199
+ const bookStyle = {
200
+ overflow: "hidden",
201
+ direction: this.pub.metadata.effectiveReadingProgression,
202
+ cursor: "",
203
+ // Static (should be moved to CSS)
204
+ // minHeight: 100%
205
+ // maxHeight: "100%",
206
+ height: "100%",
207
+ width: "100%",
208
+ position: "relative",
209
+ outline: "none",
210
+ transition: this.peripherals?.dragState ? "none" : "transform .15s ease-in-out",
211
+ touchAction: "none",
212
+ } as CSSStyleDeclaration;
213
+ Object.assign(this.bookElement.style, bookStyle);
214
+ }
215
+ this.bookElement.style.transform = `scale(${this.peripherals?.scale || 1})` + (this.peripherals ? ` translate3d(${this.peripherals.pan.translateX}px, ${this.peripherals.pan.translateY}px, 0px)` : "");
216
+ }
217
+
218
+ /**
219
+ * Go to slide with particular index
220
+ * @param {number} index - Item index to slide to.
221
+ */
222
+ goTo(index: number) {
223
+ if (this.slength <= this.perPage)
224
+ return;
225
+ index = this.reAlign(index);
226
+ const beforeChange = this.currentSlide;
227
+ this.currentSlide = Math.min(Math.max(index, 0), this.length - 1);
228
+ if (beforeChange !== this.currentSlide) {
229
+ this.slideToCurrent(false);
230
+ // this.onChange();
231
+ }
232
+ }
233
+
234
+ onChange() {
235
+ this.peripherals.scale = 1;
236
+ this.updateBookStyle();
237
+ }
238
+
239
+ private get offset() {
240
+ return (this.rtl ? 1 : -1) * this.currentSlide * (this.width / this.perPage);
241
+ }
242
+
243
+ get length() {
244
+ if(this.single)
245
+ return this.slength;
246
+ const total = this.slength + this.nLandscape;
247
+ return (this.shift && (total % 2 === 0)) ? total + 1 : total;
248
+ }
249
+
250
+ get slength() {
251
+ return this.pub.readingOrder.items.length || 0;
252
+ }
253
+
254
+ get shift() {
255
+ return this.spreader.shift;
256
+ }
257
+
258
+ private get nLandscape() {
259
+ return this.spreader.nLandscape;
260
+ }
261
+
262
+ public setPerPage(perPage: number) {
263
+ if(perPage === 0) {
264
+ // TODO this mode is auto
265
+ this.spread = true;
266
+ } else if(perPage === 1) {
267
+ this.spread = false;
268
+ } else {
269
+ this.spread = true;
270
+ }
271
+ requestAnimationFrame(() => this.resizeHandler(true));
272
+ }
273
+
274
+ /**
275
+ * Moves sliders frame to position of currently active slide
276
+ */
277
+ slideToCurrent(enableTransition?: boolean, fast = true) {
278
+ this.updateDimensions();
279
+ if (enableTransition) {
280
+ // This one is tricky, I know but this is a perfect explanation:
281
+ // https://youtu.be/cCOL7MC4Pl0
282
+ requestAnimationFrame(() => {
283
+ requestAnimationFrame(() => {
284
+ const newTransform = `translate3d(${this.offset}px, 0, 0)`;
285
+ if(this.spineElement.style.transform === newTransform) return;
286
+ this.transform = newTransform;
287
+ this.updateSpineStyle(true, fast);
288
+ this.deselect();
289
+ });
290
+ });
291
+ } else {
292
+ const newTransform = `translate3d(${this.offset}px, 0, 0)`;
293
+ if(this.spineElement.style.transform === newTransform) return;
294
+ this.transform = newTransform;
295
+ this.updateSpineStyle(false);
296
+ this.deselect();
297
+ }
298
+ }
299
+
300
+ bounce(rtl = false) {
301
+ requestAnimationFrame(() => {
302
+ this.transform = `translate3d(${this.offset+(50 * (rtl ? 1 : -1))}px, 0, 0)`;
303
+ this.updateSpineStyle(true, true);
304
+ setTimeout(() => {
305
+ this.transform = `translate3d(${this.offset}px, 0, 0)`;
306
+ this.updateSpineStyle(true, true);
307
+ }, 100);
308
+ });
309
+ }
310
+
311
+
312
+ /**
313
+ * Go to next slide.
314
+ * @param {number} [howManySlides=1] - How many items to slide forward.
315
+ * @returns {boolean} Whether or not going to next was possible
316
+ */
317
+ next(howManySlides = 1): boolean {
318
+ // early return when there is nothing to slide
319
+ if (this.slength <= this.perPage) {
320
+ return false;
321
+ }
322
+
323
+ const beforeChange = this.currentSlide;
324
+
325
+ this.currentSlide = Math.min(this.currentSlide + howManySlides, this.length - 1);
326
+ if(this.perPage > 1 && this.currentSlide % 2)
327
+ this.currentSlide--;
328
+
329
+ if(this.currentSlide === beforeChange && this.currentSlide + 1 === this.length) {
330
+ // At end and trying to go further, means trigger "last page" callback
331
+ // this.onLastPage();
332
+ }
333
+
334
+ if (beforeChange !== this.currentSlide) {
335
+ this.slideToCurrent(true);
336
+ this.onChange();
337
+ return true;
338
+ } else {
339
+ this.bounce(this.rtl);
340
+ return false;
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Go to previous slide.
346
+ * @param {number} [howManySlides=1] - How many items to slide backward.
347
+ * @returns {boolean} Whether or not going to prev was possible
348
+ */
349
+ prev(howManySlides = 1): boolean {
350
+ // early return when there is nothing to slide
351
+ if (this.slength <= this.perPage) {
352
+ return false;
353
+ }
354
+
355
+ const beforeChange = this.currentSlide;
356
+
357
+ this.currentSlide = Math.max(this.currentSlide - howManySlides, 0);
358
+ if(this.perPage > 1 && this.currentSlide % 2)
359
+ this.currentSlide++;
360
+
361
+ if (beforeChange !== this.currentSlide) {
362
+ this.slideToCurrent(true);
363
+ this.onChange();
364
+ return true;
365
+ } else
366
+ this.bounce(!this.rtl);
367
+ return false;
368
+ }
369
+
370
+ get ownerWindow() {
371
+ return this.container.ownerDocument.defaultView || window;
372
+ }
373
+
374
+
375
+ // OLD
376
+
377
+
378
+ async destroy() {
379
+ this.ownerWindow.removeEventListener("resize", this.resizeBoundHandler);
380
+ this.ownerWindow.removeEventListener("orientationchange", this.resizeBoundHandler);
381
+
382
+ // Wait for all in-progress loads to complete
383
+ let iit = this.inprogress.values();
384
+ let inp = iit.next();
385
+ const inprogressPromises: Promise<void>[] = [];
386
+ while(inp.value) {
387
+ inprogressPromises.push(inp.value);
388
+ inp = iit.next();
389
+ }
390
+ if(inprogressPromises.length > 0) {
391
+ await Promise.allSettled(inprogressPromises);
392
+ }
393
+ this.inprogress.clear();
394
+
395
+ // Destroy all frames
396
+ let fit = this.pool.values();
397
+ let frm = fit.next();
398
+ while(frm.value) {
399
+ await (frm.value as FXLFrameManager).destroy();
400
+ frm = fit.next();
401
+ }
402
+ this.pool.clear();
403
+
404
+ // Revoke all blobs
405
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
406
+
407
+ // Empty container of elements
408
+ this.container.childNodes.forEach(v => {
409
+ if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
410
+ })
411
+ }
412
+
413
+ makeSpread(itemIndex: number) {
414
+ return this.perPage < 2 ? [this.pub.readingOrder.items[itemIndex]] : this.spreader.currentSpread(itemIndex, this.perPage);
415
+ }
416
+
417
+ reAlign(index: number = this.currentSlide) {
418
+ if (index % 2 && !this.single) // Prevent getting out of track
419
+ index++;
420
+ return index;
421
+ }
422
+
423
+ spreadPosition(spread: Link[], target: Link) {
424
+ return this.perPage < 2 ? Page.center : (spread.length < 2 ? Page.center : (
425
+ target.href === spread[0].href ? (this.rtl ? Page.right : Page.left) : (this.rtl ? Page.left : Page.right)
426
+ ));
427
+ }
428
+
429
+ async waitForItem(href: string) {
430
+ if(this.inprogress.has(href))
431
+ // If this same href is already being loaded, block until the other function
432
+ // call has finished executing so we don't end up e.g. loading the blob twice.
433
+ await this.inprogress.get(href);
434
+
435
+ if(this.delayedShow.has(href)) {
436
+ const timeoutVal = this.delayedTimeout.get(href)!;
437
+ if(timeoutVal > 0) {
438
+ // Delayed resource showing has not yet commenced, cancel it
439
+ clearTimeout(timeoutVal);
440
+ } else {
441
+ // Await a current delayed showing of the resource
442
+ await this.delayedShow.get(href);
443
+ }
444
+ this.delayedTimeout.set(href, 0);
445
+ this.delayedShow.delete(href);
446
+ }
447
+ }
448
+
449
+ async cancelShowing(href: string) {
450
+ if(this.delayedShow.has(href)) {
451
+ const timeoutVal = this.delayedTimeout.get(href)!;
452
+ if(timeoutVal > 0) {
453
+ // Delayed resource showing has not yet commenced, cancel it
454
+ clearTimeout(timeoutVal);
455
+ }
456
+ this.delayedShow.delete(href);
457
+ }
458
+ }
459
+
460
+ async update(pub: Publication, locator: Locator, modules: ModuleName[], _force=false) {
461
+ let i = this.pub.readingOrder.items.findIndex(l => l.href === locator.href);
462
+ if(i < 0) throw Error("Href not found in reading order");
463
+
464
+ if(this.currentSlide !== i) {
465
+ this.currentSlide = this.reAlign(i);
466
+ this.slideToCurrent(true);
467
+ }
468
+ const spread = this.makeSpread(this.currentSlide);
469
+ if(this.perPage > 1) i++;
470
+ for (const s of spread) {
471
+ await this.waitForItem(s.href);
472
+ }
473
+
474
+ // Create a new progress that doesn't resolve until complete
475
+ // loading of the resource and its dependencies has finished.
476
+ const progressPromise = new Promise<void>(async (resolve, reject) => {
477
+ const disposal: string[] = [];
478
+ const creation: string[] = [];
479
+
480
+ this.positions.forEach((l, j) => {
481
+ if(j > (i + UPPER_BOUNDARY) || j < (i - UPPER_BOUNDARY)) {
482
+ if(!disposal.includes(l.href)) disposal.push(l.href);
483
+ }
484
+ if(j < (i + LOWER_BOUNDARY) && j > (i - LOWER_BOUNDARY)) {
485
+ if(!creation.includes(l.href)) creation.push(l.href);
486
+ }
487
+ });
488
+ disposal.forEach(async href => {
489
+ if(creation.includes(href)) return;
490
+ if(!this.pool.has(href)) return;
491
+ this.cancelShowing(href);
492
+ await this.pool.get(href)?.unload();
493
+ // this.pool.delete(href);
494
+ });
495
+
496
+ // Check if base URL of publication has changed
497
+ if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
498
+ // Revoke all blobs
499
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
500
+ this.blobs.clear();
501
+ }
502
+ this.currentBaseURL = pub.baseURL;
503
+
504
+ const creator = async (href: string) => {
505
+ const index = pub.readingOrder.findIndexWithHref(href);
506
+ const itm = pub.readingOrder.items[index];
507
+ if(!itm) return; // TODO throw?
508
+ if(!this.blobs.has(href)) {
509
+ const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm);
510
+ const blobURL = await blobBuilder.build(true);
511
+ this.blobs.set(href, blobURL);
512
+ }
513
+
514
+ // Show future offscreen frame in advance after a delay
515
+ // The added delay prevents this expensive operation from
516
+ // occuring during the sliding animation, to reduce lag
517
+ if(!this.delayedShow.has(href))
518
+ this.delayedShow.set(href, new Promise((resolve, reject) => {
519
+ let done = false;
520
+ const t = window.setTimeout(async () => {
521
+ this.delayedTimeout.set(href, 0);
522
+ const spread = this.makeSpread(this.reAlign(index));
523
+ const page = this.spreadPosition(spread, itm);
524
+ const fm = this.pool.get(href)!;
525
+ await fm.load(modules, this.blobs.get(href)!);
526
+ if(!this.peripherals.isScaled) // When scaled, positioning is screwed up, so wait to show
527
+ await fm.show(page); // Show/activate new frame
528
+ this.delayedShow.delete(href);
529
+ done = true;
530
+ resolve();
531
+ }, OFFSCREEN_LOAD_DELAY);
532
+ setTimeout(() => {
533
+ if(!done && this.delayedShow.has(href)) reject(`Offscreen load timeout: ${href}`);
534
+ }, OFFSCREEN_LOAD_TIMEOUT);
535
+ this.delayedTimeout.set(href, t);
536
+ }));
537
+ }
538
+ try {
539
+ await Promise.all(creation.map(href => creator(href)));
540
+ } catch (error) {
541
+ reject(error);
542
+ }
543
+
544
+ // Update current frame(s)
545
+ const newFrames: FXLFrameManager[] = [];
546
+ for (const s of spread) {
547
+ const newFrame = this.pool.get(s.href)!;
548
+ const source = this.blobs.get(s.href);
549
+ if(!source) continue; // This can get destroyed
550
+
551
+ this.cancelShowing(s.href);
552
+ await newFrame.load(modules, source); // In order to ensure modules match the latest configuration
553
+ await newFrame.show(this.spreadPosition(spread, s)); // Show/activate new frame
554
+ this.previousFrames.push(newFrame);
555
+ await newFrame.activate();
556
+ newFrames.push(newFrame);
557
+ }
558
+
559
+ // Unfocus previous frame(s)
560
+ while(this.previousFrames.length > 0) {
561
+ const fm = this.previousFrames.shift();
562
+ if(fm && !newFrames.includes(fm))
563
+ await fm.unfocus();
564
+ }
565
+ this.previousFrames = newFrames;
566
+
567
+ resolve();
568
+ });
569
+
570
+ for (const s of spread) {
571
+ this.inprogress.set(s.href, progressPromise); // Add the job to the in progress map
572
+ }
573
+ await progressPromise; // Wait on the job to finish...
574
+ for (const s of spread) {
575
+ this.inprogress.delete(s.href); // Delete it from the in progress map!
576
+ }
577
+ }
578
+
579
+ get currentFrames(): (FXLFrameManager | undefined)[] {
580
+ if(this.perPage < 2) {
581
+ const link = this.pub.readingOrder.items[this.currentSlide];
582
+ return [this.pool.get(link.href)];
583
+ }
584
+ const spread = this.spreader.currentSpread(this.currentSlide, this.perPage);
585
+ return spread.map(s => this.pool.get(s.href));
586
+ }
587
+
588
+ get currentBounds(): DOMRect {
589
+ const ret = {
590
+ x: 0,
591
+ y: 0,
592
+ width: 0,
593
+ height: 0,
594
+ top: 0,
595
+ right: 0,
596
+ bottom: 0,
597
+ left: 0,
598
+ toJSON() {
599
+ return this;
600
+ },
601
+ };
602
+ this.currentFrames.forEach(f => {
603
+ if(!f) return;
604
+ const b = f.realSize;
605
+ ret.x = Math.min(ret.x, b.x);
606
+ ret.y = Math.min(ret.y, b.y);
607
+ ret.width += b.width; // TODO different in vertical
608
+ ret.height = Math.max(ret.height, b.height);
609
+ ret.top = Math.min(ret.top, b.top);
610
+ ret.right = Math.min(ret.right, b.right);
611
+ ret.bottom = Math.min(ret.bottom, b.bottom);
612
+ ret.left = Math.min(ret.left, b.left);
613
+ });
614
+ return ret as DOMRect;
615
+ }
616
+
617
+ get currentNumbers(): number[] {
618
+ if(this.perPage < 2) {
619
+ const link = this.pub.readingOrder.items[this.currentSlide];
620
+ return [link.properties?.otherProperties["number"]];
621
+ }
622
+ const spread = this.spreader.currentSpread(this.currentSlide, this.perPage);
623
+ return spread.length > 1 ? [
624
+ spread[0].properties?.otherProperties["number"] as number,
625
+ spread[spread.length-1].properties?.otherProperties["number"] as number
626
+ ] : [spread[0].properties?.otherProperties["number"] as number];
627
+ }
628
+
629
+ deselect() {
630
+ this.currentFrames?.forEach(f => f?.deselect());
631
+ }
632
+ }