@rethink-js/rt-slider 1.2.0 → 1.2.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.
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  - Support for **multiple instances**
15
15
  - A clean global API under `window.rtSlider`
16
16
  - Defensive fallbacks to native scrolling on mobile
17
- - Built-in **slide state tracking**, DOM attributes, and custom events for advanced UI sync
17
+ - Built-in **slide state tracking**, **scroll progress tracking**, DOM attributes, and custom events for advanced UI sync
18
18
 
19
19
  **Primary dependency (GitHub):** <https://github.com/darkroomengineering/lenis>
20
20
 
@@ -63,7 +63,7 @@ Add the script to your page. To create a slider, add the `data-rt-slider` attrib
63
63
  - Auto-initialize on DOM ready
64
64
  - Load Lenis dynamically for desktop inertia
65
65
  - Apply native touch scrolling styles for mobile
66
- - Compute active slide state and progress automatically
66
+ - Compute active slide state and overall scroll progress automatically
67
67
  - Expose slider state through attributes, events, and the global API
68
68
 
69
69
  Example:
@@ -192,9 +192,13 @@ As the slider moves, `rt-slider` writes state back to the DOM automatically.
192
192
  | `data-rt-slider-to-index` | Current segment end slide index |
193
193
  | `data-rt-slider-segment-progress` | Current segment progress from `0` to `1` |
194
194
  | `data-rt-slider-segment-progress-percent` | Current segment progress from `0` to `100` |
195
+ | `data-rt-slider-scroll-current` | Current clamped horizontal scroll position |
196
+ | `data-rt-slider-scroll-max` | Maximum horizontal scroll position |
195
197
  | `data-rt-slider-scroll-progress` | Overall slider progress from `0` to `1` |
196
198
  | `data-rt-slider-scroll-progress-percent` | Overall slider progress from `0` to `100` |
197
199
  | `data-rt-slider-scroll-direction` | `forward`, `backward`, or `none` |
200
+ | `data-rt-slider-scroll-at-start` | `true` when the slider is at the start |
201
+ | `data-rt-slider-scroll-at-end` | `true` when the slider is at the end |
198
202
 
199
203
  #### Per-item attributes
200
204
 
@@ -212,7 +216,7 @@ As the slider moves, `rt-slider` writes state back to the DOM automatically.
212
216
  | `data-rt-slider-item-anchor-progress-percent` | This slide's anchor position from `0` to `100` |
213
217
  | `data-rt-slider-item-distance` | Distance in pixels from the current scroll position to this slide's anchor |
214
218
 
215
- These attributes are useful for CSS-driven animations, slide-aware UI, progress indicators, and syncing other interface elements to the slider.
219
+ These attributes are useful for CSS-driven animations, slide-aware UI, progress indicators, edge-aware controls, and syncing other interface elements to the slider.
216
220
 
217
221
  ---
218
222
 
@@ -220,16 +224,21 @@ These attributes are useful for CSS-driven animations, slide-aware UI, progress
220
224
 
221
225
  Each slider instance dispatches events from the root element.
222
226
 
223
- | Event | Description |
224
- | ----------------- | ------------------------------------------- |
225
- | `rtSlider:slide` | Fires when the computed slide state changes |
226
- | `rtSlider:active` | Fires when the active slide index changes |
227
+ | Event | Description |
228
+ | ------------------- | ------------------------------------------- |
229
+ | `rtSlider:progress` | Fires when computed scroll progress changes |
230
+ | `rtSlider:slide` | Fires when the computed slide state changes |
231
+ | `rtSlider:active` | Fires when the active slide index changes |
227
232
 
228
233
  Example:
229
234
 
230
235
  ```js
231
236
  const slider = document.querySelector("[data-rt-slider]");
232
237
 
238
+ slider.addEventListener("rtSlider:progress", function (event) {
239
+ console.log("Progress:", event.detail.progress);
240
+ });
241
+
233
242
  slider.addEventListener("rtSlider:slide", function (event) {
234
243
  console.log(event.detail);
235
244
  });
@@ -239,7 +248,9 @@ slider.addEventListener("rtSlider:active", function (event) {
239
248
  });
240
249
  ```
