@readium/navigator-html-injectables 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.
Files changed (77) hide show
  1. package/README.MD +13 -0
  2. package/dist/index.js +3144 -0
  3. package/dist/index.umd.cjs +69 -0
  4. package/package.json +58 -0
  5. package/src/Loader.ts +85 -0
  6. package/src/comms/comms.ts +168 -0
  7. package/src/comms/index.ts +3 -0
  8. package/src/comms/keys.ts +42 -0
  9. package/src/comms/mid.ts +5 -0
  10. package/src/helpers/animation.ts +5 -0
  11. package/src/helpers/css.ts +16 -0
  12. package/src/helpers/document.ts +43 -0
  13. package/src/helpers/dom.ts +126 -0
  14. package/src/helpers/locator.ts +67 -0
  15. package/src/helpers/rect.ts +301 -0
  16. package/src/helpers/scrollSnapperHelper.ts +43 -0
  17. package/src/index.ts +3 -0
  18. package/src/modules/Decorator.ts +462 -0
  19. package/src/modules/Module.ts +23 -0
  20. package/src/modules/ModuleLibrary.ts +44 -0
  21. package/src/modules/Peripherals.ts +148 -0
  22. package/src/modules/index.ts +3 -0
  23. package/src/modules/setup/FixedSetup.ts +73 -0
  24. package/src/modules/setup/ReflowableSetup.ts +65 -0
  25. package/src/modules/setup/Setup.ts +131 -0
  26. package/src/modules/snapper/ColumnSnapper.ts +468 -0
  27. package/src/modules/snapper/ScrollSnapper.ts +168 -0
  28. package/src/modules/snapper/Snapper.ts +49 -0
  29. package/src/vendor/approx-string-match/LICENSE +21 -0
  30. package/src/vendor/approx-string-match/README.MD +1 -0
  31. package/src/vendor/approx-string-match/index.ts +362 -0
  32. package/src/vendor/hypothesis/README.MD +1 -0
  33. package/src/vendor/hypothesis/anchoring/api-types.ts +309 -0
  34. package/src/vendor/hypothesis/anchoring/html.ts +134 -0
  35. package/src/vendor/hypothesis/anchoring/match-quote.ts +163 -0
  36. package/src/vendor/hypothesis/anchoring/placeholder.ts +59 -0
  37. package/src/vendor/hypothesis/anchoring/text-range.ts +327 -0
  38. package/src/vendor/hypothesis/anchoring/trim-range.ts +220 -0
  39. package/src/vendor/hypothesis/anchoring/types.ts +377 -0
  40. package/src/vendor/hypothesis/anchoring/xpath.ts +164 -0
  41. package/src/vendor/hypothesis/tsconfig.json +33 -0
  42. package/src/vendor/hypothesis/types/shared.ts +40 -0
  43. package/types/src/Loader.d.ts +33 -0
  44. package/types/src/comms/comms.d.ts +40 -0
  45. package/types/src/comms/index.d.ts +3 -0
  46. package/types/src/comms/keys.d.ts +2 -0
  47. package/types/src/comms/mid.d.ts +1 -0
  48. package/types/src/helpers/animation.d.ts +1 -0
  49. package/types/src/helpers/css.d.ts +4 -0
  50. package/types/src/helpers/document.d.ts +8 -0
  51. package/types/src/helpers/dom.d.ts +15 -0
  52. package/types/src/helpers/locator.d.ts +2 -0
  53. package/types/src/helpers/rect.d.ts +10 -0
  54. package/types/src/helpers/scrollSnapperHelper.d.ts +5 -0
  55. package/types/src/index.d.ts +3 -0
  56. package/types/src/modules/Decorator.d.ts +43 -0
  57. package/types/src/modules/Module.d.ts +11 -0
  58. package/types/src/modules/ModuleLibrary.d.ts +5 -0
  59. package/types/src/modules/Peripherals.d.ts +39 -0
  60. package/types/src/modules/ReflowablePeripherals.d.ts +37 -0
  61. package/types/src/modules/index.d.ts +3 -0
  62. package/types/src/modules/setup/FixedSetup.d.ts +9 -0
  63. package/types/src/modules/setup/ReflowableSetup.d.ts +9 -0
  64. package/types/src/modules/setup/Setup.d.ts +17 -0
  65. package/types/src/modules/snapper/ColumnSnapper.d.ts +41 -0
  66. package/types/src/modules/snapper/ScrollSnapper.d.ts +16 -0
  67. package/types/src/modules/snapper/Snapper.d.ts +11 -0
  68. package/types/src/vendor/approx-string-match/index.d.ts +54 -0
  69. package/types/src/vendor/hypothesis/anchoring/api-types.d.ts +266 -0
  70. package/types/src/vendor/hypothesis/anchoring/html.d.ts +17 -0
  71. package/types/src/vendor/hypothesis/anchoring/match-quote.d.ts +30 -0
  72. package/types/src/vendor/hypothesis/anchoring/placeholder.d.ts +32 -0
  73. package/types/src/vendor/hypothesis/anchoring/text-range.d.ts +103 -0
  74. package/types/src/vendor/hypothesis/anchoring/trim-range.d.ts +17 -0
  75. package/types/src/vendor/hypothesis/anchoring/types.d.ts +102 -0
  76. package/types/src/vendor/hypothesis/anchoring/xpath.d.ts +15 -0
  77. package/types/src/vendor/hypothesis/types/shared.d.ts +32 -0
