@readium/navigator 1.3.3 → 2.0.0-beta.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 (55) hide show
  1. package/dist/index.js +3744 -2819
  2. package/dist/index.umd.cjs +16 -16
  3. package/package.json +9 -9
  4. package/src/epub/EpubNavigator.ts +184 -7
  5. package/src/epub/css/Properties.ts +376 -0
  6. package/src/epub/css/ReadiumCSS.ts +348 -0
  7. package/src/epub/css/index.ts +2 -0
  8. package/src/epub/frame/FrameBlobBuilder.ts +59 -9
  9. package/src/epub/frame/FrameManager.ts +25 -6
  10. package/src/epub/frame/FramePoolManager.ts +61 -2
  11. package/src/epub/fxl/FXLFramePoolManager.ts +3 -15
  12. package/src/epub/index.ts +3 -1
  13. package/src/epub/preferences/EpubDefaults.ts +154 -0
  14. package/src/epub/preferences/EpubPreferences.ts +183 -0
  15. package/src/epub/preferences/EpubPreferencesEditor.ts +501 -0
  16. package/src/epub/preferences/EpubSettings.ts +212 -0
  17. package/src/epub/preferences/guards.ts +86 -0
  18. package/src/epub/preferences/index.ts +4 -0
  19. package/src/helpers/dimensions.ts +13 -0
  20. package/src/helpers/index.ts +1 -0
  21. package/src/helpers/lineLength.ts +293 -0
  22. package/src/helpers/sML.ts +18 -1
  23. package/src/index.ts +2 -1
  24. package/src/preferences/Configurable.ts +16 -0
  25. package/src/preferences/Preference.ts +272 -0
  26. package/src/preferences/PreferencesEditor.ts +6 -0
  27. package/src/preferences/Types.ts +39 -0
  28. package/src/preferences/index.ts +4 -0
  29. package/types/src/epub/EpubNavigator.d.ts +27 -3
  30. package/types/src/epub/css/Properties.d.ts +177 -0
  31. package/types/src/epub/css/ReadiumCSS.d.ts +32 -0
  32. package/types/src/epub/css/index.d.ts +2 -0
  33. package/types/src/epub/frame/FrameBlobBuilder.d.ts +5 -1
  34. package/types/src/epub/frame/FrameManager.d.ts +5 -0
  35. package/types/src/epub/frame/FramePoolManager.d.ts +8 -1
  36. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +1 -3
  37. package/types/src/epub/index.d.ts +2 -0
  38. package/types/src/epub/preferences/EpubDefaults.d.ts +82 -0
  39. package/types/src/epub/preferences/EpubPreferences.d.ts +86 -0
  40. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +53 -0
  41. package/types/src/epub/preferences/EpubSettings.d.ts +85 -0
  42. package/types/src/epub/preferences/guards.d.ts +9 -0
  43. package/types/src/epub/preferences/index.d.ts +4 -0
  44. package/types/src/helpers/dimensions.d.ts +7 -0
  45. package/types/src/helpers/index.d.ts +1 -0
  46. package/types/src/helpers/lineLength.d.ts +68 -0
  47. package/types/src/helpers/sML.d.ts +6 -1
  48. package/types/src/index.d.ts +1 -0
  49. package/types/src/preferences/Configurable.d.ts +13 -0
  50. package/types/src/preferences/Preference.d.ts +117 -0
  51. package/types/src/preferences/PreferencesEditor.d.ts +5 -0
  52. package/types/src/preferences/PreferencesSerializer.d.ts +5 -0
  53. package/types/src/preferences/Types.d.ts +24 -0
  54. package/types/src/preferences/index.d.ts +4 -0
  55. package/LICENSE +0 -28
