a tool for shared writing and social publishing
1import { useCallback, useEffect, useRef, useState } from "react"; 2 3export const useDrag = (args: { 4 onDrag?: (a: {}) => void; 5 onDragEnd: (d: { x: number; y: number }) => void; 6 delay?: boolean; 7}) => { 8 let [dragStart, setDragStart] = useState<{ x: number; y: number } | null>( 9 null, 10 ); 11 let touchStart = useRef<null | { x: number; y: number }>(null); 12 let timeout = useRef<null | number>(null); 13 let isLongPress = useRef(false); 14 15 let onTouchStart = useCallback( 16 (e: React.TouchEvent) => { 17 if (e.defaultPrevented) return; 18 if (args.delay) { 19 touchStart.current = { 20 x: e.touches[0].clientX, 21 y: e.touches[0].clientY, 22 }; 23 isLongPress.current = true; 24 timeout.current = window.setTimeout(() => { 25 setDragStart({ x: e.touches[0].clientX, y: e.touches[0].clientY }); 26 setDragDelta({ x: 0, y: 0 }); 27 timeout.current = null; 28 }, 400); 29 } else { 30 setDragStart({ x: e.touches[0].clientX, y: e.touches[0].clientY }); 31 setDragDelta({ x: 0, y: 0 }); 32 } 33 }, 34 [args.delay], 35 ); 36 37 let onMouseDown = useCallback( 38 (e: React.MouseEvent) => { 39 if (e.defaultPrevented) return; 40 if (args.delay) { 41 isLongPress.current = true; 42 timeout.current = window.setTimeout(() => { 43 timeout.current = null; 44 }, 400); 45 } else { 46 setDragStart({ x: e.clientX, y: e.clientY }); 47 setDragDelta({ x: 0, y: 0 }); 48 } 49 }, 50 [args.delay], 51 ); 52 53 let [dragDelta, setDragDelta] = useState<{ 54 x: number; 55 y: number; 56 } | null>(null); 57 let currentDragDelta = useRef({ x: 0, y: 0 }); 58 let end = useCallback( 59 (e: { preventDefault: () => void }) => { 60 isLongPress.current = false; 61 if (timeout.current) { 62 window.clearTimeout(timeout.current); 63 timeout.current = null; 64 return; 65 } 66 if (args.delay) e.preventDefault(); 67 args.onDragEnd({ ...currentDragDelta.current }); 68 currentDragDelta.current = { x: 0, y: 0 }; 69 setDragStart(null); 70 setDragDelta(null); 71 }, 72 [args.delay, args.onDragEnd], 73 ); 74 useEffect(() => { 75 let disconnect = new AbortController(); 76 window.addEventListener( 77 "touchmove", 78 (e) => { 79 if (args.delay && touchStart.current) { 80 const deltaX = e.touches[0].clientX - touchStart.current.x; 81 const deltaY = e.touches[0].clientY - touchStart.current.y; 82 if (Math.abs(deltaX) > 8 || Math.abs(deltaY) > 8) { 83 if (timeout.current) { 84 window.clearTimeout(timeout.current); 85 timeout.current = null; 86 } 87 touchStart.current = null; 88 } 89 } 90 if (dragDelta) e.preventDefault(); 91 }, 92 { signal: disconnect.signal, passive: false }, 93 ); 94 return () => { 95 disconnect.abort(); 96 }; 97 }, [args.delay, dragDelta]); 98 99 useEffect(() => { 100 if (!dragStart) return; 101 let disconnect = new AbortController(); 102 window.addEventListener( 103 "pointermove", 104 (e: PointerEvent) => { 105 e.preventDefault(); 106 currentDragDelta.current.x = e.clientX - dragStart.x; 107 currentDragDelta.current.y = e.clientY - dragStart.y; 108 setDragDelta({ ...currentDragDelta.current }); 109 }, 110 { signal: disconnect.signal }, 111 ); 112 113 window.addEventListener( 114 "contextmenu", 115 (e) => { 116 if (isLongPress.current) e.preventDefault(); 117 }, 118 { signal: disconnect.signal }, 119 ); 120 121 window.addEventListener("touchend", end, { signal: disconnect.signal }); 122 window.addEventListener("pointerup", end, { signal: disconnect.signal }); 123 return () => { 124 disconnect.abort(); 125 }; 126 }, [dragStart, args, end]); 127 let handlers = { onMouseDown, onTouchEnd: end, onTouchStart }; 128 return { dragDelta, handlers }; 129};