├── .nvmrc ├── dist ├── index.d.ts ├── plugins │ ├── thumbnails │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── drag-scrolling │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── full-width │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── classnames │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── dots │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── fade │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── scroll-indicator │ │ ├── index.d.ts │ │ └── index.min.js │ ├── skip-links │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── infinite-scroll │ │ ├── index.min.js │ │ ├── index.d.ts │ │ └── index.esm.js │ ├── arrows │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── core │ │ ├── index.d2.ts │ │ └── index.d.ts │ └── autoplay │ │ ├── index.d.ts │ │ └── index.min.js ├── mixins.scss ├── utils-Sxwcz8zp.js ├── utils-ayDxlweP.js └── overflow-slider.css ├── docs └── dist │ ├── index.d.ts │ ├── plugins │ ├── thumbnails │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── drag-scrolling │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── full-width │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── classnames │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── dots │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── fade │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── scroll-indicator │ │ ├── index.d.ts │ │ └── index.min.js │ ├── skip-links │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── infinite-scroll │ │ ├── index.min.js │ │ ├── index.d.ts │ │ └── index.esm.js │ ├── arrows │ │ ├── index.d.ts │ │ ├── index.min.js │ │ └── index.esm.js │ ├── core │ │ ├── index.d2.ts │ │ └── index.d.ts │ └── autoplay │ │ ├── index.d.ts │ │ └── index.min.js │ ├── mixins.scss │ ├── utils-Sxwcz8zp.js │ ├── utils-ayDxlweP.js │ └── overflow-slider.css ├── src ├── index.ts ├── plugins │ ├── autoplay │ │ └── styles.scss │ ├── drag-scrolling │ │ ├── styles.scss │ │ └── index.ts │ ├── arrows │ │ ├── styles.scss │ │ └── index.ts │ ├── skip-links │ │ ├── styles.scss │ │ └── index.ts │ ├── fade │ │ ├── styles.scss │ │ └── index.ts │ ├── dots │ │ ├── styles.scss │ │ └── index.ts │ ├── thumbnails │ │ └── index.ts │ ├── scroll-indicator │ │ ├── styles.scss │ │ └── index.ts │ ├── full-width │ │ └── index.ts │ └── classnames │ │ └── index.ts ├── mixins.scss ├── overflow-slider.scss └── core │ ├── overflow-slider.ts │ ├── utils.ts │ ├── details.ts │ └── types.ts ├── tsconfig.json ├── .gitignore ├── .editorconfig ├── .github └── workflows │ └── npm-publish.yml ├── LICENSE ├── rollup.config.js └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 2 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as OverflowSlider } from './plugins/core/index.d2.ts'; 2 | -------------------------------------------------------------------------------- /docs/dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export { default as OverflowSlider } from './plugins/core/index.d2.ts'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import './overflow-slider.scss'; 2 | 3 | import OverflowSlider from './core/overflow-slider'; 4 | 5 | export { 6 | OverflowSlider, 7 | }; 8 | -------------------------------------------------------------------------------- /dist/plugins/thumbnails/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ThumbnailsOptions = { 4 | mainSlider: Slider; 5 | }; 6 | declare function FullWidthPlugin(args: DeepPartial): (slider: Slider) => void; 7 | 8 | export { FullWidthPlugin as default }; 9 | export type { ThumbnailsOptions }; 10 | -------------------------------------------------------------------------------- /docs/dist/plugins/thumbnails/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ThumbnailsOptions = { 4 | mainSlider: Slider; 5 | }; 6 | declare function FullWidthPlugin(args: DeepPartial): (slider: Slider) => void; 7 | 8 | export { FullWidthPlugin as default }; 9 | export type { ThumbnailsOptions }; 10 | -------------------------------------------------------------------------------- /src/plugins/autoplay/styles.scss: -------------------------------------------------------------------------------- 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 | } 12 | -------------------------------------------------------------------------------- /dist/plugins/drag-scrolling/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type DragScrollingOptions = { 4 | draggedDistanceThatPreventsClick: number; 5 | }; 6 | declare function DragScrollingPlugin(args?: DeepPartial): (slider: Slider) => void; 7 | 8 | export { DragScrollingPlugin as default }; 9 | export type { DragScrollingOptions }; 10 | -------------------------------------------------------------------------------- /docs/dist/plugins/drag-scrolling/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type DragScrollingOptions = { 4 | draggedDistanceThatPreventsClick: number; 5 | }; 6 | declare function DragScrollingPlugin(args?: DeepPartial): (slider: Slider) => void; 7 | 8 | export { DragScrollingPlugin as default }; 9 | export type { DragScrollingOptions }; 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es6", 5 | "module": "ESNext", 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "sourceMap": false 9 | }, 10 | "include": [ 11 | "src/**/*.ts" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "dist", 16 | "**/*.spec.ts", 17 | "**/*.scss", 18 | "*.scss" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /dist/plugins/full-width/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type FullWidthOptions = { 4 | targetWidth?: (slider: Slider) => number; 5 | addMarginBefore: boolean; 6 | addMarginAfter: boolean; 7 | }; 8 | declare function FullWidthPlugin(args?: DeepPartial): (slider: Slider) => void; 9 | 10 | export { FullWidthPlugin as default }; 11 | export type { FullWidthOptions }; 12 | -------------------------------------------------------------------------------- /src/plugins/drag-scrolling/styles.scss: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | # DragScrollingPlugin 3 | -------------------------------------------------------------- */ 4 | 5 | [data-has-drag-scrolling] { 6 | &[data-has-overflow="true"] { 7 | // show grab cursor if has drag scrolling 8 | cursor: grab; 9 | // selecting text might make scrolling difficult 10 | user-select: none; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/dist/plugins/full-width/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type FullWidthOptions = { 4 | targetWidth?: (slider: Slider) => number; 5 | addMarginBefore: boolean; 6 | addMarginAfter: boolean; 7 | }; 8 | declare function FullWidthPlugin(args?: DeepPartial): (slider: Slider) => void; 9 | 10 | export { FullWidthPlugin as default }; 11 | export type { FullWidthOptions }; 12 | -------------------------------------------------------------------------------- /dist/mixins.scss: -------------------------------------------------------------------------------- 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 | } 15 | -------------------------------------------------------------------------------- /src/mixins.scss: -------------------------------------------------------------------------------- 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 | } 15 | -------------------------------------------------------------------------------- /docs/dist/mixins.scss: -------------------------------------------------------------------------------- 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 | } 15 | -------------------------------------------------------------------------------- /dist/plugins/classnames/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ClassnameOptions = { 4 | classNames: { 5 | visible: string; 6 | partlyVisible: string; 7 | hidden: string; 8 | }; 9 | freezeStateOnVisible: boolean; 10 | }; 11 | declare function ClassNamesPlugin(args?: DeepPartial): (slider: Slider) => void; 12 | 13 | export { ClassNamesPlugin as default }; 14 | export type { ClassnameOptions }; 15 | -------------------------------------------------------------------------------- /docs/dist/plugins/classnames/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ClassnameOptions = { 4 | classNames: { 5 | visible: string; 6 | partlyVisible: string; 7 | hidden: string; 8 | }; 9 | freezeStateOnVisible: boolean; 10 | }; 11 | declare function ClassNamesPlugin(args?: DeepPartial): (slider: Slider) => void; 12 | 13 | export { ClassNamesPlugin as default }; 14 | export type { ClassnameOptions }; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific ignores 2 | node_modules/ 3 | .eslintcache 4 | .stylelintcache 5 | 6 | # Compiled source 7 | *.com 8 | *.class 9 | *.dll 10 | *.exe 11 | *.o 12 | *.so 13 | 14 | # Packages 15 | *.7z 16 | *.dmg 17 | *.gz 18 | *.iso 19 | *.jar 20 | *.rar 21 | *.tar 22 | *.zip 23 | 24 | # Logs and databases 25 | *.log 26 | *.sql 27 | *.sqlite 28 | logs/ 29 | 30 | # OS generated files 31 | .DS_Store 32 | .DS_Store? 33 | ._* 34 | *._* 35 | .Spotlight-V100 36 | .Trashes 37 | ebs.db 38 | Thumbs.db 39 | -------------------------------------------------------------------------------- /dist/utils-Sxwcz8zp.js: -------------------------------------------------------------------------------- 1 | function t(e,n=1){const r=`${e}-${n}`;return document.getElementById(r)?t(e,n+1):r}function e(t,e){const n=Object.keys(t),r=Object.keys(e);if(n.length!==r.length)return!1;for(let r of n)if(!Object.prototype.hasOwnProperty.call(e,r)||t[r]!==e[r])return!1;return!0}function n(t){if(0===t.children.length)return 0;const e=t.children[0],n=getComputedStyle(e),r=parseFloat(n.marginLeft),o=t.children[t.children.length-1],l=getComputedStyle(o);return r+parseFloat(l.marginRight)}export{n as a,t as g,e as o}; 2 | -------------------------------------------------------------------------------- /dist/plugins/thumbnails/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e){return i=>{const t={mainSlider:e.mainSlider}.mainSlider,l=(e=null)=>{null===e&&i.slides.length>0&&(e=i.slides[0]),null!==e&&(i.slides.forEach(e=>{e.setAttribute("aria-current","false")}),e.setAttribute("aria-current","true"))};l(),i.slides.forEach((e,i)=>{e.addEventListener("click",()=>{t.moveToSlide(i),l(e)})}),t.on("scrollEnd",()=>{setTimeout(()=>{const e=t.activeSlideIdx;if(i.activeSlideIdx===e)return;const r=i.slides[e];l(r),i.moveToSlide(e)},50)})}}export{e as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/utils-Sxwcz8zp.js: -------------------------------------------------------------------------------- 1 | function t(e,n=1){const r=`${e}-${n}`;return document.getElementById(r)?t(e,n+1):r}function e(t,e){const n=Object.keys(t),r=Object.keys(e);if(n.length!==r.length)return!1;for(let r of n)if(!Object.prototype.hasOwnProperty.call(e,r)||t[r]!==e[r])return!1;return!0}function n(t){if(0===t.children.length)return 0;const e=t.children[0],n=getComputedStyle(e),r=parseFloat(n.marginLeft),o=t.children[t.children.length-1],l=getComputedStyle(o);return r+parseFloat(l.marginRight)}export{n as a,t as g,e as o}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/thumbnails/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e){return i=>{const t={mainSlider:e.mainSlider}.mainSlider,l=(e=null)=>{null===e&&i.slides.length>0&&(e=i.slides[0]),null!==e&&(i.slides.forEach(e=>{e.setAttribute("aria-current","false")}),e.setAttribute("aria-current","true"))};l(),i.slides.forEach((e,i)=>{e.addEventListener("click",()=>{t.moveToSlide(i),l(e)})}),t.on("scrollEnd",()=>{setTimeout(()=>{const e=t.activeSlideIdx;if(i.activeSlideIdx===e)return;const r=i.slides[e];l(r),i.moveToSlide(e)},50)})}}export{e as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/dots/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type DotsOptions = { 4 | type: 'view' | 'slide'; 5 | texts: { 6 | dotDescription: string; 7 | }; 8 | classNames: { 9 | dotsContainer: string; 10 | dotsItem: string; 11 | }; 12 | container: HTMLElement | null; 13 | }; 14 | declare function DotsPlugin(args?: DeepPartial): (slider: Slider) => void; 15 | 16 | export { DotsPlugin as default }; 17 | export type { DotsOptions }; 18 | -------------------------------------------------------------------------------- /docs/dist/plugins/dots/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type DotsOptions = { 4 | type: 'view' | 'slide'; 5 | texts: { 6 | dotDescription: string; 7 | }; 8 | classNames: { 9 | dotsContainer: string; 10 | dotsItem: string; 11 | }; 12 | container: HTMLElement | null; 13 | }; 14 | declare function DotsPlugin(args?: DeepPartial): (slider: Slider) => void; 15 | 16 | export { DotsPlugin as default }; 17 | export type { DotsOptions }; 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = tab 12 | insert_final_newline = true 13 | 14 | # Get rid of whitespace to avoid diffs with a bunch of EOL changes 15 | trim_trailing_whitespace = true 16 | 17 | [*.md] 18 | max_line_length = 0 19 | trim_trailing_whitespace = false 20 | 21 | [{*.css,*.scss}] 22 | indent_size = 4 23 | -------------------------------------------------------------------------------- /dist/plugins/fade/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type FadeOptions = { 4 | classNames: { 5 | fadeItem: string; 6 | fadeItemStart: string; 7 | fadeItemEnd: string; 8 | }; 9 | container: HTMLElement | null; 10 | containerStart: HTMLElement | null; 11 | containerEnd: HTMLElement | null; 12 | }; 13 | declare function FadePlugin(args?: DeepPartial): (slider: Slider) => void; 14 | 15 | export { FadePlugin as default }; 16 | export type { FadeOptions }; 17 | -------------------------------------------------------------------------------- /dist/plugins/scroll-indicator/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ScrollIndicatorOptions = { 4 | classNames: { 5 | scrollIndicator: string; 6 | scrollIndicatorBar: string; 7 | scrollIndicatorButton: string; 8 | }; 9 | container: HTMLElement | null; 10 | }; 11 | declare function ScrollIndicatorPlugin(args?: DeepPartial): (slider: Slider) => void; 12 | 13 | export { ScrollIndicatorPlugin as default }; 14 | export type { ScrollIndicatorOptions }; 15 | -------------------------------------------------------------------------------- /docs/dist/plugins/fade/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type FadeOptions = { 4 | classNames: { 5 | fadeItem: string; 6 | fadeItemStart: string; 7 | fadeItemEnd: string; 8 | }; 9 | container: HTMLElement | null; 10 | containerStart: HTMLElement | null; 11 | containerEnd: HTMLElement | null; 12 | }; 13 | declare function FadePlugin(args?: DeepPartial): (slider: Slider) => void; 14 | 15 | export { FadePlugin as default }; 16 | export type { FadeOptions }; 17 | -------------------------------------------------------------------------------- /docs/dist/plugins/scroll-indicator/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ScrollIndicatorOptions = { 4 | classNames: { 5 | scrollIndicator: string; 6 | scrollIndicatorBar: string; 7 | scrollIndicatorButton: string; 8 | }; 9 | container: HTMLElement | null; 10 | }; 11 | declare function ScrollIndicatorPlugin(args?: DeepPartial): (slider: Slider) => void; 12 | 13 | export { ScrollIndicatorPlugin as default }; 14 | export type { ScrollIndicatorOptions }; 15 | -------------------------------------------------------------------------------- /dist/plugins/skip-links/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type SkipLinkOptions = { 4 | texts: { 5 | skipList: string; 6 | }; 7 | classNames: { 8 | skipLink: string; 9 | skipLinkTarget: string; 10 | }; 11 | containerBefore: HTMLElement | null; 12 | containerAfter: HTMLElement | null; 13 | }; 14 | declare function SkipLinksPlugin(args?: DeepPartial): (slider: Slider) => void; 15 | 16 | export { SkipLinksPlugin as default }; 17 | export type { SkipLinkOptions }; 18 | -------------------------------------------------------------------------------- /docs/dist/plugins/skip-links/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type SkipLinkOptions = { 4 | texts: { 5 | skipList: string; 6 | }; 7 | classNames: { 8 | skipLink: string; 9 | skipLinkTarget: string; 10 | }; 11 | containerBefore: HTMLElement | null; 12 | containerAfter: HTMLElement | null; 13 | }; 14 | declare function SkipLinksPlugin(args?: DeepPartial): (slider: Slider) => void; 15 | 16 | export { SkipLinksPlugin as default }; 17 | export type { SkipLinkOptions }; 18 | -------------------------------------------------------------------------------- /dist/plugins/infinite-scroll/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e={}){const{lookAheadCount:t=1}=e;return e=>{const{container:l,options:o}=e;function n(e,l){return(l?e.slice(-t):e.slice(0,t)).reduce((e,t)=>e+t.offsetWidth,0)}e.on("scrollEnd",function(){const i=e.activeSlideIdx,r=e.getScrollLeft(),c=e.getInclusiveClientWidth(),s=e.getInclusiveScrollWidth();let d=Array.from(l.querySelectorAll(o.slidesSelector));if(0===d.length)return;const f=d[i],u=n(d,!1),S=n(d,!0);if(r+c>=s-u)for(let e=0;e=0&&e.canMoveToSlide(a)?e.moveToSlide(a):e.snapToClosestSlide("next")})}}export{e as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/infinite-scroll/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e={}){const{lookAheadCount:t=1}=e;return e=>{const{container:l,options:o}=e;function n(e,l){return(l?e.slice(-t):e.slice(0,t)).reduce((e,t)=>e+t.offsetWidth,0)}e.on("scrollEnd",function(){const i=e.activeSlideIdx,r=e.getScrollLeft(),c=e.getInclusiveClientWidth(),s=e.getInclusiveScrollWidth();let d=Array.from(l.querySelectorAll(o.slidesSelector));if(0===d.length)return;const f=d[i],u=n(d,!1),S=n(d,!0);if(r+c>=s-u)for(let e=0;e=0&&e.canMoveToSlide(a)?e.moveToSlide(a):e.snapToClosestSlide("next")})}}export{e as default}; 2 | -------------------------------------------------------------------------------- /src/plugins/arrows/styles.scss: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | # ArrowsPlugin 3 | -------------------------------------------------------------- */ 4 | 5 | :root { 6 | --overflow-slider-arrows-size: 1.5rem; 7 | --overflow-slider-arrows-gap: .5rem; 8 | --overflow-slider-arrows-inactive-opacity: 0.5; 9 | } 10 | 11 | .overflow-slider__arrows { 12 | display: flex; 13 | gap: var(--overflow-slider-arrows-gap); 14 | } 15 | 16 | .overflow-slider__arrows-button { 17 | display: flex; 18 | align-items: center; 19 | outline-offset: -2px; 20 | cursor: pointer; 21 | svg { 22 | width: var(--overflow-slider-arrows-size); 23 | height: var(--overflow-slider-arrows-size); 24 | 25 | } 26 | &[data-has-content="false"] { 27 | opacity: var(--overflow-slider-arrows-inactive-opacity); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dist/plugins/arrows/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ArrowsMovementTypes = 'view' | 'slide'; 4 | type ArrowsOptions = { 5 | texts: { 6 | buttonPrevious: string; 7 | buttonNext: string; 8 | }; 9 | icons: { 10 | prev: string; 11 | next: string; 12 | }; 13 | classNames: { 14 | navContainer: string; 15 | prevButton: string; 16 | nextButton: string; 17 | }; 18 | container: HTMLElement | null; 19 | containerPrev: HTMLElement | null; 20 | containerNext: HTMLElement | null; 21 | movementType: ArrowsMovementTypes; 22 | }; 23 | declare function ArrowsPlugin(args?: DeepPartial): (slider: Slider) => void; 24 | 25 | export { ArrowsPlugin as default }; 26 | export type { ArrowsMovementTypes, ArrowsOptions }; 27 | -------------------------------------------------------------------------------- /docs/dist/plugins/arrows/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type ArrowsMovementTypes = 'view' | 'slide'; 4 | type ArrowsOptions = { 5 | texts: { 6 | buttonPrevious: string; 7 | buttonNext: string; 8 | }; 9 | icons: { 10 | prev: string; 11 | next: string; 12 | }; 13 | classNames: { 14 | navContainer: string; 15 | prevButton: string; 16 | nextButton: string; 17 | }; 18 | container: HTMLElement | null; 19 | containerPrev: HTMLElement | null; 20 | containerNext: HTMLElement | null; 21 | movementType: ArrowsMovementTypes; 22 | }; 23 | declare function ArrowsPlugin(args?: DeepPartial): (slider: Slider) => void; 24 | 25 | export { ArrowsPlugin as default }; 26 | export type { ArrowsMovementTypes, ArrowsOptions }; 27 | -------------------------------------------------------------------------------- /dist/plugins/infinite-scroll/index.d.ts: -------------------------------------------------------------------------------- 1 | import { SliderPlugin } from '../core/index.js'; 2 | 3 | /** 4 | * Infinite‐scroll plugin 5 | * 6 | * Experimental work-in-progress not available for public use yet. 7 | */ 8 | 9 | /** 10 | * @typedef {Object} InfiniteScrollOptions 11 | * @property {number} [lookAheadCount=1] Number of slides to look ahead when deciding to reparent. 12 | */ 13 | /** 14 | * Creates an infinite scroll plugin for a slider that re-parents multiple slides 15 | * before hitting the container edge, to avoid blank space and keep the same 16 | * active slide visible. 17 | * 18 | * @param {InfiniteScrollOptions} [options] Plugin configuration. 19 | * @returns {SliderPlugin} The configured slider plugin. 20 | */ 21 | declare function InfiniteScrollPlugin(options?: { 22 | lookAheadCount?: number; 23 | }): SliderPlugin; 24 | 25 | export { InfiniteScrollPlugin as default }; 26 | -------------------------------------------------------------------------------- /docs/dist/plugins/infinite-scroll/index.d.ts: -------------------------------------------------------------------------------- 1 | import { SliderPlugin } from '../core/index.js'; 2 | 3 | /** 4 | * Infinite‐scroll plugin 5 | * 6 | * Experimental work-in-progress not available for public use yet. 7 | */ 8 | 9 | /** 10 | * @typedef {Object} InfiniteScrollOptions 11 | * @property {number} [lookAheadCount=1] Number of slides to look ahead when deciding to reparent. 12 | */ 13 | /** 14 | * Creates an infinite scroll plugin for a slider that re-parents multiple slides 15 | * before hitting the container edge, to avoid blank space and keep the same 16 | * active slide visible. 17 | * 18 | * @param {InfiniteScrollOptions} [options] Plugin configuration. 19 | * @returns {SliderPlugin} The configured slider plugin. 20 | */ 21 | declare function InfiniteScrollPlugin(options?: { 22 | lookAheadCount?: number; 23 | }): SliderPlugin; 24 | 25 | export { InfiniteScrollPlugin as default }; 26 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Publish to NPM 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 16 18 | - run: npm ci 19 | - run: npm test 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 16 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npm publish 32 | env: 33 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 34 | -------------------------------------------------------------------------------- /src/plugins/skip-links/styles.scss: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | # SkipLinksPlugin 3 | -------------------------------------------------------------- */ 4 | 5 | // You need a .screen-reader-text class so something like this: 6 | 7 | // .screen-reader-text { 8 | // border: 0; 9 | // clip: rect(1px, 1px, 1px, 1px); 10 | // clip-path: inset(50%); 11 | // height: 1px; 12 | // margin: -1px; 13 | // overflow: hidden; 14 | // padding: 0; 15 | // position: absolute; 16 | // width: 1px; 17 | // word-wrap: normal !important; 18 | // &:focus { 19 | // background-color: #000; 20 | // clip: auto !important; 21 | // clip-path: none; 22 | // color: #fff; 23 | // display: block; 24 | // font-weight: 700; 25 | // height: auto; 26 | // outline-offset: -2px; 27 | // padding: 1rem 1.5rem; 28 | // text-decoration: none; 29 | // width: fit-content; 30 | // z-index: 100000; 31 | // position: relative; 32 | // margin-top: 1rem; 33 | // margin-bottom: 1rem; 34 | // } 35 | // } 36 | -------------------------------------------------------------------------------- /src/plugins/fade/styles.scss: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | # FadePlugin 3 | -------------------------------------------------------------- */ 4 | 5 | :root { 6 | --overflow-slider-fade-color: #fff; 7 | --overflow-slider-fade-width: 3rem; 8 | } 9 | 10 | .overflow-slider-fade { 11 | position: absolute; 12 | top: 0; 13 | width: var(--overflow-slider-fade-width); 14 | height: 100%; 15 | pointer-events: none; 16 | z-index: 1; 17 | } 18 | 19 | .overflow-slider-fade--start { 20 | left: 0; 21 | background: linear-gradient(to right, var(--overflow-slider-fade-color) 0%, transparent 100%); 22 | [dir="rtl"] & { 23 | left: auto; 24 | right: 0; 25 | background: linear-gradient(to left, var(--overflow-slider-fade-color) 0%, transparent 100%); 26 | } 27 | } 28 | 29 | .overflow-slider-fade--end { 30 | right: 0; 31 | background: linear-gradient(to left, var(--overflow-slider-fade-color) 0%, transparent 100%); 32 | [dir="rtl"] & { 33 | right: auto; 34 | left: 0; 35 | background: linear-gradient(to right, var(--overflow-slider-fade-color) 0%, transparent 100%); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Evermade 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dist/plugins/core/index.d2.ts: -------------------------------------------------------------------------------- 1 | import { SliderOptionArgs, SliderPlugin, SliderCallback, SliderOptions, SliderDetails } from './index.js'; 2 | 3 | declare function OverflowSlider(container: HTMLElement, options?: SliderOptionArgs, plugins?: SliderPlugin[]): { 4 | container: HTMLElement; 5 | slides: HTMLElement[]; 6 | emit: (name: string) => void; 7 | moveToDirection: (direction: "prev" | "next") => void; 8 | moveToSlideInDirection: (direction: "prev" | "next") => void; 9 | snapToClosestSlide: (direction: "prev" | "next") => void; 10 | moveToSlide: (index: number) => void; 11 | canMoveToSlide: (index: number) => boolean; 12 | getInclusiveScrollWidth: () => number; 13 | getInclusiveClientWidth: () => number; 14 | getGapSize: () => number; 15 | getLeftOffset: () => number; 16 | getScrollLeft: () => number; 17 | setScrollLeft: (value: number) => void; 18 | setActiveSlideIdx: () => void; 19 | on: (name: string, cb: SliderCallback) => void; 20 | options: SliderOptions; 21 | details: SliderDetails; 22 | activeSlideIdx: number; 23 | } | undefined; 24 | 25 | export { OverflowSlider as default }; 26 | -------------------------------------------------------------------------------- /docs/dist/plugins/core/index.d2.ts: -------------------------------------------------------------------------------- 1 | import { SliderOptionArgs, SliderPlugin, SliderCallback, SliderOptions, SliderDetails } from './index.js'; 2 | 3 | declare function OverflowSlider(container: HTMLElement, options?: SliderOptionArgs, plugins?: SliderPlugin[]): { 4 | container: HTMLElement; 5 | slides: HTMLElement[]; 6 | emit: (name: string) => void; 7 | moveToDirection: (direction: "prev" | "next") => void; 8 | moveToSlideInDirection: (direction: "prev" | "next") => void; 9 | snapToClosestSlide: (direction: "prev" | "next") => void; 10 | moveToSlide: (index: number) => void; 11 | canMoveToSlide: (index: number) => boolean; 12 | getInclusiveScrollWidth: () => number; 13 | getInclusiveClientWidth: () => number; 14 | getGapSize: () => number; 15 | getLeftOffset: () => number; 16 | getScrollLeft: () => number; 17 | setScrollLeft: (value: number) => void; 18 | setActiveSlideIdx: () => void; 19 | on: (name: string, cb: SliderCallback) => void; 20 | options: SliderOptions; 21 | details: SliderDetails; 22 | activeSlideIdx: number; 23 | } | undefined; 24 | 25 | export { OverflowSlider as default }; 26 | -------------------------------------------------------------------------------- /src/overflow-slider.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Overflow Slider 3 | */ 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 | 17 | /* -------------------------------------------------------------- 18 | # Core 19 | -------------------------------------------------------------- */ 20 | 21 | .overflow-slider { 22 | overflow: auto; 23 | width: 100%; 24 | display: grid; 25 | grid-auto-flow: column; 26 | grid-template-columns: max-content; 27 | max-width: max-content; 28 | position: relative; 29 | // hide native scrollbars 30 | &::-webkit-scrollbar { 31 | display: none; 32 | } 33 | -ms-overflow-style: none; 34 | scrollbar-width: none; 35 | & > * { 36 | scroll-snap-align: start; 37 | outline-offset: -2px; 38 | } 39 | } 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /dist/plugins/skip-links/index.min.js: -------------------------------------------------------------------------------- 1 | function e(t,n=1){const i=`${t}-${n}`;return document.getElementById(i)?e(t,n+1):i}const t={skipList:"Skip list"},n={skipLink:"screen-reader-text",skipLinkTarget:"overflow-slider__skip-link-target"};function i(i){return r=>{var o,s,a,l,c,d;const u={texts:Object.assign(Object.assign({},t),(null==i?void 0:i.texts)||[]),classNames:Object.assign(Object.assign({},n),(null==i?void 0:i.classNames)||[]),containerBefore:null!==(o=null==i?void 0:i.containerAfter)&&void 0!==o?o:null,containerAfter:null!==(s=null==i?void 0:i.containerAfter)&&void 0!==s?s:null},f=e("overflow-slider-skip"),v=document.createElement("a");v.setAttribute("href",`#${f}`),v.textContent=u.texts.skipList,v.classList.add(u.classNames.skipLink);const p=document.createElement("div");p.setAttribute("id",f),p.setAttribute("tabindex","-1"),u.containerBefore?null===(a=u.containerBefore.parentNode)||void 0===a||a.insertBefore(v,u.containerBefore):null===(l=r.container.parentNode)||void 0===l||l.insertBefore(v,r.container),u.containerAfter?null===(c=u.containerAfter.parentNode)||void 0===c||c.insertBefore(p,u.containerAfter.nextSibling):null===(d=r.container.parentNode)||void 0===d||d.insertBefore(p,r.container.nextSibling)}}export{i as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/skip-links/index.min.js: -------------------------------------------------------------------------------- 1 | function e(t,n=1){const i=`${t}-${n}`;return document.getElementById(i)?e(t,n+1):i}const t={skipList:"Skip list"},n={skipLink:"screen-reader-text",skipLinkTarget:"overflow-slider__skip-link-target"};function i(i){return r=>{var o,s,a,l,c,d;const u={texts:Object.assign(Object.assign({},t),(null==i?void 0:i.texts)||[]),classNames:Object.assign(Object.assign({},n),(null==i?void 0:i.classNames)||[]),containerBefore:null!==(o=null==i?void 0:i.containerAfter)&&void 0!==o?o:null,containerAfter:null!==(s=null==i?void 0:i.containerAfter)&&void 0!==s?s:null},f=e("overflow-slider-skip"),v=document.createElement("a");v.setAttribute("href",`#${f}`),v.textContent=u.texts.skipList,v.classList.add(u.classNames.skipLink);const p=document.createElement("div");p.setAttribute("id",f),p.setAttribute("tabindex","-1"),u.containerBefore?null===(a=u.containerBefore.parentNode)||void 0===a||a.insertBefore(v,u.containerBefore):null===(l=r.container.parentNode)||void 0===l||l.insertBefore(v,r.container),u.containerAfter?null===(c=u.containerAfter.parentNode)||void 0===c||c.insertBefore(p,u.containerAfter.nextSibling):null===(d=r.container.parentNode)||void 0===d||d.insertBefore(p,r.container.nextSibling)}}export{i as default}; 2 | -------------------------------------------------------------------------------- /src/core/overflow-slider.ts: -------------------------------------------------------------------------------- 1 | import Slider from './slider'; 2 | 3 | import { 4 | SliderOptionArgs, 5 | SliderOptions, 6 | SliderPlugin, 7 | } from './types'; 8 | 9 | export default function OverflowSlider ( 10 | container: HTMLElement, 11 | options?: SliderOptionArgs, 12 | plugins?: SliderPlugin[] 13 | ) { 14 | try { 15 | 16 | // check that container HTML element 17 | if (!(container instanceof Element)) { 18 | throw new Error(`Container must be HTML element, found ${typeof container}`); 19 | } 20 | 21 | const defaults: SliderOptionArgs = { 22 | cssVariableContainer: container, 23 | scrollBehavior: "smooth", 24 | scrollStrategy: "fullSlide", 25 | slidesSelector: ":scope > *", 26 | emulateScrollSnap: false, 27 | emulateScrollSnapMaxThreshold: 64, 28 | rtl: false, 29 | }; 30 | 31 | const sliderOptions = { ...defaults, ...options }; 32 | 33 | // disable smooth scrolling if user prefers reduced motion 34 | if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { 35 | sliderOptions.scrollBehavior = "auto"; 36 | } 37 | 38 | return Slider( 39 | container, 40 | sliderOptions, 41 | plugins, 42 | ); 43 | } catch (e) { 44 | console.error(e) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /dist/plugins/drag-scrolling/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e){var t;const o=null!==(t=null==e?void 0:e.draggedDistanceThatPreventsClick)&&void 0!==t?t:20;return e=>{let t=!1,n=0,r=0,a=!1,l=!1,i=!1;e.container.setAttribute("data-has-drag-scrolling","true");if(window.addEventListener("mousedown",o=>{l=!1,e.details.hasOverflow&&e.container.contains(o.target)&&(t=!0,n=o.pageX-e.container.getBoundingClientRect().left,r=e.container.scrollLeft,e.container.style.cursor="grabbing",e.container.style.scrollBehavior="auto",o.preventDefault(),o.stopPropagation())}),window.addEventListener("mousemove",s=>{if(!e.details.hasOverflow)return void(l=!1);if(!t)return void(l=!1);s.preventDefault(),l||(l=!0,e.emit("programmaticScrollStart"));const c=s.pageX-e.container.getBoundingClientRect().left-n,d=r-c;i=!0,Math.floor(e.container.scrollLeft)!==Math.floor(d)&&(a=e.container.scrollLefto?"none":"";v.forEach(e=>{e.style.pointerEvents=f})}),window.addEventListener("mouseup",()=>{e.details.hasOverflow?(t=!1,e.container.style.cursor="",setTimeout(()=>{l=!1,e.container.style.scrollBehavior="";e.container.querySelectorAll(e.options.slidesSelector).forEach(e=>{e.style.pointerEvents=""})},50)):l=!1}),e.options.emulateScrollSnap){const o=()=>{i&&!t&&(i=!1,e.snapToClosestSlide(a?"next":"prev"))};e.on("programmaticScrollEnd",o),window.addEventListener("mouseup",o)}}}export{e as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/drag-scrolling/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e){var t;const o=null!==(t=null==e?void 0:e.draggedDistanceThatPreventsClick)&&void 0!==t?t:20;return e=>{let t=!1,n=0,r=0,a=!1,l=!1,i=!1;e.container.setAttribute("data-has-drag-scrolling","true");if(window.addEventListener("mousedown",o=>{l=!1,e.details.hasOverflow&&e.container.contains(o.target)&&(t=!0,n=o.pageX-e.container.getBoundingClientRect().left,r=e.container.scrollLeft,e.container.style.cursor="grabbing",e.container.style.scrollBehavior="auto",o.preventDefault(),o.stopPropagation())}),window.addEventListener("mousemove",s=>{if(!e.details.hasOverflow)return void(l=!1);if(!t)return void(l=!1);s.preventDefault(),l||(l=!0,e.emit("programmaticScrollStart"));const c=s.pageX-e.container.getBoundingClientRect().left-n,d=r-c;i=!0,Math.floor(e.container.scrollLeft)!==Math.floor(d)&&(a=e.container.scrollLefto?"none":"";v.forEach(e=>{e.style.pointerEvents=f})}),window.addEventListener("mouseup",()=>{e.details.hasOverflow?(t=!1,e.container.style.cursor="",setTimeout(()=>{l=!1,e.container.style.scrollBehavior="";e.container.querySelectorAll(e.options.slidesSelector).forEach(e=>{e.style.pointerEvents=""})},50)):l=!1}),e.options.emulateScrollSnap){const o=()=>{i&&!t&&(i=!1,e.snapToClosestSlide(a?"next":"prev"))};e.on("programmaticScrollEnd",o),window.addEventListener("mouseup",o)}}}export{e as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/autoplay/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type AutoplayMovementTypes = 'view' | 'slide'; 4 | type AutoplayPluginOptions = { 5 | /** Delay between auto-scrolls in milliseconds */ 6 | delayInMs: number; 7 | /** Translatable button texts */ 8 | texts: { 9 | play: string; 10 | pause: string; 11 | }; 12 | /** Icons (SVG/html string) for play/pause states */ 13 | icons: { 14 | play: string; 15 | pause: string; 16 | }; 17 | /** CSS class names */ 18 | classNames: { 19 | autoplayButton: string; 20 | }; 21 | /** Container in which to insert controls (defaults before slider) */ 22 | container: HTMLElement | null; 23 | /** Whether to advance by view or by slide */ 24 | movementType: AutoplayMovementTypes; 25 | stopOnHover: boolean; 26 | loop: boolean; 27 | }; 28 | type AutoplayPluginArgs = DeepPartial; 29 | /** 30 | * Autoplay plugin for Overflow Slider 31 | * 32 | * Loops slides infinitely, always respects reduced-motion, 33 | * provides Play/Pause controls, and shows a progress bar. 34 | * 35 | * @param {AutoplayPluginArgs} args 36 | * @returns {(slider: Slider) => void} 37 | */ 38 | declare function AutoplayPlugin(args?: AutoplayPluginArgs): (slider: Slider) => void; 39 | 40 | export { AutoplayPlugin as default }; 41 | export type { AutoplayMovementTypes, AutoplayPluginArgs, AutoplayPluginOptions }; 42 | -------------------------------------------------------------------------------- /dist/utils-ayDxlweP.js: -------------------------------------------------------------------------------- 1 | function generateId(prefix, i = 1) { 2 | const id = `${prefix}-${i}`; 3 | if (document.getElementById(id)) { 4 | return generateId(prefix, i + 1); 5 | } 6 | return id; 7 | } 8 | function objectsAreEqual(obj1, obj2) { 9 | const keys1 = Object.keys(obj1); 10 | const keys2 = Object.keys(obj2); 11 | if (keys1.length !== keys2.length) { 12 | return false; 13 | } 14 | for (let key of keys1) { 15 | // Use `Object.prototype.hasOwnProperty.call` for better safety 16 | if (!Object.prototype.hasOwnProperty.call(obj2, key) || obj1[key] !== obj2[key]) { 17 | return false; 18 | } 19 | } 20 | return true; 21 | } 22 | function getOutermostChildrenEdgeMarginSum(el) { 23 | if (el.children.length === 0) { 24 | return 0; 25 | } 26 | // get the first child and its left margin 27 | const firstChild = el.children[0]; 28 | const firstChildStyle = getComputedStyle(firstChild); 29 | const firstChildMarginLeft = parseFloat(firstChildStyle.marginLeft); 30 | // Get the last child and its right margin 31 | const lastChild = el.children[el.children.length - 1]; 32 | const lastChildStyle = getComputedStyle(lastChild); 33 | const lastChildMarginRight = parseFloat(lastChildStyle.marginRight); 34 | return firstChildMarginLeft + lastChildMarginRight; 35 | } 36 | 37 | export { getOutermostChildrenEdgeMarginSum as a, generateId as g, objectsAreEqual as o }; 38 | -------------------------------------------------------------------------------- /docs/dist/plugins/autoplay/index.d.ts: -------------------------------------------------------------------------------- 1 | import { DeepPartial, Slider } from '../core/index.js'; 2 | 3 | type AutoplayMovementTypes = 'view' | 'slide'; 4 | type AutoplayPluginOptions = { 5 | /** Delay between auto-scrolls in milliseconds */ 6 | delayInMs: number; 7 | /** Translatable button texts */ 8 | texts: { 9 | play: string; 10 | pause: string; 11 | }; 12 | /** Icons (SVG/html string) for play/pause states */ 13 | icons: { 14 | play: string; 15 | pause: string; 16 | }; 17 | /** CSS class names */ 18 | classNames: { 19 | autoplayButton: string; 20 | }; 21 | /** Container in which to insert controls (defaults before slider) */ 22 | container: HTMLElement | null; 23 | /** Whether to advance by view or by slide */ 24 | movementType: AutoplayMovementTypes; 25 | stopOnHover: boolean; 26 | loop: boolean; 27 | }; 28 | type AutoplayPluginArgs = DeepPartial; 29 | /** 30 | * Autoplay plugin for Overflow Slider 31 | * 32 | * Loops slides infinitely, always respects reduced-motion, 33 | * provides Play/Pause controls, and shows a progress bar. 34 | * 35 | * @param {AutoplayPluginArgs} args 36 | * @returns {(slider: Slider) => void} 37 | */ 38 | declare function AutoplayPlugin(args?: AutoplayPluginArgs): (slider: Slider) => void; 39 | 40 | export { AutoplayPlugin as default }; 41 | export type { AutoplayMovementTypes, AutoplayPluginArgs, AutoplayPluginOptions }; 42 | -------------------------------------------------------------------------------- /docs/dist/utils-ayDxlweP.js: -------------------------------------------------------------------------------- 1 | function generateId(prefix, i = 1) { 2 | const id = `${prefix}-${i}`; 3 | if (document.getElementById(id)) { 4 | return generateId(prefix, i + 1); 5 | } 6 | return id; 7 | } 8 | function objectsAreEqual(obj1, obj2) { 9 | const keys1 = Object.keys(obj1); 10 | const keys2 = Object.keys(obj2); 11 | if (keys1.length !== keys2.length) { 12 | return false; 13 | } 14 | for (let key of keys1) { 15 | // Use `Object.prototype.hasOwnProperty.call` for better safety 16 | if (!Object.prototype.hasOwnProperty.call(obj2, key) || obj1[key] !== obj2[key]) { 17 | return false; 18 | } 19 | } 20 | return true; 21 | } 22 | function getOutermostChildrenEdgeMarginSum(el) { 23 | if (el.children.length === 0) { 24 | return 0; 25 | } 26 | // get the first child and its left margin 27 | const firstChild = el.children[0]; 28 | const firstChildStyle = getComputedStyle(firstChild); 29 | const firstChildMarginLeft = parseFloat(firstChildStyle.marginLeft); 30 | // Get the last child and its right margin 31 | const lastChild = el.children[el.children.length - 1]; 32 | const lastChildStyle = getComputedStyle(lastChild); 33 | const lastChildMarginRight = parseFloat(lastChildStyle.marginRight); 34 | return firstChildMarginLeft + lastChildMarginRight; 35 | } 36 | 37 | export { getOutermostChildrenEdgeMarginSum as a, generateId as g, objectsAreEqual as o }; 38 | -------------------------------------------------------------------------------- /src/core/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | function generateId( prefix : string, i = 1 ): string { 3 | const id = `${prefix}-${i}`; 4 | if ( document.getElementById( id ) ) { 5 | return generateId( prefix, i + 1 ); 6 | } 7 | return id; 8 | } 9 | 10 | function objectsAreEqual(obj1: Record, obj2: Record): boolean { 11 | const keys1 = Object.keys(obj1); 12 | const keys2 = Object.keys(obj2); 13 | 14 | if (keys1.length !== keys2.length) { 15 | return false; 16 | } 17 | 18 | for (let key of keys1) { 19 | // Use `Object.prototype.hasOwnProperty.call` for better safety 20 | if (!Object.prototype.hasOwnProperty.call(obj2, key) || obj1[key] !== obj2[key]) { 21 | return false; 22 | } 23 | } 24 | 25 | return true; 26 | } 27 | 28 | function getOutermostChildrenEdgeMarginSum( el: HTMLElement ): number { 29 | if (el.children.length === 0) { 30 | return 0; 31 | } 32 | 33 | // get the first child and its left margin 34 | const firstChild = el.children[0]; 35 | const firstChildStyle = getComputedStyle(firstChild); 36 | const firstChildMarginLeft = parseFloat(firstChildStyle.marginLeft); 37 | 38 | // Get the last child and its right margin 39 | const lastChild = el.children[el.children.length - 1]; 40 | const lastChildStyle = getComputedStyle(lastChild); 41 | const lastChildMarginRight = parseFloat(lastChildStyle.marginRight); 42 | 43 | return firstChildMarginLeft + lastChildMarginRight; 44 | } 45 | 46 | export { generateId, objectsAreEqual, getOutermostChildrenEdgeMarginSum }; 47 | -------------------------------------------------------------------------------- /src/core/details.ts: -------------------------------------------------------------------------------- 1 | import { Slider, SliderDetails } from './types'; 2 | 3 | export default function details( slider: Slider) { 4 | 5 | let instance: SliderDetails; 6 | 7 | let hasOverflow = false; 8 | let slideCount = 0; 9 | let containerWidth = 0; 10 | let containerHeight = 0; 11 | let scrollableAreaWidth = 0; 12 | let amountOfPages = 0; 13 | let currentPage = 0; 14 | 15 | if ( Math.floor( slider.getInclusiveScrollWidth() ) > Math.floor( slider.getInclusiveClientWidth() ) ) { 16 | hasOverflow = true; 17 | } 18 | 19 | slideCount = slider.slides.length ?? 0; 20 | 21 | containerWidth = slider.container.offsetWidth; 22 | 23 | containerHeight = slider.container.offsetHeight; 24 | 25 | scrollableAreaWidth = slider.getInclusiveScrollWidth(); 26 | 27 | amountOfPages = Math.ceil(scrollableAreaWidth / containerWidth); 28 | 29 | if ( Math.floor( slider.getScrollLeft() ) >= 0) { 30 | currentPage = Math.floor(slider.getScrollLeft() / containerWidth); 31 | 32 | // Consider as last page if we're within tolerance of the maximum scroll position 33 | // When FullWidthPlugin is active, account for the margin offset 34 | const maxScroll = scrollableAreaWidth - containerWidth - (2 * slider.getLeftOffset() ); 35 | if ( slider.getScrollLeft() >= maxScroll - 1 ) { 36 | currentPage = amountOfPages - 1; 37 | } 38 | } 39 | 40 | instance = { 41 | hasOverflow, 42 | slideCount, 43 | containerWidth, 44 | containerHeight, 45 | scrollableAreaWidth, 46 | amountOfPages, 47 | currentPage, 48 | }; 49 | return instance; 50 | }; 51 | -------------------------------------------------------------------------------- /src/plugins/dots/styles.scss: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | # DotsPlugin 3 | -------------------------------------------------------------- */ 4 | 5 | :root { 6 | --overflow-slider-dots-gap: 0.5rem; 7 | --overflow-slider-dot-size: 0.75rem; 8 | --overflow-slider-dot-inactive-color: hsla(0, 0%, 0%, 0.1); 9 | --overflow-slider-dot-active-color: hsla(0, 0%, 0%, 0.8); 10 | } 11 | 12 | .overflow-slider__dots { 13 | display: flex; 14 | justify-content: center; 15 | align-items: center; 16 | ul { 17 | list-style: none; 18 | padding: 0; 19 | margin: 0; 20 | display: flex; 21 | flex-wrap: wrap; 22 | gap: var(--overflow-slider-dots-gap); 23 | } 24 | li { 25 | line-height: 0; 26 | padding: 0; 27 | margin: 0; 28 | } 29 | } 30 | 31 | .overflow-slider__dot-item { 32 | padding: 0; 33 | margin: 0; 34 | cursor: pointer; 35 | outline-offset: 2px; 36 | width: var(--overflow-slider-dot-size); 37 | height: var(--overflow-slider-dot-size); 38 | border-radius: 50%; 39 | background: var(--overflow-slider-dot-inactive-color); 40 | position: relative; 41 | // increase clickable area 42 | &::after { 43 | content: ''; 44 | display: block; 45 | left: calc(-1 * var(--overflow-slider-dots-gap)); 46 | top: calc(-1 * var(--overflow-slider-dots-gap)); 47 | right: calc(-1 * var(--overflow-slider-dots-gap)); 48 | bottom: calc(-1 * var(--overflow-slider-dots-gap)); 49 | position: absolute; 50 | } 51 | &[aria-pressed="true"], 52 | &:hover { 53 | background: var(--overflow-slider-dot-active-color); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /dist/plugins/full-width/index.min.js: -------------------------------------------------------------------------------- 1 | const t=t=>{var n,e;return null!==(e=null===(n=t.container.parentElement)||void 0===n?void 0:n.offsetWidth)&&void 0!==e?e:window.innerWidth};function n(n){return e=>{var i,r,o,d;const l={targetWidth:null!==(i=null==n?void 0:n.targetWidth)&&void 0!==i?i:void 0,addMarginBefore:null===(r=null==n?void 0:n.addMarginBefore)||void 0===r||r,addMarginAfter:null===(o=null==n?void 0:n.addMarginAfter)||void 0===o||o};"function"!=typeof e.options.targetWidth&&(e.options.targetWidth=null!==(d=l.targetWidth)&&void 0!==d?d:t);const a=()=>{const n=e.container.querySelectorAll(e.options.slidesSelector);if(!n.length)return;const i=(()=>{var n;return"function"==typeof e.options.targetWidth?e.options.targetWidth:null!==(n=l.targetWidth)&&void 0!==n?n:t})(),r=(window.innerWidth-i(e))/2,o=Math.max(0,Math.floor(r)),d=o?`${o}px`:"";n.forEach(t=>{const n=t;n.style.marginInlineStart="",n.style.marginInlineEnd=""});const a=n[0],c=n[n.length-1];l.addMarginBefore?(a.style.marginInlineStart=d,e.container.style.setProperty("scroll-padding-inline-start",d||"0px")):e.container.style.removeProperty("scroll-padding-inline-start"),l.addMarginAfter?(c.style.marginInlineEnd=d,e.container.style.setProperty("scroll-padding-inline-end",d||"0px")):e.container.style.removeProperty("scroll-padding-inline-end"),e.container.setAttribute("data-full-width-offset",`${o}`),s(i),e.emit("fullWidthPluginUpdate")},s=t=>{const n=t(e);e.options.cssVariableContainer.style.setProperty("--slider-container-target-width",`${n}px`)};a(),e.on("contentsChanged",a),e.on("containerSizeChanged",a),window.addEventListener("resize",a)}}export{n as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/full-width/index.min.js: -------------------------------------------------------------------------------- 1 | const t=t=>{var n,e;return null!==(e=null===(n=t.container.parentElement)||void 0===n?void 0:n.offsetWidth)&&void 0!==e?e:window.innerWidth};function n(n){return e=>{var i,r,o,d;const l={targetWidth:null!==(i=null==n?void 0:n.targetWidth)&&void 0!==i?i:void 0,addMarginBefore:null===(r=null==n?void 0:n.addMarginBefore)||void 0===r||r,addMarginAfter:null===(o=null==n?void 0:n.addMarginAfter)||void 0===o||o};"function"!=typeof e.options.targetWidth&&(e.options.targetWidth=null!==(d=l.targetWidth)&&void 0!==d?d:t);const a=()=>{const n=e.container.querySelectorAll(e.options.slidesSelector);if(!n.length)return;const i=(()=>{var n;return"function"==typeof e.options.targetWidth?e.options.targetWidth:null!==(n=l.targetWidth)&&void 0!==n?n:t})(),r=(window.innerWidth-i(e))/2,o=Math.max(0,Math.floor(r)),d=o?`${o}px`:"";n.forEach(t=>{const n=t;n.style.marginInlineStart="",n.style.marginInlineEnd=""});const a=n[0],c=n[n.length-1];l.addMarginBefore?(a.style.marginInlineStart=d,e.container.style.setProperty("scroll-padding-inline-start",d||"0px")):e.container.style.removeProperty("scroll-padding-inline-start"),l.addMarginAfter?(c.style.marginInlineEnd=d,e.container.style.setProperty("scroll-padding-inline-end",d||"0px")):e.container.style.removeProperty("scroll-padding-inline-end"),e.container.setAttribute("data-full-width-offset",`${o}`),s(i),e.emit("fullWidthPluginUpdate")},s=t=>{const n=t(e);e.options.cssVariableContainer.style.setProperty("--slider-container-target-width",`${n}px`)};a(),e.on("contentsChanged",a),e.on("containerSizeChanged",a),window.addEventListener("resize",a)}}export{n as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/classnames/index.min.js: -------------------------------------------------------------------------------- 1 | const t={visible:"is-visible",partlyVisible:"is-partly-visible",hidden:"is-hidden"};function e(e){return n=>{var i,s;const a=null!==(i=null==e?void 0:e.classNames)&&void 0!==i?i:null==e?void 0:e.classnames,l={classNames:Object.assign(Object.assign({},t),null!=a?a:{}),freezeStateOnVisible:null!==(s=null==e?void 0:e.freezeStateOnVisible)&&void 0!==s&&s},r=new WeakMap,o=Array.from(new Set(Object.values(l.classNames).filter(t=>Boolean(t)))),c=()=>{const{targetStart:t,targetEnd:e}=(()=>{const t=n.container.getBoundingClientRect(),e=t.width;if(!e)return{targetStart:t.left,targetEnd:t.right};let i=0;if("function"==typeof n.options.targetWidth)try{i=n.options.targetWidth(n)}catch(t){i=0}(!Number.isFinite(i)||i<=0)&&(i=e);const s=(e-Math.min(i,e))/2,a=Math.max(s,0);return{targetStart:t.left+a,targetEnd:t.right-a}})();n.slides.forEach(n=>{const i=n.getBoundingClientRect(),s=i.left,a=i.right;let c="hidden";a-2>t&&s+2=t&&a-2<=e?"visible":"partlyVisible");const d=r.get(n);if(l.freezeStateOnVisible&&"visible"===d)return;if(d===c)return;const u=l.classNames[c];if(d){const t=l.classNames[d];t!==u&&t&&n.classList.remove(t)}else o.forEach(t=>{t!==u&&n.classList.remove(t)});u&&!n.classList.contains(u)&&n.classList.add(u),r.set(n,c)})};n.on("created",c),n.on("pluginsLoaded",c),n.on("fullWidthPluginUpdate",c),n.on("contentsChanged",c),n.on("containerSizeChanged",c),n.on("detailsChanged",c),n.on("scrollEnd",c),n.on("scrollStart",c),requestAnimationFrame(()=>{requestAnimationFrame(()=>c())});let d=0;n.on("scroll",()=>{d&&window.cancelAnimationFrame(d),d=window.requestAnimationFrame(()=>{c()})})}}export{e as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/classnames/index.min.js: -------------------------------------------------------------------------------- 1 | const t={visible:"is-visible",partlyVisible:"is-partly-visible",hidden:"is-hidden"};function e(e){return n=>{var i,s;const a=null!==(i=null==e?void 0:e.classNames)&&void 0!==i?i:null==e?void 0:e.classnames,l={classNames:Object.assign(Object.assign({},t),null!=a?a:{}),freezeStateOnVisible:null!==(s=null==e?void 0:e.freezeStateOnVisible)&&void 0!==s&&s},r=new WeakMap,o=Array.from(new Set(Object.values(l.classNames).filter(t=>Boolean(t)))),c=()=>{const{targetStart:t,targetEnd:e}=(()=>{const t=n.container.getBoundingClientRect(),e=t.width;if(!e)return{targetStart:t.left,targetEnd:t.right};let i=0;if("function"==typeof n.options.targetWidth)try{i=n.options.targetWidth(n)}catch(t){i=0}(!Number.isFinite(i)||i<=0)&&(i=e);const s=(e-Math.min(i,e))/2,a=Math.max(s,0);return{targetStart:t.left+a,targetEnd:t.right-a}})();n.slides.forEach(n=>{const i=n.getBoundingClientRect(),s=i.left,a=i.right;let c="hidden";a-2>t&&s+2=t&&a-2<=e?"visible":"partlyVisible");const d=r.get(n);if(l.freezeStateOnVisible&&"visible"===d)return;if(d===c)return;const u=l.classNames[c];if(d){const t=l.classNames[d];t!==u&&t&&n.classList.remove(t)}else o.forEach(t=>{t!==u&&n.classList.remove(t)});u&&!n.classList.contains(u)&&n.classList.add(u),r.set(n,c)})};n.on("created",c),n.on("pluginsLoaded",c),n.on("fullWidthPluginUpdate",c),n.on("contentsChanged",c),n.on("containerSizeChanged",c),n.on("detailsChanged",c),n.on("scrollEnd",c),n.on("scrollStart",c),requestAnimationFrame(()=>{requestAnimationFrame(()=>c())});let d=0;n.on("scroll",()=>{d&&window.cancelAnimationFrame(d),d=window.requestAnimationFrame(()=>{c()})})}}export{e as default}; 2 | -------------------------------------------------------------------------------- /src/plugins/thumbnails/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | export type ThumbnailsOptions = { 4 | mainSlider: Slider, 5 | }; 6 | 7 | export default function FullWidthPlugin( args: DeepPartial ) { 8 | return ( slider: Slider ) => { 9 | 10 | const options = { 11 | mainSlider: args.mainSlider, 12 | }; 13 | 14 | const mainSlider = options.mainSlider; 15 | 16 | const setActiveThumbnail = (slide: HTMLElement | null = null) => { 17 | if ( slide === null && slider.slides.length > 0 ) { 18 | slide = slider.slides[0] as HTMLElement; 19 | } 20 | if ( slide === null ) { 21 | return; 22 | } 23 | // add aria-current to the clicked slide 24 | slider.slides.forEach((s) => { 25 | s.setAttribute('aria-current', 'false'); 26 | }); 27 | slide.setAttribute('aria-current', 'true'); 28 | }; 29 | 30 | const addClickListeners = () => { 31 | slider.slides.forEach((slide, index) => { 32 | slide.addEventListener('click', () => { 33 | mainSlider.moveToSlide(index); 34 | setActiveThumbnail(slide); 35 | }); 36 | }); 37 | }; 38 | 39 | setActiveThumbnail(); 40 | addClickListeners(); 41 | 42 | mainSlider.on( 'scrollEnd', () => { 43 | setTimeout(() => { 44 | const mainActiveSlideIdx = mainSlider.activeSlideIdx; 45 | const thumbActiveSlideIdx = slider.activeSlideIdx; 46 | if ( thumbActiveSlideIdx === mainActiveSlideIdx ) { 47 | return; 48 | } 49 | const activeThumbnail = slider.slides[mainActiveSlideIdx] as HTMLElement; 50 | setActiveThumbnail(activeThumbnail); 51 | slider.moveToSlide(mainActiveSlideIdx); 52 | }, 50); 53 | }); 54 | 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /dist/plugins/fade/index.min.js: -------------------------------------------------------------------------------- 1 | function t(t){return e=>{var n,a,o;const i={classNames:{fadeItem:"overflow-slider-fade",fadeItemStart:"overflow-slider-fade--start",fadeItemEnd:"overflow-slider-fade--end"},container:null!==(n=null==t?void 0:t.container)&&void 0!==n?n:null,containerStart:null!==(a=null==t?void 0:t.containerStart)&&void 0!==a?a:null,containerEnd:null!==(o=null==t?void 0:t.containerEnd)&&void 0!==o?o:null},l=document.createElement("div");l.classList.add(i.classNames.fadeItem,i.classNames.fadeItemStart),l.setAttribute("aria-hidden","true"),l.setAttribute("tabindex","-1");const r=document.createElement("div");r.classList.add(i.classNames.fadeItem,i.classNames.fadeItemEnd),r.setAttribute("aria-hidden","true"),r.setAttribute("tabindex","-1"),i.containerStart?i.containerStart.appendChild(l):i.container&&i.container.appendChild(l),i.containerEnd?i.containerEnd.appendChild(r):i.container&&i.container.appendChild(r);const d=()=>{l.setAttribute("data-has-fade",(e.getScrollLeft()>l.offsetWidth).toString()),l.style.opacity=(()=>{const t=e.getScrollLeft();return Math.floor(t)<=Math.floor(l.offsetWidth)?t/Math.max(l.offsetWidth,1):1})().toString(),r.setAttribute("data-has-fade",(Math.floor(e.getScrollLeft()){const t=e.getScrollLeft(),n=e.getInclusiveScrollWidth()-e.getInclusiveClientWidth()-r.offsetWidth;return Math.floor(t)>=Math.floor(n)?(n-t)/Math.max(r.offsetWidth,1)+1:1})().toString()};d(),e.on("created",d),e.on("contentsChanged",d),e.on("containerSizeChanged",d),e.on("scrollEnd",d),e.on("scrollStart",d);let s=0;e.on("scroll",()=>{s&&window.cancelAnimationFrame(s),s=window.requestAnimationFrame(()=>{d()})})}}export{t as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/fade/index.min.js: -------------------------------------------------------------------------------- 1 | function t(t){return e=>{var n,a,o;const i={classNames:{fadeItem:"overflow-slider-fade",fadeItemStart:"overflow-slider-fade--start",fadeItemEnd:"overflow-slider-fade--end"},container:null!==(n=null==t?void 0:t.container)&&void 0!==n?n:null,containerStart:null!==(a=null==t?void 0:t.containerStart)&&void 0!==a?a:null,containerEnd:null!==(o=null==t?void 0:t.containerEnd)&&void 0!==o?o:null},l=document.createElement("div");l.classList.add(i.classNames.fadeItem,i.classNames.fadeItemStart),l.setAttribute("aria-hidden","true"),l.setAttribute("tabindex","-1");const r=document.createElement("div");r.classList.add(i.classNames.fadeItem,i.classNames.fadeItemEnd),r.setAttribute("aria-hidden","true"),r.setAttribute("tabindex","-1"),i.containerStart?i.containerStart.appendChild(l):i.container&&i.container.appendChild(l),i.containerEnd?i.containerEnd.appendChild(r):i.container&&i.container.appendChild(r);const d=()=>{l.setAttribute("data-has-fade",(e.getScrollLeft()>l.offsetWidth).toString()),l.style.opacity=(()=>{const t=e.getScrollLeft();return Math.floor(t)<=Math.floor(l.offsetWidth)?t/Math.max(l.offsetWidth,1):1})().toString(),r.setAttribute("data-has-fade",(Math.floor(e.getScrollLeft()){const t=e.getScrollLeft(),n=e.getInclusiveScrollWidth()-e.getInclusiveClientWidth()-r.offsetWidth;return Math.floor(t)>=Math.floor(n)?(n-t)/Math.max(r.offsetWidth,1)+1:1})().toString()};d(),e.on("created",d),e.on("contentsChanged",d),e.on("containerSizeChanged",d),e.on("scrollEnd",d),e.on("scrollStart",d);let s=0;e.on("scroll",()=>{s&&window.cancelAnimationFrame(s),s=window.requestAnimationFrame(()=>{d()})})}}export{t as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/thumbnails/index.esm.js: -------------------------------------------------------------------------------- 1 | function FullWidthPlugin(args) { 2 | return (slider) => { 3 | const options = { 4 | mainSlider: args.mainSlider, 5 | }; 6 | const mainSlider = options.mainSlider; 7 | const setActiveThumbnail = (slide = null) => { 8 | if (slide === null && slider.slides.length > 0) { 9 | slide = slider.slides[0]; 10 | } 11 | if (slide === null) { 12 | return; 13 | } 14 | // add aria-current to the clicked slide 15 | slider.slides.forEach((s) => { 16 | s.setAttribute('aria-current', 'false'); 17 | }); 18 | slide.setAttribute('aria-current', 'true'); 19 | }; 20 | const addClickListeners = () => { 21 | slider.slides.forEach((slide, index) => { 22 | slide.addEventListener('click', () => { 23 | mainSlider.moveToSlide(index); 24 | setActiveThumbnail(slide); 25 | }); 26 | }); 27 | }; 28 | setActiveThumbnail(); 29 | addClickListeners(); 30 | mainSlider.on('scrollEnd', () => { 31 | setTimeout(() => { 32 | const mainActiveSlideIdx = mainSlider.activeSlideIdx; 33 | const thumbActiveSlideIdx = slider.activeSlideIdx; 34 | if (thumbActiveSlideIdx === mainActiveSlideIdx) { 35 | return; 36 | } 37 | const activeThumbnail = slider.slides[mainActiveSlideIdx]; 38 | setActiveThumbnail(activeThumbnail); 39 | slider.moveToSlide(mainActiveSlideIdx); 40 | }, 50); 41 | }); 42 | }; 43 | } 44 | 45 | export { FullWidthPlugin as default }; 46 | -------------------------------------------------------------------------------- /docs/dist/plugins/thumbnails/index.esm.js: -------------------------------------------------------------------------------- 1 | function FullWidthPlugin(args) { 2 | return (slider) => { 3 | const options = { 4 | mainSlider: args.mainSlider, 5 | }; 6 | const mainSlider = options.mainSlider; 7 | const setActiveThumbnail = (slide = null) => { 8 | if (slide === null && slider.slides.length > 0) { 9 | slide = slider.slides[0]; 10 | } 11 | if (slide === null) { 12 | return; 13 | } 14 | // add aria-current to the clicked slide 15 | slider.slides.forEach((s) => { 16 | s.setAttribute('aria-current', 'false'); 17 | }); 18 | slide.setAttribute('aria-current', 'true'); 19 | }; 20 | const addClickListeners = () => { 21 | slider.slides.forEach((slide, index) => { 22 | slide.addEventListener('click', () => { 23 | mainSlider.moveToSlide(index); 24 | setActiveThumbnail(slide); 25 | }); 26 | }); 27 | }; 28 | setActiveThumbnail(); 29 | addClickListeners(); 30 | mainSlider.on('scrollEnd', () => { 31 | setTimeout(() => { 32 | const mainActiveSlideIdx = mainSlider.activeSlideIdx; 33 | const thumbActiveSlideIdx = slider.activeSlideIdx; 34 | if (thumbActiveSlideIdx === mainActiveSlideIdx) { 35 | return; 36 | } 37 | const activeThumbnail = slider.slides[mainActiveSlideIdx]; 38 | setActiveThumbnail(activeThumbnail); 39 | slider.moveToSlide(mainActiveSlideIdx); 40 | }, 50); 41 | }); 42 | }; 43 | } 44 | 45 | export { FullWidthPlugin as default }; 46 | -------------------------------------------------------------------------------- /src/plugins/scroll-indicator/styles.scss: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------- 2 | # ScrollIndicatorPlugin 3 | -------------------------------------------------------------- */ 4 | 5 | :root { 6 | --overflow-slider-scroll-indicator-button-height: 4px; 7 | --overflow-slider-scroll-indicator-padding: 1rem; 8 | --overflow-slider-scroll-indicator-button-color: hsla(0, 0%, 0%, 0.75); 9 | --overflow-slider-scroll-indicator-bar-color: hsla(0, 0%, 0%, 0.25); 10 | } 11 | 12 | .overflow-slider__scroll-indicator { 13 | width: 100%; 14 | padding-block: var(--overflow-slider-scroll-indicator-padding); 15 | cursor: pointer; 16 | position: relative; 17 | outline: 0; 18 | &[data-has-overflow="false"] { 19 | display: none; 20 | } 21 | &:focus-visible .overflow-slider__scroll-indicator-button { 22 | outline: 2px solid; 23 | outline-offset: 2px; 24 | } 25 | } 26 | 27 | .overflow-slider__scroll-indicator-bar { 28 | height: 2px; 29 | background: var(--overflow-slider-scroll-indicator-bar-color); 30 | width: 100%; 31 | border-radius: 3px; 32 | position: absolute; 33 | top: 50%; 34 | left: 0; 35 | transform: translateY(-50%); 36 | } 37 | 38 | .overflow-slider__scroll-indicator-button { 39 | height: var(--overflow-slider-scroll-indicator-button-height); 40 | background: var(--overflow-slider-scroll-indicator-button-color); 41 | position: absolute; 42 | top: calc(50% - calc( var( --overflow-slider-scroll-indicator-button-height ) / 2 )); 43 | left: 0; 44 | border-radius: 3px; 45 | cursor: grab; 46 | &[data-is-grabbed="true"], 47 | &:hover { 48 | --overflow-slider-scroll-indicator-button-height: 6px; 49 | } 50 | // increase clickable area to fill container in y-axis 51 | &::after { 52 | content: ''; 53 | display: block; 54 | position: absolute; 55 | top: calc(-1 * var(--overflow-slider-scroll-indicator-padding)); 56 | bottom: calc(-1 * var(--overflow-slider-scroll-indicator-padding)); 57 | width: 100%; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/plugins/skip-links/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | import { generateId } from '../../core/utils'; 3 | 4 | const DEFAULT_TEXTS = { 5 | skipList: 'Skip list' 6 | }; 7 | 8 | const DEFAULT_CLASS_NAMES = { 9 | skipLink: 'screen-reader-text', 10 | skipLinkTarget: 'overflow-slider__skip-link-target', 11 | }; 12 | 13 | export type SkipLinkOptions = { 14 | texts: { 15 | skipList: string; 16 | }, 17 | classNames: { 18 | skipLink: string; 19 | skipLinkTarget: string; 20 | }, 21 | containerBefore: HTMLElement | null, 22 | containerAfter: HTMLElement | null, 23 | }; 24 | 25 | export default function SkipLinksPlugin( args?: DeepPartial ) { 26 | return ( slider: Slider ) => { 27 | const options = { 28 | texts: { 29 | ...DEFAULT_TEXTS, 30 | ...args?.texts || [] 31 | }, 32 | classNames: { 33 | ...DEFAULT_CLASS_NAMES, 34 | ...args?.classNames || [] 35 | }, 36 | containerBefore: args?.containerAfter ?? null, 37 | containerAfter: args?.containerAfter ?? null, 38 | }; 39 | 40 | const skipId = generateId( 'overflow-slider-skip' ); 41 | const skipLinkEl = document.createElement( 'a' ); 42 | skipLinkEl.setAttribute( 'href', `#${skipId}` ); 43 | skipLinkEl.textContent = options.texts.skipList; 44 | skipLinkEl.classList.add( options.classNames.skipLink ); 45 | 46 | const skipTargetEl = document.createElement( 'div' ); 47 | skipTargetEl.setAttribute( 'id', skipId ); 48 | skipTargetEl.setAttribute( 'tabindex', '-1' ); 49 | 50 | if ( options.containerBefore ) { 51 | options.containerBefore.parentNode?.insertBefore( skipLinkEl, options.containerBefore ); 52 | } else { 53 | slider.container.parentNode?.insertBefore( skipLinkEl, slider.container ); 54 | } 55 | if ( options.containerAfter ) { 56 | options.containerAfter.parentNode?.insertBefore( skipTargetEl, options.containerAfter.nextSibling ); 57 | } else { 58 | slider.container.parentNode?.insertBefore( skipTargetEl, slider.container.nextSibling ); 59 | } 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /dist/plugins/dots/index.min.js: -------------------------------------------------------------------------------- 1 | const t={dotDescription:"Page %d of %d"},e={dotsContainer:"overflow-slider__dots",dotsItem:"overflow-slider__dot-item"};function n(n){return i=>{var o,s,a;const l={type:null!==(o=null==n?void 0:n.type)&&void 0!==o?o:"slide",texts:Object.assign(Object.assign({},t),(null==n?void 0:n.texts)||[]),classNames:Object.assign(Object.assign({},e),(null==n?void 0:n.classNames)||[]),container:null!==(s=null==n?void 0:n.container)&&void 0!==s?s:null},r=document.createElement("div");r.classList.add(l.classNames.dotsContainer);let d=null;const c=()=>{r.setAttribute("data-has-content",i.details.hasOverflow.toString()),r.innerHTML="",console.log("buildDots");const t=document.createElement("ul"),e="view"===l.type?i.details.amountOfPages:i.details.slideCount,n="view"===l.type?i.details.currentPage:i.activeSlideIdx;if(!(e<=1)){for(let i=0;iu(i+1)),s.addEventListener("focus",()=>d=i+1),s.addEventListener("keydown",t=>{var n;const i=r.querySelector('[aria-pressed="true"]');if(!i)return;const o=parseInt(null!==(n=i.getAttribute("data-item"))&&void 0!==n?n:"1");if("ArrowLeft"===t.key){const t=o-1;if(t>0){const e=r.querySelector(`[data-item="${t}"]`);e&&e.focus(),u(t)}}if("ArrowRight"===t.key){const t=o+1;if(t<=e){const e=r.querySelector(`[data-item="${t}"]`);e&&e.focus(),u(t)}}})}if(r.appendChild(t),d){const t=r.querySelector(`[data-item="${d}"]`);t&&t.focus()}}},u=t=>{if(console.log("activateDot",t,"slider.details",i.details),"view"===l.type){const e=i.details.amountOfPages;let n=i.details.containerWidth*(t-1);if(t===e){n=i.details.scrollableAreaWidth-i.details.containerWidth}const o=i.options.rtl?-n:n;i.container.scrollTo({left:o,behavior:i.options.scrollBehavior})}else i.moveToSlide(t-1)};c(),l.container?l.container.appendChild(r):null===(a=i.container.parentNode)||void 0===a||a.insertBefore(r,i.container.nextSibling),i.on("scrollEnd",c),i.on("contentsChanged",c),i.on("containerSizeChanged",c),i.on("detailsChanged",c)}}export{n as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/dots/index.min.js: -------------------------------------------------------------------------------- 1 | const t={dotDescription:"Page %d of %d"},e={dotsContainer:"overflow-slider__dots",dotsItem:"overflow-slider__dot-item"};function n(n){return i=>{var o,s,a;const l={type:null!==(o=null==n?void 0:n.type)&&void 0!==o?o:"slide",texts:Object.assign(Object.assign({},t),(null==n?void 0:n.texts)||[]),classNames:Object.assign(Object.assign({},e),(null==n?void 0:n.classNames)||[]),container:null!==(s=null==n?void 0:n.container)&&void 0!==s?s:null},r=document.createElement("div");r.classList.add(l.classNames.dotsContainer);let d=null;const c=()=>{r.setAttribute("data-has-content",i.details.hasOverflow.toString()),r.innerHTML="",console.log("buildDots");const t=document.createElement("ul"),e="view"===l.type?i.details.amountOfPages:i.details.slideCount,n="view"===l.type?i.details.currentPage:i.activeSlideIdx;if(!(e<=1)){for(let i=0;iu(i+1)),s.addEventListener("focus",()=>d=i+1),s.addEventListener("keydown",t=>{var n;const i=r.querySelector('[aria-pressed="true"]');if(!i)return;const o=parseInt(null!==(n=i.getAttribute("data-item"))&&void 0!==n?n:"1");if("ArrowLeft"===t.key){const t=o-1;if(t>0){const e=r.querySelector(`[data-item="${t}"]`);e&&e.focus(),u(t)}}if("ArrowRight"===t.key){const t=o+1;if(t<=e){const e=r.querySelector(`[data-item="${t}"]`);e&&e.focus(),u(t)}}})}if(r.appendChild(t),d){const t=r.querySelector(`[data-item="${d}"]`);t&&t.focus()}}},u=t=>{if(console.log("activateDot",t,"slider.details",i.details),"view"===l.type){const e=i.details.amountOfPages;let n=i.details.containerWidth*(t-1);if(t===e){n=i.details.scrollableAreaWidth-i.details.containerWidth}const o=i.options.rtl?-n:n;i.container.scrollTo({left:o,behavior:i.options.scrollBehavior})}else i.moveToSlide(t-1)};c(),l.container?l.container.appendChild(r):null===(a=i.container.parentNode)||void 0===a||a.insertBefore(r,i.container.nextSibling),i.on("scrollEnd",c),i.on("contentsChanged",c),i.on("containerSizeChanged",c),i.on("detailsChanged",c)}}export{n as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/skip-links/index.esm.js: -------------------------------------------------------------------------------- 1 | function generateId(prefix, i = 1) { 2 | const id = `${prefix}-${i}`; 3 | if (document.getElementById(id)) { 4 | return generateId(prefix, i + 1); 5 | } 6 | return id; 7 | } 8 | 9 | const DEFAULT_TEXTS = { 10 | skipList: 'Skip list' 11 | }; 12 | const DEFAULT_CLASS_NAMES = { 13 | skipLink: 'screen-reader-text', 14 | skipLinkTarget: 'overflow-slider__skip-link-target', 15 | }; 16 | function SkipLinksPlugin(args) { 17 | return (slider) => { 18 | var _a, _b, _c, _d, _e, _f; 19 | const options = { 20 | texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), 21 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), 22 | containerBefore: (_a = args === null || args === void 0 ? void 0 : args.containerAfter) !== null && _a !== void 0 ? _a : null, 23 | containerAfter: (_b = args === null || args === void 0 ? void 0 : args.containerAfter) !== null && _b !== void 0 ? _b : null, 24 | }; 25 | const skipId = generateId('overflow-slider-skip'); 26 | const skipLinkEl = document.createElement('a'); 27 | skipLinkEl.setAttribute('href', `#${skipId}`); 28 | skipLinkEl.textContent = options.texts.skipList; 29 | skipLinkEl.classList.add(options.classNames.skipLink); 30 | const skipTargetEl = document.createElement('div'); 31 | skipTargetEl.setAttribute('id', skipId); 32 | skipTargetEl.setAttribute('tabindex', '-1'); 33 | if (options.containerBefore) { 34 | (_c = options.containerBefore.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(skipLinkEl, options.containerBefore); 35 | } 36 | else { 37 | (_d = slider.container.parentNode) === null || _d === void 0 ? void 0 : _d.insertBefore(skipLinkEl, slider.container); 38 | } 39 | if (options.containerAfter) { 40 | (_e = options.containerAfter.parentNode) === null || _e === void 0 ? void 0 : _e.insertBefore(skipTargetEl, options.containerAfter.nextSibling); 41 | } 42 | else { 43 | (_f = slider.container.parentNode) === null || _f === void 0 ? void 0 : _f.insertBefore(skipTargetEl, slider.container.nextSibling); 44 | } 45 | }; 46 | } 47 | 48 | export { SkipLinksPlugin as default }; 49 | -------------------------------------------------------------------------------- /docs/dist/plugins/skip-links/index.esm.js: -------------------------------------------------------------------------------- 1 | function generateId(prefix, i = 1) { 2 | const id = `${prefix}-${i}`; 3 | if (document.getElementById(id)) { 4 | return generateId(prefix, i + 1); 5 | } 6 | return id; 7 | } 8 | 9 | const DEFAULT_TEXTS = { 10 | skipList: 'Skip list' 11 | }; 12 | const DEFAULT_CLASS_NAMES = { 13 | skipLink: 'screen-reader-text', 14 | skipLinkTarget: 'overflow-slider__skip-link-target', 15 | }; 16 | function SkipLinksPlugin(args) { 17 | return (slider) => { 18 | var _a, _b, _c, _d, _e, _f; 19 | const options = { 20 | texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), 21 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), 22 | containerBefore: (_a = args === null || args === void 0 ? void 0 : args.containerAfter) !== null && _a !== void 0 ? _a : null, 23 | containerAfter: (_b = args === null || args === void 0 ? void 0 : args.containerAfter) !== null && _b !== void 0 ? _b : null, 24 | }; 25 | const skipId = generateId('overflow-slider-skip'); 26 | const skipLinkEl = document.createElement('a'); 27 | skipLinkEl.setAttribute('href', `#${skipId}`); 28 | skipLinkEl.textContent = options.texts.skipList; 29 | skipLinkEl.classList.add(options.classNames.skipLink); 30 | const skipTargetEl = document.createElement('div'); 31 | skipTargetEl.setAttribute('id', skipId); 32 | skipTargetEl.setAttribute('tabindex', '-1'); 33 | if (options.containerBefore) { 34 | (_c = options.containerBefore.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(skipLinkEl, options.containerBefore); 35 | } 36 | else { 37 | (_d = slider.container.parentNode) === null || _d === void 0 ? void 0 : _d.insertBefore(skipLinkEl, slider.container); 38 | } 39 | if (options.containerAfter) { 40 | (_e = options.containerAfter.parentNode) === null || _e === void 0 ? void 0 : _e.insertBefore(skipTargetEl, options.containerAfter.nextSibling); 41 | } 42 | else { 43 | (_f = slider.container.parentNode) === null || _f === void 0 ? void 0 : _f.insertBefore(skipTargetEl, slider.container.nextSibling); 44 | } 45 | }; 46 | } 47 | 48 | export { SkipLinksPlugin as default }; 49 | -------------------------------------------------------------------------------- /dist/plugins/arrows/index.min.js: -------------------------------------------------------------------------------- 1 | const t={buttonPrevious:"Previous items",buttonNext:"Next items"},e={prev:'',next:''},n={navContainer:"overflow-slider__arrows",prevButton:"overflow-slider__arrows-button overflow-slider__arrows-button--prev",nextButton:"overflow-slider__arrows-button overflow-slider__arrows-button--next"};function o(o){return i=>{var r,s,a,l,c,u,d;const v={texts:Object.assign(Object.assign({},t),(null==o?void 0:o.texts)||[]),icons:Object.assign(Object.assign({},e),(null==o?void 0:o.icons)||[]),classNames:Object.assign(Object.assign({},n),(null==o?void 0:o.classNames)||[]),container:null!==(r=null==o?void 0:o.container)&&void 0!==r?r:null,containerPrev:null!==(s=null==o?void 0:o.containerPrev)&&void 0!==s?s:null,containerNext:null!==(a=null==o?void 0:o.containerNext)&&void 0!==a?a:null,movementType:null!==(l=null==o?void 0:o.movementType)&&void 0!==l?l:"view"},b=document.createElement("div");b.classList.add(v.classNames.navContainer);const p=document.createElement("button");p.setAttribute("class",v.classNames.prevButton),p.setAttribute("type","button"),p.setAttribute("aria-label",v.texts.buttonPrevious),p.setAttribute("aria-controls",null!==(c=i.container.getAttribute("id"))&&void 0!==c?c:""),p.setAttribute("data-type","prev"),p.innerHTML=i.options.rtl?v.icons.next:v.icons.prev,p.addEventListener("click",()=>{"true"===p.getAttribute("data-has-content")&&("slide"===v.movementType?i.moveToSlideInDirection("prev"):i.moveToDirection("prev"))});const h=document.createElement("button");h.setAttribute("class",v.classNames.nextButton),h.setAttribute("type","button"),h.setAttribute("aria-label",v.texts.buttonNext),h.setAttribute("aria-controls",null!==(u=i.container.getAttribute("id"))&&void 0!==u?u:""),h.setAttribute("data-type","next"),h.innerHTML=i.options.rtl?v.icons.prev:v.icons.next,h.addEventListener("click",()=>{"true"===h.getAttribute("data-has-content")&&("slide"===v.movementType?i.moveToSlideInDirection("next"):i.moveToDirection("next"))}),b.appendChild(p),b.appendChild(h);const m=()=>{const t=i.getScrollLeft(),e=i.getInclusiveScrollWidth(),n=i.getInclusiveClientWidth();0===Math.floor(t)?p.setAttribute("data-has-content","false"):p.setAttribute("data-has-content","true");Math.abs(Math.floor(t+n)-Math.floor(e))<=1?h.setAttribute("data-has-content","false"):h.setAttribute("data-has-content","true")};v.containerNext&&v.containerPrev?(v.containerPrev.appendChild(p),v.containerNext.appendChild(h)):v.container?v.container.appendChild(b):null===(d=i.container.parentNode)||void 0===d||d.insertBefore(b,i.container.nextSibling),m(),i.on("scrollEnd",m),i.on("contentsChanged",m),i.on("containerSizeChanged",m)}}export{o as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/arrows/index.min.js: -------------------------------------------------------------------------------- 1 | const t={buttonPrevious:"Previous items",buttonNext:"Next items"},e={prev:'',next:''},n={navContainer:"overflow-slider__arrows",prevButton:"overflow-slider__arrows-button overflow-slider__arrows-button--prev",nextButton:"overflow-slider__arrows-button overflow-slider__arrows-button--next"};function o(o){return i=>{var r,s,a,l,c,u,d;const v={texts:Object.assign(Object.assign({},t),(null==o?void 0:o.texts)||[]),icons:Object.assign(Object.assign({},e),(null==o?void 0:o.icons)||[]),classNames:Object.assign(Object.assign({},n),(null==o?void 0:o.classNames)||[]),container:null!==(r=null==o?void 0:o.container)&&void 0!==r?r:null,containerPrev:null!==(s=null==o?void 0:o.containerPrev)&&void 0!==s?s:null,containerNext:null!==(a=null==o?void 0:o.containerNext)&&void 0!==a?a:null,movementType:null!==(l=null==o?void 0:o.movementType)&&void 0!==l?l:"view"},b=document.createElement("div");b.classList.add(v.classNames.navContainer);const p=document.createElement("button");p.setAttribute("class",v.classNames.prevButton),p.setAttribute("type","button"),p.setAttribute("aria-label",v.texts.buttonPrevious),p.setAttribute("aria-controls",null!==(c=i.container.getAttribute("id"))&&void 0!==c?c:""),p.setAttribute("data-type","prev"),p.innerHTML=i.options.rtl?v.icons.next:v.icons.prev,p.addEventListener("click",()=>{"true"===p.getAttribute("data-has-content")&&("slide"===v.movementType?i.moveToSlideInDirection("prev"):i.moveToDirection("prev"))});const h=document.createElement("button");h.setAttribute("class",v.classNames.nextButton),h.setAttribute("type","button"),h.setAttribute("aria-label",v.texts.buttonNext),h.setAttribute("aria-controls",null!==(u=i.container.getAttribute("id"))&&void 0!==u?u:""),h.setAttribute("data-type","next"),h.innerHTML=i.options.rtl?v.icons.prev:v.icons.next,h.addEventListener("click",()=>{"true"===h.getAttribute("data-has-content")&&("slide"===v.movementType?i.moveToSlideInDirection("next"):i.moveToDirection("next"))}),b.appendChild(p),b.appendChild(h);const m=()=>{const t=i.getScrollLeft(),e=i.getInclusiveScrollWidth(),n=i.getInclusiveClientWidth();0===Math.floor(t)?p.setAttribute("data-has-content","false"):p.setAttribute("data-has-content","true");Math.abs(Math.floor(t+n)-Math.floor(e))<=1?h.setAttribute("data-has-content","false"):h.setAttribute("data-has-content","true")};v.containerNext&&v.containerPrev?(v.containerPrev.appendChild(p),v.containerNext.appendChild(h)):v.container?v.container.appendChild(b):null===(d=i.container.parentNode)||void 0===d||d.insertBefore(b,i.container.nextSibling),m(),i.on("scrollEnd",m),i.on("contentsChanged",m),i.on("containerSizeChanged",m)}}export{o as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/scroll-indicator/index.min.js: -------------------------------------------------------------------------------- 1 | const t={scrollIndicator:"overflow-slider__scroll-indicator",scrollIndicatorBar:"overflow-slider__scroll-indicator-bar",scrollIndicatorButton:"overflow-slider__scroll-indicator-button"};function e(e){return o=>{var n,r,i;const a={classNames:Object.assign(Object.assign({},t),(null==e?void 0:e.classNames)||[]),container:null!==(n=null==e?void 0:e.container)&&void 0!==n?n:null},s=document.createElement("div");s.setAttribute("class",a.classNames.scrollIndicator),s.setAttribute("tabindex","0"),s.setAttribute("role","scrollbar"),s.setAttribute("aria-controls",null!==(r=o.container.getAttribute("id"))&&void 0!==r?r:""),s.setAttribute("aria-orientation","horizontal"),s.setAttribute("aria-valuemax","100"),s.setAttribute("aria-valuemin","0"),s.setAttribute("aria-valuenow","0");const l=document.createElement("div");l.setAttribute("class",a.classNames.scrollIndicatorBar);const c=document.createElement("div");c.setAttribute("class",a.classNames.scrollIndicatorButton),c.setAttribute("data-is-grabbed","false"),l.appendChild(c),s.appendChild(l);const d=()=>{s.setAttribute("data-has-overflow",o.details.hasOverflow.toString())};d();const u=()=>{const t=c.offsetWidth/o.details.containerWidth,e=o.getScrollLeft()*t;return o.options.rtl?l.offsetWidth-c.offsetWidth-e:e};let f=0;const v=()=>{f&&window.cancelAnimationFrame(f),f=window.requestAnimationFrame(()=>{const t=o.details.containerWidth/o.container.scrollWidth*100,e=u();c.style.width=`${t}%`,c.style.transform=`translateX(${e}px)`;const n=o.getScrollLeft()/(o.getInclusiveScrollWidth()-o.container.offsetWidth)*100;s.setAttribute("aria-valuenow",Math.round(Number.isNaN(n)?0:n).toString())})};a.container?a.container.appendChild(s):null===(i=o.container.parentNode)||void 0===i||i.insertBefore(s,o.container.nextSibling),v(),o.on("scroll",v),o.on("contentsChanged",v),o.on("containerSizeChanged",v),o.on("detailsChanged",d),s.addEventListener("keydown",t=>{"ArrowLeft"===t.key?o.moveToDirection("prev"):"ArrowRight"===t.key&&o.moveToDirection("next")});let b=!1,h=0,g=o.getScrollLeft();s.addEventListener("click",t=>{if(t.target==c)return;const e=c.offsetWidth,n=u(),r=n+e,i=t.pageX-s.getBoundingClientRect().left;Math.floor(i)Math.floor(r)&&(console.log("move right"),o.moveToDirection(o.options.rtl?"prev":"next"))});const m=t=>{b=!0;const e=t.pageX||t.touches[0].pageX;h=e-s.offsetLeft,g=o.getScrollLeft(),c.style.cursor="grabbing",c.setAttribute("data-is-grabbed","true"),t.preventDefault(),t.stopPropagation()},p=t=>{if(!b)return;t.preventDefault();const e=(t.pageX||t.touches[0].pageX)-s.offsetLeft,n=o.details.scrollableAreaWidth/s.offsetWidth,r=(e-h)*n,i=o.options.rtl?g-r:g+r;o.setScrollLeft(i)},w=()=>{b=!1,c.style.cursor="",c.setAttribute("data-is-grabbed","false")};c.addEventListener("mousedown",m),c.addEventListener("touchstart",m),window.addEventListener("mousemove",p),window.addEventListener("touchmove",p,{passive:!1}),window.addEventListener("mouseup",w),window.addEventListener("touchend",w)}}export{e as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/scroll-indicator/index.min.js: -------------------------------------------------------------------------------- 1 | const t={scrollIndicator:"overflow-slider__scroll-indicator",scrollIndicatorBar:"overflow-slider__scroll-indicator-bar",scrollIndicatorButton:"overflow-slider__scroll-indicator-button"};function e(e){return o=>{var n,r,i;const a={classNames:Object.assign(Object.assign({},t),(null==e?void 0:e.classNames)||[]),container:null!==(n=null==e?void 0:e.container)&&void 0!==n?n:null},s=document.createElement("div");s.setAttribute("class",a.classNames.scrollIndicator),s.setAttribute("tabindex","0"),s.setAttribute("role","scrollbar"),s.setAttribute("aria-controls",null!==(r=o.container.getAttribute("id"))&&void 0!==r?r:""),s.setAttribute("aria-orientation","horizontal"),s.setAttribute("aria-valuemax","100"),s.setAttribute("aria-valuemin","0"),s.setAttribute("aria-valuenow","0");const l=document.createElement("div");l.setAttribute("class",a.classNames.scrollIndicatorBar);const c=document.createElement("div");c.setAttribute("class",a.classNames.scrollIndicatorButton),c.setAttribute("data-is-grabbed","false"),l.appendChild(c),s.appendChild(l);const d=()=>{s.setAttribute("data-has-overflow",o.details.hasOverflow.toString())};d();const u=()=>{const t=c.offsetWidth/o.details.containerWidth,e=o.getScrollLeft()*t;return o.options.rtl?l.offsetWidth-c.offsetWidth-e:e};let f=0;const v=()=>{f&&window.cancelAnimationFrame(f),f=window.requestAnimationFrame(()=>{const t=o.details.containerWidth/o.container.scrollWidth*100,e=u();c.style.width=`${t}%`,c.style.transform=`translateX(${e}px)`;const n=o.getScrollLeft()/(o.getInclusiveScrollWidth()-o.container.offsetWidth)*100;s.setAttribute("aria-valuenow",Math.round(Number.isNaN(n)?0:n).toString())})};a.container?a.container.appendChild(s):null===(i=o.container.parentNode)||void 0===i||i.insertBefore(s,o.container.nextSibling),v(),o.on("scroll",v),o.on("contentsChanged",v),o.on("containerSizeChanged",v),o.on("detailsChanged",d),s.addEventListener("keydown",t=>{"ArrowLeft"===t.key?o.moveToDirection("prev"):"ArrowRight"===t.key&&o.moveToDirection("next")});let b=!1,h=0,g=o.getScrollLeft();s.addEventListener("click",t=>{if(t.target==c)return;const e=c.offsetWidth,n=u(),r=n+e,i=t.pageX-s.getBoundingClientRect().left;Math.floor(i)Math.floor(r)&&(console.log("move right"),o.moveToDirection(o.options.rtl?"prev":"next"))});const m=t=>{b=!0;const e=t.pageX||t.touches[0].pageX;h=e-s.offsetLeft,g=o.getScrollLeft(),c.style.cursor="grabbing",c.setAttribute("data-is-grabbed","true"),t.preventDefault(),t.stopPropagation()},p=t=>{if(!b)return;t.preventDefault();const e=(t.pageX||t.touches[0].pageX)-s.offsetLeft,n=o.details.scrollableAreaWidth/s.offsetWidth,r=(e-h)*n,i=o.options.rtl?g-r:g+r;o.setScrollLeft(i)},w=()=>{b=!1,c.style.cursor="",c.setAttribute("data-is-grabbed","false")};c.addEventListener("mousedown",m),c.addEventListener("touchstart",m),window.addEventListener("mousemove",p),window.addEventListener("touchmove",p,{passive:!1}),window.addEventListener("mouseup",w),window.addEventListener("touchend",w)}}export{e as default}; 2 | -------------------------------------------------------------------------------- /src/plugins/full-width/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | const DEFAULT_TARGET_WIDTH = ( slider: Slider ) => slider.container.parentElement?.offsetWidth ?? window.innerWidth; 4 | 5 | export type FullWidthOptions = { 6 | targetWidth?: ( slider: Slider ) => number, 7 | addMarginBefore: boolean, 8 | addMarginAfter: boolean, 9 | }; 10 | 11 | export default function FullWidthPlugin( args?: DeepPartial ) { 12 | return ( slider: Slider ) => { 13 | 14 | const options = { 15 | targetWidth: args?.targetWidth ?? undefined, 16 | addMarginBefore: args?.addMarginBefore ?? true, 17 | addMarginAfter: args?.addMarginAfter ?? true, 18 | }; 19 | 20 | if ( typeof slider.options.targetWidth !== 'function' ) { 21 | slider.options.targetWidth = options.targetWidth ?? DEFAULT_TARGET_WIDTH; 22 | } 23 | 24 | const resolveTargetWidth = () => { 25 | if ( typeof slider.options.targetWidth === 'function' ) { 26 | return slider.options.targetWidth; 27 | } 28 | return options.targetWidth ?? DEFAULT_TARGET_WIDTH; 29 | }; 30 | 31 | const update = () => { 32 | const slides = slider.container.querySelectorAll( slider.options.slidesSelector ); 33 | 34 | if ( ! slides.length ) { 35 | return; 36 | } 37 | 38 | const targetWidthFn = resolveTargetWidth(); 39 | const rawMargin = ( window.innerWidth - targetWidthFn( slider ) ) / 2; 40 | const marginAmount = Math.max( 0, Math.floor( rawMargin ) ); 41 | const marginValue = marginAmount ? `${marginAmount}px` : ''; 42 | 43 | slides.forEach( ( slide ) => { 44 | const element = slide as HTMLElement; 45 | element.style.marginInlineStart = ''; 46 | element.style.marginInlineEnd = ''; 47 | } ); 48 | 49 | const firstSlide = slides[0] as HTMLElement; 50 | const lastSlide = slides[slides.length - 1] as HTMLElement; 51 | 52 | if ( options.addMarginBefore ) { 53 | firstSlide.style.marginInlineStart = marginValue; 54 | slider.container.style.setProperty( 'scroll-padding-inline-start', marginValue || '0px' ); 55 | } 56 | else { 57 | slider.container.style.removeProperty( 'scroll-padding-inline-start' ); 58 | } 59 | if ( options.addMarginAfter ) { 60 | lastSlide.style.marginInlineEnd = marginValue; 61 | slider.container.style.setProperty( 'scroll-padding-inline-end', marginValue || '0px' ); 62 | } 63 | else { 64 | slider.container.style.removeProperty( 'scroll-padding-inline-end' ); 65 | } 66 | 67 | slider.container.setAttribute( 'data-full-width-offset', `${marginAmount}` ); 68 | setCSS( targetWidthFn ); 69 | slider.emit( 'fullWidthPluginUpdate' ); 70 | }; 71 | 72 | const setCSS = ( targetWidthFn: ( slider: Slider ) => number ) => { 73 | const width = targetWidthFn( slider ); 74 | slider.options.cssVariableContainer.style.setProperty('--slider-container-target-width', `${width}px`); 75 | }; 76 | 77 | update(); 78 | slider.on( 'contentsChanged', update ); 79 | slider.on( 'containerSizeChanged', update ); 80 | window.addEventListener( 'resize', update ); 81 | }; 82 | } 83 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import path from 'path'; 3 | import glob from 'glob'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import commonjs from '@rollup/plugin-commonjs'; 6 | import typescript from '@rollup/plugin-typescript'; 7 | import postcss from 'rollup-plugin-postcss'; 8 | import copy from 'rollup-plugin-copy'; 9 | import terser from '@rollup/plugin-terser'; 10 | import { dts } from 'rollup-plugin-dts'; 11 | 12 | // Define core entry and plugin entries separately 13 | const coreInput = 'src/index.ts'; 14 | const pluginInputs = glob 15 | .sync('src/plugins/*/index.ts') 16 | .reduce((out, file) => { 17 | const name = path.basename(path.dirname(file)); 18 | out[name] = file; 19 | return out; 20 | }, {}); 21 | // For declarations, include both core and plugins 22 | const allEntries = { index: coreInput, ...pluginInputs }; 23 | 24 | const plugins = [ 25 | typescript({ 26 | tsconfig: './tsconfig.json', 27 | declaration: false, 28 | declarationMap: false, 29 | }), 30 | nodeResolve(), 31 | commonjs({ include: 'node_modules/**' }), 32 | postcss({ 33 | extract: 'overflow-slider.css', 34 | plugins: [ require('autoprefixer'), require('cssnano')({ preset: 'default' }) ], 35 | minimize: true, 36 | sourceMap: false, 37 | extensions: ['.scss', '.css'], 38 | }), 39 | copy({ targets: [{ src: 'dist/*', dest: 'docs/dist' }], hook: 'writeBundle' }), 40 | copy({ 41 | targets: [ 42 | { src: 'src/mixins.scss', dest: 'dist' }, 43 | { src: 'dist/*', dest: 'docs/dist' }, 44 | ], 45 | }), 46 | ]; 47 | 48 | // Helper for plugin file naming 49 | function entryFmt(ext, isMin) { 50 | return chunk => { 51 | const id = chunk.facadeModuleId; 52 | const pluginName = path.basename(path.dirname(id)); 53 | return `plugins/${pluginName}/index.${ext}.js`; 54 | }; 55 | } 56 | 57 | export default [ 58 | // —— Flat core ESM build (no internal imports) —— 59 | { 60 | input: coreInput, 61 | output: { file: 'dist/index.esm.js', format: 'es', sourcemap: true, inlineDynamicImports: true }, 62 | plugins, 63 | }, 64 | 65 | // —— Flat core CJS build (minified) —— 66 | { 67 | input: coreInput, 68 | output: { file: 'dist/index.min.js', format: 'cjs', sourcemap: true, inlineDynamicImports: true, exports: 'auto' }, 69 | plugins: [...plugins, terser()], 70 | }, 71 | 72 | // —— Per-plugin ESM build —— 73 | { 74 | input: pluginInputs, 75 | output: { dir: 'dist', format: 'es', entryFileNames: entryFmt('esm', false) }, 76 | plugins, 77 | }, 78 | 79 | // —— Per-plugin minified build —— 80 | { 81 | input: pluginInputs, 82 | output: { dir: 'dist', format: 'es', entryFileNames: entryFmt('min', true) }, 83 | plugins: [...plugins, terser()], 84 | }, 85 | 86 | // —— Declarations bundle (multi-entry) —— 87 | { 88 | input: allEntries, 89 | external: [/\.scss$/], 90 | output: { 91 | dir: 'dist', 92 | format: 'es', 93 | preserveModules: true, 94 | preserveModulesRoot: 'src', 95 | entryFileNames: chunk => { 96 | if (chunk.name === 'index') return 'index.d.ts'; 97 | const pluginName = path.basename(path.dirname(chunk.facadeModuleId)); 98 | return `plugins/${pluginName}/index.d.ts`; 99 | }, 100 | }, 101 | plugins: [dts()], 102 | }, 103 | ]; 104 | -------------------------------------------------------------------------------- /dist/plugins/autoplay/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e){return t=>{var n,o,l,a,i,r,s,u,d,c,v,p,m,y,f;const h={delayInMs:null!==(n=null==e?void 0:e.delayInMs)&&void 0!==n?n:5e3,texts:{play:null!==(l=null===(o=null==e?void 0:e.texts)||void 0===o?void 0:o.play)&&void 0!==l?l:"Play",pause:null!==(i=null===(a=null==e?void 0:e.texts)||void 0===a?void 0:a.pause)&&void 0!==i?i:"Pause"},icons:{play:null!==(s=null===(r=null==e?void 0:e.icons)||void 0===r?void 0:r.play)&&void 0!==s?s:'',pause:null!==(d=null===(u=null==e?void 0:e.icons)||void 0===u?void 0:u.pause)&&void 0!==d?d:''},container:null!==(c=null==e?void 0:e.container)&&void 0!==c?c:null,classNames:{autoplayButton:null!==(p=null===(v=null==e?void 0:e.classNames)||void 0===v?void 0:v.autoplayButton)&&void 0!==p?p:"overflow-slider__autoplay"},movementType:null!==(m=null==e?void 0:e.movementType)&&void 0!==m?m:"view",stopOnHover:null===(y=null==e?void 0:e.stopOnHover)||void 0===y||y,loop:null===(f=null==e?void 0:e.loop)||void 0===f||f};let g=null,w=null,I=0,x=!1;if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return;const M=(()=>{var e;const n=document.createElement("button");n.type="button",n.className=h.classNames.autoplayButton,E(n),n.style.setProperty("--autoplay-delay-progress","0"),h.container?h.container.appendChild(n):null===(e=t.container.parentElement)||void 0===e||e.insertBefore(n,t.container),n.addEventListener("click",()=>{g?(x=!0,C()):(x=!1,T())});const o=["focusin"];h.stopOnHover&&o.push("mouseenter");const l=["focusout"];return h.stopOnHover&&l.push("mouseleave"),o.forEach(e=>t.container.addEventListener(e,()=>{g&&C()})),l.forEach(e=>t.container.addEventListener(e,()=>{g||x||T()})),n})();function b(){if("view"===h.movementType){t.getScrollLeft()+t.getInclusiveClientWidth()>=t.getInclusiveScrollWidth()?h.loop?t.moveToSlide(0):(C(),M.style.setProperty("--autoplay-delay-progress","0")):t.moveToDirection("next")}else{const e=(t.activeSlideIdx+1)%t.details.slideCount;t.canMoveToSlide(e)?t.moveToSlide(e):h.loop?t.moveToSlide(0):(C(),M.style.setProperty("--autoplay-delay-progress","0"))}I=performance.now(),null!==w&&cancelAnimationFrame(w),A()}function A(){const e=performance.now(),t=Math.min((e-I)/h.delayInMs*100,100);M.style.setProperty("--autoplay-delay-progress",`${Math.round(t)}`),t<100&&(w=requestAnimationFrame(A))}function T(){g&&clearInterval(g),w&&cancelAnimationFrame(w),function(e){e.setAttribute("aria-pressed","true"),e.setAttribute("aria-label",h.texts.pause);const t=document.createRange().createContextualFragment(h.icons.pause);e.innerHTML="",e.appendChild(t)}(M),I=performance.now(),A(),g=window.setInterval(b,h.delayInMs)}function C(e=!1){g&&clearInterval(g),w&&cancelAnimationFrame(w),g=w=null,e&&(x=!0),E(M)}function E(e){e.setAttribute("aria-pressed","false"),e.setAttribute("aria-label",h.texts.play);const t=document.createRange().createContextualFragment(h.icons.play);e.innerHTML="",e.appendChild(t),e.style.setProperty("--autoplay-delay-progress","0")}T(),new IntersectionObserver(e=>{for(const t of e)!t.isIntersecting&&g?C():!t.isIntersecting||g||x||T()},{threshold:0}).observe(t.container)}}export{e as default}; 2 | -------------------------------------------------------------------------------- /docs/dist/plugins/autoplay/index.min.js: -------------------------------------------------------------------------------- 1 | function e(e){return t=>{var n,o,l,a,i,r,s,u,d,c,v,p,m,y,f;const h={delayInMs:null!==(n=null==e?void 0:e.delayInMs)&&void 0!==n?n:5e3,texts:{play:null!==(l=null===(o=null==e?void 0:e.texts)||void 0===o?void 0:o.play)&&void 0!==l?l:"Play",pause:null!==(i=null===(a=null==e?void 0:e.texts)||void 0===a?void 0:a.pause)&&void 0!==i?i:"Pause"},icons:{play:null!==(s=null===(r=null==e?void 0:e.icons)||void 0===r?void 0:r.play)&&void 0!==s?s:'',pause:null!==(d=null===(u=null==e?void 0:e.icons)||void 0===u?void 0:u.pause)&&void 0!==d?d:''},container:null!==(c=null==e?void 0:e.container)&&void 0!==c?c:null,classNames:{autoplayButton:null!==(p=null===(v=null==e?void 0:e.classNames)||void 0===v?void 0:v.autoplayButton)&&void 0!==p?p:"overflow-slider__autoplay"},movementType:null!==(m=null==e?void 0:e.movementType)&&void 0!==m?m:"view",stopOnHover:null===(y=null==e?void 0:e.stopOnHover)||void 0===y||y,loop:null===(f=null==e?void 0:e.loop)||void 0===f||f};let g=null,w=null,I=0,x=!1;if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return;const M=(()=>{var e;const n=document.createElement("button");n.type="button",n.className=h.classNames.autoplayButton,E(n),n.style.setProperty("--autoplay-delay-progress","0"),h.container?h.container.appendChild(n):null===(e=t.container.parentElement)||void 0===e||e.insertBefore(n,t.container),n.addEventListener("click",()=>{g?(x=!0,C()):(x=!1,T())});const o=["focusin"];h.stopOnHover&&o.push("mouseenter");const l=["focusout"];return h.stopOnHover&&l.push("mouseleave"),o.forEach(e=>t.container.addEventListener(e,()=>{g&&C()})),l.forEach(e=>t.container.addEventListener(e,()=>{g||x||T()})),n})();function b(){if("view"===h.movementType){t.getScrollLeft()+t.getInclusiveClientWidth()>=t.getInclusiveScrollWidth()?h.loop?t.moveToSlide(0):(C(),M.style.setProperty("--autoplay-delay-progress","0")):t.moveToDirection("next")}else{const e=(t.activeSlideIdx+1)%t.details.slideCount;t.canMoveToSlide(e)?t.moveToSlide(e):h.loop?t.moveToSlide(0):(C(),M.style.setProperty("--autoplay-delay-progress","0"))}I=performance.now(),null!==w&&cancelAnimationFrame(w),A()}function A(){const e=performance.now(),t=Math.min((e-I)/h.delayInMs*100,100);M.style.setProperty("--autoplay-delay-progress",`${Math.round(t)}`),t<100&&(w=requestAnimationFrame(A))}function T(){g&&clearInterval(g),w&&cancelAnimationFrame(w),function(e){e.setAttribute("aria-pressed","true"),e.setAttribute("aria-label",h.texts.pause);const t=document.createRange().createContextualFragment(h.icons.pause);e.innerHTML="",e.appendChild(t)}(M),I=performance.now(),A(),g=window.setInterval(b,h.delayInMs)}function C(e=!1){g&&clearInterval(g),w&&cancelAnimationFrame(w),g=w=null,e&&(x=!0),E(M)}function E(e){e.setAttribute("aria-pressed","false"),e.setAttribute("aria-label",h.texts.play);const t=document.createRange().createContextualFragment(h.icons.play);e.innerHTML="",e.appendChild(t),e.style.setProperty("--autoplay-delay-progress","0")}T(),new IntersectionObserver(e=>{for(const t of e)!t.isIntersecting&&g?C():!t.isIntersecting||g||x||T()},{threshold:0}).observe(t.container)}}export{e as default}; 2 | -------------------------------------------------------------------------------- /dist/plugins/infinite-scroll/index.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} InfiniteScrollOptions 3 | * @property {number} [lookAheadCount=1] Number of slides to look ahead when deciding to reparent. 4 | */ 5 | /** 6 | * Creates an infinite scroll plugin for a slider that re-parents multiple slides 7 | * before hitting the container edge, to avoid blank space and keep the same 8 | * active slide visible. 9 | * 10 | * @param {InfiniteScrollOptions} [options] Plugin configuration. 11 | * @returns {SliderPlugin} The configured slider plugin. 12 | */ 13 | function InfiniteScrollPlugin(options = {}) { 14 | const { lookAheadCount = 1 } = options; 15 | return (slider) => { 16 | const { container, options: sliderOpts } = slider; 17 | /** 18 | * Sum widths of the first or last N slides for lookahead. 19 | * 20 | * @param {HTMLElement[]} slides List of slide elements. 21 | * @param {boolean} fromEnd If true, sum last N; otherwise, first N. 22 | * @returns {number} Total pixel width of N slides. 23 | */ 24 | function getLookAheadWidth(slides, fromEnd) { 25 | const slice = fromEnd 26 | ? slides.slice(-lookAheadCount) 27 | : slides.slice(0, lookAheadCount); 28 | return slice.reduce((total, slide) => total + slide.offsetWidth, 0); 29 | } 30 | /** 31 | * Handler for slider.scrollEnd event that re-parents slides 32 | * and retains the active slide element by recalculating its 33 | * new index after DOM shifts. 34 | */ 35 | function onScroll() { 36 | const activeSlideIdx = slider.activeSlideIdx; 37 | const scrollLeft = slider.getScrollLeft(); 38 | const viewportWidth = slider.getInclusiveClientWidth(); 39 | const totalWidth = slider.getInclusiveScrollWidth(); 40 | // Grab current slide elements 41 | let slides = Array.from(container.querySelectorAll(sliderOpts.slidesSelector)); 42 | if (slides.length === 0) 43 | return; 44 | // Store reference to currently active slide element 45 | const activeSlideEl = slides[activeSlideIdx]; 46 | const aheadRight = getLookAheadWidth(slides, false); 47 | const aheadLeft = getLookAheadWidth(slides, true); 48 | // 🐆 Tip: Batch DOM reads/writes inside requestAnimationFrame to avoid thrashing. 49 | if (scrollLeft + viewportWidth >= totalWidth - aheadRight) { 50 | for (let i = 0; i < lookAheadCount && slides.length; i++) { 51 | container.append(slides.shift()); 52 | } 53 | } 54 | else if (scrollLeft <= aheadLeft) { 55 | for (let i = 0; i < lookAheadCount && slides.length; i++) { 56 | container.prepend(slides.pop()); 57 | } 58 | } 59 | slider.setActiveSlideIdx(); 60 | // Re-query slides after DOM mutation 61 | slides = Array.from(container.querySelectorAll(sliderOpts.slidesSelector)); 62 | const newIndex = slides.indexOf(activeSlideEl); 63 | slides[newIndex]; 64 | if (newIndex >= 0 && slider.canMoveToSlide(newIndex)) { 65 | slider.moveToSlide(newIndex); 66 | } 67 | else { 68 | slider.snapToClosestSlide('next'); 69 | } 70 | } 71 | slider.on('scrollEnd', onScroll); 72 | }; 73 | } 74 | 75 | export { InfiniteScrollPlugin as default }; 76 | -------------------------------------------------------------------------------- /dist/plugins/full-width/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TARGET_WIDTH = (slider) => { var _a, _b; return (_b = (_a = slider.container.parentElement) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : window.innerWidth; }; 2 | function FullWidthPlugin(args) { 3 | return (slider) => { 4 | var _a, _b, _c, _d; 5 | const options = { 6 | targetWidth: (_a = args === null || args === void 0 ? void 0 : args.targetWidth) !== null && _a !== void 0 ? _a : undefined, 7 | addMarginBefore: (_b = args === null || args === void 0 ? void 0 : args.addMarginBefore) !== null && _b !== void 0 ? _b : true, 8 | addMarginAfter: (_c = args === null || args === void 0 ? void 0 : args.addMarginAfter) !== null && _c !== void 0 ? _c : true, 9 | }; 10 | if (typeof slider.options.targetWidth !== 'function') { 11 | slider.options.targetWidth = (_d = options.targetWidth) !== null && _d !== void 0 ? _d : DEFAULT_TARGET_WIDTH; 12 | } 13 | const resolveTargetWidth = () => { 14 | var _a; 15 | if (typeof slider.options.targetWidth === 'function') { 16 | return slider.options.targetWidth; 17 | } 18 | return (_a = options.targetWidth) !== null && _a !== void 0 ? _a : DEFAULT_TARGET_WIDTH; 19 | }; 20 | const update = () => { 21 | const slides = slider.container.querySelectorAll(slider.options.slidesSelector); 22 | if (!slides.length) { 23 | return; 24 | } 25 | const targetWidthFn = resolveTargetWidth(); 26 | const rawMargin = (window.innerWidth - targetWidthFn(slider)) / 2; 27 | const marginAmount = Math.max(0, Math.floor(rawMargin)); 28 | const marginValue = marginAmount ? `${marginAmount}px` : ''; 29 | slides.forEach((slide) => { 30 | const element = slide; 31 | element.style.marginInlineStart = ''; 32 | element.style.marginInlineEnd = ''; 33 | }); 34 | const firstSlide = slides[0]; 35 | const lastSlide = slides[slides.length - 1]; 36 | if (options.addMarginBefore) { 37 | firstSlide.style.marginInlineStart = marginValue; 38 | slider.container.style.setProperty('scroll-padding-inline-start', marginValue || '0px'); 39 | } 40 | else { 41 | slider.container.style.removeProperty('scroll-padding-inline-start'); 42 | } 43 | if (options.addMarginAfter) { 44 | lastSlide.style.marginInlineEnd = marginValue; 45 | slider.container.style.setProperty('scroll-padding-inline-end', marginValue || '0px'); 46 | } 47 | else { 48 | slider.container.style.removeProperty('scroll-padding-inline-end'); 49 | } 50 | slider.container.setAttribute('data-full-width-offset', `${marginAmount}`); 51 | setCSS(targetWidthFn); 52 | slider.emit('fullWidthPluginUpdate'); 53 | }; 54 | const setCSS = (targetWidthFn) => { 55 | const width = targetWidthFn(slider); 56 | slider.options.cssVariableContainer.style.setProperty('--slider-container-target-width', `${width}px`); 57 | }; 58 | update(); 59 | slider.on('contentsChanged', update); 60 | slider.on('containerSizeChanged', update); 61 | window.addEventListener('resize', update); 62 | }; 63 | } 64 | 65 | export { FullWidthPlugin as default }; 66 | -------------------------------------------------------------------------------- /docs/dist/plugins/infinite-scroll/index.esm.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {Object} InfiniteScrollOptions 3 | * @property {number} [lookAheadCount=1] Number of slides to look ahead when deciding to reparent. 4 | */ 5 | /** 6 | * Creates an infinite scroll plugin for a slider that re-parents multiple slides 7 | * before hitting the container edge, to avoid blank space and keep the same 8 | * active slide visible. 9 | * 10 | * @param {InfiniteScrollOptions} [options] Plugin configuration. 11 | * @returns {SliderPlugin} The configured slider plugin. 12 | */ 13 | function InfiniteScrollPlugin(options = {}) { 14 | const { lookAheadCount = 1 } = options; 15 | return (slider) => { 16 | const { container, options: sliderOpts } = slider; 17 | /** 18 | * Sum widths of the first or last N slides for lookahead. 19 | * 20 | * @param {HTMLElement[]} slides List of slide elements. 21 | * @param {boolean} fromEnd If true, sum last N; otherwise, first N. 22 | * @returns {number} Total pixel width of N slides. 23 | */ 24 | function getLookAheadWidth(slides, fromEnd) { 25 | const slice = fromEnd 26 | ? slides.slice(-lookAheadCount) 27 | : slides.slice(0, lookAheadCount); 28 | return slice.reduce((total, slide) => total + slide.offsetWidth, 0); 29 | } 30 | /** 31 | * Handler for slider.scrollEnd event that re-parents slides 32 | * and retains the active slide element by recalculating its 33 | * new index after DOM shifts. 34 | */ 35 | function onScroll() { 36 | const activeSlideIdx = slider.activeSlideIdx; 37 | const scrollLeft = slider.getScrollLeft(); 38 | const viewportWidth = slider.getInclusiveClientWidth(); 39 | const totalWidth = slider.getInclusiveScrollWidth(); 40 | // Grab current slide elements 41 | let slides = Array.from(container.querySelectorAll(sliderOpts.slidesSelector)); 42 | if (slides.length === 0) 43 | return; 44 | // Store reference to currently active slide element 45 | const activeSlideEl = slides[activeSlideIdx]; 46 | const aheadRight = getLookAheadWidth(slides, false); 47 | const aheadLeft = getLookAheadWidth(slides, true); 48 | // 🐆 Tip: Batch DOM reads/writes inside requestAnimationFrame to avoid thrashing. 49 | if (scrollLeft + viewportWidth >= totalWidth - aheadRight) { 50 | for (let i = 0; i < lookAheadCount && slides.length; i++) { 51 | container.append(slides.shift()); 52 | } 53 | } 54 | else if (scrollLeft <= aheadLeft) { 55 | for (let i = 0; i < lookAheadCount && slides.length; i++) { 56 | container.prepend(slides.pop()); 57 | } 58 | } 59 | slider.setActiveSlideIdx(); 60 | // Re-query slides after DOM mutation 61 | slides = Array.from(container.querySelectorAll(sliderOpts.slidesSelector)); 62 | const newIndex = slides.indexOf(activeSlideEl); 63 | slides[newIndex]; 64 | if (newIndex >= 0 && slider.canMoveToSlide(newIndex)) { 65 | slider.moveToSlide(newIndex); 66 | } 67 | else { 68 | slider.snapToClosestSlide('next'); 69 | } 70 | } 71 | slider.on('scrollEnd', onScroll); 72 | }; 73 | } 74 | 75 | export { InfiniteScrollPlugin as default }; 76 | -------------------------------------------------------------------------------- /docs/dist/plugins/full-width/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TARGET_WIDTH = (slider) => { var _a, _b; return (_b = (_a = slider.container.parentElement) === null || _a === void 0 ? void 0 : _a.offsetWidth) !== null && _b !== void 0 ? _b : window.innerWidth; }; 2 | function FullWidthPlugin(args) { 3 | return (slider) => { 4 | var _a, _b, _c, _d; 5 | const options = { 6 | targetWidth: (_a = args === null || args === void 0 ? void 0 : args.targetWidth) !== null && _a !== void 0 ? _a : undefined, 7 | addMarginBefore: (_b = args === null || args === void 0 ? void 0 : args.addMarginBefore) !== null && _b !== void 0 ? _b : true, 8 | addMarginAfter: (_c = args === null || args === void 0 ? void 0 : args.addMarginAfter) !== null && _c !== void 0 ? _c : true, 9 | }; 10 | if (typeof slider.options.targetWidth !== 'function') { 11 | slider.options.targetWidth = (_d = options.targetWidth) !== null && _d !== void 0 ? _d : DEFAULT_TARGET_WIDTH; 12 | } 13 | const resolveTargetWidth = () => { 14 | var _a; 15 | if (typeof slider.options.targetWidth === 'function') { 16 | return slider.options.targetWidth; 17 | } 18 | return (_a = options.targetWidth) !== null && _a !== void 0 ? _a : DEFAULT_TARGET_WIDTH; 19 | }; 20 | const update = () => { 21 | const slides = slider.container.querySelectorAll(slider.options.slidesSelector); 22 | if (!slides.length) { 23 | return; 24 | } 25 | const targetWidthFn = resolveTargetWidth(); 26 | const rawMargin = (window.innerWidth - targetWidthFn(slider)) / 2; 27 | const marginAmount = Math.max(0, Math.floor(rawMargin)); 28 | const marginValue = marginAmount ? `${marginAmount}px` : ''; 29 | slides.forEach((slide) => { 30 | const element = slide; 31 | element.style.marginInlineStart = ''; 32 | element.style.marginInlineEnd = ''; 33 | }); 34 | const firstSlide = slides[0]; 35 | const lastSlide = slides[slides.length - 1]; 36 | if (options.addMarginBefore) { 37 | firstSlide.style.marginInlineStart = marginValue; 38 | slider.container.style.setProperty('scroll-padding-inline-start', marginValue || '0px'); 39 | } 40 | else { 41 | slider.container.style.removeProperty('scroll-padding-inline-start'); 42 | } 43 | if (options.addMarginAfter) { 44 | lastSlide.style.marginInlineEnd = marginValue; 45 | slider.container.style.setProperty('scroll-padding-inline-end', marginValue || '0px'); 46 | } 47 | else { 48 | slider.container.style.removeProperty('scroll-padding-inline-end'); 49 | } 50 | slider.container.setAttribute('data-full-width-offset', `${marginAmount}`); 51 | setCSS(targetWidthFn); 52 | slider.emit('fullWidthPluginUpdate'); 53 | }; 54 | const setCSS = (targetWidthFn) => { 55 | const width = targetWidthFn(slider); 56 | slider.options.cssVariableContainer.style.setProperty('--slider-container-target-width', `${width}px`); 57 | }; 58 | update(); 59 | slider.on('contentsChanged', update); 60 | slider.on('containerSizeChanged', update); 61 | window.addEventListener('resize', update); 62 | }; 63 | } 64 | 65 | export { FullWidthPlugin as default }; 66 | -------------------------------------------------------------------------------- /src/plugins/fade/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | export type FadeOptions = { 4 | classNames: { 5 | fadeItem: string; 6 | fadeItemStart: string; 7 | fadeItemEnd: string; 8 | }, 9 | container: HTMLElement | null, 10 | containerStart: HTMLElement | null, 11 | containerEnd: HTMLElement | null, 12 | }; 13 | 14 | export default function FadePlugin( args?: DeepPartial ) { 15 | return ( slider: Slider ) => { 16 | 17 | const options = { 18 | classNames: { 19 | fadeItem: 'overflow-slider-fade', 20 | fadeItemStart: 'overflow-slider-fade--start', 21 | fadeItemEnd: 'overflow-slider-fade--end', 22 | }, 23 | container: args?.container ?? null, 24 | containerStart: args?.containerStart ?? null, 25 | containerEnd: args?.containerEnd ?? null, 26 | }; 27 | 28 | const fadeItemStart = document.createElement( 'div' ); 29 | fadeItemStart.classList.add( options.classNames.fadeItem, options.classNames.fadeItemStart ); 30 | fadeItemStart.setAttribute( 'aria-hidden', 'true' ); 31 | fadeItemStart.setAttribute( 'tabindex', '-1' ); 32 | 33 | const fadeItemEnd = document.createElement( 'div' ); 34 | fadeItemEnd.classList.add( options.classNames.fadeItem, options.classNames.fadeItemEnd ); 35 | fadeItemEnd.setAttribute( 'aria-hidden', 'true' ); 36 | fadeItemEnd.setAttribute( 'tabindex', '-1' ); 37 | 38 | if ( options.containerStart ) { 39 | options.containerStart.appendChild( fadeItemStart ); 40 | } else if ( options.container ) { 41 | options.container.appendChild( fadeItemStart ); 42 | } 43 | 44 | if ( options.containerEnd ) { 45 | options.containerEnd.appendChild( fadeItemEnd ); 46 | } else if ( options.container ) { 47 | options.container.appendChild( fadeItemEnd ); 48 | } 49 | 50 | const hasFadeAtStart = () => { 51 | return slider.getScrollLeft() > fadeItemStart.offsetWidth; 52 | } 53 | 54 | const fadeAtStartOpacity = () => { 55 | const position = slider.getScrollLeft(); 56 | if ( Math.floor( position ) <= Math.floor( fadeItemStart.offsetWidth ) ) { 57 | return position / Math.max(fadeItemStart.offsetWidth, 1); 58 | } 59 | return 1; 60 | } 61 | 62 | const hasFadeAtEnd = () => { 63 | return Math.floor( slider.getScrollLeft() ) < Math.floor( slider.getInclusiveScrollWidth() - slider.getInclusiveClientWidth() - fadeItemEnd.offsetWidth ); 64 | } 65 | 66 | const fadeAtEndOpacity = () => { 67 | const position = slider.getScrollLeft(); 68 | const maxPosition = slider.getInclusiveScrollWidth() - slider.getInclusiveClientWidth(); 69 | const maxFadePosition = maxPosition - fadeItemEnd.offsetWidth; 70 | if ( Math.floor( position ) >= Math.floor( maxFadePosition ) ) { 71 | return ( ( maxFadePosition - position ) / Math.max(fadeItemEnd.offsetWidth, 1) ) + 1; 72 | } 73 | return 1; 74 | } 75 | 76 | const update = () => { 77 | fadeItemStart.setAttribute( 'data-has-fade', hasFadeAtStart().toString() ); 78 | fadeItemStart.style.opacity = fadeAtStartOpacity().toString(); 79 | fadeItemEnd.setAttribute( 'data-has-fade', hasFadeAtEnd().toString() ); 80 | fadeItemEnd.style.opacity = fadeAtEndOpacity().toString(); 81 | }; 82 | 83 | update(); 84 | slider.on( 'created', update ); 85 | slider.on( 'contentsChanged', update ); 86 | slider.on( 'containerSizeChanged', update ); 87 | slider.on( 'scrollEnd', update ); 88 | slider.on( 'scrollStart', update ); 89 | let requestId = 0; 90 | const debouncedUpdate = () => { 91 | if ( requestId ) { 92 | window.cancelAnimationFrame( requestId ); 93 | } 94 | requestId = window.requestAnimationFrame(() => { 95 | update(); 96 | }); 97 | }; 98 | slider.on('scroll', debouncedUpdate); 99 | 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /dist/plugins/core/index.d.ts: -------------------------------------------------------------------------------- 1 | type Slider = { 2 | container: HTMLElement; 3 | slides: HTMLElement[]; 4 | emit: (name: H | SliderHooks) => void; 5 | moveToDirection: (direction: 'prev' | 'next') => void; 6 | moveToSlideInDirection: (direction: 'prev' | 'next') => void; 7 | snapToClosestSlide: (direction: 'prev' | 'next') => void; 8 | moveToSlide: (index: number) => void; 9 | canMoveToSlide: (index: number) => boolean; 10 | getInclusiveScrollWidth: () => number; 11 | getInclusiveClientWidth: () => number; 12 | getGapSize: () => number; 13 | getLeftOffset: () => number; 14 | getScrollLeft: () => number; 15 | setScrollLeft: (value: number) => void; 16 | setActiveSlideIdx: () => void; 17 | on: (name: H | SliderHooks, cb: SliderCallback) => void; 18 | options: SliderOptions; 19 | details: SliderDetails; 20 | activeSlideIdx: number; 21 | } & C; 22 | type SliderCallback = (props: Slider) => void; 23 | /** 24 | * Recursively makes all properties of T optional. 25 | * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#mapped-types 26 | */ 27 | type DeepPartial = { 28 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 29 | }; 30 | type SliderOptions = { 31 | scrollBehavior: string; 32 | scrollStrategy: string; 33 | slidesSelector: string; 34 | emulateScrollSnap: boolean; 35 | emulateScrollSnapMaxThreshold?: number; 36 | cssVariableContainer: HTMLElement; 37 | rtl: boolean; 38 | targetWidth?: (slider: Slider) => number; 39 | [key: string]: unknown; 40 | }; 41 | type SliderOptionArgs = { 42 | scrollBehavior?: 'smooth' | 'auto'; 43 | scrollStrategy?: 'fullSlide' | 'partialSlide'; 44 | slidesSelector?: string; 45 | emulateScrollSnap?: boolean; 46 | emulateScrollSnapMaxThreshold?: number; 47 | cssVariableContainer?: HTMLElement; 48 | rtl?: boolean; 49 | targetWidth?: (slider: Slider) => number; 50 | [key: string]: unknown; 51 | }; 52 | type SliderDetails = { 53 | hasOverflow: boolean; 54 | slideCount: number; 55 | containerWidth: number; 56 | containerHeight: number; 57 | scrollableAreaWidth: number; 58 | amountOfPages: number; 59 | currentPage: number; 60 | }; 61 | type SliderHooks = HOOK_CREATED | HOOK_CONTENTS_CHANGED | HOOK_DETAILS_CHANGED | HOOK_CONTAINER_SIZE_CHANGED | HOOK_ACTIVE_SLIDE_CHANGED | HOOK_SCROLL_START | HOOK_SCROLL | HOOK_SCROLL_END | HOOK_NATIVE_SCROLL_START | HOOK_NATIVE_SCROLL | HOOK_NATIVE_SCROLL_END | HOOK_PROGRAMMATIC_SCROLL_START | HOOK_PROGRAMMATIC_SCROLL | HOOK_PROGRAMMATIC_SCROLL_END; 62 | type HOOK_CREATED = 'created'; 63 | type HOOK_DETAILS_CHANGED = 'detailsChanged'; 64 | type HOOK_CONTENTS_CHANGED = 'contentsChanged'; 65 | type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged'; 66 | type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged'; 67 | type HOOK_SCROLL_START = 'scrollStart'; 68 | type HOOK_SCROLL = 'scroll'; 69 | type HOOK_SCROLL_END = 'scrollEnd'; 70 | type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart'; 71 | type HOOK_NATIVE_SCROLL = 'nativeScroll'; 72 | type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd'; 73 | type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart'; 74 | type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll'; 75 | type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd'; 76 | type SliderPlugin = (slider: Slider) => void; 77 | 78 | export type { DeepPartial, HOOK_ACTIVE_SLIDE_CHANGED, HOOK_CONTAINER_SIZE_CHANGED, HOOK_CONTENTS_CHANGED, HOOK_CREATED, HOOK_DETAILS_CHANGED, HOOK_NATIVE_SCROLL, HOOK_NATIVE_SCROLL_END, HOOK_NATIVE_SCROLL_START, HOOK_PROGRAMMATIC_SCROLL, HOOK_PROGRAMMATIC_SCROLL_END, HOOK_PROGRAMMATIC_SCROLL_START, HOOK_SCROLL, HOOK_SCROLL_END, HOOK_SCROLL_START, Slider, SliderCallback, SliderDetails, SliderHooks, SliderOptionArgs, SliderOptions, SliderPlugin }; 79 | -------------------------------------------------------------------------------- /docs/dist/plugins/core/index.d.ts: -------------------------------------------------------------------------------- 1 | type Slider = { 2 | container: HTMLElement; 3 | slides: HTMLElement[]; 4 | emit: (name: H | SliderHooks) => void; 5 | moveToDirection: (direction: 'prev' | 'next') => void; 6 | moveToSlideInDirection: (direction: 'prev' | 'next') => void; 7 | snapToClosestSlide: (direction: 'prev' | 'next') => void; 8 | moveToSlide: (index: number) => void; 9 | canMoveToSlide: (index: number) => boolean; 10 | getInclusiveScrollWidth: () => number; 11 | getInclusiveClientWidth: () => number; 12 | getGapSize: () => number; 13 | getLeftOffset: () => number; 14 | getScrollLeft: () => number; 15 | setScrollLeft: (value: number) => void; 16 | setActiveSlideIdx: () => void; 17 | on: (name: H | SliderHooks, cb: SliderCallback) => void; 18 | options: SliderOptions; 19 | details: SliderDetails; 20 | activeSlideIdx: number; 21 | } & C; 22 | type SliderCallback = (props: Slider) => void; 23 | /** 24 | * Recursively makes all properties of T optional. 25 | * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#mapped-types 26 | */ 27 | type DeepPartial = { 28 | [P in keyof T]?: T[P] extends object ? DeepPartial : T[P]; 29 | }; 30 | type SliderOptions = { 31 | scrollBehavior: string; 32 | scrollStrategy: string; 33 | slidesSelector: string; 34 | emulateScrollSnap: boolean; 35 | emulateScrollSnapMaxThreshold?: number; 36 | cssVariableContainer: HTMLElement; 37 | rtl: boolean; 38 | targetWidth?: (slider: Slider) => number; 39 | [key: string]: unknown; 40 | }; 41 | type SliderOptionArgs = { 42 | scrollBehavior?: 'smooth' | 'auto'; 43 | scrollStrategy?: 'fullSlide' | 'partialSlide'; 44 | slidesSelector?: string; 45 | emulateScrollSnap?: boolean; 46 | emulateScrollSnapMaxThreshold?: number; 47 | cssVariableContainer?: HTMLElement; 48 | rtl?: boolean; 49 | targetWidth?: (slider: Slider) => number; 50 | [key: string]: unknown; 51 | }; 52 | type SliderDetails = { 53 | hasOverflow: boolean; 54 | slideCount: number; 55 | containerWidth: number; 56 | containerHeight: number; 57 | scrollableAreaWidth: number; 58 | amountOfPages: number; 59 | currentPage: number; 60 | }; 61 | type SliderHooks = HOOK_CREATED | HOOK_CONTENTS_CHANGED | HOOK_DETAILS_CHANGED | HOOK_CONTAINER_SIZE_CHANGED | HOOK_ACTIVE_SLIDE_CHANGED | HOOK_SCROLL_START | HOOK_SCROLL | HOOK_SCROLL_END | HOOK_NATIVE_SCROLL_START | HOOK_NATIVE_SCROLL | HOOK_NATIVE_SCROLL_END | HOOK_PROGRAMMATIC_SCROLL_START | HOOK_PROGRAMMATIC_SCROLL | HOOK_PROGRAMMATIC_SCROLL_END; 62 | type HOOK_CREATED = 'created'; 63 | type HOOK_DETAILS_CHANGED = 'detailsChanged'; 64 | type HOOK_CONTENTS_CHANGED = 'contentsChanged'; 65 | type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged'; 66 | type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged'; 67 | type HOOK_SCROLL_START = 'scrollStart'; 68 | type HOOK_SCROLL = 'scroll'; 69 | type HOOK_SCROLL_END = 'scrollEnd'; 70 | type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart'; 71 | type HOOK_NATIVE_SCROLL = 'nativeScroll'; 72 | type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd'; 73 | type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart'; 74 | type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll'; 75 | type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd'; 76 | type SliderPlugin = (slider: Slider) => void; 77 | 78 | export type { DeepPartial, HOOK_ACTIVE_SLIDE_CHANGED, HOOK_CONTAINER_SIZE_CHANGED, HOOK_CONTENTS_CHANGED, HOOK_CREATED, HOOK_DETAILS_CHANGED, HOOK_NATIVE_SCROLL, HOOK_NATIVE_SCROLL_END, HOOK_NATIVE_SCROLL_START, HOOK_PROGRAMMATIC_SCROLL, HOOK_PROGRAMMATIC_SCROLL_END, HOOK_PROGRAMMATIC_SCROLL_START, HOOK_SCROLL, HOOK_SCROLL_END, HOOK_SCROLL_START, Slider, SliderCallback, SliderDetails, SliderHooks, SliderOptionArgs, SliderOptions, SliderPlugin }; 79 | -------------------------------------------------------------------------------- /src/core/types.ts: -------------------------------------------------------------------------------- 1 | export type Slider = { 2 | container: HTMLElement 3 | slides: HTMLElement[] 4 | emit: (name: H | SliderHooks) => void 5 | moveToDirection: ( 6 | direction: 'prev' | 'next' 7 | ) => void 8 | moveToSlideInDirection: ( 9 | direction: 'prev' | 'next' 10 | ) => void 11 | snapToClosestSlide: ( 12 | direction: 'prev' | 'next' 13 | ) => void 14 | moveToSlide: ( 15 | index: number 16 | ) => void 17 | canMoveToSlide: ( 18 | index: number 19 | ) => boolean 20 | getInclusiveScrollWidth: () => number 21 | getInclusiveClientWidth: () => number 22 | getGapSize: () => number 23 | getLeftOffset: () => number 24 | getScrollLeft: () => number 25 | setScrollLeft: (value: number) => void 26 | setActiveSlideIdx: () => void 27 | on: ( 28 | name: H | SliderHooks, 29 | cb: SliderCallback 30 | ) => void 31 | options: SliderOptions, 32 | details: SliderDetails, 33 | activeSlideIdx: number, 34 | } & C; 35 | 36 | export type SliderCallback = ( 37 | props: Slider 38 | ) => void; 39 | 40 | /** 41 | * Recursively makes all properties of T optional. 42 | * @see https://www.typescriptlang.org/docs/handbook/utility-types.html#mapped-types 43 | */ 44 | export type DeepPartial = { 45 | [P in keyof T]?: T[P] extends object 46 | ? DeepPartial 47 | : T[P] 48 | }; 49 | 50 | export type SliderOptions = { 51 | scrollBehavior: string; 52 | scrollStrategy: string; 53 | slidesSelector: string; 54 | emulateScrollSnap: boolean; 55 | emulateScrollSnapMaxThreshold?: number; 56 | cssVariableContainer: HTMLElement; 57 | rtl: boolean; 58 | targetWidth?: ( slider: Slider ) => number, 59 | [key: string]: unknown; 60 | } 61 | 62 | export type SliderOptionArgs = { 63 | scrollBehavior?: 'smooth' | 'auto'; 64 | scrollStrategy?: 'fullSlide' | 'partialSlide'; 65 | slidesSelector?: string; 66 | emulateScrollSnap?: boolean; 67 | emulateScrollSnapMaxThreshold?: number; 68 | cssVariableContainer?: HTMLElement; 69 | rtl?: boolean; 70 | targetWidth?: ( slider: Slider ) => number; 71 | [key: string]: unknown; 72 | } 73 | 74 | export type SliderDetails = { 75 | hasOverflow: boolean; 76 | slideCount: number; 77 | containerWidth: number; 78 | containerHeight: number; 79 | scrollableAreaWidth: number; 80 | amountOfPages: number; 81 | currentPage: number; 82 | } 83 | 84 | export type SliderHooks = 85 | | HOOK_CREATED 86 | | HOOK_CONTENTS_CHANGED 87 | | HOOK_DETAILS_CHANGED 88 | | HOOK_CONTAINER_SIZE_CHANGED 89 | | HOOK_ACTIVE_SLIDE_CHANGED 90 | | HOOK_SCROLL_START 91 | | HOOK_SCROLL 92 | | HOOK_SCROLL_END 93 | | HOOK_NATIVE_SCROLL_START 94 | | HOOK_NATIVE_SCROLL 95 | | HOOK_NATIVE_SCROLL_END 96 | | HOOK_PROGRAMMATIC_SCROLL_START 97 | | HOOK_PROGRAMMATIC_SCROLL 98 | | HOOK_PROGRAMMATIC_SCROLL_END; 99 | 100 | export type HOOK_CREATED = 'created'; 101 | export type HOOK_DETAILS_CHANGED = 'detailsChanged'; 102 | export type HOOK_CONTENTS_CHANGED = 'contentsChanged'; 103 | export type HOOK_CONTAINER_SIZE_CHANGED = 'containerSizeChanged'; 104 | export type HOOK_ACTIVE_SLIDE_CHANGED = 'activeSlideChanged'; 105 | 106 | // all types of scroll 107 | export type HOOK_SCROLL_START = 'scrollStart'; 108 | export type HOOK_SCROLL = 'scroll'; 109 | export type HOOK_SCROLL_END = 'scrollEnd'; 110 | 111 | // user initted scroll (touch, mouse wheel, etc.) 112 | export type HOOK_NATIVE_SCROLL_START = 'nativeScrollStart'; 113 | export type HOOK_NATIVE_SCROLL = 'nativeScroll'; 114 | export type HOOK_NATIVE_SCROLL_END = 'nativeScrollEnd'; 115 | 116 | // programmatic scroll (e.g. el.scrollTo) 117 | export type HOOK_PROGRAMMATIC_SCROLL_START = 'programmaticScrollStart'; 118 | export type HOOK_PROGRAMMATIC_SCROLL = 'programmaticScroll'; 119 | export type HOOK_PROGRAMMATIC_SCROLL_END = 'programmaticScrollEnd'; 120 | 121 | 122 | export type SliderPlugin = (slider: Slider) => void; 123 | -------------------------------------------------------------------------------- /src/plugins/drag-scrolling/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | const DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK = 20; 4 | 5 | export type DragScrollingOptions = { 6 | draggedDistanceThatPreventsClick: number, 7 | }; 8 | 9 | export default function DragScrollingPlugin( args?: DeepPartial ) { 10 | const options = { 11 | draggedDistanceThatPreventsClick: args?.draggedDistanceThatPreventsClick ?? DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK, 12 | }; 13 | return ( slider: Slider ) => { 14 | let isMouseDown = false; 15 | let startX = 0; 16 | let scrollLeft = 0; 17 | 18 | let isMovingForward = false; 19 | let programmaticScrollStarted = false; 20 | let mayNeedToSnap = false; 21 | 22 | // add data attribute to container 23 | slider.container.setAttribute( 'data-has-drag-scrolling', 'true' ); 24 | 25 | const mouseDown = (e: MouseEvent) => { 26 | programmaticScrollStarted = false; 27 | if ( ! slider.details.hasOverflow ) { 28 | return; 29 | } 30 | if ( ! slider.container.contains( e.target as Node ) ) { 31 | return; 32 | } 33 | isMouseDown = true; 34 | startX = e.pageX - slider.container.getBoundingClientRect().left; 35 | scrollLeft = slider.container.scrollLeft; 36 | // change cursor to grabbing 37 | slider.container.style.cursor = 'grabbing'; 38 | slider.container.style.scrollBehavior = 'auto'; 39 | // prevent focus going to the slides 40 | e.preventDefault(); 41 | e.stopPropagation(); 42 | }; 43 | 44 | const mouseMove = (e: MouseEvent) => { 45 | if ( ! slider.details.hasOverflow ) { 46 | programmaticScrollStarted = false; 47 | return; 48 | } 49 | if (!isMouseDown) { 50 | programmaticScrollStarted = false; 51 | return; 52 | } 53 | e.preventDefault(); 54 | if (!programmaticScrollStarted) { 55 | programmaticScrollStarted = true; 56 | slider.emit('programmaticScrollStart'); 57 | } 58 | const x = e.pageX - slider.container.getBoundingClientRect().left; 59 | const walk = (x - startX); 60 | const newScrollLeft = scrollLeft - walk; 61 | mayNeedToSnap = true; 62 | if ( Math.floor( slider.container.scrollLeft ) !== Math.floor( newScrollLeft ) ) { 63 | isMovingForward = slider.container.scrollLeft < newScrollLeft; 64 | } 65 | slider.container.scrollLeft = newScrollLeft; 66 | 67 | const absWalk = Math.abs(walk); 68 | const slides = slider.container.querySelectorAll( slider.options.slidesSelector ); 69 | const pointerEvents = absWalk > options.draggedDistanceThatPreventsClick ? 'none' : ''; 70 | slides.forEach((slide) => { 71 | (slide).style.pointerEvents = pointerEvents; 72 | }); 73 | }; 74 | 75 | const mouseUp = () => { 76 | if (!slider.details.hasOverflow) { 77 | programmaticScrollStarted = false; 78 | return; 79 | } 80 | isMouseDown = false; 81 | 82 | slider.container.style.cursor = ''; 83 | setTimeout(() => { 84 | programmaticScrollStarted = false; 85 | slider.container.style.scrollBehavior = ''; 86 | const slides = slider.container.querySelectorAll( slider.options.slidesSelector ); 87 | slides.forEach((slide) => { 88 | (slide).style.pointerEvents = ''; 89 | }); 90 | }, 50); 91 | }; 92 | 93 | window.addEventListener('mousedown', mouseDown); 94 | window.addEventListener('mousemove', mouseMove); 95 | window.addEventListener('mouseup', mouseUp); 96 | 97 | // emulate scroll snapping 98 | if ( slider.options.emulateScrollSnap ) { 99 | const snap = () => { 100 | if (!mayNeedToSnap || isMouseDown) { 101 | return; 102 | } 103 | mayNeedToSnap = false; 104 | slider.snapToClosestSlide(isMovingForward ? 'next' : 'prev'); 105 | }; 106 | slider.on( 'programmaticScrollEnd', snap ); 107 | window.addEventListener( 'mouseup', snap ); 108 | } 109 | 110 | }; 111 | }; 112 | -------------------------------------------------------------------------------- /dist/plugins/fade/index.esm.js: -------------------------------------------------------------------------------- 1 | function FadePlugin(args) { 2 | return (slider) => { 3 | var _a, _b, _c; 4 | const options = { 5 | classNames: { 6 | fadeItem: 'overflow-slider-fade', 7 | fadeItemStart: 'overflow-slider-fade--start', 8 | fadeItemEnd: 'overflow-slider-fade--end', 9 | }, 10 | container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null, 11 | containerStart: (_b = args === null || args === void 0 ? void 0 : args.containerStart) !== null && _b !== void 0 ? _b : null, 12 | containerEnd: (_c = args === null || args === void 0 ? void 0 : args.containerEnd) !== null && _c !== void 0 ? _c : null, 13 | }; 14 | const fadeItemStart = document.createElement('div'); 15 | fadeItemStart.classList.add(options.classNames.fadeItem, options.classNames.fadeItemStart); 16 | fadeItemStart.setAttribute('aria-hidden', 'true'); 17 | fadeItemStart.setAttribute('tabindex', '-1'); 18 | const fadeItemEnd = document.createElement('div'); 19 | fadeItemEnd.classList.add(options.classNames.fadeItem, options.classNames.fadeItemEnd); 20 | fadeItemEnd.setAttribute('aria-hidden', 'true'); 21 | fadeItemEnd.setAttribute('tabindex', '-1'); 22 | if (options.containerStart) { 23 | options.containerStart.appendChild(fadeItemStart); 24 | } 25 | else if (options.container) { 26 | options.container.appendChild(fadeItemStart); 27 | } 28 | if (options.containerEnd) { 29 | options.containerEnd.appendChild(fadeItemEnd); 30 | } 31 | else if (options.container) { 32 | options.container.appendChild(fadeItemEnd); 33 | } 34 | const hasFadeAtStart = () => { 35 | return slider.getScrollLeft() > fadeItemStart.offsetWidth; 36 | }; 37 | const fadeAtStartOpacity = () => { 38 | const position = slider.getScrollLeft(); 39 | if (Math.floor(position) <= Math.floor(fadeItemStart.offsetWidth)) { 40 | return position / Math.max(fadeItemStart.offsetWidth, 1); 41 | } 42 | return 1; 43 | }; 44 | const hasFadeAtEnd = () => { 45 | return Math.floor(slider.getScrollLeft()) < Math.floor(slider.getInclusiveScrollWidth() - slider.getInclusiveClientWidth() - fadeItemEnd.offsetWidth); 46 | }; 47 | const fadeAtEndOpacity = () => { 48 | const position = slider.getScrollLeft(); 49 | const maxPosition = slider.getInclusiveScrollWidth() - slider.getInclusiveClientWidth(); 50 | const maxFadePosition = maxPosition - fadeItemEnd.offsetWidth; 51 | if (Math.floor(position) >= Math.floor(maxFadePosition)) { 52 | return ((maxFadePosition - position) / Math.max(fadeItemEnd.offsetWidth, 1)) + 1; 53 | } 54 | return 1; 55 | }; 56 | const update = () => { 57 | fadeItemStart.setAttribute('data-has-fade', hasFadeAtStart().toString()); 58 | fadeItemStart.style.opacity = fadeAtStartOpacity().toString(); 59 | fadeItemEnd.setAttribute('data-has-fade', hasFadeAtEnd().toString()); 60 | fadeItemEnd.style.opacity = fadeAtEndOpacity().toString(); 61 | }; 62 | update(); 63 | slider.on('created', update); 64 | slider.on('contentsChanged', update); 65 | slider.on('containerSizeChanged', update); 66 | slider.on('scrollEnd', update); 67 | slider.on('scrollStart', update); 68 | let requestId = 0; 69 | const debouncedUpdate = () => { 70 | if (requestId) { 71 | window.cancelAnimationFrame(requestId); 72 | } 73 | requestId = window.requestAnimationFrame(() => { 74 | update(); 75 | }); 76 | }; 77 | slider.on('scroll', debouncedUpdate); 78 | }; 79 | } 80 | 81 | export { FadePlugin as default }; 82 | -------------------------------------------------------------------------------- /docs/dist/plugins/fade/index.esm.js: -------------------------------------------------------------------------------- 1 | function FadePlugin(args) { 2 | return (slider) => { 3 | var _a, _b, _c; 4 | const options = { 5 | classNames: { 6 | fadeItem: 'overflow-slider-fade', 7 | fadeItemStart: 'overflow-slider-fade--start', 8 | fadeItemEnd: 'overflow-slider-fade--end', 9 | }, 10 | container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null, 11 | containerStart: (_b = args === null || args === void 0 ? void 0 : args.containerStart) !== null && _b !== void 0 ? _b : null, 12 | containerEnd: (_c = args === null || args === void 0 ? void 0 : args.containerEnd) !== null && _c !== void 0 ? _c : null, 13 | }; 14 | const fadeItemStart = document.createElement('div'); 15 | fadeItemStart.classList.add(options.classNames.fadeItem, options.classNames.fadeItemStart); 16 | fadeItemStart.setAttribute('aria-hidden', 'true'); 17 | fadeItemStart.setAttribute('tabindex', '-1'); 18 | const fadeItemEnd = document.createElement('div'); 19 | fadeItemEnd.classList.add(options.classNames.fadeItem, options.classNames.fadeItemEnd); 20 | fadeItemEnd.setAttribute('aria-hidden', 'true'); 21 | fadeItemEnd.setAttribute('tabindex', '-1'); 22 | if (options.containerStart) { 23 | options.containerStart.appendChild(fadeItemStart); 24 | } 25 | else if (options.container) { 26 | options.container.appendChild(fadeItemStart); 27 | } 28 | if (options.containerEnd) { 29 | options.containerEnd.appendChild(fadeItemEnd); 30 | } 31 | else if (options.container) { 32 | options.container.appendChild(fadeItemEnd); 33 | } 34 | const hasFadeAtStart = () => { 35 | return slider.getScrollLeft() > fadeItemStart.offsetWidth; 36 | }; 37 | const fadeAtStartOpacity = () => { 38 | const position = slider.getScrollLeft(); 39 | if (Math.floor(position) <= Math.floor(fadeItemStart.offsetWidth)) { 40 | return position / Math.max(fadeItemStart.offsetWidth, 1); 41 | } 42 | return 1; 43 | }; 44 | const hasFadeAtEnd = () => { 45 | return Math.floor(slider.getScrollLeft()) < Math.floor(slider.getInclusiveScrollWidth() - slider.getInclusiveClientWidth() - fadeItemEnd.offsetWidth); 46 | }; 47 | const fadeAtEndOpacity = () => { 48 | const position = slider.getScrollLeft(); 49 | const maxPosition = slider.getInclusiveScrollWidth() - slider.getInclusiveClientWidth(); 50 | const maxFadePosition = maxPosition - fadeItemEnd.offsetWidth; 51 | if (Math.floor(position) >= Math.floor(maxFadePosition)) { 52 | return ((maxFadePosition - position) / Math.max(fadeItemEnd.offsetWidth, 1)) + 1; 53 | } 54 | return 1; 55 | }; 56 | const update = () => { 57 | fadeItemStart.setAttribute('data-has-fade', hasFadeAtStart().toString()); 58 | fadeItemStart.style.opacity = fadeAtStartOpacity().toString(); 59 | fadeItemEnd.setAttribute('data-has-fade', hasFadeAtEnd().toString()); 60 | fadeItemEnd.style.opacity = fadeAtEndOpacity().toString(); 61 | }; 62 | update(); 63 | slider.on('created', update); 64 | slider.on('contentsChanged', update); 65 | slider.on('containerSizeChanged', update); 66 | slider.on('scrollEnd', update); 67 | slider.on('scrollStart', update); 68 | let requestId = 0; 69 | const debouncedUpdate = () => { 70 | if (requestId) { 71 | window.cancelAnimationFrame(requestId); 72 | } 73 | requestId = window.requestAnimationFrame(() => { 74 | update(); 75 | }); 76 | }; 77 | slider.on('scroll', debouncedUpdate); 78 | }; 79 | } 80 | 81 | export { FadePlugin as default }; 82 | -------------------------------------------------------------------------------- /dist/overflow-slider.css: -------------------------------------------------------------------------------- 1 | :root{--overflow-slider-arrows-size:1.5rem;--overflow-slider-arrows-gap:.5rem;--overflow-slider-arrows-inactive-opacity:0.5}.overflow-slider__arrows{display:flex;gap:var(--overflow-slider-arrows-gap)}.overflow-slider__arrows-button{align-items:center;cursor:pointer;display:flex;outline-offset:-2px}.overflow-slider__arrows-button svg{height:var(--overflow-slider-arrows-size);width:var(--overflow-slider-arrows-size)}.overflow-slider__arrows-button[data-has-content=false]{opacity:var(--overflow-slider-arrows-inactive-opacity)}:root{--overflow-slider-autoplay-background:#000}.overflow-slider__autoplay{background:var(--overflow-slider-autoplay-background)}:root{--overflow-slider-dots-gap:0.5rem;--overflow-slider-dot-size:0.75rem;--overflow-slider-dot-inactive-color:rgba(0,0,0,.1);--overflow-slider-dot-active-color:rgba(0,0,0,.8)}.overflow-slider__dots{align-items:center;display:flex;justify-content:center}.overflow-slider__dots ul{display:flex;flex-wrap:wrap;gap:var(--overflow-slider-dots-gap);list-style:none;margin:0;padding:0}.overflow-slider__dots li{line-height:0;margin:0;padding:0}.overflow-slider__dot-item{background:var(--overflow-slider-dot-inactive-color);border-radius:50%;cursor:pointer;height:var(--overflow-slider-dot-size);margin:0;outline-offset:2px;padding:0;position:relative;width:var(--overflow-slider-dot-size)}.overflow-slider__dot-item:after{bottom:calc(var(--overflow-slider-dots-gap)*-1);content:"";display:block;left:calc(var(--overflow-slider-dots-gap)*-1);position:absolute;right:calc(var(--overflow-slider-dots-gap)*-1);top:calc(var(--overflow-slider-dots-gap)*-1)}.overflow-slider__dot-item:hover,.overflow-slider__dot-item[aria-pressed=true]{background:var(--overflow-slider-dot-active-color)}:root{--overflow-slider-fade-color:#fff;--overflow-slider-fade-width:3rem}.overflow-slider-fade{height:100%;pointer-events:none;position:absolute;top:0;width:var(--overflow-slider-fade-width);z-index:1}.overflow-slider-fade--start{background:linear-gradient(to right,var(--overflow-slider-fade-color) 0,transparent 100%);left:0}[dir=rtl] .overflow-slider-fade--start{left:auto}.overflow-slider-fade--end,[dir=rtl] .overflow-slider-fade--start{background:linear-gradient(to left,var(--overflow-slider-fade-color) 0,transparent 100%);right:0}[dir=rtl] .overflow-slider-fade--end{background:linear-gradient(to right,var(--overflow-slider-fade-color) 0,transparent 100%);left:0;right:auto}[data-has-drag-scrolling][data-has-overflow=true]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--overflow-slider-scroll-indicator-button-height:4px;--overflow-slider-scroll-indicator-padding:1rem;--overflow-slider-scroll-indicator-button-color:rgba(0,0,0,.75);--overflow-slider-scroll-indicator-bar-color:rgba(0,0,0,.25)}.overflow-slider__scroll-indicator{cursor:pointer;outline:0;padding-block:var(--overflow-slider-scroll-indicator-padding);position:relative;width:100%}.overflow-slider__scroll-indicator[data-has-overflow=false]{display:none}.overflow-slider__scroll-indicator:focus-visible .overflow-slider__scroll-indicator-button{outline:2px solid;outline-offset:2px}.overflow-slider__scroll-indicator-bar{background:var(--overflow-slider-scroll-indicator-bar-color);border-radius:3px;height:2px;left:0;position:absolute;top:50%;transform:translateY(-50%);width:100%}.overflow-slider__scroll-indicator-button{background:var(--overflow-slider-scroll-indicator-button-color);border-radius:3px;cursor:grab;height:var(--overflow-slider-scroll-indicator-button-height);left:0;position:absolute;top:calc(50% - var(--overflow-slider-scroll-indicator-button-height)/2)}.overflow-slider__scroll-indicator-button:hover,.overflow-slider__scroll-indicator-button[data-is-grabbed=true]{--overflow-slider-scroll-indicator-button-height:6px}.overflow-slider__scroll-indicator-button:after{bottom:calc(var(--overflow-slider-scroll-indicator-padding)*-1);content:"";display:block;position:absolute;top:calc(var(--overflow-slider-scroll-indicator-padding)*-1);width:100%}.overflow-slider{-ms-overflow-style:none;display:grid;grid-auto-flow:column;grid-template-columns:max-content;max-width:-moz-max-content;max-width:max-content;overflow:auto;position:relative;scrollbar-width:none;width:100%}.overflow-slider::-webkit-scrollbar{display:none}.overflow-slider>*{outline-offset:-2px;scroll-snap-align:start} -------------------------------------------------------------------------------- /docs/dist/overflow-slider.css: -------------------------------------------------------------------------------- 1 | :root{--overflow-slider-arrows-size:1.5rem;--overflow-slider-arrows-gap:.5rem;--overflow-slider-arrows-inactive-opacity:0.5}.overflow-slider__arrows{display:flex;gap:var(--overflow-slider-arrows-gap)}.overflow-slider__arrows-button{align-items:center;cursor:pointer;display:flex;outline-offset:-2px}.overflow-slider__arrows-button svg{height:var(--overflow-slider-arrows-size);width:var(--overflow-slider-arrows-size)}.overflow-slider__arrows-button[data-has-content=false]{opacity:var(--overflow-slider-arrows-inactive-opacity)}:root{--overflow-slider-autoplay-background:#000}.overflow-slider__autoplay{background:var(--overflow-slider-autoplay-background)}:root{--overflow-slider-dots-gap:0.5rem;--overflow-slider-dot-size:0.75rem;--overflow-slider-dot-inactive-color:rgba(0,0,0,.1);--overflow-slider-dot-active-color:rgba(0,0,0,.8)}.overflow-slider__dots{align-items:center;display:flex;justify-content:center}.overflow-slider__dots ul{display:flex;flex-wrap:wrap;gap:var(--overflow-slider-dots-gap);list-style:none;margin:0;padding:0}.overflow-slider__dots li{line-height:0;margin:0;padding:0}.overflow-slider__dot-item{background:var(--overflow-slider-dot-inactive-color);border-radius:50%;cursor:pointer;height:var(--overflow-slider-dot-size);margin:0;outline-offset:2px;padding:0;position:relative;width:var(--overflow-slider-dot-size)}.overflow-slider__dot-item:after{bottom:calc(var(--overflow-slider-dots-gap)*-1);content:"";display:block;left:calc(var(--overflow-slider-dots-gap)*-1);position:absolute;right:calc(var(--overflow-slider-dots-gap)*-1);top:calc(var(--overflow-slider-dots-gap)*-1)}.overflow-slider__dot-item:hover,.overflow-slider__dot-item[aria-pressed=true]{background:var(--overflow-slider-dot-active-color)}:root{--overflow-slider-fade-color:#fff;--overflow-slider-fade-width:3rem}.overflow-slider-fade{height:100%;pointer-events:none;position:absolute;top:0;width:var(--overflow-slider-fade-width);z-index:1}.overflow-slider-fade--start{background:linear-gradient(to right,var(--overflow-slider-fade-color) 0,transparent 100%);left:0}[dir=rtl] .overflow-slider-fade--start{left:auto}.overflow-slider-fade--end,[dir=rtl] .overflow-slider-fade--start{background:linear-gradient(to left,var(--overflow-slider-fade-color) 0,transparent 100%);right:0}[dir=rtl] .overflow-slider-fade--end{background:linear-gradient(to right,var(--overflow-slider-fade-color) 0,transparent 100%);left:0;right:auto}[data-has-drag-scrolling][data-has-overflow=true]{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}:root{--overflow-slider-scroll-indicator-button-height:4px;--overflow-slider-scroll-indicator-padding:1rem;--overflow-slider-scroll-indicator-button-color:rgba(0,0,0,.75);--overflow-slider-scroll-indicator-bar-color:rgba(0,0,0,.25)}.overflow-slider__scroll-indicator{cursor:pointer;outline:0;padding-block:var(--overflow-slider-scroll-indicator-padding);position:relative;width:100%}.overflow-slider__scroll-indicator[data-has-overflow=false]{display:none}.overflow-slider__scroll-indicator:focus-visible .overflow-slider__scroll-indicator-button{outline:2px solid;outline-offset:2px}.overflow-slider__scroll-indicator-bar{background:var(--overflow-slider-scroll-indicator-bar-color);border-radius:3px;height:2px;left:0;position:absolute;top:50%;transform:translateY(-50%);width:100%}.overflow-slider__scroll-indicator-button{background:var(--overflow-slider-scroll-indicator-button-color);border-radius:3px;cursor:grab;height:var(--overflow-slider-scroll-indicator-button-height);left:0;position:absolute;top:calc(50% - var(--overflow-slider-scroll-indicator-button-height)/2)}.overflow-slider__scroll-indicator-button:hover,.overflow-slider__scroll-indicator-button[data-is-grabbed=true]{--overflow-slider-scroll-indicator-button-height:6px}.overflow-slider__scroll-indicator-button:after{bottom:calc(var(--overflow-slider-scroll-indicator-padding)*-1);content:"";display:block;position:absolute;top:calc(var(--overflow-slider-scroll-indicator-padding)*-1);width:100%}.overflow-slider{-ms-overflow-style:none;display:grid;grid-auto-flow:column;grid-template-columns:max-content;max-width:-moz-max-content;max-width:max-content;overflow:auto;position:relative;scrollbar-width:none;width:100%}.overflow-slider::-webkit-scrollbar{display:none}.overflow-slider>*{outline-offset:-2px;scroll-snap-align:start} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@evermade/overflow-slider", 3 | "version": "4.2.1", 4 | "description": "Accessible slider that is powered by overflow: auto.", 5 | "type": "module", 6 | "main": "dist/index.js", 7 | "module": "dist/index.esm.js", 8 | "types": "dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "import": "./dist/index.esm.js", 12 | "require": "./dist/index.min.js", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "./plugins/drag-scrolling": { 16 | "import": "./dist/plugins/drag-scrolling/index.esm.js", 17 | "require": "./dist/plugins/drag-scrolling/index.min.js", 18 | "types": "./dist/plugins/drag-scrolling/index.d.ts" 19 | }, 20 | "./plugins/skip-links": { 21 | "import": "./dist/plugins/skip-links/index.esm.js", 22 | "require": "./dist/plugins/skip-links/index.min.js", 23 | "types": "./dist/plugins/skip-links/index.d.ts" 24 | }, 25 | "./plugins/scroll-indicator": { 26 | "import": "./dist/plugins/scroll-indicator/index.esm.js", 27 | "require": "./dist/plugins/scroll-indicator/index.min.js", 28 | "types": "./dist/plugins/scroll-indicator/index.d.ts" 29 | }, 30 | "./plugins/arrows": { 31 | "import": "./dist/plugins/arrows/index.esm.js", 32 | "require": "./dist/plugins/arrows/index.min.js", 33 | "types": "./dist/plugins/arrows/index.d.ts" 34 | }, 35 | "./plugins/autoplay": { 36 | "import": "./dist/plugins/autoplay/index.esm.js", 37 | "require": "./dist/plugins/autoplay/index.min.js", 38 | "types": "./dist/plugins/autoplay/index.d.ts" 39 | }, 40 | "./plugins/classnames": { 41 | "import": "./dist/plugins/classnames/index.esm.js", 42 | "require": "./dist/plugins/classnames/index.min.js", 43 | "types": "./dist/plugins/classnames/index.d.ts" 44 | }, 45 | "./plugins/full-width": { 46 | "import": "./dist/plugins/full-width/index.esm.js", 47 | "require": "./dist/plugins/full-width/index.min.js", 48 | "types": "./dist/plugins/full-width/index.d.ts" 49 | }, 50 | "./plugins/dots": { 51 | "import": "./dist/plugins/dots/index.esm.js", 52 | "require": "./dist/plugins/dots/index.min.js", 53 | "types": "./dist/plugins/dots/index.d.ts" 54 | }, 55 | "./plugins/thumbnails": { 56 | "import": "./dist/plugins/thumbnails/index.esm.js", 57 | "require": "./dist/plugins/thumbnails/index.min.js", 58 | "types": "./dist/plugins/thumbnails/index.d.ts" 59 | }, 60 | "./plugins/fade": { 61 | "import": "./dist/plugins/fade/index.esm.js", 62 | "require": "./dist/plugins/fade/index.min.js", 63 | "types": "./dist/plugins/fade/index.d.ts" 64 | }, 65 | "./style.css": "./dist/overflow-slider.css", 66 | "./style": "./dist/overflow-slider.css", 67 | "./mixins.scss": "./dist/mixins.scss", 68 | "./mixins": "./dist/mixins.scss" 69 | }, 70 | "repository": { 71 | "type": "git", 72 | "url": "git+https://github.com/evermade/overflow-slider.git" 73 | }, 74 | "keywords": [ 75 | "overflow-slider" 76 | ], 77 | "scripts": { 78 | "build": "rollup -c --bundleConfigAsCjs", 79 | "compress": "gzip -9 -fkc dist/index.min.js > dist/index.min.js.gz", 80 | "show": "ls -lh dist/index.min.js.gz | awk '{print \"Gzipped script size:\", $5\"B\"}'", 81 | "size": "npm run build -- --silent && npm run compress --silent && npm run show && rm dist/index.min.js.gz", 82 | "start": "rollup -c -w --bundleConfigAsCjs", 83 | "test": "echo \"No tests specified\" && exit 0" 84 | }, 85 | "author": "Teemu Suoranta, Evermade", 86 | "license": "MIT", 87 | "bugs": { 88 | "url": "https://github.com/evermade/overflow-slider/issues" 89 | }, 90 | "devDependencies": { 91 | "@rollup/plugin-commonjs": "^28.0.6", 92 | "@rollup/plugin-node-resolve": "^16.0.1", 93 | "@rollup/plugin-terser": "^0.4.4", 94 | "@rollup/plugin-typescript": "^12.1.3", 95 | "@tsconfig/recommended": "^1.0.10", 96 | "@wordpress/eslint-plugin": "^22.11.0", 97 | "@wordpress/prettier-config": "^4.25.0", 98 | "autoprefixer": "^10.4.21", 99 | "cssnano": "^7.0.7", 100 | "eslint": "^9.29.0", 101 | "rollup": "^4.44.0", 102 | "rollup-plugin-copy": "^3.5.0", 103 | "rollup-plugin-dts": "^6.2.1", 104 | "rollup-plugin-postcss": "^4.0.2", 105 | "sass": "^1.89.2", 106 | "typescript": "^5.8.3" 107 | }, 108 | "homepage": "https://github.com/evermade/overflow-slider#readme", 109 | "publishConfig": { 110 | "access": "public" 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /dist/plugins/drag-scrolling/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK = 20; 2 | function DragScrollingPlugin(args) { 3 | var _a; 4 | const options = { 5 | draggedDistanceThatPreventsClick: (_a = args === null || args === void 0 ? void 0 : args.draggedDistanceThatPreventsClick) !== null && _a !== void 0 ? _a : DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK, 6 | }; 7 | return (slider) => { 8 | let isMouseDown = false; 9 | let startX = 0; 10 | let scrollLeft = 0; 11 | let isMovingForward = false; 12 | let programmaticScrollStarted = false; 13 | let mayNeedToSnap = false; 14 | // add data attribute to container 15 | slider.container.setAttribute('data-has-drag-scrolling', 'true'); 16 | const mouseDown = (e) => { 17 | programmaticScrollStarted = false; 18 | if (!slider.details.hasOverflow) { 19 | return; 20 | } 21 | if (!slider.container.contains(e.target)) { 22 | return; 23 | } 24 | isMouseDown = true; 25 | startX = e.pageX - slider.container.getBoundingClientRect().left; 26 | scrollLeft = slider.container.scrollLeft; 27 | // change cursor to grabbing 28 | slider.container.style.cursor = 'grabbing'; 29 | slider.container.style.scrollBehavior = 'auto'; 30 | // prevent focus going to the slides 31 | e.preventDefault(); 32 | e.stopPropagation(); 33 | }; 34 | const mouseMove = (e) => { 35 | if (!slider.details.hasOverflow) { 36 | programmaticScrollStarted = false; 37 | return; 38 | } 39 | if (!isMouseDown) { 40 | programmaticScrollStarted = false; 41 | return; 42 | } 43 | e.preventDefault(); 44 | if (!programmaticScrollStarted) { 45 | programmaticScrollStarted = true; 46 | slider.emit('programmaticScrollStart'); 47 | } 48 | const x = e.pageX - slider.container.getBoundingClientRect().left; 49 | const walk = (x - startX); 50 | const newScrollLeft = scrollLeft - walk; 51 | mayNeedToSnap = true; 52 | if (Math.floor(slider.container.scrollLeft) !== Math.floor(newScrollLeft)) { 53 | isMovingForward = slider.container.scrollLeft < newScrollLeft; 54 | } 55 | slider.container.scrollLeft = newScrollLeft; 56 | const absWalk = Math.abs(walk); 57 | const slides = slider.container.querySelectorAll(slider.options.slidesSelector); 58 | const pointerEvents = absWalk > options.draggedDistanceThatPreventsClick ? 'none' : ''; 59 | slides.forEach((slide) => { 60 | slide.style.pointerEvents = pointerEvents; 61 | }); 62 | }; 63 | const mouseUp = () => { 64 | if (!slider.details.hasOverflow) { 65 | programmaticScrollStarted = false; 66 | return; 67 | } 68 | isMouseDown = false; 69 | slider.container.style.cursor = ''; 70 | setTimeout(() => { 71 | programmaticScrollStarted = false; 72 | slider.container.style.scrollBehavior = ''; 73 | const slides = slider.container.querySelectorAll(slider.options.slidesSelector); 74 | slides.forEach((slide) => { 75 | slide.style.pointerEvents = ''; 76 | }); 77 | }, 50); 78 | }; 79 | window.addEventListener('mousedown', mouseDown); 80 | window.addEventListener('mousemove', mouseMove); 81 | window.addEventListener('mouseup', mouseUp); 82 | // emulate scroll snapping 83 | if (slider.options.emulateScrollSnap) { 84 | const snap = () => { 85 | if (!mayNeedToSnap || isMouseDown) { 86 | return; 87 | } 88 | mayNeedToSnap = false; 89 | slider.snapToClosestSlide(isMovingForward ? 'next' : 'prev'); 90 | }; 91 | slider.on('programmaticScrollEnd', snap); 92 | window.addEventListener('mouseup', snap); 93 | } 94 | }; 95 | } 96 | 97 | export { DragScrollingPlugin as default }; 98 | -------------------------------------------------------------------------------- /docs/dist/plugins/drag-scrolling/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK = 20; 2 | function DragScrollingPlugin(args) { 3 | var _a; 4 | const options = { 5 | draggedDistanceThatPreventsClick: (_a = args === null || args === void 0 ? void 0 : args.draggedDistanceThatPreventsClick) !== null && _a !== void 0 ? _a : DEFAULT_DRAGGED_DISTANCE_THAT_PREVENTS_CLICK, 6 | }; 7 | return (slider) => { 8 | let isMouseDown = false; 9 | let startX = 0; 10 | let scrollLeft = 0; 11 | let isMovingForward = false; 12 | let programmaticScrollStarted = false; 13 | let mayNeedToSnap = false; 14 | // add data attribute to container 15 | slider.container.setAttribute('data-has-drag-scrolling', 'true'); 16 | const mouseDown = (e) => { 17 | programmaticScrollStarted = false; 18 | if (!slider.details.hasOverflow) { 19 | return; 20 | } 21 | if (!slider.container.contains(e.target)) { 22 | return; 23 | } 24 | isMouseDown = true; 25 | startX = e.pageX - slider.container.getBoundingClientRect().left; 26 | scrollLeft = slider.container.scrollLeft; 27 | // change cursor to grabbing 28 | slider.container.style.cursor = 'grabbing'; 29 | slider.container.style.scrollBehavior = 'auto'; 30 | // prevent focus going to the slides 31 | e.preventDefault(); 32 | e.stopPropagation(); 33 | }; 34 | const mouseMove = (e) => { 35 | if (!slider.details.hasOverflow) { 36 | programmaticScrollStarted = false; 37 | return; 38 | } 39 | if (!isMouseDown) { 40 | programmaticScrollStarted = false; 41 | return; 42 | } 43 | e.preventDefault(); 44 | if (!programmaticScrollStarted) { 45 | programmaticScrollStarted = true; 46 | slider.emit('programmaticScrollStart'); 47 | } 48 | const x = e.pageX - slider.container.getBoundingClientRect().left; 49 | const walk = (x - startX); 50 | const newScrollLeft = scrollLeft - walk; 51 | mayNeedToSnap = true; 52 | if (Math.floor(slider.container.scrollLeft) !== Math.floor(newScrollLeft)) { 53 | isMovingForward = slider.container.scrollLeft < newScrollLeft; 54 | } 55 | slider.container.scrollLeft = newScrollLeft; 56 | const absWalk = Math.abs(walk); 57 | const slides = slider.container.querySelectorAll(slider.options.slidesSelector); 58 | const pointerEvents = absWalk > options.draggedDistanceThatPreventsClick ? 'none' : ''; 59 | slides.forEach((slide) => { 60 | slide.style.pointerEvents = pointerEvents; 61 | }); 62 | }; 63 | const mouseUp = () => { 64 | if (!slider.details.hasOverflow) { 65 | programmaticScrollStarted = false; 66 | return; 67 | } 68 | isMouseDown = false; 69 | slider.container.style.cursor = ''; 70 | setTimeout(() => { 71 | programmaticScrollStarted = false; 72 | slider.container.style.scrollBehavior = ''; 73 | const slides = slider.container.querySelectorAll(slider.options.slidesSelector); 74 | slides.forEach((slide) => { 75 | slide.style.pointerEvents = ''; 76 | }); 77 | }, 50); 78 | }; 79 | window.addEventListener('mousedown', mouseDown); 80 | window.addEventListener('mousemove', mouseMove); 81 | window.addEventListener('mouseup', mouseUp); 82 | // emulate scroll snapping 83 | if (slider.options.emulateScrollSnap) { 84 | const snap = () => { 85 | if (!mayNeedToSnap || isMouseDown) { 86 | return; 87 | } 88 | mayNeedToSnap = false; 89 | slider.snapToClosestSlide(isMovingForward ? 'next' : 'prev'); 90 | }; 91 | slider.on('programmaticScrollEnd', snap); 92 | window.addEventListener('mouseup', snap); 93 | } 94 | }; 95 | } 96 | 97 | export { DragScrollingPlugin as default }; 98 | -------------------------------------------------------------------------------- /src/plugins/classnames/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | export type ClassnameOptions = { 4 | classNames: { 5 | visible: string; 6 | partlyVisible: string; 7 | hidden: string; 8 | }, 9 | freezeStateOnVisible: boolean; 10 | }; 11 | 12 | const DEFAULT_CLASS_NAMES: ClassnameOptions['classNames'] = { 13 | visible: 'is-visible', 14 | partlyVisible: 'is-partly-visible', 15 | hidden: 'is-hidden', 16 | } 17 | 18 | type VisibilityState = keyof ClassnameOptions['classNames']; 19 | 20 | export default function ClassNamesPlugin( args?: DeepPartial ) { 21 | return ( slider: Slider ) => { 22 | 23 | const providedClassNames = args?.classNames ?? (args as { classnames?: DeepPartial })?.classnames; 24 | 25 | const options = { 26 | classNames: { 27 | ...DEFAULT_CLASS_NAMES, 28 | ...providedClassNames ?? {}, 29 | }, 30 | freezeStateOnVisible: args?.freezeStateOnVisible ?? false, 31 | }; 32 | 33 | const slideStates = new WeakMap(); 34 | const uniqueClassNames = Array.from( 35 | new Set( 36 | Object.values( options.classNames ).filter( ( className ): className is string => Boolean( className ) ) 37 | ) 38 | ); 39 | 40 | const getTargetBounds = () => { 41 | const sliderRect = slider.container.getBoundingClientRect(); 42 | const sliderWidth = sliderRect.width; 43 | if (!sliderWidth) { 44 | return { targetStart: sliderRect.left, targetEnd: sliderRect.right }; 45 | } 46 | 47 | let targetWidth = 0; 48 | if ( typeof slider.options.targetWidth === 'function' ) { 49 | try { 50 | targetWidth = slider.options.targetWidth( slider ); 51 | } catch ( error ) { 52 | targetWidth = 0; 53 | } 54 | } 55 | 56 | if ( !Number.isFinite( targetWidth ) || targetWidth <= 0 ) { 57 | targetWidth = sliderWidth; 58 | } 59 | 60 | const effectiveTargetWidth = Math.min( targetWidth, sliderWidth ); 61 | const offset = ( sliderWidth - effectiveTargetWidth ) / 2; 62 | const clampedOffset = Math.max( offset, 0 ); 63 | 64 | return { 65 | targetStart: sliderRect.left + clampedOffset, 66 | targetEnd: sliderRect.right - clampedOffset, 67 | }; 68 | }; 69 | 70 | const update = () => { 71 | const { targetStart, targetEnd } = getTargetBounds(); 72 | 73 | slider.slides.forEach( ( slide ) => { 74 | const slideRect = slide.getBoundingClientRect(); 75 | const slideLeft = slideRect.left; 76 | const slideRight = slideRect.right; 77 | 78 | const tolerance = 2; 79 | const overlapsTarget = (slideRight - tolerance) > targetStart && (slideLeft + tolerance) < targetEnd; 80 | const fullyInsideTarget = (slideLeft + tolerance) >= targetStart && (slideRight - tolerance) <= targetEnd; 81 | 82 | let nextState: VisibilityState = 'hidden'; 83 | if ( overlapsTarget ) { 84 | nextState = fullyInsideTarget ? 'visible' : 'partlyVisible'; 85 | } 86 | 87 | const prevState = slideStates.get( slide ); 88 | 89 | // If freezeStateOnVisible is enabled and slide was previously visible, keep it frozen 90 | if ( options.freezeStateOnVisible && prevState === 'visible' ) { 91 | return; 92 | } 93 | 94 | if ( prevState === nextState ) { 95 | return; 96 | } 97 | 98 | const nextClass = options.classNames[ nextState ]; 99 | if ( prevState ) { 100 | const prevClass = options.classNames[ prevState ]; 101 | if ( prevClass !== nextClass && prevClass ) { 102 | slide.classList.remove( prevClass ); 103 | } 104 | } else { 105 | uniqueClassNames.forEach( ( className ) => { 106 | if ( className !== nextClass ) { 107 | slide.classList.remove( className ); 108 | } 109 | } ); 110 | } 111 | 112 | if ( nextClass && !slide.classList.contains( nextClass ) ) { 113 | slide.classList.add( nextClass ); 114 | } 115 | 116 | slideStates.set( slide, nextState ); 117 | }); 118 | }; 119 | 120 | slider.on( 'created', update ); 121 | slider.on( 'pluginsLoaded', update ); 122 | slider.on( 'fullWidthPluginUpdate', update ); 123 | slider.on( 'contentsChanged', update ); 124 | slider.on( 'containerSizeChanged', update ); 125 | slider.on( 'detailsChanged', update ); 126 | slider.on( 'scrollEnd', update ); 127 | slider.on( 'scrollStart', update ); 128 | 129 | requestAnimationFrame(() => { 130 | requestAnimationFrame(() => update()); 131 | }); 132 | 133 | let requestId = 0; 134 | const debouncedUpdate = () => { 135 | if ( requestId ) { 136 | window.cancelAnimationFrame( requestId ); 137 | } 138 | requestId = window.requestAnimationFrame(() => { 139 | update(); 140 | }); 141 | }; 142 | slider.on('scroll', debouncedUpdate); 143 | 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /src/plugins/arrows/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | const DEFAULT_TEXTS = { 4 | buttonPrevious: 'Previous items', 5 | buttonNext: 'Next items', 6 | }; 7 | 8 | const DEFAULT_ICONS = { 9 | prev: '', 10 | next: '', 11 | }; 12 | 13 | const DEFAULT_CLASS_NAMES = { 14 | navContainer: 'overflow-slider__arrows', 15 | prevButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--prev', 16 | nextButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--next', 17 | }; 18 | 19 | export type ArrowsMovementTypes = 'view' | 'slide'; 20 | 21 | export type ArrowsOptions = { 22 | texts: { 23 | buttonPrevious: string; 24 | buttonNext: string; 25 | }, 26 | icons: { 27 | prev: string; 28 | next: string; 29 | }, 30 | classNames: { 31 | navContainer: string; 32 | prevButton: string; 33 | nextButton: string; 34 | }, 35 | container: HTMLElement | null, 36 | containerPrev: HTMLElement | null, 37 | containerNext: HTMLElement | null, 38 | movementType: ArrowsMovementTypes, 39 | }; 40 | 41 | export default function ArrowsPlugin( args?: DeepPartial ) { 42 | return ( slider: Slider ) => { 43 | 44 | const options = { 45 | texts: { 46 | ...DEFAULT_TEXTS, 47 | ...args?.texts || [] 48 | }, 49 | icons: { 50 | ...DEFAULT_ICONS, 51 | ...args?.icons || [] 52 | }, 53 | classNames: { 54 | ...DEFAULT_CLASS_NAMES, 55 | ...args?.classNames || [] 56 | }, 57 | container: args?.container ?? null, 58 | containerPrev: args?.containerPrev ?? null, 59 | containerNext: args?.containerNext ?? null, 60 | movementType: args?.movementType ?? 'view', 61 | }; 62 | 63 | const nav = document.createElement( 'div' ); 64 | nav.classList.add( options.classNames.navContainer ); 65 | 66 | const prev = document.createElement( 'button' ); 67 | prev.setAttribute( 'class', options.classNames.prevButton ); 68 | prev.setAttribute( 'type', 'button' ); 69 | prev.setAttribute( 'aria-label', options.texts.buttonPrevious ); 70 | prev.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? ''); 71 | prev.setAttribute( 'data-type', 'prev' ); 72 | prev.innerHTML = slider.options.rtl ? options.icons.next : options.icons.prev; 73 | prev.addEventListener( 'click', () => { 74 | if ( prev.getAttribute('data-has-content') === 'true' ) { 75 | options.movementType === 'slide' ? slider.moveToSlideInDirection( 'prev' ) : slider.moveToDirection( 'prev' ); 76 | } 77 | } ); 78 | 79 | const next = document.createElement( 'button' ); 80 | next.setAttribute( 'class', options.classNames.nextButton ); 81 | next.setAttribute( 'type', 'button' ); 82 | next.setAttribute( 'aria-label', options.texts.buttonNext ); 83 | next.setAttribute( 'aria-controls', slider.container.getAttribute( 'id' ) ?? ''); 84 | next.setAttribute( 'data-type', 'next' ); 85 | next.innerHTML = slider.options.rtl ? options.icons.prev : options.icons.next; 86 | next.addEventListener( 'click', () => { 87 | if ( next.getAttribute('data-has-content') === 'true' ) { 88 | options.movementType === 'slide' ? slider.moveToSlideInDirection( 'next' ) : slider.moveToDirection( 'next' ); 89 | } 90 | } ); 91 | 92 | // insert buttons to the nav 93 | nav.appendChild( prev ); 94 | nav.appendChild( next ); 95 | 96 | const update = () => { 97 | const scrollLeft = slider.getScrollLeft(); 98 | const scrollWidth = slider.getInclusiveScrollWidth(); 99 | const clientWidth = slider.getInclusiveClientWidth(); 100 | const buffer = 1; 101 | if ( Math.floor( scrollLeft ) === 0 ) { 102 | prev.setAttribute( 'data-has-content', 'false' ); 103 | } else { 104 | prev.setAttribute( 'data-has-content', 'true' ); 105 | } 106 | const maxWidthDifference = Math.abs( Math.floor( scrollLeft + clientWidth ) - Math.floor( scrollWidth ) ); 107 | if ( maxWidthDifference <= buffer ) { 108 | next.setAttribute( 'data-has-content', 'false' ); 109 | } else { 110 | next.setAttribute( 'data-has-content', 'true' ); 111 | } 112 | }; 113 | 114 | if ( options.containerNext && options.containerPrev ) { 115 | options.containerPrev.appendChild( prev ); 116 | options.containerNext.appendChild( next ); 117 | } else { 118 | if ( options.container ) { 119 | options.container.appendChild( nav ); 120 | } else { 121 | slider.container.parentNode?.insertBefore( nav, slider.container.nextSibling ); 122 | } 123 | } 124 | 125 | update(); 126 | slider.on( 'scrollEnd', update ); 127 | slider.on( 'contentsChanged', update ); 128 | slider.on( 'containerSizeChanged', update ); 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /src/plugins/dots/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | export type DotsOptions = { 4 | type: 'view' | 'slide'; 5 | texts: { 6 | dotDescription: string; 7 | }, 8 | classNames: { 9 | dotsContainer: string; 10 | dotsItem: string; 11 | }, 12 | container: HTMLElement | null, 13 | }; 14 | 15 | const DEFAULT_TEXTS = { 16 | dotDescription: 'Page %d of %d', 17 | }; 18 | 19 | const DEFAULT_CLASS_NAMES = { 20 | dotsContainer: 'overflow-slider__dots', 21 | dotsItem: 'overflow-slider__dot-item', 22 | }; 23 | 24 | export default function DotsPlugin( args?: DeepPartial ) { 25 | return ( slider: Slider ) => { 26 | const options = { 27 | type: args?.type ?? 'slide', 28 | texts: { 29 | ...DEFAULT_TEXTS, 30 | ...args?.texts || [] 31 | }, 32 | classNames: { 33 | ...DEFAULT_CLASS_NAMES, 34 | ...args?.classNames || [] 35 | }, 36 | container: args?.container ?? null, 37 | }; 38 | 39 | const dots = document.createElement( 'div' ); 40 | dots.classList.add( options.classNames.dotsContainer ); 41 | 42 | let pageFocused: number|null = null; 43 | 44 | const buildDots = () => { 45 | dots.setAttribute( 'data-has-content', slider.details.hasOverflow.toString() ); 46 | dots.innerHTML = ''; 47 | 48 | console.log('buildDots'); 49 | 50 | const dotsList = document.createElement( 'ul' ); 51 | 52 | const count = options.type === 'view' ? slider.details.amountOfPages : slider.details.slideCount; 53 | const currentIndex = options.type === 'view' ? slider.details.currentPage : slider.activeSlideIdx; 54 | 55 | if ( count <= 1 ) { 56 | return; 57 | } 58 | 59 | for ( let i = 0; i < count; i++ ) { 60 | const dotListItem = document.createElement( 'li' ); 61 | const dot = document.createElement( 'button' ); 62 | dot.setAttribute( 'type', 'button' ); 63 | dot.setAttribute( 'class', options.classNames.dotsItem ); 64 | dot.setAttribute( 'aria-label', options.texts.dotDescription.replace( '%d', ( i + 1 ).toString() ).replace( '%d', count.toString() ) ); 65 | dot.setAttribute( 'aria-pressed', ( i === currentIndex ).toString() ); 66 | dot.setAttribute( 'data-item', ( i + 1 ).toString() ); 67 | dotListItem.appendChild( dot ); 68 | dotsList.appendChild( dotListItem ); 69 | dot.addEventListener( 'click', () => activateDot( i + 1 ) ); 70 | dot.addEventListener( 'focus', () => pageFocused = i + 1 ); 71 | dot.addEventListener( 'keydown', ( e ) => { 72 | const currentItemItem = dots.querySelector( `[aria-pressed="true"]` ); 73 | if ( ! currentItemItem ) { 74 | return; 75 | } 76 | const currentItem = parseInt( currentItemItem.getAttribute( 'data-item' ) ?? '1' ); 77 | if ( e.key === 'ArrowLeft' ) { 78 | const previousIndex = currentItem - 1; 79 | if ( previousIndex > 0 ) { 80 | const matchingDot = dots.querySelector( `[data-item="${previousIndex}"]` ); 81 | if ( matchingDot ) { 82 | ( matchingDot ).focus(); 83 | } 84 | activateDot( previousIndex ); 85 | } 86 | } 87 | if ( e.key === 'ArrowRight' ) { 88 | const nextIndex = currentItem + 1; 89 | if ( nextIndex <= count ) { 90 | const matchingDot = dots.querySelector( `[data-item="${nextIndex}"]` ); 91 | if ( matchingDot ) { 92 | ( matchingDot ).focus(); 93 | } 94 | activateDot( nextIndex ); 95 | } 96 | } 97 | } ); 98 | } 99 | 100 | dots.appendChild( dotsList ); 101 | 102 | // return focus to same page after rebuild 103 | if ( pageFocused ) { 104 | const matchingDot = dots.querySelector( `[data-item="${pageFocused}"]` ); 105 | if ( matchingDot ) { 106 | ( matchingDot ).focus(); 107 | } 108 | } 109 | }; 110 | const activateDot = ( index: number ) => { 111 | console.log('activateDot', index, 'slider.details', slider.details); 112 | 113 | if ( options.type === 'view' ) { 114 | const count = slider.details.amountOfPages; 115 | let targetPosition = slider.details.containerWidth * ( index - 1 ); 116 | 117 | // For the last page, scroll to the maximum scroll position to ensure it activates 118 | if ( index === count ) { 119 | const maxScroll = slider.details.scrollableAreaWidth - slider.details.containerWidth; 120 | targetPosition = maxScroll; 121 | } 122 | 123 | const scrollLeft = slider.options.rtl ? -targetPosition : targetPosition; 124 | slider.container.scrollTo({ 125 | left: scrollLeft, 126 | behavior: slider.options.scrollBehavior as ScrollBehavior 127 | }); 128 | } else { 129 | slider.moveToSlide( index - 1 ); 130 | } 131 | }; 132 | 133 | buildDots(); 134 | 135 | if ( options.container ) { 136 | options.container.appendChild( dots ); 137 | } else { 138 | slider.container.parentNode?.insertBefore( dots, slider.container.nextSibling ); 139 | } 140 | 141 | slider.on( 'scrollEnd', buildDots ); 142 | slider.on( 'contentsChanged', buildDots ); 143 | slider.on( 'containerSizeChanged', buildDots ); 144 | slider.on( 'detailsChanged', buildDots ); 145 | }; 146 | }; 147 | -------------------------------------------------------------------------------- /dist/plugins/arrows/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TEXTS = { 2 | buttonPrevious: 'Previous items', 3 | buttonNext: 'Next items', 4 | }; 5 | const DEFAULT_ICONS = { 6 | prev: '', 7 | next: '', 8 | }; 9 | const DEFAULT_CLASS_NAMES = { 10 | navContainer: 'overflow-slider__arrows', 11 | prevButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--prev', 12 | nextButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--next', 13 | }; 14 | function ArrowsPlugin(args) { 15 | return (slider) => { 16 | var _a, _b, _c, _d, _e, _f, _g; 17 | const options = { 18 | texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), 19 | icons: Object.assign(Object.assign({}, DEFAULT_ICONS), (args === null || args === void 0 ? void 0 : args.icons) || []), 20 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), 21 | container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null, 22 | containerPrev: (_b = args === null || args === void 0 ? void 0 : args.containerPrev) !== null && _b !== void 0 ? _b : null, 23 | containerNext: (_c = args === null || args === void 0 ? void 0 : args.containerNext) !== null && _c !== void 0 ? _c : null, 24 | movementType: (_d = args === null || args === void 0 ? void 0 : args.movementType) !== null && _d !== void 0 ? _d : 'view', 25 | }; 26 | const nav = document.createElement('div'); 27 | nav.classList.add(options.classNames.navContainer); 28 | const prev = document.createElement('button'); 29 | prev.setAttribute('class', options.classNames.prevButton); 30 | prev.setAttribute('type', 'button'); 31 | prev.setAttribute('aria-label', options.texts.buttonPrevious); 32 | prev.setAttribute('aria-controls', (_e = slider.container.getAttribute('id')) !== null && _e !== void 0 ? _e : ''); 33 | prev.setAttribute('data-type', 'prev'); 34 | prev.innerHTML = slider.options.rtl ? options.icons.next : options.icons.prev; 35 | prev.addEventListener('click', () => { 36 | if (prev.getAttribute('data-has-content') === 'true') { 37 | options.movementType === 'slide' ? slider.moveToSlideInDirection('prev') : slider.moveToDirection('prev'); 38 | } 39 | }); 40 | const next = document.createElement('button'); 41 | next.setAttribute('class', options.classNames.nextButton); 42 | next.setAttribute('type', 'button'); 43 | next.setAttribute('aria-label', options.texts.buttonNext); 44 | next.setAttribute('aria-controls', (_f = slider.container.getAttribute('id')) !== null && _f !== void 0 ? _f : ''); 45 | next.setAttribute('data-type', 'next'); 46 | next.innerHTML = slider.options.rtl ? options.icons.prev : options.icons.next; 47 | next.addEventListener('click', () => { 48 | if (next.getAttribute('data-has-content') === 'true') { 49 | options.movementType === 'slide' ? slider.moveToSlideInDirection('next') : slider.moveToDirection('next'); 50 | } 51 | }); 52 | // insert buttons to the nav 53 | nav.appendChild(prev); 54 | nav.appendChild(next); 55 | const update = () => { 56 | const scrollLeft = slider.getScrollLeft(); 57 | const scrollWidth = slider.getInclusiveScrollWidth(); 58 | const clientWidth = slider.getInclusiveClientWidth(); 59 | const buffer = 1; 60 | if (Math.floor(scrollLeft) === 0) { 61 | prev.setAttribute('data-has-content', 'false'); 62 | } 63 | else { 64 | prev.setAttribute('data-has-content', 'true'); 65 | } 66 | const maxWidthDifference = Math.abs(Math.floor(scrollLeft + clientWidth) - Math.floor(scrollWidth)); 67 | if (maxWidthDifference <= buffer) { 68 | next.setAttribute('data-has-content', 'false'); 69 | } 70 | else { 71 | next.setAttribute('data-has-content', 'true'); 72 | } 73 | }; 74 | if (options.containerNext && options.containerPrev) { 75 | options.containerPrev.appendChild(prev); 76 | options.containerNext.appendChild(next); 77 | } 78 | else { 79 | if (options.container) { 80 | options.container.appendChild(nav); 81 | } 82 | else { 83 | (_g = slider.container.parentNode) === null || _g === void 0 ? void 0 : _g.insertBefore(nav, slider.container.nextSibling); 84 | } 85 | } 86 | update(); 87 | slider.on('scrollEnd', update); 88 | slider.on('contentsChanged', update); 89 | slider.on('containerSizeChanged', update); 90 | }; 91 | } 92 | 93 | export { ArrowsPlugin as default }; 94 | -------------------------------------------------------------------------------- /docs/dist/plugins/arrows/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TEXTS = { 2 | buttonPrevious: 'Previous items', 3 | buttonNext: 'Next items', 4 | }; 5 | const DEFAULT_ICONS = { 6 | prev: '', 7 | next: '', 8 | }; 9 | const DEFAULT_CLASS_NAMES = { 10 | navContainer: 'overflow-slider__arrows', 11 | prevButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--prev', 12 | nextButton: 'overflow-slider__arrows-button overflow-slider__arrows-button--next', 13 | }; 14 | function ArrowsPlugin(args) { 15 | return (slider) => { 16 | var _a, _b, _c, _d, _e, _f, _g; 17 | const options = { 18 | texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), 19 | icons: Object.assign(Object.assign({}, DEFAULT_ICONS), (args === null || args === void 0 ? void 0 : args.icons) || []), 20 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), 21 | container: (_a = args === null || args === void 0 ? void 0 : args.container) !== null && _a !== void 0 ? _a : null, 22 | containerPrev: (_b = args === null || args === void 0 ? void 0 : args.containerPrev) !== null && _b !== void 0 ? _b : null, 23 | containerNext: (_c = args === null || args === void 0 ? void 0 : args.containerNext) !== null && _c !== void 0 ? _c : null, 24 | movementType: (_d = args === null || args === void 0 ? void 0 : args.movementType) !== null && _d !== void 0 ? _d : 'view', 25 | }; 26 | const nav = document.createElement('div'); 27 | nav.classList.add(options.classNames.navContainer); 28 | const prev = document.createElement('button'); 29 | prev.setAttribute('class', options.classNames.prevButton); 30 | prev.setAttribute('type', 'button'); 31 | prev.setAttribute('aria-label', options.texts.buttonPrevious); 32 | prev.setAttribute('aria-controls', (_e = slider.container.getAttribute('id')) !== null && _e !== void 0 ? _e : ''); 33 | prev.setAttribute('data-type', 'prev'); 34 | prev.innerHTML = slider.options.rtl ? options.icons.next : options.icons.prev; 35 | prev.addEventListener('click', () => { 36 | if (prev.getAttribute('data-has-content') === 'true') { 37 | options.movementType === 'slide' ? slider.moveToSlideInDirection('prev') : slider.moveToDirection('prev'); 38 | } 39 | }); 40 | const next = document.createElement('button'); 41 | next.setAttribute('class', options.classNames.nextButton); 42 | next.setAttribute('type', 'button'); 43 | next.setAttribute('aria-label', options.texts.buttonNext); 44 | next.setAttribute('aria-controls', (_f = slider.container.getAttribute('id')) !== null && _f !== void 0 ? _f : ''); 45 | next.setAttribute('data-type', 'next'); 46 | next.innerHTML = slider.options.rtl ? options.icons.prev : options.icons.next; 47 | next.addEventListener('click', () => { 48 | if (next.getAttribute('data-has-content') === 'true') { 49 | options.movementType === 'slide' ? slider.moveToSlideInDirection('next') : slider.moveToDirection('next'); 50 | } 51 | }); 52 | // insert buttons to the nav 53 | nav.appendChild(prev); 54 | nav.appendChild(next); 55 | const update = () => { 56 | const scrollLeft = slider.getScrollLeft(); 57 | const scrollWidth = slider.getInclusiveScrollWidth(); 58 | const clientWidth = slider.getInclusiveClientWidth(); 59 | const buffer = 1; 60 | if (Math.floor(scrollLeft) === 0) { 61 | prev.setAttribute('data-has-content', 'false'); 62 | } 63 | else { 64 | prev.setAttribute('data-has-content', 'true'); 65 | } 66 | const maxWidthDifference = Math.abs(Math.floor(scrollLeft + clientWidth) - Math.floor(scrollWidth)); 67 | if (maxWidthDifference <= buffer) { 68 | next.setAttribute('data-has-content', 'false'); 69 | } 70 | else { 71 | next.setAttribute('data-has-content', 'true'); 72 | } 73 | }; 74 | if (options.containerNext && options.containerPrev) { 75 | options.containerPrev.appendChild(prev); 76 | options.containerNext.appendChild(next); 77 | } 78 | else { 79 | if (options.container) { 80 | options.container.appendChild(nav); 81 | } 82 | else { 83 | (_g = slider.container.parentNode) === null || _g === void 0 ? void 0 : _g.insertBefore(nav, slider.container.nextSibling); 84 | } 85 | } 86 | update(); 87 | slider.on('scrollEnd', update); 88 | slider.on('contentsChanged', update); 89 | slider.on('containerSizeChanged', update); 90 | }; 91 | } 92 | 93 | export { ArrowsPlugin as default }; 94 | -------------------------------------------------------------------------------- /dist/plugins/classnames/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_CLASS_NAMES = { 2 | visible: 'is-visible', 3 | partlyVisible: 'is-partly-visible', 4 | hidden: 'is-hidden', 5 | }; 6 | function ClassNamesPlugin(args) { 7 | return (slider) => { 8 | var _a, _b; 9 | const providedClassNames = (_a = args === null || args === void 0 ? void 0 : args.classNames) !== null && _a !== void 0 ? _a : args === null || args === void 0 ? void 0 : args.classnames; 10 | const options = { 11 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), providedClassNames !== null && providedClassNames !== void 0 ? providedClassNames : {}), 12 | freezeStateOnVisible: (_b = args === null || args === void 0 ? void 0 : args.freezeStateOnVisible) !== null && _b !== void 0 ? _b : false, 13 | }; 14 | const slideStates = new WeakMap(); 15 | const uniqueClassNames = Array.from(new Set(Object.values(options.classNames).filter((className) => Boolean(className)))); 16 | const getTargetBounds = () => { 17 | const sliderRect = slider.container.getBoundingClientRect(); 18 | const sliderWidth = sliderRect.width; 19 | if (!sliderWidth) { 20 | return { targetStart: sliderRect.left, targetEnd: sliderRect.right }; 21 | } 22 | let targetWidth = 0; 23 | if (typeof slider.options.targetWidth === 'function') { 24 | try { 25 | targetWidth = slider.options.targetWidth(slider); 26 | } 27 | catch (error) { 28 | targetWidth = 0; 29 | } 30 | } 31 | if (!Number.isFinite(targetWidth) || targetWidth <= 0) { 32 | targetWidth = sliderWidth; 33 | } 34 | const effectiveTargetWidth = Math.min(targetWidth, sliderWidth); 35 | const offset = (sliderWidth - effectiveTargetWidth) / 2; 36 | const clampedOffset = Math.max(offset, 0); 37 | return { 38 | targetStart: sliderRect.left + clampedOffset, 39 | targetEnd: sliderRect.right - clampedOffset, 40 | }; 41 | }; 42 | const update = () => { 43 | const { targetStart, targetEnd } = getTargetBounds(); 44 | slider.slides.forEach((slide) => { 45 | const slideRect = slide.getBoundingClientRect(); 46 | const slideLeft = slideRect.left; 47 | const slideRight = slideRect.right; 48 | const tolerance = 2; 49 | const overlapsTarget = (slideRight - tolerance) > targetStart && (slideLeft + tolerance) < targetEnd; 50 | const fullyInsideTarget = (slideLeft + tolerance) >= targetStart && (slideRight - tolerance) <= targetEnd; 51 | let nextState = 'hidden'; 52 | if (overlapsTarget) { 53 | nextState = fullyInsideTarget ? 'visible' : 'partlyVisible'; 54 | } 55 | const prevState = slideStates.get(slide); 56 | // If freezeStateOnVisible is enabled and slide was previously visible, keep it frozen 57 | if (options.freezeStateOnVisible && prevState === 'visible') { 58 | return; 59 | } 60 | if (prevState === nextState) { 61 | return; 62 | } 63 | const nextClass = options.classNames[nextState]; 64 | if (prevState) { 65 | const prevClass = options.classNames[prevState]; 66 | if (prevClass !== nextClass && prevClass) { 67 | slide.classList.remove(prevClass); 68 | } 69 | } 70 | else { 71 | uniqueClassNames.forEach((className) => { 72 | if (className !== nextClass) { 73 | slide.classList.remove(className); 74 | } 75 | }); 76 | } 77 | if (nextClass && !slide.classList.contains(nextClass)) { 78 | slide.classList.add(nextClass); 79 | } 80 | slideStates.set(slide, nextState); 81 | }); 82 | }; 83 | slider.on('created', update); 84 | slider.on('pluginsLoaded', update); 85 | slider.on('fullWidthPluginUpdate', update); 86 | slider.on('contentsChanged', update); 87 | slider.on('containerSizeChanged', update); 88 | slider.on('detailsChanged', update); 89 | slider.on('scrollEnd', update); 90 | slider.on('scrollStart', update); 91 | requestAnimationFrame(() => { 92 | requestAnimationFrame(() => update()); 93 | }); 94 | let requestId = 0; 95 | const debouncedUpdate = () => { 96 | if (requestId) { 97 | window.cancelAnimationFrame(requestId); 98 | } 99 | requestId = window.requestAnimationFrame(() => { 100 | update(); 101 | }); 102 | }; 103 | slider.on('scroll', debouncedUpdate); 104 | }; 105 | } 106 | 107 | export { ClassNamesPlugin as default }; 108 | -------------------------------------------------------------------------------- /docs/dist/plugins/classnames/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_CLASS_NAMES = { 2 | visible: 'is-visible', 3 | partlyVisible: 'is-partly-visible', 4 | hidden: 'is-hidden', 5 | }; 6 | function ClassNamesPlugin(args) { 7 | return (slider) => { 8 | var _a, _b; 9 | const providedClassNames = (_a = args === null || args === void 0 ? void 0 : args.classNames) !== null && _a !== void 0 ? _a : args === null || args === void 0 ? void 0 : args.classnames; 10 | const options = { 11 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), providedClassNames !== null && providedClassNames !== void 0 ? providedClassNames : {}), 12 | freezeStateOnVisible: (_b = args === null || args === void 0 ? void 0 : args.freezeStateOnVisible) !== null && _b !== void 0 ? _b : false, 13 | }; 14 | const slideStates = new WeakMap(); 15 | const uniqueClassNames = Array.from(new Set(Object.values(options.classNames).filter((className) => Boolean(className)))); 16 | const getTargetBounds = () => { 17 | const sliderRect = slider.container.getBoundingClientRect(); 18 | const sliderWidth = sliderRect.width; 19 | if (!sliderWidth) { 20 | return { targetStart: sliderRect.left, targetEnd: sliderRect.right }; 21 | } 22 | let targetWidth = 0; 23 | if (typeof slider.options.targetWidth === 'function') { 24 | try { 25 | targetWidth = slider.options.targetWidth(slider); 26 | } 27 | catch (error) { 28 | targetWidth = 0; 29 | } 30 | } 31 | if (!Number.isFinite(targetWidth) || targetWidth <= 0) { 32 | targetWidth = sliderWidth; 33 | } 34 | const effectiveTargetWidth = Math.min(targetWidth, sliderWidth); 35 | const offset = (sliderWidth - effectiveTargetWidth) / 2; 36 | const clampedOffset = Math.max(offset, 0); 37 | return { 38 | targetStart: sliderRect.left + clampedOffset, 39 | targetEnd: sliderRect.right - clampedOffset, 40 | }; 41 | }; 42 | const update = () => { 43 | const { targetStart, targetEnd } = getTargetBounds(); 44 | slider.slides.forEach((slide) => { 45 | const slideRect = slide.getBoundingClientRect(); 46 | const slideLeft = slideRect.left; 47 | const slideRight = slideRect.right; 48 | const tolerance = 2; 49 | const overlapsTarget = (slideRight - tolerance) > targetStart && (slideLeft + tolerance) < targetEnd; 50 | const fullyInsideTarget = (slideLeft + tolerance) >= targetStart && (slideRight - tolerance) <= targetEnd; 51 | let nextState = 'hidden'; 52 | if (overlapsTarget) { 53 | nextState = fullyInsideTarget ? 'visible' : 'partlyVisible'; 54 | } 55 | const prevState = slideStates.get(slide); 56 | // If freezeStateOnVisible is enabled and slide was previously visible, keep it frozen 57 | if (options.freezeStateOnVisible && prevState === 'visible') { 58 | return; 59 | } 60 | if (prevState === nextState) { 61 | return; 62 | } 63 | const nextClass = options.classNames[nextState]; 64 | if (prevState) { 65 | const prevClass = options.classNames[prevState]; 66 | if (prevClass !== nextClass && prevClass) { 67 | slide.classList.remove(prevClass); 68 | } 69 | } 70 | else { 71 | uniqueClassNames.forEach((className) => { 72 | if (className !== nextClass) { 73 | slide.classList.remove(className); 74 | } 75 | }); 76 | } 77 | if (nextClass && !slide.classList.contains(nextClass)) { 78 | slide.classList.add(nextClass); 79 | } 80 | slideStates.set(slide, nextState); 81 | }); 82 | }; 83 | slider.on('created', update); 84 | slider.on('pluginsLoaded', update); 85 | slider.on('fullWidthPluginUpdate', update); 86 | slider.on('contentsChanged', update); 87 | slider.on('containerSizeChanged', update); 88 | slider.on('detailsChanged', update); 89 | slider.on('scrollEnd', update); 90 | slider.on('scrollStart', update); 91 | requestAnimationFrame(() => { 92 | requestAnimationFrame(() => update()); 93 | }); 94 | let requestId = 0; 95 | const debouncedUpdate = () => { 96 | if (requestId) { 97 | window.cancelAnimationFrame(requestId); 98 | } 99 | requestId = window.requestAnimationFrame(() => { 100 | update(); 101 | }); 102 | }; 103 | slider.on('scroll', debouncedUpdate); 104 | }; 105 | } 106 | 107 | export { ClassNamesPlugin as default }; 108 | -------------------------------------------------------------------------------- /dist/plugins/dots/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TEXTS = { 2 | dotDescription: 'Page %d of %d', 3 | }; 4 | const DEFAULT_CLASS_NAMES = { 5 | dotsContainer: 'overflow-slider__dots', 6 | dotsItem: 'overflow-slider__dot-item', 7 | }; 8 | function DotsPlugin(args) { 9 | return (slider) => { 10 | var _a, _b, _c; 11 | const options = { 12 | type: (_a = args === null || args === void 0 ? void 0 : args.type) !== null && _a !== void 0 ? _a : 'slide', 13 | texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), 14 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), 15 | container: (_b = args === null || args === void 0 ? void 0 : args.container) !== null && _b !== void 0 ? _b : null, 16 | }; 17 | const dots = document.createElement('div'); 18 | dots.classList.add(options.classNames.dotsContainer); 19 | let pageFocused = null; 20 | const buildDots = () => { 21 | dots.setAttribute('data-has-content', slider.details.hasOverflow.toString()); 22 | dots.innerHTML = ''; 23 | console.log('buildDots'); 24 | const dotsList = document.createElement('ul'); 25 | const count = options.type === 'view' ? slider.details.amountOfPages : slider.details.slideCount; 26 | const currentIndex = options.type === 'view' ? slider.details.currentPage : slider.activeSlideIdx; 27 | if (count <= 1) { 28 | return; 29 | } 30 | for (let i = 0; i < count; i++) { 31 | const dotListItem = document.createElement('li'); 32 | const dot = document.createElement('button'); 33 | dot.setAttribute('type', 'button'); 34 | dot.setAttribute('class', options.classNames.dotsItem); 35 | dot.setAttribute('aria-label', options.texts.dotDescription.replace('%d', (i + 1).toString()).replace('%d', count.toString())); 36 | dot.setAttribute('aria-pressed', (i === currentIndex).toString()); 37 | dot.setAttribute('data-item', (i + 1).toString()); 38 | dotListItem.appendChild(dot); 39 | dotsList.appendChild(dotListItem); 40 | dot.addEventListener('click', () => activateDot(i + 1)); 41 | dot.addEventListener('focus', () => pageFocused = i + 1); 42 | dot.addEventListener('keydown', (e) => { 43 | var _a; 44 | const currentItemItem = dots.querySelector(`[aria-pressed="true"]`); 45 | if (!currentItemItem) { 46 | return; 47 | } 48 | const currentItem = parseInt((_a = currentItemItem.getAttribute('data-item')) !== null && _a !== void 0 ? _a : '1'); 49 | if (e.key === 'ArrowLeft') { 50 | const previousIndex = currentItem - 1; 51 | if (previousIndex > 0) { 52 | const matchingDot = dots.querySelector(`[data-item="${previousIndex}"]`); 53 | if (matchingDot) { 54 | matchingDot.focus(); 55 | } 56 | activateDot(previousIndex); 57 | } 58 | } 59 | if (e.key === 'ArrowRight') { 60 | const nextIndex = currentItem + 1; 61 | if (nextIndex <= count) { 62 | const matchingDot = dots.querySelector(`[data-item="${nextIndex}"]`); 63 | if (matchingDot) { 64 | matchingDot.focus(); 65 | } 66 | activateDot(nextIndex); 67 | } 68 | } 69 | }); 70 | } 71 | dots.appendChild(dotsList); 72 | // return focus to same page after rebuild 73 | if (pageFocused) { 74 | const matchingDot = dots.querySelector(`[data-item="${pageFocused}"]`); 75 | if (matchingDot) { 76 | matchingDot.focus(); 77 | } 78 | } 79 | }; 80 | const activateDot = (index) => { 81 | console.log('activateDot', index, 'slider.details', slider.details); 82 | if (options.type === 'view') { 83 | const count = slider.details.amountOfPages; 84 | let targetPosition = slider.details.containerWidth * (index - 1); 85 | // For the last page, scroll to the maximum scroll position to ensure it activates 86 | if (index === count) { 87 | const maxScroll = slider.details.scrollableAreaWidth - slider.details.containerWidth; 88 | targetPosition = maxScroll; 89 | } 90 | const scrollLeft = slider.options.rtl ? -targetPosition : targetPosition; 91 | slider.container.scrollTo({ 92 | left: scrollLeft, 93 | behavior: slider.options.scrollBehavior 94 | }); 95 | } 96 | else { 97 | slider.moveToSlide(index - 1); 98 | } 99 | }; 100 | buildDots(); 101 | if (options.container) { 102 | options.container.appendChild(dots); 103 | } 104 | else { 105 | (_c = slider.container.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(dots, slider.container.nextSibling); 106 | } 107 | slider.on('scrollEnd', buildDots); 108 | slider.on('contentsChanged', buildDots); 109 | slider.on('containerSizeChanged', buildDots); 110 | slider.on('detailsChanged', buildDots); 111 | }; 112 | } 113 | 114 | export { DotsPlugin as default }; 115 | -------------------------------------------------------------------------------- /docs/dist/plugins/dots/index.esm.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_TEXTS = { 2 | dotDescription: 'Page %d of %d', 3 | }; 4 | const DEFAULT_CLASS_NAMES = { 5 | dotsContainer: 'overflow-slider__dots', 6 | dotsItem: 'overflow-slider__dot-item', 7 | }; 8 | function DotsPlugin(args) { 9 | return (slider) => { 10 | var _a, _b, _c; 11 | const options = { 12 | type: (_a = args === null || args === void 0 ? void 0 : args.type) !== null && _a !== void 0 ? _a : 'slide', 13 | texts: Object.assign(Object.assign({}, DEFAULT_TEXTS), (args === null || args === void 0 ? void 0 : args.texts) || []), 14 | classNames: Object.assign(Object.assign({}, DEFAULT_CLASS_NAMES), (args === null || args === void 0 ? void 0 : args.classNames) || []), 15 | container: (_b = args === null || args === void 0 ? void 0 : args.container) !== null && _b !== void 0 ? _b : null, 16 | }; 17 | const dots = document.createElement('div'); 18 | dots.classList.add(options.classNames.dotsContainer); 19 | let pageFocused = null; 20 | const buildDots = () => { 21 | dots.setAttribute('data-has-content', slider.details.hasOverflow.toString()); 22 | dots.innerHTML = ''; 23 | console.log('buildDots'); 24 | const dotsList = document.createElement('ul'); 25 | const count = options.type === 'view' ? slider.details.amountOfPages : slider.details.slideCount; 26 | const currentIndex = options.type === 'view' ? slider.details.currentPage : slider.activeSlideIdx; 27 | if (count <= 1) { 28 | return; 29 | } 30 | for (let i = 0; i < count; i++) { 31 | const dotListItem = document.createElement('li'); 32 | const dot = document.createElement('button'); 33 | dot.setAttribute('type', 'button'); 34 | dot.setAttribute('class', options.classNames.dotsItem); 35 | dot.setAttribute('aria-label', options.texts.dotDescription.replace('%d', (i + 1).toString()).replace('%d', count.toString())); 36 | dot.setAttribute('aria-pressed', (i === currentIndex).toString()); 37 | dot.setAttribute('data-item', (i + 1).toString()); 38 | dotListItem.appendChild(dot); 39 | dotsList.appendChild(dotListItem); 40 | dot.addEventListener('click', () => activateDot(i + 1)); 41 | dot.addEventListener('focus', () => pageFocused = i + 1); 42 | dot.addEventListener('keydown', (e) => { 43 | var _a; 44 | const currentItemItem = dots.querySelector(`[aria-pressed="true"]`); 45 | if (!currentItemItem) { 46 | return; 47 | } 48 | const currentItem = parseInt((_a = currentItemItem.getAttribute('data-item')) !== null && _a !== void 0 ? _a : '1'); 49 | if (e.key === 'ArrowLeft') { 50 | const previousIndex = currentItem - 1; 51 | if (previousIndex > 0) { 52 | const matchingDot = dots.querySelector(`[data-item="${previousIndex}"]`); 53 | if (matchingDot) { 54 | matchingDot.focus(); 55 | } 56 | activateDot(previousIndex); 57 | } 58 | } 59 | if (e.key === 'ArrowRight') { 60 | const nextIndex = currentItem + 1; 61 | if (nextIndex <= count) { 62 | const matchingDot = dots.querySelector(`[data-item="${nextIndex}"]`); 63 | if (matchingDot) { 64 | matchingDot.focus(); 65 | } 66 | activateDot(nextIndex); 67 | } 68 | } 69 | }); 70 | } 71 | dots.appendChild(dotsList); 72 | // return focus to same page after rebuild 73 | if (pageFocused) { 74 | const matchingDot = dots.querySelector(`[data-item="${pageFocused}"]`); 75 | if (matchingDot) { 76 | matchingDot.focus(); 77 | } 78 | } 79 | }; 80 | const activateDot = (index) => { 81 | console.log('activateDot', index, 'slider.details', slider.details); 82 | if (options.type === 'view') { 83 | const count = slider.details.amountOfPages; 84 | let targetPosition = slider.details.containerWidth * (index - 1); 85 | // For the last page, scroll to the maximum scroll position to ensure it activates 86 | if (index === count) { 87 | const maxScroll = slider.details.scrollableAreaWidth - slider.details.containerWidth; 88 | targetPosition = maxScroll; 89 | } 90 | const scrollLeft = slider.options.rtl ? -targetPosition : targetPosition; 91 | slider.container.scrollTo({ 92 | left: scrollLeft, 93 | behavior: slider.options.scrollBehavior 94 | }); 95 | } 96 | else { 97 | slider.moveToSlide(index - 1); 98 | } 99 | }; 100 | buildDots(); 101 | if (options.container) { 102 | options.container.appendChild(dots); 103 | } 104 | else { 105 | (_c = slider.container.parentNode) === null || _c === void 0 ? void 0 : _c.insertBefore(dots, slider.container.nextSibling); 106 | } 107 | slider.on('scrollEnd', buildDots); 108 | slider.on('contentsChanged', buildDots); 109 | slider.on('containerSizeChanged', buildDots); 110 | slider.on('detailsChanged', buildDots); 111 | }; 112 | } 113 | 114 | export { DotsPlugin as default }; 115 | -------------------------------------------------------------------------------- /src/plugins/scroll-indicator/index.ts: -------------------------------------------------------------------------------- 1 | import { Slider, DeepPartial } from '../../core/types'; 2 | 3 | const DEFAULT_CLASS_NAMES = { 4 | scrollIndicator: 'overflow-slider__scroll-indicator', 5 | scrollIndicatorBar: 'overflow-slider__scroll-indicator-bar', 6 | scrollIndicatorButton: 'overflow-slider__scroll-indicator-button', 7 | }; 8 | 9 | export type ScrollIndicatorOptions = { 10 | classNames: { 11 | scrollIndicator: string; 12 | scrollIndicatorBar: string; 13 | scrollIndicatorButton: string; 14 | }, 15 | container: HTMLElement | null, 16 | }; 17 | 18 | export default function ScrollIndicatorPlugin(args?: DeepPartial) { 19 | return (slider: Slider) => { 20 | 21 | const options = { 22 | classNames: { 23 | ...DEFAULT_CLASS_NAMES, 24 | ...args?.classNames || [] 25 | }, 26 | container: args?.container ?? null, 27 | }; 28 | 29 | const scrollbarContainer = document.createElement('div'); 30 | scrollbarContainer.setAttribute('class', options.classNames.scrollIndicator); 31 | scrollbarContainer.setAttribute('tabindex', '0'); 32 | scrollbarContainer.setAttribute('role', 'scrollbar'); 33 | scrollbarContainer.setAttribute('aria-controls', slider.container.getAttribute('id') ?? ''); 34 | scrollbarContainer.setAttribute('aria-orientation', 'horizontal'); 35 | scrollbarContainer.setAttribute('aria-valuemax', '100'); 36 | scrollbarContainer.setAttribute('aria-valuemin', '0'); 37 | scrollbarContainer.setAttribute('aria-valuenow', '0'); 38 | 39 | const scrollbar = document.createElement('div'); 40 | scrollbar.setAttribute('class', options.classNames.scrollIndicatorBar); 41 | 42 | const scrollbarButton = document.createElement('div'); 43 | scrollbarButton.setAttribute('class', options.classNames.scrollIndicatorButton); 44 | scrollbarButton.setAttribute('data-is-grabbed', 'false'); 45 | 46 | scrollbar.appendChild(scrollbarButton); 47 | scrollbarContainer.appendChild(scrollbar); 48 | 49 | const setDataAttributes = () => { 50 | scrollbarContainer.setAttribute('data-has-overflow', slider.details.hasOverflow.toString()); 51 | } 52 | setDataAttributes(); 53 | 54 | const getScrollbarButtonLeftOffset = () => { 55 | const contentRatio = scrollbarButton.offsetWidth / slider.details.containerWidth; 56 | const scrollAmount = slider.getScrollLeft() * contentRatio; 57 | if (slider.options.rtl) { 58 | return scrollbar.offsetWidth - scrollbarButton.offsetWidth - scrollAmount; 59 | } 60 | return scrollAmount; 61 | }; 62 | 63 | let requestId = 0; 64 | const update = () => { 65 | if (requestId) { 66 | window.cancelAnimationFrame(requestId); 67 | } 68 | 69 | requestId = window.requestAnimationFrame(() => { 70 | const scrollbarButtonWidth = (slider.details.containerWidth / slider.container.scrollWidth) * 100; 71 | 72 | const scrollLeftInPortion = getScrollbarButtonLeftOffset(); 73 | scrollbarButton.style.width = `${scrollbarButtonWidth}%`; 74 | scrollbarButton.style.transform = `translateX(${scrollLeftInPortion}px)`; 75 | 76 | const scrollLeft = slider.getScrollLeft(); 77 | const scrollWidth = slider.getInclusiveScrollWidth(); 78 | const containerWidth = slider.container.offsetWidth; 79 | const scrollPercentage = (scrollLeft / (scrollWidth - containerWidth)) * 100; 80 | scrollbarContainer.setAttribute('aria-valuenow', Math.round(Number.isNaN(scrollPercentage) ? 0 : scrollPercentage).toString()); 81 | }); 82 | }; 83 | 84 | if (options.container) { 85 | options.container.appendChild(scrollbarContainer); 86 | } else { 87 | slider.container.parentNode?.insertBefore(scrollbarContainer, slider.container.nextSibling); 88 | } 89 | 90 | update(); 91 | slider.on('scroll', update); 92 | slider.on('contentsChanged', update); 93 | slider.on('containerSizeChanged', update); 94 | slider.on('detailsChanged', setDataAttributes); 95 | 96 | scrollbarContainer.addEventListener('keydown', (e) => { 97 | if (e.key === 'ArrowLeft') { 98 | slider.moveToDirection('prev'); 99 | } else if (e.key === 'ArrowRight') { 100 | slider.moveToDirection('next'); 101 | } 102 | }); 103 | 104 | let isInteractionDown = false; 105 | let startX = 0; 106 | let scrollLeft = slider.getScrollLeft(); 107 | 108 | scrollbarContainer.addEventListener('click', (e) => { 109 | if ( e.target == scrollbarButton ) { 110 | return; 111 | } 112 | const scrollbarButtonWidth = scrollbarButton.offsetWidth; 113 | const scrollbarButtonLeft = getScrollbarButtonLeftOffset(); 114 | const scrollbarButtonRight = scrollbarButtonLeft + scrollbarButtonWidth; 115 | const clickX = (e as MouseEvent).pageX - scrollbarContainer.getBoundingClientRect().left; 116 | if (Math.floor(clickX) < Math.floor(scrollbarButtonLeft)) { 117 | console.log('move left'); 118 | slider.moveToDirection(slider.options.rtl ? 'next' : 'prev'); 119 | } else if (Math.floor(clickX) > Math.floor(scrollbarButtonRight)) { 120 | console.log('move right'); 121 | slider.moveToDirection(slider.options.rtl ? 'prev' : 'next'); 122 | } 123 | }); 124 | 125 | const onInteractionDown = (e: MouseEvent | TouchEvent) => { 126 | isInteractionDown = true; 127 | const pageX = (e as MouseEvent).pageX || (e as TouchEvent).touches[0].pageX; 128 | startX = pageX - scrollbarContainer.offsetLeft; 129 | scrollLeft = slider.getScrollLeft(); 130 | scrollbarButton.style.cursor = 'grabbing'; 131 | scrollbarButton.setAttribute('data-is-grabbed', 'true'); 132 | 133 | e.preventDefault(); 134 | e.stopPropagation(); 135 | }; 136 | 137 | const onInteractionMove = (e: MouseEvent | TouchEvent) => { 138 | if (!isInteractionDown) { 139 | return; 140 | } 141 | e.preventDefault(); 142 | const pageX = (e as MouseEvent).pageX || (e as TouchEvent).touches[0].pageX; 143 | const x = pageX - scrollbarContainer.offsetLeft; 144 | const scrollingFactor = slider.details.scrollableAreaWidth / scrollbarContainer.offsetWidth; 145 | 146 | const walk = (x - startX) * scrollingFactor; 147 | const distance = slider.options.rtl ? scrollLeft - walk : scrollLeft + walk; 148 | slider.setScrollLeft(distance); 149 | }; 150 | 151 | const onInteractionUp = () => { 152 | isInteractionDown = false; 153 | scrollbarButton.style.cursor = ''; 154 | scrollbarButton.setAttribute('data-is-grabbed', 'false'); 155 | }; 156 | 157 | scrollbarButton.addEventListener('mousedown', onInteractionDown); 158 | scrollbarButton.addEventListener('touchstart', onInteractionDown); 159 | 160 | window.addEventListener('mousemove', onInteractionMove); 161 | window.addEventListener('touchmove', onInteractionMove, { passive: false }); 162 | 163 | window.addEventListener('mouseup', onInteractionUp); 164 | window.addEventListener('touchend', onInteractionUp); 165 | }; 166 | } 167 | --------------------------------------------------------------------------------