@readium/navigator 1.2.0
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/LICENSE +28 -0
- package/README.MD +11 -0
- package/dist/assets/AccessibleDfA.otf +0 -0
- package/dist/assets/iAWriterDuospace-Regular.ttf +0 -0
- package/dist/index.js +6263 -0
- package/dist/index.umd.cjs +107 -0
- package/package.json +65 -0
- package/src/Navigator.ts +66 -0
- package/src/audio/engine/AudioEngine.ts +136 -0
- package/src/audio/engine/WebAudioEngine.ts +286 -0
- package/src/audio/engine/index.ts +2 -0
- package/src/audio/index.ts +1 -0
- package/src/epub/EpubNavigator.ts +507 -0
- package/src/epub/frame/FrameBlobBuilder.ts +211 -0
- package/src/epub/frame/FrameComms.ts +142 -0
- package/src/epub/frame/FrameManager.ts +134 -0
- package/src/epub/frame/FramePoolManager.ts +179 -0
- package/src/epub/frame/index.ts +3 -0
- package/src/epub/fxl/FXLCoordinator.ts +152 -0
- package/src/epub/fxl/FXLFrameManager.ts +286 -0
- package/src/epub/fxl/FXLFramePoolManager.ts +632 -0
- package/src/epub/fxl/FXLPeripherals.ts +587 -0
- package/src/epub/fxl/FXLPeripheralsDebug.ts +46 -0
- package/src/epub/fxl/FXLSpreader.ts +95 -0
- package/src/epub/fxl/index.ts +5 -0
- package/src/epub/index.ts +3 -0
- package/src/helpers/sML.ts +120 -0
- package/src/index.ts +3 -0
- package/types/src/Navigator.d.ts +41 -0
- package/types/src/audio/engine/AudioEngine.d.ts +114 -0
- package/types/src/audio/engine/WebAudioEngine.d.ts +107 -0
- package/types/src/audio/engine/index.d.ts +2 -0
- package/types/src/audio/index.d.ts +1 -0
- package/types/src/epub/EpubNavigator.d.ts +66 -0
- package/types/src/epub/frame/FrameBlobBuilder.d.ts +13 -0
- package/types/src/epub/frame/FrameComms.d.ts +26 -0
- package/types/src/epub/frame/FrameManager.d.ts +21 -0
- package/types/src/epub/frame/FramePoolManager.d.ts +17 -0
- package/types/src/epub/frame/index.d.ts +3 -0
- package/types/src/epub/fxl/FXLCoordinator.d.ts +37 -0
- package/types/src/epub/fxl/FXLFrameManager.d.ts +41 -0
- package/types/src/epub/fxl/FXLFramePoolManager.d.ts +93 -0
- package/types/src/epub/fxl/FXLPeripherals.d.ts +97 -0
- package/types/src/epub/fxl/FXLPeripheralsDebug.d.ts +13 -0
- package/types/src/epub/fxl/FXLSpreader.d.ts +12 -0
- package/types/src/epub/fxl/index.d.ts +5 -0
- package/types/src/epub/index.d.ts +3 -0
- package/types/src/helpers/sML.d.ts +51 -0
- package/types/src/index.d.ts +3 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { ModuleName } from "@readium/navigator-html-injectables";
|
|
2
|
+
import { Locator, Publication, ReadingProgression, Orientation, Page, Link, Spread } from "@readium/shared";
|
|
3
|
+
import { FrameCommsListener } from "../frame";
|
|
4
|
+
import FrameBlobBuider from "../frame/FrameBlobBuilder";
|
|
5
|
+
import { FXLFrameManager } from "./FXLFrameManager";
|
|
6
|
+
import { FXLPeripherals } from "./FXLPeripherals";
|
|
7
|
+
import { FXLSpreader } from "./FXLSpreader";
|
|
8
|
+
|
|
9
|
+
const UPPER_BOUNDARY = 8;
|
|
10
|
+
const LOWER_BOUNDARY = 5;
|
|
11
|
+
|
|
12
|
+
const OFFSCREEN_LOAD_DELAY = 300;
|
|
13
|
+
const OFFSCREEN_LOAD_TIMEOUT = 15000;
|
|
14
|
+
const RESIZE_UPDATE_TIMEOUT = 250;
|
|
15
|
+
const SLIDE_FAST = 150;
|
|
16
|
+
const SLIDE_SLOW = 500;
|
|
17
|
+
|
|
18
|
+
export class FXLFramePoolManager {
|
|
19
|
+
private readonly container: HTMLElement;
|
|
20
|
+
private readonly positions: Locator[];
|
|
21
|
+
private readonly pool: Map<string, FXLFrameManager> = new Map();
|
|
22
|
+
private readonly blobs: Map<string, string> = new Map();
|
|
23
|
+
private readonly inprogress: Map<string, Promise<void>> = new Map();
|
|
24
|
+
private readonly delayedShow: Map<string, Promise<void>> = new Map();
|
|
25
|
+
private readonly delayedTimeout: Map<string, number> = new Map();
|
|
26
|
+
private currentBaseURL: string | undefined;
|
|
27
|
+
private previousFrames: FXLFrameManager[] = [];
|
|
28
|
+
|
|
29
|
+
// NEW
|
|
30
|
+
private readonly bookElement: HTMLDivElement;
|
|
31
|
+
public readonly spineElement: HTMLDivElement;
|
|
32
|
+
private readonly pub: Publication;
|
|
33
|
+
public width: number = 0;
|
|
34
|
+
public height: number = 0;
|
|
35
|
+
private transform: string = "";
|
|
36
|
+
public currentSlide: number = 0;
|
|
37
|
+
private spreader: FXLSpreader;
|
|
38
|
+
private spread = true; // TODO
|
|
39
|
+
private readonly spreadPresentation: Spread;
|
|
40
|
+
private orientationInternal = -1; // Portrait = 1, Landscape = 0, Unknown = -1
|
|
41
|
+
private containerHeightCached: number;
|
|
42
|
+
private readonly resizeBoundHandler: EventListenerOrEventListenerObject;
|
|
43
|
+
private resizeTimeout: number | undefined;
|
|
44
|
+
// private readonly pages: FXLFrameManager[] = [];
|
|
45
|
+
public readonly peripherals: FXLPeripherals;
|
|
46
|
+
|
|
47
|
+
constructor(container: HTMLElement, positions: Locator[], pub: Publication) {
|
|
48
|
+
this.container = container;
|
|
49
|
+
this.positions = positions;
|
|
50
|
+
this.pub = pub;
|
|
51
|
+
this.spreadPresentation = pub.metadata.getPresentation()?.spread || Spread.auto;
|
|
52
|
+
|
|
53
|
+
if(this.pub.metadata.effectiveReadingProgression !== ReadingProgression.rtl && this.pub.metadata.effectiveReadingProgression !== ReadingProgression.ltr)
|
|
54
|
+
// TODO support TTB and BTT
|
|
55
|
+
throw Error("Unsupported reading progression for EPUB");
|
|
56
|
+
|
|
57
|
+
// NEW
|
|
58
|
+
this.spreader = new FXLSpreader(this.pub);
|
|
59
|
+
this.containerHeightCached = container.clientHeight;
|
|
60
|
+
this.resizeBoundHandler = this.nativeResizeHandler.bind(this);
|
|
61
|
+
|
|
62
|
+
this.ownerWindow.addEventListener("resize", this.resizeBoundHandler);
|
|
63
|
+
this.ownerWindow.addEventListener("orientationchange", this.resizeBoundHandler);
|
|
64
|
+
|
|
65
|
+
this.bookElement = document.createElement("div");
|
|
66
|
+
this.bookElement.ariaLabel = "Book";
|
|
67
|
+
this.bookElement.tabIndex = -1;
|
|
68
|
+
this.updateBookStyle(true);
|
|
69
|
+
|
|
70
|
+
this.spineElement = document.createElement("div");
|
|
71
|
+
this.spineElement.ariaLabel = "Spine";
|
|
72
|
+
|
|
73
|
+
this.bookElement.appendChild(this.spineElement);
|
|
74
|
+
this.container.appendChild(this.bookElement);
|
|
75
|
+
this.updateSpineStyle(true);
|
|
76
|
+
|
|
77
|
+
this.peripherals = new FXLPeripherals(this);
|
|
78
|
+
|
|
79
|
+
this.pub.readingOrder.items.forEach((link) => {
|
|
80
|
+
// Create <iframe>
|
|
81
|
+
const fm = new FXLFrameManager(this.peripherals, this.pub.metadata.effectiveReadingProgression, link.href);
|
|
82
|
+
this.spineElement.appendChild(fm.element);
|
|
83
|
+
|
|
84
|
+
// this.pages.push(fm);
|
|
85
|
+
this.pool.set(link.href, fm);
|
|
86
|
+
fm.width = 100 / this.length * (link.properties?.getOrientation() === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
|
|
87
|
+
fm.height = this.height;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private _listener!: FrameCommsListener;
|
|
92
|
+
public set listener(listener: FrameCommsListener) {
|
|
93
|
+
this._listener = listener;
|
|
94
|
+
}
|
|
95
|
+
public get listener() {
|
|
96
|
+
return this._listener;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
public get doNotDisturb() {
|
|
100
|
+
// TODO other situations
|
|
101
|
+
return this.peripherals.pan.touchID > 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private nativeResizeHandler(_: Event) {
|
|
105
|
+
this.resizeHandler(true);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* When window resizes, resize slider components as well
|
|
110
|
+
*/
|
|
111
|
+
resizeHandler(slide = true, fast = true) {
|
|
112
|
+
// relcalculate currentSlide
|
|
113
|
+
// prevent hiding items when browser width increases
|
|
114
|
+
|
|
115
|
+
if (this.currentSlide + this.perPage > this.length) {
|
|
116
|
+
this.currentSlide = this.length <= this.perPage ? 0 : this.length - 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.containerHeightCached = this.container.clientHeight;
|
|
120
|
+
|
|
121
|
+
this.orientationInternal = -1;
|
|
122
|
+
|
|
123
|
+
this.updateSpineStyle(true);
|
|
124
|
+
if(slide/* && !sML.Mobile*/) {
|
|
125
|
+
this.currentSlide = this.reAlign();
|
|
126
|
+
this.slideToCurrent(!fast, fast);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
clearTimeout(this.resizeTimeout);
|
|
130
|
+
this.resizeTimeout = window.setTimeout(() => {
|
|
131
|
+
// TODO optimize this expensive set of loops and operations
|
|
132
|
+
this.pool.forEach((frm, linkHref) => {
|
|
133
|
+
let i = this.pub.readingOrder.items.findIndex(l => l.href === linkHref);
|
|
134
|
+
const link = this.pub.readingOrder.items[i];
|
|
135
|
+
frm.width = 100 / this.length * (link.properties?.getOrientation() === Orientation.landscape || link.properties?.otherProperties["addBlank"] ? this.perPage : 1);
|
|
136
|
+
frm.height = this.height;
|
|
137
|
+
if(!frm.loaded) return;
|
|
138
|
+
const spread = this.spreader.findByLink(link)!;
|
|
139
|
+
frm.update(this.spreadPosition(spread, link));
|
|
140
|
+
});
|
|
141
|
+
}, RESIZE_UPDATE_TIMEOUT);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* It is important that these values be cached to avoid spamming them on redraws, they are expensive.
|
|
146
|
+
*/
|
|
147
|
+
private updateDimensions() {
|
|
148
|
+
this.width = this.bookElement.clientWidth;
|
|
149
|
+
this.height = this.bookElement.clientHeight;
|
|
150
|
+
// this.containerHeightCached = r.height;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public get rtl() {
|
|
154
|
+
return this.pub.metadata.effectiveReadingProgression === ReadingProgression.rtl;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private get single() {
|
|
158
|
+
return !this.spread || this.portrait;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public get perPage() {
|
|
162
|
+
return (this.spread && !this.portrait) ? 2 : 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get threshold(): number {
|
|
166
|
+
return 50;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
get portrait(): boolean {
|
|
170
|
+
if(this.spreadPresentation === Spread.none) return true; // No spreads
|
|
171
|
+
if(this.orientationInternal === -1) {
|
|
172
|
+
this.orientationInternal = this.containerHeightCached > this.container.clientWidth ? 1 : 0;
|
|
173
|
+
}
|
|
174
|
+
return this.orientationInternal === 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
public updateSpineStyle(animate: boolean, fast = true) {
|
|
178
|
+
let margin = "0";
|
|
179
|
+
this.updateDimensions();
|
|
180
|
+
if(this.perPage > 1 && true) // this.shift
|
|
181
|
+
margin = `${this.width / 2}px`;
|
|
182
|
+
|
|
183
|
+
const spineStyle = {
|
|
184
|
+
transition: animate ? `all ${fast ? SLIDE_FAST : SLIDE_SLOW}ms ease-out` : "all 0ms ease-out",
|
|
185
|
+
marginRight: this.rtl ? margin : "0",
|
|
186
|
+
marginLeft: this.rtl ? "0" : margin,
|
|
187
|
+
width: `${(this.width / this.perPage) * this.length}px`,
|
|
188
|
+
transform: this.transform,
|
|
189
|
+
|
|
190
|
+
// Static (should be moved to CSS)
|
|
191
|
+
contain: "content"
|
|
192
|
+
} as CSSStyleDeclaration;
|
|
193
|
+
|
|
194
|
+
Object.assign(this.spineElement.style, spineStyle);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
public updateBookStyle(initial=false) {
|
|
198
|
+
if(initial) {
|
|
199
|
+
const bookStyle = {
|
|
200
|
+
overflow: "hidden",
|
|
201
|
+
direction: this.pub.metadata.effectiveReadingProgression,
|
|
202
|
+
cursor: "",
|
|
203
|
+
// Static (should be moved to CSS)
|
|
204
|
+
// minHeight: 100%
|
|
205
|
+
// maxHeight: "100%",
|
|
206
|
+
height: "100%",
|
|
207
|
+
width: "100%",
|
|
208
|
+
position: "relative",
|
|
209
|
+
outline: "none",
|
|
210
|
+
transition: this.peripherals?.dragState ? "none" : "transform .15s ease-in-out",
|
|
211
|
+
touchAction: "none",
|
|
212
|
+
} as CSSStyleDeclaration;
|
|
213
|
+
Object.assign(this.bookElement.style, bookStyle);
|
|
214
|
+
}
|
|
215
|
+
this.bookElement.style.transform = `scale(${this.peripherals?.scale || 1})` + (this.peripherals ? ` translate3d(${this.peripherals.pan.translateX}px, ${this.peripherals.pan.translateY}px, 0px)` : "");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Go to slide with particular index
|
|
220
|
+
* @param {number} index - Item index to slide to.
|
|
221
|
+
*/
|
|
222
|
+
goTo(index: number) {
|
|
223
|
+
if (this.slength <= this.perPage)
|
|
224
|
+
return;
|
|
225
|
+
index = this.reAlign(index);
|
|
226
|
+
const beforeChange = this.currentSlide;
|
|
227
|
+
this.currentSlide = Math.min(Math.max(index, 0), this.length - 1);
|
|
228
|
+
if (beforeChange !== this.currentSlide) {
|
|
229
|
+
this.slideToCurrent(false);
|
|
230
|
+
// this.onChange();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
onChange() {
|
|
235
|
+
this.peripherals.scale = 1;
|
|
236
|
+
this.updateBookStyle();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private get offset() {
|
|
240
|
+
return (this.rtl ? 1 : -1) * this.currentSlide * (this.width / this.perPage);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get length() {
|
|
244
|
+
if(this.single)
|
|
245
|
+
return this.slength;
|
|
246
|
+
const total = this.slength + this.nLandscape;
|
|
247
|
+
return (this.shift && (total % 2 === 0)) ? total + 1 : total;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
get slength() {
|
|
251
|
+
return this.pub.readingOrder.items.length || 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
get shift() {
|
|
255
|
+
return this.spreader.shift;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private get nLandscape() {
|
|
259
|
+
return this.spreader.nLandscape;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
public setPerPage(perPage: number) {
|
|
263
|
+
if(perPage === 0) {
|
|
264
|
+
// TODO this mode is auto
|
|
265
|
+
this.spread = true;
|
|
266
|
+
} else if(perPage === 1) {
|
|
267
|
+
this.spread = false;
|
|
268
|
+
} else {
|
|
269
|
+
this.spread = true;
|
|
270
|
+
}
|
|
271
|
+
requestAnimationFrame(() => this.resizeHandler(true));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Moves sliders frame to position of currently active slide
|
|
276
|
+
*/
|
|
277
|
+
slideToCurrent(enableTransition?: boolean, fast = true) {
|
|
278
|
+
this.updateDimensions();
|
|
279
|
+
if (enableTransition) {
|
|
280
|
+
// This one is tricky, I know but this is a perfect explanation:
|
|
281
|
+
// https://youtu.be/cCOL7MC4Pl0
|
|
282
|
+
requestAnimationFrame(() => {
|
|
283
|
+
requestAnimationFrame(() => {
|
|
284
|
+
const newTransform = `translate3d(${this.offset}px, 0, 0)`;
|
|
285
|
+
if(this.spineElement.style.transform === newTransform) return;
|
|
286
|
+
this.transform = newTransform;
|
|
287
|
+
this.updateSpineStyle(true, fast);
|
|
288
|
+
this.deselect();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
} else {
|
|
292
|
+
const newTransform = `translate3d(${this.offset}px, 0, 0)`;
|
|
293
|
+
if(this.spineElement.style.transform === newTransform) return;
|
|
294
|
+
this.transform = newTransform;
|
|
295
|
+
this.updateSpineStyle(false);
|
|
296
|
+
this.deselect();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
bounce(rtl = false) {
|
|
301
|
+
requestAnimationFrame(() => {
|
|
302
|
+
this.transform = `translate3d(${this.offset+(50 * (rtl ? 1 : -1))}px, 0, 0)`;
|
|
303
|
+
this.updateSpineStyle(true, true);
|
|
304
|
+
setTimeout(() => {
|
|
305
|
+
this.transform = `translate3d(${this.offset}px, 0, 0)`;
|
|
306
|
+
this.updateSpineStyle(true, true);
|
|
307
|
+
}, 100);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Go to next slide.
|
|
314
|
+
* @param {number} [howManySlides=1] - How many items to slide forward.
|
|
315
|
+
* @returns {boolean} Whether or not going to next was possible
|
|
316
|
+
*/
|
|
317
|
+
next(howManySlides = 1): boolean {
|
|
318
|
+
// early return when there is nothing to slide
|
|
319
|
+
if (this.slength <= this.perPage) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const beforeChange = this.currentSlide;
|
|
324
|
+
|
|
325
|
+
this.currentSlide = Math.min(this.currentSlide + howManySlides, this.length - 1);
|
|
326
|
+
if(this.perPage > 1 && this.currentSlide % 2)
|
|
327
|
+
this.currentSlide--;
|
|
328
|
+
|
|
329
|
+
if(this.currentSlide === beforeChange && this.currentSlide + 1 === this.length) {
|
|
330
|
+
// At end and trying to go further, means trigger "last page" callback
|
|
331
|
+
// this.onLastPage();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (beforeChange !== this.currentSlide) {
|
|
335
|
+
this.slideToCurrent(true);
|
|
336
|
+
this.onChange();
|
|
337
|
+
return true;
|
|
338
|
+
} else {
|
|
339
|
+
this.bounce(this.rtl);
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Go to previous slide.
|
|
346
|
+
* @param {number} [howManySlides=1] - How many items to slide backward.
|
|
347
|
+
* @returns {boolean} Whether or not going to prev was possible
|
|
348
|
+
*/
|
|
349
|
+
prev(howManySlides = 1): boolean {
|
|
350
|
+
// early return when there is nothing to slide
|
|
351
|
+
if (this.slength <= this.perPage) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const beforeChange = this.currentSlide;
|
|
356
|
+
|
|
357
|
+
this.currentSlide = Math.max(this.currentSlide - howManySlides, 0);
|
|
358
|
+
if(this.perPage > 1 && this.currentSlide % 2)
|
|
359
|
+
this.currentSlide++;
|
|
360
|
+
|
|
361
|
+
if (beforeChange !== this.currentSlide) {
|
|
362
|
+
this.slideToCurrent(true);
|
|
363
|
+
this.onChange();
|
|
364
|
+
return true;
|
|
365
|
+
} else
|
|
366
|
+
this.bounce(!this.rtl);
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
get ownerWindow() {
|
|
371
|
+
return this.container.ownerDocument.defaultView || window;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
// OLD
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
async destroy() {
|
|
379
|
+
this.ownerWindow.removeEventListener("resize", this.resizeBoundHandler);
|
|
380
|
+
this.ownerWindow.removeEventListener("orientationchange", this.resizeBoundHandler);
|
|
381
|
+
|
|
382
|
+
// Wait for all in-progress loads to complete
|
|
383
|
+
let iit = this.inprogress.values();
|
|
384
|
+
let inp = iit.next();
|
|
385
|
+
const inprogressPromises: Promise<void>[] = [];
|
|
386
|
+
while(inp.value) {
|
|
387
|
+
inprogressPromises.push(inp.value);
|
|
388
|
+
inp = iit.next();
|
|
389
|
+
}
|
|
390
|
+
if(inprogressPromises.length > 0) {
|
|
391
|
+
await Promise.allSettled(inprogressPromises);
|
|
392
|
+
}
|
|
393
|
+
this.inprogress.clear();
|
|
394
|
+
|
|
395
|
+
// Destroy all frames
|
|
396
|
+
let fit = this.pool.values();
|
|
397
|
+
let frm = fit.next();
|
|
398
|
+
while(frm.value) {
|
|
399
|
+
await (frm.value as FXLFrameManager).destroy();
|
|
400
|
+
frm = fit.next();
|
|
401
|
+
}
|
|
402
|
+
this.pool.clear();
|
|
403
|
+
|
|
404
|
+
// Revoke all blobs
|
|
405
|
+
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
406
|
+
|
|
407
|
+
// Empty container of elements
|
|
408
|
+
this.container.childNodes.forEach(v => {
|
|
409
|
+
if(v.nodeType === Node.ELEMENT_NODE || v.nodeType === Node.TEXT_NODE) v.remove();
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
makeSpread(itemIndex: number) {
|
|
414
|
+
return this.perPage < 2 ? [this.pub.readingOrder.items[itemIndex]] : this.spreader.currentSpread(itemIndex, this.perPage);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
reAlign(index: number = this.currentSlide) {
|
|
418
|
+
if (index % 2 && !this.single) // Prevent getting out of track
|
|
419
|
+
index++;
|
|
420
|
+
return index;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
spreadPosition(spread: Link[], target: Link) {
|
|
424
|
+
return this.perPage < 2 ? Page.center : (spread.length < 2 ? Page.center : (
|
|
425
|
+
target.href === spread[0].href ? (this.rtl ? Page.right : Page.left) : (this.rtl ? Page.left : Page.right)
|
|
426
|
+
));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async waitForItem(href: string) {
|
|
430
|
+
if(this.inprogress.has(href))
|
|
431
|
+
// If this same href is already being loaded, block until the other function
|
|
432
|
+
// call has finished executing so we don't end up e.g. loading the blob twice.
|
|
433
|
+
await this.inprogress.get(href);
|
|
434
|
+
|
|
435
|
+
if(this.delayedShow.has(href)) {
|
|
436
|
+
const timeoutVal = this.delayedTimeout.get(href)!;
|
|
437
|
+
if(timeoutVal > 0) {
|
|
438
|
+
// Delayed resource showing has not yet commenced, cancel it
|
|
439
|
+
clearTimeout(timeoutVal);
|
|
440
|
+
} else {
|
|
441
|
+
// Await a current delayed showing of the resource
|
|
442
|
+
await this.delayedShow.get(href);
|
|
443
|
+
}
|
|
444
|
+
this.delayedTimeout.set(href, 0);
|
|
445
|
+
this.delayedShow.delete(href);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async cancelShowing(href: string) {
|
|
450
|
+
if(this.delayedShow.has(href)) {
|
|
451
|
+
const timeoutVal = this.delayedTimeout.get(href)!;
|
|
452
|
+
if(timeoutVal > 0) {
|
|
453
|
+
// Delayed resource showing has not yet commenced, cancel it
|
|
454
|
+
clearTimeout(timeoutVal);
|
|
455
|
+
}
|
|
456
|
+
this.delayedShow.delete(href);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async update(pub: Publication, locator: Locator, modules: ModuleName[], _force=false) {
|
|
461
|
+
let i = this.pub.readingOrder.items.findIndex(l => l.href === locator.href);
|
|
462
|
+
if(i < 0) throw Error("Href not found in reading order");
|
|
463
|
+
|
|
464
|
+
if(this.currentSlide !== i) {
|
|
465
|
+
this.currentSlide = this.reAlign(i);
|
|
466
|
+
this.slideToCurrent(true);
|
|
467
|
+
}
|
|
468
|
+
const spread = this.makeSpread(this.currentSlide);
|
|
469
|
+
if(this.perPage > 1) i++;
|
|
470
|
+
for (const s of spread) {
|
|
471
|
+
await this.waitForItem(s.href);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Create a new progress that doesn't resolve until complete
|
|
475
|
+
// loading of the resource and its dependencies has finished.
|
|
476
|
+
const progressPromise = new Promise<void>(async (resolve, reject) => {
|
|
477
|
+
const disposal: string[] = [];
|
|
478
|
+
const creation: string[] = [];
|
|
479
|
+
|
|
480
|
+
this.positions.forEach((l, j) => {
|
|
481
|
+
if(j > (i + UPPER_BOUNDARY) || j < (i - UPPER_BOUNDARY)) {
|
|
482
|
+
if(!disposal.includes(l.href)) disposal.push(l.href);
|
|
483
|
+
}
|
|
484
|
+
if(j < (i + LOWER_BOUNDARY) && j > (i - LOWER_BOUNDARY)) {
|
|
485
|
+
if(!creation.includes(l.href)) creation.push(l.href);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
disposal.forEach(async href => {
|
|
489
|
+
if(creation.includes(href)) return;
|
|
490
|
+
if(!this.pool.has(href)) return;
|
|
491
|
+
this.cancelShowing(href);
|
|
492
|
+
await this.pool.get(href)?.unload();
|
|
493
|
+
// this.pool.delete(href);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Check if base URL of publication has changed
|
|
497
|
+
if(this.currentBaseURL !== undefined && pub.baseURL !== this.currentBaseURL) {
|
|
498
|
+
// Revoke all blobs
|
|
499
|
+
this.blobs.forEach(v => URL.revokeObjectURL(v));
|
|
500
|
+
this.blobs.clear();
|
|
501
|
+
}
|
|
502
|
+
this.currentBaseURL = pub.baseURL;
|
|
503
|
+
|
|
504
|
+
const creator = async (href: string) => {
|
|
505
|
+
const index = pub.readingOrder.findIndexWithHref(href);
|
|
506
|
+
const itm = pub.readingOrder.items[index];
|
|
507
|
+
if(!itm) return; // TODO throw?
|
|
508
|
+
if(!this.blobs.has(href)) {
|
|
509
|
+
const blobBuilder = new FrameBlobBuider(pub, this.currentBaseURL || "", itm);
|
|
510
|
+
const blobURL = await blobBuilder.build(true);
|
|
511
|
+
this.blobs.set(href, blobURL);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Show future offscreen frame in advance after a delay
|
|
515
|
+
// The added delay prevents this expensive operation from
|
|
516
|
+
// occuring during the sliding animation, to reduce lag
|
|
517
|
+
if(!this.delayedShow.has(href))
|
|
518
|
+
this.delayedShow.set(href, new Promise((resolve, reject) => {
|
|
519
|
+
let done = false;
|
|
520
|
+
const t = window.setTimeout(async () => {
|
|
521
|
+
this.delayedTimeout.set(href, 0);
|
|
522
|
+
const spread = this.makeSpread(this.reAlign(index));
|
|
523
|
+
const page = this.spreadPosition(spread, itm);
|
|
524
|
+
const fm = this.pool.get(href)!;
|
|
525
|
+
await fm.load(modules, this.blobs.get(href)!);
|
|
526
|
+
if(!this.peripherals.isScaled) // When scaled, positioning is screwed up, so wait to show
|
|
527
|
+
await fm.show(page); // Show/activate new frame
|
|
528
|
+
this.delayedShow.delete(href);
|
|
529
|
+
done = true;
|
|
530
|
+
resolve();
|
|
531
|
+
}, OFFSCREEN_LOAD_DELAY);
|
|
532
|
+
setTimeout(() => {
|
|
533
|
+
if(!done && this.delayedShow.has(href)) reject(`Offscreen load timeout: ${href}`);
|
|
534
|
+
}, OFFSCREEN_LOAD_TIMEOUT);
|
|
535
|
+
this.delayedTimeout.set(href, t);
|
|
536
|
+
}));
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
await Promise.all(creation.map(href => creator(href)));
|
|
540
|
+
} catch (error) {
|
|
541
|
+
reject(error);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Update current frame(s)
|
|
545
|
+
const newFrames: FXLFrameManager[] = [];
|
|
546
|
+
for (const s of spread) {
|
|
547
|
+
const newFrame = this.pool.get(s.href)!;
|
|
548
|
+
const source = this.blobs.get(s.href);
|
|
549
|
+
if(!source) continue; // This can get destroyed
|
|
550
|
+
|
|
551
|
+
this.cancelShowing(s.href);
|
|
552
|
+
await newFrame.load(modules, source); // In order to ensure modules match the latest configuration
|
|
553
|
+
await newFrame.show(this.spreadPosition(spread, s)); // Show/activate new frame
|
|
554
|
+
this.previousFrames.push(newFrame);
|
|
555
|
+
await newFrame.activate();
|
|
556
|
+
newFrames.push(newFrame);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Unfocus previous frame(s)
|
|
560
|
+
while(this.previousFrames.length > 0) {
|
|
561
|
+
const fm = this.previousFrames.shift();
|
|
562
|
+
if(fm && !newFrames.includes(fm))
|
|
563
|
+
await fm.unfocus();
|
|
564
|
+
}
|
|
565
|
+
this.previousFrames = newFrames;
|
|
566
|
+
|
|
567
|
+
resolve();
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
for (const s of spread) {
|
|
571
|
+
this.inprogress.set(s.href, progressPromise); // Add the job to the in progress map
|
|
572
|
+
}
|
|
573
|
+
await progressPromise; // Wait on the job to finish...
|
|
574
|
+
for (const s of spread) {
|
|
575
|
+
this.inprogress.delete(s.href); // Delete it from the in progress map!
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
get currentFrames(): (FXLFrameManager | undefined)[] {
|
|
580
|
+
if(this.perPage < 2) {
|
|
581
|
+
const link = this.pub.readingOrder.items[this.currentSlide];
|
|
582
|
+
return [this.pool.get(link.href)];
|
|
583
|
+
}
|
|
584
|
+
const spread = this.spreader.currentSpread(this.currentSlide, this.perPage);
|
|
585
|
+
return spread.map(s => this.pool.get(s.href));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
get currentBounds(): DOMRect {
|
|
589
|
+
const ret = {
|
|
590
|
+
x: 0,
|
|
591
|
+
y: 0,
|
|
592
|
+
width: 0,
|
|
593
|
+
height: 0,
|
|
594
|
+
top: 0,
|
|
595
|
+
right: 0,
|
|
596
|
+
bottom: 0,
|
|
597
|
+
left: 0,
|
|
598
|
+
toJSON() {
|
|
599
|
+
return this;
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
this.currentFrames.forEach(f => {
|
|
603
|
+
if(!f) return;
|
|
604
|
+
const b = f.realSize;
|
|
605
|
+
ret.x = Math.min(ret.x, b.x);
|
|
606
|
+
ret.y = Math.min(ret.y, b.y);
|
|
607
|
+
ret.width += b.width; // TODO different in vertical
|
|
608
|
+
ret.height = Math.max(ret.height, b.height);
|
|
609
|
+
ret.top = Math.min(ret.top, b.top);
|
|
610
|
+
ret.right = Math.min(ret.right, b.right);
|
|
611
|
+
ret.bottom = Math.min(ret.bottom, b.bottom);
|
|
612
|
+
ret.left = Math.min(ret.left, b.left);
|
|
613
|
+
});
|
|
614
|
+
return ret as DOMRect;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
get currentNumbers(): number[] {
|
|
618
|
+
if(this.perPage < 2) {
|
|
619
|
+
const link = this.pub.readingOrder.items[this.currentSlide];
|
|
620
|
+
return [link.properties?.otherProperties["number"]];
|
|
621
|
+
}
|
|
622
|
+
const spread = this.spreader.currentSpread(this.currentSlide, this.perPage);
|
|
623
|
+
return spread.length > 1 ? [
|
|
624
|
+
spread[0].properties?.otherProperties["number"] as number,
|
|
625
|
+
spread[spread.length-1].properties?.otherProperties["number"] as number
|
|
626
|
+
] : [spread[0].properties?.otherProperties["number"] as number];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
deselect() {
|
|
630
|
+
this.currentFrames?.forEach(f => f?.deselect());
|
|
631
|
+
}
|
|
632
|
+
}
|