@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.
- package/dist/index.js +3974 -2928
- package/dist/index.umd.cjs +16 -16
- package/package.json +10 -9
- package/src/Navigator.ts +11 -0
- package/src/epub/EpubNavigator.ts +250 -24
- package/src/epub/css/Properties.ts +396 -0
- package/src/epub/css/ReadiumCSS.ts +339 -0
- package/src/epub/css/index.ts +2 -0
- package/src/epub/frame/FrameBlobBuilder.ts +59 -9
- package/src/epub/frame/FrameManager.ts +23 -1
- package/src/epub/frame/FramePoolManager.ts +62 -4
- package/src/epub/fxl/FXLFramePoolManager.ts +23 -16
- package/src/epub/index.ts +3 -1
- package/src/epub/preferences/EpubDefaults.ts +165 -0
- package/src/epub/preferences/EpubPreferences.ts +192 -0
- package/src/epub/preferences/EpubPreferencesEditor.ts +534 -0
- package/src/epub/preferences/EpubSettings.ts +239 -0
- package/src/epub/preferences/guards.ts +86 -0
- package/src/epub/preferences/index.ts +4 -0
- package/src/helpers/dimensions.ts +13 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/lineLength.ts +241 -0
- package/src/helpers/sML.ts +25 -3
- package/src/index.ts +2 -1
- package/src/preferences/Configurable.ts +16 -0
- package/src/preferences/Preference.ts +272 -0
- package/src/preferences/PreferencesEditor.ts +6 -0
- package/src/preferences/Types.ts +38 -0
- package/src/preferences/index.ts +4 -0
- package/types/src/Navigator.d.ts +9 -0
- package/types/src/epub/EpubNavigator.d.ts +34 -4
- package/types/src/epub/css/Properties.d.ts +183 -0
- package/types/src/epub/css/ReadiumCSS.d.ts +31 -0
- package/types/src/epub/css/index.d.ts +2 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +5 -1
- package/types/src/epub/frame/FrameManager.d.ts +4 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +8 -1
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +4 -4
- package/types/src/epub/index.d.ts +2 -0
- package/types/src/epub/preferences/EpubDefaults.d.ts +86 -0
- package/types/src/epub/preferences/EpubPreferences.d.ts +90 -0
- package/types/src/epub/preferences/EpubPreferencesEditor.d.ts +55 -0
- package/types/src/epub/preferences/EpubSettings.d.ts +89 -0
- package/types/src/epub/preferences/guards.d.ts +9 -0
- package/types/src/epub/preferences/index.d.ts +4 -0
- package/types/src/helpers/dimensions.d.ts +7 -0
- package/types/src/helpers/index.d.ts +1 -0
- package/types/src/helpers/lineLength.d.ts +54 -0
- package/types/src/helpers/sML.d.ts +6 -1
- package/types/src/index.d.ts +1 -0
- package/types/src/preferences/Configurable.d.ts +13 -0
- package/types/src/preferences/Preference.d.ts +117 -0
- package/types/src/preferences/PreferencesEditor.d.ts +5 -0
- package/types/src/preferences/PreferencesSerializer.d.ts +5 -0
- package/types/src/preferences/Types.d.ts +23 -0
- package/types/src/preferences/index.d.ts +4 -0
- 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
|
+
}
|
|
@@ -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
|
|
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
|
|
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(
|
|
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
|
}
|