@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.
- package/README.MD +13 -0
- package/dist/index.js +3144 -0
- package/dist/index.umd.cjs +69 -0
- package/package.json +58 -0
- package/src/Loader.ts +85 -0
- package/src/comms/comms.ts +168 -0
- package/src/comms/index.ts +3 -0
- package/src/comms/keys.ts +42 -0
- package/src/comms/mid.ts +5 -0
- package/src/helpers/animation.ts +5 -0
- package/src/helpers/css.ts +16 -0
- package/src/helpers/document.ts +43 -0
- package/src/helpers/dom.ts +126 -0
- package/src/helpers/locator.ts +67 -0
- package/src/helpers/rect.ts +301 -0
- package/src/helpers/scrollSnapperHelper.ts +43 -0
- package/src/index.ts +3 -0
- package/src/modules/Decorator.ts +462 -0
- package/src/modules/Module.ts +23 -0
- package/src/modules/ModuleLibrary.ts +44 -0
- package/src/modules/Peripherals.ts +148 -0
- package/src/modules/index.ts +3 -0
- package/src/modules/setup/FixedSetup.ts +73 -0
- package/src/modules/setup/ReflowableSetup.ts +65 -0
- package/src/modules/setup/Setup.ts +131 -0
- package/src/modules/snapper/ColumnSnapper.ts +468 -0
- package/src/modules/snapper/ScrollSnapper.ts +168 -0
- package/src/modules/snapper/Snapper.ts +49 -0
- package/src/vendor/approx-string-match/LICENSE +21 -0
- package/src/vendor/approx-string-match/README.MD +1 -0
- package/src/vendor/approx-string-match/index.ts +362 -0
- package/src/vendor/hypothesis/README.MD +1 -0
- package/src/vendor/hypothesis/anchoring/api-types.ts +309 -0
- package/src/vendor/hypothesis/anchoring/html.ts +134 -0
- package/src/vendor/hypothesis/anchoring/match-quote.ts +163 -0
- package/src/vendor/hypothesis/anchoring/placeholder.ts +59 -0
- package/src/vendor/hypothesis/anchoring/text-range.ts +327 -0
- package/src/vendor/hypothesis/anchoring/trim-range.ts +220 -0
- package/src/vendor/hypothesis/anchoring/types.ts +377 -0
- package/src/vendor/hypothesis/anchoring/xpath.ts +164 -0
- package/src/vendor/hypothesis/tsconfig.json +33 -0
- package/src/vendor/hypothesis/types/shared.ts +40 -0
- package/types/src/Loader.d.ts +33 -0
- package/types/src/comms/comms.d.ts +40 -0
- package/types/src/comms/index.d.ts +3 -0
- package/types/src/comms/keys.d.ts +2 -0
- package/types/src/comms/mid.d.ts +1 -0
- package/types/src/helpers/animation.d.ts +1 -0
- package/types/src/helpers/css.d.ts +4 -0
- package/types/src/helpers/document.d.ts +8 -0
- package/types/src/helpers/dom.d.ts +15 -0
- package/types/src/helpers/locator.d.ts +2 -0
- package/types/src/helpers/rect.d.ts +10 -0
- package/types/src/helpers/scrollSnapperHelper.d.ts +5 -0
- package/types/src/index.d.ts +3 -0
- package/types/src/modules/Decorator.d.ts +43 -0
- package/types/src/modules/Module.d.ts +11 -0
- package/types/src/modules/ModuleLibrary.d.ts +5 -0
- package/types/src/modules/Peripherals.d.ts +39 -0
- package/types/src/modules/ReflowablePeripherals.d.ts +37 -0
- package/types/src/modules/index.d.ts +3 -0
- package/types/src/modules/setup/FixedSetup.d.ts +9 -0
- package/types/src/modules/setup/ReflowableSetup.d.ts +9 -0
- package/types/src/modules/setup/Setup.d.ts +17 -0
- package/types/src/modules/snapper/ColumnSnapper.d.ts +41 -0
- package/types/src/modules/snapper/ScrollSnapper.d.ts +16 -0
- package/types/src/modules/snapper/Snapper.d.ts +11 -0
- package/types/src/vendor/approx-string-match/index.d.ts +54 -0
- package/types/src/vendor/hypothesis/anchoring/api-types.d.ts +266 -0
- package/types/src/vendor/hypothesis/anchoring/html.d.ts +17 -0
- package/types/src/vendor/hypothesis/anchoring/match-quote.d.ts +30 -0
- package/types/src/vendor/hypothesis/anchoring/placeholder.d.ts +32 -0
- package/types/src/vendor/hypothesis/anchoring/text-range.d.ts +103 -0
- package/types/src/vendor/hypothesis/anchoring/trim-range.d.ts +17 -0
- package/types/src/vendor/hypothesis/anchoring/types.d.ts +102 -0
- package/types/src/vendor/hypothesis/anchoring/xpath.d.ts +15 -0
- 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
|
+
}
|