fix: 修复编辑器代码块换行滚动跳动 + 移除视频和网络图片功能

- 修复 wangeditor 选区同步 scrollIntoView 导致代码块换行大幅跳动的问题
  - 移除 getBoundingClientRect 替换 hack,改用元素自身真实位置计算滚动
  - 将 block 参数从 end 改为 nearest,behavior 从 smooth 改为 instant
- 修复 #editor-container 与 .w-e-scroll 双重滚动容器冲突
  - #editor-container 和 .w-e-text-container 设为 overflow:hidden
  - .w-e-scroll 作为唯一滚动容器设 overflow-y:auto
- updateCodeLabels 改为节流执行并保存/恢复滚动位置
- 新增 ensureCursorVisible 替代内置滚动保证光标可见
- 滚动同步函数统一引用 .w-e-scroll 容器
- 工具栏排除视频相关按钮和网络图片功能
- .gitignore 添加 .codebuddy
This commit is contained in:
2026-05-28 01:44:46 +08:00
parent 2917562bf0
commit 804fd50edf
5 changed files with 1167 additions and 1089 deletions

1
.gitignore vendored
View File

@ -25,3 +25,4 @@ storage/logs/
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
.codebuddy

View File

@ -15,12 +15,18 @@
} }
#editor-container { #editor-container {
flex: 1; flex: 1;
overflow-y: auto; overflow: hidden;
min-height: 0; min-height: 0;
height: 100%; height: 100%;
} }
/* .w-e-scroll 是 wangEditor 内部唯一的滚动容器 */
.editor-pane .w-e-text-container { .editor-pane .w-e-text-container {
height: 100% !important; height: 100% !important;
overflow: hidden;
}
.editor-pane .w-e-scroll {
height: 100%;
overflow-y: auto;
} }
.editor-pane .w-e-text-container [data-slate-editor] { .editor-pane .w-e-text-container [data-slate-editor] {
min-height: 100%; min-height: 100%;

View File

@ -273,6 +273,7 @@
#editor-container { #editor-container {
background: #fff; background: #fff;
height: 100%; height: 100%;
overflow: hidden;
} }
/* wangEditor 内部文本容器撑满 */ /* wangEditor 内部文本容器撑满 */

View File

