@readium/navigator 2.1.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/index.js +3912 -2721
  2. package/dist/index.umd.cjs +286 -71
  3. package/package.json +1 -1
  4. package/src/css/Properties.ts +47 -0
  5. package/src/css/index.ts +1 -0
  6. package/src/epub/css/Properties.ts +10 -48
  7. package/src/epub/preferences/EpubDefaults.ts +1 -1
  8. package/src/epub/preferences/EpubPreferences.ts +1 -1
  9. package/src/epub/preferences/EpubPreferencesEditor.ts +30 -23
  10. package/src/index.ts +3 -1
  11. package/src/preferences/Types.ts +40 -0
  12. package/src/{epub/preferences → preferences}/guards.ts +5 -6
  13. package/src/preferences/index.ts +2 -1
  14. package/src/webpub/WebPubBlobBuilder.ts +167 -0
  15. package/src/webpub/WebPubFrameManager.ts +156 -0
  16. package/src/webpub/WebPubFramePoolManager.ts +221 -0
  17. package/src/webpub/WebPubNavigator.ts +494 -0
  18. package/src/webpub/css/Properties.ts +71 -0
  19. package/src/webpub/css/WebPubCSS.ts +42 -0
  20. package/src/webpub/css/WebPubStylesheet.ts +204 -0
  21. package/src/webpub/css/index.ts +3 -0
  22. package/src/webpub/index.ts +6 -0
  23. package/src/webpub/preferences/WebPubDefaults.ts +61 -0
  24. package/src/webpub/preferences/WebPubPreferences.ts +88 -0
  25. package/src/webpub/preferences/WebPubPreferencesEditor.ts +193 -0
  26. package/src/webpub/preferences/WebPubSettings.ts +88 -0
  27. package/src/webpub/preferences/index.ts +4 -0
  28. package/types/src/css/Properties.d.ts +20 -0
  29. package/types/src/css/index.d.ts +1 -0
  30. package/types/src/epub/css/Properties.d.ts +1 -21
  31. package/types/src/index.d.ts +2 -0
  32. package/types/src/preferences/Types.d.ts +8 -0
  33. package/types/src/preferences/guards.d.ts +9 -0
  34. package/types/src/preferences/index.d.ts +1 -0
  35. package/types/src/web/WebPubBlobBuilder.d.ts +10 -0
  36. package/types/src/web/WebPubFrameManager.d.ts +20 -0
  37. package/types/src/web/WebPubNavigator.d.ts +48 -0
  38. package/types/src/web/index.d.ts +3 -0
  39. package/types/src/webpub/WebPubBlobBuilder.d.ts +16 -0
  40. package/types/src/webpub/WebPubFrameManager.d.ts +24 -0
  41. package/types/src/webpub/WebPubFramePoolManager.d.ts +23 -0
  42. package/types/src/webpub/WebPubNavigator.d.ts +70 -0
  43. package/types/src/webpub/css/Properties.d.ts +36 -0
  44. package/types/src/webpub/css/WebPubCSS.d.ts +10 -0
  45. package/types/src/webpub/css/WebPubStylesheet.d.ts +1 -0
  46. package/types/src/webpub/css/index.d.ts +3 -0
  47. package/types/src/webpub/index.d.ts +6 -0
  48. package/types/src/webpub/preferences/WebPubDefaults.d.ts +32 -0
  49. package/types/src/webpub/preferences/WebPubPreferences.d.ts +36 -0
  50. package/types/src/webpub/preferences/WebPubPreferencesEditor.d.ts +27 -0
  51. package/types/src/webpub/preferences/WebPubSettings.d.ts +35 -0
  52. package/types/src/webpub/preferences/index.d.ts +4 -0
