import { useState, useRef, useCallback, useEffect } from 'react'; import { fromEvent, Subscription } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; import MobileDetect from 'mobile-detect'; import { getAbsoluteCoordinate } from '../helpers/position'; const defaultTime = 35; type CursorPosition = { x: number | null; y: number | null; clientX: number | null; clientY: number | null; screenX: number | null; screenY: number | null; }; type UseCursorPositionType = ( throttleTime?: number, ) => [ CursorPosition, (element: HTMLElement | SVGSVGElement | SVGCircleElement | null) => void, ]; const initialState = { x: null, y: null, clientX: null, clientY: null, screenX: null, screenY: null, }; const useCursorPosition: UseCursorPositionType = (time = defaultTime) => { const [state, setState] = useState(initialState); const [element, setElement] = useState< HTMLElement | SVGSVGElement | SVGCircleElement | null >(null); const entered = useRef(false); const isDown = useRef(false); const onMouseMoveEvent = useCallback( (e: MouseEvent | Event): void => { if (element) { const { clientX, clientY, screenX, screenY } = e as MouseEvent; const coordinate = getAbsoluteCoordinate(element, e as MouseEvent); setState({ x: coordinate.x, y: coordinate.y, clientX, clientY, screenX, screenY, }); } }, [element], ); const onTouchMoveEvent = useCallback( (e: TouchEvent | Event): void => { if (element) { const { clientX, clientY, screenX, screenY, } = (e as TouchEvent).touches[0]; const coordinate = getAbsoluteCoordinate(element, e as TouchEvent); setState({ x: coordinate.x, y: coordinate.y, clientX, clientY, screenX, screenY, }); } }, [element], ); useEffect(() => { const md = new MobileDetect(window.navigator.userAgent); let mouseSubscription: Subscription | null = null; let touchSubscription: Subscription | null = null; const onEnter = (): void => { entered.current = true; }; const onLeave = (): void => { entered.current = false; setState(initialState); }; const onDown = (e: Event): void => { entered.current = true; isDown.current = true; onMouseMoveEvent(e); }; const onTouch = (e: Event): void => { entered.current = true; isDown.current = true; onTouchMoveEvent(e); }; const onUp = (): void => { entered.current = false; isDown.current = false; setState(initialState); if (mouseSubscription) mouseSubscription.unsubscribe(); if (touchSubscription) touchSubscription.unsubscribe(); }; if (element) { const addEvent = element.addEventListener.bind(element); if (md.mobile() || md.tablet()) { touchSubscription = fromEvent(element, 'touchmove') .pipe(throttleTime(time)) .subscribe(onTouchMoveEvent); addEvent('touchstart', onTouch); addEvent('touchend', onUp); } else { mouseSubscription = fromEvent(element, 'mousemove') .pipe(throttleTime(time)) .subscribe(onMouseMoveEvent); addEvent('mouseenter', onEnter); addEvent('mouseleave', onLeave); addEvent('mousedown', onDown); addEvent('mouseup', onUp); } } return (): void => { if (element) { const removeEvent = element.removeEventListener.bind(element); if (md.mobile() || md.tablet()) { removeEvent('touchstart', onTouch); removeEvent('touchend', onUp); } else { removeEvent('mouseenter', onEnter); removeEvent('mouseleave', onLeave); removeEvent('mousedown', onDown); removeEvent('mouseup', onUp); } } }; }, [element]); return [state, setElement]; }; export default useCursorPosition;