@readium/navigator 2.10.3 → 2.11.1

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 (94) hide show
  1. package/dist/audio/AudioNavigator.js +1 -1
  2. package/dist/audio/AudioPoolManager.js +1 -1
  3. package/dist/audio/engine/WebAudioEngine.js +1 -1
  4. package/dist/divina/DivinaNavigator.js +1 -0
  5. package/dist/divina/DivinaPageManager.js +1 -0
  6. package/dist/divina/DivinaPagedPresenter.js +1 -0
  7. package/dist/divina/DivinaPeripherals.js +1 -0
  8. package/dist/divina/DivinaScrolledPresenter.js +1 -0
  9. package/dist/divina/DivinaSpreader.js +1 -0
  10. package/dist/divina/DivinaVariantSelector.js +1 -0
  11. package/dist/divina/preferences/DivinaDefaults.js +1 -0
  12. package/dist/divina/preferences/DivinaPreferences.js +1 -0
  13. package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
  14. package/dist/divina/preferences/DivinaSettings.js +1 -0
  15. package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
  16. package/dist/epub/EpubNavigator.js +1 -1
  17. package/dist/epub/frame/FrameBlobBuilder.js +3 -1
  18. package/dist/epub/frame/FrameComms.js +1 -1
  19. package/dist/epub/frame/FrameManager.js +1 -1
  20. package/dist/epub/frame/FramePoolManager.js +1 -1
  21. package/dist/epub/fxl/FXLFrameManager.js +1 -1
  22. package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
  23. package/dist/epub/fxl/FXLPeripherals.js +2 -2
  24. package/dist/helpers/serialize.js +1 -0
  25. package/dist/index.js +1 -1
  26. package/dist/peripherals/KeyboardConditionBridge.js +1 -1
  27. package/dist/peripherals/KeyboardPeripherals.js +1 -1
  28. package/dist/webpub/WebPubFrameManager.js +1 -1
  29. package/dist/webpub/WebPubFramePoolManager.js +1 -1
  30. package/dist/webpub/WebPubNavigator.js +1 -1
  31. package/package.json +5 -5
  32. package/src/audio/AudioNavigator.ts +107 -5
  33. package/src/audio/AudioPoolManager.ts +14 -8
  34. package/src/audio/engine/AudioEngine.ts +34 -1
  35. package/src/audio/engine/WebAudioEngine.ts +48 -3
  36. package/src/divina/DivinaNavigator.ts +704 -0
  37. package/src/divina/DivinaPageManager.ts +322 -0
  38. package/src/divina/DivinaPagedPresenter.ts +508 -0
  39. package/src/divina/DivinaPeripherals.ts +630 -0
  40. package/src/divina/DivinaScrolledPresenter.ts +516 -0
  41. package/src/divina/DivinaSpreader.ts +202 -0
  42. package/src/divina/DivinaVariantSelector.ts +77 -0
  43. package/src/divina/index.ts +8 -0
  44. package/src/divina/preferences/DivinaDefaults.ts +37 -0
  45. package/src/divina/preferences/DivinaPreferences.ts +77 -0
  46. package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
  47. package/src/divina/preferences/DivinaSettings.ts +38 -0
  48. package/src/divina/preferences/index.ts +4 -0
  49. package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
  50. package/src/epub/EpubNavigator.ts +73 -7
  51. package/src/epub/frame/FrameBlobBuilder.ts +144 -39
  52. package/src/epub/frame/FrameComms.ts +42 -7
  53. package/src/epub/frame/FrameManager.ts +27 -16
  54. package/src/epub/frame/FramePoolManager.ts +106 -48
  55. package/src/epub/fxl/FXLFrameManager.ts +99 -34
  56. package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
  57. package/src/epub/fxl/FXLPeripherals.ts +22 -1
  58. package/src/helpers/serialize.ts +9 -0
  59. package/src/index.ts +1 -0
  60. package/src/peripherals/KeyboardConditionBridge.ts +15 -5
  61. package/src/peripherals/KeyboardPeripherals.ts +13 -7
  62. package/src/webpub/WebPubFrameManager.ts +27 -16
  63. package/src/webpub/WebPubFramePoolManager.ts +5 -2
  64. package/src/webpub/WebPubNavigator.ts +44 -0
  65. package/types/src/audio/AudioNavigator.d.ts +29 -0
  66. package/types/src/audio/engine/AudioEngine.d.ts +27 -1
  67. package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
  68. package/types/src/divina/DivinaNavigator.d.ts +119 -0
  69. package/types/src/divina/DivinaPageManager.d.ts +66 -0
  70. package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
  71. package/types/src/divina/DivinaPeripherals.d.ts +121 -0
  72. package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
  73. package/types/src/divina/DivinaSpreader.d.ts +65 -0
  74. package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
  75. package/types/src/divina/index.d.ts +8 -0
  76. package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
  77. package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
  78. package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
  79. package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
  80. package/types/src/divina/preferences/index.d.ts +4 -0
  81. package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
  82. package/types/src/epub/EpubNavigator.d.ts +7 -1
  83. package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
  84. package/types/src/epub/frame/FrameComms.d.ts +8 -1
  85. package/types/src/epub/frame/FrameManager.d.ts +3 -0
  86. package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
  87. package/types/src/epub/fxl/FXLFrameManager.d.ts +7 -0
  88. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
  89. package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
  90. package/types/src/helpers/serialize.d.ts +1 -0
  91. package/types/src/index.d.ts +1 -0
  92. package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
  93. package/types/src/webpub/WebPubFrameManager.d.ts +3 -0
  94. package/types/src/webpub/WebPubNavigator.d.ts +2 -0
