@readium/navigator 2.10.2 → 2.11.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.
- 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/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/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/FrameManager.ts +7 -9
- package/src/epub/frame/FramePoolManager.ts +106 -48
- package/src/epub/fxl/FXLFrameManager.ts +69 -26
- package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
- package/src/epub/fxl/FXLPeripherals.ts +22 -1
- 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 +7 -9
- 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/FramePoolManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
- package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -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
|
}
|
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
|
}
|
|
@@ -29,7 +29,7 @@ export class WebPubFrameManager {
|
|
|
29
29
|
this.frame = document.createElement("iframe");
|
|
30
30
|
this.frame.classList.add("readium-navigator-iframe");
|
|
31
31
|
this.frame.style.visibility = "hidden";
|
|
32
|
-
this.frame.
|
|
32
|
+
this.frame.ariaHidden = "true";
|
|
33
33
|
this.frame.style.opacity = "0";
|
|
34
34
|
this.frame.style.position = "absolute";
|
|
35
35
|
this.frame.style.pointerEvents = "none";
|
|
@@ -140,7 +140,7 @@ export class WebPubFrameManager {
|
|
|
140
140
|
async hide(): Promise<void> {
|
|
141
141
|
if(this.destroyed) return;
|
|
142
142
|
this.frame.style.visibility = "hidden";
|
|
143
|
-
this.frame.
|
|
143
|
+
this.frame.ariaHidden = "true";
|
|
144
144
|
this.frame.style.opacity = "0";
|
|
145
145
|
this.frame.style.pointerEvents = "none";
|
|
146
146
|
this.hidden = true;
|
|
@@ -176,7 +176,7 @@ export class WebPubFrameManager {
|
|
|
176
176
|
|
|
177
177
|
const remove = () => {
|
|
178
178
|
this.frame.style.removeProperty("visibility");
|
|
179
|
-
this.frame.
|
|
179
|
+
this.frame.ariaHidden = null;
|
|
180
180
|
this.frame.style.removeProperty("opacity");
|
|
181
181
|
this.frame.style.removeProperty("pointer-events");
|
|
182
182
|
this.hidden = false;
|
|
@@ -201,14 +201,12 @@ export class WebPubFrameManager {
|
|
|
201
201
|
setCSSProperties(properties: { [key: string]: string }) {
|
|
202
202
|
if(this.destroyed || !this.frame.contentWindow) return;
|
|
203
203
|
|
|
204
|
-
//
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
|
|
204
|
+
// A halted comms can still send, so only a never-shown hidden frame needs one created
|
|
205
|
+
if (this.hidden && !this.comms) {
|
|
206
|
+
this.comms = new FrameComms(this.frame.contentWindow, this.source);
|
|
207
|
+
this.comms.halt();
|
|
209
208
|
}
|
|
210
209
|
this.comms?.send("update_properties", properties);
|
|
211
|
-
if (this.hidden) this.comms?.halt();
|
|
212
210
|
}
|
|
213
211
|
|
|
214
212
|
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 }) {
|
|
@@ -73,6 +73,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
73
73
|
private framePool!: WebPubFramePoolManager;
|
|
74
74
|
private currentIndex: number = 0;
|
|
75
75
|
private currentLocation: Locator;
|
|
76
|
+
private _destroyed = false;
|
|
76
77
|
private _currentTimelineItem: TimelineItem | undefined;
|
|
77
78
|
private _visibleFragmentIds: string[] = [];
|
|
78
79
|
private _notifiedVisibleFragmentIds: string[] = [];
|
|
@@ -192,6 +193,7 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
192
193
|
|
|
193
194
|
public async load() {
|
|
194
195
|
await this.updateCSS(false);
|
|
196
|
+
if (this._destroyed) return;
|
|
195
197
|
const cssProperties = this.compileCSSProperties(this._css);
|
|
196
198
|
this.framePool = new WebPubFramePoolManager(
|
|
197
199
|
this.container,
|
|
@@ -206,6 +208,9 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
206
208
|
[...this._decorationResizeSelectors]
|
|
207
209
|
);
|
|
208
210
|
|
|
211
|
+
if (this._destroyed) return;
|
|
212
|
+
this.currentLocation = this.completeLocator(this.currentLocation);
|
|
213
|
+
|
|
209
214
|
await this.apply();
|
|
210
215
|
}
|
|
211
216
|
|
|
@@ -456,6 +461,8 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
456
461
|
}
|
|
457
462
|
|
|
458
463
|
public async destroy() {
|
|
464
|
+
// Flag synchronously so an in-flight load() bails before attaching frames.
|
|
465
|
+
this._destroyed = true;
|
|
459
466
|
if (this._suspiciousActivityListener) {
|
|
460
467
|
window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
461
468
|
}
|
|
@@ -859,7 +866,44 @@ export class WebPubNavigator extends VisualNavigator implements Configurable<Web
|
|
|
859
866
|
cb(done);
|
|
860
867
|
}
|
|
861
868
|
|
|
869
|
+
private completeLocator(locator: Locator): Locator {
|
|
870
|
+
if (!locator.href) {
|
|
871
|
+
const items = this.pub.readingOrder.items;
|
|
872
|
+
let fellback = false;
|
|
873
|
+
if (typeof locator.locations.position === "number") {
|
|
874
|
+
const link = items[locator.locations.position - 1];
|
|
875
|
+
if (link) {
|
|
876
|
+
locator = (this.pub.manifest.locatorFromLink(link) || new Locator({
|
|
877
|
+
href: link.href,
|
|
878
|
+
type: link.type || "text/html"
|
|
879
|
+
})).copyWithLocations(locator.locations);
|
|
880
|
+
fellback = true;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
if (!fellback && typeof locator.locations?.totalProgression === "number") {
|
|
884
|
+
// WebPub has no per-resource totalProgression like EPUB's position list,
|
|
885
|
+
// so approximate by spreading the reading order evenly. This is here to
|
|
886
|
+
// help with conversion from OPDS locators which only require the total
|
|
887
|
+
// progression in the publication.
|
|
888
|
+
const targetProgression = locator.locations.totalProgression;
|
|
889
|
+
const closestIdx = Math.min(
|
|
890
|
+
items.length - 1,
|
|
891
|
+
Math.max(0, Math.round(targetProgression * (items.length - 1)))
|
|
892
|
+
);
|
|
893
|
+
const link = items[closestIdx];
|
|
894
|
+
if (link) {
|
|
895
|
+
locator = (this.pub.manifest.locatorFromLink(link) || new Locator({
|
|
896
|
+
href: link.href,
|
|
897
|
+
type: link.type || "text/html"
|
|
898
|
+
})).copyWithLocations(locator.locations);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
return locator;
|
|
903
|
+
}
|
|
904
|
+
|
|
862
905
|
public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
|
|
906
|
+
locator = this.completeLocator(locator);
|
|
863
907
|
const href = locator.href.split("#")[0];
|
|
864
908
|
let link = this.pub.readingOrder.findWithHref(href);
|
|
865
909
|
if(!link) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Link, Locator, Publication, Timeline, TimelineItem } from "@readium/shared";
|
|
2
2
|
import { MediaNavigator, IContentProtectionConfig, IKeyboardPeripheralsConfig, KeyboardPeripheralEventData } from "../Navigator.ts";
|
|
3
3
|
import { Configurable } from "../preferences/Configurable.ts";
|
|
4
|
+
import { AudioMseLoaderFactory } from "./engine/index.ts";
|
|
4
5
|
import { AudioPreferences, AudioSettings, AudioPreferencesEditor, IAudioPreferences, IAudioDefaults } from "./preferences/index.ts";
|
|
5
6
|
import { ContextMenuEvent, SuspiciousActivityEvent } from "@readium/navigator-html-injectables";
|
|
6
7
|
export interface AudioMetadata {
|
|
@@ -35,6 +36,24 @@ export interface AudioNavigatorConfiguration {
|
|
|
35
36
|
defaults: IAudioDefaults;
|
|
36
37
|
contentProtection?: IAudioContentProtectionConfig;
|
|
37
38
|
keyboardPeripherals?: IKeyboardPeripheralsConfig;
|
|
39
|
+
/**
|
|
40
|
+
* Called with the persistent playback element before
|
|
41
|
+
* the first src is assigned, so the host can prepare it. Use for MSE/EME
|
|
42
|
+
* setups. When it returns a promise, loading of
|
|
43
|
+
* the initial track (and prefetching of adjacent ones) is deferred until
|
|
44
|
+
* the promise settles; a rejection is forwarded to the error listener and
|
|
45
|
+
* loading proceeds anyway.
|
|
46
|
+
*/
|
|
47
|
+
mediaElementSetup?: (element: HTMLMediaElement) => void | Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* When provided, media bytes reach the playback element through Media
|
|
50
|
+
* Source Extensions instead of direct `src` assignment: the engine
|
|
51
|
+
* creates one loader per track via this factory and the loader owns all
|
|
52
|
+
* fetching. Supply one for e.g. EME encrypted audio, since browsers have
|
|
53
|
+
* poor support for encrypted audio directly through `src`. The container
|
|
54
|
+
* handling (WebM, fMP4, …) is the loader implementation's concern.
|
|
55
|
+
*/
|
|
56
|
+
mseLoaderFactory?: AudioMseLoaderFactory;
|
|
38
57
|
}
|
|
39
58
|
export declare class AudioNavigator extends MediaNavigator implements Configurable<AudioSettings, AudioPreferences> {
|
|
40
59
|
private readonly pub;
|
|
@@ -58,9 +77,13 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
|
|
|
58
77
|
/** True while a track transition is in progress; suppresses spurious mid-navigation events. */
|
|
59
78
|
private _isNavigating;
|
|
60
79
|
private _isStalled;
|
|
80
|
+
/** Set by destroy(); stops the deferred initial load from touching a dead instance. */
|
|
81
|
+
private _destroyed;
|
|
61
82
|
private _stalledWatchdog;
|
|
62
83
|
private _stalledCheckTime;
|
|
63
84
|
constructor(publication: Publication, listeners: AudioNavigatorListeners, initialPosition?: Locator, configuration?: AudioNavigatorConfiguration);
|
|
85
|
+
/** Sets the initial track on the primary element and seeks to the starting position. */
|
|
86
|
+
private startInitialLoad;
|
|
64
87
|
get settings(): AudioSettings;
|
|
65
88
|
get preferencesEditor(): AudioPreferencesEditor;
|
|
66
89
|
submitPreferences(preferences: AudioPreferences): Promise<void>;
|
|
@@ -73,6 +96,12 @@ export declare class AudioNavigator extends MediaNavigator implements Configurab
|
|
|
73
96
|
private hrefToTrackIndex;
|
|
74
97
|
/** Current track index derived from the current location's href. */
|
|
75
98
|
private currentTrackIndex;
|
|
99
|
+
/**
|
|
100
|
+
* Builds a locator for an overall publication progression (0–1) by walking
|
|
101
|
+
* the cumulative track durations. Falls back to the start of the first
|
|
102
|
+
* track when the reading order carries no duration metadata.
|
|
103
|
+
*/
|
|
104
|
+
private locatorFromTotalProgression;
|
|
76
105
|
get currentLocator(): Locator;
|
|
77
106
|
get isPlaying(): boolean;
|
|
78
107
|
get isPaused(): boolean;
|