@@ -0,0 +1,348 @@
1
+ import { ILineLengthsConfig, LineLengths } from "../../helpers";
2
+ import { getContentWidth } from "../../helpers/dimensions";
3
+ import { LayoutStrategy } from "../../preferences";
4
+ import { EpubSettings } from "../preferences/EpubSettings";
5
+ import { IUserProperties, RSProperties, UserProperties } from "./Properties";
6
+
7
+ type ILineLengthsProps = {
8
+ [K in Exclude<keyof ILineLengthsConfig, "fontSize" | "sample" | "isCJK" | "getRelative">]?: ILineLengthsConfig[K]
9
+ };
10
+
11
+ export interface IReadiumCSS {
12
+ rsProperties: RSProperties;
13
+ userProperties: UserProperties;
14
+ lineLengths: LineLengths;
15
+ container: HTMLElement;
16
+ constraint: number;
17
+ layoutStrategy?: LayoutStrategy | null;
18
+ }
19
+
20
+ export class ReadiumCSS {
21
+ rsProperties: RSProperties;
22
+ userProperties: UserProperties;
23
+ lineLengths: LineLengths;
24
+ container: HTMLElement;
25
+ containerParent: HTMLElement;
26
+ constraint: number;
27
+ layoutStrategy: LayoutStrategy;
28
+ private cachedColCount: number | null | undefined;
29
+ private effectiveContainerWidth: number;
30
+
31
+ constructor(props: IReadiumCSS) {
32
+ this.rsProperties = props.rsProperties;
33
+ this.userProperties = props.userProperties;
34
+ this.lineLengths = props.lineLengths;
35
+ this.container = props.container;
36
+ this.containerParent = props.container.parentElement || document.documentElement;
37
+ this.constraint = props.constraint;
38
+ this.layoutStrategy = props.layoutStrategy || LayoutStrategy.lineLength;
39
+ this.cachedColCount = props.userProperties.colCount;
40
+ this.effectiveContainerWidth = getContentWidth(this.containerParent);
41
+ }
42
+
43
+ update(settings: EpubSettings) {
44
+ // We need to keep the column count reference for resizeHandler
45
+ this.cachedColCount = settings.columnCount;
46
+
47
+ if (settings.constraint !== this.constraint)
48
+ this.constraint = settings.constraint;
49
+
50
+ if (settings.layoutStrategy && settings.layoutStrategy !== this.layoutStrategy)
51
+ this.layoutStrategy = settings.layoutStrategy;
52
+
53
+ if (settings.pageGutter !== this.rsProperties.pageGutter)
54
+ this.rsProperties.pageGutter = settings.pageGutter;
55
+
56
+ // This has to be updated before pagination
57
+ // otherwise the metrics won’t be correct for line length
58
+ this.updateLineLengths({
59
+ fontFace: settings.fontFamily,
60
+ letterSpacing: settings.letterSpacing,
61
+ pageGutter: settings.pageGutter,
62
+ wordSpacing: settings.wordSpacing,
63
+ optimalChars: settings.optimalLineLength,
64
+ userChars: settings.lineLength,
65
+ minChars: settings.minimalLineLength,
66
+ maxChars: settings.maximalLineLength
67
+ });
68
+
69
+ const layout = this.updateLayout(settings.fontSize, settings.deprecatedFontSize, settings.scroll, settings.columnCount);
70
+
71
+ if (layout?.effectiveContainerWidth)
72
+ this.effectiveContainerWidth = layout?.effectiveContainerWidth;
73
+
74
+ const updated: IUserProperties = {
75
+ a11yNormalize: settings.textNormalization,
76
+ appearance: settings.theme,
77
+ backgroundColor: settings.backgroundColor,
78
+ blendFilter: settings.blendFilter,
79
+ bodyHyphens: typeof settings.hyphens !== "boolean"
80
+ ? null
81
+ : settings.hyphens
82
+ ? "auto"
83
+ : "none",
84
+ colCount: layout?.colCount,
85
+ darkenFilter: settings.darkenFilter,
86
+ deprecatedFontSize: settings.deprecatedFontSize,
87
+ fontFamily: settings.fontFamily,
88
+ fontOpticalSizing: typeof settings.fontOpticalSizing !== "boolean"
89
+ ? null
90
+ : settings.fontOpticalSizing
91
+ ? "auto"
92
+ : "none",
93
+ fontOverride: settings.fontOverride !== null
94
+ ? settings.fontOverride
95
+ : (settings.textNormalization || settings.fontFamily
96
+ ? true
97
+ : false
98
+ ),
99
+ fontSize: settings.fontSize,
100
+ fontSizeNormalize: settings.fontSizeNormalize,
101
+ fontWeight: settings.fontWeight,
102
+ fontWidth: settings.fontWidth,
103
+ invertFilter: settings.invertFilter,
104
+ invertGaijiFilter: settings.invertGaijiFilter,
105
+ iPadOSPatch: settings.iPadOSPatch,
106
+ letterSpacing: settings.letterSpacing,
107
+ ligatures: typeof settings.ligatures !== "boolean"
108
+ ? null
109
+ : settings.ligatures
110
+ ? "common-ligatures"
111
+ : "none",
112
+ lineHeight: settings.lineHeight,
113
+ lineLength: layout?.effectiveLineLength,
114
+ linkColor: settings.linkColor,
115
+ noRuby: settings.noRuby,
116
+ paraIndent: settings.paragraphIndent,
117
+ paraSpacing: settings.paragraphSpacing,
118
+ selectionBackgroundColor: settings.selectionBackgroundColor,
119
+ selectionTextColor: settings.selectionTextColor,
120
+ textAlign: settings.textAlign,
121
+ textColor: settings.textColor,
122
+ view: typeof settings.scroll !== "boolean"
123
+ ? null
124
+ : settings.scroll
125
+ ? "scroll"
126
+ : "paged",
127
+ visitedColor: settings.visitedColor,
128
+ wordSpacing: settings.wordSpacing
129
+ };
130
+
131
+ this.userProperties = new UserProperties(updated);
132
+ }
133
+
134
+ private updateLineLengths(props: ILineLengthsProps) {
135
+ if (props.fontFace !== undefined) this.lineLengths.fontFace = props.fontFace;
136
+ if (props.letterSpacing !== undefined) this.lineLengths.letterSpacing = props.letterSpacing || 0;
137
+ if (props.pageGutter !== undefined) this.lineLengths.pageGutter = props.pageGutter || 0;
138
+ if (props.wordSpacing !== undefined) this.lineLengths.wordSpacing = props.wordSpacing || 0;
139
+ if (props.optimalChars) this.lineLengths.optimalChars = props.optimalChars;
140
+ if (props.userChars !== undefined) this.lineLengths.userChars = props.userChars;
141
+ if (props.minChars !== undefined) this.lineLengths.minChars = props.minChars;
142
+ if (props.maxChars !== undefined) this.lineLengths.maxChars = props.maxChars;
143
+ }
144
+
145
+ private updateLayout(scale: number | null, deprecatedImplem: boolean | null, scroll: boolean | null, colCount?: number | null) {
146
+ const isScroll = scroll ?? this.userProperties.view === "scroll";
147
+
148
+ if (isScroll) {
149
+ return this.computeScrollLength(scale, deprecatedImplem);
150
+ } else {
151
+ return this.paginate(scale, deprecatedImplem, colCount);
152
+ }
153
+ }
154
+
155
+ private getCompensatedMetrics(scale: number | null, deprecatedImplem: boolean | null) {
156
+ const zoomFactor = scale || this.userProperties.fontSize || 1;
157
+ const zoomCompensation = zoomFactor < 1
158
+ ? this.layoutStrategy === LayoutStrategy.margin
159
+ ? 1 / (zoomFactor + 0.003)
160
+ : 1 / zoomFactor
161
+ : deprecatedImplem
162
+ ? zoomFactor
163
+ : 1;
164
+
165
+ return {
166
+ zoomFactor: zoomFactor,
167
+ zoomCompensation: zoomCompensation,
168
+ optimal: Math.round(this.lineLengths.userLineLength || this.lineLengths.optimalLineLength) * zoomFactor,
169
+ minimal: this.lineLengths.minimalLineLength !== null
170
+ ? Math.round(this.lineLengths.minimalLineLength * zoomFactor)
171
+ : null,
172
+ maximal: this.lineLengths.maximalLineLength !== null
173
+ ? Math.round(this.lineLengths.maximalLineLength * zoomFactor)
174
+ : null
175
+ }
176
+ }
177
+
178
+ // Note: Kept intentionally verbose for debugging
179
+ // TODO: As scroll shows, the effective line-length
180
+ // should be the same as uncompensated when scale >= 1
181
+ private paginate(scale: number | null, deprecatedImplem: boolean | null, colCount?: number | null) {
182
+ const constrainedWidth = Math.round(getContentWidth(this.containerParent) - (this.constraint));
183
+ const metrics = this.getCompensatedMetrics(scale, deprecatedImplem);
184
+ const zoomCompensation = metrics.zoomCompensation;
185
+ const optimal = metrics.optimal;
186
+ const minimal = metrics.minimal;
187
+ const maximal = metrics.maximal;
188
+
189
+ let RCSSColCount = 1;
190
+ let effectiveContainerWidth = constrainedWidth;
191
+
192
+ if (colCount === undefined) {
193
+ return {
194
+ colCount: undefined,
195
+ effectiveContainerWidth: effectiveContainerWidth,
196
+ effectiveLineLength: Math.round((effectiveContainerWidth / RCSSColCount) * zoomCompensation)
197
+ };
198
+ }
199
+
200
+ if (colCount === null) {
201
+ if (this.layoutStrategy === LayoutStrategy.margin) {
202
+ if (constrainedWidth >= optimal) {
203
+ RCSSColCount = Math.floor(constrainedWidth / optimal);
204
+ const requiredWidth = Math.round(RCSSColCount * (optimal * zoomCompensation));
205
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
206
+ } else {
207
+ RCSSColCount = 1;
208
+ effectiveContainerWidth = constrainedWidth;
209
+ }
210
+ } else if (this.layoutStrategy === LayoutStrategy.lineLength) {
211
+ if (constrainedWidth < optimal || maximal === null) {
212
+ RCSSColCount = 1;
213
+ effectiveContainerWidth = constrainedWidth;
214
+ } else {
215
+ RCSSColCount = Math.floor(constrainedWidth / optimal);
216
+ const requiredWidth = Math.round(RCSSColCount * (maximal * zoomCompensation));
217
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
218
+ }
219
+ } else if (this.layoutStrategy === LayoutStrategy.columns) {
220
+ if (constrainedWidth >= optimal) {
221
+ if (maximal === null) {
222
+ RCSSColCount = Math.floor(constrainedWidth / optimal);
223
+ effectiveContainerWidth = constrainedWidth;
224
+ } else {
225
+ RCSSColCount = Math.floor(constrainedWidth / (minimal || optimal));
226
+ const requiredWidth = Math.round((RCSSColCount * (optimal * zoomCompensation)));
227
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
228
+ }
229
+ } else {
230
+ RCSSColCount = 1;
231
+ effectiveContainerWidth = constrainedWidth;
232
+ }
233
+ }
234
+ } else if (colCount > 1) {
235
+ const minRequiredWidth = Math.round(colCount * (minimal !== null ? minimal : optimal));
236
+
237
+ if (constrainedWidth >= minRequiredWidth) {
238
+ RCSSColCount = colCount;
239
+ if (this.layoutStrategy === LayoutStrategy.margin) {
240
+ const requiredWidth = Math.round(RCSSColCount * (optimal * zoomCompensation));
241
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
242
+ } else if (
243
+ this.layoutStrategy === LayoutStrategy.lineLength ||
244
+ this.layoutStrategy === LayoutStrategy.columns
245
+ ) {
246
+ if (maximal === null) {
247
+ effectiveContainerWidth = constrainedWidth
248
+ } else {
249
+ const requiredWidth = Math.round(RCSSColCount * (maximal * zoomCompensation));
250
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
251
+ }
252
+
253
+ if (this.layoutStrategy === LayoutStrategy.columns) {
254
+ console.error("Columns strategy is not compatible with a column count whose value is a number. Falling back to lineLength strategy.");
255
+ }
256
+ }
257
+ } else {
258
+ if (minimal !== null && constrainedWidth < Math.round(colCount * minimal)) {
259
+ RCSSColCount = Math.floor(constrainedWidth / minimal);
260
+ } else {
261
+ RCSSColCount = colCount;
262
+ }
263
+ const requiredWidth = Math.round((RCSSColCount * (optimal * zoomCompensation)));
264
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
265
+ }
266
+ } else {
267
+ RCSSColCount = 1;
268
+
269
+ if (constrainedWidth >= optimal) {
270
+ if (this.layoutStrategy === LayoutStrategy.margin) {
271
+ const requiredWidth = Math.round(optimal * zoomCompensation);
272
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
273
+ } else if (
274
+ this.layoutStrategy === LayoutStrategy.lineLength ||
275
+ this.layoutStrategy === LayoutStrategy.columns
276
+ ) {
277
+ if (maximal === null) {
278
+ effectiveContainerWidth = constrainedWidth
279
+ } else {
280
+ const requiredWidth = Math.round(maximal * zoomCompensation);
281
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
282
+ }
283
+
284
+ if (this.layoutStrategy === LayoutStrategy.columns) {
285
+ console.error("Columns strategy is not compatible with a column count whose value is a number. Falling back to lineLength strategy.");
286
+ }
287
+ }
288
+ } else {
289
+ effectiveContainerWidth = constrainedWidth
290
+ }
291
+ }
292
+
293
+ return {
294
+ colCount: RCSSColCount,
295
+ effectiveContainerWidth: effectiveContainerWidth,
296
+ effectiveLineLength: Math.round(((effectiveContainerWidth / RCSSColCount) / (scale && scale >= 1 ? scale : 1)) * zoomCompensation)
297
+ };
298
+ }
299
+
300
+ // This behaves as paginate where colCount = 1
301
+ private computeScrollLength(scale: number | null, deprecatedImplem: boolean | null) {
302
+ const constrainedWidth = Math.round(getContentWidth(this.containerParent) - (this.constraint));
303
+ const metrics = this.getCompensatedMetrics(scale && (scale < 1 || deprecatedImplem) ? scale : 1, deprecatedImplem);
304
+ const zoomCompensation = metrics.zoomCompensation;
305
+ const optimal = metrics.optimal;
306
+ const maximal = metrics.maximal;
307
+
308
+ let RCSSColCount = undefined;
309
+ let effectiveContainerWidth = constrainedWidth;
310
+ let effectiveLineLength = Math.round(optimal * zoomCompensation);
311
+
312
+ if (this.layoutStrategy === LayoutStrategy.margin) {
313
+ const computedWidth = Math.min(Math.round(optimal * zoomCompensation), constrainedWidth);
314
+ effectiveLineLength = deprecatedImplem ? computedWidth : Math.round(computedWidth * zoomCompensation);
315
+ } else if (
316
+ this.layoutStrategy === LayoutStrategy.lineLength ||
317
+ this.layoutStrategy === LayoutStrategy.columns
318
+ ) {
319
+ if (this.layoutStrategy === LayoutStrategy.columns) {
320
+ console.error("Columns strategy is not compatible with scroll. Falling back to lineLength strategy.");
321
+ }
322
+ if (maximal === null) {
323
+ effectiveLineLength = constrainedWidth;
324
+ } else {
325
+ const computedWidth = Math.min(Math.round(maximal * zoomCompensation), constrainedWidth);
326
+ effectiveLineLength = deprecatedImplem ? computedWidth : Math.round(computedWidth * zoomCompensation);
327
+ }
328
+ }
329
+
330
+ return {
331
+ colCount: RCSSColCount,
332
+ effectiveContainerWidth: effectiveContainerWidth,
333
+ effectiveLineLength: effectiveLineLength
334
+ }
335
+ }
336
+
337
+ setContainerWidth() {
338
+ this.container.style.width = `${ this.effectiveContainerWidth }px`;
339
+ }
340
+
341
+ resizeHandler() {
342
+ const pagination = this.updateLayout(this.userProperties.fontSize, this.userProperties.deprecatedFontSize, this.userProperties.view === "scroll", this.cachedColCount);
343
+ this.userProperties.colCount = pagination.colCount;
344
+ this.userProperties.lineLength = pagination.effectiveLineLength;
345
+ this.effectiveContainerWidth = pagination.effectiveContainerWidth;
346
+ this.container.style.width = `${ this.effectiveContainerWidth }px`;
347
+ }
348
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./Properties";
2
+ export * from "./ReadiumCSS";
@@ -82,11 +82,13 @@ export default class FrameBlobBuider {
82
82
  private readonly item: Link;
83
83
  private readonly burl: string;
84
84
  private readonly pub: Publication;
85
+ private readonly cssProperties?: { [key: string]: string };
85
86
 
86
- constructor(pub: Publication, baseURL: string, item: Link) {
87
+ constructor(pub: Publication, baseURL: string, item: Link, cssProperties?: { [key: string]: string }) {
87
88
  this.pub = pub;
88
89
  this.item = item;
89
90
  this.burl = item.toURL(baseURL) || "";
91
+ this.cssProperties = cssProperties;
90
92
  }
91
93
 
92
94
  public async build(fxl = false): Promise<string> {
@@ -113,7 +115,7 @@ export default class FrameBlobBuider {
113
115
  const details = perror.querySelector("div");
114
116
  throw new Error(`Failed parsing item ${this.item.href}: ${details?.textContent || perror.textContent}`);
115
117
  }
116
- return this.finalizeDOM(doc, this.burl, this.item.mediaType, fxl);
118
+ return this.finalizeDOM(doc, this.burl, this.item.mediaType, fxl, this.cssProperties);
117
119
  }
118
120
 
119
121
  private buildImageFrame(): string {
@@ -150,7 +152,14 @@ export default class FrameBlobBuider {
150
152
  return false;
151
153
  }
152
154
 
153
- private finalizeDOM(doc: Document, base: string | undefined, mediaType: MediaType, fxl = false): string {
155
+ private setProperties(cssProperties: { [key: string]: string }, doc: Document) {
156
+ for (const key in cssProperties) {
157
+ const value = cssProperties[key];
158
+ if (value) doc.documentElement.style.setProperty(key, value);
159
+ }
160
+ }
161
+
162
+ private finalizeDOM(doc: Document, base: string | undefined, mediaType: MediaType, fxl = false, cssProperties?: { [key: string]: string }): string {
154
163
  if(!doc) return "";
155
164
 
156
165
  // Inject styles
@@ -159,18 +168,16 @@ export default class FrameBlobBuider {
159
168
  const rcssBefore = styleify(doc, cached("ReadiumCSS-before", () => blobify(stripCSS(readiumCSSBefore), "text/css")));
160
169
  doc.head.firstChild ? doc.head.firstChild.before(rcssBefore) : doc.head.appendChild(rcssBefore);
161
170
 
162
- // Patch
163
- const patch = doc.createElement("style");
164
- patch.dataset.readium = "true";
165
- patch.innerHTML = `audio[controls] { width: revert; height: revert; }`; // https://github.com/readium/readium-css/issues/94
166
- rcssBefore.after(patch);
167
-
168
171
  // Readium CSS defaults
169
172
  if(!this.hasStyle(doc))
170
173
  rcssBefore.after(styleify(doc, cached("ReadiumCSS-default", () => blobify(stripCSS(readiumCSSDefault), "text/css"))))
171
174
 
172
175
  // Readium CSS After
173
176
  doc.head.appendChild(styleify(doc, cached("ReadiumCSS-after", () => blobify(stripCSS(readiumCSSAfter), "text/css"))));
177
+
178
+ if (cssProperties) {
179
+ this.setProperties(cssProperties, doc);
180
+ }
174
181
  }
175
182
 
176
183
  // Set all <img> elements to high priority
@@ -183,6 +190,49 @@ export default class FrameBlobBuider {
183
190
  doc.body.querySelectorAll("img").forEach((img) => {
184
191
  img.setAttribute("fetchpriority", "high");
185
192
  });
193
+
194
+ // We need to ensure that lang is set on the root element
195
+ // since it is used for settings such as font-family, hyphens, ligatures, etc.
196
+ // but also screen readers, etc.
197
+ // Metadata’s effectiveReadingProgression uses first item in array as primary language
198
+ // so we keep it consistent.
199
+ if (mediaType.isHTML && this.pub.metadata.languages?.[0]) {
200
+ const primaryLanguage = this.pub.metadata.languages[0];
201
+
202
+ if (mediaType === MediaType.XHTML) {
203
+ // InDesign is infamous for setting xml:lang on the body instead of the root element
204
+ // So we have to check whether lang is set on the body and move it to the root element
205
+ const rootLang = document.documentElement.lang || document.documentElement.getAttribute("xml:lang");
206
+ const bodyLang = document.body.lang || document.body.getAttribute("xml:lang");
207
+ if (bodyLang && !rootLang) {
208
+ document.documentElement.lang = bodyLang;
209
+ document.documentElement.setAttribute("xml:lang", bodyLang);
210
+ document.body.removeAttribute("xml:lang");
211
+ document.body.removeAttribute("lang");
212
+ } else if (!rootLang) {
213
+ document.documentElement.lang = primaryLanguage;
214
+ document.documentElement.setAttribute("xml:lang", primaryLanguage);
215
+ }
216
+ } else if (
217
+ mediaType === MediaType.HTML &&
218
+ !document.documentElement.lang
219
+ ) {
220
+ document.documentElement.lang = primaryLanguage;
221
+ }
222
+ }
223
+
224
+ // We need to ensure that dir is set on the root element if rtl
225
+ // Since body can bubble up, we also need to check it’s not here.
226
+ // https://github.com/readium/readium-css/blob/develop/docs/CSS03-injection_and_pagination.md#be-cautious-the-direction-propagates
227
+
228
+ // TODO: ReadiumCSS stylesheets are injected as LTR/default no matter what so disabled ATM
229
+ /* if (
230
+ !document.documentElement.dir &&
231
+ !document.body.dir &&
232
+ this.pub.metadata.effectiveReadingProgression === ReadingProgression.rtl
233
+ ) {
234
+ document.documentElement.dir = this.pub.metadata.effectiveReadingProgression;
235
+ } */
186
236
 
187
237
  if(base !== undefined) {
188
238
  // Set all URL bases. Very convenient!
@@ -8,6 +8,8 @@ export class FrameManager {
8
8
  private loader: Loader | undefined;
9
9
  public readonly source: string;
10
10
  private comms: FrameComms | undefined;
11
+ private hidden: boolean = true;
12
+ private destroyed: boolean = false;
11
13
 
12
14
  private currModules: ModuleName[] = [];
13
15
 
@@ -57,15 +59,18 @@ export class FrameManager {
57
59
  await this.hide();
58
60
  this.loader?.destroy();
59
61
  this.frame.remove();
62
+ this.destroyed = true;
60
63
  }
61
64
 
62
65
  async hide(): Promise<void> {
66
+ if(this.destroyed) return;
63
67
  this.frame.style.visibility = "hidden";
64
68
  this.frame.style.setProperty("aria-hidden", "true");
65
69
  this.frame.style.opacity = "0";
66
70
  this.frame.style.pointerEvents = "none";
71
+ this.hidden = true;
67
72
  if(this.frame.parentElement) {
68
- if(this.comms === undefined) return;
73
+ if(this.comms === undefined || !this.comms.ready) return;
69
74
  return new Promise((res, _) => {
70
75
  this.comms?.send("unfocus", undefined, (_: boolean) => {
71
76
  this.comms?.halt();
@@ -77,10 +82,8 @@ export class FrameManager {
77
82
  }
78
83
 
79
84
  async show(atProgress?: number): Promise<void> {
80
- if(!this.frame.parentElement) {
81
- console.warn("Trying to show frame that is not attached to the DOM");
82
- return;
83
- }
85
+ if(this.destroyed) throw Error("Trying to show frame when it doesn't exist");
86
+ if(!this.frame.parentElement) throw Error("Trying to show frame that is not attached to the DOM");
84
87
  if(this.comms) this.comms.resume();
85
88
  else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
86
89
  return new Promise((res, _) => {
@@ -91,6 +94,7 @@ export class FrameManager {
91
94
  this.frame.style.removeProperty("aria-hidden");
92
95
  this.frame.style.removeProperty("opacity");
93
96
  this.frame.style.removeProperty("pointer-events");
97
+ this.hidden = false;
94
98
  res();
95
99
  }
96
100
  if(atProgress && atProgress > 0) {
@@ -103,16 +107,31 @@ export class FrameManager {
103
107
  });
104
108
  }
105
109
 
110
+ setCSSProperties(properties: { [key: string]: string }) {
111
+ if(this.destroyed || !this.frame.contentWindow) return;
112
+
113
+ // We need to resume and halt postMessage to update the properties
114
+ // if the frame is hidden since it’s been halted in hide()
115
+ if (this.hidden) {
116
+ if (this.comms) this.comms?.resume();
117
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
118
+ }
119
+ this.comms?.send("update_properties", properties);
120
+ if (this.hidden) this.comms?.halt();
121
+ }
122
+
106
123
  get iframe() {
124
+ if(this.destroyed) throw Error("Trying to use frame when it doesn't exist");
107
125
  return this.frame;
108
126
  }
109
127
 
110
128
  get realSize() {
129
+ if(this.destroyed) throw Error("Trying to use frame client rect when it doesn't exist");
111
130
  return this.frame.getBoundingClientRect();
112
131
  }
113
132
 
114
133
  get window() {
115
- if(!this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
134
+ if(this.destroyed || !this.frame.contentWindow) throw Error("Trying to use frame window when it doesn't exist");
116
135
  return this.frame.contentWindow;
117
136
  }
118
137
 
@@ -10,14 +10,17 @@ export class FramePoolManager {
10
10
  private readonly container: HTMLElement;
11
11
  private readonly positions: Locator[];
12
12
  private _currentFrame: FrameManager | undefined;
13
+ private currentCssProperties: { [key: string]: string } | undefined;
13
14
  private readonly pool: Map<string, FrameManager> = new Map();
14
15
  private readonly blobs: Map<string, string> = new Map();
15
16
  private readonly inprogress: Map<string, Promise<void>> = new Map();
17
+ private pendingUpdates: Map<string, { inPool: boolean }> = new Map();
16
18
  private currentBaseURL: string | undefined;
17
19
 
18
- constructor(container: HTMLElement, positions: Locator[]) {
20
+ constructor(container: HTMLElement, positions: Locator[], cssProperties?: { [key: string]: string }) {
19
21
  this.container = container;
20
22
  this.positions = positions;
23
+ this.currentCssProperties = cssProperties;
21
24
  }
22
25
 
23
26
  async destroy() {
@@ -80,6 +83,8 @@ export class FramePoolManager {
80
83
  if(!this.pool.has(href)) return;
81
84
  await this.pool.get(href)?.destroy();
82
85
  this.pool.delete(href);
86
+ if(this.pendingUpdates.has(href))
87
+ this.pendingUpdates.set(href, { inPool: false });
83
88
  });
84
89
 
85
90
  // Check if base URL of publication has changed
@@ -91,11 +96,31 @@ export class FramePoolManager {
91
96
  this.currentBaseURL = pub.baseURL;
92
97
 
93
98
  const creator = async (href: string) => {
99
+ if(force) {
100
+ // Revoke all blobs so that CSSProperties are not stale
101
+ // When using force, we switch scroll/paginated
102
+ // If this property is not up to date, it creates issues
103
+ // when navigating backwards, where paginated will go the
104
+ // start of the resource instead of the end due to the
105
+ // corrupted width ColumnSnapper (injectables) gets on init
106
+ this.blobs.forEach(v => URL.revokeObjectURL(v));
107
+ this.blobs.clear();
108
+ this.pendingUpdates.clear();
109
+ }
110
+ if(this.pendingUpdates.has(href) && this.pendingUpdates.get(href)?.inPool === false) {
111
+ const url = this.blobs.get(href);
112
+ if(url) {
113
+ URL.revokeObjectURL(url);
114
+ this.blobs.delete(href);
115
+ this.pendingUpdates.delete(href);
116
+ }
117
+ }
94
118
  if(this.pool.has(href)) {
95
119
  const fm = this.pool.get(href)!;
96
120
  if(!this.blobs.has(href)) {
97
121
  await fm.destroy();
98
122
  this.pool.delete(href);
123
+ this.pendingUpdates.delete(href);
99
124
  } else {
100
125
  await fm.load(modules);
101
126
  return;
@@ -104,7 +129,7 @@ export class FramePoolManager {
104
129
  const itm = pub.readingOrder.findWithHref(href);
105
130
  if(!itm) return; // TODO throw?
106
131
  if(!this.blobs.has(href)) {
107
- const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm);
132
+ const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm, this.currentCssProperties);
108
133
  const blobURL = await blobBuilder.build();
109
134
  this.blobs.set(href, blobURL);
110
135
  }
@@ -144,6 +169,40 @@ export class FramePoolManager {
144
169
  this.inprogress.delete(newHref); // Delete it from the in progress map!
145
170
  }
146
171
 
172
+ setCSSProperties(properties: { [key: string]: string }) {
173
+ const deepCompare = (obj1: { [key: string]: string }, obj2: { [key: string]: string }) => {
174
+ const keys1 = Object.keys(obj1);
175
+ const keys2 = Object.keys(obj2);
176
+
177
+ if (keys1.length !== keys2.length) {
178
+ return false;
179
+ }
180
+
181
+ for (const key of keys1) {
182
+ if (obj1[key] !== obj2[key]) {
183
+ return false;
184
+ }
185
+ }
186
+
187
+ return true;
188
+ };
189
+
190
+ // If CSSProperties have changed, we update the currentCssProperties,
191
+ // and set the CSS Properties to all frames already in the pool
192
+ // We also need to invalidate the blobs and recreate them with the new properties.
193
+ // We do that in update, by updating them when needed (they are added into the pool)
194
+ // so that we do not invalidate and recreate blobs over and over again.
195
+ if(!deepCompare(this.currentCssProperties || {}, properties)) {
196
+ this.currentCssProperties = properties;
197
+ this.pool.forEach((frame) => {
198
+ frame.setCSSProperties(properties);
199
+ });
200
+ for (const href of this.blobs.keys()) {
201
+ this.pendingUpdates.set(href, { inPool: this.pool.has(href) });
202
+ }
203
+ }
204
+ }
205
+
147
206
  get currentFrames(): (FrameManager | undefined)[] {
148
207
  return [this._currentFrame];
149
208
  }