@readium/navigator 2.1.0 → 2.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/dist/index.js +3082 -2649
- package/dist/index.umd.cjs +86 -36
- package/package.json +8 -8
- package/src/epub/css/Properties.ts +1 -5
- package/src/epub/css/ReadiumCSS.ts +0 -1
- package/src/epub/preferences/EpubDefaults.ts +1 -5
- package/src/epub/preferences/EpubPreferences.ts +0 -4
- package/src/epub/preferences/EpubPreferencesEditor.ts +1 -14
- package/src/epub/preferences/EpubSettings.ts +1 -4
- package/src/index.ts +1 -0
- package/src/preferences/Types.ts +0 -6
- package/src/webpub/WebPubBlobBuilder.ts +145 -0
- package/src/webpub/WebPubFrameManager.ts +140 -0
- package/src/webpub/WebPubFramePoolManager.ts +174 -0
- package/src/webpub/WebPubNavigator.ts +417 -0
- package/src/webpub/index.ts +4 -0
- package/types/src/epub/css/Properties.d.ts +1 -3
- package/types/src/epub/preferences/EpubDefaults.d.ts +1 -3
- package/types/src/epub/preferences/EpubPreferences.d.ts +1 -3
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +1 -2
- package/types/src/epub/preferences/EpubSettings.d.ts +1 -3
- package/types/src/index.d.ts +1 -0
- package/types/src/preferences/Types.d.ts +0 -5
- package/types/src/web/WebPubBlobBuilder.d.ts +10 -0
- package/types/src/web/WebPubFrameManager.d.ts +20 -0
- package/types/src/web/WebPubNavigator.d.ts +48 -0
- package/types/src/web/index.d.ts +3 -0
- package/types/src/webpub/WebPubBlobBuilder.d.ts +12 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +20 -0
- package/types/src/webpub/WebPubFramePoolManager.d.ts +16 -0
- package/types/src/webpub/WebPubNavigator.d.ts +50 -0
- package/types/src/webpub/index.d.ts +4 -0
|
@@ -0,0 +1,174 @@
|
|
|
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 readonly pool: Map<string, WebPubFrameManager> = new Map();
|
|
10
|
+
private readonly blobs: Map<string, string> = new Map();
|
|
11
|
+
private readonly inprogress: Map<string, Promise<void>> = new Map();
|
|
12
|
+
private currentBaseURL: string | undefined;
|
|
13
|
+
|
|
14
|
+
constructor(container: HTMLElement) {
|
|
15
|
+
this.container = container;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async destroy() {
|
|
19
|
+
// Wait for all in-progress loads to complete
|
|
20
|
+
let iit = this.inprogress.values();
|
|
21
|
+
let inp = iit.next();
|
|
22
|
+
const inprogressPromises: Promise<void>[] = [];
|
|
23
|
+
while(inp.value) {
|
|
24
|
+
inprogressPromises.push(inp.value);
|
|
25
|
+
inp = iit.next();
|
|
26
|
+
}
|
|
27
|
+
if(inprogressPromises.length > 0) {
|
|
28
|
+
await Promise.allSettled(inprogressPromises);
|
|
29
|
+
}
|
|
30
|
+
this.inprogress.clear();
|
|
31
|
+
|
|
32
|
+
// Destroy all frames
|
|
33
|
+
let fit = this.pool.values();
|
|
34
|
+
let frm = fit.next();
|
|
35
|
+
while(frm.value) {
|
|
36
|
+
await (frm.value as WebPubFrameManager).destroy();
|
|
37
|
+
frm = fit.next();
|
|
38
|
+
}
|
|
39
|
+
this.pool.clear();
|
|
40
|
+
|
|
41
|
+
// Revoke all blobs
|
|
42
|
+
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
43
|
+
this.blobs.clear();
|
|
44
|
+
|
|
45
|
+
// Empty container of elements
|
|
46
|
+
this.container.childNodes.forEach(v => {
|
|
47
|
+
if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async update(pub: Publication, locator: Locator, modules: ModuleName[]) {
|
|
52
|
+
const readingOrder = pub.readingOrder.items;
|
|
53
|
+
let i = readingOrder.findIndex(l => l.href === locator.href);
|
|
54
|
+
if(i < 0) throw Error(`Locator not found in reading order: ${locator.href}`);
|
|
55
|
+
const newHref = readingOrder[i].href;
|
|
56
|
+
|
|
57
|
+
if(this.inprogress.has(newHref))
|
|
58
|
+
await this.inprogress.get(newHref);
|
|
59
|
+
|
|
60
|
+
const progressPromise = new Promise<void>(async (resolve, reject) => {
|
|
61
|
+
const disposal: string[] = [];
|
|
62
|
+
const creation: string[] = [];
|
|
63
|
+
pub.readingOrder.items.forEach((l, j) => {
|
|
64
|
+
// Dispose everything except current, previous, and next
|
|
65
|
+
if(j !== i && j !== i - 1 && j !== i + 1) {
|
|
66
|
+
if(!disposal.includes(l.href)) disposal.push(l.href);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// CURRENT FRAME: always create the frame we're navigating to
|
|
70
|
+
if(j === i) {
|
|
71
|
+
if(!creation.includes(l.href)) creation.push(l.href);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// PREVIOUS/NEXT FRAMES: create adjacent chapters for smooth navigation
|
|
75
|
+
// if((j === i - 1 || j === i + 1) && j >= 0 && j < pub.readingOrder.items.length) {
|
|
76
|
+
// if(!creation.includes(l.href)) creation.push(l.href);
|
|
77
|
+
// }
|
|
78
|
+
});
|
|
79
|
+
disposal.forEach(async href => {
|
|
80
|
+
if(creation.includes(href)) return;
|
|
81
|
+
if(!this.pool.has(href)) return;
|
|
82
|
+
await this.pool.get(href)?.destroy();
|
|
83
|
+
this.pool.delete(href);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
87
|
+
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
88
|
+
this.blobs.clear();
|
|
89
|
+
}
|
|
90
|
+
this.currentBaseURL = pub.baseURL;
|
|
91
|
+
|
|
92
|
+
const creator = async (href: string) => {
|
|
93
|
+
if(this.pool.has(href)) {
|
|
94
|
+
const fm = this.pool.get(href)!;
|
|
95
|
+
if(!this.blobs.has(href)) {
|
|
96
|
+
await fm.destroy();
|
|
97
|
+
this.pool.delete(href);
|
|
98
|
+
} else {
|
|
99
|
+
await fm.load(modules);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const itm = pub.readingOrder.findWithHref(href);
|
|
104
|
+
if(!itm) return;
|
|
105
|
+
if(!this.blobs.has(href)) {
|
|
106
|
+
const blobBuilder = new WebPubBlobBuilder(pub, this.currentBaseURL || "", itm);
|
|
107
|
+
const blobURL = await blobBuilder.build();
|
|
108
|
+
this.blobs.set(href, blobURL);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const fm = new WebPubFrameManager(this.blobs.get(href)!);
|
|
112
|
+
if(href !== newHref) await fm.hide();
|
|
113
|
+
this.container.appendChild(fm.iframe);
|
|
114
|
+
await fm.load(modules);
|
|
115
|
+
this.pool.set(href, fm);
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
await Promise.all(creation.map(href => creator(href)));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const newFrame = this.pool.get(newHref)!;
|
|
124
|
+
if(newFrame?.source !== this._currentFrame?.source) {
|
|
125
|
+
await this._currentFrame?.hide();
|
|
126
|
+
if(newFrame)
|
|
127
|
+
await newFrame.load(modules);
|
|
128
|
+
|
|
129
|
+
if(newFrame)
|
|
130
|
+
await newFrame.show(locator.locations.progression);
|
|
131
|
+
|
|
132
|
+
this._currentFrame = newFrame;
|
|
133
|
+
}
|
|
134
|
+
resolve();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
this.inprogress.set(newHref, progressPromise);
|
|
138
|
+
await progressPromise;
|
|
139
|
+
this.inprogress.delete(newHref);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get currentFrames(): (WebPubFrameManager | undefined)[] {
|
|
143
|
+
return [this._currentFrame];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get currentBounds(): DOMRect {
|
|
147
|
+
const ret = {
|
|
148
|
+
x: 0,
|
|
149
|
+
y: 0,
|
|
150
|
+
width: 0,
|
|
151
|
+
height: 0,
|
|
152
|
+
top: 0,
|
|
153
|
+
right: 0,
|
|
154
|
+
bottom: 0,
|
|
155
|
+
left: 0,
|
|
156
|
+
toJSON() {
|
|
157
|
+
return this;
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
this.currentFrames.forEach(f => {
|
|
161
|
+
if(!f) return;
|
|
162
|
+
const b = f.realSize;
|
|
163
|
+
ret.x = Math.min(ret.x, b.x);
|
|
164
|
+
ret.y = Math.min(ret.y, b.y);
|
|
165
|
+
ret.width += b.width;
|
|
166
|
+
ret.height = Math.max(ret.height, b.height);
|
|
167
|
+
ret.top = Math.min(ret.top, b.top);
|
|
168
|
+
ret.right = Math.min(ret.right, b.right);
|
|
169
|
+
ret.bottom = Math.min(ret.bottom, b.bottom);
|
|
170
|
+
ret.left = Math.min(ret.left, b.left);
|
|
171
|
+
});
|
|
172
|
+
return ret as DOMRect;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import { Link, Locator, Publication, ReadingProgression, LocatorLocations } from "@readium/shared";
|
|
2
|
+
import { VisualNavigator, VisualNavigatorViewport, ProgressionRange } from "../Navigator";
|
|
3
|
+
import { WebPubFramePoolManager } from "./WebPubFramePoolManager";
|
|
4
|
+
import { BasicTextSelection, CommsEventKey, FrameClickEvent, ModuleLibrary, ModuleName, WebPubModules } from "@readium/navigator-html-injectables";
|
|
5
|
+
import * as path from "path-browserify";
|
|
6
|
+
import { ManagerEventKey } from "../epub/EpubNavigator";
|
|
7
|
+
|
|
8
|
+
export interface WebPubNavigatorListeners {
|
|
9
|
+
frameLoaded: (wnd: Window) => void;
|
|
10
|
+
positionChanged: (locator: Locator) => void;
|
|
11
|
+
tap: (e: FrameClickEvent) => boolean;
|
|
12
|
+
click: (e: FrameClickEvent) => boolean;
|
|
13
|
+
zoom: (scale: number) => void;
|
|
14
|
+
scroll: (delta: number) => void;
|
|
15
|
+
customEvent: (key: string, data: unknown) => void;
|
|
16
|
+
handleLocator: (locator: Locator) => boolean;
|
|
17
|
+
textSelected: (selection: BasicTextSelection) => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const defaultListeners = (listeners: WebPubNavigatorListeners): WebPubNavigatorListeners => ({
|
|
21
|
+
frameLoaded: listeners.frameLoaded || (() => {}),
|
|
22
|
+
positionChanged: listeners.positionChanged || (() => {}),
|
|
23
|
+
tap: listeners.tap || (() => false),
|
|
24
|
+
click: listeners.click || (() => false),
|
|
25
|
+
zoom: listeners.zoom || (() => {}),
|
|
26
|
+
scroll: listeners.scroll || (() => {}),
|
|
27
|
+
customEvent: listeners.customEvent || (() => {}),
|
|
28
|
+
handleLocator: listeners.handleLocator || (() => false),
|
|
29
|
+
textSelected: listeners.textSelected || (() => {})
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
class WebPubNavigator extends VisualNavigator {
|
|
33
|
+
private readonly pub: Publication;
|
|
34
|
+
private readonly container: HTMLElement;
|
|
35
|
+
private readonly listeners: WebPubNavigatorListeners;
|
|
36
|
+
private framePool: WebPubFramePoolManager;
|
|
37
|
+
private currentIndex: number = 0;
|
|
38
|
+
private currentLocation: Locator;
|
|
39
|
+
private webViewport: VisualNavigatorViewport = {
|
|
40
|
+
readingOrder: [],
|
|
41
|
+
progressions: new Map(),
|
|
42
|
+
positions: null
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
constructor(container: HTMLElement, pub: Publication, listeners: WebPubNavigatorListeners, initialPosition: Locator | undefined = undefined) {
|
|
46
|
+
super();
|
|
47
|
+
this.pub = pub;
|
|
48
|
+
this.container = container;
|
|
49
|
+
this.listeners = defaultListeners(listeners);
|
|
50
|
+
this.framePool = new WebPubFramePoolManager(this.container);
|
|
51
|
+
if (initialPosition && typeof initialPosition.copyWithLocations === 'function') {
|
|
52
|
+
this.currentLocation = initialPosition;
|
|
53
|
+
// Update currentIndex to match the initial position
|
|
54
|
+
const index = this.pub.readingOrder.findIndexWithHref(initialPosition.href);
|
|
55
|
+
if (index >= 0) {
|
|
56
|
+
this.currentIndex = index;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
this.currentLocation = this.createCurrentLocator();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async load(): Promise<void> {
|
|
64
|
+
await this.framePool.update(this.pub, this.currentLocation, this.determineModules());
|
|
65
|
+
|
|
66
|
+
this.attachListener();
|
|
67
|
+
|
|
68
|
+
// Notify listeners of initial position
|
|
69
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
public eventListener(key: CommsEventKey | ManagerEventKey, data: unknown) {
|
|
73
|
+
switch (key) {
|
|
74
|
+
case "_pong":
|
|
75
|
+
this.listeners.frameLoaded(this.framePool.currentFrames[0]!.iframe.contentWindow!);
|
|
76
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
77
|
+
break;
|
|
78
|
+
case "first_visible_locator":
|
|
79
|
+
const loc = Locator.deserialize(data as string);
|
|
80
|
+
if(!loc) break;
|
|
81
|
+
this.currentLocation = new Locator({
|
|
82
|
+
href: this.currentLocation.href,
|
|
83
|
+
type: this.currentLocation.type,
|
|
84
|
+
title: this.currentLocation.title,
|
|
85
|
+
locations: loc?.locations,
|
|
86
|
+
text: loc?.text
|
|
87
|
+
});
|
|
88
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
89
|
+
break;
|
|
90
|
+
case "text_selected":
|
|
91
|
+
this.listeners.textSelected(data as BasicTextSelection);
|
|
92
|
+
break;
|
|
93
|
+
case "click":
|
|
94
|
+
case "tap":
|
|
95
|
+
const edata = data as FrameClickEvent;
|
|
96
|
+
if (edata.interactiveElement) {
|
|
97
|
+
const element = new DOMParser().parseFromString(
|
|
98
|
+
edata.interactiveElement,
|
|
99
|
+
"text/html"
|
|
100
|
+
).body.children[0];
|
|
101
|
+
if (
|
|
102
|
+
element.nodeType === element.ELEMENT_NODE &&
|
|
103
|
+
element.nodeName === "A" &&
|
|
104
|
+
element.hasAttribute("href")
|
|
105
|
+
) {
|
|
106
|
+
const origHref = element.attributes.getNamedItem("href")?.value!;
|
|
107
|
+
if (origHref.startsWith("#")) {
|
|
108
|
+
this.go(this.currentLocation.copyWithLocations({
|
|
109
|
+
fragments: [origHref.substring(1)]
|
|
110
|
+
}), false, () => { });
|
|
111
|
+
} else if(
|
|
112
|
+
origHref.startsWith("mailto:") ||
|
|
113
|
+
origHref.startsWith("tel:")
|
|
114
|
+
) {
|
|
115
|
+
this.listeners.handleLocator(new Link({
|
|
116
|
+
href: origHref,
|
|
117
|
+
}).locator);
|
|
118
|
+
} else {
|
|
119
|
+
// Handle internal links that should navigate within the WebPub
|
|
120
|
+
// This includes relative links and full URLs that might be in the readingOrder
|
|
121
|
+
try {
|
|
122
|
+
let hrefToCheck;
|
|
123
|
+
|
|
124
|
+
// If origHref is already a full URL, use it directly
|
|
125
|
+
if (origHref.startsWith("http://") || origHref.startsWith("https://")) {
|
|
126
|
+
hrefToCheck = origHref;
|
|
127
|
+
} else {
|
|
128
|
+
// For relative URLs, use different strategies based on base URL format
|
|
129
|
+
if (this.currentLocation.href.startsWith("http://") || this.currentLocation.href.startsWith("https://")) {
|
|
130
|
+
// Base URL is absolute, use URL constructor
|
|
131
|
+
const currentUrl = new URL(this.currentLocation.href);
|
|
132
|
+
const resolvedUrl = new URL(origHref, currentUrl);
|
|
133
|
+
hrefToCheck = resolvedUrl.href;
|
|
134
|
+
} else {
|
|
135
|
+
// Base URL is relative, use path operations
|
|
136
|
+
hrefToCheck = path.join(path.dirname(this.currentLocation.href), origHref);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const link = this.pub.readingOrder.findWithHref(hrefToCheck);
|
|
141
|
+
if (link) {
|
|
142
|
+
this.goLink(link, false, () => { });
|
|
143
|
+
} else {
|
|
144
|
+
console.warn(`Internal link not found in readingOrder: ${hrefToCheck}`);
|
|
145
|
+
this.listeners.handleLocator(new Link({
|
|
146
|
+
href: origHref,
|
|
147
|
+
}).locator);
|
|
148
|
+
}
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.warn(`Couldn't resolve internal link for ${origHref}: ${error}`);
|
|
151
|
+
this.listeners.handleLocator(new Link({
|
|
152
|
+
href: origHref,
|
|
153
|
+
}).locator);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else console.log("Clicked on", element);
|
|
157
|
+
} else {
|
|
158
|
+
const handled = key === "click" ? this.listeners.click(edata) : this.listeners.tap(edata);
|
|
159
|
+
if(handled) break;
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case "scroll":
|
|
163
|
+
this.listeners.scroll(data as number);
|
|
164
|
+
break;
|
|
165
|
+
case "zoom":
|
|
166
|
+
this.listeners.zoom(data as number);
|
|
167
|
+
break;
|
|
168
|
+
case "progress":
|
|
169
|
+
this.syncLocation(data as ProgressionRange);
|
|
170
|
+
break;
|
|
171
|
+
case "log":
|
|
172
|
+
console.log(this.framePool.currentFrames[0]?.source?.split("/")[3], ...(data as any[]));
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
this.listeners.customEvent(key, data);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private determineModules(): ModuleName[] {
|
|
181
|
+
let modules = Array.from(ModuleLibrary.keys()) as ModuleName[];
|
|
182
|
+
|
|
183
|
+
// For WebPub, use the predefined WebPubModules array and filter
|
|
184
|
+
return modules.filter((m) => WebPubModules.includes(m));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private attachListener() {
|
|
188
|
+
if (this.framePool.currentFrames[0]?.msg) {
|
|
189
|
+
this.framePool.currentFrames[0].msg.listener = (key: CommsEventKey | ManagerEventKey, value: unknown) => {
|
|
190
|
+
this.eventListener(key, value);
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private async apply() {
|
|
196
|
+
await this.framePool.update(this.pub, this.currentLocation, this.determineModules());
|
|
197
|
+
|
|
198
|
+
this.attachListener();
|
|
199
|
+
|
|
200
|
+
const idx = this.pub.readingOrder.findIndexWithHref(this.currentLocation.href);
|
|
201
|
+
if (idx < 0)
|
|
202
|
+
throw Error("Link for " + this.currentLocation.href + " not found!");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
public async destroy() {
|
|
206
|
+
await this.framePool?.destroy();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async changeResource(relative: number): Promise<boolean> {
|
|
210
|
+
if (relative === 0) return false;
|
|
211
|
+
|
|
212
|
+
const curr = this.pub.readingOrder.findIndexWithHref(this.currentLocation.href);
|
|
213
|
+
const i = Math.max(
|
|
214
|
+
0,
|
|
215
|
+
Math.min(this.pub.readingOrder.items.length - 1, curr + relative)
|
|
216
|
+
);
|
|
217
|
+
if (i === curr) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
this.currentIndex = i;
|
|
221
|
+
this.currentLocation = this.createCurrentLocator();
|
|
222
|
+
await this.apply();
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
private updateViewport(progression: ProgressionRange) {
|
|
227
|
+
this.webViewport.readingOrder = [];
|
|
228
|
+
this.webViewport.progressions.clear();
|
|
229
|
+
this.webViewport.positions = null;
|
|
230
|
+
|
|
231
|
+
// Use the current position's href
|
|
232
|
+
if (this.currentLocation) {
|
|
233
|
+
this.webViewport.readingOrder.push(this.currentLocation.href);
|
|
234
|
+
this.webViewport.progressions.set(this.currentLocation.href, progression);
|
|
235
|
+
|
|
236
|
+
if (this.currentLocation.locations?.position !== undefined) {
|
|
237
|
+
this.webViewport.positions = [this.currentLocation.locations.position];
|
|
238
|
+
// WebPub doesn't have lastLocationInView like EPUB, so no second position
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private async syncLocation(iframeProgress: ProgressionRange): Promise<void> {
|
|
244
|
+
const progression = iframeProgress;
|
|
245
|
+
if (this.currentLocation) {
|
|
246
|
+
this.currentLocation = this.currentLocation.copyWithLocations({
|
|
247
|
+
progression: progression.start
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
this.updateViewport(progression);
|
|
252
|
+
this.listeners.positionChanged(this.currentLocation);
|
|
253
|
+
await this.framePool.update(this.pub, this.currentLocation, this.determineModules());
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
goBackward(_animated: boolean, cb: (ok: boolean) => void): void {
|
|
257
|
+
this.changeResource(-1).then((success) => {
|
|
258
|
+
cb(success);
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
goForward(_animated: boolean, cb: (ok: boolean) => void): void {
|
|
263
|
+
this.changeResource(1).then((success) => {
|
|
264
|
+
cb(success);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
get currentLocator(): Locator {
|
|
269
|
+
return this.currentLocation;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
get viewport(): VisualNavigatorViewport {
|
|
273
|
+
return this.webViewport;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
get isScrollStart(): boolean {
|
|
277
|
+
const firstHref = this.viewport.readingOrder[0];
|
|
278
|
+
const progression = this.viewport.progressions.get(firstHref);
|
|
279
|
+
return progression?.start === 0;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
get isScrollEnd(): boolean {
|
|
283
|
+
const lastHref = this.viewport.readingOrder[this.viewport.readingOrder.length - 1];
|
|
284
|
+
const progression = this.viewport.progressions.get(lastHref);
|
|
285
|
+
return progression?.end === 1;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
get canGoBackward(): boolean {
|
|
289
|
+
const firstResource = this.pub.readingOrder.items[0]?.href;
|
|
290
|
+
return !(this.viewport.progressions.has(firstResource) && this.viewport.progressions.get(firstResource)?.start === 0);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
get canGoForward(): boolean {
|
|
294
|
+
const lastResource = this.pub.readingOrder.items[this.pub.readingOrder.items.length - 1]?.href;
|
|
295
|
+
return !(this.viewport.progressions.has(lastResource) && this.viewport.progressions.get(lastResource)?.end === 1);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
get readingProgression(): ReadingProgression {
|
|
299
|
+
return this.pub.metadata.effectiveReadingProgression;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
get publication(): Publication {
|
|
303
|
+
return this.pub;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async loadLocator(locator: Locator, cb: (ok: boolean) => void) {
|
|
307
|
+
let done = false;
|
|
308
|
+
let cssSelector = (typeof locator.locations.getCssSelector === 'function') && locator.locations.getCssSelector();
|
|
309
|
+
if(locator.text?.highlight) {
|
|
310
|
+
done = await new Promise<boolean>((res, _) => {
|
|
311
|
+
// Attempt to go to a highlighted piece of text in the resource
|
|
312
|
+
this.framePool.currentFrames[0]!.msg!.send(
|
|
313
|
+
"go_text",
|
|
314
|
+
cssSelector ? [
|
|
315
|
+
locator.text?.serialize(),
|
|
316
|
+
cssSelector // Include CSS selector if it exists
|
|
317
|
+
] : locator.text?.serialize(),
|
|
318
|
+
(ok) => res(ok)
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
} else if(cssSelector) {
|
|
322
|
+
done = await new Promise<boolean>((res, _) => {
|
|
323
|
+
this.framePool.currentFrames[0]!.msg!.send(
|
|
324
|
+
"go_text",
|
|
325
|
+
[
|
|
326
|
+
"", // No text!
|
|
327
|
+
cssSelector // Just CSS selector
|
|
328
|
+
],
|
|
329
|
+
(ok) => res(ok)
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if(done) {
|
|
334
|
+
cb(done);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
// This sanity check has to be performed because we're still passing non-locator class
|
|
338
|
+
// locator objects to this function. This is not good and should eventually be forbidden
|
|
339
|
+
// or the locator should be deserialized sometime before this function.
|
|
340
|
+
const hid = (typeof locator.locations.htmlId === 'function') && locator.locations.htmlId();
|
|
341
|
+
if(hid)
|
|
342
|
+
done = await new Promise<boolean>((res, _) => {
|
|
343
|
+
// Attempt to go to an HTML ID in the resource
|
|
344
|
+
this.framePool.currentFrames[0]!.msg!.send("go_id", hid, (ok) => res(ok));
|
|
345
|
+
});
|
|
346
|
+
if(done) {
|
|
347
|
+
cb(done);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const progression = locator?.locations?.progression;
|
|
352
|
+
const hasProgression = progression && progression > 0;
|
|
353
|
+
if(hasProgression)
|
|
354
|
+
done = await new Promise<boolean>((res, _) => {
|
|
355
|
+
// Attempt to go to a progression in the resource
|
|
356
|
+
this.framePool.currentFrames[0]!.msg!.send("go_progression", progression, (ok) => res(ok));
|
|
357
|
+
});
|
|
358
|
+
else done = true;
|
|
359
|
+
cb(done);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public go(locator: Locator, _: boolean, cb: (ok: boolean) => void): void {
|
|
363
|
+
const href = locator.href.split("#")[0];
|
|
364
|
+
let link = this.pub.readingOrder.findWithHref(href);
|
|
365
|
+
if(!link) {
|
|
366
|
+
return cb(this.listeners.handleLocator(locator));
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Update currentIndex to point to the found link
|
|
370
|
+
const index = this.pub.readingOrder.findIndexWithHref(href);
|
|
371
|
+
if (index >= 0) {
|
|
372
|
+
this.currentIndex = index;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
this.currentLocation = this.createCurrentLocator();
|
|
376
|
+
this.apply().then(() => this.loadLocator(locator, (ok) => cb(ok))).then(() => {
|
|
377
|
+
// Now that we've gone to the right locator, we can attach the listeners.
|
|
378
|
+
// Doing this only at this stage reduces janky UI with multiple locator updates.
|
|
379
|
+
this.attachListener();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public goLink(link: Link, animated: boolean, cb: (ok: boolean) => void): void {
|
|
384
|
+
return this.go(link.locator, animated, cb);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Specifics to WebPub
|
|
388
|
+
// Util method
|
|
389
|
+
private createCurrentLocator(): Locator {
|
|
390
|
+
const readingOrder = this.pub.readingOrder;
|
|
391
|
+
const currentLink = readingOrder.items[this.currentIndex];
|
|
392
|
+
|
|
393
|
+
if (!currentLink) {
|
|
394
|
+
throw new Error("No current resource available");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Check if we're on the same resource
|
|
398
|
+
const isSameResource = this.currentLocation && this.currentLocation.href === currentLink.href;
|
|
399
|
+
|
|
400
|
+
// Preserve progression if staying on same resource, otherwise start from beginning
|
|
401
|
+
const progression = isSameResource && this.currentLocation.locations.progression
|
|
402
|
+
? this.currentLocation.locations.progression
|
|
403
|
+
: 0;
|
|
404
|
+
|
|
405
|
+
return this.pub.manifest.locatorFromLink(currentLink) || new Locator({
|
|
406
|
+
href: currentLink.href,
|
|
407
|
+
type: currentLink.type || "text/html",
|
|
408
|
+
locations: new LocatorLocations({
|
|
409
|
+
fragments: [],
|
|
410
|
+
progression: progression,
|
|
411
|
+
position: this.currentIndex + 1
|
|
412
|
+
})
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export const ExperimentalWebPubNavigator = WebPubNavigator;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextAlignment
|
|
1
|
+
import { TextAlignment } from "../../preferences/Types";
|
|
2
2
|
export type BodyHyphens = "auto" | "none";
|
|
3
3
|
export type BoxSizing = "content-box" | "border-box";
|
|
4
4
|
export type FontOpticalSizing = "auto" | "none";
|
|
@@ -22,7 +22,6 @@ declare abstract class Properties {
|
|
|
22
22
|
export interface IUserProperties {
|
|
23
23
|
advancedSettings?: boolean | null;
|
|
24
24
|
a11yNormalize?: boolean | null;
|
|
25
|
-
appearance?: Theme | null;
|
|
26
25
|
backgroundColor?: string | null;
|
|
27
26
|
blendFilter?: boolean | null;
|
|
28
27
|
bodyHyphens?: BodyHyphens | null;
|
|
@@ -57,7 +56,6 @@ export interface IUserProperties {
|
|
|
57
56
|
}
|
|
58
57
|
export declare class UserProperties extends Properties {
|
|
59
58
|
a11yNormalize: boolean | null;
|
|
60
|
-
appearance: Theme | null;
|
|
61
59
|
backgroundColor: string | null;
|
|
62
60
|
blendFilter: boolean | null;
|
|
63
61
|
bodyHyphens: BodyHyphens | null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { TextAlignment
|
|
1
|
+
import { TextAlignment } from "../../preferences/Types";
|
|
2
2
|
export interface IEpubDefaults {
|
|
3
3
|
backgroundColor?: string | null;
|
|
4
4
|
blendFilter?: boolean | null;
|
|
@@ -36,7 +36,6 @@ export interface IEpubDefaults {
|
|
|
36
36
|
textAlign?: TextAlignment | null;
|
|
37
37
|
textColor?: string | null;
|
|
38
38
|
textNormalization?: boolean | null;
|
|
39
|
-
theme?: Theme | null;
|
|
40
39
|
visitedColor?: string | null;
|
|
41
40
|
wordSpacing?: number | null;
|
|
42
41
|
}
|
|
@@ -77,7 +76,6 @@ export declare class EpubDefaults {
|
|
|
77
76
|
textAlign: TextAlignment | null;
|
|
78
77
|
textColor: string | null;
|
|
79
78
|
textNormalization: boolean | null;
|
|
80
|
-
theme: Theme | null;
|
|
81
79
|
visitedColor: string | null;
|
|
82
80
|
wordSpacing: number | null;
|
|
83
81
|
constructor(defaults: IEpubDefaults);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigurablePreferences } from "../../preferences/Configurable";
|
|
2
|
-
import { TextAlignment
|
|
2
|
+
import { TextAlignment } from "../../preferences/Types";
|
|
3
3
|
export interface IEpubPreferences {
|
|
4
4
|
backgroundColor?: string | null;
|
|
5
5
|
blendFilter?: boolean | null;
|
|
@@ -37,7 +37,6 @@ export interface IEpubPreferences {
|
|
|
37
37
|
textAlign?: TextAlignment | null;
|
|
38
38
|
textColor?: string | null;
|
|
39
39
|
textNormalization?: boolean | null;
|
|
40
|
-
theme?: Theme | null;
|
|
41
40
|
visitedColor?: string | null;
|
|
42
41
|
wordSpacing?: number | null;
|
|
43
42
|
}
|
|
@@ -78,7 +77,6 @@ export declare class EpubPreferences implements ConfigurablePreferences {
|
|
|
78
77
|
textAlign?: TextAlignment | null;
|
|
79
78
|
textColor?: string | null;
|
|
80
79
|
textNormalization?: boolean | null;
|
|
81
|
-
theme?: Theme | null;
|
|
82
80
|
visitedColor?: string | null;
|
|
83
81
|
wordSpacing?: number | null;
|
|
84
82
|
constructor(preferences?: IEpubPreferences);
|
|
@@ -3,7 +3,7 @@ import { IPreferencesEditor } from "../../preferences/PreferencesEditor";
|
|
|
3
3
|
import { EpubPreferences } from "./EpubPreferences";
|
|
4
4
|
import { EpubSettings } from "./EpubSettings";
|
|
5
5
|
import { BooleanPreference, EnumPreference, Preference, RangePreference } from "../../preferences/Preference";
|
|
6
|
-
import { TextAlignment
|
|
6
|
+
import { TextAlignment } from "../../preferences/Types";
|
|
7
7
|
export declare class EpubPreferencesEditor implements IPreferencesEditor {
|
|
8
8
|
preferences: EpubPreferences;
|
|
9
9
|
private settings;
|
|
@@ -48,7 +48,6 @@ export declare class EpubPreferencesEditor implements IPreferencesEditor {
|
|
|
48
48
|
get textAlign(): EnumPreference<TextAlignment>;
|
|
49
49
|
get textColor(): Preference<string>;
|
|
50
50
|
get textNormalization(): BooleanPreference;
|
|
51
|
-
get theme(): EnumPreference<Theme>;
|
|
52
51
|
get visitedColor(): Preference<string>;
|
|
53
52
|
get wordSpacing(): RangePreference<number>;
|
|
54
53
|
}
|