@readium/navigator-html-injectables 2.3.1-alpha.2 → 2.4.0-beta.1

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 (60) hide show
  1. package/dist/index.js +852 -701
  2. package/dist/index.umd.cjs +24 -13
  3. package/package.json +8 -8
  4. package/src/Loader.ts +3 -3
  5. package/src/comms/comms.ts +5 -5
  6. package/src/comms/index.ts +3 -3
  7. package/src/helpers/css.ts +2 -4
  8. package/src/helpers/document.ts +13 -10
  9. package/src/helpers/locator.ts +2 -2
  10. package/src/index.ts +4 -4
  11. package/src/modules/Decorator.ts +18 -18
  12. package/src/modules/Module.ts +4 -4
  13. package/src/modules/ModuleLibrary.ts +15 -11
  14. package/src/modules/Peripherals.ts +70 -61
  15. package/src/modules/index.ts +4 -4
  16. package/src/modules/setup/FixedSetup.ts +6 -6
  17. package/src/modules/setup/ReflowableSetup.ts +6 -6
  18. package/src/modules/setup/Setup.ts +5 -5
  19. package/src/modules/setup/WebPubSetup.ts +5 -5
  20. package/src/modules/snapper/CJKVerticalSnapper.ts +322 -0
  21. package/src/modules/snapper/ColumnSnapper.ts +161 -99
  22. package/src/modules/snapper/ScrollSnapper.ts +18 -18
  23. package/src/modules/snapper/Snapper.ts +5 -5
  24. package/src/modules/snapper/WebPubSnapper.ts +15 -15
  25. package/src/protection/BulkCopyProtector.ts +15 -15
  26. package/src/protection/KeyCombinationManager.ts +8 -8
  27. package/src/protection/PatternAnalyzer.ts +18 -18
  28. package/src/protection/PrintProtector.ts +12 -12
  29. package/src/protection/SelectionAnalyzer.ts +1 -1
  30. package/src/protection/config.ts +4 -4
  31. package/src/protection/index.ts +6 -6
  32. package/src/vendor/hypothesis/anchoring/html.ts +1 -1
  33. package/src/vendor/hypothesis/anchoring/types.ts +1 -1
  34. package/types/src/Loader.d.ts +2 -2
  35. package/types/src/comms/comms.d.ts +2 -2
  36. package/types/src/comms/index.d.ts +3 -3
  37. package/types/src/helpers/css.d.ts +1 -2
  38. package/types/src/helpers/document.d.ts +1 -1
  39. package/types/src/index.d.ts +4 -4
  40. package/types/src/modules/Decorator.d.ts +4 -4
  41. package/types/src/modules/Module.d.ts +3 -3
  42. package/types/src/modules/ModuleLibrary.d.ts +2 -2
  43. package/types/src/modules/Peripherals.d.ts +10 -9
  44. package/types/src/modules/index.d.ts +4 -4
  45. package/types/src/modules/setup/FixedSetup.d.ts +4 -4
  46. package/types/src/modules/setup/ReflowableSetup.d.ts +3 -3
  47. package/types/src/modules/setup/Setup.d.ts +4 -4
  48. package/types/src/modules/setup/WebPubSetup.d.ts +3 -3
  49. package/types/src/modules/snapper/CJKVerticalSnapper.d.ts +57 -0
  50. package/types/src/modules/snapper/ColumnSnapper.d.ts +16 -5
  51. package/types/src/modules/snapper/ScrollSnapper.d.ts +5 -5
  52. package/types/src/modules/snapper/Snapper.d.ts +4 -4
  53. package/types/src/modules/snapper/WebPubSnapper.d.ts +5 -5
  54. package/types/src/protection/BulkCopyProtector.d.ts +1 -1
  55. package/types/src/protection/KeyCombinationManager.d.ts +3 -3
  56. package/types/src/protection/PrintProtector.d.ts +4 -4
  57. package/types/src/protection/config.d.ts +3 -3
  58. package/types/src/protection/index.d.ts +6 -6
  59. package/types/src/vendor/hypothesis/anchoring/html.d.ts +1 -1
  60. package/types/src/vendor/hypothesis/anchoring/types.d.ts +1 -1
@@ -1,14 +1,13 @@
1
- import { Comms } from "../../comms";
2
- import { Snapper } from "./Snapper";
3
- import { getColumnCountPerScreen, isRTL, appendVirtualColumnIfNeeded } from "../../helpers/document";
4
- import { easeInOutQuad } from "../../helpers/animation";
5
- import { ModuleName } from "../ModuleLibrary";
1
+ import { Comms } from "../../comms/index.ts";
2
+ import { Snapper } from "./Snapper.ts";
3
+ import { getColumnCountPerScreen, isRTL, appendVirtualColumnIfNeeded } from "../../helpers/document.ts";
4
+ import { easeInOutQuad } from "../../helpers/animation.ts";
5
+ import { ModuleName } from "../ModuleLibrary.ts";
6
6
  import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
