forked from athasdev/athas
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathuse-drag-scroll.ts
More file actions
113 lines (91 loc) · 3.68 KB
/
use-drag-scroll.ts
File metadata and controls
113 lines (91 loc) · 3.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
import { type RefObject, useCallback, useEffect, useRef } from "react";
const EDGE_MARGIN = 40;
const MAX_SCROLL_SPEED = 25;
let dragScrollActive = false;
export function isDragScrolling() {
return dragScrollActive;
}
export function useDragScroll(textareaRef: RefObject<HTMLTextAreaElement | null>) {
const isDraggingRef = useRef(false);
const rafRef = useRef<number | null>(null);
const mousePositionRef = useRef({ x: 0, y: 0 });
const scrollTick = useCallback(() => {
const textarea = textareaRef.current;
if (!textarea || !isDraggingRef.current) {
rafRef.current = null;
return;
}
const rect = textarea.getBoundingClientRect();
const { x, y } = mousePositionRef.current;
let scrollX = 0;
let scrollY = 0;
const distFromBottom = rect.bottom - y;
const distFromTop = y - rect.top;
const distFromRight = rect.right - x;
const distFromLeft = x - rect.left;
if (distFromBottom < EDGE_MARGIN && distFromBottom >= 0) {
scrollY = Math.ceil(MAX_SCROLL_SPEED * (1 - distFromBottom / EDGE_MARGIN));
} else if (distFromTop < EDGE_MARGIN && distFromTop >= 0) {
scrollY = -Math.ceil(MAX_SCROLL_SPEED * (1 - distFromTop / EDGE_MARGIN));
}
if (distFromRight < EDGE_MARGIN && distFromRight >= 0) {
scrollX = Math.ceil(MAX_SCROLL_SPEED * (1 - distFromRight / EDGE_MARGIN));
} else if (distFromLeft < EDGE_MARGIN && distFromLeft >= 0) {
scrollX = -Math.ceil(MAX_SCROLL_SPEED * (1 - distFromLeft / EDGE_MARGIN));
}
// Also scroll when the mouse is outside the textarea bounds
if (y > rect.bottom) {
scrollY = Math.ceil(MAX_SCROLL_SPEED * Math.min((y - rect.bottom) / EDGE_MARGIN + 1, 3));
} else if (y < rect.top) {
scrollY = -Math.ceil(MAX_SCROLL_SPEED * Math.min((rect.top - y) / EDGE_MARGIN + 1, 3));
}
if (x > rect.right) {
scrollX = Math.ceil(MAX_SCROLL_SPEED * Math.min((x - rect.right) / EDGE_MARGIN + 1, 3));
} else if (x < rect.left) {
scrollX = -Math.ceil(MAX_SCROLL_SPEED * Math.min((rect.left - x) / EDGE_MARGIN + 1, 3));
}
if (scrollX !== 0 || scrollY !== 0) {
textarea.scrollTop += scrollY;
textarea.scrollLeft += scrollX;
// Dispatch scroll event so overlay layers stay in sync
textarea.dispatchEvent(new Event("scroll", { bubbles: true }));
}
rafRef.current = requestAnimationFrame(scrollTick);
}, [textareaRef]);
useEffect(() => {
const textarea = textareaRef.current;
if (!textarea) return;
const handleMouseDown = (e: MouseEvent) => {
if (e.button !== 0) return;
isDraggingRef.current = true;
dragScrollActive = true;
mousePositionRef.current = { x: e.clientX, y: e.clientY };
};
const handleMouseMove = (e: MouseEvent) => {
if (!isDraggingRef.current) return;
mousePositionRef.current = { x: e.clientX, y: e.clientY };
if (rafRef.current === null) {
rafRef.current = requestAnimationFrame(scrollTick);
}
};
const handleMouseUp = () => {
isDraggingRef.current = false;
dragScrollActive = false;
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
};
textarea.addEventListener("mousedown", handleMouseDown);
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
return () => {
textarea.removeEventListener("mousedown", handleMouseDown);
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
}
};
}, [textareaRef, scrollTick]);
}