@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.
- package/.nvmrc +1 -1
- package/README.md +115 -29
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +609 -1
- package/dist/index.esm.js.map +1 -0
- package/dist/index.min.js +2 -1
- package/dist/index.min.js.map +1 -0
- package/dist/mixins.scss +14 -0
- package/dist/overflow-slider.css +1 -1
- package/dist/plugins/arrows/index.d.ts +26 -0
- package/dist/plugins/arrows/index.min.js +1 -1
- package/dist/plugins/autoplay/index.d.ts +41 -0
- package/dist/plugins/autoplay/index.esm.js +233 -0
- package/dist/plugins/autoplay/index.min.js +1 -0
- package/dist/plugins/core/index.d.ts +75 -0
- package/dist/plugins/core/index.d2.ts +23 -0
- package/dist/plugins/dots/index.d.ts +16 -0
- package/dist/plugins/dots/index.min.js +1 -1
- package/dist/plugins/drag-scrolling/index.d.ts +9 -0
- package/dist/plugins/drag-scrolling/index.esm.js +2 -2
- package/dist/plugins/drag-scrolling/index.min.js +1 -1
- package/dist/plugins/fade/index.d.ts +16 -0
- package/dist/plugins/fade/index.min.js +1 -1
- package/dist/plugins/full-width/index.d.ts +11 -0
- package/dist/plugins/full-width/index.esm.js +14 -3
- package/dist/plugins/full-width/index.min.js +1 -1
- package/dist/plugins/infinite-scroll/index.d.ts +25 -0
- package/dist/plugins/infinite-scroll/index.esm.js +75 -0
- package/dist/plugins/infinite-scroll/index.min.js +1 -0
- package/dist/plugins/scroll-indicator/index.d.ts +14 -0
- package/dist/plugins/scroll-indicator/index.esm.js +3 -1
- package/dist/plugins/scroll-indicator/index.min.js +1 -1
- package/dist/plugins/skip-links/index.d.ts +17 -0
- package/dist/plugins/skip-links/index.esm.js +7 -1
- package/dist/plugins/skip-links/index.min.js +1 -1
- package/dist/plugins/thumbnails/index.d.ts +9 -0
- package/dist/plugins/thumbnails/index.min.js +1 -1
- package/dist/{core/utils.min.js → utils-Sxwcz8zp.js} +1 -1
- package/dist/{core/utils.esm.js → utils-ayDxlweP.js} +1 -1
- package/docs/assets/demo.css +115 -0
- package/docs/assets/demo.js +68 -0
- package/docs/dist/index.d.ts +1 -0
- package/docs/dist/index.esm.js +609 -1
- package/docs/dist/index.esm.js.map +1 -0
- package/docs/dist/index.min.js +2 -1
- package/docs/dist/index.min.js.map +1 -0
- package/docs/dist/mixins.scss +14 -0
- package/docs/dist/overflow-slider.css +1 -1
- package/docs/dist/plugins/arrows/index.d.ts +26 -0
- package/docs/dist/plugins/arrows/index.min.js +1 -1
- package/docs/dist/plugins/autoplay/index.d.ts +41 -0
- package/docs/dist/plugins/autoplay/index.esm.js +233 -0
- package/docs/dist/plugins/autoplay/index.min.js +1 -0
- package/docs/dist/plugins/core/index.d.ts +23 -0
- package/docs/dist/plugins/core/index.d2.ts +75 -0
- package/docs/dist/plugins/dots/index.d.ts +16 -0
- package/docs/dist/plugins/dots/index.min.js +1 -1
- package/docs/dist/plugins/drag-scrolling/index.d.ts +9 -0
- package/docs/dist/plugins/drag-scrolling/index.esm.js +2 -2
- package/docs/dist/plugins/drag-scrolling/index.min.js +1 -1
- package/docs/dist/plugins/fade/index.d.ts +16 -0
- package/docs/dist/plugins/fade/index.min.js +1 -1
- package/docs/dist/plugins/full-width/index.d.ts +11 -0
- package/docs/dist/plugins/full-width/index.esm.js +14 -3
- package/docs/dist/plugins/full-width/index.min.js +1 -1
- package/docs/dist/plugins/infinite-scroll/index.d.ts +25 -0
- package/docs/dist/plugins/infinite-scroll/index.esm.js +75 -0
- package/docs/dist/plugins/infinite-scroll/index.min.js +1 -0
- package/docs/dist/plugins/scroll-indicator/index.d.ts +14 -0
- package/docs/dist/plugins/scroll-indicator/index.esm.js +3 -1
- package/docs/dist/plugins/scroll-indicator/index.min.js +1 -1
- package/docs/dist/plugins/skip-links/index.d.ts +17 -0
- package/docs/dist/plugins/skip-links/index.esm.js +7 -1
- package/docs/dist/plugins/skip-links/index.min.js +1 -1
- package/docs/dist/plugins/thumbnails/index.d.ts +9 -0
- package/docs/dist/plugins/thumbnails/index.min.js +1 -1
- package/docs/dist/{core/utils.min.js → utils-Sxwcz8zp.js} +1 -1
- package/docs/dist/{core/utils.esm.js → utils-ayDxlweP.js} +1 -1
- package/docs/index-rtl.html +78 -2
- package/docs/index.html +77 -1
- package/package.json +50 -27
- package/rollup.config.js +90 -66
- package/src/core/details.ts +4 -0
- package/src/core/overflow-slider.ts +4 -2
- package/src/core/slider.ts +91 -64
- package/src/core/types.ts +29 -1
- package/src/mixins.scss +14 -0
- package/src/overflow-slider.scss +12 -10
- package/src/plugins/arrows/index.ts +2 -2
- package/src/plugins/autoplay/index.ts +276 -0
- package/src/plugins/autoplay/styles.scss +11 -0
- package/src/plugins/dots/index.ts +2 -2
- package/src/plugins/drag-scrolling/index.ts +4 -4
- package/src/plugins/fade/index.ts +2 -2
- package/src/plugins/full-width/index.ts +17 -5
- package/src/plugins/infinite-scroll/index.ts +109 -0
- package/src/plugins/scroll-indicator/index.ts +5 -3
- package/src/plugins/skip-links/index.ts +2 -2
- package/src/plugins/thumbnails/index.ts +2 -2
- package/tsconfig.json +4 -2
- package/changelog.md +0 -5
- package/dist/core/details.esm.js +0 -35
- package/dist/core/details.min.js +0 -1
- package/dist/core/overflow-slider.esm.js +0 -29
- package/dist/core/overflow-slider.min.js +0 -1
- package/dist/core/slider.esm.js +0 -499
- package/dist/core/slider.min.js +0 -1
- package/docs/dist/core/details.esm.js +0 -35
- package/docs/dist/core/details.min.js +0 -1
- package/docs/dist/core/overflow-slider.esm.js +0 -29
- package/docs/dist/core/overflow-slider.min.js +0 -1
- package/docs/dist/core/slider.esm.js +0 -499
- package/docs/dist/core/slider.min.js +0 -1
package/src/core/slider.ts
CHANGED
|
@@ -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 :
|
|
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.
|
|
204
|
-
slider.
|
|
205
|
-
slider.
|
|
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
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
477
|
-
|
|
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
|
-
|
|
520
|
+
if (targetStart == null) return;
|
|
495
521
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
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;
|
package/src/mixins.scss
ADDED
|
@@ -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
|
+
}
|
package/src/overflow-slider.scss
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
14
|
+
export default function FadePlugin( args?: DeepPartial<FadeOptions> ) {
|
|
15
15
|
return ( slider: Slider ) => {
|
|
16
16
|
|
|
17
17
|
const options = <FadeOptions>{
|