@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.
- package/dist/audio/AudioNavigator.js +1 -1
- package/dist/audio/AudioPoolManager.js +1 -1
- package/dist/audio/engine/WebAudioEngine.js +1 -1
- package/dist/divina/DivinaNavigator.js +1 -0
- package/dist/divina/DivinaPageManager.js +1 -0
- package/dist/divina/DivinaPagedPresenter.js +1 -0
- package/dist/divina/DivinaPeripherals.js +1 -0
- package/dist/divina/DivinaScrolledPresenter.js +1 -0
- package/dist/divina/DivinaSpreader.js +1 -0
- package/dist/divina/DivinaVariantSelector.js +1 -0
- package/dist/divina/preferences/DivinaDefaults.js +1 -0
- package/dist/divina/preferences/DivinaPreferences.js +1 -0
- package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
- package/dist/divina/preferences/DivinaSettings.js +1 -0
- package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FrameBlobBuilder.js +3 -1
- package/dist/epub/frame/FrameComms.js +1 -1
- package/dist/epub/frame/FrameManager.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLFrameManager.js +1 -1
- package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLPeripherals.js +2 -2
- package/dist/helpers/serialize.js +1 -0
- package/dist/index.js +1 -1
- package/dist/peripherals/KeyboardConditionBridge.js +1 -1
- package/dist/peripherals/KeyboardPeripherals.js +1 -1
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +5 -5
- package/src/audio/AudioNavigator.ts +107 -5
- package/src/audio/AudioPoolManager.ts +14 -8
- package/src/audio/engine/AudioEngine.ts +34 -1
- package/src/audio/engine/WebAudioEngine.ts +48 -3
- package/src/divina/DivinaNavigator.ts +704 -0
- package/src/divina/DivinaPageManager.ts +322 -0
- package/src/divina/DivinaPagedPresenter.ts +508 -0
- package/src/divina/DivinaPeripherals.ts +630 -0
- package/src/divina/DivinaScrolledPresenter.ts +516 -0
- package/src/divina/DivinaSpreader.ts +202 -0
- package/src/divina/DivinaVariantSelector.ts +77 -0
- package/src/divina/index.ts +8 -0
- package/src/divina/preferences/DivinaDefaults.ts +37 -0
- package/src/divina/preferences/DivinaPreferences.ts +77 -0
- package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
- package/src/divina/preferences/DivinaSettings.ts +38 -0
- package/src/divina/preferences/index.ts +4 -0
- package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
- package/src/epub/EpubNavigator.ts +73 -7
- package/src/epub/frame/FrameBlobBuilder.ts +144 -39
- package/src/epub/frame/FrameComms.ts +42 -7
- package/src/epub/frame/FrameManager.ts +27 -16
- package/src/epub/frame/FramePoolManager.ts +106 -48
- package/src/epub/fxl/FXLFrameManager.ts +99 -34
- package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
- package/src/epub/fxl/FXLPeripherals.ts +22 -1
- package/src/helpers/serialize.ts +9 -0
- package/src/index.ts +1 -0
- package/src/peripherals/KeyboardConditionBridge.ts +15 -5
- package/src/peripherals/KeyboardPeripherals.ts +13 -7
- package/src/webpub/WebPubFrameManager.ts +27 -16
- package/src/webpub/WebPubFramePoolManager.ts +5 -2
- package/src/webpub/WebPubNavigator.ts +44 -0
- package/types/src/audio/AudioNavigator.d.ts +29 -0
- package/types/src/audio/engine/AudioEngine.d.ts +27 -1
- package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
- package/types/src/divina/DivinaNavigator.d.ts +119 -0
- package/types/src/divina/DivinaPageManager.d.ts +66 -0
- package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
- package/types/src/divina/DivinaPeripherals.d.ts +121 -0
- package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
- package/types/src/divina/DivinaSpreader.d.ts +65 -0
- package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
- package/types/src/divina/index.d.ts +8 -0
- package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
- package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
- package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
- package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
- package/types/src/divina/preferences/index.d.ts +4 -0
- package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
- package/types/src/epub/EpubNavigator.d.ts +7 -1
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
- package/types/src/epub/frame/FrameComms.d.ts +8 -1
- package/types/src/epub/frame/FrameManager.d.ts +3 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +7 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
- package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
- package/types/src/helpers/serialize.d.ts +1 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +3 -0
- 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
|
|
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,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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 =
|
|
196
|
+
let margin = 0;
|
|
188
197
|
this.updateDimensions();
|
|
189
198
|
if(this.perPage > 1 && true) // this.shift
|
|
190
|
-
margin =
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
353
|
+
this.transform = this.offset + (50 * (rtl ? 1 : -1));
|
|
312
354
|
this.updateSpineStyle(true, true);
|
|
313
355
|
setTimeout(() => {
|
|
314
|
-
this.transform =
|
|
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 =>
|
|
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
|
-
|
|
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 =>
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
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("
|
|
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.
|
|
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.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
212
|
-
|
|
211
|
+
try {
|
|
212
|
+
await progressPromise;
|
|
213
|
+
} finally {
|
|
214
|
+
this.inprogress.delete(newHref);
|
|
215
|
+
}
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
setCSSProperties(properties: { [key: string]: string }) {
|