@evermade/overflow-slider 3.3.1 → 4.1.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 (119) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +158 -33
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.esm.js +645 -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/classnames/index.d.ts +14 -0
  16. package/dist/plugins/classnames/index.esm.js +108 -0
  17. package/dist/plugins/classnames/index.min.js +1 -0
  18. package/dist/plugins/core/index.d.ts +76 -0
  19. package/dist/plugins/core/index.d2.ts +23 -0
  20. package/dist/plugins/dots/index.d.ts +16 -0
  21. package/dist/plugins/dots/index.min.js +1 -1
  22. package/dist/plugins/drag-scrolling/index.d.ts +9 -0
  23. package/dist/plugins/drag-scrolling/index.esm.js +2 -2
  24. package/dist/plugins/drag-scrolling/index.min.js +1 -1
  25. package/dist/plugins/fade/index.d.ts +16 -0
  26. package/dist/plugins/fade/index.min.js +1 -1
  27. package/dist/plugins/full-width/index.d.ts +11 -0
  28. package/dist/plugins/full-width/index.esm.js +37 -9
  29. package/dist/plugins/full-width/index.min.js +1 -1
  30. package/dist/plugins/infinite-scroll/index.d.ts +25 -0
  31. package/dist/plugins/infinite-scroll/index.esm.js +75 -0
  32. package/dist/plugins/infinite-scroll/index.min.js +1 -0
  33. package/dist/plugins/scroll-indicator/index.d.ts +14 -0
  34. package/dist/plugins/scroll-indicator/index.esm.js +3 -1
  35. package/dist/plugins/scroll-indicator/index.min.js +1 -1
  36. package/dist/plugins/skip-links/index.d.ts +17 -0
  37. package/dist/plugins/skip-links/index.esm.js +7 -1
  38. package/dist/plugins/skip-links/index.min.js +1 -1
  39. package/dist/plugins/thumbnails/index.d.ts +9 -0
  40. package/dist/plugins/thumbnails/index.min.js +1 -1
  41. package/dist/{core/utils.min.js → utils-Sxwcz8zp.js} +1 -1
  42. package/dist/{core/utils.esm.js → utils-ayDxlweP.js} +1 -1
  43. package/docs/assets/demo.css +156 -0
  44. package/docs/assets/demo.js +92 -8
  45. package/docs/dist/index.d.ts +1 -0
  46. package/docs/dist/index.esm.js +645 -1
  47. package/docs/dist/index.esm.js.map +1 -0
  48. package/docs/dist/index.min.js +2 -1
  49. package/docs/dist/index.min.js.map +1 -0
  50. package/docs/dist/mixins.scss +14 -0
  51. package/docs/dist/overflow-slider.css +1 -1
  52. package/docs/dist/plugins/arrows/index.d.ts +26 -0
  53. package/docs/dist/plugins/arrows/index.min.js +1 -1
  54. package/docs/dist/plugins/autoplay/index.d.ts +41 -0
  55. package/docs/dist/plugins/autoplay/index.esm.js +233 -0
  56. package/docs/dist/plugins/autoplay/index.min.js +1 -0
  57. package/docs/dist/plugins/classnames/index.d.ts +14 -0
  58. package/docs/dist/plugins/classnames/index.esm.js +108 -0
  59. package/docs/dist/plugins/classnames/index.min.js +1 -0
  60. package/docs/dist/plugins/core/index.d.ts +76 -0
  61. package/docs/dist/plugins/core/index.d2.ts +23 -0
  62. package/docs/dist/plugins/dots/index.d.ts +16 -0
  63. package/docs/dist/plugins/dots/index.min.js +1 -1
  64. package/docs/dist/plugins/drag-scrolling/index.d.ts +9 -0
  65. package/docs/dist/plugins/drag-scrolling/index.esm.js +2 -2
  66. package/docs/dist/plugins/drag-scrolling/index.min.js +1 -1
  67. package/docs/dist/plugins/fade/index.d.ts +16 -0
  68. package/docs/dist/plugins/fade/index.min.js +1 -1
  69. package/docs/dist/plugins/full-width/index.d.ts +11 -0
  70. package/docs/dist/plugins/full-width/index.esm.js +37 -9
  71. package/docs/dist/plugins/full-width/index.min.js +1 -1
  72. package/docs/dist/plugins/infinite-scroll/index.d.ts +25 -0
  73. package/docs/dist/plugins/infinite-scroll/index.esm.js +75 -0
  74. package/docs/dist/plugins/infinite-scroll/index.min.js +1 -0
  75. package/docs/dist/plugins/scroll-indicator/index.d.ts +14 -0
  76. package/docs/dist/plugins/scroll-indicator/index.esm.js +3 -1
  77. package/docs/dist/plugins/scroll-indicator/index.min.js +1 -1
  78. package/docs/dist/plugins/skip-links/index.d.ts +17 -0
  79. package/docs/dist/plugins/skip-links/index.esm.js +7 -1
  80. package/docs/dist/plugins/skip-links/index.min.js +1 -1
  81. package/docs/dist/plugins/thumbnails/index.d.ts +9 -0
  82. package/docs/dist/plugins/thumbnails/index.min.js +1 -1
  83. package/docs/dist/{core/utils.min.js → utils-Sxwcz8zp.js} +1 -1
  84. package/docs/dist/{core/utils.esm.js → utils-ayDxlweP.js} +1 -1
  85. package/docs/index-rtl.html +78 -2
  86. package/docs/index.html +86 -1
  87. package/package.json +50 -27
  88. package/rollup.config.js +90 -66
  89. package/src/core/details.ts +4 -0
  90. package/src/core/overflow-slider.ts +4 -2
  91. package/src/core/slider.ts +127 -62
  92. package/src/core/types.ts +30 -1
  93. package/src/mixins.scss +14 -0
  94. package/src/overflow-slider.scss +12 -10
  95. package/src/plugins/arrows/index.ts +2 -2
  96. package/src/plugins/autoplay/index.ts +276 -0
  97. package/src/plugins/autoplay/styles.scss +11 -0
  98. package/src/plugins/classnames/index.ts +147 -0
  99. package/src/plugins/dots/index.ts +2 -2
  100. package/src/plugins/drag-scrolling/index.ts +4 -4
  101. package/src/plugins/fade/index.ts +2 -2
  102. package/src/plugins/full-width/index.ts +43 -11
  103. package/src/plugins/scroll-indicator/index.ts +5 -3
  104. package/src/plugins/skip-links/index.ts +2 -2
  105. package/src/plugins/thumbnails/index.ts +2 -2
  106. package/tsconfig.json +4 -2
  107. package/changelog.md +0 -5
  108. package/dist/core/details.esm.js +0 -35
  109. package/dist/core/details.min.js +0 -1
  110. package/dist/core/overflow-slider.esm.js +0 -29
  111. package/dist/core/overflow-slider.min.js +0 -1
  112. package/dist/core/slider.esm.js +0 -499
  113. package/dist/core/slider.min.js +0 -1
  114. package/docs/dist/core/details.esm.js +0 -35
  115. package/docs/dist/core/details.min.js +0 -1
  116. package/docs/dist/core/overflow-slider.esm.js +0 -29
  117. package/docs/dist/core/overflow-slider.min.js +0 -1
  118. package/docs/dist/core/slider.esm.js +0 -499
  119. package/docs/dist/core/slider.min.js +0 -1
