@readium/navigator 2.10.3 → 2.11.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/dist/audio/AudioNavigator.js +1 -1
- package/dist/audio/AudioPoolManager.js +1 -1
- package/dist/audio/engine/WebAudioEngine.js +1 -1
- package/dist/divina/DivinaNavigator.js +1 -0
- package/dist/divina/DivinaPageManager.js +1 -0
- package/dist/divina/DivinaPagedPresenter.js +1 -0
- package/dist/divina/DivinaPeripherals.js +1 -0
- package/dist/divina/DivinaScrolledPresenter.js +1 -0
- package/dist/divina/DivinaSpreader.js +1 -0
- package/dist/divina/DivinaVariantSelector.js +1 -0
- package/dist/divina/preferences/DivinaDefaults.js +1 -0
- package/dist/divina/preferences/DivinaPreferences.js +1 -0
- package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
- package/dist/divina/preferences/DivinaSettings.js +1 -0
- package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
- package/dist/epub/EpubNavigator.js +1 -1
- package/dist/epub/frame/FrameBlobBuilder.js +3 -1
- package/dist/epub/frame/FrameComms.js +1 -1
- package/dist/epub/frame/FrameManager.js +1 -1
- package/dist/epub/frame/FramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLFrameManager.js +1 -1
- package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
- package/dist/epub/fxl/FXLPeripherals.js +2 -2
- package/dist/helpers/serialize.js +1 -0
- package/dist/index.js +1 -1
- package/dist/peripherals/KeyboardConditionBridge.js +1 -1
- package/dist/peripherals/KeyboardPeripherals.js +1 -1
- package/dist/webpub/WebPubFrameManager.js +1 -1
- package/dist/webpub/WebPubFramePoolManager.js +1 -1
- package/dist/webpub/WebPubNavigator.js +1 -1
- package/package.json +5 -5
- package/src/audio/AudioNavigator.ts +107 -5
- package/src/audio/AudioPoolManager.ts +14 -8
- package/src/audio/engine/AudioEngine.ts +34 -1
- package/src/audio/engine/WebAudioEngine.ts +48 -3
- package/src/divina/DivinaNavigator.ts +704 -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 +96 -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 +73 -7
- package/src/epub/frame/FrameBlobBuilder.ts +144 -39
- package/src/epub/frame/FrameComms.ts +42 -7
- package/src/epub/frame/FrameManager.ts +27 -16
- package/src/epub/frame/FramePoolManager.ts +106 -48
- package/src/epub/fxl/FXLFrameManager.ts +99 -34
- package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
- package/src/epub/fxl/FXLPeripherals.ts +22 -1
- package/src/helpers/serialize.ts +9 -0
- 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 +27 -16
- package/src/webpub/WebPubFramePoolManager.ts +5 -2
- package/src/webpub/WebPubNavigator.ts +44 -0
- package/types/src/audio/AudioNavigator.d.ts +29 -0
- package/types/src/audio/engine/AudioEngine.d.ts +27 -1
- package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
- package/types/src/divina/DivinaNavigator.d.ts +119 -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 +7 -1
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
- package/types/src/epub/frame/FrameComms.d.ts +8 -1
- package/types/src/epub/frame/FrameManager.d.ts +3 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
- package/types/src/epub/fxl/FXLFrameManager.d.ts +7 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
- package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
- package/types/src/helpers/serialize.d.ts +1 -0
- package/types/src/index.d.ts +1 -0
- package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
- package/types/src/webpub/WebPubFrameManager.d.ts +3 -0
- package/types/src/webpub/WebPubNavigator.d.ts +2 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { ModuleName } from "@readium/navigator-html-injectables";
|
|
2
2
|
import { Locator, Publication } from "@readium/shared";
|
|
3
|
-
import
|
|
3
|
+
import FrameBlobBuilder, { UpgradeInsecureRequests } from "./FrameBlobBuilder.ts";
|
|
4
4
|
import { FrameManager } from "./FrameManager.ts";
|
|
5
5
|
import { Injector } from "../../injection/Injector.ts";
|
|
6
6
|
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
7
7
|
|
|
8
|
-
const UPPER_BOUNDARY =
|
|
9
|
-
const LOWER_BOUNDARY =
|
|
8
|
+
const UPPER_BOUNDARY = 10;
|
|
9
|
+
const LOWER_BOUNDARY = 5;
|
|
10
10
|
|
|
11
11
|
export class FramePoolManager {
|
|
12
12
|
private readonly container: HTMLElement;
|
|
@@ -14,15 +14,18 @@ export class FramePoolManager {
|
|
|
14
14
|
private _currentFrame: FrameManager | undefined;
|
|
15
15
|
private currentCssProperties: { [key: string]: string } | undefined;
|
|
16
16
|
private readonly pool: Map<string, FrameManager> = new Map();
|
|
17
|
-
private readonly blobs: Map<string,
|
|
18
|
-
private readonly inprogress: Map<string, Promise<
|
|
17
|
+
private readonly blobs: Map<string, FrameBlobBuilder> = new Map();
|
|
18
|
+
private readonly inprogress: Map<string, Promise<unknown>> = new Map();
|
|
19
19
|
private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
|
|
20
20
|
private currentBaseURL: string | undefined;
|
|
21
21
|
private readonly injector: Injector | null = null;
|
|
22
22
|
private readonly contentProtectionConfig: IContentProtectionConfig;
|
|
23
23
|
private readonly keyboardPeripheralsConfig: IKeyboardPeripheralsConfig;
|
|
24
|
+
private updateSequence = 0;
|
|
25
|
+
private forcedUpdate: Promise<void> | undefined;
|
|
24
26
|
private resizeWatchSelectors: string[];
|
|
25
27
|
private readonly getFragmentIds: (href: string) => string[];
|
|
28
|
+
private readonly upgradeInsecureRequests: UpgradeInsecureRequests;
|
|
26
29
|
|
|
27
30
|
constructor(
|
|
28
31
|
container: HTMLElement,
|
|
@@ -32,7 +35,8 @@ export class FramePoolManager {
|
|
|
32
35
|
contentProtectionConfig?: IContentProtectionConfig,
|
|
33
36
|
keyboardPeripheralsConfig?: IKeyboardPeripheralsConfig,
|
|
34
37
|
getFragmentIds?: (href: string) => string[],
|
|
35
|
-
resizeWatchSelectors: string[] = []
|
|
38
|
+
resizeWatchSelectors: string[] = [],
|
|
39
|
+
upgradeInsecureRequests?: UpgradeInsecureRequests
|
|
36
40
|
) {
|
|
37
41
|
this.container = container;
|
|
38
42
|
this.positions = positions;
|
|
@@ -42,6 +46,7 @@ export class FramePoolManager {
|
|
|
42
46
|
this.keyboardPeripheralsConfig = keyboardPeripheralsConfig || [];
|
|
43
47
|
this.getFragmentIds = getFragmentIds ?? (() => []);
|
|
44
48
|
this.resizeWatchSelectors = [...resizeWatchSelectors];
|
|
49
|
+
this.upgradeInsecureRequests = upgradeInsecureRequests ?? 'always';
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
addResizeTarget(selector: string): void {
|
|
@@ -61,7 +66,7 @@ export class FramePoolManager {
|
|
|
61
66
|
// Wait for all in-progress loads to complete
|
|
62
67
|
let iit = this.inprogress.values();
|
|
63
68
|
let inp = iit.next();
|
|
64
|
-
const inprogressPromises: Promise<
|
|
69
|
+
const inprogressPromises: Promise<unknown>[] = [];
|
|
65
70
|
while(inp.value) {
|
|
66
71
|
inprogressPromises.push(inp.value);
|
|
67
72
|
inp = iit.next();
|
|
@@ -81,10 +86,8 @@ export class FramePoolManager {
|
|
|
81
86
|
this.pool.clear();
|
|
82
87
|
|
|
83
88
|
// Revoke all blobs
|
|
84
|
-
this.blobs.forEach(v =>
|
|
85
|
-
|
|
86
|
-
URL.revokeObjectURL(v);
|
|
87
|
-
});
|
|
89
|
+
this.blobs.forEach(v => v.reset());
|
|
90
|
+
this.blobs.clear();
|
|
88
91
|
|
|
89
92
|
// Clean up injector if it exists
|
|
90
93
|
this.injector?.dispose();
|
|
@@ -96,6 +99,9 @@ export class FramePoolManager {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
async update(pub: Publication, locator: Locator, modules: ModuleName[], force=false) {
|
|
102
|
+
// Updates arriving mid-rebuild must not cancel it, or the old frame is gone with no replacement
|
|
103
|
+
while (this.forcedUpdate) await this.forcedUpdate;
|
|
104
|
+
const updateSequence = ++this.updateSequence;
|
|
99
105
|
let i = this.positions.findIndex(l => l.locations.position === locator.locations.position);
|
|
100
106
|
if(i < 0) throw Error(`Locator not found in position list: ${locator.locations.position} > ${this.positions.reduce<number>((acc, l) => l.locations.position || 0 > acc ? l.locations.position || 0 : acc, 0) }`);
|
|
101
107
|
const newHref = this.positions[i].href;
|
|
@@ -125,39 +131,33 @@ export class FramePoolManager {
|
|
|
125
131
|
this.pool.delete(href);
|
|
126
132
|
if(this.pendingUpdates.has(href))
|
|
127
133
|
this.pendingUpdates.set(href, { inPool: false });
|
|
134
|
+
// Note that we don't reset the blob here, unlike in the FXL pool.
|
|
135
|
+
// This is because FXL tends to have a ton more blobs. Maybe we'll adjust
|
|
136
|
+
// this at a later point with a much larger boundary for resets to deal
|
|
137
|
+
// with extremely long/large reflowable publications.
|
|
138
|
+
// Reflowable publication resources also tend to be much larger documents,
|
|
139
|
+
// so they're more expensive to preprocess with the FrameBlobBuilder.
|
|
128
140
|
});
|
|
129
141
|
|
|
130
142
|
// Check if base URL of publication has changed
|
|
131
143
|
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
132
144
|
// Revoke all blobs
|
|
133
|
-
this.blobs.forEach(v =>
|
|
134
|
-
this.injector?.releaseBlobUrl?.(v);
|
|
135
|
-
URL.revokeObjectURL(v);
|
|
136
|
-
});
|
|
145
|
+
this.blobs.forEach(v => v.reset());
|
|
137
146
|
this.blobs.clear();
|
|
138
147
|
}
|
|
139
148
|
this.currentBaseURL = pub.baseURL;
|
|
140
149
|
|
|
150
|
+
if(force) {
|
|
151
|
+
this.blobs.forEach(v => v.reset());
|
|
152
|
+
this.blobs.clear();
|
|
153
|
+
this.pendingUpdates.clear();
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
const creator = async (href: string) => {
|
|
142
|
-
if(force) {
|
|
143
|
-
// Revoke all blobs so that CSSProperties are not stale
|
|
144
|
-
// When using force, we switch scroll/paginated
|
|
145
|
-
// If this property is not up to date, it creates issues
|
|
146
|
-
// when navigating backwards, where paginated will go the
|
|
147
|
-
// start of the resource instead of the end due to the
|
|
148
|
-
// corrupted width ColumnSnapper (injectables) gets on init
|
|
149
|
-
this.blobs.forEach(v => {
|
|
150
|
-
this.injector?.releaseBlobUrl?.(v);
|
|
151
|
-
URL.revokeObjectURL(v);
|
|
152
|
-
});
|
|
153
|
-
this.blobs.clear();
|
|
154
|
-
this.pendingUpdates.clear();
|
|
155
|
-
}
|
|
156
157
|
if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
|
|
157
|
-
const
|
|
158
|
-
if(
|
|
159
|
-
|
|
160
|
-
URL.revokeObjectURL(url);
|
|
158
|
+
const v = this.blobs.get(href);
|
|
159
|
+
if(v) {
|
|
160
|
+
v.reset();
|
|
161
161
|
this.blobs.delete(href);
|
|
162
162
|
this.pendingUpdates.delete(href);
|
|
163
163
|
}
|
|
@@ -176,44 +176,92 @@ export class FramePoolManager {
|
|
|
176
176
|
const itm = pub.readingOrder.findWithHref(href);
|
|
177
177
|
if(!itm) return; // TODO throw?
|
|
178
178
|
if(!this.blobs.has(href)) {
|
|
179
|
-
|
|
179
|
+
this.blobs.set(href, new FrameBlobBuilder(
|
|
180
180
|
pub,
|
|
181
181
|
this.currentBaseURL || "",
|
|
182
182
|
itm,
|
|
183
183
|
{
|
|
184
184
|
cssProperties: this.currentCssProperties,
|
|
185
|
-
injector: this.injector
|
|
185
|
+
injector: this.injector,
|
|
186
|
+
upgradeInsecureRequests: this.upgradeInsecureRequests
|
|
186
187
|
}
|
|
187
|
-
);
|
|
188
|
-
const blobURL = await blobBuilder.build();
|
|
189
|
-
this.blobs.set(href, blobURL);
|
|
188
|
+
));
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
// Create <iframe>
|
|
193
|
-
const fm = new FrameManager(this.blobs.get(href)
|
|
192
|
+
const fm = new FrameManager(await this.blobs.get(href)!.build(), this.contentProtectionConfig, this.keyboardPeripheralsConfig, this.getFragmentIds(href), this.resizeWatchSelectors);
|
|
194
193
|
if(href !== newHref) await fm.hide(); // Avoid unecessary hide
|
|
195
194
|
this.container.appendChild(fm.iframe);
|
|
196
195
|
await fm.load(modules);
|
|
197
196
|
this.pool.set(href, fm);
|
|
197
|
+
return fm;
|
|
198
198
|
}
|
|
199
|
+
|
|
200
|
+
// Target frame is awaited
|
|
199
201
|
try {
|
|
200
|
-
await Promise.all(creation.map(href => creator(href)));
|
|
202
|
+
await Promise.all(creation.filter(href => href === newHref).map(href => creator(href)));
|
|
201
203
|
} catch (error) {
|
|
202
204
|
reject(error);
|
|
205
|
+
return;
|
|
203
206
|
}
|
|
204
207
|
|
|
208
|
+
if (updateSequence !== this.updateSequence) {
|
|
209
|
+
resolve();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
Promise.all(creation.filter(href => href !== newHref && !this.inprogress.has(href)).map(async href => {
|
|
214
|
+
const c = creator(href);
|
|
215
|
+
this.inprogress.set(href, c);
|
|
216
|
+
try {
|
|
217
|
+
await c;
|
|
218
|
+
} finally {
|
|
219
|
+
if (this.inprogress.get(href) === c) this.inprogress.delete(href);
|
|
220
|
+
}
|
|
221
|
+
})).catch(error => console.warn("Failed preloading frame", error));
|
|
222
|
+
|
|
205
223
|
// Update current frame
|
|
206
|
-
const newFrame = this.pool.get(newHref)
|
|
224
|
+
const newFrame = this.pool.get(newHref);
|
|
225
|
+
if (!newFrame) {
|
|
226
|
+
resolve();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
207
230
|
if(newFrame?.source !== this._currentFrame?.source || force) {
|
|
208
231
|
await this._currentFrame?.hide(); // Hide current frame. It's possible it no longer even exists in the DOM at this point
|
|
209
|
-
|
|
210
|
-
|
|
232
|
+
|
|
233
|
+
// Resolve if there is a concurrent update that has started since this one began
|
|
234
|
+
if (updateSequence !== this.updateSequence) {
|
|
235
|
+
resolve();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// If user is speeding through the publication, this can get destroyed
|
|
240
|
+
const currentFrame = this.pool.get(newHref);
|
|
241
|
+
if (!currentFrame) {
|
|
242
|
+
resolve();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
await currentFrame.load(modules); // In order to ensure modules match the latest configuration
|
|
247
|
+
|
|
248
|
+
// Resolve if there is a concurrent update that has started since this one began
|
|
249
|
+
if (updateSequence !== this.updateSequence) {
|
|
250
|
+
resolve();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Check if it was destroyed, again
|
|
255
|
+
const latestFrame = this.pool.get(newHref);
|
|
256
|
+
if (!latestFrame) {
|
|
257
|
+
resolve();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
211
260
|
|
|
212
261
|
// Update progression if necessary and show the new frame
|
|
213
|
-
|
|
214
|
-
await newFrame.show(locator.locations.progression); // Show/activate new frame
|
|
262
|
+
await latestFrame.show(locator.locations.progression); // Show/activate new frame
|
|
215
263
|
|
|
216
|
-
this._currentFrame =
|
|
264
|
+
this._currentFrame = latestFrame;
|
|
217
265
|
|
|
218
266
|
// Safari retains focus on the hidden iframe; transfer it to the new
|
|
219
267
|
// frame only if no meaningful element in the parent document owns focus.
|
|
@@ -227,8 +275,18 @@ export class FramePoolManager {
|
|
|
227
275
|
});
|
|
228
276
|
|
|
229
277
|
this.inprogress.set(newHref, progressPromise); // Add the job to the in progress map
|
|
230
|
-
|
|
231
|
-
|
|
278
|
+
if (force) {
|
|
279
|
+
const forced = progressPromise.catch(() => {});
|
|
280
|
+
this.forcedUpdate = forced;
|
|
281
|
+
forced.then(() => {
|
|
282
|
+
if (this.forcedUpdate === forced) this.forcedUpdate = undefined;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
await progressPromise; // Wait on the job to finish...
|
|
287
|
+
} finally {
|
|
288
|
+
this.inprogress.delete(newHref); // Delete it from the in progress map!
|
|
289
|
+
}
|
|
232
290
|
}
|
|
233
291
|
|
|
234
292
|
setCSSProperties(properties: { [key: string]: string }) {
|
|
@@ -1,13 +1,15 @@
|
|
|
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 "@readium/navigator-html-injectables";
|
|
6
6
|
import { IContentProtectionConfig, IKeyboardPeripheralsConfig } from "../../Navigator.ts";
|
|
7
7
|
import { KeyboardConditionBridge } from "../../peripherals/KeyboardConditionBridge.ts";
|
|
8
|
+
import { createSerializer } from "../../helpers/serialize.ts";
|
|
8
9
|
|
|
9
10
|
export class FXLFrameManager {
|
|
10
11
|
private frame: HTMLIFrameElement;
|
|
12
|
+
private frameIsAppended = false;
|
|
11
13
|
private loader: Loader | undefined;
|
|
12
14
|
public source: string;
|
|
13
15
|
private comms: FrameComms | undefined;
|
|
@@ -24,6 +26,9 @@ export class FXLFrameManager {
|
|
|
24
26
|
public debugHref: string;
|
|
25
27
|
private loadPromise: Promise<Window> | undefined;
|
|
26
28
|
private showPromise: Promise<void> | undefined;
|
|
29
|
+
private viewportSize: { width: number, height: number } | undefined = undefined;
|
|
30
|
+
// Halting comms drops pending acks, so an operation must not start while another awaits one
|
|
31
|
+
private readonly serialize = createSerializer();
|
|
27
32
|
|
|
28
33
|
constructor(
|
|
29
34
|
peripherals: FXLPeripherals,
|
|
@@ -45,7 +50,7 @@ export class FXLFrameManager {
|
|
|
45
50
|
this.frame.classList.add("blank");
|
|
46
51
|
this.frame.scrolling = "no";
|
|
47
52
|
this.frame.style.visibility = "hidden";
|
|
48
|
-
this.frame.
|
|
53
|
+
this.frame.ariaHidden = "true";
|
|
49
54
|
this.frame.style.display = "none";
|
|
50
55
|
this.frame.style.position = "absolute";
|
|
51
56
|
this.frame.style.pointerEvents = "none";
|
|
@@ -56,15 +61,21 @@ export class FXLFrameManager {
|
|
|
56
61
|
this.frame.dataset.originalHref = debugHref;
|
|
57
62
|
this.source = "about:blank";
|
|
58
63
|
|
|
59
|
-
// NEW
|
|
60
64
|
this.wrapper = document.createElement("div");
|
|
61
65
|
this.wrapper.style.position = "relative";
|
|
66
|
+
this.wrapper.style.contain = "strict";
|
|
62
67
|
this.wrapper.style.float = this.wrapper.style.cssFloat = direction === ReadingProgression.rtl ? "right" : "left";
|
|
63
|
-
|
|
64
|
-
this.wrapper.appendChild(this.frame);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
async load(modules: ModuleName[], source: string): Promise<Window> {
|
|
71
|
+
return this.serialize(() => this.loadNow(modules, source));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async loadNow(modules: ModuleName[], source: string): Promise<Window> {
|
|
75
|
+
if(!this.frameIsAppended) {
|
|
76
|
+
this.wrapper.appendChild(this.frame);
|
|
77
|
+
this.frameIsAppended = true;
|
|
78
|
+
}
|
|
68
79
|
if(this.source === source && this.loadPromise/* && this.loaded*/) {
|
|
69
80
|
if([...this.currModules].sort().join("|") === [...modules].sort().join("|")) {
|
|
70
81
|
return this.loadPromise;
|
|
@@ -110,6 +121,7 @@ export class FXLFrameManager {
|
|
|
110
121
|
|
|
111
122
|
// Parses the page size from the viewport meta tag of the loaded resource.
|
|
112
123
|
loadPageSize(): { width: number, height: number } {
|
|
124
|
+
if(this.viewportSize) return this.viewportSize;
|
|
113
125
|
const wnd = this.frame.contentWindow!;
|
|
114
126
|
|
|
115
127
|
// Try to get the page size from the viewport meta tag
|
|
@@ -126,8 +138,10 @@ export class FXLFrameManager {
|
|
|
126
138
|
else if(match[1] === "height")
|
|
127
139
|
height = Number.parseFloat(match[2]);
|
|
128
140
|
}
|
|
129
|
-
if(width > 0 && height > 0)
|
|
130
|
-
|
|
141
|
+
if(width > 0 && height > 0) {
|
|
142
|
+
this.viewportSize = { width, height };
|
|
143
|
+
return this.viewportSize;
|
|
144
|
+
}
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
// Otherwise get it from the size of the loaded content
|
|
@@ -140,42 +154,70 @@ export class FXLFrameManager {
|
|
|
140
154
|
update(page?: Page) {
|
|
141
155
|
if(!this.loaded) return;
|
|
142
156
|
const dimensions = this.loadPageSize();
|
|
143
|
-
this.frame.style.height = `${dimensions.height}px`;
|
|
144
|
-
this.frame.style.width = `${dimensions.width}px`;
|
|
145
157
|
const ratio = Math.min(this.wrapper.clientWidth / dimensions.width, this.wrapper.clientHeight / dimensions.height);
|
|
146
|
-
|
|
158
|
+
if(isTypedOMSupported()) {
|
|
159
|
+
this.frame.attributeStyleMap.set("width", CSS.px(dimensions.width));
|
|
160
|
+
this.frame.attributeStyleMap.set("height", CSS.px(dimensions.height));
|
|
161
|
+
this.frame.attributeStyleMap.set("transform", new CSSTransformValue([
|
|
162
|
+
new CSSScale(ratio, ratio),
|
|
163
|
+
]));
|
|
164
|
+
} else {
|
|
165
|
+
this.frame.style.height = `${dimensions.height}px`;
|
|
166
|
+
this.frame.style.width = `${dimensions.width}px`;
|
|
167
|
+
this.frame.style.transform = `scale(${ratio})`;
|
|
168
|
+
}
|
|
147
169
|
const bcr = this.frame.getBoundingClientRect();
|
|
148
170
|
const hdiff = this.wrapper.clientHeight - bcr.height;
|
|
149
|
-
|
|
150
|
-
if(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
|
|
172
|
+
if(isTypedOMSupported()) {
|
|
173
|
+
this.frame.attributeStyleMap.set("top", CSS.px(hdiff / 2));
|
|
174
|
+
if(page === Page.left) {
|
|
175
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
176
|
+
this.frame.attributeStyleMap.set("left", CSS.px(wdiff));
|
|
177
|
+
} else if(page === Page.center) {
|
|
178
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
179
|
+
this.frame.attributeStyleMap.set("left", CSS.px(wdiff / 2));
|
|
180
|
+
} else {
|
|
181
|
+
this.frame.attributeStyleMap.set("left", CSS.px(0));
|
|
182
|
+
}
|
|
156
183
|
} else {
|
|
157
|
-
this.frame.style.
|
|
184
|
+
this.frame.style.top = `${hdiff / 2}px`;
|
|
185
|
+
if(page === Page.left) {
|
|
186
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
187
|
+
this.frame.style.left = `${wdiff}px`;
|
|
188
|
+
} else if(page === Page.center) {
|
|
189
|
+
const wdiff = this.wrapper.clientWidth - bcr.width;
|
|
190
|
+
this.frame.style.left = `${wdiff / 2}px`;
|
|
191
|
+
} else {
|
|
192
|
+
this.frame.style.left = "0px";
|
|
193
|
+
}
|
|
158
194
|
}
|
|
159
195
|
|
|
160
196
|
this.frame.style.removeProperty("visibility");
|
|
161
|
-
this.frame.
|
|
197
|
+
this.frame.ariaHidden = null;
|
|
162
198
|
this.frame.style.removeProperty("pointer-events");
|
|
163
199
|
this.frame.classList.remove("blank");
|
|
164
200
|
this.frame.classList.add("loaded");
|
|
165
201
|
}
|
|
166
202
|
|
|
167
203
|
async destroy() {
|
|
168
|
-
this.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
204
|
+
return this.serialize(async () => {
|
|
205
|
+
this.conditionBridge?.destroy();
|
|
206
|
+
await this.unfocusNow();
|
|
207
|
+
this.loader?.destroy();
|
|
208
|
+
this.wrapper.remove();
|
|
209
|
+
});
|
|
172
210
|
}
|
|
173
211
|
|
|
174
212
|
async unload() {
|
|
213
|
+
return this.serialize(() => this.unloadNow());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private async unloadNow() {
|
|
175
217
|
if(!this.loaded) return;
|
|
176
218
|
this.deselect();
|
|
177
219
|
this.frame.style.visibility = "hidden";
|
|
178
|
-
this.frame.
|
|
220
|
+
this.frame.ariaHidden = "true";
|
|
179
221
|
this.frame.style.pointerEvents = "none";
|
|
180
222
|
this.frame.classList.add("blank");
|
|
181
223
|
this.frame.classList.remove("loaded");
|
|
@@ -193,6 +235,8 @@ export class FXLFrameManager {
|
|
|
193
235
|
this.source = "about:blank";
|
|
194
236
|
this.frame.contentWindow!.location.replace("about:blank");
|
|
195
237
|
this.frame.style.display = "none";
|
|
238
|
+
this.frame.style.width = "0px";
|
|
239
|
+
this.frame.style.height = "0px";
|
|
196
240
|
});
|
|
197
241
|
}
|
|
198
242
|
|
|
@@ -201,8 +245,13 @@ export class FXLFrameManager {
|
|
|
201
245
|
}
|
|
202
246
|
|
|
203
247
|
async unfocus(): Promise<void> {
|
|
248
|
+
return this.serialize(() => this.unfocusNow());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private async unfocusNow(): Promise<void> {
|
|
204
252
|
if(this.frame.parentElement) {
|
|
205
|
-
|
|
253
|
+
// A halted channel no longer receives acks
|
|
254
|
+
if(this.comms === undefined || !this.comms.ready) return;
|
|
206
255
|
// Return focus to the parent document so keyboard events aren't silently
|
|
207
256
|
// swallowed by an off-screen iframe whose comms channel has been halted.
|
|
208
257
|
this.frame.blur();
|
|
@@ -270,6 +319,10 @@ export class FXLFrameManager {
|
|
|
270
319
|
|
|
271
320
|
private cachedPage: Page | undefined = undefined;
|
|
272
321
|
async show(page: Page): Promise<void> {
|
|
322
|
+
return this.serialize(() => this.showNow(page));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
private async showNow(page: Page): Promise<void> {
|
|
273
326
|
if(!this.frame.parentElement) {
|
|
274
327
|
console.warn("Trying to show frame that is not attached to the DOM");
|
|
275
328
|
return;
|
|
@@ -301,12 +354,12 @@ export class FXLFrameManager {
|
|
|
301
354
|
}
|
|
302
355
|
|
|
303
356
|
async activate(): Promise<void> {
|
|
304
|
-
return new Promise<void>((res, _) => {
|
|
305
|
-
if(!this.comms) return res(); // TODO: investigate when this is the case
|
|
357
|
+
return this.serialize(() => new Promise<void>((res, _) => {
|
|
358
|
+
if(!this.comms?.ready) return res(); // TODO: investigate when this is the case
|
|
306
359
|
this.comms?.send("activate", undefined, () => {
|
|
307
360
|
res();
|
|
308
361
|
});
|
|
309
|
-
});
|
|
362
|
+
}));
|
|
310
363
|
}
|
|
311
364
|
|
|
312
365
|
get element() {
|
|
@@ -326,15 +379,27 @@ export class FXLFrameManager {
|
|
|
326
379
|
}
|
|
327
380
|
|
|
328
381
|
set width(width: number) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
382
|
+
if(isTypedOMSupported()) {
|
|
383
|
+
const current = this.wrapper.attributeStyleMap.get("width");
|
|
384
|
+
if(current instanceof CSSUnitValue && current.unit === "percent" && current.value === width) return;
|
|
385
|
+
this.wrapper.attributeStyleMap.set("width", CSS.percent(width));
|
|
386
|
+
} else {
|
|
387
|
+
const newWidth = `${width}%`;
|
|
388
|
+
if(this.wrapper.style.width === newWidth) return;
|
|
389
|
+
this.wrapper.style.width = newWidth;
|
|
390
|
+
}
|
|
332
391
|
}
|
|
333
392
|
|
|
334
393
|
set height(height: number) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
394
|
+
if(isTypedOMSupported()) {
|
|
395
|
+
const current = this.wrapper.attributeStyleMap.get("height");
|
|
396
|
+
if(current instanceof CSSUnitValue && current.unit === "px" && current.value === height) return;
|
|
397
|
+
this.wrapper.attributeStyleMap.set("height", CSS.px(height));
|
|
398
|
+
} else {
|
|
399
|
+
const newHeight = `${height}px`;
|
|
400
|
+
if(this.wrapper.style.height === newHeight) return;
|
|
401
|
+
this.wrapper.style.height = newHeight;
|
|
402
|
+
}
|
|
338
403
|
}
|
|
339
404
|
|
|
340
405
|
get window() {
|