@evermade/overflow-slider 3.3.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +115 -29
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.esm.js +609 -1
  5. package/dist/index.esm.js.map +1 -0
  6. package/dist/index.min.js +2 -1
  7. package/dist/index.min.js.map +1 -0
  8. package/dist/mixins.scss +14 -0
  9. package/dist/overflow-slider.css +1 -1
  10. package/dist/plugins/arrows/index.d.ts +26 -0
  11. package/dist/plugins/arrows/index.min.js +1 -1
  12. package/dist/plugins/autoplay/index.d.ts +41 -0
  13. package/dist/plugins/autoplay/index.esm.js +233 -0
  14. package/dist/plugins/autoplay/index.min.js +1 -0
  15. package/dist/plugins/core/index.d.ts +75 -0
  16. package/dist/plugins/core/index.d2.ts +23 -0
  17. package/dist/plugins/dots/index.d.ts +16 -0
  18. package/dist/plugins/dots/index.min.js +1 -1
  19. package/dist/plugins/drag-scrolling/index.d.ts +9 -0
  20. package/dist/plugins/drag-scrolling/index.esm.js +2 -2
  21. package/dist/plugins/drag-scrolling/index.min.js +1 -1
  22. package/dist/plugins/fade/index.d.ts +16 -0
  23. package/dist/plugins/fade/index.min.js +1 -1
  24. package/dist/plugins/full-width/index.d.ts +11 -0
  25. package/dist/plugins/full-width/index.esm.js +14 -3
  26. package/dist/plugins/full-width/index.min.js +1 -1
  27. package/dist/plugins/infinite-scroll/index.d.ts +25 -0
  28. package/dist/plugins/infinite-scroll/index.esm.js +75 -0
  29. package/dist/plugins/infinite-scroll/index.min.js +1 -0
  30. package/dist/plugins/scroll-indicator/index.d.ts +14 -0
  31. package/dist/plugins/scroll-indicator/index.esm.js +3 -1
  32. package/dist/plugins/scroll-indicator/index.min.js +1 -1
  33. package/dist/plugins/skip-links/index.d.ts +17 -0
  34. package/dist/plugins/skip-links/index.esm.js +7 -1
  35. package/dist/plugins/skip-links/index.min.js +1 -1
  36. package/dist/plugins/thumbnails/index.d.ts +9 -0
  37. package/dist/plugins/thumbnails/index.min.js +1 -1
  38. package/dist/{core/utils.min.js → utils-Sxwcz8zp.js} +1 -1
  39. package/dist/{core/utils.esm.js → utils-ayDxlweP.js} +1 -1
  40. package/docs/assets/demo.css +115 -0
  41. package/docs/assets/demo.js +68 -0
  42. package/docs/dist/index.d.ts +1 -0
  43. package/docs/dist/index.esm.js +609 -1
  44. package/docs/dist/index.esm.js.map +1 -0
  45. package/docs/dist/index.min.js +2 -1
  46. package/docs/dist/index.min.js.map +1 -0
  47. package/docs/dist/mixins.scss +14 -0
  48. package/docs/dist/overflow-slider.css +1 -1
  49. package/docs/dist/plugins/arrows/index.d.ts +26 -0
  50. package/docs/dist/plugins/arrows/index.min.js +1 -1
  51. package/docs/dist/plugins/autoplay/index.d.ts +41 -0
  52. package/docs/dist/plugins/autoplay/index.esm.js +233 -0
  53. package/docs/dist/plugins/autoplay/index.min.js +1 -0
  54. package/docs/dist/plugins/core/index.d.ts +23 -0
  55. package/docs/dist/plugins/core/index.d2.ts +75 -0
  56. package/docs/dist/plugins/dots/index.d.ts +16 -0
  57. package/docs/dist/plugins/dots/index.min.js +1 -1
  58. package/docs/dist/plugins/drag-scrolling/index.d.ts +9 -0
  59. package/docs/dist/plugins/drag-scrolling/index.esm.js +2 -2
  60. package/docs/dist/plugins/drag-scrolling/index.min.js +1 -1
  61. package/docs/dist/plugins/fade/index.d.ts +16 -0
  62. package/docs/dist/plugins/fade/index.min.js +1 -1
  63. package/docs/dist/plugins/full-width/index.d.ts +11 -0
  64. package/docs/dist/plugins/full-width/index.esm.js +14 -3
  65. package/docs/dist/plugins/full-width/index.min.js +1 -1
  66. package/docs/dist/plugins/infinite-scroll/index.d.ts +25 -0
  67. package/docs/dist/plugins/infinite-scroll/index.esm.js +75 -0
  68. package/docs/dist/plugins/infinite-scroll/index.min.js +1 -0
  69. package/docs/dist/plugins/scroll-indicator/index.d.ts +14 -0
  70. package/docs/dist/plugins/scroll-indicator/index.esm.js +3 -1
  71. package/docs/dist/plugins/scroll-indicator/index.min.js +1 -1
  72. package/docs/dist/plugins/skip-links/index.d.ts +17 -0
  73. package/docs/dist/plugins/skip-links/index.esm.js +7 -1
  74. package/docs/dist/plugins/skip-links/index.min.js +1 -1
  75. package/docs/dist/plugins/thumbnails/index.d.ts +9 -0
  76. package/docs/dist/plugins/thumbnails/index.min.js +1 -1
  77. package/docs/dist/{core/utils.min.js → utils-Sxwcz8zp.js} +1 -1
  78. package/docs/dist/{core/utils.esm.js → utils-ayDxlweP.js} +1 -1
  79. package/docs/index-rtl.html +78 -2
  80. package/docs/index.html +77 -1
  81. package/package.json +50 -27
  82. package/rollup.config.js +90 -66
  83. package/src/core/details.ts +4 -0
  84. package/src/core/overflow-slider.ts +4 -2
  85. package/src/core/slider.ts +91 -64
  86. package/src/core/types.ts +29 -1
  87. package/src/mixins.scss +14 -0
  88. package/src/overflow-slider.scss +12 -10
  89. package/src/plugins/arrows/index.ts +2 -2
  90. package/src/plugins/autoplay/index.ts +276 -0
  91. package/src/plugins/autoplay/styles.scss +11 -0
  92. package/src/plugins/dots/index.ts +2 -2
  93. package/src/plugins/drag-scrolling/index.ts +4 -4
  94. package/src/plugins/fade/index.ts +2 -2
  95. package/src/plugins/full-width/index.ts +17 -5
  96. package/src/plugins/infinite-scroll/index.ts +109 -0
  97. package/src/plugins/scroll-indicator/index.ts +5 -3
  98. package/src/plugins/skip-links/index.ts +2 -2
  99. package/src/plugins/thumbnails/index.ts +2 -2
  100. package/tsconfig.json +4 -2
  101. package/changelog.md +0 -5
  102. package/dist/core/details.esm.js +0 -35
  103. package/dist/core/details.min.js +0 -1
  104. package/dist/core/overflow-slider.esm.js +0 -29
  105. package/dist/core/overflow-slider.min.js +0 -1
  106. package/dist/core/slider.esm.js +0 -499
  107. package/dist/core/slider.min.js +0 -1
  108. package/docs/dist/core/details.esm.js +0 -35
  109. package/docs/dist/core/details.min.js +0 -1
  110. package/docs/dist/core/overflow-slider.esm.js +0 -29
  111. package/docs/dist/core/overflow-slider.min.js +0 -1
  112. package/docs/dist/core/slider.esm.js +0 -499
  113. package/docs/dist/core/slider.min.js +0 -1
