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;
|
const ta = this.textarea;
|
||||||
if (!ta) return;
|
if (!ta) return;
|
||||||
|
|
||||||
// 找到光标所在位置最近的标题
|
// 1. 先做比例同步(基础行为,保证平滑)
|
||||||
const pos = ta.selectionStart;
|
|
||||||
const before = this._value.substring(0, pos);
|
|
||||||
const headings = [...before.matchAll(/^(#{1,6})\s+(.+)$/gm)];
|
|
||||||
if (!headings.length) {
|
|
||||||
// 无标题,回退到比例同步
|
|
||||||
const max = ta.scrollHeight - ta.clientHeight;
|
const max = ta.scrollHeight - ta.clientHeight;
|
||||||
if (max <= 0) return;
|
if (max <= 0) return;
|
||||||
const ratio = ta.scrollTop / max;
|
const ratio = ta.scrollTop / max;
|
||||||
const pmax = this.previewPane.scrollHeight - this.previewPane.clientHeight;
|
const pmax = this.previewPane.scrollHeight - this.previewPane.clientHeight;
|
||||||
this.previewPane.scrollTop = ratio * pmax;
|
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 lastH = headings[headings.length - 1];
|
||||||
const headingText = lastH[2].trim();
|
const headingText = lastH[2].trim();
|
||||||
|
|
||||||
// 在预览区查找对应标题元素
|
// 在预览区查找对应标题
|
||||||
const previewEls = this.previewEl.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
const previewEls = this.previewEl.querySelectorAll('h1, h2, h3, h4, h5, h6');
|
||||||
|
let found;
|
||||||
for (const el of previewEls) {
|
for (const el of previewEls) {
|
||||||
if (el.textContent.trim() === headingText) {
|
if (el.textContent.trim() === headingText) { found = el; break; }
|
||||||
el.scrollIntoView({ block: 'start', behavior: 'instant' });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
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() {
|
_updateWordCount() {
|
||||||
|
|||||||
Reference in New Issue
Block a user