@evermade/overflow-slider 2.0.1 → 3.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 (51) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +17 -3
  3. package/dist/core/overflow-slider.esm.js +2 -0
  4. package/dist/core/overflow-slider.min.js +1 -1
  5. package/dist/core/slider.esm.js +159 -9
  6. package/dist/core/slider.min.js +1 -1
  7. package/dist/overflow-slider.css +1 -1
  8. package/dist/plugins/dots/dots/index.esm.js +21 -22
  9. package/dist/plugins/dots/dots/index.min.js +1 -1
  10. package/dist/plugins/drag-scrolling/drag-scrolling/index.esm.js +30 -3
  11. package/dist/plugins/drag-scrolling/drag-scrolling/index.min.js +1 -1
  12. package/dist/plugins/fade/fade/index.esm.js +81 -0
  13. package/dist/plugins/fade/fade/index.min.js +1 -0
  14. package/dist/plugins/full-width/full-width/index.esm.js +1 -0
  15. package/dist/plugins/full-width/full-width/index.min.js +1 -1
  16. package/dist/plugins/scroll-indicator/scroll-indicator/index.esm.js +0 -2
  17. package/dist/plugins/scroll-indicator/scroll-indicator/index.min.js +1 -1
  18. package/dist/plugins/thumbnails/thumbnails/index.esm.js +2 -1
  19. package/dist/plugins/thumbnails/thumbnails/index.min.js +1 -1
  20. package/docs/assets/demo.css +16 -0
  21. package/docs/assets/demo.js +44 -7
  22. package/docs/dist/core/overflow-slider.esm.js +2 -0
  23. package/docs/dist/core/overflow-slider.min.js +1 -1
  24. package/docs/dist/core/slider.esm.js +159 -9
  25. package/docs/dist/core/slider.min.js +1 -1
  26. package/docs/dist/overflow-slider.css +1 -1
  27. package/docs/dist/plugins/dots/dots/index.esm.js +21 -22
  28. package/docs/dist/plugins/dots/dots/index.min.js +1 -1
  29. package/docs/dist/plugins/drag-scrolling/drag-scrolling/index.esm.js +30 -3
  30. package/docs/dist/plugins/drag-scrolling/drag-scrolling/index.min.js +1 -1
  31. package/docs/dist/plugins/fade/fade/index.esm.js +81 -0
  32. package/docs/dist/plugins/fade/fade/index.min.js +1 -0
  33. package/docs/dist/plugins/full-width/full-width/index.esm.js +1 -0
  34. package/docs/dist/plugins/full-width/full-width/index.min.js +1 -1
  35. package/docs/dist/plugins/scroll-indicator/scroll-indicator/index.esm.js +0 -2
  36. package/docs/dist/plugins/scroll-indicator/scroll-indicator/index.min.js +1 -1
  37. package/docs/dist/plugins/thumbnails/thumbnails/index.esm.js +2 -1
  38. package/docs/dist/plugins/thumbnails/thumbnails/index.min.js +1 -1
  39. package/docs/index.html +39 -7
  40. package/package.json +7 -2
  41. package/src/core/overflow-slider.ts +2 -0
  42. package/src/core/slider.ts +176 -14
  43. package/src/core/types.ts +31 -1
  44. package/src/overflow-slider.scss +2 -1
  45. package/src/plugins/dots/index.ts +21 -23
  46. package/src/plugins/drag-scrolling/index.ts +34 -5
  47. package/src/plugins/fade/index.ts +102 -0
  48. package/src/plugins/fade/styles.scss +27 -0
  49. package/src/plugins/full-width/index.ts +1 -0
  50. package/src/plugins/scroll-indicator/index.ts +0 -2
  51. package/src/plugins/thumbnails/index.ts +2 -1