@@ -1,8 +1,8 @@
1
- import { Slider, SliderOptions, SliderPlugin, SliderCallback } from './types';
1
+ import { Slider, SliderOptions, SliderOptionArgs, SliderPlugin, SliderCallback } from './types';
2
2
  import details from './details';
3
3
  import { generateId, objectsAreEqual, getOutermostChildrenEdgeMarginSum } from './utils';
4
4
 
5
- export default function Slider( container: HTMLElement, options : SliderOptions, plugins? : SliderPlugin[] ) {
5
+ export default function Slider( container: HTMLElement, options : SliderOptionArgs, plugins? : SliderPlugin[] ) {
6
6
  let slider: Slider;
7
7
  let subs: { [key: string]: SliderCallback[] } = {};
8
8
 
@@ -65,7 +65,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
65
65
  };
66
66
 
67
67
  function setSlides() {
68
- slider.slides = Array.from(slider.container.querySelectorAll(slider.options.slidesSelector));
68
+ slider.slides = Array.from(slider.container.querySelectorAll(slider.options.slidesSelector)) as HTMLElement[];
69
69
  }
70
70
 
71
71
  function addEventListeners() {
@@ -200,9 +200,14 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
200
200
  };
201
201
 
202
202
  function setCSSVariables() {
203
- slider.container.style.setProperty('--slider-container-width', `${slider.details.containerWidth}px`);
204
- slider.container.style.setProperty('--slider-scrollable-width', `${slider.details.scrollableAreaWidth}px`);
205
- slider.container.style.setProperty('--slider-slides-count', `${slider.details.slideCount}`);
203
+ slider.options.cssVariableContainer.style.setProperty('--slider-container-height', `${slider.details.containerHeight}px`);
204
+ slider.options.cssVariableContainer.style.setProperty('--slider-container-width', `${slider.details.containerWidth}px`);
205
+ slider.options.cssVariableContainer.style.setProperty('--slider-scrollable-width', `${slider.details.scrollableAreaWidth}px`);
206
+ slider.options.cssVariableContainer.style.setProperty('--slider-slides-count', `${slider.details.slideCount}`);
207
+ slider.options.cssVariableContainer.style.setProperty('--slider-x-offset', `${getLeftOffset()}px`);
208
+ if (typeof slider.options.targetWidth === 'function') {
209
+ slider.options.cssVariableContainer.style.setProperty('--slider-container-target-width', `${slider.options.targetWidth(slider)}px`);
210
+ }
206
211
  }
207
212
 
208
213
  function setDataAttributes() {
@@ -305,6 +310,41 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
305
310
  }
306
311
  };