7
- import { rangeFromLocator } from "../../helpers/locator";
8
- import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
9
- import { PatternAnalyzer } from "../../protection/PatternAnalyzer";
10
- import { BaseSuspiciousActivityEvent } from "../Peripherals";
11
- import { isTypedOMSupported } from "../../helpers/css";
7
+ import { rangeFromLocator } from "../../helpers/locator.ts";
8
+ import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom.ts";
9
+ import { PatternAnalyzer } from "../../protection/PatternAnalyzer.ts";
10
+ import { BaseSuspiciousActivityEvent } from "../Peripherals.ts";
12
11
 
13
12
  const COLUMN_SNAPPER_STYLE_ID = "readium-column-snapper-style";
14
13
 
@@ -42,7 +41,9 @@ export class ColumnSnapper extends Snapper {
42
41
  // The reason we do this is because when the document is transformed (translate3d),
43
42
  // the scrollLeft value is 0 because... reasons. So we have to use the cached value
44
43
  // from this.alreadyScrollLeft instead.
45
- return (this.doc().scrollLeft > 0) ? this.doc().scrollLeft : this.alreadyScrollLeft;
44
+ // Note: for RTL, scrollLeft is negative, so we check !== 0, not > 0.
45
+ const sl = this.doc().scrollLeft;
46
+ return (sl !== 0) ? sl : this.alreadyScrollLeft;
46
47
  }
47
48
 
