fix(frontend): 多行输入时保持光标可见

adjustTextareaSize 重算高度会打乱 scrollTop,导致多行输入时光标被滚出视野。
新增 keepEditorSelectionVisible:高度重设后,聚焦态用编辑器 coordsAtPos 算出
光标位置并滚回可视区(上下留 lineHeight*0.3 padding),非聚焦态恢复原 scrollTop。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
JOJO 2026-06-05 23:33:48 +08:00
parent 9be775d346
commit e34e2c965c

View File

@ -1528,6 +1528,9 @@ const adjustTextareaSize = () => {
return;
}
const target = root;
const previousScrollTop = target.scrollTop;
const selectionShouldStayVisible =
document.activeElement === target || target.contains(document.activeElement);
target.style.height = 'auto';
const computedStyle = window.getComputedStyle(target);
const lineHeight = parseFloat(computedStyle.lineHeight || '20') || 20;
@ -1537,11 +1540,58 @@ const adjustTextareaSize = () => {
const multiline = targetHeight > lineHeight * 1.4;
applyLineMetrics(lines, multiline);
target.style.height = `${targetHeight}px`;
keepEditorSelectionVisible(target, {
previousScrollTop,
selectionShouldStayVisible,
padding: Math.max(4, lineHeight * 0.3)
});
nextTick(() => {
emitComposerHeight();
});
};
const keepEditorSelectionVisible = (
target: HTMLElement,
options: { previousScrollTop: number; selectionShouldStayVisible: boolean; padding: number }
) => {
const maxScrollTop = Math.max(0, target.scrollHeight - target.clientHeight);
if (maxScrollTop <= 0) {
target.scrollTop = 0;
return;
}
if (!options.selectionShouldStayVisible) {
target.scrollTop = Math.min(maxScrollTop, Math.max(0, options.previousScrollTop));
return;
}
requestAnimationFrame(() => {
const instance = editor.value;
if (!instance) {
target.scrollTop = Math.min(maxScrollTop, Math.max(0, options.previousScrollTop));
return;
}
try {
const caretRect = instance.view.coordsAtPos(instance.state.selection.head);
const targetRect = target.getBoundingClientRect();
const padding = options.padding;
const currentMaxScrollTop = Math.max(0, target.scrollHeight - target.clientHeight);
let nextScrollTop = target.scrollTop;
if (caretRect.bottom > targetRect.bottom - padding) {
nextScrollTop += caretRect.bottom - targetRect.bottom + padding;
} else if (caretRect.top < targetRect.top + padding) {
nextScrollTop -= targetRect.top + padding - caretRect.top;
}
target.scrollTop = Math.min(currentMaxScrollTop, Math.max(0, nextScrollTop));
} catch {
target.scrollTop = Math.min(maxScrollTop, Math.max(0, options.previousScrollTop));
}
});
};
const onInput = (event: Event) => {
const target = event.target as HTMLTextAreaElement;
if (!props.isConnected || props.inputLocked) {