package/src/core/types.ts CHANGED
@@ -5,6 +5,9 @@ export type Slider<O = {}, C = {}, H extends string = string> = {
5
5
  moveToDirection: (
6
6
  direction: 'prev' | 'next'
7
7
  ) => void
8
+ snapToClosestSlide: (
9
+ direction: 'prev' | 'next'
10
+ ) => void
8
11
  moveToSlide: (
9
12
  index: number
10
13
  ) => void
@@ -21,6 +24,8 @@ export type SliderOptions = {
21
24
  scrollBehavior: string;
22
25
  scrollStrategy: string;
23
26
  slidesSelector: string;
27
+ emulateScrollSnap: boolean;
28
+ emulateScrollSnapMaxThreshold: number;
24
29
  [key: string]: any;
25
30
  }
26
31
 
@@ -38,7 +43,16 @@ export type SliderHooks =
38
43
  | HOOK_CONTENTS_CHANGED
39
44
  | HOOK_DETAILS_CHANGED
40
45
  | HOOK_CONTAINER_SIZE_CHANGED
41
- | HOOK_ACTIVE_SLIDE_CHANGED;
46
+ | HOOK_ACTIVE_SLIDE_CHANGED
47
+ | HOOK_SCROLL_START
48
+ | HOOK_SCROLL
49
+ | HOOK_SCROLL_END
50
+ | HOOK_NATIVE_SCROLL_START
51
+ | HOOK_NATIVE_SCROLL
52
+ | HOOK_NATIVE_SCROLL_END
53
+ | HOOK_PROGRAMMATIC_SCROLL_START
54
+ | HOOK_PROGRAMMATIC_SCROLL
55
+ | HOOK_PROGRAMMATIC_SCROLL_END;
42
56
 
43
57
  export type HOOK_CREATED = 'created';
44
58
  export type HOOK_DETAILS_CHANGED = 'detailsChanged';
@@ -46,4 +60,20 @@ export type HOOK_CONTENTS_CHANGED = 'contentsChanged';
46
60
  export type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged';
47
61
  export type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged';
48
62
 
63
+ // any type of scroll
64
+ export type HOOK_SCROLL_START = 'scrollStart';
65
+ export type HOOK_SCROLL = 'scroll';
66
+ export type HOOK_SCROLL_END = 'scrollEnd';
67
+
68
+ // user initted scroll (touch, mouse wheel, etc.)
69
+ export type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart';
70
+ export type HOOK_NATIVE_SCROLL = 'nativeScroll';
71
+ export type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd';
72
+
73
+ // programmatic scroll (e.g. el.scrollTo)
74
+ export type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart';
75
+ export type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll';
76
+ export type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd';
77
+
78
+
49
79
  export type SliderPlugin = (slider: Slider) => void;
@@ -9,11 +9,11 @@
9
9
  .overflow-slider {
10
10
  overflow: auto;
11
11
  width: 100%;
12
- overflow: auto;
13
12
  display: grid;
14
13
  grid-auto-flow: column;
15
14
  grid-template-columns: max-content;
16
15
  max-width: max-content;
16
+ position: relative;
17
17
  // hide native scrollbars
18
18
  &::-webkit-scrollbar {
19
19
  display: none;
@@ -32,6 +32,7 @@
32
32
 
33
33
  @import 'plugins/arrows/styles.scss';
34
34
  @import 'plugins/dots/styles.scss';
35
+ @import 'plugins/fade/styles.scss';
35
36
  @import 'plugins/drag-scrolling/styles.scss';
36
37
  @import 'plugins/scroll-indicator/styles.scss';
37
38
  @import 'plugins/skip-links/styles.scss';
@@ -45,8 +45,8 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
45
45
 
46
46
  const dotsList = document.createElement( 'ul' );
47
47
 
48
- const pages = slider.details.amountOfPages;
49
- const currentPage = slider.details.currentPage;
48
+ const pages = slider.details.slideCount;
49
+ const currentItem = slider.activeSlideIdx;
50
50
 
51
51
  if ( pages <= 1 ) {
52
52
  return;
@@ -58,22 +58,22 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
58
58
  dot.setAttribute( 'type', 'button' );
59
59
  dot.setAttribute( 'class', options.classNames.dotsItem );
60
60
  dot.setAttribute( 'aria-label', options.texts.dotDescription.replace( '%d', ( i + 1 ).toString() ).replace( '%d', pages.toString() ) );
61
- dot.setAttribute( 'aria-pressed', ( i === currentPage ).toString() );
62
- dot.setAttribute( 'data-page', ( i + 1 ).toString() );
61
+ dot.setAttribute( 'aria-pressed', ( i === currentItem ).toString() );
62
+ dot.setAttribute( 'data-item', ( i + 1 ).toString() );
63
63
  dotListItem.appendChild( dot );
64
64
  dotsList.appendChild( dotListItem );
65
65
  dot.addEventListener( 'click', () => activateDot( i + 1 ) );
66
66
  dot.addEventListener( 'focus', () => pageFocused = i + 1 );
67
67
  dot.addEventListener( 'keydown', ( e ) => {
68
- const currentPageItem = dots.querySelector( `[aria-pressed="true"]` );
69
- if ( ! currentPageItem ) {
68
+ const currentItemItem = dots.querySelector( `[aria-pressed="true"]` );
69
+ if ( ! currentItemItem ) {
70
70
  return;
71
71
  }
72
- const currentPage = parseInt( currentPageItem.getAttribute( 'data-page' ) ?? '1' );
72
+ const currentItem = parseInt( currentItemItem.getAttribute( 'data-item' ) ?? '1' );
73
73
  if ( e.key === 'ArrowLeft' ) {
74
- const previousPage = currentPage - 1;
74
+ const previousPage = currentItem - 1;
75
75
  if ( previousPage > 0 ) {
76
- const matchingDot = dots.querySelector( `[data-page="${previousPage}"]` );
76
+ const matchingDot = dots.querySelector( `[data-item="${previousPage}"]` );
77
77
  if ( matchingDot ) {
78
78
  ( <HTMLElement>matchingDot ).focus();
79
79
  }
@@ -81,9 +81,9 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
81
81
  }
82
82
  }
83
83
  if ( e.key === 'ArrowRight' ) {
84
- const nextPage = currentPage + 1;
84
+ const nextPage = currentItem + 1;
85
85
  if ( nextPage <= pages ) {
86
- const matchingDot = dots.querySelector( `[data-page="${nextPage}"]` );
86
+ const matchingDot = dots.querySelector( `[data-item="${nextPage}"]` );
87
87
  if ( matchingDot ) {
88
88
  ( <HTMLElement>matchingDot ).focus();
89
89
  }
@@ -97,20 +97,19 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
97
97
 
98
98
  // return focus to same page after rebuild
99
99
  if ( pageFocused ) {
100
- const matchingDot = dots.querySelector( `[data-page="${pageFocused}"]` );
100
+ const matchingDot = dots.querySelector( `[data-item="${pageFocused}"]` );
101
101
  if ( matchingDot ) {
102
102
  ( <HTMLElement>matchingDot ).focus();
103
103
  }
104
104
  }
105
105
  };
106
106
 
107
- const activateDot = ( page: number ) => {
108
- const scrollTargetPosition = slider.details.containerWidth * ( page - 1 );
109
- slider.container.style.scrollBehavior = slider.options.scrollBehavior;
110
- slider.container.style.scrollSnapType = 'none';
111
- slider.container.scrollLeft = scrollTargetPosition;
112
- slider.container.style.scrollBehavior = '';
113
- slider.container.style.scrollSnapType = '';
107
+ const activateDot = ( item: number ) => {
108
+ // const scrollTargetPosition = slider.details.containerWidth * ( page - 1 );
109
+ // slider.container.style.scrollBehavior = slider.options.scrollBehavior;
110
+ // slider.container.scrollLeft = scrollTargetPosition;
111
+ // slider.container.style.scrollBehavior = '';
112
+ slider.moveToSlide( item - 1 );
114
113
  };
115
114
 
116
115
  buildDots();
@@ -121,9 +120,8 @@ export default function DotsPlugin( args: { [key: string]: any } ) {
121
120
  slider.container.parentNode?.insertBefore( dots, slider.container.nextSibling );
122
121
  }
123
122
 
124
- slider.on( 'detailsChanged', () => {
125
- buildDots();
126
- } );
127
-
123
+ slider.on( 'scrollEnd', buildDots );
124
+ slider.on( 'contentsChanged', buildDots );
125
+ slider.on( 'containerSizeChanged', buildDots );
128
126
  };
129
127
  };
@@ -15,23 +15,26 @@ export default function DragScrollingPlugin( args: { [key: string]: any } ) {
15
15
  let startX = 0;
16
16
  let scrollLeft = 0;
17
17
 
18
+ let isMovingForward = false;
19
+ let programmaticScrollStarted = false;
20
+ let mayNeedToSnap = false;
21
+
18
22
  // add data attribute to container
19
23
  slider.container.setAttribute( 'data-has-drag-scrolling', 'true' );
20
24
 
21
25
  const mouseDown = (e: MouseEvent) => {
26
+ programmaticScrollStarted = false;
22
27
  if ( ! slider.details.hasOverflow ) {
23
28
  return;
24
29
  }
25
30
  if ( ! slider.container.contains( e.target as Node ) ) {
26
31
  return;
27
32
  }
28
-
29
33
  isMouseDown = true;
30
34
  startX = e.pageX - slider.container.offsetLeft;
31
35
  scrollLeft = slider.container.scrollLeft;
32
36
  // change cursor to grabbing
33
37
  slider.container.style.cursor = 'grabbing';
34
- slider.container.style.scrollSnapType = 'none';
35
38
  slider.container.style.scrollBehavior = 'auto';
36
39
  // prevent focus going to the slides
37
40
  e.preventDefault();
@@ -40,15 +43,26 @@ export default function DragScrollingPlugin( args: { [key: string]: any } ) {
40
43
 
41
44
  const mouseMove = (e: MouseEvent) => {
42
45
  if ( ! slider.details.hasOverflow ) {
46
+ programmaticScrollStarted = false;
43
47
  return;
44
48
  }
45
49
  if (!isMouseDown) {
50
+ programmaticScrollStarted = false;
46
51
  return;
47
52
  }
48
53
  e.preventDefault();
54
+ if (!programmaticScrollStarted) {
55
+ programmaticScrollStarted = true;
56
+ slider.emit('programmaticScrollStart');
57
+ }
49
58
  const x = e.pageX - slider.container.offsetLeft;
50
59
  const walk = (x - startX);
51
- slider.container.scrollLeft = scrollLeft - walk;
60
+ const newScrollLeft = scrollLeft - walk;
61
+ mayNeedToSnap = true;
62
+ if ( slider.container.scrollLeft !== newScrollLeft ) {
63
+ isMovingForward = slider.container.scrollLeft < newScrollLeft;
64
+ }
65
+ slider.container.scrollLeft = newScrollLeft;
52
66
 
53
67
  const absWalk = Math.abs(walk);
54
68
  const slides = slider.container.querySelectorAll( slider.options.slidesSelector );
@@ -59,13 +73,15 @@ export default function DragScrollingPlugin( args: { [key: string]: any } ) {
59
73
  };
60
74
 
61
75
  const mouseUp = () => {
62
- if ( ! slider.details.hasOverflow ) {
76
+ if (!slider.details.hasOverflow) {
77
+ programmaticScrollStarted = false;
63
78
  return;
64
79
  }
65
80
  isMouseDown = false;
81
+
66
82
  slider.container.style.cursor = '';
67
83
  setTimeout(() => {
68
- slider.container.style.scrollSnapType = '';
84
+ programmaticScrollStarted = false;
69
85
  slider.container.style.scrollBehavior = '';
70
86
  const slides = slider.container.querySelectorAll( slider.options.slidesSelector );
71
87
  slides.forEach((slide) => {
@@ -78,5 +94,18 @@ export default function DragScrollingPlugin( args: { [key: string]: any } ) {
78
94
  window.addEventListener('mousemove', mouseMove);
79
95
  window.addEventListener('mouseup', mouseUp);
80
96
 
97
+ // emulate scroll snapping
98
+ if ( slider.options.emulateScrollSnap ) {
99
+ const snap = () => {
100
+ if (!mayNeedToSnap || isMouseDown) {
101
+ return;
102
+ }
103
+ mayNeedToSnap = false;
104
+ slider.snapToClosestSlide(isMovingForward ? 'next' : 'prev');
105
+ };
106
+ slider.on( 'programmaticScrollEnd', snap );
107
+ window.addEventListener( 'mouseup', snap );
108
+ }
109
+
81
110
  };
82
111
  };
@@ -0,0 +1,102 @@
1
+ import { Slider } from '../../core/types';
2
+
3
+ export type FadeOptions = {
4
+ classNames: {
5
+ fadeItem: string;
6
+ fadeItemStart: string;
7
+ fadeItemEnd: string;
8
+ },
9
+ container: HTMLElement | null,
10
+ containerStart: HTMLElement | null,
11
+ containerEnd: HTMLElement | null,
12
+ };
13
+
14
+ export default function FadePlugin( args: { [key: string]: any } ) {
15
+ return ( slider: Slider ) => {
16
+
17
+ const options = <FadeOptions>{
18
+ classNames: {
19
+ fadeItem: 'overflow-slider-fade',
20
+ fadeItemStart: 'overflow-slider-fade--start',
21
+ fadeItemEnd: 'overflow-slider-fade--end',
22
+ },
23
+ container: args?.container ?? null,
24
+ containerStart: args?.containerStart ?? null,
25
+ containerEnd: args?.containerEnd ?? null,
26
+ };
27
+
28
+ const fadeItemStart = document.createElement( 'div' );
29
+ fadeItemStart.classList.add( options.classNames.fadeItem, options.classNames.fadeItemStart );
30
+ fadeItemStart.setAttribute( 'aria-hidden', 'true' );
31
+ fadeItemStart.setAttribute( 'tabindex', '-1' );
32
+
33
+ const fadeItemEnd = document.createElement( 'div' );
34
+ fadeItemEnd.classList.add( options.classNames.fadeItem, options.classNames.fadeItemEnd );
35
+ fadeItemEnd.setAttribute( 'aria-hidden', 'true' );
36
+ fadeItemEnd.setAttribute( 'tabindex', '-1' );
37
+
38
+ if ( options.containerStart ) {
39
+ options.containerStart.appendChild( fadeItemStart );
40
+ } else if ( options.container ) {
41
+ options.container.appendChild( fadeItemStart );
42
+ }
43
+
44
+ if ( options.containerEnd ) {
45
+ options.containerEnd.appendChild( fadeItemEnd );
46
+ } else if ( options.container ) {
47
+ options.container.appendChild( fadeItemEnd );
48
+ }
49
+
50
+ const hasFadeAtStart = () => {
51
+ return slider.container.scrollLeft > fadeItemStart.offsetWidth;
52
+ }
53
+
54
+ const fadeAtStartOpacity = () => {
55
+ const position = slider.container.scrollLeft;
56
+ if ( position <= fadeItemStart.offsetWidth ) {
57
+ return position / Math.max(fadeItemStart.offsetWidth, 1);
58
+ }
59
+ return 1;
60
+ }
61
+
62
+ const hasFadeAtEnd = () => {
63
+ return slider.container.scrollLeft < (slider.container.scrollWidth - slider.container.clientWidth - fadeItemEnd.offsetWidth);
64
+ }
65
+
66
+ const fadeAtEndOpacity = () => {
67
+ const position = slider.container.scrollLeft;
68
+ const maxPosition = slider.container.scrollWidth - slider.container.clientWidth;
69
+ const maxFadePosition = maxPosition - fadeItemEnd.offsetWidth;
70
+ if ( position >= maxFadePosition ) {
71
+ return ( ( maxFadePosition - position ) / Math.max(fadeItemEnd.offsetWidth, 1) ) + 1;
72
+ }
73
+ return 1;
74
+ }
75
+
76
+ const update = () => {
77
+ fadeItemStart.setAttribute( 'data-has-fade', hasFadeAtStart().toString() );
78
+ fadeItemStart.style.opacity = fadeAtStartOpacity().toString();
79
+ fadeItemEnd.setAttribute( 'data-has-fade', hasFadeAtEnd().toString() );
80
+ fadeItemEnd.style.opacity = fadeAtEndOpacity().toString();
81
+ };
82
+
83
+ update();
84
+ slider.on( 'created', update );
85
+ slider.on( 'contentsChanged', update );
86
+ slider.on( 'containerSizeChanged', update );
87
+ slider.on( 'scrollEnd', update );
88
+ slider.on( 'scrollStart', update );
89
+ let requestId = 0;
90
+ const debouncedUpdate = () => {
91
+ if ( requestId ) {
92
+ window.cancelAnimationFrame( requestId );
93
+ }
94
+ requestId = window.requestAnimationFrame(() => {
95
+ update();
96
+ });
97
+ };
98
+ slider.on('scroll', debouncedUpdate);
99
+
100
+
101
+ };
102
+ }
@@ -0,0 +1,27 @@
1
+ /* --------------------------------------------------------------
2
+ # FadePlugin
3
+ -------------------------------------------------------------- */
4
+
5
+ :root {
6
+ --overflow-slider-fade-color: #fff;
7
+ --overflow-slider-fade-width: 3rem;
8
+ }
9
+
10
+ .overflow-slider-fade {
11
+ position: absolute;
12
+ top: 0;
13
+ width: var(--overflow-slider-fade-width);
14
+ height: 100%;
15
+ pointer-events: none;
16
+ z-index: 1;
17
+ }
18
+
19
+ .overflow-slider-fade--start {
20
+ left: 0;
21
+ background: linear-gradient(to right, var(--overflow-slider-fade-color) 0%, transparent 100%);
22
+ }
23
+
24
+ .overflow-slider-fade--end {
25
+ right: 0;
26
+ background: linear-gradient(to left, var(--overflow-slider-fade-color) 0%, transparent 100%);
27
+ }
@@ -34,6 +34,7 @@ export default function FullWidthPlugin( args: { [key: string]: any } ) {
34
34
  if ( options.addMarginAfter ) {
35
35
  lastSlide.style.marginRight = `${marginAmount}px`;
36
36
  }
37
+ slider.container.setAttribute( 'data-full-width-offset', marginAmount.toString() );
37
38
  };
38
39
 
39
40
  update();
@@ -128,7 +128,6 @@ export default function ScrollIndicatorPlugin( args: { [key: string]: any } ) {
128
128
  scrollLeft = slider.container.scrollLeft;
129
129
  // change cursor to grabbing
130
130
  scrollbarButton.style.cursor = 'grabbing';
131
- slider.container.style.scrollSnapType = 'none';
132
131
  scrollbarButton.setAttribute( 'data-is-grabbed', 'true' );
133
132
 
134
133
  e.preventDefault();
@@ -151,7 +150,6 @@ export default function ScrollIndicatorPlugin( args: { [key: string]: any } ) {
151
150
  const onInteractionUp = () => {
152
151
  isInteractionDown = false;
153
152
  scrollbarButton.style.cursor = '';
154
- slider.container.style.scrollSnapType = '';
155
153
  scrollbarButton.setAttribute( 'data-is-grabbed', 'false' );
156
154
  };
157
155
 
@@ -40,7 +40,8 @@ export default function FullWidthPlugin( args: { [key: string]: any } ) {
40
40
  setActiveThumbnail();
41
41
  addClickListeners();
42
42
 
43
- mainSlider.on( 'activeSlideChanged', () => {
43
+ // @todo debounce on scroll
44
+ mainSlider.on( 'scrollEnd', () => {
44
45
  setTimeout(() => {
45
46
  const activeSlideIdx = mainSlider.activeSlideIdx;
46
47
  const activeThumbnail = slider.slides[activeSlideIdx] as HTMLElement;