import { defineComponent , onMounted , onUnmounted , ref , reactive , provide , computed , watchEffect , h , watch , } from 'vue' ; import { defaultConfigs } from '@/partials/defaults' ; import { debounce , throttle , getSlidesVNodes , getCurrentSlideIndex , getMaxSlideIndex , getMinSlideIndex , getSlidesToScroll , } from '@/partials/utils' ; import { Data , SetupContext , CarouselConfig , Ref , CarouselNav , ElementStyleObject , Breakpoints , } from '@/types' ; export default defineComponent ( { name : 'Carousel' , props : { itemsToShow : { default : defaultConfigs . itemsToShow , type : Number , } , itemsToScroll : { default : defaultConfigs . itemsToScroll , type : Number , } , wrapAround : { default : defaultConfigs . wrapAround , type : Boolean , } , snapAlign : { default : defaultConfigs . snapAlign , validator ( value ) { return [ 'start' , 'end' , 'center' , 'center-even' , 'center-odd' ] . includes ( value ) ; } , } , transition : { default : defaultConfigs . transition , type : Number , } , breakpoints : { default : defaultConfigs . breakpoints , type : Object , } , autoplay : { default : defaultConfigs . autoplay , type : Number , } , pauseAutoplayOnHover : { default : defaultConfigs . pauseAutoplayOnHover , type : Boolean , } , modelValue : { default : undefined , type : Number , } , mouseDrag : { default : defaultConfigs . mouseDrag , type : Boolean , } , touchDrag : { default : defaultConfigs . touchDrag , type : Boolean , } , settings : { default ( ) { return { } ; } , type : Object , } , } , setup ( props , { slots , emit , expose } ) { const root = ref ( null ) ; const slides = ref ( [ ] ) ; const slidesBuffer = ref ( [ ] ) ; const slideWidth = ref ( 0 ) ; const slidesCount = ref ( 1 ) ; const autoplayTimer = ref ( null ) ; const transitionTimer = ref ( null ) ; let breakpoints = ref ( { } ) ; let defaultConfig = { ... defaultConfigs } ; const config = reactive < CarouselConfig > ( { ... defaultConfigs } ) ; const currentSlideIndex = ref ( config . modelValue ?? 0 ) ; const prevSlideIndex = ref ( 0 ) ; const middleSlideIndex = ref ( 0 ) ; const maxSlideIndex = ref ( 0 ) ; const minSlideIndex = ref ( 0 ) ; provide ( 'config' , config ) ; provide ( 'slidesBuffer' , slidesBuffer ) ; provide ( 'slidesCount' , slidesCount ) ; provide ( 'currentSlide' , currentSlideIndex ) ; provide ( 'maxSlide' , maxSlideIndex ) ; provide ( 'minSlide' , minSlideIndex ) ; /**
         * Configs
         */ function initDefaultConfigs ( ) { const mergedConfigs = { ... props , ... ( props . settings as CarouselConfig ) , } ; breakpoints = ref ( { ... mergedConfigs . breakpoints } ) ; defaultConfig = { ... mergedConfigs , settings : undefined , breakpoints : undefined } ; bindConfigs ( defaultConfig ) ; } function updateBreakpointsConfigs ( ) { const breakpointsArray = Object . keys ( breakpoints . value ) . map ( ( key ) : number => Number ( key ) ) . sort ( ( a , b ) => + b - + a ) ; let newConfig = { ... defaultConfig } ; breakpointsArray . some ( ( breakpoint ) : boolean => { const isMatched = window . matchMedia ( `(min-width: ${ breakpoint }px)` ) . matches ; if ( isMatched ) { newConfig = { ... newConfig , ... ( breakpoints . value [ breakpoint ] as CarouselConfig ) , } ; return true ; } return false ; } ) ; bindConfigs ( newConfig ) ; } function bindConfigs ( newConfig ) { for ( let key in newConfig ) { config [ key ] = newConfig [ key ] ; } } const handleWindowResize = debounce ( ( ) => { if ( breakpoints . value ) { updateBreakpointsConfigs ( ) ; updateSlidesData ( ) ; } updateSlideWidth ( ) ; } , 16 ) ; /**
         * Setup functions
         */ function updateSlideWidth ( ) { if ( ! root . value ) return ; const rect = root . value . getBoundingClientRect ( ) ; slideWidth . value = rect . width / config . itemsToShow ; } function updateSlidesData ( ) { slidesCount . value = slides . value . length ; if ( slidesCount . value <= 0 ) return ; middleSlideIndex . value = Math . ceil ( ( slidesCount . value - 1 ) / 2 ) ; maxSlideIndex . value = getMaxSlideIndex ( config , slidesCount . value ) ; minSlideIndex . value = getMinSlideIndex ( config ) ; currentSlideIndex . value = getCurrentSlideIndex ( config , currentSlideIndex . value , maxSlideIndex . value , minSlideIndex . value ) ; } function updateSlidesBuffer ( ) { const slidesArray = [ ... Array ( slidesCount . value ) . keys ( ) ] ; const shouldShiftSlides = config . wrapAround && config . itemsToShow + 1 <= slidesCount . value ; if ( shouldShiftSlides ) { const buffer = config . itemsToShow !== 1 ? Math . round ( ( slidesCount . value - config . itemsToShow ) / 2 ) : 0 ; let shifts = buffer - currentSlideIndex . value ; if ( config . snapAlign === 'end' ) { shifts += Math . floor ( config . itemsToShow - 1 ) ; } else if ( config . snapAlign === 'center' || config . snapAlign === 'center-odd' ) { shifts ++ ; } if ( shifts < 0 ) { for ( let i = shifts ; i < 0 ; i ++ ) { slidesArray . push ( Number ( slidesArray . shift ( ) ) ) ; } } else { for ( let i = 0 ; i < shifts ; i ++ ) { slidesArray . unshift ( Number ( slidesArray . pop ( ) ) ) ; } } } slidesBuffer . value = slidesArray ; } onMounted ( ( ) : void => { if ( breakpoints . value ) { updateBreakpointsConfigs ( ) ; updateSlidesData ( ) ; } updateSlideWidth ( ) ; if ( config . autoplay && config . autoplay > 0 ) { initializeAutoplay ( ) ; } window . addEventListener ( 'resize' , handleWindowResize , { passive : true } ) ; } ) ; onUnmounted ( ( ) => { if ( transitionTimer . value ) { clearTimeout ( transitionTimer . value ) ; } resetAutoplayTimer ( false ) ; } ) ; /**
         * Carousel Event listeners
         */ let isTouch = false ; const startPosition = { x : 0 , y : 0 } ; const endPosition = { x : 0 , y : 0 } ; const dragged = reactive ( { x : 0 , y : 0 } ) ; const isDragging = ref ( false ) ; const isHover = ref ( false ) ; const handleMouseEnter = ( ) : void => { isHover . value = true ; } ; const handleMouseLeave = ( ) : void => { isHover . value = false ; } ; const handleDrag = throttle ( ( event ) : void => { if ( ! isTouch ) event . preventDefault ( ) ; endPosition . x = isTouch ? event . touches [ 0 ] . clientX : event . clientX ; endPosition . y = isTouch ? event . touches [ 0 ] . clientY : event . clientY ; const deltaX = endPosition . x - startPosition . x ; const deltaY = endPosition . y - startPosition . y ; dragged . y = deltaY ; dragged . x = deltaX ; } , 16 ) ; function handleDragStart ( event ) { isTouch = event . type === 'touchstart' ; if ( ! isTouch ) event . preventDefault ( ) ; if ( ( ! isTouch && event . button !== 0 ) || isSliding . value ) { return ; } isDragging . value = true ; startPosition . x = isTouch ? event . touches [ 0 ] . clientX : event . clientX ; startPosition . y = isTouch ? event . touches [ 0 ] . clientY : event . clientY ; document . addEventListener ( isTouch ? 'touchmove' : 'mousemove' , handleDrag ) ; document . addEventListener ( isTouch ? 'touchend' : 'mouseup' , handleDragEnd ) ; } function handleDragEnd ( ) { isDragging . value = false ; const tolerance = Math . sign ( dragged . x ) * 0.4 ; const draggedSlides = Math . round ( dragged . x / slideWidth . value + tolerance ) ; let newSlide = getCurrentSlideIndex ( config , currentSlideIndex . value - draggedSlides , maxSlideIndex . value , minSlideIndex . value ) ; slideTo ( newSlide ) ; dragged . x = 0 ; dragged . y = 0 ; document . removeEventListener ( isTouch ? 'touchmove' : 'mousemove' , handleDrag ) ; document . removeEventListener ( isTouch ? 'touchend' : 'mouseup' , handleDragEnd ) ; } /**
         * Autoplay
         */ function initializeAutoplay ( ) { autoplayTimer . value = setInterval ( ( ) => { if ( config . pauseAutoplayOnHover && isHover . value ) { return ; } next ( ) ; } , config . autoplay ) ; } function resetAutoplayTimer ( restart = true ) { if ( ! autoplayTimer . value ) { return ; } clearInterval ( autoplayTimer . value ) ; if ( restart ) { initializeAutoplay ( ) ; } } /**
         * Navigation function
         */ const isSliding = ref ( false ) ; function slideTo ( slideIndex , mute = false ) { resetAutoplayTimer ( ) ; if ( currentSlideIndex . value === slideIndex || isSliding . value ) { return ; } const lastSlideIndex = slidesCount . value - 1 ; if ( slideIndex > lastSlideIndex ) { return slideTo ( slideIndex - slidesCount . value ) ; } if ( slideIndex < 0 ) { return slideTo ( slideIndex + slidesCount . value ) ; } isSliding . value = true ; prevSlideIndex . value = currentSlideIndex . value ; currentSlideIndex . value = slideIndex ; if ( ! mute ) { emit ( 'update:modelValue' , currentSlideIndex . value ) ; } transitionTimer . value = setTimeout ( ( ) : void => { if ( config . wrapAround ) updateSlidesBuffer ( ) ; isSliding . value = false ; } , config . transition ) ; } function next ( ) { let nextSlide = currentSlideIndex . value + config . itemsToScroll ; if ( ! config . wrapAround ) { nextSlide = Math . min ( nextSlide , maxSlideIndex . value ) ; } slideTo ( nextSlide ) ; } function prev ( ) { let prevSlide = currentSlideIndex . value - config . itemsToScroll ; if ( ! config . wrapAround ) { prevSlide = Math . max ( prevSlide , minSlideIndex . value ) ; } slideTo ( prevSlide ) ; } const nav = { slideTo , next , prev } ; provide ( 'nav' , nav ) ; /**
         * Track style
         */ const slidesToScroll = computed ( ( ) => getSlidesToScroll ( { slidesBuffer : slidesBuffer . value , itemsToShow : config . itemsToShow , snapAlign : config . snapAlign , wrapAround : Boolean ( config . wrapAround ) , currentSlide : currentSlideIndex . value , slidesCount : slidesCount . value , } ) ) ; provide ( 'slidesToScroll' , slidesToScroll ) ; const trackStyle = computed ( ( ) : ElementStyleObject => { const xScroll = dragged . x - slidesToScroll . value * slideWidth . value ; return { transform : `translateX(${ xScroll }px)` , transition : `${ isSliding . value ? config . transition : 0 }ms` , } ; } ) ; function initCarousel ( ) { initDefaultConfigs ( ) ; } function restartCarousel ( ) { initDefaultConfigs ( ) ; updateBreakpointsConfigs ( ) ; updateSlidesData ( ) ; updateSlidesBuffer ( ) ; updateSlideWidth ( ) ; } function updateCarousel ( ) { updateSlidesData ( ) ; updateSlidesBuffer ( ) ; } watch ( props , restartCarousel ) ; initCarousel ( ) ; watchEffect ( ( ) : void => { const needToUpdate = slidesCount . value !== slides . value . length ; const currentSlideUpdated = props . modelValue !== undefined && currentSlideIndex . value !== props . modelValue ; if ( currentSlideUpdated ) { slideTo ( Number ( props . modelValue ) , true ) ; } if ( needToUpdate ) { updateCarousel ( ) ; } } ) ; const data = { config , slidesBuffer , slidesCount , slideWidth , currentSlide : currentSlideIndex , maxSlide : maxSlideIndex , minSlide : minSlideIndex , middleSlide : middleSlideIndex , } ; expose ( { updateBreakpointsConfigs , updateSlidesData , updateSlideWidth , updateSlidesBuffer , initCarousel , restartCarousel , updateCarousel , slideTo , next , prev , nav , data , } ) ; const slotSlides = slots . default || slots . slides ; const slotAddons = slots . addons ; const slotsProps = reactive ( data ) ; return ( ) => { const slidesElements = getSlidesVNodes ( slotSlides ?. ( slotsProps ) ) ; const addonsElements = slotAddons ?. ( slotsProps ) || [ ] ; slides . value = slidesElements ; slidesElements . forEach ( ( el , index ) => ( el . props . index = index ) ) ; const trackEl = h ( 'ol' , { class : 'carousel__track' , style : trackStyle . value , onMousedown : config . mouseDrag ? handleDragStart : null , onTouchstart : config . touchDrag ? handleDragStart : null , } , slidesElements ) ; const viewPortEl = h ( 'div' , { class : 'carousel__viewport' } , trackEl ) ; return h ( 'section' , { ref : root , class : 'carousel' , 'aria-label' : 'Gallery' , onMouseenter : handleMouseEnter , onMouseleave : handleMouseLeave , } , [ viewPortEl , addonsElements ] ) ; } ; } , } ) ;