@@ -1,11 +1,23 @@
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
 
9
+ const overrideTransitions = () => {
10
+ slider.slides.forEach( ( slide ) => {
11
+ slide.style.transition = 'none';
12
+ });
13
+ };
14
+
15
+ const restoreTransitions = () => {
16
+ slider.slides.forEach( ( slide ) => {
17
+ slide.style.removeProperty('transition');
18
+ });
19
+ };
20
+
9
21
  function init() {
10
22
  slider.container = container;
11
23
  // ensure container has id
@@ -15,6 +27,8 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
15
27
  container.setAttribute( 'id', containerId );
16
28
  }
17
29
  setSlides();
30
+ // CSS transitions can cause delays for calculations
31
+ overrideTransitions();
18
32
  setDetails(true);
19
33
  setActiveSlideIdx();
20
34
  slider.on('contentsChanged', () => {
@@ -44,12 +58,20 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
44
58
  for (const plugin of plugins) {
45
59
  plugin(slider);
46
60
  }
61
+ // plugins may mutate layout: refresh details and derived data after they run
62
+ // setTimeout( () => {
63
+ setDetails();
64
+ setActiveSlideIdx();
65
+ setCSSVariables();
66
+ slider.emit('pluginsLoaded');
67
+ // }, 250 );
47
68
  }
48
69
  slider.on('detailsChanged', () => {
49
70
  setDataAttributes();
50
71
  setCSSVariables();
51
72
  });
52
73
  slider.emit('created');
74
+ restoreTransitions();
53
75
  slider.container.setAttribute('data-ready', 'true');
54
76
  };