307
312
 
313
+ function canMoveToSlide( idx: number ) : boolean {
314
+ if ( idx < 0 || idx >= slider.slides.length ) {
315
+ return false;
316
+ }
317
+ if (idx === slider.activeSlideIdx) {
318
+ return false;
319
+ }
320
+ const direction = slider.options.rtl ? (idx < slider.activeSlideIdx ? 'backwards' : 'forwards') : (idx < slider.activeSlideIdx ? 'backwards' : 'forwards');
321
+
322
+ // check if the slide is already in view
323
+ const sliderRect = slider.container.getBoundingClientRect();
324
+ const scrollLeft = slider.getScrollLeft();
325
+ const containerWidth = slider.details.containerWidth;
326
+
327
+ const hasUpcomingContent = slider.slides.some((s, i) => {
328
+ if (i === slider.activeSlideIdx) {
329
+ return false; // skip the slide we are checking
330
+ }
331
+ const sRect = s.getBoundingClientRect();
332
+ const sStart = sRect.left - sliderRect.left + scrollLeft;
333
+ const sEnd = sStart + sRect.width;
334
+ if (slider.options.rtl) {
335
+ if ( scrollLeft === 0 && slider.details.hasOverflow ) {
336
+ return true;
337
+ }
338
+ return (direction === 'forwards' && i > slider.activeSlideIdx && Math.floor(sStart) < Math.floor(scrollLeft)) ||
339
+ (direction === 'backwards' && i < slider.activeSlideIdx && Math.floor(sEnd) > Math.floor(scrollLeft + containerWidth));
340
+ } else {
341
+ return (direction === 'forwards' && i > slider.activeSlideIdx && Math.floor(sEnd) > Math.floor(scrollLeft + containerWidth)) ||
342
+ (direction === 'backwards' && i < slider.activeSlideIdx && Math.floor(sStart) < Math.floor(scrollLeft));
343
+ }
344
+ });
345
+ return hasUpcomingContent;
346
+ }
347
+
308
348
  function moveToSlideInDirection( direction: 'prev' | 'next' ) {
309
349
  const activeSlideIdx = slider.activeSlideIdx;
310
350
  if (direction === 'prev') {
@@ -437,68 +477,53 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
437
477
  };
438
478
 
439
479
  function snapToClosestSlide(direction = "prev") {
440
- const isMovingForward = slider.options.rtl ? direction === 'prev' : direction === 'next';
441
- const slideReference = [];
442
- for (let i = 0; i < slider.slides.length; i++) {
443
- const slide = slider.slides[i];
444
- const slideWidth = slide.offsetWidth;
445
- const slideStart = slider.options.rtl ? Math.abs( slide.offsetLeft + slideWidth - slider.details.containerWidth ) : slide.offsetLeft;
446
- const slideEnd = slideStart + slideWidth;
447
- const slideMiddle = slideStart + slideWidth / 2;
448
- const trigger = Math.min(slideMiddle, slideStart + slider.options.emulateScrollSnapMaxThreshold);
449
- slideReference.push({
450
- start: slideStart,
451
- middle: slideMiddle,
452
- end: slideEnd,
453
- width: slideWidth,
454
- trigger: trigger,
455
- slide: slide,
456
- // debug
457
- offSetleft: slide.offsetLeft,
458
- rect: slide.getBoundingClientRect(),
459
- });
460
- }
461
- let snapTarget = null;
462
- const scrollPosition = getScrollLeft();
463
- if (isMovingForward) {
464
- for (let i = 0; i < slideReference.length; i++) {
465
- const item = slideReference[i];
466
- if ( i === 0 && Math.floor( scrollPosition ) <= Math.floor( item.trigger ) ) {
467
- snapTarget = 0;
468
- break;
469
- }
470
- if ( Math.floor( getScrollLeft() ) <= Math.floor( item.trigger ) ) {
471
- snapTarget = item.start;
472
- break;
473
- }
474
- }
480
+ const { slides, options, container } = slider;
481
+ const {
482
+ rtl,
483
+ emulateScrollSnapMaxThreshold = 10,
484
+ scrollBehavior = 'smooth',
485
+ } = options;
486
+
487
+ const isForward = rtl ? direction === 'prev' : direction === 'next';
488
+ const scrollPos = getScrollLeft();
489
+
490
+ // Get container rect once (includes any CSS transforms)
491
+ const containerRect = container.getBoundingClientRect();
492
+ const factor = rtl ? -1 : 1;
493
+
494
+ // Build slide metadata
495
+ const slideData = [...slides].map(slide => {
496
+ const { width } = slide.getBoundingClientRect();
497
+ const slideRect = slide.getBoundingClientRect();
498
+
499
+ // position relative to container’s left edge
500
+ const relativeStart = (slideRect.left - containerRect.left) + scrollPos;
501
+ const triggerPoint = Math.min(
502
+ relativeStart + width / 2,
503
+ relativeStart + emulateScrollSnapMaxThreshold
504
+ );
505
+
506
+ return { start: relativeStart, trigger: triggerPoint };
507
+ });
508
+
509
+ // Pick the target start based on drag direction
510
+ let targetStart = null;
511
+
512
+ if (isForward) {
513
+ const found = slideData.find(item => scrollPos <= item.trigger);
514
+ targetStart = found?.start ?? null;
475
515
  } else {
476
- for (let i = slideReference.length - 1; i >= 0; i--) {
477
- const item = slideReference[i];
478
- if ( i === slideReference.length - 1 && Math.floor( scrollPosition ) >= Math.floor( item.trigger ) ) {
479
- snapTarget = item.start;
480
- break;
481
- }
482
- if ( Math.floor( getScrollLeft() ) >= Math.floor( item.trigger ) ) {
483
- snapTarget = item.start;
484
- break;
485
- }
486
- }
516
+ const found = [...slideData].reverse().find(item => scrollPos >= item.trigger);
517
+ targetStart = found?.start ?? null;
487
518
  }
488
- if ( snapTarget !== null ) {
489
- const offsettedSnapTarget = snapTarget - getLeftOffset();
490
- if ( Math.floor( offsettedSnapTarget ) >= 0 ) {
491
- snapTarget = offsettedSnapTarget;
492
- }
493
519
 
494
- const scrollBehavior = slider.options.scrollBehavior || 'smooth';
520
+ if (targetStart == null) return;
495
521
 
496
- slider.container.scrollTo({
497
- left: slider.options.rtl ? -snapTarget : snapTarget,
498
- behavior: scrollBehavior as ScrollBehavior
499
- });
500
- }
501
- };
522
+ // Clamp to zero and apply RTL factor
523
+ const finalLeft = Math.max(0, Math.floor(targetStart)) * factor;
524
+
525
+ container.scrollTo({ left: finalLeft, behavior: scrollBehavior as ScrollBehavior });
526
+ }
502
527
 
503
528
  function on(name: string, cb: SliderCallback) {
504
529
  if (!subs[name]) {
@@ -524,6 +549,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
524
549
  slider = <Slider>{
525
550
  emit,
526
551
  moveToDirection,
552
+ canMoveToSlide,
527
553
  moveToSlide,
528
554
  moveToSlideInDirection,
529
555
  snapToClosestSlide,
@@ -531,6 +557,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
531
557
  getInclusiveClientWidth,
532
558
  getScrollLeft,
533
559
  setScrollLeft,
560
+ setActiveSlideIdx,
534
561
  on,
535
562
  options,
536
563
  };
package/src/core/types.ts CHANGED
@@ -14,10 +14,14 @@ export type Slider<O = {}, C = {}, H extends string = string> = {
14
14
  moveToSlide: (
15
15
  index: number
16
16
  ) => void
17
+ canMoveToSlide: (
18
+ index: number
19
+ ) => boolean
17
20
  getInclusiveScrollWidth: () => number
18
21
  getInclusiveClientWidth: () => number
19
22
  getScrollLeft: () => number
20
23
  setScrollLeft: (value: number) => void
24
+ setActiveSlideIdx: () => void
21
25
  on: (
22
26
  name: H | SliderHooks,
23
27
  cb: SliderCallback
@@ -31,20 +35,44 @@ export type SliderCallback<O = {}, C = {}, H extends string = string> = (
31
35
  props: Slider<O, C, H>
32
36
  ) => void;
33
37
 
38
+ /**
39
+ * Recursively makes all properties of T optional.
40
+ * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#mapped-types
41
+ */
42
+ export type DeepPartial<T> = {
43
+ [P in keyof T]?: T[P] extends object
44
+ ? DeepPartial<T[P]>
45
+ : T[P]
46
+ };
47
+
34
48
  export type SliderOptions = {
35
49
  scrollBehavior: string;
36
50
  scrollStrategy: string;
37
51
  slidesSelector: string;
38
52
  emulateScrollSnap: boolean;
39
- emulateScrollSnapMaxThreshold: number;
53
+ emulateScrollSnapMaxThreshold?: number;
54
+ cssVariableContainer: HTMLElement;
40
55
  rtl: boolean;
56
+ targetWidth?: ( slider: Slider ) => number,
41
57
  [key: string]: unknown;
42
58
  }
43
59
 
60
+ export type SliderOptionArgs = {
61
+ scrollBehavior?: 'smooth' | 'auto';
62
+ scrollStrategy?: 'fullSlide' | 'partialSlide';
63
+ slidesSelector?: string;
64
+ emulateScrollSnap?: boolean;
65
+ emulateScrollSnapMaxThreshold?: number;
66
+ cssVariableContainer?: HTMLElement;
67
+ rtl?: boolean;
68
+ [key: string]: unknown;
69
+ }
70
+
44
71
  export type SliderDetails = {
45
72
  hasOverflow: boolean;
46
73
  slideCount: number;
47
74
  containerWidth: number;
75
+ containerHeight: number;
48
76
  scrollableAreaWidth: number;
49
77
  amountOfPages: number;
50
78
  currentPage: number;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Overflow Slider mixins
3
+ */
4
+
5
+ @mixin os-slide-width($slidesPerView: 3, $gap: var(--slide-gap, 1rem), $containerWidth: var(--slider-container-width, 90vw)) {
6
+ width: calc( ( #{$containerWidth} / #{$slidesPerView} ) - calc( #{$slidesPerView} - 1 ) / #{$slidesPerView} * #{$gap});
7
+ }
8
+
9
+ @mixin os-break-out-full-width {
10
+ position: relative;
11
+ left: 50%;
12
+ width: 100vw;
13
+ margin-left: -50vw;
14
+ }
@@ -2,6 +2,18 @@
2
2
  * Overflow Slider
3
3
  */
4
4
 
5
+ /* --------------------------------------------------------------
6
+ # Plugins
7
+ -------------------------------------------------------------- */
8
+
9
+ @forward 'plugins/arrows/styles.scss';
10
+ @forward 'plugins/autoplay/styles.scss';
11
+ @forward 'plugins/dots/styles.scss';
12
+ @forward 'plugins/fade/styles.scss';
13
+ @forward 'plugins/drag-scrolling/styles.scss';
14
+ @forward 'plugins/scroll-indicator/styles.scss';
15
+ @forward 'plugins/skip-links/styles.scss';
16
+
5
17
  /* --------------------------------------------------------------
6
18
  # Core
7
19
  -------------------------------------------------------------- */
@@ -26,16 +38,6 @@
26
38
  }
27
39
  }
28
40
 
29
- /* --------------------------------------------------------------
30
- # Plugins
31
- -------------------------------------------------------------- */
32
-
33
- @import 'plugins/arrows/styles.scss';
34
- @import 'plugins/dots/styles.scss';
35
- @import 'plugins/fade/styles.scss';
36
- @import 'plugins/drag-scrolling/styles.scss';
37
- @import 'plugins/scroll-indicator/styles.scss';
38
- @import 'plugins/skip-links/styles.scss';
39
41
 
40
42
 
41
43
 
@@ -1,4 +1,4 @@
1
- import { Slider } from '../../core/types';
1
+ import { Slider, DeepPartial } from '../../core/types';
2
2
 
3
3
  const DEFAULT_TEXTS = {
4
4
  buttonPrevious: 'Previous items',
@@ -38,7 +38,7 @@ export type ArrowsOptions = {
38
38
  movementType: ArrowsMovementTypes,
39
39
  };
40
40
 
41
- export default function ArrowsPlugin( args: { [key: string]: unknown } ) {
41
+ export default function ArrowsPlugin( args?: DeepPartial<ArrowsOptions> ) {
42
42
  return ( slider: Slider ) => {
43
43
 
44
44
  const options = <ArrowsOptions>{
@@ -0,0 +1,276 @@
1
+ import { Slider, DeepPartial } from '../../core/types';
2
+
3
+ export type AutoplayMovementTypes = 'view' | 'slide';
4
+
5
+ export type AutoplayPluginOptions = {
6
+ /** Delay between auto-scrolls in milliseconds */
7
+ delayInMs: number;
8
+ /** Translatable button texts */
9
+ texts: {
10
+ play: string;
11
+ pause: string;
12
+ };
13
+ /** Icons (SVG/html string) for play/pause states */
14
+ icons: {
15
+ play: string;
16
+ pause: string;
17
+ };
18
+ /** CSS class names */
19
+ classNames: {
20
+ autoplayButton: string;
21
+ };
22
+ /** Container in which to insert controls (defaults before slider) */
23
+ container: HTMLElement | null;
24
+ /** Whether to advance by view or by slide */
25
+ movementType: AutoplayMovementTypes;
26
+ stopOnHover: boolean;
27
+ loop: boolean;
28
+ };
29
+
30
+ export type AutoplayPluginArgs = DeepPartial<AutoplayPluginOptions>;
31
+
32
+ /**
33
+ * Autoplay plugin for Overflow Slider
34
+ *
35
+ * Loops slides infinitely, always respects reduced-motion,
36
+ * provides Play/Pause controls, and shows a progress bar.
37
+ *
38
+ * @param {AutoplayPluginArgs} args
39
+ * @returns {(slider: Slider) => void}
40
+ */
41
+ export default function AutoplayPlugin( args?: AutoplayPluginArgs ) {
42
+ return ( slider: Slider ) => {
43
+ const opts = <AutoplayPluginOptions>{
44
+ delayInMs: args?.delayInMs ?? 5000,
45
+ texts: {
46
+ play: args?.texts?.play ?? 'Play',
47
+ pause: args?.texts?.pause ?? 'Pause',
48
+ },
49
+ icons: {
50
+ play: args?.icons?.play ??
51
+ '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393"/></svg>',
52
+ pause: args?.icons?.pause ??
53
+ '<svg width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5m5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5"/></svg>',
54
+ },
55
+ container: args?.container ?? null,
56
+ classNames: {
57
+ autoplayButton: args?.classNames?.autoplayButton ?? 'overflow-slider__autoplay',
58
+ },
59
+ movementType: args?.movementType ?? 'view',
60
+ stopOnHover: args?.stopOnHover ?? true,
61
+ loop: args?.loop ?? true,
62
+ };
63
+
64
+ let intervalId: number | null = null;
65
+ let rafId: number | null = null;
66
+ let startTime: number = 0;
67
+ let manualPause: boolean = false;
68
+
69
+ // a11y: respect reduced motion preference
70
+ if ( window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) {
71
+ return;
72
+ }
73
+
74
+ /**
75
+ * Create Play/Pause button and insert into DOM
76
+ * @private
77
+ * @returns {HTMLButtonElement}
78
+ */
79
+ const createButton = (): HTMLButtonElement => {
80
+ const btn = document.createElement( 'button' );
81
+ btn.type = 'button';
82
+ btn.className = opts.classNames.autoplayButton;
83
+ setButtonPaused( btn );
84
+ // initialize CSS var
85
+ btn.style.setProperty( '--autoplay-delay-progress', '0' );
86
+
87
+ if ( opts.container ) {
88
+ opts.container.appendChild( btn );
89
+ } else {
90
+ slider.container.parentElement?.insertBefore( btn, slider.container );
91
+ }
92
+
93
+ // click toggles manual play/pause
94
+ btn.addEventListener( 'click', () => {
95
+ if ( intervalId ) {
96
+ manualPause = true;
97
+ stop();
98
+ } else {
99
+ manualPause = false;
100
+ start();
101
+ }
102
+ } );
103
+
104
+ // always pause on hover/focus (but don't clear manualPause)
105
+ const pausableInteractionStart = [ 'focusin' ];
106
+ if ( opts.stopOnHover ) {
107
+ pausableInteractionStart.push( 'mouseenter');
108
+ }
109
+
110
+ const pausableInteractionEnd = [ 'focusout' ];
111
+ if ( opts.stopOnHover ) {
112
+ pausableInteractionEnd.push( 'mouseleave' );
113
+ }
114
+
115
+ pausableInteractionStart.forEach( evt =>
116
+ slider.container.addEventListener( evt, () => {
117
+ if ( intervalId ) stop();
118
+ } )
119
+ );
120
+ pausableInteractionEnd.forEach( evt =>
121
+ slider.container.addEventListener( evt, () => {
122
+ if ( !intervalId && !manualPause ) start();
123
+ } )
124
+ );
125
+
126
+ return btn;
127
+ };
128
+
129
+ const btn = createButton();
130
+
131
+ /**
132
+ * Compute next slide, reset timer
133
+ *
134
+ * @private
135
+ */
136
+ function scrollNext(): void {
137
+ if ( opts.movementType === 'view' ) {
138
+ const scrollLeft = slider.getScrollLeft();
139
+ const viewWidth = slider.getInclusiveClientWidth();
140
+ const totalWidth = slider.getInclusiveScrollWidth();
141
+ if ( scrollLeft + viewWidth >= totalWidth ) {
142
+ if (opts.loop) {
143
+ slider.moveToSlide( 0 );
144
+ } else {
145
+ stop();
146
+ btn.style.setProperty( '--autoplay-delay-progress', '0' );
147
+ }
148
+ } else {
149
+ slider.moveToDirection( 'next' );
150
+ }
151
+ } else {
152
+ const nextIdx = ( slider.activeSlideIdx + 1 ) % slider.details.slideCount;
153
+ if ( slider.canMoveToSlide( nextIdx ) ) {
154
+ slider.moveToSlide( nextIdx );
155
+ } else {
156
+ if (opts.loop) {
157
+ slider.moveToSlide( 0 );
158
+ } else {
159
+ stop();
160
+ btn.style.setProperty( '--autoplay-delay-progress', '0' );
161
+ }
162
+ }
163
+ }
164
+ // reset progress timer
165
+ startTime = performance.now();
166
+
167
+ // restart the progress loop
168
+ if (rafId !== null) {
169
+ cancelAnimationFrame(rafId);
170
+ }
171
+ tick();
172
+ }
173
+
174
+ /**
175
+ * Animation-loop to update CSS var
176
+ *
177
+ * @private
178
+ */
179
+ function tick(): void {
180
+ const now = performance.now();
181
+ const pct = Math.min( (( now - startTime ) / opts.delayInMs) * 100, 100 );
182
+ btn.style.setProperty( '--autoplay-delay-progress', `${Math.round( pct )}` );
183
+
184
+ if ( pct < 100 ) {
185
+ rafId = requestAnimationFrame( tick );
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Start autoplay
191
+ *
192
+ * @returns {void}
193
+ */
194
+ function start(): void {
195
+ if ( intervalId ) clearInterval( intervalId );
196
+ if ( rafId ) cancelAnimationFrame( rafId );
197
+
198
+ setButtonPlaying( btn );
199
+
200
+ // reset timer and animate progress
201
+ startTime = performance.now();
202
+ tick();
203
+
204
+ intervalId = window.setInterval( scrollNext, opts.delayInMs );
205
+ }
206
+
207
+ /**
208
+ * Stop autoplay
209
+ *
210
+ * @param {boolean} [fromManual=false] Whether this stop was user‐initiated
211
+ * @returns {void}
212
+ */
213
+ function stop( fromManual = false ): void {
214
+ if ( intervalId ) clearInterval( intervalId );
215
+ if ( rafId ) cancelAnimationFrame( rafId );
216
+
217
+ intervalId = rafId = null;
218
+ if ( fromManual ) {
219
+ manualPause = true;
220
+ }
221
+ setButtonPaused( btn );
222
+ }
223
+
224
+ /**
225
+ * Set button state to “playing”
226
+ *
227
+ * @private
228
+ * @param {HTMLElement} b
229
+ */
230
+ function setButtonPlaying( b: HTMLElement ): void {
231
+ b.setAttribute( 'aria-pressed', 'true' );
232
+ b.setAttribute( 'aria-label', opts.texts.pause );
233
+ const frag = document.createRange()
234
+ .createContextualFragment( opts.icons.pause );
235
+ b.innerHTML = '';
236
+ b.appendChild( frag );
237
+ }
238
+
239
+ /**
240
+ * Set button state to “paused”
241
+ *
242
+ * @private
243
+ * @param {HTMLElement} b
244
+ */
245
+ function setButtonPaused( b: HTMLElement ): void {
246
+ b.setAttribute( 'aria-pressed', 'false' );
247
+ b.setAttribute( 'aria-label', opts.texts.play );
248
+ const frag = document.createRange()
249
+ .createContextualFragment( opts.icons.play );
250
+ b.innerHTML = '';
251
+ b.appendChild( frag );
252
+ b.style.setProperty( '--autoplay-delay-progress', '0' );
253
+ }
254
+
255
+ /**
256
+ * Pause/resume when slider enters/leaves viewport
257
+ * @private
258
+ */
259
+ function observeVisibility(): void {
260
+ const observer = new IntersectionObserver( entries => {
261
+ for ( const e of entries ) {
262
+ if ( !e.isIntersecting && intervalId ) {
263
+ stop();
264
+ } else if ( e.isIntersecting && !intervalId && !manualPause ) {
265
+ start();
266
+ }
267
+ }
268
+ }, { threshold: 0 } );
269
+ observer.observe( slider.container );
270
+ }
271
+
272
+ // Initialize autoplay
273
+ start();
274
+ observeVisibility();
275
+ };
276
+ }
@@ -0,0 +1,11 @@
1
+ /* --------------------------------------------------------------
2
+ # AutoplayPlugin
3
+ -------------------------------------------------------------- */
4
+
5
+ :root {
6
+ --overflow-slider-autoplay-background: #000;
7
+ }
8
+
9
+ .overflow-slider__autoplay {
10
+ background: var(--overflow-slider-autoplay-background);
11
+ }
@@ -1,4 +1,4 @@
1
- import { Slider } from '../../core/types';
1
+ import { Slider, DeepPartial } from '../../core/types';
2
2
 
3
3
  export type DotsOptions = {
4
4
  texts: {
@@ -20,7 +20,7 @@ const DEFAULT_CLASS_NAMES = {
20
20
  dotsItem: 'overflow-slider__dot-item',
21
21
  };
22
22
 
23
- export default function DotsPlugin( args: { [key: string]: unknown } ) {
23
+ export default function DotsPlugin( args?: DeepPartial<DotsOptions> ) {
24
24
  return ( slider: Slider ) => {
25
25
  const options = <DotsOptions>{
26
26
  texts: {
@@ -1,4 +1,4 @@
1
- import { Slider } from '../../core/types';
1
+ import { Slider, DeepPartial } from '../../core/types';
2
2
 
3
3
  const DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK = 20;
4
4
 
@@ -6,7 +6,7 @@ export type DragScrollingOptions = {
6
6
  draggedDistanceThatPreventsClick: number,
7
7
  };
8
8
 
9
- export default function DragScrollingPlugin( args: { [key: string]: unknown } ) {
9
+ export default function DragScrollingPlugin( args?: DeepPartial<DragScrollingOptions> ) {
10
10
  const options = <DragScrollingOptions>{
11
11
  draggedDistanceThatPreventsClick: args?.draggedDistanceThatPreventsClick ?? DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK,
12
12
  };
@@ -31,7 +31,7 @@ export default function DragScrollingPlugin( args: { [key: string]: unknown } )
31
31
  return;
32
32
  }
33
33
  isMouseDown = true;
34
- startX = e.pageX - slider.container.offsetLeft;
34
+ startX = e.pageX - slider.container.getBoundingClientRect().left;
35
35
  scrollLeft = slider.container.scrollLeft;
36
36
  // change cursor to grabbing
37
37
  slider.container.style.cursor = 'grabbing';
@@ -55,7 +55,7 @@ export default function DragScrollingPlugin( args: { [key: string]: unknown } )
55
55
  programmaticScrollStarted = true;
56
56
  slider.emit('programmaticScrollStart');
57
57
  }
58
- const x = e.pageX - slider.container.offsetLeft;
58
+ const x = e.pageX - slider.container.getBoundingClientRect().left;
59
59
  const walk = (x - startX);
60
60
  const newScrollLeft = scrollLeft - walk;
61
61
  mayNeedToSnap = true;
@@ -1,4 +1,4 @@
1
- import { Slider } from '../../core/types';
1
+ import { Slider, DeepPartial } from '../../core/types';
2
2
 
3
3
  export type FadeOptions = {
4
4
  classNames: {
@@ -11,7 +11,7 @@ export type FadeOptions = {
11
11
  containerEnd: HTMLElement | null,
12
12
  };
13
13
 
14
- export default function FadePlugin( args: { [key: string]: unknown } ) {
14
+ export default function FadePlugin( args?: DeepPartial<FadeOptions> ) {
15
15
  return ( slider: Slider ) => {
16
16
 
17
17
  const options = <FadeOptions>{