@readium/navigator 1.3.4 → 2.0.0-beta.10

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 (57) hide show
  1. package/dist/index.js +3974 -2928
  2. package/dist/index.umd.cjs +16 -16
  3. package/package.json +10 -9
  4. package/src/Navigator.ts +11 -0
  5. package/src/epub/EpubNavigator.ts +250 -24
  6. package/src/epub/css/Properties.ts +396 -0
  7. package/src/epub/css/ReadiumCSS.ts +339 -0
  8. package/src/epub/css/index.ts +2 -0
  9. package/src/epub/frame/FrameBlobBuilder.ts +59 -9
  10. package/src/epub/frame/FrameManager.ts +23 -1
  11. package/src/epub/frame/FramePoolManager.ts +62 -4
  12. package/src/epub/fxl/FXLFramePoolManager.ts +23 -16
  13. package/src/epub/index.ts +3 -1
  14. package/src/epub/preferences/EpubDefaults.ts +165 -0
  15. package/src/epub/preferences/EpubPreferences.ts +192 -0
  16. package/src/epub/preferences/EpubPreferencesEditor.ts +534 -0
  17. package/src/epub/preferences/EpubSettings.ts +239 -0
  18. package/src/epub/preferences/guards.ts +86 -0
  19. package/src/epub/preferences/index.ts +4 -0
  20. package/src/helpers/dimensions.ts +13 -0
  21. package/src/helpers/index.ts +1 -0
  22. package/src/helpers/lineLength.ts +241 -0
  23. package/src/helpers/sML.ts +25 -3
  24. package/src/index.ts +2 -1
  25. package/src/preferences/Configurable.ts +16 -0
  26. package/src/preferences/Preference.ts +272 -0
  27. package/src/preferences/PreferencesEditor.ts +6 -0
  28. package/src/preferences/Types.ts +38 -0
  29. package/src/preferences/index.ts +4 -0
  30. package/types/src/Navigator.d.ts +9 -0
  31. package/types/src/epub/EpubNavigator.d.ts +34 -4
  32. package/types/src/epub/css/Properties.d.ts +183 -0
  33. package/types/src/epub/css/ReadiumCSS.d.ts +31 -0
  34. package/types/src/epub/css/index.d.ts +2 -0
  35. package/types/src/epub/frame/FrameBlobBuilder.d.ts +5 -1
  36. package/types/src/epub/frame/FrameManager.d.ts +4 -0
  37. package/types/src/epub/frame/FramePoolManager.d.ts +8 -1
  38. package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -4
  39. package/types/src/epub/index.d.ts +2 -0
  40. package/types/src/epub/preferences/EpubDefaults.d.ts +86 -0
  41. package/types/src/epub/preferences/EpubPreferences.d.ts +90 -0
  42. package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +55 -0
  43. package/types/src/epub/preferences/EpubSettings.d.ts +89 -0
  44. package/types/src/epub/preferences/guards.d.ts +9 -0
  45. package/types/src/epub/preferences/index.d.ts +4 -0
  46. package/types/src/helpers/dimensions.d.ts +7 -0
  47. package/types/src/helpers/index.d.ts +1 -0
  48. package/types/src/helpers/lineLength.d.ts +54 -0
  49. package/types/src/helpers/sML.d.ts +6 -1
  50. package/types/src/index.d.ts +1 -0
  51. package/types/src/preferences/Configurable.d.ts +13 -0
  52. package/types/src/preferences/Preference.d.ts +117 -0
  53. package/types/src/preferences/PreferencesEditor.d.ts +5 -0
  54. package/types/src/preferences/PreferencesSerializer.d.ts +5 -0
  55. package/types/src/preferences/Types.d.ts +23 -0
  56. package/types/src/preferences/index.d.ts +4 -0
  57. package/LICENSE +0 -28
