import { isString } from 'lodash'; import { useEffect } from 'react'; import { useCallbackRef } from './use-callback-ref'; export enum MouseButton { left = 0, wheel = 1, right = 2, } export enum MouseButtons { none = 0, left = 1, right = 2, leftAndRight = 3, wheel = 4, leftAndWheel = 5, rightAndWheel = 6, leftAndRightAndWheel = 7, browserBack = 8, browserForward = 16, } type MouseState = { button: MouseButton; buttons: MouseButtons; pageDownX: number; pageDownY: number; pageUpX: number; pageUpY: number; downDistX: number; downDistY: number; downDist: number; downRect: [x: number, y: number, width: number, height: number]; timestampDown: number; }; export type MouseStateEvent = Omit & MouseState; type MouseListener = (state: MouseStateEvent) => void; const state: Omit = { pageDownX: 0, pageDownY: 0, pageUpX: 0, pageUpY: 0, downDist: 0, downDistX: 0, downDistY: 0, downRect: [0, 0, 0, 0], timestampDown: 0, }; const emit = (listeners: MouseListener[]) => (e: MouseEvent) => { if (e.type === 'mousedown') { state.pageDownX = e.pageX; state.pageDownY = e.pageY; state.timestampDown = e.timeStamp; } if (e.buttons === MouseButtons.left) { state.downDistX = e.pageX - state.pageDownX; state.downDistY = e.pageY - state.pageDownY; state.downDist = Math.sqrt( state.downDistX * state.downDistX + state.downDistY * state.downDistY, ); state.downRect[0] = Math.min(e.pageX, state.pageDownX); state.downRect[1] = Math.min(e.pageY, state.pageDownY); state.downRect[2] = Math.abs(state.downDistX); state.downRect[3] = Math.abs(state.downDistY); } Object.assign(e, state); for (const l of listeners) l(e as MouseStateEvent); }; const downListeners: MouseListener[] = []; const upListeners: MouseListener[] = []; const moveListeners: MouseListener[] = []; let initializedListeners = false; function initListeners() { if (initializedListeners) return; initializedListeners = true; document.addEventListener('mousedown', emit(downListeners), { passive: true }); document.addEventListener('mousemove', emit(moveListeners), { passive: true }); document.addEventListener('mouseup', emit(upListeners), { passive: true }); } function useMouse(cb: MouseListener, disabled: boolean, listeners: MouseListener[]) { const cbRef = useCallbackRef(cb); useEffect(() => { if (disabled) return; initListeners(); const handler = (e: MouseStateEvent) => cbRef.current(e); listeners.push(handler); return () => { const index = listeners.indexOf(handler); if (index !== -1) listeners.splice(index, 1); }; }, [disabled, cbRef, listeners]); } export function useMouseMove(cb: MouseListener, disabled = false) { return useMouse(cb, disabled, moveListeners); } export function useMouseDown(cb: MouseListener, disabled = false) { return useMouse(cb, disabled, downListeners); } export function useMouseUp(cb: MouseListener, disabled = false) { return useMouse(cb, disabled, upListeners); } export function useMouseDownOutside( ref: React.RefObject, cb: MouseListener, disabled?: boolean, ) { return useMouseDown((e) => { const el = ref.current; if ( el && document.contains(e.target as HTMLElement) && !isInside(e, el) && !isInsideRole(e, 'popover') ) cb(e); }, disabled); } export function isInside(e: MouseStateEvent, element: HTMLElement) { let parent: HTMLElement | null = e.target as HTMLElement; while (parent) { if (parent === element) return true; parent = parent.parentElement; } return false; } export function isInsideRole(e: MouseStateEvent, role: RegExp | string) { let parent: HTMLElement | null = e.target as HTMLElement; if (isString(role)) role = new RegExp('^' + role + '$', 'i'); while (parent) { const roleAttr = parent?.getAttribute('role'); if (roleAttr && role.test(roleAttr)) return true; parent = parent.parentElement; } return false; }