@@ -0,0 +1,468 @@
1
+ import { ResizeObserver as Polyfill } from '@juggle/resize-observer';
2
+ import { Comms } from "../../comms";
3
+ import { Snapper } from "./Snapper";
4
+ import { getColumnCountPerScreen, isRTL, appendVirtualColumnIfNeeded } from "../../helpers/document";
5
+ import { easeInOutQuad } from "../../helpers/animation";
6
+ import { ModuleName } from "../ModuleLibrary";
7
+ import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
8
+ import { rangeFromLocator } from "../../helpers/locator";
9
+ import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
10
+
11
+ const COLUMN_SNAPPER_STYLE_ID = "readium-column-snapper-style";
12
+ const SNAP_DURATION = 200; // Milliseconds
13
+
14
+ enum ScrollTouchState {
15
+ END = 0,
16
+ START = 1,
17
+ MOVE = 2
18
+ }
19
+
20
+ /**
21
+ * A {Snapper} for reflowable resources using a column-based layout
22
+ */
23
+ export class ColumnSnapper extends Snapper {
24
+ static readonly moduleName: ModuleName = "column_snapper";
25
+ private resizeObserver!: ResizeObserver;
26
+ private mutationObserver!: MutationObserver;
27
+ private wnd!: ReadiumWindow;
28
+ private comms!: Comms;
29
+ private doc() { return this.wnd.document.scrollingElement as HTMLElement; }
30
+ private scrollOffset() {
31
+ // The reason we do this is because when the document is transformed (translate3d),
32
+ // the scrollLeft value is 0 because... reasons. So we have to use the cached value
33
+ // from this.alreadyScrollLeft instead.
34
+ return (this.doc().scrollLeft > 0) ? this.doc().scrollLeft : this.alreadyScrollLeft;
35
+ }
36
+
37
+ snapOffset(offset: number) {
38
+ const value = offset + (isRTL(this.wnd) ? -1 : 1);
39
+ return value - (value % this.wnd.innerWidth);
40
+ }
41
+
42
+ reportProgress() {
43
+ this.comms.send("progress", this.wnd.scrollX / this.cachedScrollWidth);
44
+ }
45
+
46
+ private shakeTimeout = 0;
47
+ shake() {
48
+ // - If already overscrolling (touchscreen), then shaking on top of it looks ugly
49
+ // - If already shaking, wait until it's finished before allowing another shake
50
+ if(this.overscroll !== 0 || this.shakeTimeout !== 0) return;
51
+ const doc = this.doc();
52
+
53
+ doc.classList.add((isRTL(this.wnd) ? "readium-bounce-l" : "readium-bounce-r"));
54
+ const curScrollLeft = this.scrollOffset();
55
+ this.shakeTimeout = this.wnd.setTimeout(() => {
56
+ doc.classList.remove("readium-bounce-l");
57
+ doc.classList.remove("readium-bounce-r");
58
+ this.shakeTimeout = 0;
59
+ this.doc().scrollLeft = curScrollLeft;
60
+ }, 150);
61
+ }
62
+
63
+ private snappingCancelled = false;
64
+ private alreadyScrollLeft = 0;
65
+ private overscroll = 0;
66
+ private cachedScrollWidth = 0; // We have to cache this because during overscroll (transform, or left) the width is incorrect due to browser
67
+ private takeOverSnap() {
68
+ this.snappingCancelled = true;
69
+ this.clearTouches();
70
+ const doc = this.doc();
71
+
72
+ // translate3d(XXXpx, 0px, 0px) -> slice 12 -> XXXpx, 0px, 0px) -> split "px" [0] -> XXX
73
+ this.overscroll = doc.style.transform?.length > 12 ? parseFloat(doc.style.transform.slice(12).split("px")[0]) : 0;
74
+ }
75
+
76
+ // Snaps the current offset to the page width.
77
+ snapCurrentOffset(smooth=false, noprogress=false) {
78
+ const startX = this.wnd.scrollX > 0 ? this.wnd.scrollX : this.alreadyScrollLeft;
79
+ const doc = this.doc();
80
+ const cdo = this.dragOffset();
81
+ const columnCount = getColumnCountPerScreen(this.wnd);
82
+ const currentOffset = Math.min(Math.max(0, startX), this.cachedScrollWidth);
83
+ const factor = isRTL(this.wnd) ? -1 : 1;
84
+ const hurdle =
85
+ // The hurdle to overcome in order to change pages
86
+ factor *
87
+ (this.wnd.innerWidth / 3) *
88
+ ((factor * cdo) > 0 ? 2 : 1);
89
+
90
+ const so = this.snapOffset(currentOffset + hurdle);
91
+ if(smooth && so !== this.scrollOffset()) { // Smooth snapping
92
+ this.snappingCancelled = false;
93
+ const position = (start: number, end: number, elapsed: number, period: number) => {
94
+ if (elapsed > period) {
95
+ return end;
96
+ }
97
+ return start + (end - start) * easeInOutQuad(elapsed / period);
98
+ }
99
+ const period = /*Math.abs(startX - (this.useTransform ? currentOffset : 0)) < 10 ? 1 : */SNAP_DURATION * columnCount; // TODO revamp
100
+ let startTime: number;
101
+ const step = (timestamp: number) => {
102
+ if(this.snappingCancelled) return;
103
+
104
+ if (!startTime) startTime = timestamp;
105
+ const elapsed = timestamp - startTime;
106
+
107
+ const lpos = position(this.overscroll, 0, elapsed, period);
108
+ const spos = position(startX, so, elapsed, period);
109
+ doc.scrollLeft = spos;
110
+ if(this.overscroll !== 0)
111
+ doc.style.transform = `translate3d(${-lpos}px, 0px, 0px)`;
112
+
113
+ if (elapsed < period)
114
+ this.wnd.requestAnimationFrame(step);
115
+ else {
116
+ this.clearTouches();
117
+ doc.style.removeProperty("transform");
118
+ doc.scrollLeft = so;
119
+ if(!noprogress) this.reportProgress();
120
+ }
121
+ }
122
+ this.wnd.requestAnimationFrame(step);
123
+ } else { // Instant snapping
124
+ doc.style.removeProperty("transform");
125
+ this.wnd.requestAnimationFrame(() => {
126
+ doc.scrollLeft = so;
127
+ this.clearTouches();
128
+ if(!noprogress) this.reportProgress();
129
+ });
130
+ }
131
+ }
132
+
133
+ // Current touch state cycler, to assist with swipe detection etc.
134
+ private touchState: ScrollTouchState = ScrollTouchState.END;
135
+ private startingX: number | undefined = undefined;
136
+ private endingX: number | undefined = undefined;
137
+ private dragOffset() { return (this.startingX ?? 0) - (this.endingX ?? 0); }
138
+ private clearTouches() {
139
+ this.startingX = undefined; this.endingX = undefined;
140
+ this.overscroll = 0;
141
+ // this.doc().style.removeProperty("will-change");
142
+ }
143
+
144
+ onTouchStart(e: TouchEvent) {
145
+ e.stopPropagation();
146
+ this.takeOverSnap();
147
+ switch (e.touches.length) {
148
+ case 1: // Single finger - handle it
149
+ break;
150
+ case 2: // Pinch - abort
151
+ this.onTouchEnd(e);
152
+ return;
153
+ default: { // More fingers - abort, notify
154
+ this.onTouchEnd(e);
155
+ this.comms.send("tap_more", e.touches.length);
156
+ return;
157
+ }
158
+ }
159
+
160
+ // this.doc().style.willChange = "transform, scroll-position";
161
+ this.startingX = e.touches[0].clientX;
162
+ this.alreadyScrollLeft = this.doc().scrollLeft;
163
+ this.touchState = ScrollTouchState.START;
164
+ }
165
+ private readonly onTouchStarter = this.onTouchStart.bind(this);
166
+
167
+ onTouchEnd(_: TouchEvent) {
168
+ if(this.touchState === ScrollTouchState.MOVE) {
169
+ // Get the horizontal drag distance
170
+ const dragOffset = this.dragOffset();
171
+ const scrollOffset = this.scrollOffset();
172
+ // this.cachedScrollWidth = this.doc().scrollWidth!;
173
+ if(this.cachedScrollWidth <= this.wnd.innerWidth) {
174
+ // Only a single page, meaning any swipe triggers next/prev
175
+ this.reportProgress();
176
+ if(dragOffset > 5) this.comms.send("no_more", undefined);
177
+ if(dragOffset < -5) this.comms.send("no_less", undefined);
178
+ } else if(scrollOffset < 5 && dragOffset < 5) {
179
+ this.alreadyScrollLeft = 0;
180
+ this.comms.send("no_less", undefined);
181
+ } else if((this.cachedScrollWidth - scrollOffset - this.wnd.innerWidth) < 5 && dragOffset > 5) {
182
+ this.alreadyScrollLeft = this.cachedScrollWidth;
183
+ this.comms.send("no_more", undefined);
184
+ }
185
+
186
+ this.snapCurrentOffset(true);
187
+ this.comms.send("swipe", dragOffset);
188
+ }
189
+ this.touchState = ScrollTouchState.END;
190
+ }
191
+ private readonly onTouchEnder = this.onTouchEnd.bind(this);
192
+
193
+ private onWidthChange() {
194
+ this.cachedScrollWidth = this.doc().scrollWidth!;
195
+ if(this.comms.ready)
196
+ // This function can be called while the frame is still hidden
197
+ // so it should only be snapped if it's actually active because
198
+ // it sends a comms message to update progress.
199
+ this.snapCurrentOffset();
200
+ }
201
+ private readonly onWidthChanger = this.onWidthChange.bind(this);
202
+
203
+ onTouchMove(e: TouchEvent) {
204
+ if(this.touchState === ScrollTouchState.END) return;
205
+ if(this.touchState === ScrollTouchState.START) {
206
+ this.touchState = ScrollTouchState.MOVE;
207
+ deselect(this.wnd);
208
+ }
209
+ this.endingX = e.touches[0].clientX;
210
+
211
+ const dro = this.dragOffset();
212
+ const newpos = this.alreadyScrollLeft + dro;
213
+ if(newpos < 0) {
214
+ this.overscroll = newpos;
215
+ this.doc().style.transform = `translate3d(${-this.overscroll}px, 0px, 0px)`;
216
+ } else if((newpos + this.wnd.innerWidth) > this.cachedScrollWidth) {
217
+ this.overscroll = newpos;
218
+ this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
219
+ } else {
220
+ this.overscroll = 0;
221
+ this.doc().style.removeProperty("transform");
222
+ this.doc().scrollLeft = this.alreadyScrollLeft + dro;
223
+ }
224
+ }
225
+ private readonly onTouchMover = this.onTouchMove.bind(this);
226
+
227
+ mount(wnd: ReadiumWindow, comms: Comms): boolean {
228
+ this.wnd = wnd;
229
+ this.comms = comms;
230
+ if(!super.mount(wnd, comms)) return false;
231
+
232
+ // Add styling to hide the scrollbar
233
+ const d = wnd.document.createElement("style");
234
+ d.dataset.readium = "true";
235
+ d.id = COLUMN_SNAPPER_STYLE_ID;
236
+ d.textContent = `
237
+ @keyframes readium-bounce-l-animation {
238
+ 0%, 100% {transform: translate3d(0, 0, 0);}
239
+ 50% {transform: translate3d(-50px, 0, 0);}
240
+ }
241
+
242
+ @keyframes readium-bounce-r-animation {
243
+ 0%, 100% {transform: translate3d(0, 0, 0);}
244
+ 50% {transform: translate3d(50px, 0, 0);}
245
+ }
246
+
247
+ .readium-bounce-l {
248
+ animation: readium-bounce-l-animation 150ms ease-out 1;
249
+ }
250
+
251
+ .readium-bounce-r {
252
+ animation: readium-bounce-r-animation 150ms ease-out 1;
253
+ }
254
+
255
+ html {
256
+ overflow: hidden;
257
+ }
258
+
259
+ body {
260
+ -ms-overflow-style: none; /* for Internet Explorer, Edge */
261
+ }
262
+
263
+ * {
264
+ scrollbar-width: none; /* for Firefox */
265
+ }
266
+
267
+ body::-webkit-scrollbar {
268
+ display: none; /* for Chrome, Safari, and Opera */
269
+ }
270
+ `;
271
+ wnd.document.head.appendChild(d);
272
+
273
+ // Necessary for iOS 13 and below
274
+ const ResizeObserver = (wnd as ReadiumWindow & typeof globalThis).ResizeObserver || Polyfill;
275
+
276
+ this.resizeObserver = new ResizeObserver(() => wnd.requestAnimationFrame(() => {
277
+ wnd && appendVirtualColumnIfNeeded(wnd);
278
+ }));
279
+ this.resizeObserver.observe(wnd.document.body);
280
+ this.mutationObserver = new MutationObserver(() => {
281
+ this.wnd.requestAnimationFrame(() => this.cachedScrollWidth = this.doc().scrollWidth!);
282
+ });
283
+ wnd.frameElement && this.mutationObserver.observe(wnd.frameElement, {attributes: true, attributeFilter: ["style"]});
284
+ this.mutationObserver.observe(wnd.document, {attributes: true, attributeFilter: ["style"]});
285
+
286
+ const scrollToOffset = (offset: number): boolean => {
287
+ const oldScrollLeft = this.doc().scrollLeft;
288
+ this.doc().scrollLeft = this.snapOffset(offset); // TODO assert if never undefined (same for rest of !)
289
+
290
+ return oldScrollLeft !== this.doc().scrollLeft;
291
+ }
292
+
293
+ wnd.addEventListener("orientationchange", this.onWidthChanger);
294
+ wnd.addEventListener("resize", this.onWidthChanger);
295
+ wnd.requestAnimationFrame(() => this.cachedScrollWidth = this.doc().scrollWidth!);
296
+
297
+ comms.register("go_progression", ColumnSnapper.moduleName, (data, ack) => {
298
+ const position = data as number;
299
+ if (position < 0 || position > 1) {
300
+ comms.send("error", {
301
+ message: "go_progression must be given a position from 0.0 to 1.0"
302
+ });
303
+ ack(false);
304
+ return;
305
+ }
306
+ this.wnd.requestAnimationFrame(() => {
307
+ this.cachedScrollWidth = this.doc().scrollWidth!;
308
+ const documentWidth = this.cachedScrollWidth;
309
+ const factor = isRTL(wnd) ? -1 : 1;
310
+ const offset = documentWidth * position * factor;
311
+ this.doc().scrollLeft = this.snapOffset(offset);
312
+ this.reportProgress();
313
+ deselect(this.wnd);
314
+ ack(true);
315
+ });
316
+ })
317
+
318
+ comms.register("go_id", ColumnSnapper.moduleName, (data, ack) => {
319
+ const element = wnd.document.getElementById(data as string);
320
+ if(!element) {
321
+ ack(false);
322
+ return;
323
+ }
324
+ this.wnd.requestAnimationFrame(() => {
325
+ this.doc().scrollLeft = this.snapOffset(element.getBoundingClientRect().left + wnd.scrollX);
326
+ this.reportProgress();
327
+ deselect(this.wnd);
328
+ ack(true);
329
+ });
330
+ });
331
+
332
+ comms.register("go_text", ColumnSnapper.moduleName, (data: unknown | unknown[], ack) => {
333
+ let cssSelector = undefined;
334
+ if(Array.isArray(data)) {
335
+ if(data.length > 1)
336
+ // Second element is presumed to be the CSS selector
337
+ cssSelector = (data as unknown[])[1] as string;
338
+ data = data[0]; // First element will always be the locator text object
339
+ }
340
+ const text = LocatorText.deserialize(data);
341
+ const r = rangeFromLocator(this.wnd.document, new Locator({
342
+ href: wnd.location.href,
343
+ type: "text/html",
344
+ text,
345
+ locations: cssSelector ? new LocatorLocations({
346
+ otherLocations: new Map([
347
+ ["cssSelector", cssSelector]
348
+ ])
349
+ }) : undefined
350
+ }));
351
+ if(!r) {
352
+ ack(false);
353
+ return;
354
+ }
355
+ this.wnd.requestAnimationFrame(() => {
356
+ this.doc().scrollLeft = this.snapOffset(r.getBoundingClientRect().left + wnd.scrollX);
357
+ this.reportProgress();
358
+ deselect(this.wnd);
359
+ ack(true);
360
+ });
361
+ });
362
+
363
+ comms.register("go_end", ColumnSnapper.moduleName, (_, ack) => {
364
+ const factor = isRTL(wnd) ? -1 : 1;
365
+ this.wnd.requestAnimationFrame(() => {
366
+ this.cachedScrollWidth = this.doc().scrollWidth!;
367
+ const final = this.cachedScrollWidth * factor;
368
+ if(this.doc().scrollLeft === final) return ack(false);
369
+ this.doc().scrollLeft = this.snapOffset(final);
370
+ this.reportProgress();
371
+ deselect(this.wnd);
372
+ ack(true);
373
+ });
374
+ })
375
+
376
+ comms.register("go_start", ColumnSnapper.moduleName, (_, ack) => {
377
+ this.wnd.requestAnimationFrame(() => {
378
+ if(this.doc().scrollLeft === 0) return ack(false);
379
+ this.doc().scrollLeft = 0;
380
+ this.reportProgress();
381
+ deselect(this.wnd);
382
+ ack(true);
383
+ });
384
+ })
385
+
386
+ comms.register("go_prev", ColumnSnapper.moduleName, (_, ack) => {
387
+ this.wnd.requestAnimationFrame(() => {
388
+ const offset = wnd.scrollX - wnd.innerWidth;
389
+ const minOffset = isRTL(wnd) ? - (this.cachedScrollWidth - wnd.innerWidth) : 0;
390
+ const change = scrollToOffset(Math.max(offset, minOffset));
391
+ if(change) {
392
+ this.reportProgress();
393
+ deselect(this.wnd);
394
+ }
395
+ ack(change);
396
+ });
397
+ });
398
+
399
+ comms.register("go_next", ColumnSnapper.moduleName, (_, ack) => {
400
+ this.wnd.requestAnimationFrame(() => {
401
+ const offset = wnd.scrollX + wnd.innerWidth;
402
+ const maxOffset = isRTL(wnd) ? 0 : this.cachedScrollWidth - wnd.innerWidth;
403
+ const change = scrollToOffset(Math.min(offset, maxOffset));
404
+ if(change) {
405
+ this.reportProgress();
406
+ deselect(this.wnd);
407
+ }
408
+ ack(change);
409
+ });
410
+ });
411
+
412
+ comms.register("unfocus", ColumnSnapper.moduleName, (_, ack) => {
413
+ this.snappingCancelled = true;
414
+ deselect(this.wnd);
415
+ ack(true);
416
+ });
417
+
418
+ comms.register("shake", ColumnSnapper.moduleName, (_, ack) => {
419
+ this.shake();
420
+ ack(true);
421
+ });
422
+
423
+ comms.register("focus", ColumnSnapper.moduleName, (_, ack) => {
424
+ this.wnd.requestAnimationFrame(() => {
425
+ this.cachedScrollWidth = this.doc().scrollWidth!;
426
+ this.snapCurrentOffset(false, true);
427
+ this.reportProgress();
428
+ ack(true);
429
+ });
430
+ });
431
+
432
+ comms.register("first_visible_locator", ColumnSnapper.moduleName, (_, ack) => {
433
+ const locator = findFirstVisibleLocator(wnd as ReadiumWindow, false);
434
+ this.comms.send("first_visible_locator", locator.serialize());
435
+ ack(true);
436
+ });
437
+
438
+ // Add interaction listeners
439
+ wnd.addEventListener("touchstart", this.onTouchStarter, { passive: true });
440
+ wnd.addEventListener("touchend", this.onTouchEnder, { passive: true });
441
+ wnd.addEventListener("touchmove", this.onTouchMover, { passive: true });
442
+
443
+ // Safari hack, otherwise other events won't register
444
+ wnd.document.addEventListener('touchstart', () => {});
445
+
446
+ comms.log("ColumnSnapper Mounted");
447
+ return true;
448
+ }
449
+
450
+ unmount(wnd: ReadiumWindow, comms: Comms): boolean {
451
+ this.snappingCancelled = true;
452
+ comms.unregisterAll(ColumnSnapper.moduleName);
453
+ this.resizeObserver.disconnect();
454
+ this.mutationObserver.disconnect();
455
+
456
+ wnd.removeEventListener("touchstart", this.onTouchStarter);
457
+ wnd.removeEventListener("touchend", this.onTouchEnder);
458
+ wnd.removeEventListener("touchmove", this.onTouchMover);
459
+
460
+ wnd.removeEventListener("orientationchange", this.onWidthChanger);
461
+ wnd.removeEventListener("resize", this.onWidthChanger);
462
+
463
+ wnd.document.getElementById(COLUMN_SNAPPER_STYLE_ID)?.remove();
464
+
465
+ comms.log("ColumnSnapper Unmounted");
466
+ return super.unmount(wnd, comms);
467
+ }
468
+ }
@@ -0,0 +1,168 @@
1
+ import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
2
+ import { Comms } from "../../comms";
3
+ import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
4
+ import { AnchorObserver, helperCreateAnchorElements, helperRemoveAnchorElements } from '../../helpers/scrollSnapperHelper';
5
+ import { ModuleName } from "../ModuleLibrary";
6
+ import { Snapper } from "./Snapper";
7
+ import { rangeFromLocator } from "../../helpers/locator";
8
+
9
+ const SCROLL_SNAPPER_STYLE_ID = "readium-scroll-snapper-style";
10
+
11
+ export class ScrollSnapper extends Snapper {
12
+ static readonly moduleName: ModuleName = "scroll_snapper";
13
+ private wnd!: ReadiumWindow;
14
+ private comms!: Comms;
15
+
16
+ private doc() {
17
+ return this.wnd.document.scrollingElement as HTMLElement;
18
+ }
19
+
20
+ private createAnchorElements = () => {
21
+ helperCreateAnchorElements(this.doc());
22
+ }
23
+
24
+ private removeAnchorElements = () => {
25
+ helperRemoveAnchorElements(this.doc());
26
+ }
27
+
28
+ private createCustomElement = () => {
29
+ customElements.get("anchor-observer") ||
30
+ customElements.define("anchor-observer", AnchorObserver);
31
+ }
32
+
33
+ private reportProgress(progress: number) {
34
+ this.comms.send("progress", progress);
35
+ }
36
+
37
+ mount(wnd: ReadiumWindow, comms: Comms): boolean {
38
+ this.wnd = wnd;
39
+ this.comms = comms;
40
+
41
+ // Add styling to hide the scrollbar
42
+ const style = wnd.document.createElement("style");
43
+ style.dataset.readium = "true";
44
+ style.id = SCROLL_SNAPPER_STYLE_ID;
45
+ style.textContent = `
46
+ * {
47
+ scrollbar-width: none; /* for Firefox */
48
+ }
49
+
50
+ body::-webkit-scrollbar {
51
+ display: none; /* for Chrome, Safari, and Opera */
52
+ }
53
+ `;
54
+ wnd.document.head.appendChild(style);
55
+
56
+ comms.register("go_progression", ScrollSnapper.moduleName, (data, ack) => {
57
+ const position = data as number;
58
+
59
+ if (position < 0 || position > 1) {
60
+ comms.send("error", {
61
+ message: "go_progression must be given a position from 0.0 to 1.0"
62
+ });
63
+ ack(false);
64
+ return;
65
+ }
66
+
67
+ this.wnd.requestAnimationFrame(() => {
68
+ this.doc().scrollTop = this.doc().offsetHeight * position;
69
+ this.reportProgress(position);
70
+ deselect(this.wnd);
71
+ ack(true);
72
+ });
73
+ });
74
+
75
+ comms.register("go_id", ScrollSnapper.moduleName, (data, ack) => {
76
+ const element = wnd.document.getElementById(data as string);
77
+ if(!element) {
78
+ ack(false);
79
+ return;
80
+ }
81
+ this.wnd.requestAnimationFrame(() => {
82
+ this.doc().scrollTop = element.getBoundingClientRect().top + wnd.scrollY - wnd.innerHeight / 2;
83
+ this.reportProgress(this.doc().scrollTop / this.doc().offsetHeight);
84
+ deselect(this.wnd);
85
+ ack(true);
86
+ });
87
+ });
88
+
89
+ comms.register("go_text", ScrollSnapper.moduleName, (data, ack) => {
90
+ let cssSelector = undefined;
91
+ if(Array.isArray(data)) {
92
+ if(data.length > 1)
93
+ // Second element is presumed to be the CSS selector
94
+ cssSelector = (data as unknown[])[1] as string;
95
+ data = data[0]; // First element will always be the locator text object
96
+ }
97
+ const text = LocatorText.deserialize(data);
98
+ const r = rangeFromLocator(this.wnd.document, new Locator({
99
+ href: wnd.location.href,
100
+ type: "text/html",
101
+ text,
102
+ locations: cssSelector ? new LocatorLocations({
103
+ otherLocations: new Map([
104
+ ["cssSelector", cssSelector]
105
+ ])
106
+ }) : undefined
107
+ }));
108
+ if(!r) {
109
+ ack(false);
110
+ return;
111
+ }
112
+ this.wnd.requestAnimationFrame(() => {
113
+ this.doc().scrollTop = r.getBoundingClientRect().top + wnd.scrollY - wnd.innerHeight / 2;
114
+ this.reportProgress(this.doc().scrollTop / this.doc().offsetHeight);
115
+ deselect(this.wnd);
116
+ ack(true);
117
+ });
118
+ });
119
+
120
+ comms.register("go_start", ScrollSnapper.moduleName, (_, ack) => {
121
+ if (this.doc().scrollTop === 0) return ack(false);
122
+ this.doc().scrollTop = 0;
123
+ this.reportProgress(0);
124
+ ack(true);
125
+ });
126
+
127
+ comms.register("go_end", ScrollSnapper.moduleName, (_, ack) => {
128
+ if (this.doc().scrollTop === 0) return ack(false);
129
+ this.doc().scrollTop = 0;
130
+ this.reportProgress(0);
131
+ ack(true);
132
+ })
133
+
134
+ comms.register("unfocus", ScrollSnapper.moduleName, (_, ack) => {
135
+ deselect(this.wnd);
136
+ ack(true);
137
+ });
138
+
139
+ comms.register([
140
+ "go_next",
141
+ "go_prev",
142
+ ], ScrollSnapper.moduleName, (_, ack) => ack(false));
143
+
144
+ comms.register("focus", ScrollSnapper.moduleName, (_, ack) => {
145
+ this.reportProgress(this.doc().scrollTop / this.doc().offsetHeight);
146
+ ack(true);
147
+ });
148
+
149
+ comms.register("first_visible_locator", ScrollSnapper.moduleName, (_, ack) => {
150
+ const locator = findFirstVisibleLocator(wnd as ReadiumWindow, true);
151
+ this.comms.send("first_visible_locator", locator.serialize());
152
+ ack(true);
153
+ });
154
+
155
+ comms.log("ScrollSnapper Mounted");
156
+ this.createCustomElement();
157
+ this.createAnchorElements();
158
+ return true;
159
+ }
160
+
161
+ unmount(wnd: ReadiumWindow, comms: Comms): boolean {
162
+ comms.unregisterAll(ScrollSnapper.moduleName);
163
+ this.removeAnchorElements();
164
+ wnd.document.getElementById(SCROLL_SNAPPER_STYLE_ID)?.remove();
165
+ comms.log("ScrollSnapper Unmounted");
166
+ return true;
167
+ }
168
+ }
@@ -0,0 +1,49 @@
1
+ import { Comms } from "../../comms";
2
+ import { ReadiumWindow } from "../../helpers/dom";
3
+ import { Module } from "../Module";
4
+ import { ModuleName } from "../ModuleLibrary";
5
+
6
+ const SNAPPER_STYLE_ID = "readium-snapper-style";
7
+
8
+ export abstract class Snapper extends Module {
9
+ static readonly moduleName: ModuleName = "snapper";
10
+
11
+ private protected = false;
12
+
13
+ buildStyles() {
14
+ return `
15
+ html, body {
16
+ touch-action: manipulation;
17
+ user-select: ${this.protected ? "none" : "auto"};
18
+ }`;
19
+ }
20
+
21
+ mount(wnd: ReadiumWindow, comms: Comms): boolean {
22
+ const d = wnd.document.createElement("style");
23
+ d.dataset.readium = "true";
24
+ d.id = SNAPPER_STYLE_ID;
25
+ d.textContent = this.buildStyles();
26
+ wnd.document.head.appendChild(d);
27
+
28
+ comms.register("protect", Snapper.moduleName, (_, ack) => {
29
+ this.protected = true;
30
+ d.textContent = this.buildStyles();
31
+ ack(true);
32
+ });
33
+ comms.register("unprotect", Snapper.moduleName, (_, ack) => {
34
+ this.protected = false;
35
+ d.textContent = this.buildStyles();
36
+ ack(true);
37
+ });
38
+
39
+ comms.log("Snapper Mounted");
40
+ return true;
41
+ }
42
+
43
+ unmount(wnd: ReadiumWindow, comms: Comms): boolean {
44
+ wnd.document.getElementById(SNAPPER_STYLE_ID)?.remove();
45
+
46
+ comms.log("Snapper Unmounted");
47
+ return true;
48
+ }
49
+ }