@readium/navigator 1.2.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.
Files changed (49) hide show
  1. package/LICENSE +28 -0
  2. package/README.MD +11 -0
  3. package/dist/assets/AccessibleDfA.otf +0 -0
  4. package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
  5. package/dist/index.js +6263 -0
  6. package/dist/index.umd.cjs +107 -0
  7. package/package.json +65 -0
  8. package/src/Navigator.ts +66 -0
  9. package/src/audio/engine/AudioEngine.ts +136 -0
  10. package/src/audio/engine/WebAudioEngine.ts +286 -0
  11. package/src/audio/engine/index.ts +2 -0
  12. package/src/audio/index.ts +1 -0
  13. package/src/epub/EpubNavigator.ts +507 -0
  14. package/src/epub/frame/FrameBlobBuilder.ts +211 -0
  15. package/src/epub/frame/FrameComms.ts +142 -0
  16. package/src/epub/frame/FrameManager.ts +134 -0
  17. package/src/epub/frame/FramePoolManager.ts +179 -0
  18. package/src/epub/frame/index.ts +3 -0
  19. package/src/epub/fxl/FXLCoordinator.ts +152 -0
  20. package/src/epub/fxl/FXLFrameManager.ts +286 -0
  21. package/src/epub/fxl/FXLFramePoolManager.ts +632 -0
  22. package/src/epub/fxl/FXLPeripherals.ts +587 -0
  23. package/src/epub/fxl/FXLPeripheralsDebug.ts +46 -0
  24. package/src/epub/fxl/FXLSpreader.ts +95 -0
  25. package/src/epub/fxl/index.ts +5 -0
  26. package/src/epub/index.ts +3 -0
  27. package/src/helpers/sML.ts +120 -0
  28. package/src/index.ts +3 -0
  29. package/types/src/Navigator.d.ts +41 -0
  30. package/types/src/audio/engine/AudioEngine.d.ts +114 -0
  31. package/types/src/audio/engine/WebAudioEngine.d.ts +107 -0
  32. package/types/src/audio/engine/index.d.ts +2 -0
  33. package/types/src/audio/index.d.ts +1 -0
  34. package/types/src/epub/EpubNavigator.d.ts +66 -0
  35. package/types/src/epub/frame/FrameBlobBuilder.d.ts +13 -0
  36. package/types/src/epub/frame/FrameComms.d.ts +26 -0
  37. package/types/src/epub/frame/FrameManager.d.ts +21 -0
  38. package/types/src/epub/frame/FramePoolManager.d.ts +17 -0
  39. package/types/src/epub/frame/index.d.ts +3 -0
  40. package/types/src/epub/fxl/FXLCoordinator.d.ts +37 -0
  41. package/types/src/epub/fxl/FXLFrameManager.d.ts +41 -0
  42. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +93 -0
  43. package/types/src/epub/fxl/FXLPeripherals.d.ts +97 -0
  44. package/types/src/epub/fxl/FXLPeripheralsDebug.d.ts +13 -0
  45. package/types/src/epub/fxl/FXLSpreader.d.ts +12 -0
  46. package/types/src/epub/fxl/index.d.ts +5 -0
  47. package/types/src/epub/index.d.ts +3 -0
  48. package/types/src/helpers/sML.d.ts +51 -0
  49. package/types/src/index.d.ts +3 -0