241
250
 
242
- Both events include a full cloned slide-state object in `event.detail`.
251
+ `rtSlider:progress` includes a cloned scroll-state object in `event.detail`.
252
+
253
+ `rtSlider:slide` and `rtSlider:active` include a full cloned slide-state object in `event.detail`.
243
254
 
244
255
  ---
245
256
 
@@ -261,7 +272,7 @@ Each instance:
261
272
 
262
273
  - Has its own independent scroll physics
263
274
  - Calculates its own progress bars and button states
264
- - Tracks its own active slide and segment state
275
+ - Tracks its own active slide, segment state, and overall scroll progress
265
276
  - Dispatches its own custom events
266
277
  - Is registered internally with a unique ID
267
278
 
@@ -277,13 +288,15 @@ window.rtSlider;
277
288
 
278
289
  ### Common Methods
279
290
 
280
- | Method | Description |
281
- | ------------------- | --------------------------------------------------- |
282
- | `ids()` | Returns an array of active slider IDs |
283
- | `get(id)` | Returns the slider instance object |
284
- | `getSlideState(id)` | Returns a cloned slide-state object for an instance |
285
- | `refresh()` | Forces a recalculation of layout (all instances) |
286
- | `destroy(id?)` | Destroys specific instance or all if no ID given |
291
+ | Method | Description |
292
+ | -------------------- | ---------------------------------------------------- |
293
+ | `ids()` | Returns an array of active slider IDs |
294
+ | `get(id)` | Returns the slider instance object |
295
+ | `getSlideState(id)` | Returns a cloned slide-state object for an instance |
296
+ | `getScrollState(id)` | Returns a cloned scroll-state object for an instance |
297
+ | `getProgress(id)` | Returns overall slider progress from `0` to `1` |
298
+ | `refresh()` | Forces a recalculation of layout (all instances) |
299
+ | `destroy(id?)` | Destroys specific instance or all if no ID given |
287
300
 
288
301
  Example usage:
289
302
 
@@ -291,9 +304,11 @@ Example usage:
291
304
  // Refresh layout after an AJAX load
292
305
  window.rtSlider.refresh();
293
306
 
294
- // Get computed slide state
307
+ // Get computed state
295
308
  const ids = window.rtSlider.ids();
296
- const firstSliderState = window.rtSlider.getSlideState(ids[0]);
309
+ const firstSliderSlideState = window.rtSlider.getSlideState(ids[0]);
310
+ const firstSliderScrollState = window.rtSlider.getScrollState(ids[0]);
311
+ const firstSliderProgress = window.rtSlider.getProgress(ids[0]);
297
312
 
298
313
  // Destroy a specific slider
299
314
  window.rtSlider.destroy("my-slider-id");
@@ -303,11 +318,13 @@ window.rtSlider.destroy("my-slider-id");
303
318
 
304
319
  When using `window.rtSlider.get(id)`, each instance also exposes helper methods:
305
320
 
306
- | Method | Description |
307
- | -------------------- | ----------------------------------------------------- |
308
- | `getSlideState()` | Returns a cloned slide-state object for that instance |
309
- | `getActiveIndex()` | Returns the current active slide index |
310
- | `getActiveElement()` | Returns the current active slide element |
321
+ | Method | Description |
322
+ | -------------------- | ------------------------------------------------------ |
323
+ | `getSlideState()` | Returns a cloned slide-state object for that instance |
324
+ | `getScrollState()` | Returns a cloned scroll-state object for that instance |
325
+ | `getProgress()` | Returns overall slider progress from `0` to `1` |
326
+ | `getActiveIndex()` | Returns the current active slide index |
327
+ | `getActiveElement()` | Returns the current active slide element |
311
328
 
312
329
  ---
313
330
 
@@ -342,11 +359,17 @@ It does not rely on console output during normal use. If initialization fails, t
342
359
  - Ensure the slider is actually scrollable. If content does not overflow horizontally, active state will still resolve, but progress and movement-driven changes will be limited.
343
360
  - Ensure `data-rt-item` matches the actual slide elements and not a wrapper around them.
344
361
 
