diff --git a/src/core.js b/src/core.js index efc5d01..27bc4ef 100644 --- a/src/core.js +++ b/src/core.js @@ -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) { - // 无标题,回退到比例同步 - 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; - } + // 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; + + // 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() {