@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.
- package/LICENSE +28 -0
- package/README.MD +11 -0
- package/dist/assets/AccessibleDfA.otf +0 -0
- package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
- package/dist/index.js +6263 -0
- package/dist/index.umd.cjs +107 -0
- package/package.json +65 -0
- package/src/Navigator.ts +66 -0
- package/src/audio/engine/AudioEngine.ts +136 -0
- package/src/audio/engine/WebAudioEngine.ts +286 -0
- package/src/audio/engine/index.ts +2 -0
- package/src/audio/index.ts +1 -0
- package/src/epub/EpubNavigator.ts +507 -0
- package/src/epub/frame/FrameBlobBuilder.ts +211 -0
- package/src/epub/frame/FrameComms.ts +142 -0
- package/src/epub/frame/FrameManager.ts +134 -0
- package/src/epub/frame/FramePoolManager.ts +179 -0
- package/src/epub/frame/index.ts +3 -0
- package/src/epub/fxl/FXLCoordinator.ts +152 -0
- package/src/epub/fxl/FXLFrameManager.ts +286 -0
- package/src/epub/fxl/FXLFramePoolManager.ts +632 -0
- package/src/epub/fxl/FXLPeripherals.ts +587 -0
- package/src/epub/fxl/FXLPeripheralsDebug.ts +46 -0
- package/src/epub/fxl/FXLSpreader.ts +95 -0
- package/src/epub/fxl/index.ts +5 -0
- package/src/epub/index.ts +3 -0
- package/src/helpers/sML.ts +120 -0
- package/src/index.ts +3 -0
- package/types/src/Navigator.d.ts +41 -0
- package/types/src/audio/engine/AudioEngine.d.ts +114 -0
- package/types/src/audio/engine/WebAudioEngine.d.ts +107 -0
- package/types/src/audio/engine/index.d.ts +2 -0
- package/types/src/audio/index.d.ts +1 -0
- package/types/src/epub/EpubNavigator.d.ts +66 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +13 -0
- package/types/src/epub/frame/FrameComms.d.ts +26 -0
- package/types/src/epub/frame/FrameManager.d.ts +21 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +17 -0
- package/types/src/epub/frame/index.d.ts +3 -0
- package/types/src/epub/fxl/FXLCoordinator.d.ts +37 -0
- package/types/src/epub/fxl/FXLFrameManager.d.ts +41 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +93 -0
- package/types/src/epub/fxl/FXLPeripherals.d.ts +97 -0
- package/types/src/epub/fxl/FXLPeripheralsDebug.d.ts +13 -0
- package/types/src/epub/fxl/FXLSpreader.d.ts +12 -0
- package/types/src/epub/fxl/index.d.ts +5 -0
- package/types/src/epub/index.d.ts +3 -0
- package/types/src/helpers/sML.d.ts +51 -0
- package/types/src/index.d.ts +3 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { EPUBLayout, Link, Locator, Publication, ReadingProgression } from "@readium/shared";
|
|
2
|
+
import { VisualNavigator } from "../";
|
|
3
|
+
import { FramePoolManager } from "./frame/FramePoolManager";
|
|
4
|
+
import { FXLFramePoolManager } from "./fxl/FXLFramePoolManager";
|
|
5
|
+
import { CommsEventKey, FXLModules, ModuleLibrary, ModuleName, ReflowableModules } from "@readium/navigator-html-injectables";
|
|
6
|
+
import { BasicTextSelection, FrameClickEvent } from "@readium/navigator-html-injectables";
|
|
7
|
+
import * as path from "path-browserify";
|
|
8
|
+
import { FXLFrameManager } from "./fxl/FXLFrameManager";
|
|
9
|
+
import { FrameManager } from "./frame/FrameManager";
|
|
10
|
+
|
|
11
|
+
export type ManagerEventKey = "zoom";
|
|
12
|
+
|
|
13
|
+
export interface EpubNavigatorListeners {
|
|
14
|
+
frameLoaded: (wnd: Window) => void;
|
|
15
|
+
positionChanged: (locator: Locator) => void;
|
|
16
|
+
tap: (e: FrameClickEvent) => boolean; // Return true to prevent handling here
|
|
17
|
+
click: (e: FrameClickEvent) => boolean; // Return true to prevent handling here
|
|
18
|
+
zoom: (scale: number) => void;
|
|
19
|
+
miscPointer: (amount: number) => void;
|
|
20
|
+
customEvent: (key: string, data: unknown) => void;
|
|
21
|
+
handleLocator: (locator: Locator) => boolean; // Retrun true to prevent handling here
|
|
22
|
+
textSelected: (selection: BasicTextSelection) => void;
|
|
23
|
+
// showToc: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const defaultListeners = (listeners: EpubNavigatorListeners): EpubNavigatorListeners => ({
|
|
27
|
+
frameLoaded: listeners.frameLoaded || (() => {}),
|
|
28
|
+
positionChanged: listeners.positionChanged || (() => {}),
|
|
29
|
+
tap: listeners.tap || (() => false),
|
|
30
|
+
click: listeners.click || (() => false),
|
|
31
|
+
zoom: listeners.zoom || (() => {}),
|
|
32
|
+
miscPointer: listeners.miscPointer || (() => {}),
|
|
33
|
+
customEvent: listeners.customEvent || (() => {}),
|
|
34
|
+
handleLocator: listeners.handleLocator || (() => false),
|
|
35
|
+
textSelected: listeners.textSelected || (() => {})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
export class EpubNavigator extends VisualNavigator {
|
|
39
|
+
private readonly pub: Publication;
|
|
40
|
+
private readonly container: HTMLElement;
|
|
41
|
+
private readonly listeners: EpubNavigatorListeners;
|
|
42
|
+
private framePool!: FramePoolManager | FXLFramePoolManager;
|
|
43
|
+
private positions!: Locator[];
|
|
44
|
+
private currentLocation!: Locator;
|
|
45
|
+
private currentProgression: ReadingProgression;
|
|
46
|
+
public readonly layout: EPUBLayout;
|
|
47
|
+
|
|
48
|
+
constructor(container: HTMLElement, pub: Publication, listeners: EpubNavigatorListeners, positions: Locator[] = [], initialPosition: Locator | undefined = undefined) {
|
|
49
|
+
super();
|
|
50
|
+
this.pub = pub;
|
|
51
|
+
this.layout = EpubNavigator.determineLayout(pub);
|
|
52
|
+
this.currentProgression = pub.metadata.effectiveReadingProgression;
|
|
53
|
+
this.container = container;
|
|
54
|
+
this.listeners = defaultListeners(listeners);
|
|
55
|
+
this.currentLocation = initialPosition!;
|
|
56
|
+
if (positions.length)
|
|
57
|
+
this.positions = positions;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public static determineLayout(pub: Publication): EPUBLayout {
|
|
61
|
+
const presentation = pub.metadata.getPresentation();
|
|
62
|
+
if(presentation?.layout == EPUBLayout.fixed) return EPUBLayout.fixed;
|
|
63
|
+
if(pub.metadata.otherMetadata && ("http://openmangaformat.org/schema/1.0#version" in pub.metadata.otherMetadata))
|
|
64
|
+
return EPUBLayout.fixed; // It's fixed layout even though it lacks presentation, although this should really be a divina
|
|
65
|
+
if(pub.metadata.otherMetadata?.conformsTo === "https://readium.org/webpub-manifest/profiles/divina")
|
|
66
|
+
// TODO: this is temporary until there's a divina reader in place
|
|
67
|
+
return EPUBLayout.fixed;
|
|
68
|
+
// TODO other logic to detect fixed layout publications
|
|
69
|
+
|
|
70
|
+
return EPUBLayout.reflowable;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async load() {
|
|
74
|
+
if (!this.positions?.length)
|
|
75
|
+
this.positions = await this.pub.positionsFromManifest();
|
|
76
|
+
if(this.layout === EPUBLayout.fixed) {
|
|
77
|
+
this.framePool = new FXLFramePoolManager(this.container, this.positions, this.pub);
|
|
78
|
+
this.framePool.listener = (key: CommsEventKey | ManagerEventKey, data: unknown) => {
|
|
79
|
+
this.eventListener(key, data);
|
|
80
|
+
}
|
|
81
|
+
} else
|
|
82
|
+
this.framePool = new FramePoolManager(this.container, this.positions);
|
|
83
|
+
if(this.currentLocation === undefined)
|
|
84
|
+
this.currentLocation = this.positions[0];
|
|
85
|
+
await this.apply();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Exposed to the public to compensate for lack of implemented readium conveniences
|
|
90
|
+
* TODO remove when settings management is incorporated
|
|
91
|
+
*/
|
|
92
|
+
public get _cframes(): (FXLFrameManager | FrameManager | undefined)[] {
|
|
93
|
+
return this.framePool.currentFrames;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Exposed to the public to compensate for lack of implemented readium conveniences
|
|
98
|
+
* TODO remove when settings management is incorporated
|
|
99
|
+
*/
|
|
100
|
+
public get pool() {
|
|
101
|
+
return this.framePool;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Left intentionally public so you can pass in your own events here
|
|
106
|
+
* to trigger the navigator when user's mouse/keyboard focus is
|
|
107
|
+
* outside the readium-controller navigator. Be careful!
|
|
108
|
+
*/
|
|
109
|
+
public eventListener(key: CommsEventKey | ManagerEventKey, data: unknown) {
|
|
110
|
+
switch (key) {
|
|
111
|
+
case "_pong":
|
|
112
|
+
this.listeners.frameLoaded(this._cframes[0]!.iframe.contentWindow!);
|
|
113
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
114
|
+
break;
|
|
115
|
+
case "first_visible_locator":
|
|
116
|
+
const loc = Locator.deserialize(data as string);
|
|
117
|
+
if(!loc) break;
|
|
118
|
+
this.currentLocation = new Locator({
|
|
119
|
+
href: this.currentLocation.href,
|
|
120
|
+
type: this.currentLocation.type,
|
|
121
|
+
title: this.currentLocation.title,
|
|
122
|
+
locations: loc?.locations,
|
|
123
|
+
text: loc?.text
|
|
124
|
+
});
|
|
125
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
126
|
+
break;
|
|
127
|
+
case "text_selected":
|
|
128
|
+
this.listeners.textSelected(data as BasicTextSelection);
|
|
129
|
+
break;
|
|
130
|
+
case "click":
|
|
131
|
+
case "tap":
|
|
132
|
+
const edata = data as FrameClickEvent;
|
|
133
|
+
if (edata.interactiveElement) {
|
|
134
|
+
const element = new DOMParser().parseFromString(
|
|
135
|
+
edata.interactiveElement,
|
|
136
|
+
"text/html"
|
|
137
|
+
).body.children[0];
|
|
138
|
+
if (
|
|
139
|
+
element.nodeType === element.ELEMENT_NODE &&
|
|
140
|
+
element.nodeName === "A" &&
|
|
141
|
+
element.hasAttribute("href")
|
|
142
|
+
) {
|
|
143
|
+
const origHref = element.attributes.getNamedItem("href")?.value!;
|
|
144
|
+
if (origHref.startsWith("#")) {
|
|
145
|
+
this.go(this.currentLocation.copyWithLocations({
|
|
146
|
+
fragments: [origHref.substring(1)]
|
|
147
|
+
}), false, () => { });
|
|
148
|
+
} else if(
|
|
149
|
+
origHref.startsWith("http://") ||
|
|
150
|
+
origHref.startsWith("https://") ||
|
|
151
|
+
origHref.startsWith("mailto:") ||
|
|
152
|
+
origHref.startsWith("tel:")
|
|
153
|
+
) {
|
|
154
|
+
this.listeners.handleLocator(new Link({
|
|
155
|
+
href: origHref,
|
|
156
|
+
}).locator);
|
|
157
|
+
} else {
|
|
158
|
+
try {
|
|
159
|
+
this.goLink(new Link({
|
|
160
|
+
href: path.join(path.dirname(this.currentLocation.href), origHref)
|
|
161
|
+
}), false, () => { });
|
|
162
|
+
} catch (error) {
|
|
163
|
+
console.warn(`Couldn't go to link for ${origHref}: ${error}`);
|
|
164
|
+
this.listeners.handleLocator(new Link({
|
|
165
|
+
href: origHref,
|
|
166
|
+
}).locator);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} else console.log("Clicked on", element);
|
|
170
|
+
} else {
|
|
171
|
+
if(this.layout === EPUBLayout.fixed && (this.framePool as FXLFramePoolManager).doNotDisturb)
|
|
172
|
+
edata.doNotDisturb = true;
|
|
173
|
+
|
|
174
|
+
if(this.layout === EPUBLayout.fixed
|
|
175
|
+
// TODO handle ttb/btt
|
|
176
|
+
&& (
|
|
177
|
+
this.currentProgression === ReadingProgression.rtl ||
|
|
178
|
+
this.currentProgression === ReadingProgression.ltr
|
|
179
|
+
)
|
|
180
|
+
) {
|
|
181
|
+
if(this.framePool.currentFrames.length > 1) {
|
|
182
|
+
// Spread page dimensions
|
|
183
|
+
const cfs = this.framePool.currentFrames;
|
|
184
|
+
if(edata.targetFrameSrc === cfs[this.currentProgression === ReadingProgression.rtl ? 0 : 1]?.source) {
|
|
185
|
+
// The right page (screen-wise) was clicked, so we add the left page's width to the click's x
|
|
186
|
+
edata.x += (cfs[this.currentProgression === ReadingProgression.rtl ? 1 : 0]?.iframe.contentWindow?.innerWidth ?? 0) * window.devicePixelRatio;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const handled = key === "click" ? this.listeners.click(edata) : this.listeners.tap(edata);
|
|
192
|
+
if(handled) break;
|
|
193
|
+
if (this.currentProgression === ReadingProgression.ttb || this.currentProgression === ReadingProgression.btt)
|
|
194
|
+
return; // Not applicable to vertical reading yet. TODO
|
|
195
|
+
|
|
196
|
+
const oneQuarter = ((this._cframes.length === 2 ? this._cframes[0]!.window.innerWidth + this._cframes[1]!.window.innerWidth : this._cframes[0]!.window.innerWidth) * window.devicePixelRatio) / 4;
|
|
197
|
+
// open UI if middle screen is clicked/tapped
|
|
198
|
+
if (edata.x >= oneQuarter && edata.x <= oneQuarter * 3) this.listeners.miscPointer(1);
|
|
199
|
+
if (edata.x < oneQuarter) this.goLeft(false, () => { }); // Go left if left quarter clicked
|
|
200
|
+
else if (edata.x > oneQuarter * 3) this.goRight(false, () => { }); // Go right if right quarter clicked
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
case "tap_more":
|
|
204
|
+
this.listeners.miscPointer(data as number);
|
|
205
|
+
break;
|
|
206
|
+
case "no_more":
|
|
207
|
+
this.changeResource(1);
|
|
208
|
+
break;
|
|
209
|
+
case "no_less":
|
|
210
|
+
this.changeResource(-1);
|
|
211
|
+
break;
|
|
212
|
+
case "swipe":
|
|
213
|
+
// Swipe event
|
|
214
|
+
break;
|
|
215
|
+
case "zoom":
|
|
216
|
+
this.listeners.zoom(data as number);
|
|
217
|
+
break;
|
|
218
|
+
case "progress":
|
|
219
|
+
this.syncLocation(data as number);
|
|
220
|
+
break;
|
|
221
|
+
case "log":
|
|
222
|
+
console.log(this._cframes[0]?.source?.split("/")[3], ...(data as any[]));
|
|
223
|
+
break;
|
|
224
|
+
default:
|
|
225
|
+
this.listeners.customEvent(key, data);
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private determineModules() {
|
|
231
|
+
let modules = Array.from(ModuleLibrary.keys()) as ModuleName[];
|
|
232
|
+
|
|
233
|
+
if(this.layout === EPUBLayout.fixed) {
|
|
234
|
+
return modules.filter((m) => FXLModules.includes(m));
|
|
235
|
+
} else modules = modules.filter((m) => ReflowableModules.includes(m));
|
|
236
|
+
|
|
237
|
+
// Horizontal vs. Vertical reading
|
|
238
|
+
if (this.readingProgression === ReadingProgression.ttb || this.readingProgression === ReadingProgression.btt)
|
|
239
|
+
modules = modules.filter((m) => m !== "column_snapper");
|
|
240
|
+
else
|
|
241
|
+
modules = modules.filter((m) => m !== "scroll_snapper");
|
|
242
|
+
|
|
243
|
+
return modules;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Start listening to messages from the current iframe
|
|
247
|
+
private attachListener() {
|
|
248
|
+
const vframes = this._cframes.filter(f => !!f) as (FXLFrameManager | FrameManager)[];
|
|
249
|
+
if(vframes.length === 0) throw Error("no cframe to attach listener to");
|
|
250
|
+
vframes.forEach(f => {
|
|
251
|
+
if(f.msg) f.msg.listener = (key: CommsEventKey | ManagerEventKey, value: unknown) => {
|
|
252
|
+
this.eventListener(key, value);
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async apply() {
|
|
259
|
+
await this.framePool.update(this.pub, this.currentLocator, this.determineModules());
|
|
260
|
+
|
|
261
|
+
this.attachListener();
|
|
262
|
+
|
|
263
|
+
const idx = this.pub.readingOrder.findIndexWithHref(this.currentLocation.href);
|
|
264
|
+
if (idx < 0)
|
|
265
|
+
throw Error("Link for " + this.currentLocation.href + " not found!");
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
public async destroy() {
|
|
269
|
+
await this.framePool?.destroy();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
private async changeResource(relative: number): Promise<boolean> {
|
|
273
|
+
if (relative === 0) return false;
|
|
274
|
+
|
|
275
|
+
if(this.layout === EPUBLayout.fixed) {
|
|
276
|
+
const p = this.framePool as FXLFramePoolManager;
|
|
277
|
+
const old = p.currentNumbers[0];
|
|
278
|
+
if(relative === 1) {
|
|
279
|
+
if(!p.next(p.perPage)) return false;
|
|
280
|
+
} else if(relative === -1) {
|
|
281
|
+
if(!p.prev(p.perPage)) return false;
|
|
282
|
+
} else
|
|
283
|
+
throw Error("Invalid relative value for FXL");
|
|
284
|
+
|
|
285
|
+
// Apply change
|
|
286
|
+
const neW = p.currentNumbers[0]
|
|
287
|
+
if(old > neW)
|
|
288
|
+
for (let j = this.positions.length - 1; j >= 0; j--) {
|
|
289
|
+
if(this.positions[j].href === this.pub.readingOrder.items[neW-1].href) {
|
|
290
|
+
this.currentLocation = this.positions[j].copyWithLocations({
|
|
291
|
+
progression: 0.999999999999
|
|
292
|
+
});
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
else if(old < neW)
|
|
297
|
+
for (let j = 0; j < this.positions.length; j++) {
|
|
298
|
+
if(this.positions[j].href === this.pub.readingOrder.items[neW-1].href) {
|
|
299
|
+
this.currentLocation = this.positions[j];
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
await this.apply();
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const curr = this.pub.readingOrder.findIndexWithHref(this.currentLocation.href);
|
|
308
|
+
const i = Math.max(
|
|
309
|
+
0,
|
|
310
|
+
Math.min(this.pub.readingOrder.items.length - 1, curr + relative)
|
|
311
|
+
);
|
|
312
|
+
if (i === curr) {
|
|
313
|
+
this._cframes[0]?.msg?.send("shake", undefined, async (_) => {});
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Apply change
|
|
318
|
+
if(curr > i)
|
|
319
|
+
for (let j = this.positions.length - 1; j >= 0; j--) {
|
|
320
|
+
if(this.positions[j].href === this.pub.readingOrder.items[i].href) {
|
|
321
|
+
this.currentLocation = this.positions[j].copyWithLocations({
|
|
322
|
+
progression: 0.999999999999
|
|
323
|
+
});
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
else
|
|
328
|
+
for (let j = 0; j < this.positions.length; j++) {
|
|
329
|
+
if(this.positions[j].href === this.pub.readingOrder.items[i].href) {
|
|
330
|
+
this.currentLocation = this.positions[j];
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
await this.apply();
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
private findNearestPosition(fromProgression: number): Locator {
|
|
340
|
+
// TODO replace with locator service
|
|
341
|
+
const potentialPositions = this.positions.filter(
|
|
342
|
+
(p) => p.href === this.currentLocation.href
|
|
343
|
+
);
|
|
344
|
+
let pos = this.currentLocation;
|
|
345
|
+
|
|
346
|
+
// Find the last locator with a progrssion that's
|
|
347
|
+
// smaller than or equal to the requested progression.
|
|
348
|
+
potentialPositions.some((p) => {
|
|
349
|
+
const pr = p.locations.progression ?? 0;
|
|
350
|
+
if (fromProgression <= pr) {
|
|
351
|
+
pos = p;
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
else return false;
|
|
355
|
+
});
|
|
356
|
+
return pos;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private async syncLocation(iframeProgress: number) {
|
|
360
|
+
this.currentLocation = this.findNearestPosition(iframeProgress).copyWithLocations({
|
|
361
|
+
progression: iframeProgress // Most accurate progression in resource
|
|
362
|
+
});
|
|
363
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
364
|
+
await this.framePool.update(this.pub, this.currentLocation, this.determineModules());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public goBackward(_: boolean, cb: (ok: boolean) => void): void {
|
|
368
|
+
if(this.layout === EPUBLayout.fixed) {
|
|
369
|
+
this.changeResource(-1);
|
|
370
|
+
cb(true);
|
|
371
|
+
} else {
|
|
372
|
+
this._cframes[0]?.msg?.send("go_prev", undefined, async (ack) => {
|
|
373
|
+
if(ack)
|
|
374
|
+
// OK
|
|
375
|
+
cb(true);
|
|
376
|
+
else
|
|
377
|
+
// Need to change resources because we're at the beginning of the current one
|
|
378
|
+
cb(await this.changeResource(-1));
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public goForward(_: boolean, cb: (ok: boolean) => void): void {
|
|
384
|
+
if(this.layout === EPUBLayout.fixed) {
|
|
385
|
+
this.changeResource(1);
|
|
386
|
+
cb(true);
|
|
387
|
+
} else {
|
|
388
|
+
this._cframes[0]?.msg?.send("go_next", undefined, async (ack) => {
|
|
389
|
+
if(ack)
|
|
390
|
+
// OK
|
|
391
|
+
cb(true);
|
|
392
|
+
else
|
|
393
|
+
// Need to change resources because we're at the end of the current one
|
|
394
|
+
cb(await this.changeResource(1));
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
get currentLocator(): Locator {
|
|
400
|
+
// TODO seed locator with detailed info if this property is accessed
|
|
401
|
+
/*return (async () => { // Wrapped because JS doesn't support async getters
|
|
402
|
+
return this.currentLocation;
|
|
403
|
+
})();*/
|
|
404
|
+
|
|
405
|
+
return this.currentLocation;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Starting and ending position currently showing in the reader
|
|
409
|
+
get currentPositionNumbers(): number[] {
|
|
410
|
+
if(this.layout === EPUBLayout.fixed)
|
|
411
|
+
return (this.framePool as FXLFramePoolManager).currentNumbers;
|
|
412
|
+
|
|
413
|
+
return [this.currentLocator?.locations.position ?? 0];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// TODO: This is temporary until user settings are implemented.
|
|
417
|
+
get readingProgression(): ReadingProgression {
|
|
418
|
+
return this.currentProgression;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// TODO: This is temporary until user settings are implemented.
|
|
422
|
+
public async setReadingProgression(newProgression: ReadingProgression) {
|
|
423
|
+
if(this.currentProgression === newProgression) return;
|
|
424
|
+
this.currentProgression = newProgression;
|
|
425
|
+
await this.framePool.update(this.pub, this.currentLocator, this.determineModules(), true);
|
|
426
|
+
this.attachListener();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
get publication(): Publication {
|
|
430
|
+
return this.pub;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private async loadLocator(locator: Locator, cb: (ok: boolean) => void) {
|
|
434
|
+
let done = false;
|
|
435
|
+
let cssSelector = (typeof locator.locations.getCssSelector === 'function') && locator.locations.getCssSelector();
|
|
436
|
+
if(locator.text?.highlight) {
|
|
437
|
+
done = await new Promise<boolean>((res, _) => {
|
|
438
|
+
// Attempt to go to a highlighted piece of text in the resource
|
|
439
|
+
this._cframes[0]!.msg!.send(
|
|
440
|
+
"go_text",
|
|
441
|
+
cssSelector ? [
|
|
442
|
+
locator.text?.serialize(),
|
|
443
|
+
cssSelector // Include CSS selector if it exists
|
|
444
|
+
] : locator.text?.serialize(),
|
|
445
|
+
(ok) => res(ok)
|
|
446
|
+
);
|
|
447
|
+
});
|
|
448
|
+
} else if(cssSelector) {
|
|
449
|
+
done = await new Promise<boolean>((res, _) => {
|
|
450
|
+
this._cframes[0]!.msg!.send(
|
|
451
|
+
"go_text",
|
|
452
|
+
[
|
|
453
|
+
"", // No text!
|
|
454
|
+
cssSelector // Just CSS selector
|
|
455
|
+
],
|
|
456
|
+
(ok) => res(ok)
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
if(done) {
|
|
461
|
+
cb(done);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
// This sanity check has to be performed because we're still passing non-locator class
|
|
465
|
+
// locator objects to this function. This is not good and should eventually be forbidden
|
|
466
|
+
// or the locator should be deserialized sometime before this function.
|
|
467
|
+
const hid = (typeof locator.locations.htmlId === 'function') && locator.locations.htmlId();
|
|
468
|
+
if(hid)
|
|
469
|
+
done = await new Promise<boolean>((res, _) => {
|
|
470
|
+
// Attempt to go to an HTML ID in the resource
|
|
471
|
+
this._cframes[0]!.msg!.send("go_id", hid, (ok) => res(ok));
|
|
472
|
+
});
|
|
473
|
+
if(done) {
|
|
474
|
+
cb(done);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const progression = locator?.locations?.progression;
|
|
479
|
+
const hasProgression = progression && progression > 0;
|
|
480
|
+
if(hasProgression)
|
|
481
|
+
done = await new Promise<boolean>((res, _) => {
|
|
482
|
+
// Attempt to go to a progression in the resource
|
|
483
|
+
this._cframes[0]!.msg!.send("go_progression", progression, (ok) => res(ok));
|
|
484
|
+
});
|
|
485
|
+
else done = true;
|
|
486
|
+
cb(done);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
|
|
490
|
+
const href = locator.href.split("#")[0];
|
|
491
|
+
let link = this.pub.readingOrder.findWithHref(href);
|
|
492
|
+
if(!link) {
|
|
493
|
+
return cb(this.listeners.handleLocator(locator));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
this.currentLocation = this.positions.find(p => p.href === link!.href)!;
|
|
497
|
+
this.apply().then(() => this.loadLocator(locator, (ok) => cb(ok))).then(() => {
|
|
498
|
+
// Now that we've gone to the right locator, we can attach the listeners.
|
|
499
|
+
// Doing this only at this stage reduces janky UI with multiple locator updates.
|
|
500
|
+
this.attachListener();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
public goLink(link: Link, animated: boolean, cb: (ok: boolean) => void): void {
|
|
505
|
+
return this.go(link.locator, animated, cb);
|
|
506
|
+
}
|
|
507
|
+
}
|