362
+ ### Progress not reaching `1` at the end
363
+
364
+ - `rt-slider` uses edge-aware scroll detection and clamps progress to `1` when the visible viewport reaches the end of the slider.
365
+ - If you are building custom progress indicators, use `window.rtSlider.getProgress(id)`, `window.rtSlider.getScrollState(id)`, or the `rtSlider:progress` event instead of deriving progress manually from raw DOM measurements.
366
+
345
367
  ### Custom events not firing as expected
346
368
 
369
+ - `rtSlider:progress` fires when computed scroll progress changes.
347
370
  - `rtSlider:slide` fires when computed slide state changes.
348
371
  - `rtSlider:active` fires when the active slide changes.
349
- - Both events are dispatched from the element with `data-rt-slider`, not from the list or item elements.
372
+ - All events are dispatched from the element with `data-rt-slider`, not from the list or item elements.
350
373
 
351
374
  ---
352
375
 
@@ -355,7 +378,7 @@ It does not rely on console output during normal use. If initialization fails, t
355
378
  MIT License
356
379
 
357
380
  Package: `@rethink-js/rt-slider` <br>
358
- GitHub: [https://github.com/Rethink-JS/rt-slider](https://github.com/Rethink-JS/rt-slider)
381
+ GitHub: [https://github.com/Rethink-JS/rt-slider](https://github.com/Rethink-JS)
359
382
 
360
383
  ---
361
384
 
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @rethink-js/rt-slider v1.2.0 | MIT */
1
+ /*! @rethink-js/rt-slider v1.2.1 | MIT */
2
2
  (() => {
3
3
  // src/index.js
4
4
  (function() {
@@ -317,6 +317,7 @@
317
317
  this._lastGapPx = 0;
318
318
  this.slideState = null;
319
319
  this._slideStateSig = "";
320
+ this._progressSig = "";
320
321
  this._lastSlideStateActiveIndex = -1;
321
322
  this._lastSlideStateScrollLeft = 0;
322
323
  var self = this;
@@ -336,6 +337,9 @@
336
337
  var dpr = Math.max(1, window.devicePixelRatio || 1);
337
338
  return 1 / dpr;
338
339
  };
340
+ Slider.prototype.scrollEdgeEpsilon = function() {
341
+ return Math.max(2, this.devicePixelEpsilon() * 4);
342
+ };
339
343
  Slider.prototype.listGap = function() {
340
344
  var cs = getComputedStyle(this.list);
341
345
  var g1 = parseFloat(cs.columnGap || "0") || 0;
@@ -436,6 +440,38 @@
436
440
  Slider.prototype.maxScroll = function() {
437
441
  return Math.max(0, this.scroller.scrollWidth - this.scroller.clientWidth);
438
442
  };
443
+ Slider.prototype.getLiveScrollMetrics = function() {
444
+ var scrollWidth = Math.max(0, this.scroller.scrollWidth || 0);
445
+ var clientWidth = Math.max(0, this.scroller.clientWidth || 0);
446
+ var max = Math.max(0, scrollWidth - clientWidth);
447
+ var current = this.safeClampScroll(this.scroller.scrollLeft);
448
+ var epsilon = this.scrollEdgeEpsilon();
449
+ if (max <= 0 || scrollWidth <= clientWidth + epsilon) {
450
+ return {
451
+ current: 0,
452
+ max: 0,
453
+ progress: 0,
454
+ progressPercent: 0,
455
+ isAtStart: true,
456
+ isAtEnd: true
457
+ };
458
+ }
459
+ var atStart = current <= epsilon;
460
+ var atEnd = current >= max - epsilon || current + clientWidth >= scrollWidth - epsilon;
461
+ if (atStart) current = 0;
462
+ if (atEnd) current = max;
463
+ var progress = max > 0 ? clamp(current / max) : 0;
464
+ if (atEnd) progress = 1;
465
+ if (atStart) progress = 0;
466
+ return {
467
+ current,
468
+ max,
469
+ progress,
470
+ progressPercent: progress * 100,
471
+ isAtStart: atStart,
472
+ isAtEnd: atEnd
473
+ };
474
+ };
439
475
  Slider.prototype.updateOverlays = function() {
440
476
  if (!this.overlayStart && !this.overlayEnd) return;
441
477
  var total = this.scroller.scrollWidth;
@@ -451,13 +487,9 @@
451
487
  setVis(this.overlayEnd, false);
452
488
  return;
453
489
  }
454
- var m = this.maxScroll();
455
- var current = this.scroller.scrollLeft;
456
- var tolerance = 10;
457
- var atStart = current <= tolerance;
458
- var atEnd = current >= m - tolerance;
459
- setVis(this.overlayStart, !atStart);
460
- setVis(this.overlayEnd, !atEnd);
490
+ var metrics = this.getLiveScrollMetrics();
491
+ setVis(this.overlayStart, !metrics.isAtStart);
492
+ setVis(this.overlayEnd, !metrics.isAtEnd);
461
493
  };
462
494
  Slider.prototype.updateButtons = function() {
463
495
  if (!this.btnPrev && !this.btnNext) return;
@@ -477,17 +509,13 @@
477
509
  }
478
510
  if (this.btnPrev) this.btnPrev.style.display = "";
479
511
  if (this.btnNext) this.btnNext.style.display = "";
480
- var m = this.maxScroll();
481
- var current = this.scroller.scrollLeft;
482
- var tolerance = 10;
483
- var atStart = current <= tolerance;
484
- var atEnd = current >= m - tolerance;
512
+ var metrics = this.getLiveScrollMetrics();
485
513
  if (this.btnPrev) {
486
- if (atStart) this.btnPrev.classList.add("inactive");
514
+ if (metrics.isAtStart) this.btnPrev.classList.add("inactive");
487
515
  else this.btnPrev.classList.remove("inactive");
488
516
  }
489
517
  if (this.btnNext) {
490
- if (atEnd) this.btnNext.classList.add("inactive");
518
+ if (metrics.isAtEnd) this.btnNext.classList.add("inactive");
491
519
  else this.btnNext.classList.remove("inactive");
492
520
  }
493
521
  };
@@ -501,10 +529,10 @@
501
529
  var avgItemWidth = total / Math.max(1, items + 2);
502
530
  var visibleItems = Math.max(1, Math.round(visible / avgItemWidth));
503
531
  var barWidth = Math.max(8, visibleItems / (items + 2) * trackWidth);
504
- var maxS = Math.max(1, total - visible);
532
+ var metrics = this.getLiveScrollMetrics();
533
+ var maxS = Math.max(1, metrics.max);
505
534
  var maxX = Math.max(0, trackWidth - barWidth);
506
- var progress = Math.min(1, Math.max(0, this.scroller.scrollLeft / maxS));
507
- var x = maxX * progress;
535
+ var x = maxX * metrics.progress;
508
536
  return {
509
537
  trackWidth,
510
538
  barWidth,
@@ -565,7 +593,9 @@
565
593
  self._touchClampTimer = 0;
566
594
  var cur = self.scroller.scrollLeft;
567
595
  var max = self.maxScroll();
568
- var eps = 1;
596
+ var clientWidth = Math.max(0, self.scroller.clientWidth || 0);
597
+ var scrollWidth = Math.max(0, self.scroller.scrollWidth || 0);
598
+ var eps = self.scrollEdgeEpsilon();
569
599
  if (!Number.isFinite(cur)) {
570
600
  self.scroller.scrollLeft = 0;
571
601
  } else if (cur < -eps || cur > max + eps) {
@@ -574,6 +604,10 @@
574
604
  self.scroller.scrollLeft = 0;
575
605
  } else if (cur > max) {
576
606
  self.scroller.scrollLeft = max;
607
+ } else if (cur <= eps) {
608
+ self.scroller.scrollLeft = 0;
609
+ } else if (cur + clientWidth >= scrollWidth - eps) {
610
+ self.scroller.scrollLeft = max;
577
611
  }
578
612
  self.updateScrollbar();
579
613
  self.updateButtons();
@@ -659,7 +693,8 @@
659
693
  return bestIdx;
660
694
  };
661
695
  Slider.prototype.getItemAnchorScrolls = function(items) {
662
- var current = this.safeClampScroll(this.scroller.scrollLeft);
696
+ var metrics = this.getLiveScrollMetrics();
697
+ var current = metrics.current;
663
698
  var alignLeft = this.getAlignTargetLeft();
664
699
  var anchors = [];
665
700
  var last = -Infinity;
@@ -673,6 +708,13 @@
673
708
  anchors.push(anchor);
674
709
  last = anchor;
675
710
  }
711
+ if (anchors.length) {
712
+ var max = this.maxScroll();
713
+ var eps = this.scrollEdgeEpsilon();
714
+ if (anchors[0] <= eps) anchors[0] = 0;
715
+ if (anchors[anchors.length - 1] >= max - eps)
716
+ anchors[anchors.length - 1] = max;
717
+ }
676
718
  return anchors;
677
719
  };
678
720
  Slider.prototype.cloneSlideState = function(state2) {
@@ -734,6 +776,18 @@
734
776
  })
735
777
  };
736
778
  };