48
49
  snapOffset(offset: number) {
@@ -50,13 +51,35 @@ export class ColumnSnapper extends Snapper {
50
51
  return value - (value % this.wnd.innerWidth);
51
52
  }
52
53
 
54
+ /**
55
+ * Snap a non-negative normalized offset (distance from document start) to
56
+ * the nearest page boundary. Works identically regardless of reading direction.
57
+ */
58
+ private snapNormOffset(offset: number): number {
59
+ const value = offset + 1;
60
+ return value - (value % this.wnd.innerWidth);
61
+ }
62
+
63
+ /**
64
+ * Normalized scroll position: distance from document start, always in
65
+ * [0, scrollWidth - innerWidth] regardless of reading direction.
66
+ * scrollLeft may be negative depending on the browser — Math.abs() normalizes it.
67
+ */
68
+ private normScroll(): number {
69
+ const rtl = isRTL(this.wnd);
70
+ const raw = this.doc().scrollLeft || this.alreadyScrollLeft;
71
+ return rtl ? Math.abs(raw) : Math.max(0, this.wnd.scrollX > 0 ? this.wnd.scrollX : raw);
72
+ }
73
+
53
74
  reportProgress() {
54
- // Contrary to ScrollTop, Android slightly adds to scrollX
55
- // So we do not need to round it up
56
- const scrollX = this.wnd.scrollX;
57
75
  const scrollWidth = this.cachedScrollWidth;
58
- const progress = Math.max(0, Math.min(1, scrollX / scrollWidth));
59
- const viewportEnd = Math.max(0, Math.min(1, (scrollX + this.wnd.innerWidth) / scrollWidth));
76
+ const viewportWidth = this.wnd.innerWidth;
77
+ const norm = this.normScroll();
78
+ const scrollable = Math.max(1, scrollWidth - viewportWidth);
79
+
80
+ // progress 0 = document start, 1 = document end — same regardless of direction.
81
+ const progress = Math.max(0, Math.min(1, norm / scrollable));
82
+ const viewportEnd = Math.max(0, Math.min(1, (norm + viewportWidth) / scrollWidth));
60
83
  this.comms.send("progress", {
61
84
  start: progress,
62
85
  end: viewportEnd
@@ -70,11 +93,17 @@ export class ColumnSnapper extends Snapper {
70
93
  if(this.overscroll !== 0 || this.shakeTimeout !== 0) return;
71
94
  const doc = this.doc();
72
95
 
73
- doc.classList.add((isRTL(this.wnd) ? "readium-bounce-l" : "readium-bounce-r"));
96
+ // Bounce toward the boundary that was hit.
97
+ // LTR: end is on the right → bounce-r. RTL: start is on the right (norm≈0),
98
+ // end is on the left → bounce direction flips based on which boundary we're at.
99
+ const atStart = this.normScroll() < 5;
100
+ const bounceClass = isRTL(this.wnd)
101
+ ? (atStart ? "readium-bounce-r" : "readium-bounce-l")
102
+ : "readium-bounce-r";
103
+ doc.classList.add(bounceClass);
74
104
  const curScrollLeft = this.scrollOffset();
75
105
  this.shakeTimeout = this.wnd.setTimeout(() => {
76
- doc.classList.remove("readium-bounce-l");
77
- doc.classList.remove("readium-bounce-r");
106
+ doc.classList.remove(bounceClass);
78
107
  this.shakeTimeout = 0;
79
108
  this.doc().scrollLeft = curScrollLeft;
80
109
  }, 150);
@@ -84,7 +113,7 @@ export class ColumnSnapper extends Snapper {
84
113
  private alreadyScrollLeft = 0;
85
114
  private overscroll = 0;
86
115
  private cachedScrollWidth = 0; // We have to cache this because during overscroll (transform, or left) the width is incorrect due to browser
87
-
116
+
88
117
  private takeOverSnap() {
89
118
  this.snappingCancelled = true;
90
119
  this.clearTouches();
@@ -93,36 +122,40 @@ export class ColumnSnapper extends Snapper {
93
122
  // translate3d(XXXpx, 0px, 0px) -> slice 12 -> XXXpx, 0px, 0px) -> split "px" [0] -> XXX
94
123
  this.overscroll = doc.style.transform?.length > 12 ? parseFloat(doc.style.transform.slice(12).split("px")[0]) : 0;
95
124
  }
96
-
125
+
97
126
  // Snaps the current offset to the page width.
98
127
  snapCurrentOffset(smooth=false, noprogress=false) {
99
- const startX = this.wnd.scrollX > 0 ? this.wnd.scrollX : this.alreadyScrollLeft;
128
+ const rtl = isRTL(this.wnd);
100
129
  const doc = this.doc();
101
- const cdo = this.dragOffset();
102
130
  const columnCount = getColumnCountPerScreen(this.wnd);
103
- const currentOffset = Math.min(Math.max(0, startX), this.cachedScrollWidth);
104
- const factor = isRTL(this.wnd) ? -1 : 1;
105
- const hurdle =
106
- // The hurdle to overcome in order to change pages
107
- factor *
108
- (this.wnd.innerWidth / 3) *
109
- ((factor * cdo) > 0 ? 2 : 1);
110
-
111
- const so = this.snapOffset(currentOffset + hurdle);
112
-
113
- const direction = so > this.scrollOffset() ? "right" : "left";
114
-
115
- this.checkSuspiciousSnap(direction, Math.abs(so - this.scrollOffset()));
116
-
117
- if(smooth && so !== this.scrollOffset()) { // Smooth snapping
131
+ const maxScroll = this.cachedScrollWidth - this.wnd.innerWidth;
132
+
133
+ // Work in normalized (non-negative, distance-from-start) coordinates
134
+ // so all arithmetic is direction-agnostic.
135
+ const currentNorm = Math.min(Math.max(0, this.normScroll()), maxScroll);
136
+
137
+ // Forward drag: LTR = drag left (cdo > 0), RTL = drag right (cdo < 0).
138
+ // Normalize so that positive = moving forward in reading order.
139
+ const cdo = this.dragOffset();
140
+ const normalizedCdo = rtl ? -cdo : cdo;
141
+ const hurdle = (this.wnd.innerWidth / 3) * (normalizedCdo > 0 ? 2 : 1);
142
+
143
+ const snappedNorm = Math.min(maxScroll, Math.max(0, this.snapNormOffset(currentNorm + hurdle)));
144
+
145
+ // Convert back to signed scrollLeft for the actual DOM write.
146
+ const so = rtl ? -snappedNorm : snappedNorm;
147
+ const currentScrollLeft = rtl ? -currentNorm : currentNorm;
148
+
149
+ const direction = so > currentScrollLeft ? "right" : "left";
150
+ this.checkSuspiciousSnap(direction, Math.abs(so - currentScrollLeft));
151
+
152
+ if(smooth && so !== currentScrollLeft) { // Smooth snapping
118
153
  this.snappingCancelled = false;
119
154
  const position = (start: number, end: number, elapsed: number, period: number) => {
120
- if (elapsed > period) {
121
- return end;
122
- }
155
+ if (elapsed > period) return end;
123
156
  return start + (end - start) * easeInOutQuad(elapsed / period);
124
- }
125
- const period = /*Math.abs(startX - (this.useTransform ? currentOffset : 0)) < 10 ? 1 : */SNAP_DURATION * columnCount; // TODO revamp
157
+ };
158
+ const period = SNAP_DURATION * columnCount;
126
159
  let startTime: number;
127
160
  const step = (timestamp: number) => {
128
161
  if(this.snappingCancelled) return;
@@ -131,16 +164,10 @@ export class ColumnSnapper extends Snapper {
131
164
  const elapsed = timestamp - startTime;
132
165
 
133
166
  const lpos = position(this.overscroll, 0, elapsed, period);
134
- const spos = position(startX, so, elapsed, period);
167
+ const spos = position(currentScrollLeft, so, elapsed, period);
135
168
  doc.scrollLeft = spos;
136
169
  if(this.overscroll !== 0)
137
- if(isTypedOMSupported()) {
138
- doc.attributeStyleMap.set("transform", new CSSTransformValue([
139
- new CSSTranslate(CSS.px(-lpos), CSS.px(0), CSS.px(0))
140
- ]));
141
- } else {
142
- doc.style.transform = `translate3d(${-lpos}px, 0px, 0px)`;
143
- }
170
+ doc.style.transform = `translate3d(${-lpos}px, 0px, 0px)`;
144
171
 
145
172
  if (elapsed < period)
146
173
  this.wnd.requestAnimationFrame(step);
@@ -150,7 +177,7 @@ export class ColumnSnapper extends Snapper {
150
177
  doc.scrollLeft = so;
151
178
  if(!noprogress) this.reportProgress();
152
179
  }
153
- }
180
+ };
154
181
  this.wnd.requestAnimationFrame(step);
155
182
  } else { // Instant snapping
156
183
  doc.style.removeProperty("transform");
@@ -200,18 +227,25 @@ export class ColumnSnapper extends Snapper {
200
227
  if(this.touchState === ScrollTouchState.MOVE) {
201
228
  // Get the horizontal drag distance
202
229
  const dragOffset = this.dragOffset();
203
- const scrollOffset = this.scrollOffset();
204
- // this.cachedScrollWidth = this.doc().scrollWidth!;
230
+ const rtl = isRTL(this.wnd);
231
+
232
+ // Normalize: distance from document start, forward drag is positive.
233
+ const norm = this.normScroll();
234
+ const maxScroll = this.cachedScrollWidth - this.wnd.innerWidth;
235
+ const normalizedDrag = rtl ? -dragOffset : dragOffset;
236
+
205
237
  if(this.cachedScrollWidth <= this.wnd.innerWidth) {
206
238
  // Only a single page, meaning any swipe triggers next/prev
207
239
  this.reportProgress();
208
- if(dragOffset > 5) this.comms.send("no_more", undefined);
209
- if(dragOffset < -5) this.comms.send("no_less", undefined);
210
- } else if(scrollOffset < 5 && dragOffset < 5) {
240
+ if(normalizedDrag > 5) this.comms.send("no_more", undefined);
241
+ if(normalizedDrag < -5) this.comms.send("no_less", undefined);
242
+ } else if(norm < 5 && normalizedDrag < 5) {
243
+ // At document start, tried to go backward
211
244
  this.alreadyScrollLeft = 0;
212
245
  this.comms.send("no_less", undefined);
213
- } else if((this.cachedScrollWidth - scrollOffset - this.wnd.innerWidth) < 5 && dragOffset > 5) {
214
- this.alreadyScrollLeft = this.cachedScrollWidth;
246
+ } else if((maxScroll - norm) < 5 && normalizedDrag > 5) {
247
+ // At document end, tried to go forward
248
+ this.alreadyScrollLeft = rtl ? -maxScroll : maxScroll;
215
249
  this.comms.send("no_more", undefined);
216
250
  }
217
251
 
@@ -242,28 +276,22 @@ export class ColumnSnapper extends Snapper {
242
276
 
243
277
  const dro = this.dragOffset();
244
278
  const newpos = this.alreadyScrollLeft + dro;
245
- if(newpos < 0) {
279
+
280
+ // For RTL, scrollLeft is in [-(scrollWidth - innerWidth), 0].
281
+ // For LTR, scrollLeft is in [0, scrollWidth - innerWidth].
282
+ const minScrollLeft = isRTL(this.wnd) ? -(this.cachedScrollWidth - this.wnd.innerWidth) : 0;
283
+ const maxScrollLeft = isRTL(this.wnd) ? 0 : (this.cachedScrollWidth - this.wnd.innerWidth);
284
+
285
+ if(newpos < minScrollLeft) {
246
286
  this.overscroll = newpos;
247
- if(isTypedOMSupported()) {
248
- this.doc().attributeStyleMap.set("transform", new CSSTransformValue([
249
- new CSSTranslate(CSS.px(-newpos), CSS.px(0), CSS.px(0))
250
- ]));
251
- } else {
252
- this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
253
- }
254
- } else if((newpos + this.wnd.innerWidth) > this.cachedScrollWidth) {
287
+ this.doc().style.transform = `translate3d(${-this.overscroll}px, 0px, 0px)`;
288
+ } else if(newpos > maxScrollLeft) {
255
289
  this.overscroll = newpos;
256
- if(isTypedOMSupported()) {
257
- this.doc().attributeStyleMap.set("transform", new CSSTransformValue([
258
- new CSSTranslate(CSS.px(-newpos), CSS.px(0), CSS.px(0))
259
- ]));
260
- } else {
261
- this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
262
- }
290
+ this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
263
291
  } else {
264
292
  this.overscroll = 0;
265
293
  this.doc().style.removeProperty("transform");
266
- this.doc().scrollLeft = this.alreadyScrollLeft + dro;
294
+ this.doc().scrollLeft = newpos;
267
295
  }
268
296
  }
269
297
  private readonly onTouchMover = this.onTouchMove.bind(this);
@@ -340,7 +368,7 @@ export class ColumnSnapper extends Snapper {
340
368
  * {
341
369
  scrollbar-width: none; /* for Firefox */
342
370
  }
343
-
371
+
344
372
  body::-webkit-scrollbar {
345
373
  display: none; /* for Chrome, Safari, and Opera */
346
374
  }
@@ -365,7 +393,7 @@ export class ColumnSnapper extends Snapper {
365
393
  const oldValueTransform = oldValue?.match(transformRegex);
366
394
  const newValueTransform = newValue?.match(transformRegex);
367
395
  if (
368
- (!oldValueTransform && !newValueTransform) ||
396
+ (!oldValueTransform && !newValueTransform) ||
369
397
  (oldValueTransform && !newValueTransform) ||
370
398
  (oldValueTransform && newValueTransform && oldValueTransform[1] !== newValueTransform[1])
371
399
  ) {
@@ -384,16 +412,25 @@ export class ColumnSnapper extends Snapper {
384
412
  // For cases the resizeObserver is not able to detect cos body is not resizing despite colCount,
385
413
  // we need to check the syle attribute on the documentElement (ReadiumCSS props)
386
414
  this.mutationObserver.observe(wnd.document.documentElement, {attributes: true, attributeFilter: ["style"]});
387
-
415
+
388
416
  comms.register("scroll_protection", ColumnSnapper.moduleName, (_, ack) => {
389
417
  this.enableSnapProtection();
390
418
  ack(true);
391
419
  });
392
420
 
421
+ // LTR only — snapOffset only works with non-negative values.
393
422
  const scrollToOffset = (offset: number): boolean => {
394
423
  const oldScrollLeft = this.doc().scrollLeft;
395
- this.doc().scrollLeft = this.snapOffset(offset); // TODO assert if never undefined (same for rest of !)
424
+ this.doc().scrollLeft = this.snapOffset(offset);
425
+ return oldScrollLeft !== this.doc().scrollLeft;
426
+ }
396
427
 
428
+ // RTL: offset is normalized (non-negative, 0 = start, scrollable = end).
429
+ // Snaps to page boundary and sets scrollLeft = -snappedNorm.
430
+ const rtlScrollToNorm = (norm: number): boolean => {
431
+ const oldScrollLeft = this.doc().scrollLeft;
432
+ const snapped = this.snapNormOffset(Math.max(0, Math.min(this.cachedScrollWidth - wnd.innerWidth, norm)));
433
+ this.doc().scrollLeft = -snapped;
397
434
  return oldScrollLeft !== this.doc().scrollLeft;
398
435
  }
399
436
 
@@ -412,10 +449,13 @@ export class ColumnSnapper extends Snapper {
412
449
  }
413
450
  this.wnd.requestAnimationFrame(() => {
414
451
  this.cachedScrollWidth = this.doc().scrollWidth!;
415
- const documentWidth = this.cachedScrollWidth;
416
- const factor = isRTL(wnd) ? -1 : 1;
417
- const offset = documentWidth * position * factor;
418
- this.doc().scrollLeft = this.snapOffset(offset);
452
+ const scrollable = this.cachedScrollWidth - wnd.innerWidth;
453
+ const norm = scrollable * position;
454
+ if (isRTL(wnd)) {
455
+ this.doc().scrollLeft = -this.snapNormOffset(norm);
456
+ } else {
457
+ this.doc().scrollLeft = this.snapOffset(norm);
458
+ }
419
459
  this.reportProgress();
420
460
  deselect(this.wnd);
421
461
  ack(true);
@@ -429,7 +469,11 @@ export class ColumnSnapper extends Snapper {
429
469
  return;
430
470
  }
431
471
  this.wnd.requestAnimationFrame(() => {
432
- this.doc().scrollLeft = this.snapOffset(element.getBoundingClientRect().left + wnd.scrollX);
472
+ if (isRTL(wnd)) {
473
+ this.doc().scrollLeft = -this.snapNormOffset(element.getBoundingClientRect().left + wnd.scrollX);
474
+ } else {
475
+ this.doc().scrollLeft = this.snapOffset(element.getBoundingClientRect().left + wnd.scrollX);
476
+ }
433
477
  this.reportProgress();
434
478
  deselect(this.wnd);
435
479
  ack(true);
@@ -460,7 +504,11 @@ export class ColumnSnapper extends Snapper {
460
504
  return;
461
505
  }
462
506
  this.wnd.requestAnimationFrame(() => {
463
- this.doc().scrollLeft = this.snapOffset(r.getBoundingClientRect().left + wnd.scrollX);
507
+ if (isRTL(wnd)) {
508
+ this.doc().scrollLeft = -this.snapNormOffset(r.getBoundingClientRect().left + wnd.scrollX);
509
+ } else {
510
+ this.doc().scrollLeft = this.snapOffset(r.getBoundingClientRect().left + wnd.scrollX);
511
+ }
464
512
  this.reportProgress();
465
513
  deselect(this.wnd);
466
514
  ack(true);
@@ -468,12 +516,18 @@ export class ColumnSnapper extends Snapper {
468
516
  });
469
517
 
470
518
  comms.register("go_end", ColumnSnapper.moduleName, (_, ack) => {
471
- const factor = isRTL(wnd) ? -1 : 1;
472
519
  this.wnd.requestAnimationFrame(() => {
473
520
  this.cachedScrollWidth = this.doc().scrollWidth!;
474
- const final = this.cachedScrollWidth * factor;
475
- if(this.doc().scrollLeft === final) return ack(false);
476
- this.doc().scrollLeft = this.snapOffset(final);
521
+ let snappedFinal: number;
522
+ if (isRTL(wnd)) {
523
+ // RTL: end is the leftmost position, using normalized snapping first
524
+ snappedFinal = -this.snapNormOffset(this.cachedScrollWidth - wnd.innerWidth);
525
+ } else {
526
+ // LTR: end is scrollWidth (browser clamps to scrollWidth - innerWidth)
527
+ snappedFinal = this.snapOffset(this.cachedScrollWidth);
528
+ }
529
+ if(this.doc().scrollLeft === snappedFinal) return ack(false);
530
+ this.doc().scrollLeft = snappedFinal;
477
531
  this.reportProgress();
478
532
  deselect(this.wnd);
479
533
  ack(true);
@@ -493,11 +547,15 @@ export class ColumnSnapper extends Snapper {
493
547
  comms.register("go_prev", ColumnSnapper.moduleName, (_, ack) => {
494
548
  this.wnd.requestAnimationFrame(() => {
495
549
  this.cachedScrollWidth = this.doc().scrollWidth!;
496
- const offset = wnd.scrollX - wnd.innerWidth;
497
- const minOffset = isRTL(wnd) ? - (this.cachedScrollWidth - wnd.innerWidth) : 0;
498
- const change = scrollToOffset(Math.max(offset, minOffset));
550
+ let change: boolean;
551
+ if (isRTL(wnd)) {
552
+ // RTL: go_prev = toward document start = decreasing norm = scrollLeft toward 0
553
+ change = rtlScrollToNorm(this.normScroll() - wnd.innerWidth);
554
+ } else {
555
+ change = scrollToOffset(wnd.scrollX - wnd.innerWidth);
556
+ }
557
+ this.reportProgress();
499
558
  if(change) {
500
- this.reportProgress();
501
559
  deselect(this.wnd);
502
560
  this.checkSuspiciousSnap("left", this.wnd.innerWidth);
503
561
  }
@@ -508,11 +566,15 @@ export class ColumnSnapper extends Snapper {
508
566
  comms.register("go_next", ColumnSnapper.moduleName, (_, ack) => {
509
567
  this.wnd.requestAnimationFrame(() => {
510
568
  this.cachedScrollWidth = this.doc().scrollWidth!;
511
- const offset = wnd.scrollX + wnd.innerWidth;
512
- const maxOffset = isRTL(wnd) ? 0 : this.cachedScrollWidth - wnd.innerWidth;
513
- const change = scrollToOffset(Math.min(offset, maxOffset));
569
+ let change: boolean;
570
+ if (isRTL(wnd)) {
571
+ // RTL: go_next = toward document end = increasing norm = scrollLeft more negative
572
+ change = rtlScrollToNorm(this.normScroll() + wnd.innerWidth);
573
+ } else {
574
+ change = scrollToOffset(wnd.scrollX + wnd.innerWidth);
575
+ }
576
+ this.reportProgress();
514
577
  if(change) {
515
- this.reportProgress();
516
578
  deselect(this.wnd);
517
579
  this.checkSuspiciousSnap("right", this.wnd.innerWidth);
518
580
  }
@@ -582,4 +644,4 @@ export class ColumnSnapper extends Snapper {
582
644
  comms.log("ColumnSnapper Unmounted");
583
645
  return super.unmount(wnd, comms);
584
646
  }
585
- }
647
+ }
@@ -1,13 +1,13 @@
1
1
  import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
2
- import { Comms } from "../../comms";
3
- import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
4
- import { ModuleName } from "../ModuleLibrary";
5
- import { Snapper } from "./Snapper";
6
- import { rangeFromLocator } from "../../helpers/locator";
7
- import { forceWebkitRecalc } from "../../helpers/document";
8
- import { PatternAnalyzer } from "../../protection/PatternAnalyzer";
9
- import { SCROLL_PROTECTION_CONFIG } from "../../protection/config";
10
- import { BaseSuspiciousActivityEvent } from "../Peripherals";
2
+ import { Comms } from "../../comms/index.ts";
3
+ import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom.ts";
4
+ import { ModuleName } from "../ModuleLibrary.ts";
5
+ import { Snapper } from "./Snapper.ts";
6
+ import { rangeFromLocator } from "../../helpers/locator.ts";
7
+ import { forceWebkitRecalc } from "../../helpers/document.ts";
8
+ import { PatternAnalyzer } from "../../protection/PatternAnalyzer.ts";
9
+ import { SCROLL_PROTECTION_CONFIG } from "../../protection/config.ts";
10
+ import { BaseSuspiciousActivityEvent } from "../Peripherals.ts";
11
11
 
12
12
  const SCROLL_SNAPPER_STYLE_ID = "readium-scroll-snapper-style";
13
13
 
@@ -56,7 +56,7 @@ export class ScrollSnapper extends Snapper {
56
56
 
57
57
  private handleScroll = (_e: Event) => {
58
58
  if (!this.comms.ready) return;
59
-
59
+
60
60
  // We have to filter scroll from resize events
61
61
  if (this.isResizing) {
62
62
  return;
@@ -91,9 +91,9 @@ export class ScrollSnapper extends Snapper {
91
91
  timeDelta
92
92
  );
93
93
  if (isSuspicious) {
94
- const target = _e.target && "tagName" in _e.target ?
94
+ const target = _e.target && "tagName" in _e.target ?
95
95
  { tagName: (_e.target as Element).tagName } : null;
96
-
96
+
97
97
  this.comms?.send("content_protection", {
98
98
  type: "suspicious_scrolling",
99
99
  timestamp: Date.now(),
@@ -107,7 +107,7 @@ export class ScrollSnapper extends Snapper {
107
107
  }
108
108
 
109
109
  this.comms.send("scroll", deltaY);
110
-
110
+
111
111
  this.isScrolling = false;
112
112
  });
113
113
  }
@@ -143,7 +143,7 @@ export class ScrollSnapper extends Snapper {
143
143
  * {
144
144
  scrollbar-width: none; /* for Firefox */
145
145
  }
146
-
146
+
147
147
  body::-webkit-scrollbar {
148
148
  display: none; /* for Chrome, Safari, and Opera */
149
149
  }
@@ -157,7 +157,7 @@ export class ScrollSnapper extends Snapper {
157
157
  if (this.resizeDebounce) {
158
158
  this.wnd.clearTimeout(this.resizeDebounce);
159
159
  }
160
-
160
+
161
161
  this.isResizing = true;
162
162
  this.resizeDebounce = this.wnd.setTimeout(() => {
163
163
  this.isResizing = false;
@@ -173,8 +173,8 @@ export class ScrollSnapper extends Snapper {
173
173
  forceWebkitRecalc(this.wnd);
174
174
 
175
175
  // We absolutely must do this because overflown content
176
- // won’t be rendered if we do not trigger scroll…
177
- // Only the content at the start of the document,
176
+ // won’t be rendered if we do not trigger scroll…
177
+ // Only the content at the start of the document,
178
178
  // whose height is the viewport height, will be rendered.
179
179
  const currentScroll = this.doc().scrollTop;
180
180
  if (currentScroll > 1) {
@@ -308,4 +308,4 @@ export class ScrollSnapper extends Snapper {
308
308
  comms.log("ScrollSnapper Unmounted");
309
309
  return true;
310
310
  }
311
- }
311
+ }
@@ -1,7 +1,7 @@
1
- import { Comms } from "../../comms";
2
- import { ReadiumWindow } from "../../helpers/dom";
3
- import { Module } from "../Module";
4
- import { ModuleName } from "../ModuleLibrary";
1
+ import { Comms } from "../../comms/index.ts";
2
+ import { ReadiumWindow } from "../../helpers/dom.ts";
3
+ import { Module } from "../Module.ts";
4
+ import { ModuleName } from "../ModuleLibrary.ts";
5
5
 
6
6
  const SNAPPER_STYLE_ID = "readium-snapper-style";
7
7
 
@@ -46,4 +46,4 @@ export abstract class Snapper extends Module {
46
46
  comms.log("Snapper Unmounted");
47
47
  return true;
48
48
  }
49
- }
49
+ }
@@ -1,13 +1,13 @@
1
1
  import { Locator, LocatorLocations, LocatorText } from "@readium/shared";
2
- import { Comms } from "../../comms/comms";
3
- import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom";
4
- import { ModuleName } from "../ModuleLibrary";
5
- import { Snapper } from "./Snapper";
6
- import { rangeFromLocator } from "../../helpers/locator";
7
- import { forceWebkitRecalc } from "../../helpers/document";
8
- import { PatternAnalyzer } from "../../protection/PatternAnalyzer";
9
- import { SCROLL_PROTECTION_CONFIG } from "../../protection/config";
10
- import { SuspiciousScrollingEvent } from "./ScrollSnapper";
2
+ import { Comms } from "../../comms/comms.ts";
3
+ import { ReadiumWindow, deselect, findFirstVisibleLocator } from "../../helpers/dom.ts";
4
+ import { ModuleName } from "../ModuleLibrary.ts";
5
+ import { Snapper } from "./Snapper.ts";
6
+ import { rangeFromLocator } from "../../helpers/locator.ts";
7
+ import { forceWebkitRecalc } from "../../helpers/document.ts";
8
+ import { PatternAnalyzer } from "../../protection/PatternAnalyzer.ts";
9
+ import { SCROLL_PROTECTION_CONFIG } from "../../protection/config.ts";
10
+ import { SuspiciousScrollingEvent } from "./ScrollSnapper.ts";
11
11
 
12
12
  export type { SuspiciousScrollingEvent };
13
13
 
@@ -82,9 +82,9 @@ export class WebPubSnapper extends Snapper {
82
82
  timeDelta
83
83
  );
84
84
  if (isSuspicious) {
85
- const target = _e.target && "tagName" in _e.target ?
85
+ const target = _e.target && "tagName" in _e.target ?
86
86
  { tagName: (_e.target as Element).tagName } : null;
87
-
87
+
88
88
  this.comms?.send("content_protection", {
89
89
  type: "suspicious_scrolling",
90
90
  timestamp: Date.now(),
@@ -145,8 +145,8 @@ export class WebPubSnapper extends Snapper {
145
145
  forceWebkitRecalc(this.wnd);
146
146
 
147
147
  // We absolutely must do this because overflown content
148
- // won’t be rendered if we do not trigger scroll…
149
- // Only the content at the start of the document,
148
+ // won’t be rendered if we do not trigger scroll…
149
+ // Only the content at the start of the document,
150
150
  // whose height is the viewport height, will be rendered.
151
151
  const currentScroll = this.doc().scrollTop;
152
152
  if (currentScroll > 1) {
@@ -267,7 +267,7 @@ export class WebPubSnapper extends Snapper {
267
267
  comms.unregisterAll(WebPubSnapper.moduleName);
268
268
  this.resizeObserver.disconnect();
269
269
  if (this.handleScroll) wnd.removeEventListener("scroll", this.handleScroll);
270
-
270
+
271
271
  if (this.patternAnalyzer) {
272
272
  this.patternAnalyzer.clear();
273
273
  this.patternAnalyzer = null;
@@ -277,4 +277,4 @@ export class WebPubSnapper extends Snapper {
277
277
  comms.log("WebPubSnapper Unmounted");
278
278
  return true;
279
279
  }
280
- }
280
+ }