@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.
- package/.nvmrc +1 -1
- package/README.md +158 -33
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +645 -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/classnames/index.d.ts +14 -0
- package/dist/plugins/classnames/index.esm.js +108 -0
- package/dist/plugins/classnames/index.min.js +1 -0
- package/dist/plugins/core/index.d.ts +76 -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 +37 -9
- 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 +156 -0
- package/docs/assets/demo.js +92 -8
- package/docs/dist/index.d.ts +1 -0
- package/docs/dist/index.esm.js +645 -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/classnames/index.d.ts +14 -0
- package/docs/dist/plugins/classnames/index.esm.js +108 -0
- package/docs/dist/plugins/classnames/index.min.js +1 -0
- package/docs/dist/plugins/core/index.d.ts +76 -0
- package/docs/dist/plugins/core/index.d2.ts +23 -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 +37 -9
- 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 +86 -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 +127 -62
- package/src/core/types.ts +30 -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/classnames/index.ts +147 -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 +43 -11
- 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,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 :
|
|
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.
|
|
204
|
-
slider.
|
|
205
|
-
slider.
|
|
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
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
|
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;
|
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
|
+
}
|