Skip to content

Commit d2eb67e

Browse files
authored
fix(editor): prevent cursor snap after scrollbar drag (#2263)
* fix(editor): prevent cursor snap after scrollbar drag * fix * fix
1 parent aaa8983 commit d2eb67e

2 files changed

Lines changed: 132 additions & 8 deletions

File tree

src/components/scrollbar/style.scss

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
animation: scrollbar-show 300ms ease 1;
88
transition: all 300ms ease;
99
opacity: 1;
10+
touch-action: none;
11+
user-select: none;
12+
-webkit-user-select: none;
1013

1114
&.hide {
1215
opacity: 0;
@@ -98,4 +101,4 @@
98101
bottom: 0;
99102
top: auto;
100103
}
101-
}
104+
}

src/lib/editorManager.js

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ async function EditorManager($header, $body) {
103103
let lastScrollTop = 0;
104104
let lastScrollLeft = 0;
105105
let suppressCursorRevealUntil = 0;
106+
let scrollbarScrollLockUntil = 0;
107+
let scrollbarScrollLockTop = null;
108+
let scrollbarScrollLockLeft = null;
106109

107110
// Debounce timers for CodeMirror change handling
108111
let checkTimeout = null;
@@ -185,7 +188,7 @@ async function EditorManager($header, $body) {
185188

186189
const pointerCursorVisibilityExtension = EditorView.updateListener.of(
187190
(update) => {
188-
if (!update.selectionSet) return;
191+
if (!update.transactions.length) return;
189192
const pointerTriggered = update.transactions.some(
190193
(tr) =>
191194
tr.isUserEvent("pointer") ||
@@ -194,14 +197,17 @@ async function EditorManager($header, $body) {
194197
tr.isUserEvent("touch") ||
195198
tr.isUserEvent("select.touch"),
196199
);
197-
if (!pointerTriggered) return;
200+
if (!pointerTriggered) {
201+
clearScrollbarScrollLock();
202+
return;
203+
}
204+
if (!update.selectionSet) return;
198205
requestAnimationFrame(() => {
199206
if (isCursorRevealSuppressed()) return;
200207
if (!isCursorVisible()) scrollCursorIntoView({ behavior: "instant" });
201208
});
202209
},
203210
);
204-
205211
const isShiftSelectionActive = (event) => {
206212
if (!appSettings.value.shiftClickSelection) return false;
207213
return !!event?.shiftKey || quickTools?.$footer?.dataset?.shift != null;
@@ -1155,6 +1161,7 @@ async function EditorManager($header, $body) {
11551161
if (!scroller) return false;
11561162

11571163
if (row === Number.POSITIVE_INFINITY) {
1164+
clearScrollbarScrollLock();
11581165
scroller.scrollTop = Math.max(
11591166
scroller.scrollHeight - scroller.clientHeight,
11601167
0,
@@ -2196,12 +2203,15 @@ async function EditorManager($header, $body) {
21962203

21972204
function syncScrollUi() {
21982205
scrollSyncRaf = 0;
2199-
onscrolltop();
2200-
onscrollleft();
2206+
editor.requestMeasure({
2207+
read: () => readScrollMetrics(),
2208+
write: updateScrollbarsFromMetrics,
2209+
});
22012210
}
22022211

22032212
function handleEditorScroll() {
22042213
if (!scroller) return;
2214+
if (restoreScrollbarScrollLock()) return;
22052215
if (!isScrolling) {
22062216
isScrolling = true;
22072217
if (hasHoverTooltips(editor.state)) {
@@ -2220,6 +2230,15 @@ async function EditorManager($header, $body) {
22202230
}
22212231

22222232
scroller?.addEventListener("scroll", handleEditorScroll, { passive: true });
2233+
scroller?.addEventListener("pointerdown", clearScrollbarScrollLock, {
2234+
passive: true,
2235+
});
2236+
scroller?.addEventListener("touchstart", clearScrollbarScrollLock, {
2237+
passive: true,
2238+
});
2239+
scroller?.addEventListener("wheel", clearScrollbarScrollLock, {
2240+
passive: true,
2241+
});
22232242
syncScrollUi();
22242243

22252244
keyboardHandler.on("keyboardShowStart", () => {
@@ -2328,6 +2347,49 @@ async function EditorManager($header, $body) {
23282347
return Date.now() < suppressCursorRevealUntil;
23292348
}
23302349

2350+
function lockScrollbarScrollPosition({ top, left }, duration = 1200) {
2351+
const scroller = editor?.scrollDOM;
2352+
if (!scroller) return;
2353+
scrollbarScrollLockUntil = Date.now() + duration;
2354+
if (typeof top === "number") scrollbarScrollLockTop = top;
2355+
if (typeof left === "number") scrollbarScrollLockLeft = left;
2356+
}
2357+
2358+
function clearScrollbarScrollLock() {
2359+
scrollbarScrollLockUntil = 0;
2360+
scrollbarScrollLockTop = null;
2361+
scrollbarScrollLockLeft = null;
2362+
}
2363+
2364+
function restoreScrollbarScrollLock() {
2365+
if (Date.now() >= scrollbarScrollLockUntil) {
2366+
clearScrollbarScrollLock();
2367+
return false;
2368+
}
2369+
2370+
const scroller = editor?.scrollDOM;
2371+
if (!scroller) return false;
2372+
2373+
let restored = false;
2374+
if (
2375+
typeof scrollbarScrollLockTop === "number" &&
2376+
Math.abs(scroller.scrollTop - scrollbarScrollLockTop) > 1
2377+
) {
2378+
scroller.scrollTop = scrollbarScrollLockTop;
2379+
lastScrollTop = scroller.scrollTop;
2380+
restored = true;
2381+
}
2382+
if (
2383+
typeof scrollbarScrollLockLeft === "number" &&
2384+
Math.abs(scroller.scrollLeft - scrollbarScrollLockLeft) > 1
2385+
) {
2386+
scroller.scrollLeft = scrollbarScrollLockLeft;
2387+
lastScrollLeft = scroller.scrollLeft;
2388+
restored = true;
2389+
}
2390+
return restored;
2391+
}
2392+
23312393
/**
23322394
* Checks if the cursor is visible within the CodeMirror viewport.
23332395
* @returns {boolean} - True if the cursor is visible, false otherwise.
@@ -2369,13 +2431,15 @@ async function EditorManager($header, $body) {
23692431
preventScrollbarV = true;
23702432
scroller.scrollTop = normalized * maxScroll;
23712433
lastScrollTop = scroller.scrollTop;
2434+
lockScrollbarScrollPosition({ top: lastScrollTop });
23722435
}
23732436

23742437
/**
23752438
* Handles the onscroll event for the vend element.
23762439
*/
23772440
function onscrollVend() {
2378-
suppressCursorReveal();
2441+
suppressCursorReveal(1200);
2442+
lockScrollbarScrollPosition({ top: editor?.scrollDOM?.scrollTop }, 1200);
23792443
preventScrollbarV = false;
23802444
setVScrollValue();
23812445
}
@@ -2394,13 +2458,15 @@ async function EditorManager($header, $body) {
23942458
preventScrollbarH = true;
23952459
scroller.scrollLeft = normalized * maxScroll;
23962460
lastScrollLeft = scroller.scrollLeft;
2461+
lockScrollbarScrollPosition({ left: lastScrollLeft });
23972462
}
23982463

23992464
/**
24002465
* Handles the event when the horizontal scrollbar reaches the end.
24012466
*/
24022467
function onscrollHEnd() {
2403-
suppressCursorReveal();
2468+
suppressCursorReveal(1200);
2469+
lockScrollbarScrollPosition({ left: editor?.scrollDOM?.scrollLeft }, 1200);
24042470
preventScrollbarH = false;
24052471
setHScrollValue();
24062472
}
@@ -2447,6 +2513,61 @@ async function EditorManager($header, $body) {
24472513
$hScrollbar.render();
24482514
}
24492515

2516+
function readScrollMetrics() {
2517+
const scroller = editor?.scrollDOM;
2518+
if (!scroller) return null;
2519+
return {
2520+
scrollTop: scroller.scrollTop,
2521+
scrollLeft: scroller.scrollLeft,
2522+
scrollHeight: scroller.scrollHeight,
2523+
scrollWidth: scroller.scrollWidth,
2524+
clientHeight: scroller.clientHeight,
2525+
clientWidth: scroller.clientWidth,
2526+
};
2527+
}
2528+
2529+
function updateScrollbarsFromMetrics(metrics) {
2530+
if (!metrics) return;
2531+
2532+
const maxScrollTop = Math.max(
2533+
metrics.scrollHeight - metrics.clientHeight,
2534+
0,
2535+
);
2536+
if (maxScrollTop <= 0) {
2537+
$vScrollbar.hide();
2538+
lastScrollTop = 0;
2539+
$vScrollbar.value = 0;
2540+
} else {
2541+
if (!preventScrollbarV && metrics.scrollTop !== lastScrollTop) {
2542+
lastScrollTop = metrics.scrollTop;
2543+
$vScrollbar.value = clamp01(metrics.scrollTop / maxScrollTop);
2544+
}
2545+
$vScrollbar.render();
2546+
}
2547+
2548+
if (appSettings.value.textWrap) {
2549+
$hScrollbar.hide();
2550+
return;
2551+
}
2552+
2553+
const maxScrollLeft = Math.max(
2554+
metrics.scrollWidth - metrics.clientWidth,
2555+
0,
2556+
);
2557+
if (maxScrollLeft <= 0) {
2558+
$hScrollbar.hide();
2559+
lastScrollLeft = 0;
2560+
$hScrollbar.value = 0;
2561+
return;
2562+
}
2563+
2564+
if (!preventScrollbarH && metrics.scrollLeft !== lastScrollLeft) {
2565+
lastScrollLeft = metrics.scrollLeft;
2566+
$hScrollbar.value = clamp01(metrics.scrollLeft / maxScrollLeft);
2567+
}
2568+
$hScrollbar.render();
2569+
}
2570+
24502571
/**
24512572
* Sets scrollbars value based on the editor's scroll position.
24522573
*/

0 commit comments

Comments
 (0)