@readium/navigator 2.6.1 → 2.7.0-alpha.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/LICENSE +28 -0
- package/dist/index.js +4795 -3156
- package/dist/index.umd.cjs +42 -40
- package/package.json +10 -10
- package/src/divina/DivinaNavigator.ts +662 -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 +89 -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 +59 -3
- package/src/epub/frame/FrameBlobBuilder.ts +85 -32
- package/src/epub/frame/FrameManager.ts +3 -3
- package/src/epub/frame/FramePoolManager.ts +78 -34
- package/src/epub/fxl/FXLFrameManager.ts +69 -26
- package/src/epub/fxl/FXLFramePoolManager.ts +88 -50
- 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 +3 -3
- package/types/src/divina/DivinaNavigator.d.ts +113 -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 +3 -1
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +6 -3
- package/types/src/epub/frame/FramePoolManager.d.ts +1 -0
- package/types/src/epub/fxl/FXLFrameManager.d.ts +2 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +6 -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
|
@@ -6,6 +6,7 @@ import { CommsEventKey, ContextMenuEvent, DecorationActivatedEvent, FXLModules,
|
|
|
6
6
|
import { Decoration, DecorationActivationEvent, DecorationObserver, DecorableNavigator, DecoratorConfig, decorationsEqual, resolveDecorationForWire, BUILTIN_DECORATION_TYPES } from "../decorations/index.ts";
|
|
7
7
|
import * as path from "path-browserify";
|
|
8
8
|
import { FXLFrameManager } from "./fxl/FXLFrameManager.ts";
|
|
9
|
+
import { isTypedOMSupported } from "./fxl/FXLPeripherals.ts";
|
|
9
10
|
import { FrameManager } from "./frame/FrameManager.ts";
|
|
10
11
|
import { IEpubPreferences, EpubPreferences } from "./preferences/EpubPreferences.ts";
|
|
11
12
|
import { IEpubDefaults, EpubDefaults } from "./preferences/EpubDefaults.ts";
|
|
@@ -71,6 +72,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
71
72
|
private readonly container: HTMLElement;
|
|
72
73
|
private readonly listeners: EpubNavigatorListeners;
|
|
73
74
|
private framePool!: FramePoolManager | FXLFramePoolManager;
|
|
75
|
+
private _destroyed = false;
|
|
74
76
|
private positions!: Locator[];
|
|
75
77
|
private currentLocation!: Locator;
|
|
76
78
|
private lastLocationInView: Locator | undefined;
|
|
@@ -249,11 +251,14 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
249
251
|
});
|
|
250
252
|
}
|
|
251
253
|
|
|
254
|
+
if (this._destroyed) return;
|
|
255
|
+
|
|
252
256
|
if(this._layout === Layout.fixed) {
|
|
253
257
|
this.framePool = new FXLFramePoolManager(
|
|
254
258
|
this.container,
|
|
255
259
|
this.positions,
|
|
256
260
|
this.pub,
|
|
261
|
+
this._layout,
|
|
257
262
|
this._injector,
|
|
258
263
|
this._contentProtection,
|
|
259
264
|
this._keyboardPeripherals
|
|
@@ -263,6 +268,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
263
268
|
}
|
|
264
269
|
} else {
|
|
265
270
|
await this.updateCSS(false);
|
|
271
|
+
if (this._destroyed) return;
|
|
266
272
|
const cssProperties = this.compileCSSProperties(this._css);
|
|
267
273
|
this.framePool = new FramePoolManager(
|
|
268
274
|
this.container,
|
|
@@ -276,9 +282,17 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
276
282
|
|
|
277
283
|
if(this.currentLocation === undefined)
|
|
278
284
|
this.currentLocation = this.positions[0];
|
|
285
|
+
else
|
|
286
|
+
this.currentLocation = this.completeLocator(this.currentLocation);
|
|
279
287
|
|
|
280
288
|
await this.resizeHandler();
|
|
281
|
-
|
|
289
|
+
if (this._destroyed) return;
|
|
290
|
+
return new Promise(async res => {
|
|
291
|
+
if (this._destroyed) return res(false);
|
|
292
|
+
await this.go(this.currentLocation, false, (s) => {
|
|
293
|
+
res(s);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
282
296
|
}
|
|
283
297
|
|
|
284
298
|
public get settings(): Readonly<EpubSettings> {
|
|
@@ -383,7 +397,12 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
383
397
|
const parentEl = this.container.parentElement || document.documentElement;
|
|
384
398
|
|
|
385
399
|
if (this._layout === Layout.fixed) {
|
|
386
|
-
|
|
400
|
+
const width = getContentWidth(parentEl) - this._settings.constraint;
|
|
401
|
+
if (isTypedOMSupported()) {
|
|
402
|
+
this.container.attributeStyleMap.set("width", CSS.px(width));
|
|
403
|
+
} else {
|
|
404
|
+
this.container.style.width = `${ width }px`;
|
|
405
|
+
}
|
|
387
406
|
if (!this.framePool) return;
|
|
388
407
|
(this.framePool as FXLFramePoolManager).resizeHandler();
|
|
389
408
|
} else {
|
|
@@ -779,6 +798,8 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
779
798
|
// End of Decoration
|
|
780
799
|
|
|
781
800
|
public async destroy() {
|
|
801
|
+
// Flag synchronously so an in-flight load() bails before attaching frames.
|
|
802
|
+
this._destroyed = true;
|
|
782
803
|
if (this._suspiciousActivityListener) {
|
|
783
804
|
window.removeEventListener(NAVIGATOR_SUSPICIOUS_ACTIVITY_EVENT, this._suspiciousActivityListener);
|
|
784
805
|
}
|
|
@@ -1078,7 +1099,7 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
1078
1099
|
return;
|
|
1079
1100
|
}
|
|
1080
1101
|
|
|
1081
|
-
const progression = locator
|
|
1102
|
+
const progression = locator.locations?.progression;
|
|
1082
1103
|
const hasProgression = progression && progression > 0;
|
|
1083
1104
|
if(hasProgression)
|
|
1084
1105
|
done = await new Promise<boolean>((res, _) => {
|
|
@@ -1089,7 +1110,42 @@ export class EpubNavigator extends VisualNavigator implements Configurable<Confi
|
|
|
1089
1110
|
cb(done);
|
|
1090
1111
|
}
|
|
1091
1112
|
|
|
1113
|
+
private completeLocator(locator: Locator): Locator {
|
|
1114
|
+
if(!locator.href) {
|
|
1115
|
+
let fellback = false;
|
|
1116
|
+
if(typeof locator.locations.position === "number") {
|
|
1117
|
+
const match = this.positions.find(p => p.locations.position === locator.locations.position);
|
|
1118
|
+
if (match) {
|
|
1119
|
+
locator = match.copyWithLocations(locator.locations);
|
|
1120
|
+
fellback = true;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
if(!fellback && typeof locator.locations?.totalProgression === "number") {
|
|
1124
|
+
// If locator has no href, but it does have a totalProgression,
|
|
1125
|
+
// we can attempt to find the right resource from the positions list.
|
|
1126
|
+
// This is here to help with conversion from OPDS locators which only
|
|
1127
|
+
// require the total progression in the publication.
|
|
1128
|
+
const targetProgression = locator.locations.totalProgression;
|
|
1129
|
+
let closestIdx = 0;
|
|
1130
|
+
let closestDist = Infinity;
|
|
1131
|
+
for (let i = 0; i < this.positions.length; i++) {
|
|
1132
|
+
const pos = this.positions[i];
|
|
1133
|
+
// Use totalProgression if available, otherwise estimate from index
|
|
1134
|
+
const posProg = pos.locations.totalProgression ?? (i / this.positions.length);
|
|
1135
|
+
const dist = Math.abs(posProg - targetProgression);
|
|
1136
|
+
if (dist < closestDist) {
|
|
1137
|
+
closestDist = dist;
|
|
1138
|
+
closestIdx = i;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
locator = this.positions[closestIdx].copyWithLocations(locator.locations);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
return locator;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1092
1147
|
public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
|
|
1148
|
+
locator = this.completeLocator(locator);
|
|
1093
1149
|
const href = locator.href.split("#")[0];
|
|
1094
1150
|
let link = this.pub.readingOrder.findWithHref(href);
|
|
1095
1151
|
if(!link) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Link, MediaType, Publication, ReadingProgression } from "@readium/shared";
|
|
1
|
+
import { Link, MediaType, Publication, ReadingProgression, Resource } from "@readium/shared";
|
|
2
2
|
import { Injector } from "../../injection/Injector.ts";
|
|
3
3
|
import { getScriptMode } from "../../helpers/scriptMode.ts";
|
|
4
4
|
|
|
@@ -20,73 +20,121 @@ const csp = (domains: string[]) => {
|
|
|
20
20
|
].join("; ");
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
export default class
|
|
24
|
-
private readonly item: Link;
|
|
25
|
-
private readonly burl: string;
|
|
26
|
-
private readonly pub: Publication;
|
|
23
|
+
export default class FrameBlobBuilder {
|
|
27
24
|
private readonly cssProperties?: { [key: string]: string };
|
|
28
25
|
private readonly injector: Injector | null = null;
|
|
29
26
|
|
|
27
|
+
private currentUrl?: string;
|
|
28
|
+
private currentResource?: Resource;
|
|
29
|
+
|
|
30
30
|
constructor(
|
|
31
|
-
pub: Publication,
|
|
32
|
-
baseURL: string,
|
|
33
|
-
item: Link,
|
|
31
|
+
private readonly pub: Publication,
|
|
32
|
+
private readonly baseURL: string,
|
|
33
|
+
private readonly item: Link,
|
|
34
34
|
options: {
|
|
35
35
|
cssProperties?: { [key: string]: string };
|
|
36
36
|
injector?: Injector | null;
|
|
37
37
|
}
|
|
38
38
|
) {
|
|
39
|
-
this.pub = pub;
|
|
40
39
|
this.item = item;
|
|
41
|
-
this.burl = item.toURL(baseURL) || "";
|
|
42
40
|
this.cssProperties = options.cssProperties;
|
|
43
41
|
this.injector = options.injector ?? null;
|
|
44
42
|
}
|
|
45
43
|
|
|
44
|
+
public reset() {
|
|
45
|
+
this.currentUrl && URL.revokeObjectURL(this.currentUrl);
|
|
46
|
+
this.currentUrl = undefined;
|
|
47
|
+
this.currentResource?.close();
|
|
48
|
+
this.currentResource = undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
46
51
|
public async build(fxl = false): Promise<string> {
|
|
47
|
-
if(
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
if(this.currentUrl) return this.currentUrl;
|
|
53
|
+
|
|
54
|
+
this.currentResource = this.pub.get(this.item);
|
|
55
|
+
const link = await this.currentResource.link();
|
|
56
|
+
if(!this.currentResource) {
|
|
57
|
+
// Reset has occured in the meantime
|
|
58
|
+
return "about:blank";
|
|
59
|
+
}
|
|
60
|
+
if(!link.mediaType.isHTML) {
|
|
61
|
+
if(link.mediaType.isBitmap || link.mediaType.equals(MediaType.SVG)) {
|
|
62
|
+
const blobUrl = await this.buildImageFrame();
|
|
63
|
+
this.currentUrl = blobUrl;
|
|
64
|
+
return blobUrl;
|
|
50
65
|
} else
|
|
51
|
-
throw Error("Unsupported frame mediatype " +
|
|
66
|
+
throw Error("Unsupported frame mediatype " + link.mediaType.string);
|
|
52
67
|
} else {
|
|
53
|
-
|
|
68
|
+
const blobUrl = await this.buildHtmlFrame(fxl);
|
|
69
|
+
this.currentUrl = blobUrl;
|
|
70
|
+
return blobUrl;
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
|
|
57
74
|
private async buildHtmlFrame(fxl = false): Promise<string> {
|
|
58
|
-
|
|
59
|
-
const txt = await this.pub.get(this.item).readAsString();
|
|
60
|
-
if(!txt) throw new Error(`Failed reading item ${this.item.href}`);
|
|
75
|
+
if(!this.currentResource) throw new Error("No resource loaded");
|
|
61
76
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
77
|
+
// Load the HTML resource
|
|
78
|
+
const link = await this.currentResource.link();
|
|
79
|
+
const doc = await this.currentResource.readAsXML() as HTMLDocument;
|
|
80
|
+
if(!doc) throw new Error(`Failed reading item ${link.href}`);
|
|
66
81
|
|
|
67
82
|
const perror = doc.querySelector("parsererror");
|
|
68
83
|
if (perror) {
|
|
69
84
|
const details = perror.querySelector("div");
|
|
70
|
-
throw new Error(`Failed parsing item ${
|
|
85
|
+
throw new Error(`Failed parsing item ${link.href}: ${details?.textContent || perror.textContent}`);
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
// Apply resource injections if injection service is provided
|
|
74
89
|
if (this.injector) {
|
|
75
|
-
await this.injector.injectForDocument(doc,
|
|
90
|
+
await this.injector.injectForDocument(doc, link);
|
|
76
91
|
}
|
|
77
92
|
|
|
78
|
-
return this.finalizeDOM(doc, this.pub.baseURL, this.
|
|
93
|
+
return this.finalizeDOM(doc, this.pub.baseURL, link.toURL(this.baseURL) || "", link.mediaType, fxl, this.cssProperties);
|
|
79
94
|
}
|
|
80
95
|
|
|
81
|
-
private buildImageFrame(): string {
|
|
82
|
-
|
|
83
|
-
const
|
|
96
|
+
private async buildImageFrame(): Promise<string> {
|
|
97
|
+
if(!this.currentResource) throw new Error("No resource loaded");
|
|
98
|
+
const link = await this.currentResource.link();
|
|
99
|
+
const burl = link.toURL(this.baseURL) || ""
|
|
100
|
+
|
|
101
|
+
// Rudimentary image display in an HTML doc
|
|
102
|
+
const doc = document.implementation.createHTMLDocument(link.title || link.href);
|
|
103
|
+
|
|
104
|
+
// Add viewport if available
|
|
105
|
+
if((link?.height || 0) > 0 && (link?.width || 0) > 0) {
|
|
106
|
+
const viewportMeta = doc.createElement("meta");
|
|
107
|
+
viewportMeta.name = "viewport";
|
|
108
|
+
viewportMeta.content = `width=${link.width}, height=${link.height}`;
|
|
109
|
+
viewportMeta.dataset.readium = "true";
|
|
110
|
+
doc.head.appendChild(viewportMeta);
|
|
111
|
+
}
|
|
112
|
+
|
|
84
113
|
const simg = document.createElement("img");
|
|
85
|
-
simg.src =
|
|
86
|
-
simg.alt =
|
|
87
|
-
simg.
|
|
114
|
+
simg.src = burl || "";
|
|
115
|
+
simg.alt = link.title || "";
|
|
116
|
+
await simg.decode(); // Reduce repaints
|
|
88
117
|
doc.body.appendChild(simg);
|
|
89
|
-
|
|
118
|
+
|
|
119
|
+
// Apply resource injections if injection service is provided
|
|
120
|
+
if (this.injector) {
|
|
121
|
+
await this.injector.injectForDocument(doc, new Link({
|
|
122
|
+
// Temporary solution to address injector only expecting (X)HTML
|
|
123
|
+
// documents for injection, which we are technically providing
|
|
124
|
+
href: "readium-image-frame.xhtml",
|
|
125
|
+
type: MediaType.XHTML.string
|
|
126
|
+
}));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Add image style
|
|
130
|
+
const sstyle = doc.createElement("style");
|
|
131
|
+
sstyle.dataset.readium = "true";
|
|
132
|
+
sstyle.textContent = `
|
|
133
|
+
html, body { width: 100%; height: 100%; margin: 0; padding: 0; font-size: 0; }
|
|
134
|
+
img { margin: 0; padding: 0; border: 0; }`;
|
|
135
|
+
doc.head.appendChild(sstyle);
|
|
136
|
+
|
|
137
|
+
return this.finalizeDOM(doc, this.pub.baseURL, burl, link.mediaType, true);
|
|
90
138
|
}
|
|
91
139
|
|
|
92
140
|
private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
|
|
@@ -102,6 +150,10 @@ export default class FrameBlobBuider {
|
|
|
102
150
|
// Get allowed domains from injector if it exists
|
|
103
151
|
const allowedDomains = this.injector?.getAllowedDomains?.() || [];
|
|
104
152
|
|
|
153
|
+
// Remove query from root if present, as CSP doesn't allow them
|
|
154
|
+
root = root?.split("?")[0];
|
|
155
|
+
|
|
156
|
+
|
|
105
157
|
// Always include the root domain if provided
|
|
106
158
|
const domains = [...new Set([
|
|
107
159
|
...(root ? [root] : []),
|
|
@@ -124,6 +176,7 @@ export default class FrameBlobBuider {
|
|
|
124
176
|
// loaded in parallel, greatly increasing overall speed.
|
|
125
177
|
doc.body.querySelectorAll("img").forEach((img) => {
|
|
126
178
|
img.setAttribute("fetchpriority", "high");
|
|
179
|
+
img.setAttribute("referrerpolicy", "origin");
|
|
127
180
|
});
|
|
128
181
|
|
|
129
182
|
// We need to ensure that lang is set on the root element
|
|
@@ -27,7 +27,7 @@ export class FrameManager {
|
|
|
27
27
|
this.frame.sandbox.value = "allow-same-origin allow-scripts";
|
|
28
28
|
this.frame.classList.add("readium-navigator-iframe");
|
|
29
29
|
this.frame.style.visibility = "hidden";
|
|
30
|
-
this.frame.
|
|
30
|
+
this.frame.ariaHidden = "true";
|
|
31
31
|
this.frame.style.opacity = "0";
|
|
32
32
|
this.frame.style.position = "absolute";
|
|
33
33
|
this.frame.style.pointerEvents = "none";
|
|
@@ -111,7 +111,7 @@ export class FrameManager {
|
|
|
111
111
|
async hide(): Promise<void> {
|
|
112
112
|
if(this.destroyed) return;
|
|
113
113
|
this.frame.style.visibility = "hidden";
|
|
114
|
-
this.frame.
|
|
114
|
+
this.frame.ariaHidden = "true";
|
|
115
115
|
this.frame.style.opacity = "0";
|
|
116
116
|
this.frame.style.pointerEvents = "none";
|
|
117
117
|
this.hidden = true;
|
|
@@ -143,9 +143,9 @@ export class FrameManager {
|
|
|
143
143
|
|
|
144
144
|
const remove = () => {
|
|
145
145
|
this.frame.style.removeProperty("visibility");
|
|
146
|
-
this.frame.style.removeProperty("aria-hidden");
|
|
147
146
|
this.frame.style.removeProperty("opacity");
|
|
148
147
|
this.frame.style.removeProperty("pointer-events");
|
|
148
|
+
this.frame.ariaHidden = null;
|
|
149
149
|
this.hidden = false;
|
|
150
150
|
|
|
151
151
|
if (sML.UA.WebKit) {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ModuleName } from "@readium/navigator-html-injectables";
|
|
2
2
|
import { Locator, Publication } from "@readium/shared";
|
|
3
|
-
import
|
|
3
|
+
import FrameBlobBuilder from "./FrameBlobBuilder.ts";
|
|
4
4
|
import { FrameManager } from "./FrameManager.ts";
|
|
5
5
|
import { Injector } from "../../injection/Injector.ts";
|
|
6
6
|
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
7
7
|
|
|
8
|
-
const UPPER_BOUNDARY =
|
|
9
|
-
const LOWER_BOUNDARY =
|
|
8
|
+
const UPPER_BOUNDARY = 10;
|
|
9
|
+
const LOWER_BOUNDARY = 5;
|
|
10
10
|
|
|
11
11
|
export class FramePoolManager {
|
|
12
12
|
private readonly container: HTMLElement;
|
|
@@ -14,13 +14,14 @@ export class FramePoolManager {
|
|
|
14
14
|
private _currentFrame: FrameManager | undefined;
|
|
15
15
|
private currentCssProperties: { [key: string]: string } | undefined;
|
|
16
16
|
private readonly pool: Map<string, FrameManager> = new Map();
|
|
17
|
-
private readonly blobs: Map<string,
|
|
18
|
-
private readonly inprogress: Map<string, Promise<
|
|
17
|
+
private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
|
|
18
|
+
private readonly inprogress: Map<string, Promise<unknown>> = new Map();
|
|
19
19
|
private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
|
|
20
20
|
private currentBaseURL: string | undefined;
|
|
21
21
|
private readonly injector: Injector | null = null;
|
|
22
22
|
private readonly contentProtectionConfig: IContentProtectionConfig;
|
|
23
23
|
private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
|
|
24
|
+
private updateSequence = 0;
|
|
24
25
|
|
|
25
26
|
constructor(
|
|
26
27
|
container: HTMLElement,
|
|
@@ -42,7 +43,7 @@ export class FramePoolManager {
|
|
|
42
43
|
// Wait for all in-progress loads to complete
|
|
43
44
|
let iit = this.inprogress.values();
|
|
44
45
|
let inp = iit.next();
|
|
45
|
-
const inprogressPromises: Promise<
|
|
46
|
+
const inprogressPromises: Promise<unknown>[] = [];
|
|
46
47
|
while(inp.value) {
|
|
47
48
|
inprogressPromises.push(inp.value);
|
|
48
49
|
inp = iit.next();
|
|
@@ -62,10 +63,8 @@ export class FramePoolManager {
|
|
|
62
63
|
this.pool.clear();
|
|
63
64
|
|
|
64
65
|
// Revoke all blobs
|
|
65
|
-
this.blobs.forEach(v =>
|
|
66
|
-
|
|
67
|
-
URL.revokeObjectURL(v);
|
|
68
|
-
});
|
|
66
|
+
this.blobs.forEach(v => v.reset());
|
|
67
|
+
this.blobs.clear();
|
|
69
68
|
|
|
70
69
|
// Clean up injector if it exists
|
|
71
70
|
this.injector?.dispose();
|
|
@@ -77,6 +76,7 @@ export class FramePoolManager {
|
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
async update(pub: Publication, locator: Locator, modules: ModuleName[], force=false) {
|
|
79
|
+
const updateSequence = ++this.updateSequence;
|
|
80
80
|
let i = this.positions.findIndex(l => l.locations.position === locator.locations.position);
|
|
81
81
|
if(i < 0) throw Error(`Locator not found in position list: ${locator.locations.position} > ${this.positions.reduce<number>((acc, l) => l.locations.position || 0 > acc ? l.locations.position || 0 : acc, 0) }`);
|
|
82
82
|
const newHref = this.positions[i].href;
|
|
@@ -106,15 +106,18 @@ export class FramePoolManager {
|
|
|
106
106
|
this.pool.delete(href);
|
|
107
107
|
if(this.pendingUpdates.has(href))
|
|
108
108
|
this.pendingUpdates.set(href, { inPool: false });
|
|
109
|
+
// Note that we don't reset the blob here, unlike in the FXL pool.
|
|
110
|
+
// This is because FXL tends to have a ton more blobs. Maybe we'll adjust
|
|
111
|
+
// this at a later point with a much larger boundary for resets to deal
|
|
112
|
+
// with extremely long/large reflowable publications.
|
|
113
|
+
// Reflowable publication resources also tend to be much larger documents,
|
|
114
|
+
// so they're more expensive to preprocess with the FrameBlobBuilder.
|
|
109
115
|
});
|
|
110
116
|
|
|
111
117
|
// Check if base URL of publication has changed
|
|
112
118
|
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
113
119
|
// Revoke all blobs
|
|
114
|
-
this.blobs.forEach(v =>
|
|
115
|
-
this.injector?.releaseBlobUrl?.(v);
|
|
116
|
-
URL.revokeObjectURL(v);
|
|
117
|
-
});
|
|
120
|
+
this.blobs.forEach(v => v.reset());
|
|
118
121
|
this.blobs.clear();
|
|
119
122
|
}
|
|
120
123
|
this.currentBaseURL = pub.baseURL;
|
|
@@ -127,18 +130,14 @@ export class FramePoolManager {
|
|
|
127
130
|
// when navigating backwards, where paginated will go the
|
|
128
131
|
// start of the resource instead of the end due to the
|
|
129
132
|
// corrupted width ColumnSnapper (injectables) gets on init
|
|
130
|
-
this.blobs.forEach(v =>
|
|
131
|
-
this.injector?.releaseBlobUrl?.(v);
|
|
132
|
-
URL.revokeObjectURL(v);
|
|
133
|
-
});
|
|
133
|
+
this.blobs.forEach(v => v.reset());
|
|
134
134
|
this.blobs.clear();
|
|
135
135
|
this.pendingUpdates.clear();
|
|
136
136
|
}
|
|
137
137
|
if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
|
|
138
|
-
const
|
|
139
|
-
if(
|
|
140
|
-
|
|
141
|
-
URL.revokeObjectURL(url);
|
|
138
|
+
const v = this.blobs.get(href);
|
|
139
|
+
if(v) {
|
|
140
|
+
v.reset();
|
|
142
141
|
this.blobs.delete(href);
|
|
143
142
|
this.pendingUpdates.delete(href);
|
|
144
143
|
}
|
|
@@ -157,7 +156,7 @@ export class FramePoolManager {
|
|
|
157
156
|
const itm = pub.readingOrder.findWithHref(href);
|
|
158
157
|
if(!itm) return; // TODO throw?
|
|
159
158
|
if(!this.blobs.has(href)) {
|
|
160
|
-
|
|
159
|
+
this.blobs.set(href, new FrameBlobBuilder(
|
|
161
160
|
pub,
|
|
162
161
|
this.currentBaseURL || "",
|
|
163
162
|
itm,
|
|
@@ -165,36 +164,81 @@ export class FramePoolManager {
|
|
|
165
164
|
cssProperties: this.currentCssProperties,
|
|
166
165
|
injector: this.injector
|
|
167
166
|
}
|
|
168
|
-
);
|
|
169
|
-
const blobURL = await blobBuilder.build();
|
|
170
|
-
this.blobs.set(href, blobURL);
|
|
167
|
+
));
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
// Create <iframe>
|
|
174
|
-
const fm = new FrameManager(this.blobs.get(href)
|
|
171
|
+
const fm = new FrameManager(await this.blobs.get(href)!.build(), this.contentProtectionConfig, this.keyboardPeripheralsConfig);
|
|
175
172
|
if(href !== newHref) await fm.hide(); // Avoid unecessary hide
|
|
176
173
|
this.container.appendChild(fm.iframe);
|
|
177
174
|
await fm.load(modules);
|
|
178
175
|
this.pool.set(href, fm);
|
|
176
|
+
return fm;
|
|
179
177
|
}
|
|
178
|
+
|
|
179
|
+
// Target frame is awaited
|
|
180
180
|
try {
|
|
181
|
-
await Promise.all(creation.map(href => creator(href)));
|
|
181
|
+
await Promise.all(creation.filter(href => href === newHref).map(href => creator(href)));
|
|
182
182
|
} catch (error) {
|
|
183
183
|
reject(error);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (updateSequence !== this.updateSequence) {
|
|
188
|
+
resolve();
|
|
189
|
+
return;
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
// Remaining frames can resolve later
|
|
193
|
+
Promise.all(creation.map(async href => {
|
|
194
|
+
const c = creator(href);
|
|
195
|
+
this.inprogress.set(href, c);
|
|
196
|
+
await c;
|
|
197
|
+
this.inprogress.delete(href);
|
|
198
|
+
}));
|
|
199
|
+
|
|
186
200
|
// Update current frame
|
|
187
|
-
const newFrame = this.pool.get(newHref)
|
|
201
|
+
const newFrame = this.pool.get(newHref);
|
|
202
|
+
if (!newFrame) {
|
|
203
|
+
resolve();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
188
207
|
if(newFrame?.source !== this._currentFrame?.source || force) {
|
|
189
208
|
await this._currentFrame?.hide(); // Hide current frame. It's possible it no longer even exists in the DOM at this point
|
|
190
|
-
|
|
191
|
-
|
|
209
|
+
|
|
210
|
+
// Resolve if there is a concurrent update that has started since this one began
|
|
211
|
+
if (updateSequence !== this.updateSequence) {
|
|
212
|
+
resolve();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// If user is speeding through the publication, this can get destroyed
|
|
217
|
+
const currentFrame = this.pool.get(newHref);
|
|
218
|
+
if (!currentFrame) {
|
|
219
|
+
resolve();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await currentFrame.load(modules); // In order to ensure modules match the latest configuration
|
|
224
|
+
|
|
225
|
+
// Resolve if there is a concurrent update that has started since this one began
|
|
226
|
+
if (updateSequence !== this.updateSequence) {
|
|
227
|
+
resolve();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Check if it was destroyed, again
|
|
232
|
+
const latestFrame = this.pool.get(newHref);
|
|
233
|
+
if (!latestFrame) {
|
|
234
|
+
resolve();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
192
237
|
|
|
193
238
|
// Update progression if necessary and show the new frame
|
|
194
|
-
|
|
195
|
-
await newFrame.show(locator.locations.progression); // Show/activate new frame
|
|
239
|
+
await latestFrame.show(locator.locations.progression); // Show/activate new frame
|
|
196
240
|
|
|
197
|
-
this._currentFrame =
|
|
241
|
+
this._currentFrame = latestFrame;
|
|
198
242
|
|
|
199
243
|
// Safari retains focus on the hidden iframe; transfer it to the new
|
|
200
244
|
// frame only if no meaningful element in the parent document owns focus.
|