@@ -1,9 +1,9 @@
1
1
  import { ModuleName } from "@readium/navigator-html-injectables";
2
- import { Locator, Publication, ReadingProgression, Page, Link } from "@readium/shared";
2
+ import { Locator, Publication, ReadingProgression, Page, Link, Layout } from "@readium/shared";
3
3
  import { FrameCommsListener } from "../frame/index.ts";
4
- import FrameBlobBuider from "../frame/FrameBlobBuilder.ts";
4
+ import FrameBlobBuilder, { UpgradeInsecureRequests } from "../frame/FrameBlobBuilder.ts";
5
5
  import { FXLFrameManager } from "./FXLFrameManager.ts";
6
- import { FXLPeripherals } from "./FXLPeripherals.ts";
6
+ import { FXLPeripherals, isTypedOMSupported } from "./FXLPeripherals.ts";
7
7
  import { FXLSpreader, Orientation, Spread } from "./FXLSpreader.ts";
8
8
  import { VisualNavigatorViewport, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
9
9
  import { Injector } from "../../injection/Injector.ts";
@@ -21,7 +21,7 @@ export class FXLFramePoolManager {
21
21
  private readonly container: HTMLElement;
22
22
  private readonly positions: Locator[];
23
23
  private readonly pool: Map<string, FXLFrameManager> = new Map();
24
- private readonly blobs: Map<string, string> = new Map();
24
+ private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
25
25
  private readonly inprogress: Map<string, Promise<void>> = new Map();
26
26
  private readonly delayedShow: Map<string, Promise<void>> = new Map();
27
27
  private readonly delayedTimeout: Map<string, number> = new Map();
@@ -31,6 +31,7 @@ export class FXLFramePoolManager {
31
31
  private readonly contentProtectionConfig: IContentProtectionConfig;
32
32
  private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
33
33
  private resizeWatchSelectors: string[];
34
+ private readonly upgradeInsecureRequests: UpgradeInsecureRequests;
34
35
 
35
36
  // NEW
36
37
  private readonly bookElement: HTMLDivElement;
@@ -38,7 +39,7 @@ export class FXLFramePoolManager {
38
39
  private readonly pub: Publication;
39
40
  public width: number = 0;
40
41
  public height: number = 0;
41
- private transform: string = "";
42
+ private transform: number | undefined;
42
43
  public currentSlide: number = 0;
43
44
  private spreader: FXLSpreader;
44
45
  private spread = true; // TODO
@@ -53,10 +54,12 @@ export class FXLFramePoolManager {
53
54
  container: HTMLElement,
54
55
  positions: Locator[],
55
56
  pub: Publication,
57
+ _layout: Layout, // TODO: used by upcoming scrolled-layout support
56
58
  injector?: Injector | null,
57
59
  contentProtectionConfig?: IContentProtectionConfig,
58
60
  keyboardPeripheralsConfig?: IKeyboardPeripheralsConfig,
59
61
  resizeWatchSelectors: string[] = [],
62
+ upgradeInsecureRequests?: UpgradeInsecureRequests,
60
63
  ) {
61
64
  this.container = container;
62
65
  this.positions = positions;
@@ -65,11 +68,12 @@ export class FXLFramePoolManager {
65
68
  this.contentProtectionConfig = contentProtectionConfig || {};
66
69
  this.keyboardPeripheralsConfig = keyboardPeripheralsConfig || [];
67
70
  this.resizeWatchSelectors = [...resizeWatchSelectors];
71
+ this.upgradeInsecureRequests = upgradeInsecureRequests ?? 'always';
68
72
  this.spreadPresentation = pub.metadata.otherMetadata?.spread || Spread.auto;
69
73
 
70
74
  if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
71
75
  // TODO support TTB and BTT
72
- throw Error("Unsupported reading progression for EPUB");
76
+ throw Error("Unsupported reading progression for fixed-layout EPUB");
73
77
 
74
78
  // NEW
75
79
  this.spreader = new FXLSpreader(this.pub);
@@ -89,16 +93,18 @@ export class FXLFramePoolManager {
89
93
 
90
94
  this.peripherals = new FXLPeripherals(this);
91
95
 
96
+ const fragment = document.createDocumentFragment();
92
97
  this.pub.readingOrder.items.forEach((link) => {
93
98
  // Create <iframe>
94
99
  const fm = new FXLFrameManager(this.peripherals, this.pub.metadata.effectiveReadingProgression, link.href, this.contentProtectionConfig, this.keyboardPeripheralsConfig, this.resizeWatchSelectors);
95
- this.spineElement.appendChild(fm.element);
96
100
 
97
- // this.pages.push(fm);
98
101
  this.pool.set(link.href, fm);
99
102
  fm.width = 100 / this.length * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
100
103
  fm.height = this.height;
104
+ fragment.appendChild(fm.wrapper);
101
105
  });
106
+ this.spineElement.appendChild(fragment);
107
+
102
108
  }
103
109
 
104
110
  private _listener!: FrameCommsListener;
@@ -137,15 +143,18 @@ export class FXLFramePoolManager {
137
143
 
138
144
  clearTimeout(this.resizeTimeout);
139
145
  this.resizeTimeout = window.setTimeout(() => {
140
- // TODO optimize this expensive set of loops and operations
146
+ const baseWidthFactor = 100 / this.length;
147
+ const itemLookup = new Map(this.pub.readingOrder.items.map(item => [item.href, item]));
141
148
  this.pool.forEach((frm, linkHref) => {
142
- let i = this.pub.readingOrder.items.findIndex(l => l.href === linkHref);
143
- const link = this.pub.readingOrder.items[i];
144
- frm.width = 100 / this.length * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
145
- frm.height = this.height;
146
- if(!frm.loaded) return;
147
- const spread = this.spreader.findByLink(link)!;
148
- frm.update(this.spreadPosition(spread, link));
149
+ const link = itemLookup.get(linkHref);
150
+ if(!link) return;
151
+ requestAnimationFrame(() => {
152
+ frm.width = baseWidthFactor * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
153
+ frm.height = this.height;
154
+ if(!frm.loaded) return;
155
+ const spread = this.spreader.findByLink(link)!;
156
+ frm.update(this.spreadPosition(spread, link));
157
+ });
149
158
  });
150
159
  }, RESIZE_UPDATE_TIMEOUT);
151
160
  }
@@ -184,25 +193,42 @@ export class FXLFramePoolManager {
184
193
  }
185
194
 
186
195
  public updateSpineStyle(animate: boolean, fast = true) {
187
- let margin = "0";
196
+ let margin = 0;
188
197
  this.updateDimensions();
189
198
  if(this.perPage > 1 && true) // this.shift
190
- margin = `${this.width / 2}px`;
191
-
192
- const spineStyle = {
193
- transition: animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out",
194
- marginRight: this.rtl ? margin : "0",
195
- marginLeft: this.rtl ? "0" : margin,
196
- width: `${(this.width / this.perPage) * this.length}px`,
197
- transform: this.transform,
198
-
199
- // Static (should be moved to CSS)
200
- contain: "content"
201
- } as CSSStyleDeclaration;
202
-
203
- Object.assign(this.spineElement.style, spineStyle);
199
+ margin = this.width / 2;
200
+
201
+ if(isTypedOMSupported()) {
202
+ // TODO: don't use UnparsedValue, but rather the longhand of every transition property
203
+ this.spineElement.attributeStyleMap.set("transition", new CSSUnparsedValue([animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out"]));
204
+ this.spineElement.attributeStyleMap.set("margin-right", CSS.px(this.rtl ? margin : 0));
205
+ this.spineElement.attributeStyleMap.set("margin-left", CSS.px(this.rtl ? 0 : margin));
206
+ this.spineElement.attributeStyleMap.set("width", CSS.px((this.width / this.perPage) * this.length));
207
+ this.transform ? this.spineElement.attributeStyleMap.set("transform", new CSSTransformValue([
208
+ new CSSTranslate(CSS.px(this.transform || 0), CSS.px(0), CSS.px(0))
209
+ ])) : this.spineElement.attributeStyleMap.delete("transform");
210
+ this.spineElement.attributeStyleMap.set("contain", new CSSUnparsedValue(["content"]));
211
+ } else {
212
+ const spineStyle = {
213
+ transition: animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out",
214
+ marginRight: this.rtl ? `${margin}px` : "0",
215
+ marginLeft: this.rtl ? "0" : `${margin}px`,
216
+ width: `${(this.width / this.perPage) * this.length}px`,
217
+ transform: this.transform ? `translate3d(${this.transform}px, 0px, 0px)` : "",
218
+ // Static (should be moved to CSS)
219
+ contain: "content"
220
+ } as CSSStyleDeclaration;
221
+ Object.assign(this.spineElement.style, spineStyle);
222
+ }
204
223
  }
205
224
 
225
+ // Reused typed transform for the per-event zoom/pan write: allocating
226
+ // fresh CSSTransformValue objects per write is slower than string
227
+ // parsing; mutating a persistent one is faster than both
228
+ private bookScale: CSSScale | null = null;
229
+ private bookTranslate: CSSTranslate | null = null;
230
+ private bookTransform: CSSTransformValue | null = null;
231
+
206
232
  public updateBookStyle(initial=false) {
207
233
  if(initial) {
208
234
  const bookStyle = {
@@ -221,7 +247,23 @@ export class FXLFramePoolManager {
221
247
  } as CSSStyleDeclaration;
222
248
  Object.assign(this.bookElement.style, bookStyle);
223
249
  }
224
- this.bookElement.style.transform = `scale(${this.peripherals?.scale || 1})` + (this.peripherals ? ` translate3d(${this.peripherals.pan.translateX}px, ${this.peripherals.pan.translateY}px, 0px)` : "");
250
+ const scale = this.peripherals?.scale || 1;
251
+ const translateX = this.peripherals?.pan.translateX || 0;
252
+ const translateY = this.peripherals?.pan.translateY || 0;
253
+ if(isTypedOMSupported()) {
254
+ if(!this.bookTransform) {
255
+ this.bookScale = new CSSScale(1, 1);
256
+ this.bookTranslate = new CSSTranslate(CSS.px(0), CSS.px(0), CSS.px(0));
257
+ this.bookTransform = new CSSTransformValue([this.bookScale, this.bookTranslate]);
258
+ }
259
+ (this.bookScale!.x as CSSUnitValue).value = scale;
260
+ (this.bookScale!.y as CSSUnitValue).value = scale;
261
+ (this.bookTranslate!.x as CSSUnitValue).value = translateX;
262
+ (this.bookTranslate!.y as CSSUnitValue).value = translateY;
263
+ this.bookElement.attributeStyleMap.set("transform", this.bookTransform);
264
+ } else {
265
+ this.bookElement.style.transform = `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0px)`;
266
+ }
225
267
  }
226
268
 
227
269
  /**
@@ -285,22 +327,22 @@ export class FXLFramePoolManager {
285
327
  */
286
328
  slideToCurrent(enableTransition?: boolean, fast = true) {
287
329
  this.updateDimensions();
330
+ // Always commit: the spine transform may have been written directly
331
+ // by the peripherals during a drag (snap-back relies on this), and
332
+ // reading style.transform back as a string for comparison doesn't
333
+ // match under CSS Typed OM (its serialization differs).
288
334
  if (enableTransition) {
289
335
  // This one is tricky, I know but this is a perfect explanation:
290
336
  // https://youtu.be/cCOL7MC4Pl0
291
337
  requestAnimationFrame(() => {
292
338
  requestAnimationFrame(() => {
293
- const newTransform = `translate3d(${this.offset}px, 0, 0)`;
294
- if(this.spineElement.style.transform === newTransform) return;
295
- this.transform = newTransform;
339
+ this.transform = this.offset;
296
340
  this.updateSpineStyle(true, fast);
297
341
  this.deselect();
298
342
  });
299
343
  });
300
344
  } else {
301
- const newTransform = `translate3d(${this.offset}px, 0, 0)`;
302
- if(this.spineElement.style.transform === newTransform) return;
303
- this.transform = newTransform;
345
+ this.transform = this.offset;
304
346
  this.updateSpineStyle(false);
305
347
  this.deselect();
306
348
  }
@@ -308,10 +350,10 @@ export class FXLFramePoolManager {
308
350
 
309
351
  bounce(rtl = false) {
310
352
  requestAnimationFrame(() => {
311
- this.transform = `translate3d(${this.offset+(50 * (rtl ? 1 : -1))}px, 0, 0)`;
353
+ this.transform = this.offset + (50 * (rtl ? 1 : -1));
312
354
  this.updateSpineStyle(true, true);
313
355
  setTimeout(() => {
314
- this.transform = `translate3d(${this.offset}px, 0, 0)`;
356
+ this.transform = this.offset;
315
357
  this.updateSpineStyle(true, true);
316
358
  }, 100);
317
359
  });
@@ -408,7 +450,8 @@ export class FXLFramePoolManager {
408
450
  this.pool.clear();
409
451
 
410
452
  // Revoke all blobs
411
- this.blobs.forEach(v => URL.revokeObjectURL(v));
453
+ this.blobs.forEach(v => v.reset());
454
+ this.blobs.clear();
412
455
 
413
456
  // Clean up injector if it exists
414
457
  this.injector?.dispose();
@@ -499,13 +542,13 @@ export class FXLFramePoolManager {
499
542
  if(!this.pool.has(href)) return;
500
543
  this.cancelShowing(href);
501
544
  await this.pool.get(href)?.unload();
502
- // this.pool.delete(href);
545
+ this.blobs.get(href)?.reset();
503
546
  });
504
547
 
505
548
  // Check if base URL of publication has changed
506
549
  if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
507
550
  // Revoke all blobs
508
- this.blobs.forEach(v => URL.revokeObjectURL(v));
551
+ this.blobs.forEach(v => v.reset());
509
552
  this.blobs.clear();
510
553
  }
511
554
  this.currentBaseURL = pub.baseURL;
@@ -515,16 +558,15 @@ export class FXLFramePoolManager {
515
558
  const itm = pub.readingOrder.items[index];
516
559
  if(!itm) return; // TODO throw?
517
560
  if(!this.blobs.has(href)) {
518
- const blobBuilder = new FrameBlobBuider(
561
+ this.blobs.set(href, new FrameBlobBuilder(
519
562
  pub,
520
563
  this.currentBaseURL || "",
521
564
  itm,
522
565
  {
523
- injector: this.injector
566
+ injector: this.injector,
567
+ upgradeInsecureRequests: this.upgradeInsecureRequests
524
568
  }
525
- );
526
- const blobURL = await blobBuilder.build(true);
527
- this.blobs.set(href, blobURL);
569
+ ));
528
570
  }
529
571
 
530
572
  // Show future offscreen frame in advance after a delay
@@ -538,7 +580,7 @@ export class FXLFramePoolManager {
538
580
  const spread = this.makeSpread(this.reAlign(index));
539
581
  const page = this.spreadPosition(spread, itm);
540
582
  const fm = this.pool.get(href)!;
541
- await fm.load(modules, this.blobs.get(href)!);
583
+ await fm.load(modules, await this.blobs.get(href)!.build(true));
542
584
  if(!this.peripherals.isScaled) // When scaled, positioning is screwed up, so wait to show
543
585
  await fm.show(page); // Show/activate new frame
544
586
  this.delayedShow.delete(href);
@@ -565,7 +607,7 @@ export class FXLFramePoolManager {
565
607
  if(!source) continue; // This can get destroyed
566
608
 
567
609
  this.cancelShowing(s.href);
568
- await newFrame.load(modules, source); // In order to ensure modules match the latest configuration
610
+ await newFrame.load(modules, await source.build(true)); // In order to ensure modules match the latest configuration
569
611
  await newFrame.show(this.spreadPosition(spread, s)); // Show/activate new frame
570
612
  this.previousFrames.push(newFrame);
571
613
  await newFrame.activate();
@@ -592,9 +634,12 @@ export class FXLFramePoolManager {
592
634
  for (const s of spread) {
593
635
  this.inprogress.set(s.href, progressPromise); // Add the job to the in progress map
594
636
  }
595
- await progressPromise; // Wait on the job to finish...
596
- for (const s of spread) {
597
- this.inprogress.delete(s.href); // Delete it from the in progress map!
637
+ try {
638
+ await progressPromise; // Wait on the job to finish...
639
+ } finally {
640
+ for (const s of spread) {
641
+ this.inprogress.delete(s.href); // Delete it from the in progress map!
642
+ }
598
643
  }
599
644
  }
600
645
 
@@ -35,6 +35,8 @@ export interface Zoomer {
35
35
  };
36
36
  }
37
37
 
38
+ export const isTypedOMSupported = () => typeof window.CSSTransformValue !== 'undefined';
39
+
38
40
  export class FXLPeripherals {
39
41
 
40
42
  private readonly manager: FXLFramePoolManager;
@@ -339,6 +341,12 @@ export class FXLPeripherals {
339
341
 
340
342
  private moveFrame = 0;
341
343
 
344
+ // Reused typed transform for the per-frame drag write: allocating fresh
345
+ // CSSTransformValue/CSSTranslate objects per write is slower than string
346
+ // parsing; mutating a persistent one is faster than both
347
+ private dragTranslate: CSSTranslate | null = null;
348
+ private dragTransform: CSSTransformValue | null = null;
349
+
342
350
  /**
343
351
  * touchmove event handler
344
352
  */
@@ -494,11 +502,24 @@ export class FXLPeripherals {
494
502
  const currentOffset = currentSlide * (this.manager.width / this.manager.perPage);
495
503
  const dragOffset = (this.pan.endX - this.pan.startX);
496
504
  const offset = this.manager.rtl ? currentOffset + dragOffset : currentOffset - dragOffset;
505
+ // Snap the translation to device pixels: at fractional offsets the
506
+ // spread gutter is rasterized with a hairline gap between the pages
507
+ const dpr = window.devicePixelRatio || 1;
508
+ const snapped = Math.round((this.manager.rtl ? 1 : -1) * offset * dpr) / dpr;
497
509
 
498
510
  cancelAnimationFrame(this.moveFrame);
499
511
  this.moveFrame = requestAnimationFrame(() => {
500
512
  requestAnimationFrame(() => {
501
- this.manager.spineElement.style.transform = `translate3d(${(this.manager.rtl ? 1 : -1) * offset}px, 0, 0)`;
513
+ if(isTypedOMSupported()) {
514
+ if(!this.dragTransform) {
515
+ this.dragTranslate = new CSSTranslate(CSS.px(0), CSS.px(0), CSS.px(0));
516
+ this.dragTransform = new CSSTransformValue([this.dragTranslate]);
517
+ }
518
+ (this.dragTranslate!.x as CSSUnitValue).value = snapped;
519
+ this.manager.spineElement.attributeStyleMap.set("transform", this.dragTransform);
520
+ } else {
521
+ this.manager.spineElement.style.transform = `translate3d(${snapped}px, 0, 0)`;
522
+ }
502
523
  })
503
524
  });
504
525
  }
@@ -0,0 +1,9 @@
1
+ // Returns a function that runs operations one after another, even when one rejects
2
+ export function createSerializer() {
3
+ let queue: Promise<unknown> = Promise.resolve();
4
+ return <T>(operation: () => Promise<T>): Promise<T> => {
5
+ const result = queue.then(operation);
6
+ queue = result.catch(() => {});
7
+ return result;
8
+ };
9
+ }
package/src/index.ts CHANGED
@@ -2,6 +2,7 @@ export * from './Navigator.ts';
2
2
  export * from './decorations/index.ts';
3
3
  export * from './webpub/index.ts';
4
4
  export * from './epub/index.ts';
5
+ export * from './divina/index.ts';
5
6
  export * from './audio/index.ts';
6
7
  export * from './helpers/index.ts';
7
8
  export * from './preferences/index.ts';
@@ -12,18 +12,28 @@ export class KeyboardConditionBridge {
12
12
  setup() {
13
13
  let initializing = true;
14
14
 
15
+ // Subscribe once per distinct condition object: combos commonly share
16
+ // one condition, and a per-combo subscription would trigger one full
17
+ // onUpdate rebuild per combo for a single condition change.
18
+ const combosByCondition = new Map<NonNullable<KeyCombo["condition"]>, KeyCombo[]>();
15
19
  this.config.forEach(peripheral =>
16
20
  peripheral.keyCombos.forEach(combo => {
17
21
  if (combo.condition) {
18
- const unsub = combo.condition.subscribe((value) => {
19
- this.conditionValues.set(combo, value);
20
- if (!initializing) this.onUpdate(this.buildSerializable());
21
- });
22
- this.unsubs.push(unsub);
22
+ const combos = combosByCondition.get(combo.condition) ?? [];
23
+ combos.push(combo);
24
+ combosByCondition.set(combo.condition, combos);
23
25
  }
24
26
  })
25
27
  );
26
28
 
29
+ combosByCondition.forEach((combos, condition) => {
30
+ const unsub = condition.subscribe((value) => {
31
+ combos.forEach(combo => this.conditionValues.set(combo, value));
32
+ if (!initializing) this.onUpdate(this.buildSerializable());
33
+ });
34
+ this.unsubs.push(unsub);
35
+ });
36
+
27
37
  initializing = false;
28
38
  this.onUpdate(this.buildSerializable());
29
39
  }
@@ -14,11 +14,16 @@ export interface KeyboardPeripheralOptions {
14
14
  export class KeyboardPeripherals {
15
15
  private keydownHandler?: (event: KeyboardEvent) => void;
16
16
  private keyManager = new KeyCombinationManager();
17
+ // pagehide instead of unload: an unload listener disqualifies the page
18
+ // from the back/forward cache; only tear down on real unloads.
19
+ private readonly pagehideHandler = (e: PageTransitionEvent) => {
20
+ if (!e.persisted) this.destroy();
21
+ };
17
22
 
18
23
  constructor(options: KeyboardPeripheralOptions = {}) {
19
24
  this.setupKeyboardPeripherals(options.keyboardPeripherals || []);
20
25
  }
21
-
26
+
22
27
  private setupKeyboardPeripherals(keyboardPeripherals: KeyboardPeripheral[]) {
23
28
  if (keyboardPeripherals.length > 0) {
24
29
  // Create activity event dispatcher
@@ -28,26 +33,27 @@ export class KeyboardPeripherals {
28
33
  });
29
34
  window.dispatchEvent(customEvent);
30
35
  };
31
-
36
+
32
37
  // Create unified handler using the provided keyboard peripherals
33
38
  this.keydownHandler = this.keyManager.createUnifiedHandler(
34
39
  "", // Empty string as target frame source for main window
35
- keyboardPeripherals,
40
+ keyboardPeripherals,
36
41
  dispatcher
37
42
  );
38
-
43
+
39
44
  if (this.keydownHandler) {
40
45
  document.addEventListener("keydown", this.keydownHandler, true);
41
46
  }
42
47
  }
43
-
44
- window.addEventListener("unload", () => this.destroy());
48
+
49
+ window.addEventListener("pagehide", this.pagehideHandler);
45
50
  }
46
-
51
+
47
52
  public destroy() {
48
53
  if (this.keydownHandler) {
49
54
  document.removeEventListener("keydown", this.keydownHandler, true);
50
55
  this.keydownHandler = undefined;
51
56
  }
57
+ window.removeEventListener("pagehide", this.pagehideHandler);
52
58
  }
53
59
  }
@@ -4,6 +4,7 @@ import type { ReadiumWindow } from "@readium/navigator-html-injectables";
4
4
  import { sML } from "@readium/helpers";
5
5
  import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../Navigator.ts";
6
6
  import { KeyboardConditionBridge } from "../peripherals/KeyboardConditionBridge.ts";
7
+ import { createSerializer } from "../helpers/serialize.ts";
7
8
 
8
9
  export class WebPubFrameManager {
9
10
  private frame: HTMLIFrameElement;
@@ -18,6 +19,8 @@ export class WebPubFrameManager {
18
19
  private pendingUnwatchSelectors: string[] = [];
19
20
  private conditionBridge?: KeyboardConditionBridge;
20
21
  private currModules: ModuleName[] = [];
22
+ // Halting comms drops pending acks, so an operation must not start while another awaits one
23
+ private readonly serialize = createSerializer();
21
24
 
22
25
  constructor(
23
26
  source: string,
@@ -29,7 +32,7 @@ export class WebPubFrameManager {
29
32
  this.frame = document.createElement("iframe");
30
33
  this.frame.classList.add("readium-navigator-iframe");
31
34
  this.frame.style.visibility = "hidden";
32
- this.frame.style.setProperty("aria-hidden", "true");
35
+ this.frame.ariaHidden = "true";
33
36
  this.frame.style.opacity = "0";
34
37
  this.frame.style.position = "absolute";
35
38
  this.frame.style.pointerEvents = "none";
@@ -45,7 +48,7 @@ export class WebPubFrameManager {
45
48
  }
46
49
 
47
50
  async load(modules: ModuleName[] = []): Promise<Window> {
48
- return new Promise((res, rej) => {
51
+ return this.serialize(() => new Promise((res, rej) => {
49
52
  if (this.loader) {
50
53
  const wnd = this.frame.contentWindow!;
51
54
  if ([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
@@ -70,7 +73,7 @@ export class WebPubFrameManager {
70
73
  try { rej(err); } catch (error) {}
71
74
  }
72
75
  this.frame.contentWindow!.location.replace(this.source);
73
- });
76
+ }));
74
77
  }
75
78
 
76
79
  private applyContentProtection() {
@@ -130,17 +133,23 @@ export class WebPubFrameManager {
130
133
  }
131
134
 
132
135
  async destroy() {
133
- this.conditionBridge?.destroy();
134
- await this.hide();
135
- this.loader?.destroy();
136
- this.frame.remove();
137
- this.destroyed = true;
136
+ return this.serialize(async () => {
137
+ this.conditionBridge?.destroy();
138
+ await this.hideNow();
139
+ this.loader?.destroy();
140
+ this.frame.remove();
141
+ this.destroyed = true;
142
+ });
138
143
  }
139
144
 
140
145
  async hide(): Promise<void> {
146
+ return this.serialize(() => this.hideNow());
147
+ }
148
+
149
+ private async hideNow(): Promise<void> {
141
150
  if(this.destroyed) return;
142
151
  this.frame.style.visibility = "hidden";
143
- this.frame.style.setProperty("aria-hidden", "true");
152
+ this.frame.ariaHidden = "true";
144
153
  this.frame.style.opacity = "0";
145
154
  this.frame.style.pointerEvents = "none";
146
155
  this.hidden = true;
@@ -159,6 +168,10 @@ export class WebPubFrameManager {
159
168
  }
160
169
 
161
170
  async show(atProgress?: number): Promise<void> {
171
+ return this.serialize(() => this.showNow(atProgress));
172
+ }
173
+
174
+ private async showNow(atProgress?: number): Promise<void> {
162
175
  if (this.destroyed) throw Error("Trying to show frame when it doesn't exist");
163
176
  if (!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
164
177
  if (this.comms) this.comms.resume();
@@ -176,7 +189,7 @@ export class WebPubFrameManager {
176
189
 
177
190
  const remove = () => {
178
191
  this.frame.style.removeProperty("visibility");
179
- this.frame.style.removeProperty("aria-hidden");
192
+ this.frame.ariaHidden = null;
180
193
  this.frame.style.removeProperty("opacity");
181
194
  this.frame.style.removeProperty("pointer-events");
182
195
  this.hidden = false;
@@ -201,14 +214,12 @@ export class WebPubFrameManager {
201
214
  setCSSProperties(properties: { [key: string]: string }) {
202
215
  if(this.destroyed || !this.frame.contentWindow) return;
203
216
 
204
- // We need to resume and halt postMessage to update the properties
205
- // if the frame is hidden since it's been halted in hide()
206
- if (this.hidden) {
207
- if (this.comms) this.comms?.resume();
208
- else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
217
+ // A halted comms can still send, so only a never-shown hidden frame needs one created
218
+ if (this.hidden && !this.comms) {
219
+ this.comms = new FrameComms(this.frame.contentWindow, this.source);
220
+ this.comms.halt();
209
221
  }
210
222
  this.comms?.send("update_properties", properties);
211
- if (this.hidden) this.comms?.halt();
212
223
  }
213
224
 
214
225
  get iframe() {
@@ -208,8 +208,11 @@ export class WebPubFramePoolManager {
208
208
  });
209
209
 
210
210
  this.inprogress.set(newHref, progressPromise);
211
- await progressPromise;
212
- this.inprogress.delete(newHref);
211
+ try {
212
+ await progressPromise;
213
+ } finally {
214
+ this.inprogress.delete(newHref);
215
+ }
213
216
  }
214
217
 
215
218
  setCSSProperties(properties: { [key: string]: string }) {