@@ -0,0 +1,339 @@
1
+ import { 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
+ export interface IReadiumCSS {
8
+ rsProperties: RSProperties;
9
+ userProperties: UserProperties;
10
+ lineLengths: LineLengths;
11
+ container: HTMLElement;
12
+ constraint: number;
13
+ layoutStrategy?: LayoutStrategy | null;
14
+ }
15
+
16
+ export class ReadiumCSS {
17
+ rsProperties: RSProperties;
18
+ userProperties: UserProperties;
19
+ lineLengths: LineLengths;
20
+ container: HTMLElement;
21
+ containerParent: HTMLElement;
22
+ constraint: number;
23
+ layoutStrategy: LayoutStrategy;
24
+ private cachedColCount: number | null | undefined;
25
+ private effectiveContainerWidth: number;
26
+
27
+ constructor(props: IReadiumCSS) {
28
+ this.rsProperties = props.rsProperties;
29
+ this.userProperties = props.userProperties;
30
+ this.lineLengths = props.lineLengths;
31
+ this.container = props.container;
32
+ this.containerParent = props.container.parentElement || document.documentElement;
33
+ this.constraint = props.constraint;
34
+ this.layoutStrategy = props.layoutStrategy || LayoutStrategy.lineLength;
35
+ this.cachedColCount = props.userProperties.colCount;
36
+ this.effectiveContainerWidth = getContentWidth(this.containerParent);
37
+ }
38
+
39
+ update(settings: EpubSettings) {
40
+ // We need to keep the column count reference for resizeHandler
41
+ this.cachedColCount = settings.columnCount;
42
+
43
+ if (settings.constraint !== this.constraint)
44
+ this.constraint = settings.constraint;
45
+
46
+ if (settings.layoutStrategy && settings.layoutStrategy !== this.layoutStrategy)
47
+ this.layoutStrategy = settings.layoutStrategy;
48
+
49
+ if (settings.pageGutter !== this.rsProperties.pageGutter)
50
+ this.rsProperties.pageGutter = settings.pageGutter;
51
+
52
+ if (settings.scrollPaddingBottom !== this.rsProperties.scrollPaddingBottom)
53
+ this.rsProperties.scrollPaddingBottom = settings.scrollPaddingBottom;
54
+
55
+ // if (settings.scrollPaddingLeft !== this.rsProperties.scrollPaddingLeft)
56
+ // this.rsProperties.scrollPaddingLeft = settings.scrollPaddingLeft;
57
+
58
+ // if (settings.scrollPaddingRight !== this.rsProperties.scrollPaddingRight)
59
+ // this.rsProperties.scrollPaddingRight = settings.scrollPaddingRight;
60
+
61
+ if (settings.scrollPaddingTop !== this.rsProperties.scrollPaddingTop)
62
+ this.rsProperties.scrollPaddingTop = settings.scrollPaddingTop;
63
+
64
+ // This has to be updated before pagination
65
+ // otherwise the metrics won’t be correct for line length
66
+ this.lineLengths.update({
67
+ fontFace: settings.fontFamily,
68
+ letterSpacing: settings.letterSpacing,
69
+ pageGutter: settings.pageGutter,
70
+ wordSpacing: settings.wordSpacing,
71
+ optimalChars: settings.optimalLineLength,
72
+ minChars: settings.minimalLineLength,
73
+ maxChars: settings.maximalLineLength
74
+ });
75
+
76
+ const layout = this.updateLayout(settings.fontSize, settings.deprecatedFontSize || settings.iOSPatch, settings.scroll, settings.columnCount);
77
+
78
+ if (layout?.effectiveContainerWidth)
79
+ this.effectiveContainerWidth = layout?.effectiveContainerWidth;
80
+
81
+ const updated: IUserProperties = {
82
+ a11yNormalize: settings.textNormalization,
83
+ appearance: settings.theme,
84
+ backgroundColor: settings.backgroundColor,
85
+ blendFilter: settings.blendFilter,
86
+ bodyHyphens: typeof settings.hyphens !== "boolean"
87
+ ? null
88
+ : settings.hyphens
89
+ ? "auto"
90
+ : "none",
91
+ colCount: layout?.colCount,
92
+ darkenFilter: settings.darkenFilter,
93
+ deprecatedFontSize: settings.deprecatedFontSize,
94
+ fontFamily: settings.fontFamily,
95
+ fontOpticalSizing: typeof settings.fontOpticalSizing !== "boolean"
96
+ ? null
97
+ : settings.fontOpticalSizing
98
+ ? "auto"
99
+ : "none",
100
+ fontSize: settings.fontSize,
101
+ fontSizeNormalize: settings.fontSizeNormalize,
102
+ fontWeight: settings.fontWeight,
103
+ fontWidth: settings.fontWidth,
104
+ invertFilter: settings.invertFilter,
105
+ invertGaijiFilter: settings.invertGaijiFilter,
106
+ iOSPatch: settings.iOSPatch,
107
+ iPadOSPatch: settings.iPadOSPatch,
108
+ letterSpacing: settings.letterSpacing,
109
+ ligatures: typeof settings.ligatures !== "boolean"
110
+ ? null
111
+ : settings.ligatures
112
+ ? "common-ligatures"
113
+ : "none",
114
+ lineHeight: settings.lineHeight,
115
+ lineLength: layout?.effectiveLineLength,
116
+ linkColor: settings.linkColor,
117
+ noRuby: settings.noRuby,
118
+ paraIndent: settings.paragraphIndent,
119
+ paraSpacing: settings.paragraphSpacing,
120
+ selectionBackgroundColor: settings.selectionBackgroundColor,
121
+ selectionTextColor: settings.selectionTextColor,
122
+ textAlign: settings.textAlign,
123
+ textColor: settings.textColor,
124
+ view: typeof settings.scroll !== "boolean"
125
+ ? null
126
+ : settings.scroll
127
+ ? "scroll"
128
+ : "paged",
129
+ visitedColor: settings.visitedColor,
130
+ wordSpacing: settings.wordSpacing
131
+ };
132
+
133
+ this.userProperties = new UserProperties(updated);
134
+ }
135
+
136
+ private updateLayout(scale: number | null, ignoreCompensation: boolean | null, scroll: boolean | null, colCount?: number | null) {
137
+ const isScroll = scroll ?? this.userProperties.view === "scroll";
138
+
139
+ if (isScroll) {
140
+ return this.computeScrollLength(scale, ignoreCompensation);
141
+ } else {
142
+ return this.paginate(scale, ignoreCompensation, colCount);
143
+ }
144
+ }
145
+
146
+ private getCompensatedMetrics(scale: number | null, ignoreCompensation: boolean | null) {
147
+ const zoomFactor = scale || this.userProperties.fontSize || 1;
148
+ const zoomCompensation = zoomFactor < 1
149
+ ? this.layoutStrategy === LayoutStrategy.margin
150
+ ? 1 / (zoomFactor + 0.003)
151
+ : 1 / zoomFactor
152
+ : ignoreCompensation
153
+ ? zoomFactor
154
+ : 1;
155
+
156
+ return {
157
+ zoomFactor: zoomFactor,
158
+ zoomCompensation: zoomCompensation,
159
+ optimal: Math.round(this.lineLengths.optimalLineLength) * zoomFactor,
160
+ minimal: this.lineLengths.minimalLineLength !== null
161
+ ? Math.round(this.lineLengths.minimalLineLength * zoomFactor)
162
+ : null,
163
+ maximal: this.lineLengths.maximalLineLength !== null
164
+ ? Math.round(this.lineLengths.maximalLineLength * zoomFactor)
165
+ : null
166
+ }
167
+ }
168
+
169
+ // Note: Kept intentionally verbose for debugging
170
+ // TODO: As scroll shows, the effective line-length
171
+ // should be the same as uncompensated when scale >= 1
172
+ private paginate(scale: number | null, ignoreCompensation: boolean | null, colCount?: number | null) {
173
+ const constrainedWidth = Math.round(getContentWidth(this.containerParent) - (this.constraint));
174
+ const metrics = this.getCompensatedMetrics(scale, ignoreCompensation);
175
+ const zoomCompensation = metrics.zoomCompensation;
176
+ const optimal = metrics.optimal;
177
+ const minimal = metrics.minimal;
178
+ const maximal = metrics.maximal;
179
+
180
+ let RCSSColCount = 1;
181
+ let effectiveContainerWidth = constrainedWidth;
182
+
183
+ if (colCount === undefined) {
184
+ return {
185
+ colCount: undefined,
186
+ effectiveContainerWidth: effectiveContainerWidth,
187
+ effectiveLineLength: Math.round((effectiveContainerWidth / RCSSColCount) * zoomCompensation)
188
+ };
189
+ }
190
+
191
+ if (colCount === null) {
192
+ if (this.layoutStrategy === LayoutStrategy.margin) {
193
+ if (constrainedWidth >= optimal) {
194
+ RCSSColCount = Math.floor(constrainedWidth / optimal);
195
+ const requiredWidth = Math.round(RCSSColCount * (optimal * zoomCompensation));
196
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
197
+ } else {
198
+ RCSSColCount = 1;
199
+ effectiveContainerWidth = constrainedWidth;
200
+ }
201
+ } else if (this.layoutStrategy === LayoutStrategy.lineLength) {
202
+ if (constrainedWidth < optimal || maximal === null) {
203
+ RCSSColCount = 1;
204
+ effectiveContainerWidth = constrainedWidth;
205
+ } else {
206
+ RCSSColCount = Math.floor(constrainedWidth / optimal);
207
+ const requiredWidth = Math.round(RCSSColCount * (maximal * zoomCompensation));
208
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
209
+ }
210
+ } else if (this.layoutStrategy === LayoutStrategy.columns) {
211
+ if (constrainedWidth >= optimal) {
212
+ if (maximal === null) {
213
+ RCSSColCount = Math.floor(constrainedWidth / optimal);
214
+ effectiveContainerWidth = constrainedWidth;
215
+ } else {
216
+ RCSSColCount = Math.floor(constrainedWidth / (minimal || optimal));
217
+ const requiredWidth = Math.round((RCSSColCount * (optimal * zoomCompensation)));
218
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
219
+ }
220
+ } else {
221
+ RCSSColCount = 1;
222
+ effectiveContainerWidth = constrainedWidth;
223
+ }
224
+ }
225
+ } else if (colCount > 1) {
226
+ const minRequiredWidth = Math.round(colCount * (minimal !== null ? minimal : optimal));
227
+
228
+ if (constrainedWidth >= minRequiredWidth) {
229
+ RCSSColCount = colCount;
230
+ if (this.layoutStrategy === LayoutStrategy.margin) {
231
+ const requiredWidth = Math.round(RCSSColCount * (optimal * zoomCompensation));
232
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
233
+ } else if (
234
+ this.layoutStrategy === LayoutStrategy.lineLength ||
235
+ this.layoutStrategy === LayoutStrategy.columns
236
+ ) {
237
+ if (maximal === null) {
238
+ effectiveContainerWidth = constrainedWidth
239
+ } else {
240
+ const requiredWidth = Math.round(RCSSColCount * (maximal * zoomCompensation));
241
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
242
+ }
243
+
244
+ if (this.layoutStrategy === LayoutStrategy.columns) {
245
+ console.error("Columns strategy is not compatible with a column count whose value is a number. Falling back to lineLength strategy.");
246
+ }
247
+ }
248
+ } else {
249
+ if (minimal !== null && constrainedWidth < Math.round(colCount * minimal)) {
250
+ RCSSColCount = Math.floor(constrainedWidth / minimal);
251
+ } else {
252
+ RCSSColCount = colCount;
253
+ }
254
+ const requiredWidth = Math.round((RCSSColCount * (optimal * zoomCompensation)));
255
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
256
+ }
257
+ } else {
258
+ RCSSColCount = 1;
259
+
260
+ if (constrainedWidth >= optimal) {
261
+ if (this.layoutStrategy === LayoutStrategy.margin) {
262
+ const requiredWidth = Math.round(optimal * zoomCompensation);
263
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
264
+ } else if (
265
+ this.layoutStrategy === LayoutStrategy.lineLength ||
266
+ this.layoutStrategy === LayoutStrategy.columns
267
+ ) {
268
+ if (maximal === null) {
269
+ effectiveContainerWidth = constrainedWidth
270
+ } else {
271
+ const requiredWidth = Math.round(maximal * zoomCompensation);
272
+ effectiveContainerWidth = Math.min(requiredWidth, constrainedWidth);
273
+ }
274
+
275
+ if (this.layoutStrategy === LayoutStrategy.columns) {
276
+ console.error("Columns strategy is not compatible with a column count whose value is a number. Falling back to lineLength strategy.");
277
+ }
278
+ }
279
+ } else {
280
+ effectiveContainerWidth = constrainedWidth
281
+ }
282
+ }
283
+
284
+ return {
285
+ colCount: RCSSColCount,
286
+ effectiveContainerWidth: effectiveContainerWidth,
287
+ effectiveLineLength: Math.round(((effectiveContainerWidth / RCSSColCount) / (scale && scale >= 1 ? scale : 1)) * zoomCompensation)
288
+ };
289
+ }
290
+
291
+ // This behaves as paginate where colCount = 1
292
+ private computeScrollLength(scale: number | null, ignoreCompensation: boolean | null) {
293
+ const constrainedWidth = Math.round(getContentWidth(this.containerParent) - (this.constraint));
294
+ const metrics = this.getCompensatedMetrics(scale && (scale < 1 || ignoreCompensation) ? scale : 1, ignoreCompensation);
295
+ const zoomCompensation = metrics.zoomCompensation;
296
+ const optimal = metrics.optimal;
297
+ const maximal = metrics.maximal;
298
+
299
+ let RCSSColCount = undefined;
300
+ let effectiveContainerWidth = constrainedWidth;
301
+ let effectiveLineLength = Math.round(optimal * zoomCompensation);
302
+
303
+ if (this.layoutStrategy === LayoutStrategy.margin) {
304
+ const computedWidth = Math.min(Math.round(optimal * zoomCompensation), constrainedWidth);
305
+ effectiveLineLength = ignoreCompensation ? computedWidth : Math.round(computedWidth * zoomCompensation);
306
+ } else if (
307
+ this.layoutStrategy === LayoutStrategy.lineLength ||
308
+ this.layoutStrategy === LayoutStrategy.columns
309
+ ) {
310
+ if (this.layoutStrategy === LayoutStrategy.columns) {
311
+ console.error("Columns strategy is not compatible with scroll. Falling back to lineLength strategy.");
312
+ }
313
+ if (maximal === null) {
314
+ effectiveLineLength = constrainedWidth;
315
+ } else {
316
+ const computedWidth = Math.min(Math.round(maximal * zoomCompensation), constrainedWidth);
317
+ effectiveLineLength = ignoreCompensation ? computedWidth : Math.round(computedWidth * zoomCompensation);
318
+ }
319
+ }
320
+
321
+ return {
322
+ colCount: RCSSColCount,
323
+ effectiveContainerWidth: effectiveContainerWidth,
324
+ effectiveLineLength: effectiveLineLength
325
+ }
326
+ }
327
+
328
+ setContainerWidth() {
329
+ this.container.style.width = `${ this.effectiveContainerWidth }px`;
330
+ }
331
+
332
+ resizeHandler() {
333
+ const pagination = this.updateLayout(this.userProperties.fontSize, this.userProperties.deprecatedFontSize || this.userProperties.iOSPatch, this.userProperties.view === "scroll", this.cachedColCount);
334
+ this.userProperties.colCount = pagination.colCount;
335
+ this.userProperties.lineLength = pagination.effectiveLineLength;
336
+ this.effectiveContainerWidth = pagination.effectiveContainerWidth;
337
+ this.container.style.width = `${ this.effectiveContainerWidth }px`;
338
+ }
339
+ }
@@ -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!
@@ -1,6 +1,7 @@
1
1
  import { Loader, ModuleName } from "@readium/navigator-html-injectables";
2
2
  import { FrameComms } from "./FrameComms";
3
3
  import { ReadiumWindow } from "../../../../navigator-html-injectables/types/src/helpers/dom";
4
+ import { sML } from "../../helpers";
4
5
 
5
6
 
6
7
  export class FrameManager {
@@ -8,6 +9,7 @@ export class FrameManager {
8
9
  private loader: Loader | undefined;
9
10
  public readonly source: string;
10
11
  private comms: FrameComms | undefined;
12
+ private hidden: boolean = true;
11
13
  private destroyed: boolean = false;
12
14
 
13
15
  private currModules: ModuleName[] = [];
@@ -67,6 +69,7 @@ export class FrameManager {
67
69
  this.frame.style.setProperty("aria-hidden", "true");
68
70
  this.frame.style.opacity = "0";
69
71
  this.frame.style.pointerEvents = "none";
72
+ this.hidden = true;
70
73
  if(this.frame.parentElement) {
71
74
  if(this.comms === undefined || !this.comms.ready) return;
72
75
  return new Promise((res, _) => {
@@ -92,9 +95,15 @@ export class FrameManager {
92
95
  this.frame.style.removeProperty("aria-hidden");
93
96
  this.frame.style.removeProperty("opacity");
94
97
  this.frame.style.removeProperty("pointer-events");
98
+ this.hidden = false;
99
+
100
+ if (sML.UA.WebKit) {
101
+ this.comms?.send("force_webkit_recalc", undefined);
102
+ }
103
+
95
104
  res();
96
105
  }
97
- if(atProgress && atProgress > 0) {
106
+ if(atProgress !== undefined) {
98
107
  this.comms?.send("go_progression", atProgress, remove);
99
108
  } else {
100
109
  remove();
@@ -104,6 +113,19 @@ export class FrameManager {
104
113
  });
105
114
  }
106
115
 
116
+ setCSSProperties(properties: { [key: string]: string }) {
117
+ if(this.destroyed || !this.frame.contentWindow) return;
118
+
119
+ // We need to resume and halt postMessage to update the properties
120
+ // if the frame is hidden since it’s been halted in hide()
121
+ if (this.hidden) {
122
+ if (this.comms) this.comms?.resume();
123
+ else this.comms = new FrameComms(this.frame.contentWindow!, this.source);
124
+ }
125
+ this.comms?.send("update_properties", properties);
126
+ if (this.hidden) this.comms?.halt();
127
+ }
128
+
107
129
  get iframe() {
108
130
  if(this.destroyed) throw Error("Trying to use frame when it doesn't exist");
109
131
  return this.frame;
@@ -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
  }
@@ -130,9 +155,8 @@ export class FramePoolManager {
130
155
  await newFrame.load(modules); // In order to ensure modules match the latest configuration
131
156
 
132
157
  // Update progression if necessary and show the new frame
133
- const hasProgression = (locator?.locations?.progression ?? 0) > 0;
134
158
  if(newFrame) // If user is speeding through the publication, this can get destroyed
135
- await newFrame.show(hasProgression ? locator.locations.progression! : undefined); // Show/activate new frame
159
+ await newFrame.show(locator.locations.progression); // Show/activate new frame
136
160
 
137
161
  this._currentFrame = newFrame;
138
162
  }
@@ -144,6 +168,40 @@ export class FramePoolManager {
144
168
  this.inprogress.delete(newHref); // Delete it from the in progress map!
145
169
  }
146
170
 
171
+ setCSSProperties(properties: { [key: string]: string }) {
172
+ const deepCompare = (obj1: { [key: string]: string }, obj2: { [key: string]: string }) => {
173
+ const keys1 = Object.keys(obj1);
174
+ const keys2 = Object.keys(obj2);
175
+
176
+ if (keys1.length !== keys2.length) {
177
+ return false;
178
+ }
179
+
180
+ for (const key of keys1) {
181
+ if (obj1[key] !== obj2[key]) {
182
+ return false;
183
+ }
184
+ }
185
+
186
+ return true;
187
+ };
188
+
189
+ // If CSSProperties have changed, we update the currentCssProperties,
190
+ // and set the CSS Properties to all frames already in the pool
191
+ // We also need to invalidate the blobs and recreate them with the new properties.
192
+ // We do that in update, by updating them when needed (they are added into the pool)
193
+ // so that we do not invalidate and recreate blobs over and over again.
194
+ if(!deepCompare(this.currentCssProperties || {}, properties)) {
195
+ this.currentCssProperties = properties;
196
+ this.pool.forEach((frame) => {
197
+ frame.setCSSProperties(properties);
198
+ });
199
+ for (const href of this.blobs.keys()) {
200
+ this.pendingUpdates.set(href, { inPool: this.pool.has(href) });
201
+ }
202
+ }
203
+ }
204
+
147
205
  get currentFrames(): (FrameManager | undefined)[] {
148
206
  return [this._currentFrame];
149
207
  }