@readium/navigator-html-injectables 2.3.1 → 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.
@@ -41,7 +41,9 @@ export class ColumnSnapper extends Snapper {
41
41
  // The reason we do this is because when the document is transformed (translate3d),
42
42
  // the scrollLeft value is 0 because... reasons. So we have to use the cached value
43
43
  // from this.alreadyScrollLeft instead.
44
- 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;
45
47
  }
46
48
 
47
49
  snapOffset(offset: number) {
@@ -49,13 +51,35 @@ export class ColumnSnapper extends Snapper {
49
51
  return value - (value % this.wnd.innerWidth);
50
52
  }
51
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
+
52
74
  reportProgress() {
53
- // Contrary to ScrollTop, Android slightly adds to scrollX
54
- // So we do not need to round it up
55
- const scrollX = this.wnd.scrollX;
56
75
  const scrollWidth = this.cachedScrollWidth;
57
- const progress = Math.max(0, Math.min(1, scrollX / scrollWidth));
58
- 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));
59
83
  this.comms.send("progress", {
60
84
  start: progress,
61
85
  end: viewportEnd
@@ -69,11 +93,17 @@ export class ColumnSnapper extends Snapper {
69
93
  if(this.overscroll !== 0 || this.shakeTimeout !== 0) return;
70
94
  const doc = this.doc();
71
95
 
72
- 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);
73
104
  const curScrollLeft = this.scrollOffset();
74
105
  this.shakeTimeout = this.wnd.setTimeout(() => {
75
- doc.classList.remove("readium-bounce-l");
76
- doc.classList.remove("readium-bounce-r");
106
+ doc.classList.remove(bounceClass);
77
107
  this.shakeTimeout = 0;
78
108
  this.doc().scrollLeft = curScrollLeft;
79
109
  }, 150);
@@ -95,33 +125,37 @@ export class ColumnSnapper extends Snapper {
95
125
 
96
126
  // Snaps the current offset to the page width.
97
127
  snapCurrentOffset(smooth=false, noprogress=false) {
98
- const startX = this.wnd.scrollX > 0 ? this.wnd.scrollX : this.alreadyScrollLeft;
128
+ const rtl = isRTL(this.wnd);
99
129
  const doc = this.doc();
100
- const cdo = this.dragOffset();
101
130
  const columnCount = getColumnCountPerScreen(this.wnd);
102
- const currentOffset = Math.min(Math.max(0, startX), this.cachedScrollWidth);
103
- const factor = isRTL(this.wnd) ? -1 : 1;
104
- const hurdle =
105
- // The hurdle to overcome in order to change pages
106
- factor *
107
- (this.wnd.innerWidth / 3) *
108
- ((factor * cdo) > 0 ? 2 : 1);
131
+ const maxScroll = this.cachedScrollWidth - this.wnd.innerWidth;
109
132
 
110
- const so = this.snapOffset(currentOffset + hurdle);
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);
111
142
 
112
- const direction = so > this.scrollOffset() ? "right" : "left";
143
+ const snappedNorm = Math.min(maxScroll, Math.max(0, this.snapNormOffset(currentNorm + hurdle)));
113
144
 
114
- this.checkSuspiciousSnap(direction, Math.abs(so - this.scrollOffset()));
145
+ // Convert back to signed scrollLeft for the actual DOM write.
146
+ const so = rtl ? -snappedNorm : snappedNorm;
147
+ const currentScrollLeft = rtl ? -currentNorm : currentNorm;
115
148
 
116
- if(smooth && so !== this.scrollOffset()) { // Smooth snapping
149
+ const direction = so > currentScrollLeft ? "right" : "left";
150
+ this.checkSuspiciousSnap(direction, Math.abs(so - currentScrollLeft));
151
+
152
+ if(smooth && so !== currentScrollLeft) { // Smooth snapping
117
153
  this.snappingCancelled = false;
118
154
  const position = (start: number, end: number, elapsed: number, period: number) => {
119
- if (elapsed > period) {
120
- return end;
121
- }
155
+ if (elapsed > period) return end;
122
156
  return start + (end - start) * easeInOutQuad(elapsed / period);
123
- }
124
- const period = /*Math.abs(startX - (this.useTransform ? currentOffset : 0)) < 10 ? 1 : */SNAP_DURATION * columnCount; // TODO revamp
157
+ };
158
+ const period = SNAP_DURATION * columnCount;
125
159
  let startTime: number;
126
160
  const step = (timestamp: number) => {
127
161
  if(this.snappingCancelled) return;
@@ -130,7 +164,7 @@ export class ColumnSnapper extends Snapper {
130
164
  const elapsed = timestamp - startTime;
131
165
 
132
166
  const lpos = position(this.overscroll, 0, elapsed, period);
133
- const spos = position(startX, so, elapsed, period);
167
+ const spos = position(currentScrollLeft, so, elapsed, period);
134
168
  doc.scrollLeft = spos;
135
169
  if(this.overscroll !== 0)
136
170
  doc.style.transform = `translate3d(${-lpos}px, 0px, 0px)`;
@@ -143,7 +177,7 @@ export class ColumnSnapper extends Snapper {
143
177
  doc.scrollLeft = so;
144
178
  if(!noprogress) this.reportProgress();
145
179
  }
146
- }
180
+ };
147
181
  this.wnd.requestAnimationFrame(step);
148
182
  } else { // Instant snapping
149
183
  doc.style.removeProperty("transform");
@@ -193,18 +227,25 @@ export class ColumnSnapper extends Snapper {
193
227
  if(this.touchState === ScrollTouchState.MOVE) {
194
228
  // Get the horizontal drag distance
195
229
  const dragOffset = this.dragOffset();
196
- const scrollOffset = this.scrollOffset();
197
- // 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
+
198
237
  if(this.cachedScrollWidth <= this.wnd.innerWidth) {
199
238
  // Only a single page, meaning any swipe triggers next/prev
200
239
  this.reportProgress();
201
- if(dragOffset > 5) this.comms.send("no_more", undefined);
202
- if(dragOffset < -5) this.comms.send("no_less", undefined);
203
- } 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
204
244
  this.alreadyScrollLeft = 0;
205
245
  this.comms.send("no_less", undefined);
206
- } else if((this.cachedScrollWidth - scrollOffset - this.wnd.innerWidth) < 5 && dragOffset > 5) {
207
- 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;
208
249
  this.comms.send("no_more", undefined);
209
250
  }
210
251
 
@@ -235,16 +276,22 @@ export class ColumnSnapper extends Snapper {
235
276
 
236
277
  const dro = this.dragOffset();
237
278
  const newpos = this.alreadyScrollLeft + dro;
238
- 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) {
239
286
  this.overscroll = newpos;
240
287
  this.doc().style.transform = `translate3d(${-this.overscroll}px, 0px, 0px)`;
241
- } else if((newpos + this.wnd.innerWidth) > this.cachedScrollWidth) {
288
+ } else if(newpos > maxScrollLeft) {
242
289
  this.overscroll = newpos;
243
290
  this.doc().style.transform = `translate3d(${-newpos}px, 0px, 0px)`;
244
291
  } else {
245
292
  this.overscroll = 0;
246
293
  this.doc().style.removeProperty("transform");
247
- this.doc().scrollLeft = this.alreadyScrollLeft + dro;
294
+ this.doc().scrollLeft = newpos;
248
295
  }
249
296
  }
250
297
  private readonly onTouchMover = this.onTouchMove.bind(this);
@@ -371,10 +418,19 @@ export class ColumnSnapper extends Snapper {
371
418
  ack(true);
372
419
  });
373
420
 
421
+ // LTR only — snapOffset only works with non-negative values.
374
422
  const scrollToOffset = (offset: number): boolean => {
375
423
  const oldScrollLeft = this.doc().scrollLeft;
376
- 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
+ }
377
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;
378
434
  return oldScrollLeft !== this.doc().scrollLeft;
379
435
  }
380
436
 
@@ -393,10 +449,13 @@ export class ColumnSnapper extends Snapper {
393
449
  }
394
450
  this.wnd.requestAnimationFrame(() => {
395
451
  this.cachedScrollWidth = this.doc().scrollWidth!;
396
- const documentWidth = this.cachedScrollWidth;
397
- const factor = isRTL(wnd) ? -1 : 1;
398
- const offset = documentWidth * position * factor;
399
- 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
+ }
400
459
  this.reportProgress();
401
460
  deselect(this.wnd);
402
461
  ack(true);
@@ -410,7 +469,11 @@ export class ColumnSnapper extends Snapper {
410
469
  return;
411
470
  }
412
471
  this.wnd.requestAnimationFrame(() => {
413
- 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
+ }
414
477
  this.reportProgress();
415
478
  deselect(this.wnd);
416
479
  ack(true);
@@ -441,7 +504,11 @@ export class ColumnSnapper extends Snapper {
441
504
  return;
442
505
  }
443
506
  this.wnd.requestAnimationFrame(() => {
444
- 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
+ }
445
512
  this.reportProgress();
446
513
  deselect(this.wnd);
447
514
  ack(true);
@@ -449,12 +516,18 @@ export class ColumnSnapper extends Snapper {
449
516
  });
450
517
 
451
518
  comms.register("go_end", ColumnSnapper.moduleName, (_, ack) => {
452
- const factor = isRTL(wnd) ? -1 : 1;
453
519
  this.wnd.requestAnimationFrame(() => {
454
520
  this.cachedScrollWidth = this.doc().scrollWidth!;
455
- const final = this.cachedScrollWidth * factor;
456
- if(this.doc().scrollLeft === final) return ack(false);
457
- 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;
458
531
  this.reportProgress();
459
532
  deselect(this.wnd);
460
533
  ack(true);
@@ -474,11 +547,15 @@ export class ColumnSnapper extends Snapper {
474
547
  comms.register("go_prev", ColumnSnapper.moduleName, (_, ack) => {
475
548
  this.wnd.requestAnimationFrame(() => {
476
549
  this.cachedScrollWidth = this.doc().scrollWidth!;
477
- const offset = wnd.scrollX - wnd.innerWidth;
478
- const minOffset = isRTL(wnd) ? - (this.cachedScrollWidth - wnd.innerWidth) : 0;
479
- 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();
480
558
  if(change) {
481
- this.reportProgress();
482
559
  deselect(this.wnd);
483
560
  this.checkSuspiciousSnap("left", this.wnd.innerWidth);
484
561
  }
@@ -489,11 +566,15 @@ export class ColumnSnapper extends Snapper {
489
566
  comms.register("go_next", ColumnSnapper.moduleName, (_, ack) => {
490
567
  this.wnd.requestAnimationFrame(() => {
491
568
  this.cachedScrollWidth = this.doc().scrollWidth!;
492
- const offset = wnd.scrollX + wnd.innerWidth;
493
- const maxOffset = isRTL(wnd) ? 0 : this.cachedScrollWidth - wnd.innerWidth;
494
- 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();
495
577
  if(change) {
496
- this.reportProgress();
497
578
  deselect(this.wnd);
498
579
  this.checkSuspiciousSnap("right", this.wnd.innerWidth);
499
580
  }
@@ -1,5 +1,5 @@
1
- import { ReadiumWindow } from "./helpers/dom";
2
- import { ModuleName } from "./modules";
1
+ import { ReadiumWindow } from "./helpers/dom.ts";
2
+ import { ModuleName } from "./modules/index.ts";
3
3
  /**
4
4
  * The Module loader. Handles initialization of the HTML injectables
5
5
  * in the target Window, which could be an IFrame, or the current window).
@@ -1,5 +1,5 @@
1
1
  import { ModuleDerived } from "./Module.ts";
2
- export type ModuleName = "setup" | "snapper" | "column_snapper" | "scroll_snapper" | "webpub_snapper" | "fixed_setup" | "decorator" | "reflowable_setup" | "peripherals" | "webpub_setup" | "print_protection";
2
+ export type ModuleName = "setup" | "snapper" | "column_snapper" | "scroll_snapper" | "cjk_vertical_snapper" | "webpub_snapper" | "fixed_setup" | "decorator" | "reflowable_setup" | "peripherals" | "webpub_setup" | "print_protection";
3
3
  export declare const FXLModules: ModuleName[];
4
4
  export declare const ReflowableModules: ModuleName[];
5
5
  export declare const WebPubModules: ModuleName[];
@@ -0,0 +1,57 @@
1
+ import { Comms } from "../../comms/comms.ts";
2
+ import { ReadiumWindow } from "../../helpers/dom.ts";
3
+ import { ModuleName } from "../ModuleLibrary.ts";
4
+ import { Snapper } from "./Snapper.ts";
5
+ import { BaseSuspiciousActivityEvent } from "../Peripherals.ts";
6
+ export interface SuspiciousCJKScrollingEvent extends BaseSuspiciousActivityEvent {
7
+ type: "suspicious_scrolling";
8
+ scrollDelta: number;
9
+ scrollDirection: "left" | "right";
10
+ targetElement: {
11
+ tagName: string;
12
+ } | null;
13
+ }
14
+ /**
15
+ * Snapper for CJK vertical writing (writing-mode: vertical-rl).
16
+ *
17
+ * In vertical-rl, content overflows on the X axis, flowing from right (start)
18
+ * to left (end). This snapper is the horizontal counterpart of ScrollSnapper:
19
+ * it tracks scrollLeft / scrollWidth / innerWidth and reports progress
20
+ * accordingly.
21
+ *
22
+ * Progress convention:
23
+ * scrollLeft = 0 (right edge, document start) → progress 0
24
+ * scrollLeft = ±(scrollWidth − clientWidth) → progress 1 (document end, left edge)
25
+ *
26
+ * All modern browsers use negative scrollLeft for vertical-rl (range: [−scrollable, 0]).
27
+ * Read it with Math.abs() to get a non-negative offset from document start.
28
+ * Write absolute positions as negative values (go_progression, go_end).
29
+ * Write relative adjustments as viewport-coordinate deltas (go_id, go_text) — this
30
+ * is sign-agnostic and works regardless of browser convention.
31
+ *
32
+ * go_next / go_prev always return false so that the navigator advances to the
33
+ * next or previous spine item — this snapper intentionally provides free
34
+ * horizontal scroll without intra-resource page fragmentation.
35
+ */
36
+ export declare class CJKVerticalSnapper extends Snapper {
37
+ static readonly moduleName: ModuleName;
38
+ private wnd;
39
+ private comms;
40
+ private resizeObserver;
41
+ private patternAnalyzer;
42
+ private lastScrollTime;
43
+ private isScrollProtectionEnabled;
44
+ private initialScrollHandled;
45
+ private isScrolling;
46
+ private lastScrollLeft;
47
+ private isResizing;
48
+ private resizeDebounce;
49
+ private doc;
50
+ /** Total horizontally scrollable distance (magnitude). */
51
+ private scrollable;
52
+ private reportProgress;
53
+ private handleScroll;
54
+ private enableScrollProtection;
55
+ mount(wnd: ReadiumWindow, comms: Comms): boolean;
56
+ unmount(wnd: ReadiumWindow, comms: Comms): boolean;
57
+ }
@@ -22,6 +22,17 @@ export declare class ColumnSnapper extends Snapper {
22
22
  private doc;
23
23
  private scrollOffset;
24
24
  snapOffset(offset: number): number;
25
+ /**
26
+ * Snap a non-negative normalized offset (distance from document start) to
27
+ * the nearest page boundary. Works identically regardless of reading direction.
28
+ */
29
+ private snapNormOffset;
30
+ /**
31
+ * Normalized scroll position: distance from document start, always in
32
+ * [0, scrollWidth - innerWidth] regardless of reading direction.
33
+ * scrollLeft may be negative depending on the browser — Math.abs() normalizes it.
34
+ */
35
+ private normScroll;
25
36
  reportProgress(): void;
26
37
  private shakeTimeout;
27
38
  shake(): void;