@@ -0,0 +1,3 @@
1
+ export * from "./FrameComms";
2
+ export * from "./FrameManager";
3
+ export * from "./FramePoolManager";
@@ -0,0 +1,152 @@
1
+ import sML from "../../helpers/sML";
2
+
3
+ export interface Point {
4
+ X: number;
5
+ Y: number;
6
+ }
7
+
8
+ export enum HorizontalThird {
9
+ Left,
10
+ Center,
11
+ Right
12
+ }
13
+
14
+ export enum VerticalThird {
15
+ Top,
16
+ Middle,
17
+ Bottom
18
+ }
19
+
20
+ export interface NinthPoint {
21
+ X: HorizontalThird | null;
22
+ Y: VerticalThird | null;
23
+ }
24
+
25
+ export interface BibiEvent {
26
+ Target: EventTarget | null;
27
+ Coord: Point | null;
28
+ Ratio: Point | null;
29
+ Division: NinthPoint | null;
30
+ }
31
+
32
+ export class FXLCoordinator {
33
+ HTML: HTMLElement;
34
+ Head: HTMLHeadElement;
35
+ Body: HTMLElement;
36
+
37
+ constructor() {
38
+ this.HTML = document.documentElement;
39
+ this.Head = document.head;
40
+ this.Body = document.body;
41
+ }
42
+
43
+ /*
44
+ getElementCoord(El: any) {
45
+ var Coord = { X: El["offsetLeft"], Y: El["offsetTop"] };
46
+ while(El.offsetParent) El = El.offsetParent, Coord.X += El["offsetLeft"], Coord.Y += El["offsetTop"];
47
+ return Coord;
48
+ }
49
+ */
50
+
51
+ private outerWidth = 0;
52
+ private outerHeight = 0;
53
+ refreshOuterPixels(_: DOMRect) {
54
+ if(sML.OS.iOS) return; // No need on iOS
55
+ this.outerHeight = window.outerHeight - window.innerHeight;
56
+ if(sML.OS.Android && sML.UA.Chrome) {
57
+ if(window.screen.height > window.innerHeight)
58
+ // This is a hack: since outer/inner are zero, we assume there's a
59
+ // top (chrome url bar) and bottom (android controls) bar and divide
60
+ // by 1.5 because the top bar is roughtly 2x height of the bottom one
61
+ this.outerHeight = (window.screen.height - window.innerHeight) / 1.5;
62
+ }
63
+ this.outerWidth = window.outerWidth - window.innerWidth;
64
+ }
65
+
66
+ getBibiEventCoord(Eve: TouchEvent | MouseEvent, touch=0): Point {
67
+ const Coord: Point = { X:0, Y:0 };
68
+ if(/^touch/.test(Eve.type)) {
69
+ Coord.X = (Eve as TouchEvent).touches[touch].screenX;
70
+ Coord.Y = (Eve as TouchEvent).touches[touch].screenY;
71
+ } else {
72
+ Coord.X = (Eve as MouseEvent).screenX;
73
+ Coord.Y = (Eve as MouseEvent).screenY;
74
+ }
75
+ if(((Eve.target as HTMLElement).ownerDocument?.documentElement || (Eve.target as HTMLDocument).documentElement) === this.HTML) {
76
+ Coord.X -= (this.HTML.scrollLeft + this.Body.scrollLeft);
77
+ Coord.Y -= (this.HTML.scrollTop + this.Body.scrollTop);
78
+ } else {
79
+ /*
80
+ var Item = Eve.target.ownerDocument.documentElement.Item;
81
+ ItemCoord = this.getElementCoord(Item);
82
+ if(!Item.PrePaginated && !Item.Outsourcing) ItemCoord.X += settings.S["item-padding-left"], ItemCoord.Y += settings.S["item-padding-top"];
83
+ Coord.X = (Coord.X + ItemCoord.X - R.Main.scrollLeft) * R.Main.Transformation.Scale + R.Main.Transformation.Translation.X;
84
+ Coord.Y = (Coord.Y + ItemCoord.Y - R.Main.scrollTop ) * R.Main.Transformation.Scale + R.Main.Transformation.Translation.Y;
85
+ */
86
+ }
87
+ Coord.X -= this.outerWidth;
88
+ Coord.Y -= this.outerHeight;
89
+ return Coord;
90
+ }
91
+
92
+ getTouchDistance(Eve: TouchEvent) {
93
+ if (Eve.touches.length !== 2) return 0;
94
+ const x1 = Eve.touches[0].screenX - this.outerWidth;
95
+ const y1 = Eve.touches[0].screenY - this.outerHeight;
96
+ const x2 = Eve.touches[1].screenX - this.outerWidth;
97
+ const y2 = Eve.touches[1].screenY - this.outerHeight;
98
+ return Math.sqrt((Math.pow((x2 - x1), 2)) + (Math.pow((y2 - y1), 2)));
99
+ }
100
+
101
+ getTouchCenter(Eve: TouchEvent): Point | null {
102
+ if (Eve.touches.length !== 2) return null;
103
+ const subL = (this.HTML.scrollLeft + this.Body.scrollLeft);
104
+ const subT = (this.HTML.scrollTop + this.Body.scrollTop);
105
+ const x1 = Eve.touches[0].screenX - this.outerWidth - subL;
106
+ const y1 = Eve.touches[0].screenY - this.outerHeight - subT;
107
+ const x2 = Eve.touches[1].screenX - this.outerWidth - subL;
108
+ const y2 = Eve.touches[1].screenY - this.outerHeight - subT;
109
+ return { X: (x1 + x2) / 2, Y: (y1 + y2) / 2 };
110
+ }
111
+
112
+ getBibiEvent(Eve: Event): BibiEvent {
113
+ if(!Eve) return {
114
+ Coord: null,
115
+ Division: null,
116
+ Ratio: null,
117
+ Target: null
118
+ };
119
+ const Coord = this.getBibiEventCoord(Eve as TouchEvent | MouseEvent);
120
+ let FlipperWidth = 0.3; // TODO flipper-width
121
+ const Ratio = {
122
+ X: Coord.X / window.innerWidth,
123
+ Y: Coord.Y / window.innerHeight
124
+ };
125
+ let BorderT, BorderB, BorderL, BorderR;
126
+ if(FlipperWidth < 1) { // Ratio
127
+ BorderL = BorderT = FlipperWidth;
128
+ BorderR = BorderB = 1 - FlipperWidth;
129
+ } else { // Pixel to Ratio
130
+ BorderL = FlipperWidth / window.innerWidth;
131
+ BorderT = FlipperWidth / window.innerHeight;
132
+ BorderR = 1 - BorderL;
133
+ BorderB = 1 - BorderT;
134
+ }
135
+ const Division: NinthPoint = {
136
+ X: null,
137
+ Y: null
138
+ };
139
+ if(Ratio.X < BorderL) Division.X = HorizontalThird.Left;
140
+ else if(BorderR < Ratio.X) Division.X = HorizontalThird.Right;
141
+ else Division.X = HorizontalThird.Center;
142
+ if(Ratio.Y < BorderT) Division.Y = VerticalThird.Top;
143
+ else if(BorderB < Ratio.Y) Division.Y = VerticalThird.Bottom;
144
+ else Division.Y = VerticalThird.Middle;
145
+ return {
146
+ Target: Eve.target,
147
+ Coord: Coord,
148
+ Ratio: Ratio,
149
+ Division: Division
150
+ };
151
+ }
152
+ }
@@ -0,0 +1,286 @@
1
+ import { Loader, ModuleName } from "@readium/navigator-html-injectables";
2
+ import { Page, ReadingProgression } from "@readium/shared";
3
+ import { FrameComms } from "../frame/FrameComms";
4
+ import { FXLPeripherals } from "./FXLPeripherals";
5
+ import { ReadiumWindow } from "../../../../navigator-html-injectables/types/src/helpers/dom";
6
+
7
+ export class FXLFrameManager {
8
+ private frame: HTMLIFrameElement;
9
+ private loader: Loader | undefined;
10
+ public source: string;
11
+ private comms: FrameComms | undefined;
12
+ private readonly peripherals: FXLPeripherals;
13
+
14
+ private currModules: ModuleName[] = [];
15
+
16
+ // NEW
17
+ public wrapper: HTMLDivElement;
18
+ public debugHref: string;
19
+ private loadPromise: Promise<Window> | undefined;
20
+ private showPromise: Promise<void> | undefined;
21
+
22
+ constructor(peripherals: FXLPeripherals, direction: ReadingProgression, debugHref: string) {
23
+ this.peripherals = peripherals;
24
+ this.debugHref = debugHref;
25
+ this.frame = document.createElement("iframe");
26
+ this.frame.classList.add("readium-navigator-iframe");
27
+ this.frame.classList.add("blank");
28
+ this.frame.scrolling = "no";
29
+ this.frame.style.visibility = "hidden";
30
+ this.frame.style.setProperty("aria-hidden", "true");
31
+ this.frame.style.display = "none";
32
+ this.frame.style.position = "absolute";
33
+ this.frame.style.pointerEvents = "none";
34
+ this.frame.style.transformOrigin = "0 0";
35
+ this.frame.style.transform = "scale(1)";
36
+ this.frame.style.background = "#fff";
37
+ this.frame.style.touchAction = "none";
38
+ this.frame.dataset.originalHref = debugHref;
39
+ this.source = "about:blank";
40
+
41
+ // NEW
42
+ this.wrapper = document.createElement("div");
43
+ this.wrapper.style.position = "relative";
44
+ this.wrapper.style.float = this.wrapper.style.cssFloat = direction === ReadingProgression.rtl ? "right" : "left";
45
+
46
+ this.wrapper.appendChild(this.frame);
47
+ }
48
+
49
+ async load(modules: ModuleName[], source: string): Promise<Window> {
50
+ if(this.source === source && this.loadPromise/* && this.loaded*/) {
51
+ if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
52
+ return this.loadPromise;
53
+ }
54
+ }
55
+ if(this.loaded && this.source !== source) {
56
+ this.window.stop();
57
+ }
58
+ this.source = source;
59
+ this.loadPromise = new Promise((res, rej) => {
60
+ if(this.loader && this.loaded) {
61
+ const wnd = this.frame.contentWindow!;
62
+ // Check if currently loaded modules are equal
63
+ if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
64
+ try { res(wnd); this.loadPromise = undefined; } catch (error) { };
65
+ return;
66
+ }
67
+ // TODO
68
+ this.comms?.halt();
69
+ this.loader.destroy();
70
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
71
+ this.currModules = modules;
72
+ this.comms = undefined;
73
+ try { res(wnd); this.loadPromise = undefined; } catch (error) {}
74
+ return;
75
+ }
76
+ this.frame.addEventListener("load", () => {
77
+ const wnd = this.frame.contentWindow!;
78
+ this.loader = new Loader(wnd as ReadiumWindow, modules);
79
+ this.currModules = modules;
80
+ this.peripherals.observe(this.wrapper);
81
+ this.peripherals.observe(wnd);
82
+ try { res(wnd); } catch (error) {};
83
+ }, { once: true });
84
+ this.frame.addEventListener("error", (e) => {
85
+ try { rej(e.error); this.loadPromise = undefined; } catch (error) {};
86
+ }, { once: true });
87
+ this.frame.style.removeProperty("display");
88
+ this.frame.contentWindow!.location.replace(this.source);
89
+ });
90
+ return this.loadPromise;
91
+ }
92
+
93
+ // Parses the page size from the viewport meta tag of the loaded resource.
94
+ loadPageSize(): { width: number, height: number } {
95
+ const wnd = this.frame.contentWindow!;
96
+
97
+ // Try to get the page size from the viewport meta tag
98
+ const viewport = wnd.document.head.querySelector(
99
+ "meta[name=viewport]"
100
+ ) as HTMLMetaElement;
101
+ if (viewport) {
102
+ const regex = /(\w+) *= *([^\s,]+)/g;
103
+ let match;
104
+ let width = 0, height = 0;
105
+ while ((match = regex.exec(viewport.content))) {
106
+ if(match[1] === "width")
107
+ width = Number.parseFloat(match[2]);
108
+ else if(match[1] === "height")
109
+ height = Number.parseFloat(match[2]);
110
+ }
111
+ if(width > 0 && height > 0)
112
+ return { width, height };
113
+ }
114
+
115
+ // Otherwise get it from the size of the loaded content
116
+ return {
117
+ width: wnd.document.body.scrollWidth,
118
+ height: wnd.document.body.scrollHeight
119
+ }
120
+ }
121
+
122
+ update(page?: Page) {
123
+ if(!this.loaded) return;
124
+ const dimensions = this.loadPageSize();
125
+ this.frame.style.height = `${dimensions.height}px`;
126
+ this.frame.style.width = `${dimensions.width}px`;
127
+ const ratio = Math.min(this.wrapper.clientWidth / dimensions.width, this.wrapper.clientHeight / dimensions.height);
128
+ this.frame.style.transform = `scale(${ratio})`;
129
+ const bcr = this.frame.getBoundingClientRect();
130
+ const hdiff = this.wrapper.clientHeight - bcr.height;
131
+ this.frame.style.top = `${hdiff / 2}px`;
132
+ if(page === Page.left) {
133
+ const wdiff = this.wrapper.clientWidth - bcr.width;
134
+ this.frame.style.left = `${wdiff}px`;
135
+ } else if(page === Page.center) {
136
+ const wdiff = this.wrapper.clientWidth - bcr.width;
137
+ this.frame.style.left = `${wdiff / 2}px`;
138
+ } else {
139
+ this.frame.style.left = "0px";
140
+ }
141
+
142
+ this.frame.style.removeProperty("visibility");
143
+ this.frame.style.removeProperty("aria-hidden");
144
+ this.frame.style.removeProperty("pointer-events");
145
+ this.frame.classList.remove("blank");
146
+ this.frame.classList.add("loaded");
147
+ }
148
+
149
+ async destroy() {
150
+ await this.unfocus();
151
+ this.loader?.destroy();
152
+ this.wrapper.remove();
153
+ }
154
+
155
+ async unload() {
156
+ if(!this.loaded) return;
157
+ this.deselect();
158
+ this.frame.style.visibility = "hidden";
159
+ this.frame.style.setProperty("aria-hidden", "true");
160
+ this.frame.style.pointerEvents = "none";
161
+ this.frame.classList.add("blank");
162
+ this.frame.classList.remove("loaded");
163
+ this.comms?.halt();
164
+ this.loader?.destroy();
165
+ this.comms = undefined;
166
+ this.frame.blur();
167
+ return new Promise<void>((res, rej) => {
168
+ this.frame.addEventListener("load", () => {
169
+ try { this.showPromise = undefined; res(); } catch (error) {};
170
+ }, { once: true });
171
+ this.frame.addEventListener("error", (e) => {
172
+ try { this.showPromise = undefined; rej(e.error); } catch (error) {};
173
+ }, { once: true });
174
+ this.source = "about:blank";
175
+ this.frame.contentWindow!.location.replace("about:blank");
176
+ this.frame.style.display = "none";
177
+ });
178
+ }
179
+
180
+ deselect() {
181
+ this.frame.contentWindow?.getSelection()?.removeAllRanges();
182
+ }
183
+
184
+ async unfocus(): Promise<void> {
185
+ if(this.frame.parentElement) {
186
+ if(this.comms === undefined) return;
187
+ return new Promise((res, _) => {
188
+ this.comms?.send("unfocus", undefined, (_: boolean) => {
189
+ this.comms?.halt();
190
+ this.showPromise = undefined;
191
+ res();
192
+ });
193
+ });
194
+ } else
195
+ this.comms?.halt();
196
+ }
197
+
198
+ private cachedPage: Page | undefined = undefined;
199
+ async show(page: Page): Promise<void> {
200
+ if(!this.frame.parentElement) {
201
+ console.warn("Trying to show frame that is not attached to the DOM");
202
+ return;
203
+ }
204
+ if(!this.loaded) {
205
+ this.showPromise = undefined;
206
+ return;
207
+ }
208
+ if(this.showPromise) {
209
+ if(this.cachedPage !== page) {
210
+ this.update(page); // TODO fix that this can theoretically happen before the page is fully loaded
211
+ this.cachedPage = page;
212
+ }
213
+ return this.showPromise;
214
+ };
215
+ // this.update(page);
216
+ this.cachedPage = page;
217
+ if(this.comms) this.comms.resume();
218
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
219
+ this.showPromise = new Promise<void>((res, _) => {
220
+ this.comms!.send("focus", undefined, (_: boolean) => {
221
+ // this.showPromise = undefined; Don't do this
222
+ this.update(this.cachedPage);
223
+ res();
224
+ });
225
+ });
226
+ return this.showPromise;
227
+ }
228
+
229
+ async activate(): Promise<void> {
230
+ return new Promise<void>((res, _) => {
231
+ if(!this.comms) return res(); // TODO: investigate when this is the case
232
+ this.comms?.send("activate", undefined, () => {
233
+ res();
234
+ });
235
+ });
236
+ }
237
+
238
+ get element() {
239
+ return this.wrapper;
240
+ }
241
+
242
+ get iframe() {
243
+ return this.frame;
244
+ }
245
+
246
+ get realSize() {
247
+ return this.frame.getBoundingClientRect();
248
+ }
249
+
250
+ get loaded() {
251
+ return this.frame.contentWindow && this.frame.contentWindow.location.href !== "about:blank";
252
+ }
253
+
254
+ set width(width: number) {
255
+ const newWidth = `${width}%`;
256
+ if(this.wrapper.style.width === newWidth) return;
257
+ this.wrapper.style.width = newWidth;
258
+ }
259
+
260
+ set height(height: number) {
261
+ const newHeight = `${height}px`;
262
+ if(this.wrapper.style.height === newHeight) return;
263
+ this.wrapper.style.height = newHeight;
264
+ }
265
+
266
+ get window() {
267
+ if(!this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
268
+ return this.frame.contentWindow;
269
+ }
270
+
271
+ get atLeft() {
272
+ return this.window.scrollX < 5;
273
+ }
274
+
275
+ get atRight() {
276
+ return this.window.scrollX > this.window.document.scrollingElement!.scrollWidth - this.window.innerWidth - 5
277
+ }
278
+
279
+ get msg() {
280
+ return this.comms;
281
+ }
282
+
283
+ get ldr() {
284
+ return this.loader;
285
+ }
286
+ }