fix(core): scroll sync v2 — use scrollTop instead of selectionStart
Root cause: _syncScrollByHeading used cursor position (selectionStart) to find nearest heading, but cursor doesn't move during scroll. Preview was stuck at the same heading regardless of scroll position. Fix: base heading detection on estimated viewport top line from scrollTop + lineHeight. Always run proportional sync first for smooth scrolling, then fine-tune to nearest heading at viewport top.
This commit is contained in:
+20
-18
@@ -1243,37 +1243,39 @@ class MarkdownEditor {
|
||||
const ta = this.textarea;
|
||||
if (!ta) return;
|
||||
|
||||
// 找到光标所在位置最近的标题
|
||||
const pos = ta.selectionStart;
|
||||
const before = this._value.substring(0, pos);
|
||||
const headings = [...before.matchAll(/^(#{1,6})\s+(.+)$/gm)];
|
||||
if (!headings.length) {
|
||||
// 无标题,回退到比例同步
|
||||
// 1. 先做比例同步(基础行为,保证平滑)
|
||||
const max = ta.scrollHeight - ta.clientHeight;
|
||||
if (max <= 0) return;
|
||||
const ratio = ta.scrollTop / max;
|
||||
const pmax = this.previewPane.scrollHeight - this.previewPane.clientHeight;
|
||||
this.previewPane.scrollTop = ratio * pmax;
|
||||
return;
|
||||
}
|
||||
|
||||
// 取最后一个标题(光标之前最近的标题)
|
||||
// 2. 如果有标题,微调到最近的标题位置
|
||||
const val = this._value;
|
||||
if (!val) return;
|
||||
|
||||
// 估计 textarea 视口顶部的行号
|
||||
const lineHeight = 13.5 * 1.7; // font-size * line-height
|
||||
const scrollLines = Math.floor(ta.scrollTop / lineHeight);
|
||||
const beforeLines = val.split('\n').slice(0, Math.max(0, scrollLines));
|
||||
const visibleText = beforeLines.join('\n');
|
||||
|
||||
// 找到视口顶部之前最近的标题
|
||||
const headings = [...visibleText.matchAll(/^(#{1,6})\s+(.+)$/gm)];
|
||||
if (!headings.length) return;
|
||||
|
||||
const lastH = headings[headings.length - 1];
|
||||
const headingText = lastH[2].trim();
|
||||
|
||||
// 在预览区查找对应标题元素
|
||||
// 在预览区查找对应标题
|
||||
const previewEls = this.previewEl.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||
let found;
|
||||
for (const el of previewEls) {
|
||||
if (el.textContent.trim() === headingText) {
|
||||
el.scrollIntoView({ block: 'start', behavior: 'instant' });
|
||||
return;
|
||||
if (el.textContent.trim() === headingText) { found = el; break; }
|
||||
}
|
||||
if (found) {
|
||||
found.scrollIntoView({ block: 'start', behavior: 'instant' });
|
||||
}
|
||||
|
||||
// fallback
|
||||
const max = ta.scrollHeight - ta.clientHeight;
|
||||
if (max <= 0) return;
|
||||
this.previewPane.scrollTop = (ta.scrollTop / max) * (this.previewPane.scrollHeight - this.previewPane.clientHeight);
|
||||
}
|
||||
|
||||
_updateWordCount() {
|
||||
|
||||
Reference in New Issue
Block a user