@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
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Loader, ModuleName } from "@readium/navigator-html-injectables";
|
|
2
2
|
import { Page, ReadingProgression } from "@readium/shared";
|
|
3
3
|
import { FrameComms } from "../frame/FrameComms.ts";
|
|
4
|
-
import { FXLPeripherals } from "./FXLPeripherals.ts";
|
|
4
|
+
import { FXLPeripherals, isTypedOMSupported } from "./FXLPeripherals.ts";
|
|
5
5
|
import type { ReadiumWindow } from "../../../../navigator-html-injectables/types/src/helpers/dom";
|
|
6
6
|
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
7
7
|
import { KeyboardConditionBridge } from "../../peripherals/KeyboardConditionBridge.ts";
|
|
8
8
|
|
|
9
9
|
export class FXLFrameManager {
|
|
10
10
|
private frame: HTMLIFrameElement;
|
|
11
|
+
private frameIsAppended = false;
|
|
11
12
|
private loader: Loader | undefined;
|
|
12
13
|
public source: string;
|
|
13
14
|
private comms: FrameComms | undefined;
|
|
@@ -22,6 +23,7 @@ export class FXLFrameManager {
|
|
|
22
23
|
public debugHref: string;
|
|
23
24
|
private loadPromise: Promise<Window> | undefined;
|
|
24
25
|
private showPromise: Promise<void> | undefined;
|
|
26
|
+
private viewportSize: { width: number, height: number } | undefined = undefined;
|
|
25
27
|
|
|
26
28
|
constructor(
|
|
27
29
|
peripherals: FXLPeripherals,
|
|
@@ -41,7 +43,7 @@ export class FXLFrameManager {
|
|
|
41
43
|
this.frame.classList.add("blank");
|
|
42
44
|
this.frame.scrolling = "no";
|
|
43
45
|
this.frame.style.visibility = "hidden";
|
|
44
|
-
this.frame.
|
|
46
|
+
this.frame.ariaHidden = "true";
|
|
45
47
|
this.frame.style.display = "none";
|
|
46
48
|
this.frame.style.position = "absolute";
|
|
47
49
|
this.frame.style.pointerEvents = "none";
|
|
@@ -52,15 +54,17 @@ export class FXLFrameManager {
|
|
|
52
54
|
this.frame.dataset.originalHref = debugHref;
|
|
53
55
|
this.source = "about:blank";
|
|
54
56
|
|
|
55
|
-
// NEW
|
|
56
57
|
this.wrapper = document.createElement("div");
|
|
57
58
|
this.wrapper.style.position = "relative";
|
|
59
|
+
this.wrapper.style.contain = "strict";
|
|
58
60
|
this.wrapper.style.float = this.wrapper.style.cssFloat = direction === ReadingProgression.rtl ? "right" : "left";
|
|
59
|
-
|
|
60
|
-
this.wrapper.appendChild(this.frame);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
async load(modules: ModuleName[], source: string): Promise<Window> {
|
|
64
|
+
if(!this.frameIsAppended) {
|
|
65
|
+
this.wrapper.appendChild(this.frame);
|
|
66
|
+
this.frameIsAppended = true;
|
|
67
|
+
}
|
|
64
68
|
if(this.source === source && this.loadPromise/* && this.loaded*/) {
|
|
65
69
|
if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
|
|
66
70
|
return this.loadPromise;
|
|
@@ -106,6 +110,7 @@ export class FXLFrameManager {
|
|
|
106
110
|
|
|
107
111
|
// Parses the page size from the viewport meta tag of the loaded resource.
|
|
108
112
|
loadPageSize(): { width: number, height: number } {
|
|
113
|
+
if(this.viewportSize) return this.viewportSize;
|
|
109
114
|
const wnd = this.frame.contentWindow!;
|
|
110
115
|
|
|
111
116
|
// Try to get the page size from the viewport meta tag
|
|
@@ -122,8 +127,10 @@ export class FXLFrameManager {
|
|
|
122
127
|
else if(match[1] === "height")
|
|
123
128
|
height = Number.parseFloat(match[2]);
|
|
124
129
|
}
|
|
125
|
-
if(width > 0 && height > 0)
|
|
126
|
-
|
|
130
|
+
if(width > 0 && height > 0) {
|
|
131
|
+
this.viewportSize = { width, height };
|
|
132
|
+
return this.viewportSize;
|
|
133
|
+
}
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
// Otherwise get it from the size of the loaded content
|
|
@@ -136,25 +143,47 @@ export class FXLFrameManager {
|
|
|
136
143
|
update(page?: Page) {
|
|
137
144
|
if(!this.loaded) return;
|
|
138
145
|
const dimensions = this.loadPageSize();
|
|
139
|
-
this.frame.style.height = `${dimensions.height}px`;
|
|
140
|
-
this.frame.style.width = `${dimensions.width}px`;
|
|
141
146
|
const ratio = Math.min(this.wrapper.clientWidth / dimensions.width, this.wrapper.clientHeight / dimensions.height);
|
|
142
|
-
|
|
147
|
+
if(isTypedOMSupported()) {
|
|
148
|
+
this.frame.attributeStyleMap.set("width", CSS.px(dimensions.width));
|
|
149
|
+
this.frame.attributeStyleMap.set("height", CSS.px(dimensions.height));
|
|
150
|
+
this.frame.attributeStyleMap.set("transform", new CSSTransformValue([
|
|
151
|
+
new CSSScale(ratio, ratio),
|
|
152
|
+
]));
|
|
153
|
+
} else {
|
|
154
|
+
this.frame.style.height = `${dimensions.height}px`;
|
|
155
|
+
this.frame.style.width = `${dimensions.width}px`;
|
|
156
|
+
this.frame.style.transform = `scale(${ratio})`;
|
|
157
|
+
}
|
|
143
158
|
const bcr = this.frame.getBoundingClientRect();
|
|
144
159
|
const hdiff = this.wrapper.clientHeight - bcr.height;
|
|
145
|
-
|
|
146
|
-
if(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
160
|
+
|
|
161
|
+
if(isTypedOMSupported()) {
|
|
162
|
+
this.frame.attributeStyleMap.set("top", CSS.px(hdiff / 2));
|
|
163
|
+
if(page === Page.left) {
|
|
164
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
165
|
+
this.frame.attributeStyleMap.set("left", CSS.px(wdiff));
|
|
166
|
+
} else if(page === Page.center) {
|
|
167
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
168
|
+
this.frame.attributeStyleMap.set("left", CSS.px(wdiff / 2));
|
|
169
|
+
} else {
|
|
170
|
+
this.frame.attributeStyleMap.set("left", CSS.px(0));
|
|
171
|
+
}
|
|
152
172
|
} else {
|
|
153
|
-
this.frame.style.
|
|
173
|
+
this.frame.style.top = `${hdiff / 2}px`;
|
|
174
|
+
if(page === Page.left) {
|
|
175
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
176
|
+
this.frame.style.left = `${wdiff}px`;
|
|
177
|
+
} else if(page === Page.center) {
|
|
178
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
179
|
+
this.frame.style.left = `${wdiff / 2}px`;
|
|
180
|
+
} else {
|
|
181
|
+
this.frame.style.left = "0px";
|
|
182
|
+
}
|
|
154
183
|
}
|
|
155
184
|
|
|
156
185
|
this.frame.style.removeProperty("visibility");
|
|
157
|
-
this.frame.
|
|
186
|
+
this.frame.ariaHidden = null;
|
|
158
187
|
this.frame.style.removeProperty("pointer-events");
|
|
159
188
|
this.frame.classList.remove("blank");
|
|
160
189
|
this.frame.classList.add("loaded");
|
|
@@ -171,7 +200,7 @@ export class FXLFrameManager {
|
|
|
171
200
|
if(!this.loaded) return;
|
|
172
201
|
this.deselect();
|
|
173
202
|
this.frame.style.visibility = "hidden";
|
|
174
|
-
this.frame.
|
|
203
|
+
this.frame.ariaHidden = "true";
|
|
175
204
|
this.frame.style.pointerEvents = "none";
|
|
176
205
|
this.frame.classList.add("blank");
|
|
177
206
|
this.frame.classList.remove("loaded");
|
|
@@ -189,6 +218,8 @@ export class FXLFrameManager {
|
|
|
189
218
|
this.source = "about:blank";
|
|
190
219
|
this.frame.contentWindow!.location.replace("about:blank");
|
|
191
220
|
this.frame.style.display = "none";
|
|
221
|
+
this.frame.style.width = "0px";
|
|
222
|
+
this.frame.style.height = "0px";
|
|
192
223
|
});
|
|
193
224
|
}
|
|
194
225
|
|
|
@@ -296,15 +327,27 @@ export class FXLFrameManager {
|
|
|
296
327
|
}
|
|
297
328
|
|
|
298
329
|
set width(width: number) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
if(isTypedOMSupported()) {
|
|
331
|
+
const current = this.wrapper.attributeStyleMap.get("width");
|
|
332
|
+
if(current instanceof CSSUnitValue && current.unit === "percent" && current.value === width) return;
|
|
333
|
+
this.wrapper.attributeStyleMap.set("width", CSS.percent(width));
|
|
334
|
+
} else {
|
|
335
|
+
const newWidth = `${width}%`;
|
|
336
|
+
if(this.wrapper.style.width === newWidth) return;
|
|
337
|
+
this.wrapper.style.width = newWidth;
|
|
338
|
+
}
|
|
302
339
|
}
|
|
303
340
|
|
|
304
341
|
set height(height: number) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
342
|
+
if(isTypedOMSupported()) {
|
|
343
|
+
const current = this.wrapper.attributeStyleMap.get("height");
|
|
344
|
+
if(current instanceof CSSUnitValue && current.unit === "px" && current.value === height) return;
|
|
345
|
+
this.wrapper.attributeStyleMap.set("height", CSS.px(height));
|
|
346
|
+
} else {
|
|
347
|
+
const newHeight = `${height}px`;
|
|
348
|
+
if(this.wrapper.style.height === newHeight) return;
|
|
349
|
+
this.wrapper.style.height = newHeight;
|
|
350
|
+
}
|
|
308
351
|
}
|
|
309
352
|
|
|
310
353
|
get window() {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ModuleName } from "@readium/navigator-html-injectables";
|
|
2
|
-
import { Locator, Publication, ReadingProgression, Page, Link } from "@readium/shared";
|
|
2
|
+
import { Locator, Publication, ReadingProgression, Page, Link, Layout } from "@readium/shared";
|
|
3
3
|
import { FrameCommsListener } from "../frame/index.ts";
|
|
4
|
-
import
|
|
4
|
+
import FrameBlobBuilder from "../frame/FrameBlobBuilder.ts";
|
|
5
5
|
import { FXLFrameManager } from "./FXLFrameManager.ts";
|
|
6
|
-
import { FXLPeripherals } from "./FXLPeripherals.ts";
|
|
6
|
+
import { FXLPeripherals, isTypedOMSupported } from "./FXLPeripherals.ts";
|
|
7
7
|
import { FXLSpreader, Orientation, Spread } from "./FXLSpreader.ts";
|
|
8
8
|
import { VisualNavigatorViewport, IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
9
9
|
import { Injector } from "../../injection/Injector.ts";
|
|
@@ -21,7 +21,7 @@ export class FXLFramePoolManager {
|
|
|
21
21
|
private readonly container: HTMLElement;
|
|
22
22
|
private readonly positions: Locator[];
|
|
23
23
|
private readonly pool: Map<string, FXLFrameManager> = new Map();
|
|
24
|
-
private readonly blobs: Map<string,
|
|
24
|
+
private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
|
|
25
25
|
private readonly inprogress: Map<string, Promise<void>> = new Map();
|
|
26
26
|
private readonly delayedShow: Map<string, Promise<void>> = new Map();
|
|
27
27
|
private readonly delayedTimeout: Map<string, number> = new Map();
|
|
@@ -37,7 +37,7 @@ export class FXLFramePoolManager {
|
|
|
37
37
|
private readonly pub: Publication;
|
|
38
38
|
public width: number = 0;
|
|
39
39
|
public height: number = 0;
|
|
40
|
-
private transform:
|
|
40
|
+
private transform: number | undefined;
|
|
41
41
|
public currentSlide: number = 0;
|
|
42
42
|
private spreader: FXLSpreader;
|
|
43
43
|
private spread = true; // TODO
|
|
@@ -52,6 +52,7 @@ export class FXLFramePoolManager {
|
|
|
52
52
|
container: HTMLElement,
|
|
53
53
|
positions: Locator[],
|
|
54
54
|
pub: Publication,
|
|
55
|
+
_layout: Layout, // TODO: used by upcoming scrolled-layout support
|
|
55
56
|
injector?: Injector | null,
|
|
56
57
|
contentProtectionConfig?: IContentProtectionConfig,
|
|
57
58
|
keyboardPeripheralsConfig?: IKeyboardPeripheralsConfig,
|
|
@@ -66,7 +67,7 @@ export class FXLFramePoolManager {
|
|
|
66
67
|
|
|
67
68
|
if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
|
|
68
69
|
// TODO support TTB and BTT
|
|
69
|
-
throw Error("Unsupported reading progression for EPUB");
|
|
70
|
+
throw Error("Unsupported reading progression for fixed-layout EPUB");
|
|
70
71
|
|
|
71
72
|
// NEW
|
|
72
73
|
this.spreader = new FXLSpreader(this.pub);
|
|
@@ -86,16 +87,18 @@ export class FXLFramePoolManager {
|
|
|
86
87
|
|
|
87
88
|
this.peripherals = new FXLPeripherals(this);
|
|
88
89
|
|
|
90
|
+
const fragment = document.createDocumentFragment();
|
|
89
91
|
this.pub.readingOrder.items.forEach((link) => {
|
|
90
92
|
// Create <iframe>
|
|
91
93
|
const fm = new FXLFrameManager(this.peripherals, this.pub.metadata.effectiveReadingProgression, link.href, this.contentProtectionConfig, this.keyboardPeripheralsConfig);
|
|
92
|
-
this.spineElement.appendChild(fm.element);
|
|
93
94
|
|
|
94
|
-
// this.pages.push(fm);
|
|
95
95
|
this.pool.set(link.href, fm);
|
|
96
96
|
fm.width = 100 / this.length * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
|
|
97
97
|
fm.height = this.height;
|
|
98
|
+
fragment.appendChild(fm.wrapper);
|
|
98
99
|
});
|
|
100
|
+
this.spineElement.appendChild(fragment);
|
|
101
|
+
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
private _listener!: FrameCommsListener;
|
|
@@ -134,15 +137,18 @@ export class FXLFramePoolManager {
|
|
|
134
137
|
|
|
135
138
|
clearTimeout(this.resizeTimeout);
|
|
136
139
|
this.resizeTimeout = window.setTimeout(() => {
|
|
137
|
-
|
|
140
|
+
const baseWidthFactor = 100 / this.length;
|
|
141
|
+
const itemLookup = new Map(this.pub.readingOrder.items.map(item => [item.href, item]));
|
|
138
142
|
this.pool.forEach((frm, linkHref) => {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
143
|
+
const link = itemLookup.get(linkHref);
|
|
144
|
+
if(!link) return;
|
|
145
|
+
requestAnimationFrame(() => {
|
|
146
|
+
frm.width = baseWidthFactor * (link.properties?.otherProperties["orientation"] === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
|
|
147
|
+
frm.height = this.height;
|
|
148
|
+
if(!frm.loaded) return;
|
|
149
|
+
const spread = this.spreader.findByLink(link)!;
|
|
150
|
+
frm.update(this.spreadPosition(spread, link));
|
|
151
|
+
});
|
|
146
152
|
});
|
|
147
153
|
}, RESIZE_UPDATE_TIMEOUT);
|
|
148
154
|
}
|
|
@@ -181,25 +187,42 @@ export class FXLFramePoolManager {
|
|
|
181
187
|
}
|
|
182
188
|
|
|
183
189
|
public updateSpineStyle(animate: boolean, fast = true) {
|
|
184
|
-
let margin =
|
|
190
|
+
let margin = 0;
|
|
185
191
|
this.updateDimensions();
|
|
186
192
|
if(this.perPage > 1 && true) // this.shift
|
|
187
|
-
margin =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
193
|
+
margin = this.width / 2;
|
|
194
|
+
|
|
195
|
+
if(isTypedOMSupported()) {
|
|
196
|
+
// TODO: don't use UnparsedValue, but rather the longhand of every transition property
|
|
197
|
+
this.spineElement.attributeStyleMap.set("transition", new CSSUnparsedValue([animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out"]));
|
|
198
|
+
this.spineElement.attributeStyleMap.set("margin-right", CSS.px(this.rtl ? margin : 0));
|
|
199
|
+
this.spineElement.attributeStyleMap.set("margin-left", CSS.px(this.rtl ? 0 : margin));
|
|
200
|
+
this.spineElement.attributeStyleMap.set("width", CSS.px((this.width / this.perPage) * this.length));
|
|
201
|
+
this.transform ? this.spineElement.attributeStyleMap.set("transform", new CSSTransformValue([
|
|
202
|
+
new CSSTranslate(CSS.px(this.transform || 0), CSS.px(0), CSS.px(0))
|
|
203
|
+
])) : this.spineElement.attributeStyleMap.delete("transform");
|
|
204
|
+
this.spineElement.attributeStyleMap.set("contain", new CSSUnparsedValue(["content"]));
|
|
205
|
+
} else {
|
|
206
|
+
const spineStyle = {
|
|
207
|
+
transition: animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out",
|
|
208
|
+
marginRight: this.rtl ? `${margin}px` : "0",
|
|
209
|
+
marginLeft: this.rtl ? "0" : `${margin}px`,
|
|
210
|
+
width: `${(this.width / this.perPage) * this.length}px`,
|
|
211
|
+
transform: this.transform ? `translate3d(${this.transform}px, 0px, 0px)` : "",
|
|
212
|
+
// Static (should be moved to CSS)
|
|
213
|
+
contain: "content"
|
|
214
|
+
} as CSSStyleDeclaration;
|
|
215
|
+
Object.assign(this.spineElement.style, spineStyle);
|
|
216
|
+
}
|
|
201
217
|
}
|
|
202
218
|
|
|
219
|
+
// Reused typed transform for the per-event zoom/pan write: allocating
|
|
220
|
+
// fresh CSSTransformValue objects per write is slower than string
|
|
221
|
+
// parsing; mutating a persistent one is faster than both
|
|
222
|
+
private bookScale: CSSScale | null = null;
|
|
223
|
+
private bookTranslate: CSSTranslate | null = null;
|
|
224
|
+
private bookTransform: CSSTransformValue | null = null;
|
|
225
|
+
|
|
203
226
|
public updateBookStyle(initial=false) {
|
|
204
227
|
if(initial) {
|
|
205
228
|
const bookStyle = {
|
|
@@ -218,7 +241,23 @@ export class FXLFramePoolManager {
|
|
|
218
241
|
} as CSSStyleDeclaration;
|
|
219
242
|
Object.assign(this.bookElement.style, bookStyle);
|
|
220
243
|
}
|
|
221
|
-
|
|
244
|
+
const scale = this.peripherals?.scale || 1;
|
|
245
|
+
const translateX = this.peripherals?.pan.translateX || 0;
|
|
246
|
+
const translateY = this.peripherals?.pan.translateY || 0;
|
|
247
|
+
if(isTypedOMSupported()) {
|
|
248
|
+
if(!this.bookTransform) {
|
|
249
|
+
this.bookScale = new CSSScale(1, 1);
|
|
250
|
+
this.bookTranslate = new CSSTranslate(CSS.px(0), CSS.px(0), CSS.px(0));
|
|
251
|
+
this.bookTransform = new CSSTransformValue([this.bookScale, this.bookTranslate]);
|
|
252
|
+
}
|
|
253
|
+
(this.bookScale!.x as CSSUnitValue).value = scale;
|
|
254
|
+
(this.bookScale!.y as CSSUnitValue).value = scale;
|
|
255
|
+
(this.bookTranslate!.x as CSSUnitValue).value = translateX;
|
|
256
|
+
(this.bookTranslate!.y as CSSUnitValue).value = translateY;
|
|
257
|
+
this.bookElement.attributeStyleMap.set("transform", this.bookTransform);
|
|
258
|
+
} else {
|
|
259
|
+
this.bookElement.style.transform = `scale(${scale}) translate3d(${translateX}px, ${translateY}px, 0px)`;
|
|
260
|
+
}
|
|
222
261
|
}
|
|
223
262
|
|
|
224
263
|
/**
|
|
@@ -282,22 +321,22 @@ export class FXLFramePoolManager {
|
|
|
282
321
|
*/
|
|
283
322
|
slideToCurrent(enableTransition?: boolean, fast = true) {
|
|
284
323
|
this.updateDimensions();
|
|
324
|
+
// Always commit: the spine transform may have been written directly
|
|
325
|
+
// by the peripherals during a drag (snap-back relies on this), and
|
|
326
|
+
// reading style.transform back as a string for comparison doesn't
|
|
327
|
+
// match under CSS Typed OM (its serialization differs).
|
|
285
328
|
if (enableTransition) {
|
|
286
329
|
// This one is tricky, I know but this is a perfect explanation:
|
|
287
330
|
// https://youtu.be/cCOL7MC4Pl0
|
|
288
331
|
requestAnimationFrame(() => {
|
|
289
332
|
requestAnimationFrame(() => {
|
|
290
|
-
|
|
291
|
-
if(this.spineElement.style.transform === newTransform) return;
|
|
292
|
-
this.transform = newTransform;
|
|
333
|
+
this.transform = this.offset;
|
|
293
334
|
this.updateSpineStyle(true, fast);
|
|
294
335
|
this.deselect();
|
|
295
336
|
});
|
|
296
337
|
});
|
|
297
338
|
} else {
|
|
298
|
-
|
|
299
|
-
if(this.spineElement.style.transform === newTransform) return;
|
|
300
|
-
this.transform = newTransform;
|
|
339
|
+
this.transform = this.offset;
|
|
301
340
|
this.updateSpineStyle(false);
|
|
302
341
|
this.deselect();
|
|
303
342
|
}
|
|
@@ -305,10 +344,10 @@ export class FXLFramePoolManager {
|
|
|
305
344
|
|
|
306
345
|
bounce(rtl = false) {
|
|
307
346
|
requestAnimationFrame(() => {
|
|
308
|
-
this.transform =
|
|
347
|
+
this.transform = this.offset + (50 * (rtl ? 1 : -1));
|
|
309
348
|
this.updateSpineStyle(true, true);
|
|
310
349
|
setTimeout(() => {
|
|
311
|
-
this.transform =
|
|
350
|
+
this.transform = this.offset;
|
|
312
351
|
this.updateSpineStyle(true, true);
|
|
313
352
|
}, 100);
|
|
314
353
|
});
|
|
@@ -405,7 +444,8 @@ export class FXLFramePoolManager {
|
|
|
405
444
|
this.pool.clear();
|
|
406
445
|
|
|
407
446
|
// Revoke all blobs
|
|
408
|
-
this.blobs.forEach(v =>
|
|
447
|
+
this.blobs.forEach(v => v.reset());
|
|
448
|
+
this.blobs.clear();
|
|
409
449
|
|
|
410
450
|
// Clean up injector if it exists
|
|
411
451
|
this.injector?.dispose();
|
|
@@ -496,13 +536,13 @@ export class FXLFramePoolManager {
|
|
|
496
536
|
if(!this.pool.has(href)) return;
|
|
497
537
|
this.cancelShowing(href);
|
|
498
538
|
await this.pool.get(href)?.unload();
|
|
499
|
-
|
|
539
|
+
this.blobs.get(href)?.reset();
|
|
500
540
|
});
|
|
501
541
|
|
|
502
542
|
// Check if base URL of publication has changed
|
|
503
543
|
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
504
544
|
// Revoke all blobs
|
|
505
|
-
this.blobs.forEach(v =>
|
|
545
|
+
this.blobs.forEach(v => v.reset());
|
|
506
546
|
this.blobs.clear();
|
|
507
547
|
}
|
|
508
548
|
this.currentBaseURL = pub.baseURL;
|
|
@@ -512,16 +552,14 @@ export class FXLFramePoolManager {
|
|
|
512
552
|
const itm = pub.readingOrder.items[index];
|
|
513
553
|
if(!itm) return; // TODO throw?
|
|
514
554
|
if(!this.blobs.has(href)) {
|
|
515
|
-
|
|
555
|
+
this.blobs.set(href, new FrameBlobBuilder(
|
|
516
556
|
pub,
|
|
517
557
|
this.currentBaseURL || "",
|
|
518
558
|
itm,
|
|
519
559
|
{
|
|
520
560
|
injector: this.injector
|
|
521
561
|
}
|
|
522
|
-
);
|
|
523
|
-
const blobURL = await blobBuilder.build(true);
|
|
524
|
-
this.blobs.set(href, blobURL);
|
|
562
|
+
));
|
|
525
563
|
}
|
|
526
564
|
|
|
527
565
|
// Show future offscreen frame in advance after a delay
|
|
@@ -535,7 +573,7 @@ export class FXLFramePoolManager {
|
|
|
535
573
|
const spread = this.makeSpread(this.reAlign(index));
|
|
536
574
|
const page = this.spreadPosition(spread, itm);
|
|
537
575
|
const fm = this.pool.get(href)!;
|
|
538
|
-
await fm.load(modules, this.blobs.get(href)
|
|
576
|
+
await fm.load(modules, await this.blobs.get(href)!.build(true));
|
|
539
577
|
if(!this.peripherals.isScaled) // When scaled, positioning is screwed up, so wait to show
|
|
540
578
|
await fm.show(page); // Show/activate new frame
|
|
541
579
|
this.delayedShow.delete(href);
|
|
@@ -559,10 +597,10 @@ export class FXLFramePoolManager {
|
|
|
559
597
|
for (const s of spread) {
|
|
560
598
|
const newFrame = this.pool.get(s.href)!;
|
|
561
599
|
const source = this.blobs.get(s.href);
|
|
562
|
-
if(!source) continue; //
|
|
600
|
+
if(!source) continue; // Thfis can get destroyed
|
|
563
601
|
|
|
564
602
|
this.cancelShowing(s.href);
|
|
565
|
-
await newFrame.load(modules, source); // In order to ensure modules match the latest configuration
|
|
603
|
+
await newFrame.load(modules, await source.build(true)); // In order to ensure modules match the latest configuration
|
|
566
604
|
await newFrame.show(this.spreadPosition(spread, s)); // Show/activate new frame
|
|
567
605
|
this.previousFrames.push(newFrame);
|
|
568
606
|
await newFrame.activate();
|
|
@@ -35,6 +35,8 @@ export interface Zoomer {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
export const isTypedOMSupported = () => typeof window.CSSTransformValue !== 'undefined';
|
|
39
|
+
|
|
38
40
|
export class FXLPeripherals {
|
|
39
41
|
|
|
40
42
|
private readonly manager: FXLFramePoolManager;
|
|
@@ -339,6 +341,12 @@ export class FXLPeripherals {
|
|
|
339
341
|
|
|
340
342
|
private moveFrame = 0;
|
|
341
343
|
|
|
344
|
+
// Reused typed transform for the per-frame drag write: allocating fresh
|
|
345
|
+
// CSSTransformValue/CSSTranslate objects per write is slower than string
|
|
346
|
+
// parsing; mutating a persistent one is faster than both
|
|
347
|
+
private dragTranslate: CSSTranslate | null = null;
|
|
348
|
+
private dragTransform: CSSTransformValue | null = null;
|
|
349
|
+
|
|
342
350
|
/**
|
|
343
351
|
* touchmove event handler
|
|
344
352
|
*/
|
|
@@ -494,11 +502,24 @@ export class FXLPeripherals {
|
|
|
494
502
|
const currentOffset = currentSlide * (this.manager.width / this.manager.perPage);
|
|
495
503
|
const dragOffset = (this.pan.endX - this.pan.startX);
|
|
496
504
|
const offset = this.manager.rtl ? currentOffset + dragOffset : currentOffset - dragOffset;
|
|
505
|
+
// Snap the translation to device pixels: at fractional offsets the
|
|
506
|
+
// spread gutter is rasterized with a hairline gap between the pages
|
|
507
|
+
const dpr = window.devicePixelRatio || 1;
|
|
508
|
+
const snapped = Math.round((this.manager.rtl ? 1 : -1) * offset * dpr) / dpr;
|
|
497
509
|
|
|
498
510
|
cancelAnimationFrame(this.moveFrame);
|
|
499
511
|
this.moveFrame = requestAnimationFrame(() => {
|
|
500
512
|
requestAnimationFrame(() => {
|
|
501
|
-
|
|
513
|
+
if(isTypedOMSupported()) {
|
|
514
|
+
if(!this.dragTransform) {
|
|
515
|
+
this.dragTranslate = new CSSTranslate(CSS.px(0), CSS.px(0), CSS.px(0));
|
|
516
|
+
this.dragTransform = new CSSTransformValue([this.dragTranslate]);
|
|
517
|
+
}
|
|
518
|
+
(this.dragTranslate!.x as CSSUnitValue).value = snapped;
|
|
519
|
+
this.manager.spineElement.attributeStyleMap.set("transform", this.dragTransform);
|
|
520
|
+
} else {
|
|
521
|
+
this.manager.spineElement.style.transform = `translate3d(${snapped}px, 0, 0)`;
|
|
522
|
+
}
|
|
502
523
|
})
|
|
503
524
|
});
|
|
504
525
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './Navigator.ts';
|
|
|
2
2
|
export * from './decorations/index.ts';
|
|
3
3
|
export * from './webpub/index.ts';
|
|
4
4
|
export * from './epub/index.ts';
|
|
5
|
+
export * from './divina/index.ts';
|
|
5
6
|
export * from './audio/index.ts';
|
|
6
7
|
export * from './helpers/index.ts';
|
|
7
8
|
export * from './preferences/index.ts';
|
|
@@ -12,18 +12,28 @@ export class KeyboardConditionBridge {
|
|
|
12
12
|
setup() {
|
|
13
13
|
let initializing = true;
|
|
14
14
|
|
|
15
|
+
// Subscribe once per distinct condition object: combos commonly share
|
|
16
|
+
// one condition, and a per-combo subscription would trigger one full
|
|
17
|
+
// onUpdate rebuild per combo for a single condition change.
|
|
18
|
+
const combosByCondition = new Map<NonNullable<KeyCombo["condition"]>, KeyCombo[]>();
|
|
15
19
|
this.config.forEach(peripheral =>
|
|
16
20
|
peripheral.keyCombos.forEach(combo => {
|
|
17
21
|
if (combo.condition) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
});
|
|
22
|
-
this.unsubs.push(unsub);
|
|
22
|
+
const combos = combosByCondition.get(combo.condition) ?? [];
|
|
23
|
+
combos.push(combo);
|
|
24
|
+
combosByCondition.set(combo.condition, combos);
|
|
23
25
|
}
|
|
24
26
|
})
|
|
25
27
|
);
|
|
26
28
|
|
|
29
|
+
combosByCondition.forEach((combos, condition) => {
|
|
30
|
+
const unsub = condition.subscribe((value) => {
|
|
31
|
+
combos.forEach(combo => this.conditionValues.set(combo, value));
|
|
32
|
+
if (!initializing) this.onUpdate(this.buildSerializable());
|
|
33
|
+
});
|
|
34
|
+
this.unsubs.push(unsub);
|
|
35
|
+
});
|
|
36
|
+
|
|
27
37
|
initializing = false;
|
|
28
38
|
this.onUpdate(this.buildSerializable());
|
|
29
39
|
}
|
|
@@ -14,11 +14,16 @@ export interface KeyboardPeripheralOptions {
|
|
|
14
14
|
export class KeyboardPeripherals {
|
|
15
15
|
private keydownHandler?: (event: KeyboardEvent) => void;
|
|
16
16
|
private keyManager = new KeyCombinationManager();
|
|
17
|
+
// pagehide instead of unload: an unload listener disqualifies the page
|
|
18
|
+
// from the back/forward cache; only tear down on real unloads.
|
|
19
|
+
private readonly pagehideHandler = (e: PageTransitionEvent) => {
|
|
20
|
+
if (!e.persisted) this.destroy();
|
|
21
|
+
};
|
|
17
22
|
|
|
18
23
|
constructor(options: KeyboardPeripheralOptions = {}) {
|
|
19
24
|
this.setupKeyboardPeripherals(options.keyboardPeripherals || []);
|
|
20
25
|
}
|
|
21
|
-
|
|
26
|
+
|
|
22
27
|
private setupKeyboardPeripherals(keyboardPeripherals: KeyboardPeripheral[]) {
|
|
23
28
|
if (keyboardPeripherals.length > 0) {
|
|
24
29
|
// Create activity event dispatcher
|
|
@@ -28,26 +33,27 @@ export class KeyboardPeripherals {
|
|
|
28
33
|
});
|
|
29
34
|
window.dispatchEvent(customEvent);
|
|
30
35
|
};
|
|
31
|
-
|
|
36
|
+
|
|
32
37
|
// Create unified handler using the provided keyboard peripherals
|
|
33
38
|
this.keydownHandler = this.keyManager.createUnifiedHandler(
|
|
34
39
|
"", // Empty string as target frame source for main window
|
|
35
|
-
keyboardPeripherals,
|
|
40
|
+
keyboardPeripherals,
|
|
36
41
|
dispatcher
|
|
37
42
|
);
|
|
38
|
-
|
|
43
|
+
|
|
39
44
|
if (this.keydownHandler) {
|
|
40
45
|
document.addEventListener("keydown", this.keydownHandler, true);
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
window.addEventListener("
|
|
48
|
+
|
|
49
|
+
window.addEventListener("pagehide", this.pagehideHandler);
|
|
45
50
|
}
|
|
46
|
-
|
|
51
|
+
|
|
47
52
|
public destroy() {
|
|
48
53
|
if (this.keydownHandler) {
|
|
49
54
|
document.removeEventListener("keydown", this.keydownHandler, true);
|
|
50
55
|
this.keydownHandler = undefined;
|
|
51
56
|
}
|
|
57
|
+
window.removeEventListener("pagehide", this.pagehideHandler);
|
|
52
58
|
}
|
|
53
59
|
}
|
|
@@ -25,7 +25,7 @@ export class WebPubFrameManager {
|
|
|
25
25
|
this.frame = document.createElement("iframe");
|
|
26
26
|
this.frame.classList.add("readium-navigator-iframe");
|
|
27
27
|
this.frame.style.visibility = "hidden";
|
|
28
|
-
this.frame.
|
|
28
|
+
this.frame.ariaHidden = "true";
|
|
29
29
|
this.frame.style.opacity = "0";
|
|
30
30
|
this.frame.style.position = "absolute";
|
|
31
31
|
this.frame.style.pointerEvents = "none";
|
|
@@ -109,7 +109,7 @@ export class WebPubFrameManager {
|
|
|
109
109
|
async hide(): Promise<void> {
|
|
110
110
|
if(this.destroyed) return;
|
|
111
111
|
this.frame.style.visibility = "hidden";
|
|
112
|
-
this.frame.
|
|
112
|
+
this.frame.ariaHidden = "true";
|
|
113
113
|
this.frame.style.opacity = "0";
|
|
114
114
|
this.frame.style.pointerEvents = "none";
|
|
115
115
|
this.hidden = true;
|
|
@@ -140,7 +140,7 @@ export class WebPubFrameManager {
|
|
|
140
140
|
this.applyContentProtection();
|
|
141
141
|
const remove = () => {
|
|
142
142
|
this.frame.style.removeProperty("visibility");
|
|
143
|
-
this.frame.
|
|
143
|
+
this.frame.ariaHidden = null;
|
|
144
144
|
this.frame.style.removeProperty("opacity");
|
|
145
145
|
this.frame.style.removeProperty("pointer-events");
|
|
146
146
|
this.hidden = false;
|