import React , { useState , useEffect , useRef , ReactNode } from 'react' ; import { applyDragForce , applyBoundForce } from './force' ; import getBoundaries from './getBoundaries' ; import { roundNum , isMouseEvent } from './utils' ; type draggerRefParams = { setPosition : ( position : number , instant : boolean ) => void ; outerWidth : number ; innerWidth : number ; } ; export type OnFrameType = { x : number ; outerWidth : number ; innerWidth : number ; progress : number ; } ; type TProps = { friction ? : number ; disabled ? : boolean ; setCursorStyles ? : boolean ; draggerRef ? : ( args : draggerRefParams ) => void ; onFrame ? : ( args : OnFrameType ) => void ; onStaticClick ? : ( e : EventTarget ) => void ; onDown ? : ( ) => void ; onUp ? : ( ) => void ; style ? : React . CSSProperties ; innerStyle ? : React . CSSProperties ; className ? : string ; children : ReactNode ; ResizeObserverPolyfill ? : Function ; } ; export default function Dragger ( { friction = 0.95 , disabled = false , setCursorStyles = true , onFrame , onUp , onDown , onStaticClick , draggerRef , style , innerStyle , className , children , ResizeObserverPolyfill , } ) { const outerEl = useRef < HTMLDivElement > ( null ) ; const innerEl = useRef < HTMLDivElement > ( null ) ; const outerWidth = useRef ( 0 ) ; const innerWidth = useRef ( 0 ) ; const leftBound = useRef ( 0 ) ; const rightBound = useRef ( 0 ) ; const isDragging = useRef ( false ) ; const [ isDraggingStyle , setIsDraggingStyle ] = useState ( false ) ; const restPositionX = useRef ( 0 ) ; const velocityX = useRef ( 0 ) ; const downX = useRef ( 0 ) ; const dragStartPosition = useRef ( 0 ) ; const nativePosition = useRef ( 0 ) ; const dragPosition = useRef ( nativePosition . current ) ; const rafId = useRef < number | null > ( null ) ; const docStyle = typeof window !== 'undefined' ? document . documentElement . style : { cursor : '' , userSelect : '' } ; const mouseCursorStyle = setCursorStyles && isDraggingStyle ? 'grabbing' : setCursorStyles ? 'grab' : undefined ; useEffect ( ( ) => { outerWidth . current = outerEl . current ! . scrollWidth ; innerWidth . current = innerEl . current ! . scrollWidth ; const { left , right } = getBoundaries ( { outerWidth : outerWidth . current , innerWidth : innerWidth . current , elClientLeft : ( outerEl . current && outerEl . current . clientLeft ) || 0 } ) ; leftBound . current = left ; rightBound . current = right ; if ( ! ( window as any ) . ResizeObserver && ! ResizeObserverPolyfill ) { throw new Error ( 'No ResizeObserver is available. Please check the docs for instructions on how to add a polyfill.' ) ; } const Ro = ( window as any ) . ResizeObserver || ResizeObserverPolyfill ; const observer = new Ro ( ( entries ) => { const id = entries [ 0 ] . target . dataset . id ; if ( id === 'dragger-inner' ) innerWidth . current = entries [ 0 ] . contentRect . width ; if ( id === 'dragger-outer' ) outerWidth . current = entries [ 0 ] . contentRect . width ; const { left , right } = getBoundaries ( { outerWidth : outerWidth . current , innerWidth : innerWidth . current , elClientLeft : ( outerEl . current && outerEl . current . clientLeft ) || 0 } ) ; leftBound . current = left ; rightBound . current = right ; if ( onFrame ) { onFrame ( { x : roundNum ( nativePosition . current ) , outerWidth : outerWidth . current , innerWidth : innerWidth . current , progress : roundNum ( nativePosition . current / ( outerWidth . current - innerWidth . current ) ) } ) ; } if ( draggerRef ) { draggerRef ( { setPosition , outerWidth : outerWidth . current , innerWidth : innerWidth . current } ) ; } } ) ; observer . observe ( outerEl . current ) ; observer . observe ( innerEl . current ) ; return ( ) => { observer . disconnect ( ) ; } ; } , [ ] ) ; function setPosition ( position , instant ) { if ( disabled ) return ; const finalPosition = Math . min ( Math . max ( leftBound . current , position ) , rightBound . current ) ; if ( instant ) { window . cancelAnimationFrame ( rafId . current ! ) ; if ( innerEl . current ) { nativePosition . current = finalPosition ; restPositionX . current = finalPosition ; innerEl . current . style . transform = `translate3d(${ roundNum ( finalPosition ) }px,0,0)` ; if ( onFrame ) { onFrame ( { x : roundNum ( nativePosition . current ) , outerWidth : outerWidth . current , innerWidth : innerWidth . current , progress : roundNum ( nativePosition . current / ( outerWidth . current - innerWidth . current ) ) } ) ; } } } else { window . cancelAnimationFrame ( rafId . current ! ) ; rafId . current = window . requestAnimationFrame ( ( ) => updateLoop ( finalPosition ) ) ; } } function updateLoop ( optionalFinalPosition ) { if ( ! outerEl . current ) { window . cancelAnimationFrame ( rafId . current ! ) ; return ; } velocityX . current *= friction ; if ( optionalFinalPosition !== null ) { const distance = optionalFinalPosition - nativePosition . current ; const force = distance * ( 1 - friction ) - velocityX . current ; velocityX . current = velocityX . current + force ; } else { if ( ! isDragging . current && nativePosition . current < leftBound . current ) { velocityX . current = applyBoundForce ( { bound : leftBound . current , edge : 'left' , nativePosition : nativePosition . current , friction : friction , velocityX : velocityX . current } ) ; } if ( ! isDragging . current && nativePosition . current > rightBound . current ) { velocityX . current = applyBoundForce ( { bound : rightBound . current , edge : 'right' , nativePosition : nativePosition . current , friction : friction , velocityX : velocityX . current } ) ; } } velocityX . current = applyDragForce ( { isDragging : isDragging . current , dragPosition : dragPosition . current , nativePosition : nativePosition . current , velocityX : velocityX . current } ) ; nativePosition . current += velocityX . current ; const isInfinitesimal = roundNum ( Math . abs ( velocityX . current ) ) < 0.001 ; if ( ! isDragging . current && isInfinitesimal ) { window . cancelAnimationFrame ( rafId . current ! ) ; restPositionX . current = roundNum ( nativePosition . current ) ; } else { if ( innerEl . current ) { innerEl . current . style . transform = `translate3d(${ roundNum ( nativePosition . current ) }px,0,0)` ; rafId . current = window . requestAnimationFrame ( ( ) => updateLoop ( null ) ) ; } } if ( onFrame ) { onFrame ( { x : roundNum ( nativePosition . current ) , outerWidth : outerWidth . current , innerWidth : innerWidth . current , progress : roundNum ( nativePosition . current / ( outerWidth . current - innerWidth . current ) ) } ) ; } } function onMouseMove ( e ) { const x = e . pageX ; onMove ( x ) ; } function onTouchMove ( e ) { const x = e . touches [ 0 ] . pageX ; onMove ( x ) ; } function onMove ( x ) { const moveVector = x - downX . current ; let dragX = dragStartPosition . current + moveVector ; const originBound = Math . max ( rightBound . current , dragStartPosition . current ) ; if ( dragX > originBound ) { dragX = ( dragX + originBound ) * 0.5 ; } const endBound = Math . min ( leftBound . current , dragStartPosition . current ) ; if ( dragX < endBound ) { dragX = ( dragX + endBound ) * 0.5 ; } dragPosition . current = dragX ; } function onRelease ( e ) { isDragging . current = false ; setIsDraggingStyle ( false ) ; if ( onUp ) onUp ( ) ; const moveVector = Math . abs ( downX . current - e . pageX ) ; if ( moveVector < 20 && onStaticClick ) { onStaticClick ( e . target ) ; } if ( setCursorStyles ) { docStyle . cursor = '' ; docStyle . userSelect = '' ; } if ( isMouseEvent ( e ) ) { window . removeEventListener ( 'mousemove' , onMouseMove ) ; window . removeEventListener ( 'mouseup' , onRelease ) ; } else { window . removeEventListener ( 'touchmove' , onTouchMove ) ; window . removeEventListener ( 'touchend' , onRelease ) ; } } function onMouseStart ( e ) { if ( disabled ) return ; const mouseButton = e . button ; if ( mouseButton && mouseButton !== 0 && mouseButton !== 1 ) return ; isDragging . current = true ; setIsDraggingStyle ( true ) ; if ( onDown ) onDown ( ) ; if ( setCursorStyles ) { docStyle . cursor = 'grabbing' ; docStyle . userSelect = 'none' ; } window . cancelAnimationFrame ( rafId . current ! ) ; rafId . current = window . requestAnimationFrame ( ( ) => updateLoop ( null ) ) ; downX . current = e . pageX ; dragStartPosition . current = nativePosition . current ; onMouseMove ( e ) ; window . addEventListener ( 'mousemove' , onMouseMove ) ; window . addEventListener ( 'mouseup' , onRelease ) ; } function onTouchStart ( e ) { if ( disabled ) return ; isDragging . current = true ; if ( onDown ) onDown ( ) ; window . cancelAnimationFrame ( rafId . current ! ) ; rafId . current = window . requestAnimationFrame ( ( ) => updateLoop ( null ) ) ; downX . current = e . touches [ 0 ] . pageX ; dragStartPosition . current = nativePosition . current ; onTouchMove ( e ) ; window . addEventListener ( 'touchmove' , onTouchMove ) ; window . addEventListener ( 'touchend' , onRelease ) ; } function onBlur ( ) { outerEl . current ! . scrollTo ( 0 , 0 ) ; } return ( < div data - id = 'dragger-outer' ref = { outerEl } className = { [ disabled ? ' is-disabled' : null , className , ] . join ( ' ' ) } onTouchStart = { onTouchStart } onMouseDown = { onMouseStart } onBlur = { onBlur } style = { { cursor : mouseCursorStyle , ... style } } > < div data - id = 'dragger-inner' ref = { innerEl } className = 'dragger-inner' style = { { transform : `translateX(${ restPositionX . current }px)` , whiteSpace : 'nowrap' , userSelect : 'none' , WebkitOverflowScrolling : 'touch' , willChange : 'transform' , display : 'inline-block' , ... innerStyle } } > { children } < /div> < /div>); }