import type { $FixMe } from '@theatre/shared/utils/types' ; import { useLayoutEffect , useRef } from 'react' ; import useRefAndState from '@theatre/studio/utils/useRefAndState' ; import { useCursorLock } from './PointerEventsHandler' ; export type UseDragOpts = { disabled ? : boolean ; dontBlockMouseDown ? : boolean ; lockCursorTo ? : string ; onDragStart ? : ( event : MouseEvent ) => void | false ; onDragEnd ? : ( dragHappened : boolean ) => void ; onDrag : ( dx : number , dy : number , event : MouseEvent ) => void ; } ; export default function useDrag ( target , opts ) { const optsRef = useRef < typeof opts > ( opts ) ; optsRef . current = opts ; const [ modeRef , mode ] = useRefAndState < 'dragStartCalled' | 'dragging' | 'notDragging' > ( 'notDragging' ) ; useCursorLock ( mode === 'dragging' && typeof opts . lockCursorTo === 'string' , 'dragging' , opts . lockCursorTo ! ) ; const stateRef = useRef < { dragHappened : boolean ; startPos : { x : number ; y : number ; } ; } > ( { dragHappened : false , startPos : { x : 0 , y : 0 } } ) ; useLayoutEffect ( ( ) => { if ( ! target ) return ; const getDistances = ( event ) : [ number , number ] => { const { startPos } = stateRef . current ; return [ event . screenX - startPos . x , event . screenY - startPos . y ] ; } ; const dragHandler = ( event ) => { if ( ! stateRef . current . dragHappened ) stateRef . current . dragHappened = true ; modeRef . current = 'dragging' ; const deltas = getDistances ( event ) ; optsRef . current . onDrag ( deltas [ 0 ] , deltas [ 1 ] , event ) ; } ; const dragEndHandler = ( ) => { removeDragListeners ( ) ; modeRef . current = 'notDragging' ; optsRef . current . onDragEnd && optsRef . current . onDragEnd ( stateRef . current . dragHappened ) ; } ; const addDragListeners = ( ) => { document . addEventListener ( 'mousemove' , dragHandler ) ; document . addEventListener ( 'mouseup' , dragEndHandler ) ; } ; const removeDragListeners = ( ) => { document . removeEventListener ( 'mousemove' , dragHandler ) ; document . removeEventListener ( 'mouseup' , dragEndHandler ) ; } ; const preventUnwantedClick = ( event ) => { if ( optsRef . current . disabled ) return ; if ( stateRef . current . dragHappened ) { if ( ! optsRef . current . dontBlockMouseDown && modeRef . current !== 'notDragging' ) { event . stopPropagation ( ) ; event . preventDefault ( ) ; } stateRef . current . dragHappened = false ; } } ; const dragStartHandler = ( event ) => { const opts = optsRef . current ; if ( opts . disabled === true ) return ; if ( event . button !== 0 ) return ; const resultOfStart = opts . onDragStart && opts . onDragStart ( event ) ; if ( resultOfStart === false ) return ; if ( ! opts . dontBlockMouseDown ) { event . stopPropagation ( ) ; event . preventDefault ( ) ; } modeRef . current = 'dragStartCalled' ; const { screenX , screenY } = event ; stateRef . current . startPos = { x : screenX , y : screenY } ; stateRef . current . dragHappened = false ; addDragListeners ( ) ; } ; const onMouseDown = ( e ) => { dragStartHandler ( e ) ; } ; target . addEventListener ( 'mousedown' , onMouseDown as $FixMe ) ; target . addEventListener ( 'click' , preventUnwantedClick as $FixMe ) ; return ( ) => { removeDragListeners ( ) ; target . removeEventListener ( 'mousedown' , onMouseDown as $FixMe ) ; target . removeEventListener ( 'click' , preventUnwantedClick as $FixMe ) ; if ( modeRef . current !== 'notDragging' ) { optsRef . current . onDragEnd && optsRef . current . onDragEnd ( modeRef . current === 'dragging' ) ; } modeRef . current = 'notDragging' ; } ; } , [ target ] ) ; return [ mode === 'dragging' ] ; }