55
77
 
@@ -65,7 +87,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
65
87
  };
66
88
 
67
89
  function setSlides() {
68
- slider.slides = Array.from(slider.container.querySelectorAll(slider.options.slidesSelector));
90
+ slider.slides = Array.from(slider.container.querySelectorAll(slider.options.slidesSelector)) as HTMLElement[];
69
91
  }
70
92
 
71
93
  function addEventListeners() {
@@ -200,9 +222,14 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
200
222
  };
201
223
 
202
224
  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}`);
225
+ slider.options.cssVariableContainer.style.setProperty('--slider-container-height', `${slider.details.containerHeight}px`);
226
+ slider.options.cssVariableContainer.style.setProperty('--slider-container-width', `${slider.details.containerWidth}px`);
227
+ slider.options.cssVariableContainer.style.setProperty('--slider-scrollable-width', `${slider.details.scrollableAreaWidth}px`);
228
+ slider.options.cssVariableContainer.style.setProperty('--slider-slides-count', `${slider.details.slideCount}`);
229
+ slider.options.cssVariableContainer.style.setProperty('--slider-x-offset', `${getLeftOffset()}px`);
230
+ if (typeof slider.options.targetWidth === 'function') {
231
+ slider.options.cssVariableContainer.style.setProperty('--slider-container-target-width', `${slider.options.targetWidth(slider)}px`);
232
+ }
206
233
  }
207
234
 
208
235
  function setDataAttributes() {
@@ -305,6 +332,41 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
305
332
  }
306
333
  };
307
334
 
335
+ function canMoveToSlide( idx: number ) : boolean {
336
+ if ( idx < 0 || idx >= slider.slides.length ) {
337
+ return false;
338
+ }
339
+ if (idx === slider.activeSlideIdx) {
340
+ return false;
341
+ }
342
+ const direction = slider.options.rtl ? (idx < slider.activeSlideIdx ? 'backwards' : 'forwards') : (idx < slider.activeSlideIdx ? 'backwards' : 'forwards');
343
+
344
+ // check if the slide is already in view
345
+ const sliderRect = slider.container.getBoundingClientRect();
346
+ const scrollLeft = slider.getScrollLeft();
347
+ const containerWidth = slider.details.containerWidth;
348
+
349
+ const hasUpcomingContent = slider.slides.some((s, i) => {
350
+ if (i === slider.activeSlideIdx) {
351
+ return false; // skip the slide we are checking
352
+ }
353
+ const sRect = s.getBoundingClientRect();
354
+ const sStart = sRect.left - sliderRect.left + scrollLeft;
355
+ const sEnd = sStart + sRect.width;
356
+ if (slider.options.rtl) {
357
+ if ( scrollLeft === 0 && slider.details.hasOverflow ) {
358
+ return true;
359
+ }
360
+ return (direction === 'forwards' && i > slider.activeSlideIdx && Math.floor(sStart) < Math.floor(scrollLeft)) ||
361
+ (direction === 'backwards' && i < slider.activeSlideIdx && Math.floor(sEnd) > Math.floor(scrollLeft + containerWidth));
362
+ } else {
363
+ return (direction === 'forwards' && i > slider.activeSlideIdx && Math.floor(sEnd) > Math.floor(scrollLeft + containerWidth)) ||
364
+ (direction === 'backwards' && i < slider.activeSlideIdx && Math.floor(sStart) < Math.floor(scrollLeft));
365
+ }
366
+ });
367
+ return hasUpcomingContent;
368
+ }
369
+
308
370
  function moveToSlideInDirection( direction: 'prev' | 'next' ) {
309
371
  const activeSlideIdx = slider.activeSlideIdx;
310
372
  if (direction === 'prev') {
@@ -437,68 +499,69 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
437
499
  };
438
500
 
439
501
  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
- }
475
- } 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;
502
+ const { slides, options, container } = slider;
503
+ const {
504
+ rtl,
505
+ emulateScrollSnapMaxThreshold = 10,
506
+ scrollBehavior = 'smooth',
507
+ } = options;
508
+
509
+ const isForward = rtl ? direction === 'prev' : direction === 'next';
510
+ const scrollPos = getScrollLeft();
511
+
512
+ // Get container rect once (includes any CSS transforms)
513
+ const containerRect = container.getBoundingClientRect();
514
+ const factor = rtl ? -1 : 1;
515
+
516
+ // Calculate target area offset if targetWidth is defined
517
+ let targetAreaOffset = 0;
518
+ if (typeof options.targetWidth === 'function') {
519
+ try {
520
+ const targetWidth = options.targetWidth(slider);
521
+ const containerWidth = containerRect.width;
522
+ if (Number.isFinite(targetWidth) && targetWidth > 0 && targetWidth < containerWidth) {
523
+ targetAreaOffset = (containerWidth - targetWidth) / 2;
485
524
  }
525
+ } catch (error) {
526
+ // ignore errors, use default offset of 0
486
527
  }
487
528
  }
488
- if ( snapTarget !== null ) {
489
- const offsettedSnapTarget = snapTarget - getLeftOffset();
490
- if ( Math.floor( offsettedSnapTarget ) >= 0 ) {
491
- snapTarget = offsettedSnapTarget;
492
- }
493
529
 
494
- const scrollBehavior = slider.options.scrollBehavior || 'smooth';
530
+ // Build slide metadata
531
+ const slideData = [...slides].map(slide => {
532
+ const { width } = slide.getBoundingClientRect();
533
+ const slideRect = slide.getBoundingClientRect();
534
+
535
+ // position relative to container's left edge
536
+ const relativeStart = (slideRect.left - containerRect.left) + scrollPos;
537
+ // Adjust trigger point to align with target area start instead of container edge
538
+ const alignmentPoint = relativeStart - targetAreaOffset;
539
+ const triggerPoint = Math.min(
540
+ alignmentPoint + width / 2,
541
+ alignmentPoint + emulateScrollSnapMaxThreshold
542
+ );
543
+
544
+ return { start: relativeStart - targetAreaOffset, trigger: triggerPoint };
545
+ });
495
546
 
496
- slider.container.scrollTo({
497
- left: slider.options.rtl ? -snapTarget : snapTarget,
498
- behavior: scrollBehavior as ScrollBehavior
499
- });
547
+ // Pick the target start based on drag direction
548
+ let targetStart = null;
549
+
550
+ if (isForward) {
551
+ const found = slideData.find(item => scrollPos <= item.trigger);
552
+ targetStart = found?.start ?? null;
553
+ } else {
554
+ const found = [...slideData].reverse().find(item => scrollPos >= item.trigger);
555
+ targetStart = found?.start ?? null;
500
556
  }
501
- };
557
+
558
+ if (targetStart == null) return;
559
+
560
+ // Clamp to zero and apply RTL factor
561
+ const finalLeft = Math.max(0, Math.floor(targetStart)) * factor;
562
+
563
+ container.scrollTo({ left: finalLeft, behavior: scrollBehavior as ScrollBehavior });
564
+ }
502
565
 
503
566
  function on(name: string, cb: SliderCallback) {
504
567
  if (!subs[name]) {
@@ -524,6 +587,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
524
587
  slider = <Slider>{
525
588
  emit,
526
589
  moveToDirection,
590
+ canMoveToSlide,
527
591
  moveToSlide,
528
592
  moveToSlideInDirection,
529
593
  snapToClosestSlide,
@@ -531,6 +595,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
531
595
  getInclusiveClientWidth,
532
596
  getScrollLeft,
533
597
  setScrollLeft,
598
+ setActiveSlideIdx,
534
599
  on,
535
600
  options,
536
601
  };
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,45 @@ 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
+ targetWidth?: ( slider: Slider ) => number;
69
+ [key: string]: unknown;
70
+ }
71
+
44
72
  export type SliderDetails = {
45
73
  hasOverflow: boolean;
46
74
  slideCount: number;
47
75
  containerWidth: number;
76
+ containerHeight: number;
48
77
  scrollableAreaWidth: number;
49
78
  amountOfPages: number;
50
79
  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
+ }