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};