import * as React from "react" ; import CharacterString from "../CharacterString" ; import * as Actions from "../../actionCreators" ; import * as Selectors from "../../selectors" ; import { useTypedSelector , useActionCreator } from "../../hooks" ; import * as Utils from "../../utils" ; const SEPARATOR = "  ***  " ; const CHAR_WIDTH = 5 ; const MARQUEE_MAX_LENGTH = 31 ; export const mod = ( n , m ) : number => ( ( n % m ) + m ) % m ; const isLong = ( text ) : boolean => text . length >= MARQUEE_MAX_LENGTH ; export const stepOffset = ( text , step , pixels ) : number => { if ( ! isLong ( text ) ) { return 0 ; } const stepOffsetWidth = step * CHAR_WIDTH ; const offset = stepOffsetWidth + pixels ; const stringLength = ( text . length + SEPARATOR . length ) * CHAR_WIDTH ; return mod ( offset , stringLength ) ; } ; export const pixelUnits = ( pixels ) : string => `${ pixels }px` ; export const loopText = ( text ) : string => isLong ( text ) ? `${ text }${ SEPARATOR }${ text }` : text . padEnd ( MARQUEE_MAX_LENGTH , " " ) ; interface UseStepperArgs { step : ( ) => void ; dragging : boolean ; } function useStepper ( { step , dragging } ) { const [ stepping , setStepping ] = React . useState ( true ) ; React . useEffect ( ( ) => { if ( stepping === false ) { return ; } const stepHandle = setInterval ( step , 220 ) ; return ( ) => clearInterval ( stepHandle ) ; } , [ step , stepping ] ) ; React . useEffect ( ( ) => { if ( dragging ) { setStepping ( false ) ; return ; } const steppingTimeout = window . setTimeout ( ( ) => { setStepping ( true ) ; } , 1000 ) ; return ( ) => { window . clearTimeout ( steppingTimeout ) ; } ; } , [ dragging ] ) ; } function useDragX ( ) { const [ mouseDownX , setMouseDownX ] = React . useState < number | null > ( null ) ; const [ dragOffset , setDragOffset ] = React . useState ( 0 ) ; React . useEffect ( ( ) => { if ( mouseDownX == null ) { return ; } const xStart = mouseDownX ; const handleMouseMove = ( ee ) => { const diff = Utils . getX ( ee ) - xStart ; setDragOffset ( - diff ) ; } ; let cleanedUp = false ; const handleMouseUp = ( ) => { if ( cleanedUp ) { return ; } document . removeEventListener ( "mousemove" , handleMouseMove ) ; document . removeEventListener ( "touchmove" , handleMouseMove ) ; document . removeEventListener ( "mouseup" , handleMouseUp ) ; document . removeEventListener ( "touchend" , handleMouseUp ) ; setMouseDownX ( null ) ; cleanedUp = true ; } ; document . addEventListener ( "mousemove" , handleMouseMove ) ; document . addEventListener ( "touchmove" , handleMouseMove ) ; document . addEventListener ( "touseup" , handleMouseUp ) ; document . addEventListener ( "touchend" , handleMouseUp ) ; return handleMouseUp ; } , [ mouseDownX ] ) ; const handleMouseDown = React . useCallback ( ( e ) : void => { setMouseDownX ( Utils . getX ( e ) ) ; } , [ ] ) ; return { handleMouseDown , dragOffset , dragging : mouseDownX != null } ; } const Marquee = React . memo ( ( ) => { const text = useTypedSelector ( Selectors . getMarqueeText ) ; const doubled = useTypedSelector ( Selectors . getDoubled ) ; const marqueeStep = useTypedSelector ( Selectors . getMarqueeStep ) ; const stepMarquee = useActionCreator ( Actions . stepMarquee ) ; const { handleMouseDown , dragOffset , dragging } = useDragX ( ) ; const offset = stepOffset ( text , marqueeStep , dragOffset ) ; const offsetPixels = pixelUnits ( - offset ) ; useStepper ( { step : stepMarquee , dragging } ) ; return ( < div id = "marquee" className = "text" onMouseDown = { handleMouseDown } onTouchStart = { handleMouseDown } title = "Song Title" > < div style = { { whiteSpace : "nowrap" , willChange : "transform" , transform : `translateX(${ offsetPixels })` , } } key = { doubled ? "doubled" : "not-doubled" } > < CharacterString > { loopText ( text ) } < /CharacterString> < /div> < /div>); } ) ; export default Marquee ;