@evermade/overflow-slider 2.0.2 → 3.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 (64) hide show
  1. package/.nvmrc +1 -1
  2. package/README.md +26 -4
  3. package/dist/core/details.esm.js +4 -4
  4. package/dist/core/details.min.js +1 -1
  5. package/dist/core/overflow-slider.esm.js +2 -0
  6. package/dist/core/overflow-slider.min.js +1 -1
  7. package/dist/core/slider.esm.js +202 -29
  8. package/dist/core/slider.min.js +1 -1
  9. package/dist/core/utils.esm.js +15 -1
  10. package/dist/core/utils.min.js +1 -1
  11. package/dist/overflow-slider.css +1 -1
  12. package/dist/plugins/arrows/arrows/index.esm.js +15 -8
  13. package/dist/plugins/arrows/arrows/index.min.js +1 -1
  14. package/dist/plugins/dots/dots/index.esm.js +21 -22
  15. package/dist/plugins/dots/dots/index.min.js +1 -1
  16. package/dist/plugins/drag-scrolling/drag-scrolling/index.esm.js +30 -3
  17. package/dist/plugins/drag-scrolling/drag-scrolling/index.min.js +1 -1
  18. package/dist/plugins/fade/fade/index.esm.js +81 -0
  19. package/dist/plugins/fade/fade/index.min.js +1 -0
  20. package/dist/plugins/full-width/full-width/index.esm.js +1 -0
  21. package/dist/plugins/full-width/full-width/index.min.js +1 -1
  22. package/dist/plugins/scroll-indicator/scroll-indicator/index.esm.js +4 -6
  23. package/dist/plugins/scroll-indicator/scroll-indicator/index.min.js +1 -1
  24. package/dist/plugins/thumbnails/thumbnails/index.esm.js +1 -0
  25. package/docs/assets/demo.css +16 -0
  26. package/docs/assets/demo.js +44 -7
  27. package/docs/dist/core/details.esm.js +4 -4
  28. package/docs/dist/core/details.min.js +1 -1
  29. package/docs/dist/core/overflow-slider.esm.js +2 -0
  30. package/docs/dist/core/overflow-slider.min.js +1 -1
  31. package/docs/dist/core/slider.esm.js +202 -29
  32. package/docs/dist/core/slider.min.js +1 -1
  33. package/docs/dist/core/utils.esm.js +15 -1
  34. package/docs/dist/core/utils.min.js +1 -1
  35. package/docs/dist/overflow-slider.css +1 -1
  36. package/docs/dist/plugins/arrows/arrows/index.esm.js +15 -8
  37. package/docs/dist/plugins/arrows/arrows/index.min.js +1 -1
  38. package/docs/dist/plugins/dots/dots/index.esm.js +21 -22
  39. package/docs/dist/plugins/dots/dots/index.min.js +1 -1
  40. package/docs/dist/plugins/drag-scrolling/drag-scrolling/index.esm.js +30 -3
  41. package/docs/dist/plugins/drag-scrolling/drag-scrolling/index.min.js +1 -1
  42. package/docs/dist/plugins/fade/fade/index.esm.js +81 -0
  43. package/docs/dist/plugins/fade/fade/index.min.js +1 -0
  44. package/docs/dist/plugins/full-width/full-width/index.esm.js +1 -0
  45. package/docs/dist/plugins/full-width/full-width/index.min.js +1 -1
  46. package/docs/dist/plugins/scroll-indicator/scroll-indicator/index.esm.js +4 -6
  47. package/docs/dist/plugins/scroll-indicator/scroll-indicator/index.min.js +1 -1
  48. package/docs/dist/plugins/thumbnails/thumbnails/index.esm.js +1 -0
  49. package/docs/index.html +39 -7
  50. package/package.json +5 -1
  51. package/src/core/details.ts +4 -4
  52. package/src/core/overflow-slider.ts +2 -0
  53. package/src/core/slider.ts +226 -36
  54. package/src/core/types.ts +33 -1
  55. package/src/core/utils.ts +19 -1
  56. package/src/overflow-slider.scss +2 -1
  57. package/src/plugins/arrows/index.ts +16 -8
  58. package/src/plugins/dots/index.ts +21 -23
  59. package/src/plugins/drag-scrolling/index.ts +34 -5
  60. package/src/plugins/fade/index.ts +101 -0
  61. package/src/plugins/fade/styles.scss +27 -0
  62. package/src/plugins/full-width/index.ts +1 -0
  63. package/src/plugins/scroll-indicator/index.ts +4 -6
  64. package/src/plugins/thumbnails/index.ts +1 -1