@ -61,9 +61,12 @@ function initEditor() {
const editorConfig = { const editorConfig = {
placeholder: '在此输入正文...', placeholder: '在此输入正文...',
onChange() { onChange() {
// 同步更新代码块语言标签(立即生效 // 延迟更新代码块语言标签(同步操作 DOM 会干扰 wangEditor 内部同步,导致滚动跳动
updateCodeLabels(); updateCodeLabels();
// 替代 wangEditor 内置 scrollIntoView已禁用仅在光标不可见时微调滚动
ensureCursorVisible();
clearTimeout(syncTimer); clearTimeout(syncTimer);
syncTimer = setTimeout(() => { syncTimer = setTimeout(() => {
updateStats(); updateStats();
@ -128,7 +131,14 @@ function initEditor() {
createToolbar({ createToolbar({
editor, editor,
selector: '#toolbar-container', selector: '#toolbar-container',
config: {}, config: {
excludeKeys: [
// 移除视频相关按钮:不允许视频上传,不在前端提供视频按钮
'group-video', 'insertVideo', 'uploadVideo',
// 移除"网络图片"功能,仅保留上传图片
'insertImage',
],
},
mode: 'default', mode: 'default',
}); });
@ -144,16 +154,48 @@ function initEditor() {
saveDraft(); saveDraft();
}); });
// 滚动同步 // 滚动同步.w-e-scroll 是编辑区唯一滚动容器)
previewCont.addEventListener('scroll', syncScrollToEditor); previewCont.addEventListener('scroll', syncScrollToEditor);
setTimeout(() => { setTimeout(() => {
const scroller = document.getElementById('editor-container') || editorPane; const scroller = (document.getElementById('editor-container') || editorPane).querySelector('.w-e-scroll');
scroller.addEventListener('scroll', syncScrollToPreview); if (scroller) scroller.addEventListener('scroll', syncScrollToPreview);
}, 500); }, 500);
console.log('%c📝 MetaLab Editor (wangEditor) ready', 'color:#6366f1;font-weight:bold'); console.log('%c📝 MetaLab Editor (wangEditor) ready', 'color:#6366f1;font-weight:bold');
} }
/* ================================================================
确保光标可见 — 替代 wangEditor 内置 scrollIntoView
wangEditor 默认使用 block:"end" 会在代码块换行时导致大幅滚动,
已在 wangeditor.js 中禁用该调用,此处用最小偏移量保证光标可见
================================================================ */
function ensureCursorVisible() {
if (!editor) return;
requestAnimationFrame(() => {
try {
const sel = window.getSelection();
if (!sel || !sel.rangeCount) return;
const range = sel.getRangeAt(0);
// .w-e-scroll 是编辑区滚动容器
const scroller = document.querySelector('#editor-container .w-e-scroll');
if (!scroller) return;
const cursorRect = range.getBoundingClientRect();
const scrollRect = scroller.getBoundingClientRect();
// 光标低于可见区 → 向下滚到光标可见
if (cursorRect.bottom > scrollRect.bottom) {
scroller.scrollTop += cursorRect.bottom - scrollRect.bottom + 8;
}
// 光标高于可见区 → 向上滚到光标可见
else if (cursorRect.top < scrollRect.top) {
scroller.scrollTop -= scrollRect.top - cursorRect.top + 8;
}
} catch (_) { /* 忽略 */ }
});
}
/* ================================================================ /* ================================================================
字数统计 字数统计
================================================================ */ ================================================================ */
@ -168,9 +210,20 @@ function getEditorHtml() {
/* ================================================================ /* ================================================================
代码块语言标签 — 从 <code class="language-xxx"> 提取并注入标签 代码块语言标签 — 从 <code class="language-xxx"> 提取并注入标签
================================================================ */ ================================================================ */
// 代码块语言标签更新节流定时器
let codeLabelTimer = null;
function updateCodeLabels() { function updateCodeLabels() {
if (!editor) return; if (!editor) return;
// 节流:避免 onChange 中频繁调用导致滚动跳动
clearTimeout(codeLabelTimer);
codeLabelTimer = setTimeout(_updateCodeLabelsImpl, 300);
}
function _updateCodeLabelsImpl() {
if (!editor) return;
// wangEditor 编辑区 <code> 不带 class="language-xxx" // wangEditor 编辑区 <code> 不带 class="language-xxx"
// 但 getHtml() 输出带 class="language-xxx"。 // 但 getHtml() 输出带 class="language-xxx"。
// 用 DOMParser 解析 HTML 获取语言,再注入到编辑区对应 pre。 // 用 DOMParser 解析 HTML 获取语言,再注入到编辑区对应 pre。
@ -184,6 +237,11 @@ function updateCodeLabels() {
if (!editorEl) return; if (!editorEl) return;
const domPres = editorEl.querySelectorAll('pre'); const domPres = editorEl.querySelectorAll('pre');
// 保存当前滚动位置,防止 DOM 操作引起跳动
// .w-e-scroll 是 wangEditor 内部的滚动容器
const scroller = editorEl.querySelector('.w-e-scroll');
const savedScrollTop = scroller ? scroller.scrollTop : 0;
htmlPres.forEach((htmlPre, idx) => { htmlPres.forEach((htmlPre, idx) => {
const domPre = domPres[idx]; const domPre = domPres[idx];
if (!domPre || domPre.querySelector('.code-lang-label')) return; if (!domPre || domPre.querySelector('.code-lang-label')) return;
@ -200,6 +258,11 @@ function updateCodeLabels() {
} }
} }
}); });
// 恢复滚动位置
if (scroller && scroller.scrollTop !== savedScrollTop) {
scroller.scrollTop = savedScrollTop;
}
} catch (_) { /* 忽略 */ } } catch (_) { /* 忽略 */ }
} }
@ -256,10 +319,16 @@ function togglePreview() {
================================================================ */ ================================================================ */
let scrollSyncing = false; let scrollSyncing = false;
function getEditorScroller() {
const el = document.getElementById('editor-container') || editorPane;
return el ? (el.querySelector('.w-e-scroll') || el) : null;
}
function syncScrollToPreview() { function syncScrollToPreview() {
if (!previewVisible || scrollSyncing) return; if (!previewVisible || scrollSyncing) return;
scrollSyncing = true; scrollSyncing = true;
const scroller = document.getElementById('editor-container') || editorPane; const scroller = getEditorScroller();
if (!scroller) { scrollSyncing = false; return; }
const ratio = scroller.scrollTop / Math.max(1, scroller.scrollHeight - scroller.clientHeight); const ratio = scroller.scrollTop / Math.max(1, scroller.scrollHeight - scroller.clientHeight);
previewCont.scrollTop = ratio * Math.max(1, previewCont.scrollHeight - previewCont.clientHeight); previewCont.scrollTop = ratio * Math.max(1, previewCont.scrollHeight - previewCont.clientHeight);
requestAnimationFrame(() => { scrollSyncing = false; }); requestAnimationFrame(() => { scrollSyncing = false; });
@ -268,8 +337,9 @@ function syncScrollToPreview() {
function syncScrollToEditor() { function syncScrollToEditor() {
if (!previewVisible || scrollSyncing) return; if (!previewVisible || scrollSyncing) return;
scrollSyncing = true; scrollSyncing = true;
const scroller = getEditorScroller();
if (!scroller) { scrollSyncing = false; return; }
const ratio = previewCont.scrollTop / Math.max(1, previewCont.scrollHeight - previewCont.clientHeight); const ratio = previewCont.scrollTop / Math.max(1, previewCont.scrollHeight - previewCont.clientHeight);
const scroller = document.getElementById('editor-container') || editorPane;
scroller.scrollTop = ratio * Math.max(1, scroller.scrollHeight - scroller.clientHeight); scroller.scrollTop = ratio * Math.max(1, scroller.scrollHeight - scroller.clientHeight);
requestAnimationFrame(() => { scrollSyncing = false; }); requestAnimationFrame(() => { scrollSyncing = false; });
} }

File diff suppressed because one or more lines are too long