779
+ Slider.prototype.cloneScrollState = function(scroll) {
780
+ if (!scroll) return null;
781
+ return {
782
+ current: scroll.current,
783
+ max: scroll.max,
784
+ progress: scroll.progress,
785
+ progressPercent: scroll.progressPercent,
786
+ direction: scroll.direction,
787
+ isAtStart: scroll.isAtStart,
788
+ isAtEnd: scroll.isAtEnd
789
+ };
790
+ };
737
791
  Slider.prototype.dispatchSliderEvent = function(name, detail) {
738
792
  var payload = this.cloneSlideState(detail);
739
793
  try {
@@ -749,6 +803,21 @@
749
803
  this.root.dispatchEvent(ev);
750
804
  }
751
805
  };
806
+ Slider.prototype.dispatchProgressEvent = function(name, detail) {
807
+ var payload = this.cloneScrollState(detail);
808
+ try {
809
+ this.root.dispatchEvent(
810
+ new CustomEvent(name, {
811
+ bubbles: true,
812
+ detail: payload
813
+ })
814
+ );
815
+ } catch (e) {
816
+ var ev = document.createEvent("CustomEvent");
817
+ ev.initCustomEvent(name, true, false, payload);
818
+ this.root.dispatchEvent(ev);
819
+ }
820
+ };
752
821
  Slider.prototype.applySlideStateAttributes = function(state2) {
753
822
  var root = this.root;
754
823
  root.setAttribute(
@@ -768,6 +837,14 @@
768
837
  "data-rt-slider-segment-progress-percent",
769
838
  String(round(state2.segment.progressPercent, 4))
770
839
  );
840
+ root.setAttribute(
841
+ "data-rt-slider-scroll-current",
842
+ String(round(state2.scroll.current, 4))
843
+ );
844
+ root.setAttribute(
845
+ "data-rt-slider-scroll-max",
846
+ String(round(state2.scroll.max, 4))
847
+ );
771
848
  root.setAttribute(
772
849
  "data-rt-slider-scroll-progress",
773
850
  String(round(state2.scroll.progress, 6))
@@ -780,6 +857,14 @@
780
857
  "data-rt-slider-scroll-direction",
781
858
  state2.scroll.direction
782
859
  );
860
+ root.setAttribute(
861
+ "data-rt-slider-scroll-at-start",
862
+ state2.scroll.isAtStart ? "true" : "false"
863
+ );
864
+ root.setAttribute(
865
+ "data-rt-slider-scroll-at-end",
866
+ state2.scroll.isAtEnd ? "true" : "false"
867
+ );
783
868
  for (var i = 0; i < state2.slides.length; i++) {
784
869
  var slide = state2.slides[i];
785
870
  var el = slide.element;
@@ -826,8 +911,9 @@
826
911
  };
827
912
  Slider.prototype.buildSlideState = function() {
828
913
  var items = this.getOrderedItems();
829
- var max = this.maxScroll();
830
- var current = this.safeClampScroll(this.scroller.scrollLeft);
914
+ var metrics = this.getLiveScrollMetrics();
915
+ var max = metrics.max;
916
+ var current = metrics.current;
831
917
  var prevScroll = this._lastSlideStateScrollLeft;
832
918
  var scrollDirection = "none";
833
919
  if (current > prevScroll + 0.01) scrollDirection = "forward";
@@ -839,11 +925,11 @@
839
925
  scroll: {
840
926
  current,
841
927
  max,
842
- progress: max > 0 ? clamp(current / max) : 0,
843
- progressPercent: max > 0 ? clamp(current / max) * 100 : 0,
928
+ progress: metrics.progress,
929
+ progressPercent: metrics.progressPercent,
844
930
  direction: scrollDirection,
845
- isAtStart: current <= 0.5,
846
- isAtEnd: current >= max - 0.5
931
+ isAtStart: metrics.isAtStart,
932
+ isAtEnd: metrics.isAtEnd
847
933
  },
848
934
  active: {
849
935
  index: -1,
@@ -950,11 +1036,11 @@
950
1036
  scroll: {
951
1037
  current,
952
1038
  max,
953
- progress: max > 0 ? clamp(current / max) : 0,
954
- progressPercent: max > 0 ? clamp(current / max) * 100 : 0,
1039
+ progress: metrics.progress,
1040
+ progressPercent: metrics.progressPercent,
955
1041
  direction: scrollDirection,
956
- isAtStart: current <= 0.5,
957
- isAtEnd: current >= max - 0.5
1042
+ isAtStart: metrics.isAtStart,
1043
+ isAtEnd: metrics.isAtEnd
958
1044
  },
959
1045
  active: {
960
1046
  index: nearestIndex,
@@ -989,9 +1075,14 @@
989
1075
  var state2 = this.buildSlideState();
990
1076
  var prevActiveIndex = this._lastSlideStateActiveIndex;
991
1077
  var sig = state2.active.index + "|" + state2.segment.fromIndex + "|" + state2.segment.toIndex + "|" + round(state2.segment.progress, 4) + "|" + round(state2.scroll.progress, 4) + "|" + state2.scroll.direction;
1078
+ var progressSig = round(state2.scroll.progress, 6) + "|" + round(state2.scroll.current, 4) + "|" + round(state2.scroll.max, 4) + "|" + state2.scroll.direction + "|" + (state2.scroll.isAtStart ? "1" : "0") + "|" + (state2.scroll.isAtEnd ? "1" : "0");
992
1079
  this.slideState = state2;
993
1080
  this.applySlideStateAttributes(state2);
994
1081
  this._lastSlideStateScrollLeft = state2.scroll.current;
1082
+ if (force || progressSig !== this._progressSig) {
1083
+ this._progressSig = progressSig;
1084
+ this.dispatchProgressEvent("rtSlider:progress", state2.scroll);
1085
+ }
995
1086
  if (force || sig !== this._slideStateSig) {
996
1087
  this._slideStateSig = sig;
997
1088
  this.dispatchSliderEvent("rtSlider:slide", state2);
@@ -1005,6 +1096,14 @@
1005
1096
  if (!this.slideState) this.updateSlideState(true);
1006
1097
  return this.cloneSlideState(this.slideState);
1007
1098
  };
1099
+ Slider.prototype.getScrollState = function() {
1100
+ var state2 = this.getSlideState();
1101
+ return state2 ? this.cloneScrollState(state2.scroll) : null;
1102
+ };
1103
+ Slider.prototype.getProgress = function() {
1104
+ var scroll = this.getScrollState();
1105
+ return scroll ? scroll.progress : 0;
1106
+ };
1008
1107
  Slider.prototype.getActiveIndex = function() {
1009
1108
  var state2 = this.getSlideState();
1010
1109
  return state2 && state2.active ? state2.active.index : -1;
@@ -1739,6 +1838,14 @@
1739
1838
  var inst = state.instances[id];
1740
1839
  return inst ? inst.getSlideState() : null;
1741
1840
  },
1841
+ getScrollState: function(id) {
1842
+ var inst = state.instances[id];
1843
+ return inst ? inst.getScrollState() : null;
1844
+ },
1845
+ getProgress: function(id) {
1846
+ var inst = state.instances[id];
1847
+ return inst ? inst.getProgress() : 0;
1848
+ },
1742
1849
  refresh: function() {
1743
1850
  var keys = state.order;
1744
1851
  for (var i = 0; i < keys.length; i++) {