import * as React from "react" ; import { throttle , getClones , checkClonesPosition , getInitialState , throwError , getItemClientSideWidth , populateNextSlides , populatePreviousSlides , populateSlidesOnMouseTouchMove , isInLeftEnd , isInRightEnd , getInitialSlideInInfiniteMode , notEnoughChildren } from "./utils" ; import { CarouselInternalState , CarouselProps , StateCallBack , Direction , isMouseMoveEvent , SkipCallbackOptions } from "./types" ; import Dots from "./Dots" ; import { LeftArrow , RightArrow } from "./Arrows" ; import CarouselItems from "./CarouselItems" ; import { getTransform } from "./utils/common" ; const defaultTransitionDuration = 400 ; const defaultTransition = "transform 400ms ease-in-out" ; class Carousel extends React . Component < CarouselProps , CarouselInternalState > { public static defaultProps = { slidesToSlide : 1 , infinite : false , draggable : true , swipeable : true , arrows : true , renderArrowsWhenDisabled : false , containerClass : "" , sliderClass : "" , itemClass : "" , keyBoardControl : true , autoPlaySpeed : 3000 , showDots : false , renderDotsOutside : false , renderButtonGroupOutside : false , minimumTouchDrag : 80 , className : "" , dotListClass : "" , focusOnSelect : false , centerMode : false , additionalTransfrom : 0 , pauseOnHover : true } ; private readonly containerRef : React . RefObject < HTMLDivElement > ; private readonly listRef : React . RefObject < HTMLUListElement > ; public onMove : boolean ; public initialX : number ; public lastX : number ; public isAnimationAllowed : boolean ; public direction : Direction ; public autoPlay ? : any ; public isInThrottle ? : boolean ; public initialY : number ; private transformPlaceHolder : number ; private itemsToShowTimeout : any ; constructor ( props ) { super ( props ) ; this . containerRef = React . createRef ( ) ; this . listRef = React . createRef ( ) ; this . state = { itemWidth : 0 , slidesToShow : 0 , currentSlide : 0 , totalItems : React . Children . count ( props . children ) , deviceType : "" , domLoaded : false , transform : 0 , containerWidth : 0 } ; this . onResize = this . onResize . bind ( this ) ; this . handleDown = this . handleDown . bind ( this ) ; this . handleMove = this . handleMove . bind ( this ) ; this . handleOut = this . handleOut . bind ( this ) ; this . onKeyUp = this . onKeyUp . bind ( this ) ; this . handleEnter = this . handleEnter . bind ( this ) ; this . setIsInThrottle = this . setIsInThrottle . bind ( this ) ; this . next = throttle ( this . next . bind ( this ) , props . transitionDuration || defaultTransitionDuration , this . setIsInThrottle ) ; this . previous = throttle ( this . previous . bind ( this ) , props . transitionDuration || defaultTransitionDuration , this . setIsInThrottle ) ; this . goToSlide = throttle ( this . goToSlide . bind ( this ) , props . transitionDuration || defaultTransitionDuration , this . setIsInThrottle ) ; this . onMove = false ; this . initialX = 0 ; this . lastX = 0 ; this . isAnimationAllowed = false ; this . direction = "" ; this . initialY = 0 ; this . isInThrottle = false ; this . transformPlaceHolder = 0 ; } public resetTotalItems ( ) { const totalItems = React . Children . count ( this . props . children ) ; const currentSlide = notEnoughChildren ( this . state ) ? 0 : Math . max ( 0 , Math . min ( this . state . currentSlide , totalItems ) ) ; this . setState ( { totalItems , currentSlide } , ( ) => { this . setContainerAndItemWidth ( this . state . slidesToShow , true ) ; } ) ; } public setIsInThrottle ( isInThrottle = false ) { this . isInThrottle = isInThrottle ; } public setTransformDirectly ( position , withAnimation ? ) { const { additionalTransfrom } = this . props ; const currentTransform = getTransform ( this . state , this . props , position ) ; this . transformPlaceHolder = position ; if ( this . listRef && this . listRef . current ) { this . setAnimationDirectly ( withAnimation ) ; this . listRef . current . style . transform = `translate3d(${ currentTransform + additionalTransfrom ! }px,0,0)` ; } } public setAnimationDirectly ( animationAllowed ? ) { if ( this . listRef && this . listRef . current ) { if ( animationAllowed ) { this . listRef . current . style . transition = this . props . customTransition || defaultTransition ; } else { this . listRef . current . style . transition = "none" ; } } } public componentDidMount ( ) { this . setState ( { domLoaded : true } ) ; this . setItemsToShow ( ) ; window . addEventListener ( "resize" , this . onResize as React . EventHandler < any > ) ; this . onResize ( true ) ; if ( this . props . keyBoardControl ) { window . addEventListener ( "keyup" , this . onKeyUp as React . EventHandler < any > ) ; } if ( this . props . autoPlay && this . props . autoPlaySpeed ) { this . autoPlay = setInterval ( this . next , this . props . autoPlaySpeed ) ; } } public setClones ( slidesToShow , itemWidth ? , forResizing ? , resetCurrentSlide = false ) { this . isAnimationAllowed = false ; const childrenArr = React . Children . toArray ( this . props . children ) ; const initialSlide = getInitialSlideInInfiniteMode ( slidesToShow || this . state . slidesToShow , childrenArr ) ; const clones = getClones ( this . state . slidesToShow , childrenArr ) ; const currentSlide = childrenArr . length < this . state . slidesToShow ? 0 : this . state . currentSlide ; this . setState ( { totalItems : clones . length , currentSlide : forResizing && ! resetCurrentSlide ? currentSlide : initialSlide } , ( ) => { this . correctItemsPosition ( itemWidth || this . state . itemWidth ) ; } ) ; } public setItemsToShow ( shouldCorrectItemPosition ? , resetCurrentSlide ? ) { const { responsive } = this . props ; Object . keys ( responsive ) . forEach ( item => { const { breakpoint , items } = responsive [ item ] ; const { max , min } = breakpoint ; if ( window . innerWidth >= min && window . innerWidth <= max ) { this . setState ( { slidesToShow : items , deviceType : item } ) ; this . setContainerAndItemWidth ( items , shouldCorrectItemPosition , resetCurrentSlide ) ; } } ) ; } public setContainerAndItemWidth ( slidesToShow , shouldCorrectItemPosition ? , resetCurrentSlide ? ) { if ( this . containerRef && this . containerRef . current ) { const containerWidth = this . containerRef . current . offsetWidth ; const itemWidth = getItemClientSideWidth ( this . props , slidesToShow , containerWidth ) ; this . setState ( { containerWidth , itemWidth } , ( ) => { if ( this . props . infinite ) { this . setClones ( slidesToShow , itemWidth , shouldCorrectItemPosition , resetCurrentSlide ) ; } } ) ; if ( shouldCorrectItemPosition ) { this . correctItemsPosition ( itemWidth ) ; } } } public correctItemsPosition ( itemWidth , isAnimationAllowed ? , setToDomDirectly ? ) { if ( isAnimationAllowed ) { this . isAnimationAllowed = true ; } if ( ! isAnimationAllowed && this . isAnimationAllowed ) { this . isAnimationAllowed = false ; } const nextTransform = this . state . totalItems < this . state . slidesToShow ? 0 : - ( itemWidth * this . state . currentSlide ) ; if ( setToDomDirectly ) { this . setTransformDirectly ( nextTransform , true ) ; } this . setState ( { transform : nextTransform } ) ; } public onResize ( value ? ) { const { infinite } = this . props ; let shouldCorrectItemPosition ; if ( ! infinite ) { shouldCorrectItemPosition = false ; } else { if ( typeof value === "boolean" && value ) { shouldCorrectItemPosition = false ; } else { shouldCorrectItemPosition = true ; } } this . setItemsToShow ( shouldCorrectItemPosition ) ; } public componentDidUpdate ( { keyBoardControl , autoPlay , children } , { containerWidth , domLoaded , currentSlide } ) { if ( this . containerRef && this . containerRef . current && this . containerRef . current . offsetWidth !== containerWidth ) { if ( this . itemsToShowTimeout ) { clearTimeout ( this . itemsToShowTimeout ) ; } this . itemsToShowTimeout = setTimeout ( ( ) => { this . setItemsToShow ( true ) ; } , this . props . transitionDuration || defaultTransitionDuration ) ; } if ( keyBoardControl && ! this . props . keyBoardControl ) { window . removeEventListener ( "keyup" , this . onKeyUp as React . EventHandler < any > ) ; } if ( ! keyBoardControl && this . props . keyBoardControl ) { window . addEventListener ( "keyup" , this . onKeyUp as React . EventHandler < any > ) ; } if ( autoPlay && ! this . props . autoPlay && this . autoPlay ) { clearInterval ( this . autoPlay ) ; this . autoPlay = undefined ; } if ( ! autoPlay && this . props . autoPlay && ! this . autoPlay ) { this . autoPlay = setInterval ( this . next , this . props . autoPlaySpeed ) ; } if ( children . length !== this . props . children . length ) { setTimeout ( ( ) => { if ( this . props . infinite ) { this . setClones ( this . state . slidesToShow , this . state . itemWidth , true , true ) ; } else { this . resetTotalItems ( ) ; } } , this . props . transitionDuration || defaultTransitionDuration ) ; } else if ( this . props . infinite && this . state . currentSlide !== currentSlide ) { this . correctClonesPosition ( { domLoaded } ) ; } if ( this . transformPlaceHolder !== this . state . transform ) { this . transformPlaceHolder = this . state . transform ; } } public correctClonesPosition ( { domLoaded } ) { const childrenArr = React . Children . toArray ( this . props . children ) ; const { isReachingTheEnd , isReachingTheStart , nextSlide , nextPosition } = checkClonesPosition ( this . state , childrenArr , this . props ) ; if ( this . state . domLoaded && domLoaded ) { if ( isReachingTheEnd || isReachingTheStart ) { this . isAnimationAllowed = false ; setTimeout ( ( ) => { this . setState ( { transform : nextPosition , currentSlide : nextSlide } ) ; } , this . props . transitionDuration || defaultTransitionDuration ) ; } } } public next ( slidesHavePassed = 0 ) { const { afterChange , beforeChange } = this . props ; if ( notEnoughChildren ( this . state ) ) { return ; } const { nextSlides , nextPosition } = populateNextSlides ( this . state , this . props , slidesHavePassed ) ; const previousSlide = this . state . currentSlide ; if ( nextSlides === undefined || nextPosition === undefined ) { return ; } if ( typeof beforeChange === "function" ) { beforeChange ( nextSlides , this . getState ( ) ) ; } this . isAnimationAllowed = true ; this . setState ( { transform : nextPosition , currentSlide : nextSlides } , ( ) => { if ( typeof afterChange === "function" ) { setTimeout ( ( ) => { afterChange ( previousSlide , this . getState ( ) ) ; } , this . props . transitionDuration || defaultTransitionDuration ) ; } } ) ; } public previous ( slidesHavePassed = 0 ) { const { afterChange , beforeChange } = this . props ; if ( notEnoughChildren ( this . state ) ) { return ; } const { nextSlides , nextPosition } = populatePreviousSlides ( this . state , this . props , slidesHavePassed ) ; if ( nextSlides === undefined || nextPosition === undefined ) { return ; } const previousSlide = this . state . currentSlide ; if ( typeof beforeChange === "function" ) { beforeChange ( nextSlides , this . getState ( ) ) ; } this . isAnimationAllowed = true ; this . setState ( { transform : nextPosition , currentSlide : nextSlides } , ( ) => { if ( typeof afterChange === "function" ) { setTimeout ( ( ) => { afterChange ( previousSlide , this . getState ( ) ) ; } , this . props . transitionDuration || defaultTransitionDuration ) ; } } ) ; } public componentWillUnmount ( ) { window . removeEventListener ( "resize" , this . onResize as React . EventHandler < any > ) ; if ( this . props . keyBoardControl ) { window . removeEventListener ( "keyup" , this . onKeyUp as React . EventHandler < any > ) ; } if ( this . props . autoPlay && this . autoPlay ) { clearInterval ( this . autoPlay ) ; this . autoPlay = undefined ; } if ( this . itemsToShowTimeout ) { clearTimeout ( this . itemsToShowTimeout ) ; } } public resetMoveStatus ( ) { this . onMove = false ; this . initialX = 0 ; this . lastX = 0 ; this . direction = "" ; this . initialY = 0 ; } public handleDown ( e ) { if ( ( ! isMouseMoveEvent ( e ) && ! this . props . swipeable ) || ( isMouseMoveEvent ( e ) && ! this . props . draggable ) || this . isInThrottle ) { return ; } const { clientX , clientY } = isMouseMoveEvent ( e ) ? e : e . touches [ 0 ] ; this . onMove = true ; this . initialX = clientX ; this . initialY = clientY ; this . lastX = clientX ; this . isAnimationAllowed = false ; } public handleMove ( e ) { if ( ( ! isMouseMoveEvent ( e ) && ! this . props . swipeable ) || ( isMouseMoveEvent ( e ) && ! this . props . draggable ) || notEnoughChildren ( this . state ) ) { return ; } const { clientX , clientY } = isMouseMoveEvent ( e ) ? e : e . touches [ 0 ] ; const diffX = this . initialX - clientX ; const diffY = this . initialY - clientY ; if ( this . onMove ) { if ( ! ( Math . abs ( diffX ) > Math . abs ( diffY ) ) ) { return ; } const { direction , nextPosition , canContinue } = populateSlidesOnMouseTouchMove ( this . state , this . props , this . initialX , this . lastX , clientX , this . transformPlaceHolder ) ; if ( direction ) { this . direction = direction ; if ( canContinue && nextPosition !== undefined ) { this . setTransformDirectly ( nextPosition ) ; } } this . lastX = clientX ; } } public handleOut ( e ) { if ( this . props . autoPlay && ! this . autoPlay ) { this . autoPlay = setInterval ( this . next , this . props . autoPlaySpeed ) ; } const shouldDisableOnMobile = e . type === "touchend" && ! this . props . swipeable ; const shouldDisableOnDesktop = ( e . type === "mouseleave" || e . type === "mouseup" ) && ! this . props . draggable ; if ( shouldDisableOnMobile || shouldDisableOnDesktop ) { return ; } if ( this . onMove ) { this . setAnimationDirectly ( true ) ; if ( this . direction === "right" ) { const canGoNext = this . initialX - this . lastX >= this . props . minimumTouchDrag ! ; if ( canGoNext ) { const slidesHavePassed = Math . round ( ( this . initialX - this . lastX ) / this . state . itemWidth ) ; this . next ( slidesHavePassed ) ; } else { this . correctItemsPosition ( this . state . itemWidth , true , true ) ; } } if ( this . direction === "left" ) { const canGoNext = this . lastX - this . initialX > this . props . minimumTouchDrag ! ; if ( canGoNext ) { const slidesHavePassed = Math . round ( ( this . lastX - this . initialX ) / this . state . itemWidth ) ; this . previous ( slidesHavePassed ) ; } else { this . correctItemsPosition ( this . state . itemWidth , true , true ) ; } } this . resetMoveStatus ( ) ; } } private isInViewport ( el ) { const { top = 0 , left = 0 , bottom = 0 , right = 0 } = el . getBoundingClientRect ( ) ; return ( top >= 0 && left >= 0 && bottom <= ( window . innerHeight || document . documentElement . clientHeight ) && right <= ( window . innerWidth || document . documentElement . clientWidth ) ) ; } private isChildOfCarousel ( el ) { if ( el instanceof Element && this . listRef && this . listRef . current ) { return this . listRef . current . contains ( el ) ; } return false ; } public onKeyUp ( e ) { const { target , keyCode } = e ; switch ( keyCode ) { case 37 : if ( this . isChildOfCarousel ( target ) ) return this . previous ( ) ; break ; case 39 : if ( this . isChildOfCarousel ( target ) ) return this . next ( ) ; break ; case 9 : if ( this . isChildOfCarousel ( target ) && target instanceof HTMLInputElement && ! this . isInViewport ( target ) ) { return this . next ( ) ; } break ; } } public handleEnter ( e ) { if ( isMouseMoveEvent ( e ) && this . autoPlay && this . props . autoPlay && this . props . pauseOnHover ) { clearInterval ( this . autoPlay ) ; this . autoPlay = undefined ; } } public goToSlide ( slide , skipCallbacks ? ) { if ( this . isInThrottle ) { return ; } const { itemWidth } = this . state ; const { afterChange , beforeChange } = this . props ; const previousSlide = this . state . currentSlide ; if ( typeof beforeChange === "function" && ( ! skipCallbacks || ( typeof skipCallbacks === "object" && ! skipCallbacks . skipBeforeChange ) ) ) { beforeChange ( slide , this . getState ( ) ) ; } this . isAnimationAllowed = true ; this . setState ( { currentSlide : slide , transform : - ( itemWidth * slide ) } , ( ) => { if ( this . props . infinite ) { this . correctClonesPosition ( { domLoaded : true } ) ; } if ( typeof afterChange === "function" && ( ! skipCallbacks || ( typeof skipCallbacks === "object" && ! skipCallbacks . skipAfterChange ) ) ) { setTimeout ( ( ) => { afterChange ( previousSlide , this . getState ( ) ) ; } , this . props . transitionDuration || defaultTransitionDuration ) ; } } ) ; } public getState ( ) { return this . state ; } public renderLeftArrow ( disbaled ) { const { customLeftArrow } = this . props ; return ( < LeftArrow customLeftArrow = { customLeftArrow } getState = { ( ) => this . getState ( ) } previous = { this . previous } disabled = { disbaled } / > ) ; } public renderRightArrow ( disbaled ) { const { customRightArrow } = this . props ; return ( < RightArrow customRightArrow = { customRightArrow } getState = { ( ) => this . getState ( ) } next = { this . next } disabled = { disbaled } / > ) ; } public renderButtonGroups ( ) { const { customButtonGroup } = this . props ; if ( customButtonGroup ) { return React . cloneElement ( customButtonGroup , { previous : ( ) => this . previous ( ) , next : ( ) => this . next ( ) , goToSlide : ( slideIndex , skipCallbacks ? ) => this . goToSlide ( slideIndex , skipCallbacks ) , carouselState : this . getState ( ) } ) ; } return null ; } public renderDotsList ( ) { return ( < Dots state = { this . state } props = { this . props } goToSlide = { this . goToSlide } getState = { ( ) => this . getState ( ) } />); } public renderCarouselItems ( ) { let clones = [ ] ; if ( this . props . infinite ) { const childrenArr = React . Children . toArray ( this . props . children ) ; clones = getClones ( this . state . slidesToShow , childrenArr ) ; } return ( < CarouselItems clones = { clones } goToSlide = { this . goToSlide } state = { this . state } notEnoughChildren = { notEnoughChildren ( this . state ) } props = { this . props } / > ) ; } public render ( ) { const { deviceType , arrows , renderArrowsWhenDisabled , removeArrowOnDeviceType , infinite , containerClass , sliderClass , customTransition , additionalTransfrom , renderDotsOutside , renderButtonGroupOutside , className } = this . props ; if ( process . env . NODE_ENV !== "production" ) { throwError ( this . state , this . props ) ; } const { shouldRenderOnSSR , shouldRenderAtAll } = getInitialState ( this . state , this . props ) ; const isLeftEndReach = isInLeftEnd ( this . state ) ; const isRightEndReach = isInRightEnd ( this . state ) ; const shouldShowArrows = arrows && ! ( removeArrowOnDeviceType && ( ( deviceType && removeArrowOnDeviceType . indexOf ( deviceType ) > - 1 ) || ( this . state . deviceType && removeArrowOnDeviceType . indexOf ( this . state . deviceType ) > - 1 ) ) ) && ! notEnoughChildren ( this . state ) && shouldRenderAtAll ; const disableLeftArrow = ! infinite && isLeftEndReach ; const disableRightArrow = ! infinite && isRightEndReach ; const currentTransform = getTransform ( this . state , this . props ) ; return ( < > < div className = { `react-multi-carousel-list ${ containerClass } ${ className }` } ref = { this . containerRef } > < ul ref = { this . listRef } className = { `react-multi-carousel-track ${ sliderClass }` } style = { { transition : this . isAnimationAllowed ? customTransition || defaultTransition : "none" , overflow : shouldRenderOnSSR ? "hidden" : "unset" , transform : `translate3d(${ currentTransform + additionalTransfrom ! }px,0,0)` } } onMouseMove = { this . handleMove } onMouseDown = { this . handleDown } onMouseUp = { this . handleOut } onMouseEnter = { this . handleEnter } onMouseLeave = { this . handleOut } onTouchStart = { this . handleDown } onTouchMove = { this . handleMove } onTouchEnd = { this . handleOut } > { this . renderCarouselItems ( ) } < /ul> { shouldShowArrows && ( ! disableLeftArrow || renderArrowsWhenDisabled ) && this . renderLeftArrow ( disableLeftArrow ) } { shouldShowArrows && ( ! disableRightArrow || renderArrowsWhenDisabled ) && this . renderRightArrow ( disableRightArrow ) } { shouldRenderAtAll && ! renderButtonGroupOutside && this . renderButtonGroups ( ) } { shouldRenderAtAll && ! renderDotsOutside && this . renderDotsList ( ) } < /div> { shouldRenderAtAll && renderDotsOutside && this . renderDotsList ( ) } { shouldRenderAtAll && renderButtonGroupOutside && this . renderButtonGroups ( ) } < />); } } export default Carousel ;