@@ -0,0 +1,156 @@
1
+ import { Loader, ModuleName } from "@readium/navigator-html-injectables";
2
+ import { FrameComms } from "../epub/frame/FrameComms";
3
+ import { ReadiumWindow } from "../../../navigator-html-injectables/types/src/helpers/dom";
4
+ import { sML } from "../helpers";
5
+
6
+ export class WebPubFrameManager {
7
+ private frame: HTMLIFrameElement;
8
+ private loader: Loader | undefined;
9
+ public readonly source: string;
10
+ private comms: FrameComms | undefined;
11
+ private hidden: boolean = true;
12
+ private destroyed: boolean = false;
13
+
14
+ private currModules: ModuleName[] = [];
15
+
16
+ constructor(source: string) {
17
+ this.frame = document.createElement("iframe");
18
+ this.frame.classList.add("readium-navigator-iframe");
19
+ this.frame.style.visibility = "hidden";
20
+ this.frame.style.setProperty("aria-hidden", "true");
21
+ this.frame.style.opacity = "0";
22
+ this.frame.style.position = "absolute";
23
+ this.frame.style.pointerEvents = "none";
24
+ this.frame.style.transition = "visibility 0s, opacity 0.1s linear";
25
+ // Protect against background color bleeding
26
+ this.frame.style.backgroundColor = "#FFFFFF";
27
+ this.source = source;
28
+ }
29
+
30
+ async load(modules: ModuleName[] = []): Promise<Window> {
31
+ return new Promise((res, rej) => {
32
+ if(this.loader) {
33
+ const wnd = this.frame.contentWindow!;
34
+ // Check if currently loaded modules are equal
35
+ if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
36
+ try { res(wnd); } catch (error) {};
37
+ return;
38
+ }
39
+ this.comms?.halt();
40
+ this.loader.destroy();
41
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
42
+ this.currModules = modules;
43
+ this.comms = undefined;
44
+ try { res(wnd); } catch (error) {}
45
+ return;
46
+ }
47
+ this.frame.onload = () => {
48
+ const wnd = this.frame.contentWindow!;
49
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
50
+ this.currModules = modules;
51
+ try { res(wnd); } catch (error) {}
52
+ };
53
+ this.frame.onerror = (err) => {
54
+ try { rej(err); } catch (error) {}
55
+ }
56
+ this.frame.contentWindow!.location.replace(this.source);
57
+ });
58
+ }
59
+
60
+ async destroy() {
61
+ await this.hide();
62
+ this.loader?.destroy();
63
+ this.frame.remove();
64
+ this.destroyed = true;
65
+ }
66
+
67
+ async hide(): Promise<void> {
68
+ if(this.destroyed) return;
69
+ this.frame.style.visibility = "hidden";
70
+ this.frame.style.setProperty("aria-hidden", "true");
71
+ this.frame.style.opacity = "0";
72
+ this.frame.style.pointerEvents = "none";
73
+ this.hidden = true;
74
+
75
+ if(this.frame.parentElement) {
76
+ if(this.comms === undefined || !this.comms.ready) return;
77
+ return new Promise((res, _) => {
78
+ this.comms?.send("unfocus", undefined, (_: boolean) => {
79
+ this.comms?.halt();
80
+ res();
81
+ });
82
+ });
83
+ } else {
84
+ this.comms?.halt();
85
+ }
86
+ }
87
+
88
+ async show(atProgress?: number): Promise<void> {
89
+ if (this.destroyed) throw Error("Trying to show frame when it doesn't exist");
90
+ if (!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
91
+ if (this.comms) this.comms.resume();
92
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
93
+
94
+ return new Promise((res, _) => {
95
+ this.comms?.send("activate", undefined, () => {
96
+ this.comms?.send("focus", undefined, () => {
97
+ const remove = () => {
98
+ this.frame.style.removeProperty("visibility");
99
+ this.frame.style.removeProperty("aria-hidden");
100
+ this.frame.style.removeProperty("opacity");
101
+ this.frame.style.removeProperty("pointer-events");
102
+ this.hidden = false;
103
+
104
+ if (sML.UA.WebKit) {
105
+ this.comms?.send("force_webkit_recalc", undefined);
106
+ }
107
+
108
+ res();
109
+ }
110
+
111
+ if (atProgress !== undefined) {
112
+ this.comms?.send("go_progression", atProgress, remove);
113
+ } else {
114
+ remove();
115
+ }
116
+ });
117
+ });
118
+ });
119
+ }
120
+
121
+ setCSSProperties(properties: { [key: string]: string }) {
122
+ if(this.destroyed || !this.frame.contentWindow) return;
123
+
124
+ // We need to resume and halt postMessage to update the properties
125
+ // if the frame is hidden since it's been halted in hide()
126
+ if (this.hidden) {
127
+ if (this.comms) this.comms?.resume();
128
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
129
+ }
130
+ this.comms?.send("update_properties", properties);
131
+ if (this.hidden) this.comms?.halt();
132
+ }
133
+
134
+ get iframe() {
135
+ if(this.destroyed) throw Error("Trying to use frame when it doesn't exist");
136
+ return this.frame;
137
+ }
138
+
139
+ get realSize() {
140
+ if(this.destroyed) throw Error("Trying to use frame client rect when it doesn't exist");
141
+ return this.frame.getBoundingClientRect();
142
+ }
143
+
144
+ get window() {
145
+ if(this.destroyed || !this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
146
+ return this.frame.contentWindow;
147
+ }
148
+
149
+ get msg() {
150
+ return this.comms;
151
+ }
152
+
153
+ get ldr() {
154
+ return this.loader;
155
+ }
156
+ }
@@ -0,0 +1,221 @@
1
+ import { ModuleName } from "@readium/navigator-html-injectables";
2
+ import { Locator, Publication } from "@readium/shared";
3
+ import { WebPubBlobBuilder } from "./WebPubBlobBuilder";
4
+ import { WebPubFrameManager } from "./WebPubFrameManager";
5
+
6
+ export class WebPubFramePoolManager {
7
+ private readonly container: HTMLElement;
8
+ private _currentFrame: WebPubFrameManager | undefined;
9
+ private currentCssProperties: { [key: string]: string } | undefined;
10
+ private readonly pool: Map<string, WebPubFrameManager> = new Map();
11
+ private readonly blobs: Map<string, string> = new Map();
12
+ private readonly inprogress: Map<string, Promise<void>> = new Map();
13
+ private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
14
+ private currentBaseURL: string | undefined;
15
+
16
+ constructor(container: HTMLElement, cssProperties?: { [key: string]: string }) {
17
+ this.container = container;
18
+ this.currentCssProperties = cssProperties;
19
+ }
20
+
21
+ async destroy() {
22
+ // Wait for all in-progress loads to complete
23
+ let iit = this.inprogress.values();
24
+ let inp = iit.next();
25
+ const inprogressPromises: Promise<void>[] = [];
26
+ while(inp.value) {
27
+ inprogressPromises.push(inp.value);
28
+ inp = iit.next();
29
+ }
30
+ if(inprogressPromises.length > 0) {
31
+ await Promise.allSettled(inprogressPromises);
32
+ }
33
+ this.inprogress.clear();
34
+
35
+ // Destroy all frames
36
+ let fit = this.pool.values();
37
+ let frm = fit.next();
38
+ while(frm.value) {
39
+ await (frm.value as WebPubFrameManager).destroy();
40
+ frm = fit.next();
41
+ }
42
+ this.pool.clear();
43
+
44
+ // Revoke all blobs
45
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
46
+ this.blobs.clear();
47
+
48
+ // Empty container of elements
49
+ this.container.childNodes.forEach(v => {
50
+ if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
51
+ })
52
+ }
53
+
54
+ async update(pub: Publication, locator: Locator, modules: ModuleName[]) {
55
+ const readingOrder = pub.readingOrder.items;
56
+ let i = readingOrder.findIndex(l => l.href === locator.href);
57
+ if(i < 0) throw Error(`Locator not found in reading order: ${locator.href}`);
58
+ const newHref = readingOrder[i].href;
59
+
60
+ if(this.inprogress.has(newHref))
61
+ await this.inprogress.get(newHref);
62
+
63
+ const progressPromise = new Promise<void>(async (resolve, reject) => {
64
+ const disposal: string[] = [];
65
+ const creation: string[] = [];
66
+ pub.readingOrder.items.forEach((l, j) => {
67
+ // Dispose everything except current, previous, and next
68
+ if(j !== i && j !== i - 1 && j !== i + 1) {
69
+ if(!disposal.includes(l.href)) disposal.push(l.href);
70
+ }
71
+
72
+ // CURRENT FRAME: always create the frame we're navigating to
73
+ if(j === i) {
74
+ if(!creation.includes(l.href)) creation.push(l.href);
75
+ }
76
+
77
+ // PREVIOUS/NEXT FRAMES: create adjacent chapters for smooth navigation
78
+ // if((j === i - 1 || j === i + 1) && j >= 0 && j < pub.readingOrder.items.length) {
79
+ // if(!creation.includes(l.href)) creation.push(l.href);
80
+ // }
81
+ });
82
+ disposal.forEach(async href => {
83
+ if(creation.includes(href)) return;
84
+ if(!this.pool.has(href)) return;
85
+ await this.pool.get(href)?.destroy();
86
+ this.pool.delete(href);
87
+ });
88
+
89
+ if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
90
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
91
+ this.blobs.clear();
92
+ }
93
+ this.currentBaseURL = pub.baseURL;
94
+
95
+ const creator = async (href: string) => {
96
+ // Check if blob needs to be recreated due to CSS property changes
97
+ if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
98
+ const url = this.blobs.get(href);
99
+ if(url) {
100
+ URL.revokeObjectURL(url);
101
+ this.blobs.delete(href);
102
+ this.pendingUpdates.delete(href);
103
+ }
104
+ }
105
+
106
+ if(this.pool.has(href)) {
107
+ const fm = this.pool.get(href)!;
108
+ if(!this.blobs.has(href)) {
109
+ await fm.destroy();
110
+ this.pool.delete(href);
111
+ this.pendingUpdates.delete(href);
112
+ } else {
113
+ await fm.load(modules);
114
+ return;
115
+ }
116
+ }
117
+ const itm = pub.readingOrder.findWithHref(href);
118
+ if(!itm) return;
119
+ if(!this.blobs.has(href)) {
120
+ const blobBuilder = new WebPubBlobBuilder(pub, this.currentBaseURL || "", itm, this.currentCssProperties);
121
+ const blobURL = await blobBuilder.build();
122
+ this.blobs.set(href, blobURL);
123
+ }
124
+
125
+ const fm = new WebPubFrameManager(this.blobs.get(href)!);
126
+ if(href !== newHref) await fm.hide();
127
+ this.container.appendChild(fm.iframe);
128
+ await fm.load(modules);
129
+ this.pool.set(href, fm);
130
+ }
131
+ try {
132
+ await Promise.all(creation.map(href => creator(href)));
133
+ } catch (error) {
134
+ reject(error);
135
+ }
136
+
137
+ const newFrame = this.pool.get(newHref)!;
138
+ if(newFrame?.source !== this._currentFrame?.source) {
139
+ await this._currentFrame?.hide();
140
+ if(newFrame)
141
+ await newFrame.load(modules);
142
+
143
+ if(newFrame)
144
+ await newFrame.show(locator.locations.progression);
145
+
146
+ this._currentFrame = newFrame;
147
+ }
148
+ resolve();
149
+ });
150
+
151
+ this.inprogress.set(newHref, progressPromise);
152
+ await progressPromise;
153
+ this.inprogress.delete(newHref);
154
+ }
155
+
156
+ setCSSProperties(properties: { [key: string]: string }) {
157
+ const deepCompare = (obj1: { [key: string]: string }, obj2: { [key: string]: string }) => {
158
+ const keys1 = Object.keys(obj1);
159
+ const keys2 = Object.keys(obj2);
160
+
161
+ if (keys1.length !== keys2.length) {
162
+ return false;
163
+ }
164
+
165
+ for (const key of keys1) {
166
+ if (obj1[key] !== obj2[key]) {
167
+ return false;
168
+ }
169
+ }
170
+
171
+ return true;
172
+ };
173
+
174
+ // If CSSProperties have changed, we update the currentCssProperties,
175
+ // and set the CSS Properties to all frames already in the pool
176
+ // We also need to invalidate the blobs and recreate them with the new properties.
177
+ // We do that in update, by updating them when needed (they are added into the pool)
178
+ // so that we do not invalidate and recreate blobs over and over again.
179
+ if(!deepCompare(this.currentCssProperties || {}, properties)) {
180
+ this.currentCssProperties = properties;
181
+ this.pool.forEach((frame) => {
182
+ frame.setCSSProperties(properties);
183
+ });
184
+ for (const href of this.blobs.keys()) {
185
+ this.pendingUpdates.set(href, { inPool: this.pool.has(href) });
186
+ }
187
+ }
188
+ }
189
+ get currentFrames(): (WebPubFrameManager | undefined)[] {
190
+ return [this._currentFrame];
191
+ }
192
+
193
+ get currentBounds(): DOMRect {
194
+ const ret = {
195
+ x: 0,
196
+ y: 0,
197
+ width: 0,
198
+ height: 0,
199
+ top: 0,
200
+ right: 0,
201
+ bottom: 0,
202
+ left: 0,
203
+ toJSON() {
204
+ return this;
205
+ },
206
+ };
207
+ this.currentFrames.forEach(f => {
208
+ if(!f) return;
209
+ const b = f.realSize;
210
+ ret.x = Math.min(ret.x, b.x);
211
+ ret.y = Math.min(ret.y, b.y);
212
+ ret.width += b.width;
213
+ ret.height = Math.max(ret.height, b.height);
214
+ ret.top = Math.min(ret.top, b.top);
215
+ ret.right = Math.min(ret.right, b.right);
216
+ ret.bottom = Math.min(ret.bottom, b.bottom);
217
+ ret.left = Math.min(ret.left, b.left);
218
+ });
219
+ return ret as DOMRect;
220
+ }
221
+ }