fix(frontend): use selectionchange event for cursor tracking instead of per-render
The per-render tracking effect was reading DOM cursor position before the browser had finished positioning the caret after innerHTML updates, causing ms.cursorOffset to be set to the old position and overwriting the deferred correct value. Switch to document.selectionchange event which: - Fires only when the user actually moves the cursor (arrow keys, mouse clicks) - Does NOT fire during programmatic DOM updates - Uses focusNode/focusOffset directly from the selection API This completely eliminates the timing race between render effects and programmatic cursor updates.
This commit is contained in:
parent
4330325bfc
commit
53b0b03716
@ -81,37 +81,39 @@ const ChatInputArea = memo(function ChatInputArea({
|
|||||||
!!aiConfigsLoading,
|
!!aiConfigsLoading,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Track DOM cursor offset → ms.cursorOffset on every render
|
// ─── Track DOM cursor offset → ms.cursorOffset on user navigation ──────
|
||||||
|
// Uses selectionchange event which fires when caret moves via arrows/clicks.
|
||||||
|
// Does NOT fire during programmatic updates, avoiding cursor jumps.
|
||||||
const prevCursorRef = useRef(ms.cursorOffset);
|
const prevCursorRef = useRef(ms.cursorOffset);
|
||||||
// Set when a deferred cursor update is scheduled; cleared when it fires.
|
|
||||||
// Prevents the tracking effect from overwriting a programmatically-set cursor.
|
|
||||||
const skipCursorTrackingRef = useRef(false);
|
const skipCursorTrackingRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const readCursor = () => {
|
||||||
if (skipCursorTrackingRef.current) return;
|
if (skipCursorTrackingRef.current) return;
|
||||||
const el = containerRef.current;
|
const el = containerRef.current;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
const sel = window.getSelection();
|
const sel = window.getSelection();
|
||||||
if (!sel || sel.rangeCount === 0) return;
|
if (!sel || sel.rangeCount === 0 || !el.contains(sel.focusNode)) return;
|
||||||
const range = sel.getRangeAt(0);
|
const range = sel.getRangeAt(0);
|
||||||
const pre = range.cloneRange();
|
// Build a range from start of contenteditable to caret
|
||||||
|
const pre = document.createRange();
|
||||||
pre.selectNodeContents(el);
|
pre.selectNodeContents(el);
|
||||||
pre.setEnd(range.startContainer, range.startOffset);
|
pre.setEnd(sel.focusNode, sel.focusOffset);
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
||||||
|
let found = false;
|
||||||
while (walker.nextNode()) {
|
while (walker.nextNode()) {
|
||||||
const node = walker.currentNode as Text;
|
const node = walker.currentNode as Text;
|
||||||
if (node === pre.startContainer) { count += pre.startOffset; break; }
|
if (node === sel.focusNode) { count += sel.focusOffset; found = true; break; }
|
||||||
count += node.length;
|
count += node.length;
|
||||||
}
|
}
|
||||||
|
if (!found) return;
|
||||||
if (count !== prevCursorRef.current) {
|
if (count !== prevCursorRef.current) {
|
||||||
// Skip update when caret is at end of text (programmatic value change
|
|
||||||
// that already positioned the caret — the caret hasn't moved from the
|
|
||||||
// user's perspective). Only update on real user-initiated cursor movement.
|
|
||||||
if (count !== ms.value.length || prevCursorRef.current !== ms.value.length) {
|
|
||||||
prevCursorRef.current = count;
|
prevCursorRef.current = count;
|
||||||
ms.setCursorOffset(count);
|
ms.setCursorOffset(count);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
document.addEventListener('selectionchange', readCursor);
|
||||||
|
return () => document.removeEventListener('selectionchange', readCursor);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Imperative handle ──────────────────────────────────────────────────
|
// ─── Imperative handle ──────────────────────────────────────────────────
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user