@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.
Files changed (94) hide show
  1. package/dist/audio/AudioNavigator.js +1 -1
  2. package/dist/audio/AudioPoolManager.js +1 -1
  3. package/dist/audio/engine/WebAudioEngine.js +1 -1
  4. package/dist/divina/DivinaNavigator.js +1 -0
  5. package/dist/divina/DivinaPageManager.js +1 -0
  6. package/dist/divina/DivinaPagedPresenter.js +1 -0
  7. package/dist/divina/DivinaPeripherals.js +1 -0
  8. package/dist/divina/DivinaScrolledPresenter.js +1 -0
  9. package/dist/divina/DivinaSpreader.js +1 -0
  10. package/dist/divina/DivinaVariantSelector.js +1 -0
  11. package/dist/divina/preferences/DivinaDefaults.js +1 -0
  12. package/dist/divina/preferences/DivinaPreferences.js +1 -0
  13. package/dist/divina/preferences/DivinaPreferencesEditor.js +1 -0
  14. package/dist/divina/preferences/DivinaSettings.js +1 -0
  15. package/dist/divina/protection/DivinaNavigatorProtector.js +1 -0
  16. package/dist/epub/EpubNavigator.js +1 -1
  17. package/dist/epub/frame/FrameBlobBuilder.js +3 -1
  18. package/dist/epub/frame/FrameComms.js +1 -1
  19. package/dist/epub/frame/FrameManager.js +1 -1
  20. package/dist/epub/frame/FramePoolManager.js +1 -1
  21. package/dist/epub/fxl/FXLFrameManager.js +1 -1
  22. package/dist/epub/fxl/FXLFramePoolManager.js +1 -1
  23. package/dist/epub/fxl/FXLPeripherals.js +2 -2
  24. package/dist/helpers/serialize.js +1 -0
  25. package/dist/index.js +1 -1
  26. package/dist/peripherals/KeyboardConditionBridge.js +1 -1
  27. package/dist/peripherals/KeyboardPeripherals.js +1 -1
  28. package/dist/webpub/WebPubFrameManager.js +1 -1
  29. package/dist/webpub/WebPubFramePoolManager.js +1 -1
  30. package/dist/webpub/WebPubNavigator.js +1 -1
  31. package/package.json +5 -5
  32. package/src/audio/AudioNavigator.ts +107 -5
  33. package/src/audio/AudioPoolManager.ts +14 -8
  34. package/src/audio/engine/AudioEngine.ts +34 -1
  35. package/src/audio/engine/WebAudioEngine.ts +48 -3
  36. package/src/divina/DivinaNavigator.ts +704 -0
  37. package/src/divina/DivinaPageManager.ts +322 -0
  38. package/src/divina/DivinaPagedPresenter.ts +508 -0
  39. package/src/divina/DivinaPeripherals.ts +630 -0
  40. package/src/divina/DivinaScrolledPresenter.ts +516 -0
  41. package/src/divina/DivinaSpreader.ts +202 -0
  42. package/src/divina/DivinaVariantSelector.ts +77 -0
  43. package/src/divina/index.ts +8 -0
  44. package/src/divina/preferences/DivinaDefaults.ts +37 -0
  45. package/src/divina/preferences/DivinaPreferences.ts +77 -0
  46. package/src/divina/preferences/DivinaPreferencesEditor.ts +96 -0
  47. package/src/divina/preferences/DivinaSettings.ts +38 -0
  48. package/src/divina/preferences/index.ts +4 -0
  49. package/src/divina/protection/DivinaNavigatorProtector.ts +44 -0
  50. package/src/epub/EpubNavigator.ts +73 -7
  51. package/src/epub/frame/FrameBlobBuilder.ts +144 -39
  52. package/src/epub/frame/FrameComms.ts +42 -7
  53. package/src/epub/frame/FrameManager.ts +27 -16
  54. package/src/epub/frame/FramePoolManager.ts +106 -48
  55. package/src/epub/fxl/FXLFrameManager.ts +99 -34
  56. package/src/epub/fxl/FXLFramePoolManager.ts +98 -53
  57. package/src/epub/fxl/FXLPeripherals.ts +22 -1
  58. package/src/helpers/serialize.ts +9 -0
  59. package/src/index.ts +1 -0
  60. package/src/peripherals/KeyboardConditionBridge.ts +15 -5
  61. package/src/peripherals/KeyboardPeripherals.ts +13 -7
  62. package/src/webpub/WebPubFrameManager.ts +27 -16
  63. package/src/webpub/WebPubFramePoolManager.ts +5 -2
  64. package/src/webpub/WebPubNavigator.ts +44 -0
  65. package/types/src/audio/AudioNavigator.d.ts +29 -0
  66. package/types/src/audio/engine/AudioEngine.d.ts +27 -1
  67. package/types/src/audio/engine/WebAudioEngine.d.ts +14 -2
  68. package/types/src/divina/DivinaNavigator.d.ts +119 -0
  69. package/types/src/divina/DivinaPageManager.d.ts +66 -0
  70. package/types/src/divina/DivinaPagedPresenter.d.ts +122 -0
  71. package/types/src/divina/DivinaPeripherals.d.ts +121 -0
  72. package/types/src/divina/DivinaScrolledPresenter.d.ts +125 -0
  73. package/types/src/divina/DivinaSpreader.d.ts +65 -0
  74. package/types/src/divina/DivinaVariantSelector.d.ts +30 -0
  75. package/types/src/divina/index.d.ts +8 -0
  76. package/types/src/divina/preferences/DivinaDefaults.d.ts +18 -0
  77. package/types/src/divina/preferences/DivinaPreferences.d.ts +33 -0
  78. package/types/src/divina/preferences/DivinaPreferencesEditor.d.ts +20 -0
  79. package/types/src/divina/preferences/DivinaSettings.d.ts +26 -0
  80. package/types/src/divina/preferences/index.d.ts +4 -0
  81. package/types/src/divina/protection/DivinaNavigatorProtector.d.ts +14 -0
  82. package/types/src/epub/EpubNavigator.d.ts +7 -1
  83. package/types/src/epub/frame/FrameBlobBuilder.d.ts +19 -3
  84. package/types/src/epub/frame/FrameComms.d.ts +8 -1
  85. package/types/src/epub/frame/FrameManager.d.ts +3 -0
  86. package/types/src/epub/frame/FramePoolManager.d.ts +5 -1
  87. package/types/src/epub/fxl/FXLFrameManager.d.ts +7 -0
  88. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +8 -2
  89. package/types/src/epub/fxl/FXLPeripherals.d.ts +3 -0
  90. package/types/src/helpers/serialize.d.ts +1 -0
  91. package/types/src/index.d.ts +1 -0
  92. package/types/src/peripherals/KeyboardPeripherals.d.ts +1 -0
  93. package/types/src/webpub/WebPubFrameManager.d.ts +3 -0
  94. 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 FrameBlobBuider from "./FrameBlobBuilder.ts";
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 = 5;
9
- const LOWER_BOUNDARY = 3;
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, string> = new Map();
18
- private readonly inprogress: Map<string, Promise<void>> = new Map();
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<void>[] = [];
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
- this.injector?.releaseBlobUrl?.(v);
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 url = this.blobs.get(href);
158
- if(url) {
159
- this.injector?.releaseBlobUrl?.(url);
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
- const blobBuilder = new FrameBlobBuider(
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)!, this.contentProtectionConfig, this.keyboardPeripheralsConfig, this.getFragmentIds(href), this.resizeWatchSelectors);
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
- if(newFrame) // If user is speeding through the publication, this can get destroyed
210
- await newFrame.load(modules); // In order to ensure modules match the latest configuration
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
- if(newFrame) // If user is speeding through the publication, this can get destroyed
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 = newFrame;
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
- await progressPromise; // Wait on the job to finish...
231
- this.inprogress.delete(newHref); // Delete it from the in progress map!
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.style.setProperty("aria-hidden", "true");
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
- return { width, height };
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
- this.frame.style.transform = `scale(${ratio})`;
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
- this.frame.style.top = `${hdiff / 2}px`;
150
- if(page === Page.left) {
151
- const wdiff = this.wrapper.clientWidth - bcr.width;
152
- this.frame.style.left = `${wdiff}px`;
153
- } else if(page === Page.center) {
154
- const wdiff = this.wrapper.clientWidth - bcr.width;
155
- this.frame.style.left = `${wdiff / 2}px`;
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.left = "0px";
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.style.removeProperty("aria-hidden");
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.conditionBridge?.destroy();
169
- await this.unfocus();
170
- this.loader?.destroy();
171
- this.wrapper.remove();
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.style.setProperty("aria-hidden", "true");
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
- if(this.comms === undefined) return;
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
- const newWidth = `${width}%`;
330
- if(this.wrapper.style.width === newWidth) return;
331
- this.wrapper.style.width = newWidth;
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
- const newHeight = `${height}px`;
336
- if(this.wrapper.style.height === newHeight) return;
337
- this.wrapper.style.height = newHeight;
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() {