@@ -1,6 +1,6 @@
1
1
  import { Slider, SliderOptions, SliderPlugin } from './types';
2
2
  import details from './details';
3
- import { generateId, objectsAreEqual } from './utils';
3
+ import { generateId, objectsAreEqual, getOutermostChildrenEdgeMarginSum } from './utils';
4
4
 
5
5
  export default function Slider( container: HTMLElement, options : SliderOptions, plugins? : SliderPlugin[] ) {
6
6
  let slider: Slider;
@@ -39,6 +39,7 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
39
39
  addEventListeners();
40
40
  setDataAttributes();
41
41
  setCSSVariables();
42
+
42
43
  if (plugins) {
43
44
  for (const plugin of plugins) {
44
45
  plugin(slider);
@@ -78,7 +79,96 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
78
79
  resizeObserver.observe( slider.container );
79
80
 
80
81
  // scroll event with debouncing
81
- slider.container.addEventListener('scroll', () => slider.emit('scroll'));
82
+ let scrollTimeout: ReturnType<typeof setTimeout>;
83
+ let nativeScrollTimeout: ReturnType<typeof setTimeout>;
84
+ let programmaticScrollTimeout: ReturnType<typeof setTimeout>;
85
+
86
+ let scrollLeft = slider.container.scrollLeft;
87
+ let nativeScrollLeft = slider.container.scrollLeft;
88
+ let programmaticScrollLeft = slider.container.scrollLeft;
89
+
90
+ let isScrolling = false;
91
+ let isUserScrolling = false;
92
+ let isProgrammaticScrolling = false;
93
+
94
+ // any scroll
95
+ slider.container.addEventListener('scroll', () => {
96
+ const newScrollLeft = slider.container.scrollLeft;
97
+ if ( Math.floor( scrollLeft ) !== Math.floor( newScrollLeft ) ) {
98
+ if (!isScrolling) {
99
+ isScrolling = true;
100
+ slider.emit('scrollStart');
101
+ }
102
+ scrollLeft = newScrollLeft;
103
+ clearTimeout(scrollTimeout);
104
+ scrollTimeout = setTimeout(() => {
105
+ isScrolling = false;
106
+ slider.emit('scrollEnd');
107
+ }, 50);
108
+ slider.emit('scroll');
109
+ }
110
+ // keep up nativeScrolling to take into account scroll-snap
111
+ if ( isUserScrolling ) {
112
+ nativeScrollHandler();
113
+ }
114
+ });
115
+
116
+ // user initted scroll (touchmove, mouse wheel, etc.)
117
+ const nativeScrollHandler = () => {
118
+ const newScrollLeft = slider.container.scrollLeft;
119
+ if ( Math.floor( nativeScrollLeft ) !== Math.floor( newScrollLeft ) && ! isProgrammaticScrolling ) {
120
+ if (!isUserScrolling) {
121
+ slider.emit('nativeScrollStart');
122
+ isUserScrolling = true;
123
+ }
124
+ slider.emit('nativeScroll');
125
+ nativeScrollLeft = newScrollLeft;
126
+ clearTimeout(nativeScrollTimeout);
127
+ nativeScrollTimeout = setTimeout(() => {
128
+ isUserScrolling = false;
129
+ slider.emit('nativeScrollEnd');
130
+ // update programmaticScrollLeft to match nativeScrollLeft
131
+ // this prevents programmaticScroll triggering with no real change to scrollLeft
132
+ programmaticScrollLeft = nativeScrollLeft;
133
+ }, 50);
134
+ }
135
+ };
136
+
137
+ slider.container.addEventListener('touchmove', nativeScrollHandler);
138
+ slider.container.addEventListener('mousewheel', nativeScrollHandler);
139
+ slider.container.addEventListener('wheel', nativeScrollHandler);
140
+
141
+ // programmatic scroll (scrollTo, etc.)
142
+ slider.on('programmaticScrollStart', () => {
143
+ isProgrammaticScrolling = true;
144
+ });
145
+
146
+ slider.container.addEventListener('scroll', () => {
147
+ const newScrollLeft = slider.container.scrollLeft;
148
+ if ( Math.floor( programmaticScrollLeft ) !== Math.floor( newScrollLeft ) && !isUserScrolling && isProgrammaticScrolling) {
149
+ programmaticScrollLeft = newScrollLeft;
150
+ clearTimeout(programmaticScrollTimeout);
151
+ programmaticScrollTimeout = setTimeout(() => {
152
+ isProgrammaticScrolling = false;
153
+ slider.emit('programmaticScrollEnd');
154
+ // update nativeScrollLeft to match programmaticScrollLeft
155
+ // this prevents nativeScroll triggering with no real change to scrollLeft
156
+ nativeScrollLeft = programmaticScrollLeft;
157
+ }, 50);
158
+ slider.emit('programmaticScroll');
159
+ }
160
+ });
161
+
162
+ // Fix issues on scroll snapping not working on programmatic scroll (it's not smooth)
163
+ // by disabling scroll snap if scrolling is programmatic
164
+ slider.on( 'programmaticScrollStart', () => {
165
+ slider.container.style.scrollSnapType = 'none';
166
+ } );
167
+
168
+ // restore scroll snap if user scroll starts
169
+ slider.on( 'nativeScrollStart', () => {
170
+ slider.container.style.scrollSnapType = '';
171
+ } );
82
172
 
83
173
  // Listen for mouse down and touch start events on the document
84
174
  // This handles both mouse clicks and touch interactions
@@ -128,26 +218,20 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
128
218
  const slideStart = slideRect.left - sliderRect.left + scrollLeft;
129
219
  const slideEnd = slideStart + slideRect.width;
130
220
  let scrollTarget = null;
131
- if (slideStart < scrollLeft) {
221
+ if ( Math.floor( slideStart ) < Math.floor( scrollLeft ) ) {
132
222
  scrollTarget = slideStart;
133
- } else if (slideEnd > scrollLeft + containerWidth) {
223
+ } else if ( Math.floor( slideEnd ) > Math.floor( scrollLeft ) + Math.floor( containerWidth ) ) {
134
224
  scrollTarget = slideEnd - containerWidth;
135
- } else if (slideStart === 0) {
225
+ } else if ( Math.floor( slideStart ) === 0) {
136
226
  scrollTarget = 0;
227
+ } else {
228
+ scrollTarget = slideStart;
137
229
  }
138
230
  if (scrollTarget !== null) {
139
- slider.container.style.scrollSnapType = 'none';
140
- // seems like in order for scroll behavior: smooth to work, we need to wait a bit to disable scrollSnapType
141
231
  setTimeout((scrollTarget) => {
232
+ slider.emit('programmaticScrollStart');
142
233
  slider.container.scrollTo({ left: scrollTarget, behavior: behavior });
143
234
  }, 50, scrollTarget);
144
- setTimeout(() => {
145
- // leave snapping off to fix issues with slide moving back on focus
146
- if ( behavior == 'smooth' ) {
147
- slider.container.style.scrollSnapType = '';
148
- }
149
-
150
- }, 500);
151
235
  }
152
236
  };
153
237
 
@@ -156,24 +240,34 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
156
240
  const scrollLeft = slider.container.scrollLeft;
157
241
  const slides = slider.slides;
158
242
  let activeSlideIdx = 0;
243
+ let scrolledPastLastSlide = false;
159
244
 
160
245
  for (let i = 0; i < slides.length; i++) {
161
246
  const slideRect = slides[i].getBoundingClientRect();
162
247
  const slideStart = slideRect.left - sliderRect.left + scrollLeft + getGapSize();
163
248
 
164
- if (slideStart > scrollLeft) {
249
+ if ( Math.floor( slideStart ) >= Math.floor( scrollLeft ) ) {
165
250
  activeSlideIdx = i;
166
251
  break;
167
252
  }
253
+ if ( i === slides.length - 1 ) {
254
+ scrolledPastLastSlide = true;
255
+ }
256
+ }
257
+
258
+ if ( scrolledPastLastSlide ) {
259
+ activeSlideIdx = slides.length - 1;
168
260
  }
169
261
 
170
262
  const oldActiveSlideIdx = slider.activeSlideIdx;
171
263
  slider.activeSlideIdx = activeSlideIdx;
264
+
172
265
  if (oldActiveSlideIdx !== activeSlideIdx) {
173
266
  slider.emit('activeSlideChanged');
174
267
  }
175
268
  }
176
269
 
270
+
177
271
  function moveToSlide( idx: number ) {
178
272
  const slide = slider.slides[idx];
179
273
  if (slide) {
@@ -181,15 +275,32 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
181
275
  }
182
276
  };
183
277
 
278
+ function getInclusiveScrollWidth() : number {
279
+ return slider.container.scrollWidth + getOutermostChildrenEdgeMarginSum(slider.container);
280
+ };
281
+
282
+ function getInclusiveClientWidth() : number {
283
+ return slider.container.clientWidth + getOutermostChildrenEdgeMarginSum(slider.container);
284
+ }
285
+
184
286
  function getGapSize() : number {
185
287
  let gapSize = 0;
186
288
  if (slider.slides.length > 1) {
187
289
  const firstSlideRect = slider.slides[0].getBoundingClientRect();
188
290
  const secondSlideRect = slider.slides[1].getBoundingClientRect();
189
- gapSize = secondSlideRect.left - firstSlideRect.right;
291
+ gapSize = Math.floor( secondSlideRect.left - firstSlideRect.right );
190
292
  }
191
293
  return gapSize;
192
- }
294
+ };
295
+
296
+ function getLeftOffset() : number {
297
+ let offset = 0;
298
+ const fullWidthOffset = slider.container.getAttribute('data-full-width-offset');
299
+ if (fullWidthOffset) {
300
+ offset = parseInt(fullWidthOffset);
301
+ }
302
+ return Math.floor( offset );
303
+ };
193
304
 
194
305
  function moveToDirection(direction = "prev") {
195
306
  const scrollStrategy = slider.options.scrollStrategy;
@@ -200,16 +311,16 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
200
311
  if (direction === 'prev') {
201
312
  targetScrollPosition = Math.max(0, scrollLeft - slider.container.offsetWidth);
202
313
  } else if (direction === 'next') {
203
- targetScrollPosition = Math.min(slider.container.scrollWidth, scrollLeft + slider.container.offsetWidth);
314
+ targetScrollPosition = Math.min(slider.getInclusiveScrollWidth(), scrollLeft + slider.container.offsetWidth);
204
315
  }
205
316
  if (scrollStrategy === 'fullSlide') {
206
- let fullSldeTargetScrollPosition = null;
317
+ let fullSlideTargetScrollPosition = null;
207
318
 
208
319
  // extend targetScrollPosition to include gap
209
320
  if (direction === 'prev') {
210
- fullSldeTargetScrollPosition = Math.max(0, targetScrollPosition - getGapSize());
321
+ fullSlideTargetScrollPosition = Math.max(0, targetScrollPosition - getGapSize());
211
322
  } else {
212
- fullSldeTargetScrollPosition = Math.min(slider.container.scrollWidth, targetScrollPosition + getGapSize());
323
+ fullSlideTargetScrollPosition = Math.min(slider.getInclusiveScrollWidth(), targetScrollPosition + getGapSize());
213
324
  }
214
325
 
215
326
  if (direction === 'next') {
@@ -218,49 +329,125 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
218
329
  const slideRect = slide.getBoundingClientRect();
219
330
  const slideStart = slideRect.left - sliderRect.left + scrollLeft;
220
331
  const slideEnd = slideStart + slideRect.width;
221
- if (slideStart < targetScrollPosition && slideEnd > targetScrollPosition) {
222
- fullSldeTargetScrollPosition = slideStart;
332
+ if ( Math.floor( slideStart ) < Math.floor( targetScrollPosition ) && Math.floor( slideEnd ) > Math.floor( targetScrollPosition ) ) {
333
+ fullSlideTargetScrollPosition = slideStart;
223
334
  partialSlideFound = true;
224
335
  break;
225
336
  }
226
337
  }
227
- if (!partialSlideFound) {
228
- fullSldeTargetScrollPosition = Math.min(targetScrollPosition, slider.container.scrollWidth - slider.container.offsetWidth);
338
+ if ( ! partialSlideFound ) {
339
+ fullSlideTargetScrollPosition = Math.min(targetScrollPosition, slider.getInclusiveScrollWidth() - slider.container.offsetWidth);
229
340
  }
230
- if (fullSldeTargetScrollPosition && fullSldeTargetScrollPosition > scrollLeft) {
231
- targetScrollPosition = fullSldeTargetScrollPosition;
341
+ if ( fullSlideTargetScrollPosition ) {
342
+ if ( Math.floor( fullSlideTargetScrollPosition ) > Math.floor( scrollLeft ) ) {
343
+ // make sure fullSlideTargetScrollPosition is possible considering the container width
344
+ const maxScrollPosition = Math.floor( slider.getInclusiveScrollWidth() ) - Math.floor( containerWidth );
345
+ targetScrollPosition = Math.min( fullSlideTargetScrollPosition, maxScrollPosition );
346
+ } else {
347
+ // cannot snap to slide, move one page worth of distance
348
+ targetScrollPosition = Math.min(slider.getInclusiveScrollWidth(), scrollLeft + containerWidth);
349
+ }
232
350
  }
351
+
233
352
  } else {
234
353
  let partialSlideFound = false;
235
354
  for (let slide of slider.slides) {
236
355
  const slideRect = slide.getBoundingClientRect();
237
356
  const slideStart = slideRect.left - sliderRect.left + scrollLeft;
238
357
  const slideEnd = slideStart + slideRect.width;
239
- if (slideStart < scrollLeft && slideEnd > scrollLeft) {
240
- fullSldeTargetScrollPosition = slideEnd - containerWidth;
358
+ if ( Math.floor( slideStart ) < Math.floor( scrollLeft ) && Math.floor( slideEnd ) > Math.floor( scrollLeft ) ) {
359
+ fullSlideTargetScrollPosition = slideEnd - containerWidth;
241
360
  partialSlideFound = true;
242
361
  break;
243
362
  }
244
363
  }
245
- if (!partialSlideFound) {
246
- fullSldeTargetScrollPosition = Math.max(0, scrollLeft - containerWidth);
364
+ if ( ! partialSlideFound ) {
365
+ fullSlideTargetScrollPosition = Math.max(0, scrollLeft - containerWidth);
247
366
  }
248
- if (fullSldeTargetScrollPosition && fullSldeTargetScrollPosition < scrollLeft) {
249
- targetScrollPosition = fullSldeTargetScrollPosition;
367
+ if ( fullSlideTargetScrollPosition && Math.floor( fullSlideTargetScrollPosition ) < Math.floor( scrollLeft ) ) {
368
+ targetScrollPosition = fullSlideTargetScrollPosition;
250
369
  }
251
370
  }
252
371
  }
372
+
373
+ // add left offset
374
+ const offsettedTargetScrollPosition = targetScrollPosition - getLeftOffset();
375
+ if ( Math.floor( offsettedTargetScrollPosition ) >= 0) {
376
+ targetScrollPosition = offsettedTargetScrollPosition;
377
+ }
378
+
379
+ slider.emit('programmaticScrollStart');
253
380
  slider.container.style.scrollBehavior = slider.options.scrollBehavior;
254
381
  slider.container.scrollLeft = targetScrollPosition;
255
382
  setTimeout(() => slider.container.style.scrollBehavior = '', 50);
256
- }
383
+ };
384
+
385
+ function snapToClosestSlide(direction = "prev") {
386
+ const isMovingForward = direction === 'next';
387
+ const slideReference = [];
388
+ for (let i = 0; i < slider.slides.length; i++) {
389
+ const slide = slider.slides[i];
390
+ const slideWidth = slide.offsetWidth;
391
+ const slideStart = slide.offsetLeft;
392
+ const slideEnd = slideStart + slideWidth;
393
+ const slideMiddle = slideStart + slideWidth / 2;
394
+ const trigger = Math.min(slideMiddle, slideStart + slider.options.emulateScrollSnapMaxThreshold);
395
+ slideReference.push({
396
+ start: slideStart,
397
+ middle: slideMiddle,
398
+ end: slideEnd,
399
+ width: slideWidth,
400
+ trigger: trigger,
401
+ slide: slide,
402
+ });
403
+ }
404
+ let snapTarget = null;
405
+ const scrollPosition = slider.container.scrollLeft;
406
+ if (isMovingForward) {
407
+ for (let i = 0; i < slideReference.length; i++) {
408
+ const item = slideReference[i];
409
+ if ( i === 0 && Math.floor( scrollPosition ) <= Math.floor( item.trigger ) ) {
410
+ snapTarget = 0;
411
+ break;
412
+ }
413
+ if ( Math.floor( slider.container.scrollLeft ) <= Math.floor( item.trigger ) ) {
414
+ snapTarget = item.start;
415
+ break;
416
+ }
417
+ }
418
+ } else {
419
+ for (let i = slideReference.length - 1; i >= 0; i--) {
420
+ const item = slideReference[i];
421
+ if ( i === slideReference.length - 1 && Math.floor( scrollPosition ) >= Math.floor( item.trigger ) ) {
422
+ snapTarget = item.start;
423
+ break;
424
+ }
425
+ if ( Math.floor( slider.container.scrollLeft ) >= Math.floor( item.trigger ) ) {
426
+ snapTarget = item.start;
427
+ break;
428
+ }
429
+ }
430
+ }
431
+ if ( snapTarget !== null ) {
432
+ const offsettedSnapTarget = snapTarget - getLeftOffset();
433
+ if ( Math.floor( offsettedSnapTarget ) >= 0 ) {
434
+ snapTarget = offsettedSnapTarget;
435
+ }
436
+
437
+ const scrollBehavior = slider.options.scrollBehavior || 'smooth';
438
+ slider.container.scrollTo({
439
+ left: snapTarget,
440
+ behavior: scrollBehavior as ScrollBehavior
441
+ });
442
+ }
443
+ };
257
444
 
258
445
  function on(name: string, cb: any) {
259
446
  if (!subs[name]) {
260
447
  subs[name] = [];
261
448
  }
262
449
  subs[name].push(cb);
263
- }
450
+ };
264
451
 
265
452
  function emit(name: string) {
266
453
  if (subs && subs[name]) {
@@ -272,12 +459,15 @@ export default function Slider( container: HTMLElement, options : SliderOptions,
272
459
  if (typeof optionCallBack === 'function') {
273
460
  optionCallBack(slider);
274
461
  }
275
- }
462
+ };
276
463
 
277
464
  slider = <Slider>{
278
465
  emit,
279
466
  moveToDirection,
280
467
  moveToSlide,
468
+ snapToClosestSlide,
469
+ getInclusiveScrollWidth,
470
+ getInclusiveClientWidth,
281
471
  on,
282
472
  options,
283
473
  };
package/src/core/types.ts CHANGED
@@ -5,9 +5,14 @@ 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
14
+ getInclusiveScrollWidth: () => number
15
+ getInclusiveClientWidth: () => number
11
16
  on: (
12
17
  name: H | SliderHooks,
13
18
  cb: (props: Slider<O, C, H>) => void
@@ -21,6 +26,8 @@ export type SliderOptions = {
21
26
  scrollBehavior: string;
22
27
  scrollStrategy: string;
23
28
  slidesSelector: string;
29
+ emulateScrollSnap: boolean;
30
+ emulateScrollSnapMaxThreshold: number;
24
31
  [key: string]: any;
25
32
  }
26
33
 
@@ -38,7 +45,16 @@ export type SliderHooks =
38
45
  | HOOK_CONTENTS_CHANGED
39
46
  | HOOK_DETAILS_CHANGED
40
47
  | HOOK_CONTAINER_SIZE_CHANGED
41
- | HOOK_ACTIVE_SLIDE_CHANGED;
48
+ | HOOK_ACTIVE_SLIDE_CHANGED
49
+ | HOOK_SCROLL_START
50
+ | HOOK_SCROLL
51
+ | HOOK_SCROLL_END
52
+ | HOOK_NATIVE_SCROLL_START
53
+ | HOOK_NATIVE_SCROLL
54
+ | HOOK_NATIVE_SCROLL_END
55
+ | HOOK_PROGRAMMATIC_SCROLL_START
56
+ | HOOK_PROGRAMMATIC_SCROLL
57
+ | HOOK_PROGRAMMATIC_SCROLL_END;
42
58
 
43
59
  export type HOOK_CREATED = 'created';
44
60
  export type HOOK_DETAILS_CHANGED = 'detailsChanged';
@@ -46,4 +62,20 @@ export type HOOK_CONTENTS_CHANGED = 'contentsChanged';
46
62
  export type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged';
47
63
  export type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged';
48
64
 
65
+ // any type of scroll
66
+ export type HOOK_SCROLL_START = 'scrollStart';
67
+ export type HOOK_SCROLL = 'scroll';
68
+ export type HOOK_SCROLL_END = 'scrollEnd';
69
+
70
+ // user initted scroll (touch, mouse wheel, etc.)
71
+ export type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart';
72
+ export type HOOK_NATIVE_SCROLL = 'nativeScroll';
73
+ export type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd';
74
+
75
+ // programmatic scroll (e.g. el.scrollTo)
76
+ export type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart';
77
+ export type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll';
78
+ export type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd';
79
+
80
+
49
81
  export type SliderPlugin = (slider: Slider) => void;
package/src/core/utils.ts CHANGED
@@ -21,4 +21,22 @@ function objectsAreEqual( obj1: any, obj2: any ) {
21
21
  return true;
22
22
  }
23
23
 
24
- export { generateId, objectsAreEqual };
24
+ function getOutermostChildrenEdgeMarginSum( el: HTMLElement ): number {
25
+ if (el.children.length === 0) {
26
+ return 0;
27
+ }
28
+
29
+ // get the first child and its left margin
30
+ const firstChild = el.children[0];
31
+ const firstChildStyle = getComputedStyle(firstChild);
32
+ const firstChildMarginLeft = parseFloat(firstChildStyle.marginLeft);
33
+
34
+ // Get the last child and its right margin
35
+ const lastChild = el.children[el.children.length - 1];
36
+ const lastChildStyle = getComputedStyle(lastChild);
37
+ const lastChildMarginRight = parseFloat(lastChildStyle.marginRight);
38
+
39
+ return firstChildMarginLeft + lastChildMarginRight;
40
+ }
41
+
42
+ export { generateId, objectsAreEqual, getOutermostChildrenEdgeMarginSum };
@@ -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';
@@ -66,7 +66,11 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
66
66
  prev.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? '');
67
67
  prev.setAttribute( 'data-type', 'prev' );
68
68
  prev.innerHTML = options.icons.prev;
69
- prev.addEventListener( 'click', () => slider.moveToDirection( 'prev' ) );
69
+ prev.addEventListener( 'click', () => {
70
+ if ( prev.getAttribute('data-has-content') === 'true' ) {
71
+ slider.moveToDirection( 'prev' );
72
+ }
73
+ } );
70
74
 
71
75
  const next = document.createElement( 'button' );
72
76
  next.setAttribute( 'class', options.classNames.nextButton );
@@ -75,7 +79,11 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
75
79
  next.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? '');
76
80
  next.setAttribute( 'data-type', 'next' );
77
81
  next.innerHTML = options.icons.next;
78
- next.addEventListener( 'click', () => slider.moveToDirection( 'next' ) );
82
+ next.addEventListener( 'click', () => {
83
+ if ( next.getAttribute('data-has-content') === 'true' ) {
84
+ slider.moveToDirection( 'next' );
85
+ }
86
+ } );
79
87
 
80
88
  // insert buttons to the nav
81
89
  nav.appendChild( prev );
@@ -83,15 +91,15 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
83
91
 
84
92
  const update = () => {
85
93
  const scrollLeft = slider.container.scrollLeft;
86
- const scrollWidth = slider.container.scrollWidth;
87
- const clientWidth = slider.container.clientWidth;
88
- const buffer = 1;
89
- if ( scrollLeft === 0 ) {
94
+ const scrollWidth = slider.getInclusiveScrollWidth();
95
+ const clientWidth = slider.getInclusiveClientWidth();
96
+ const buffer = 0;
97
+ if ( Math.floor( scrollLeft ) === 0 ) {
90
98
  prev.setAttribute( 'data-has-content', 'false' );
91
99
  } else {
92
100
  prev.setAttribute( 'data-has-content', 'true' );
93
101
  }
94
- if ( scrollLeft + clientWidth >= scrollWidth - buffer ) {
102
+ if ( Math.floor( scrollLeft + clientWidth ) >= Math.floor( scrollWidth ) ) {
95
103
  next.setAttribute( 'data-has-content', 'false' );
96
104
  } else {
97
105
  next.setAttribute( 'data-has-content', 'true' );
@@ -110,7 +118,7 @@ export default function ArrowsPlugin( args: { [key: string]: any } ) {
110
118
  }
111
119
 
112
120
  update();
113
- slider.on( 'scroll', update );
121
+ slider.on( 'scrollEnd', update );
114
122
  slider.on( 'contentsChanged', update );
115
123
  slider.on( 'containerSizeChanged', update );
116
124
  };
@@ -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
  };