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/
*.swp
*.swo
.codebuddy

View File

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

View File

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

View File

@ -61,9 +61,12 @@ function initEditor() {
const editorConfig = {
placeholder: '在此输入正文...',
onChange() {
// 同步更新代码块语言标签(立即生效
// 延迟更新代码块语言标签(同步操作 DOM 会干扰 wangEditor 内部同步,导致滚动跳动
updateCodeLabels();
// 替代 wangEditor 内置 scrollIntoView已禁用仅在光标不可见时微调滚动
ensureCursorVisible();
clearTimeout(syncTimer);
syncTimer = setTimeout(() => {
updateStats();
@ -128,7 +131,14 @@ function initEditor() {
createToolbar({
editor,
selector: '#toolbar-container',
config: {},
config: {
excludeKeys: [
// 移除视频相关按钮:不允许视频上传,不在前端提供视频按钮
'group-video', 'insertVideo', 'uploadVideo',
// 移除"网络图片"功能,仅保留上传图片
'insertImage',
],
},
mode: 'default',
});
@ -144,16 +154,48 @@ function initEditor() {
saveDraft();
});
// 滚动同步
// 滚动同步.w-e-scroll 是编辑区唯一滚动容器)
previewCont.addEventListener('scroll', syncScrollToEditor);
setTimeout(() => {
const scroller = document.getElementById('editor-container') || editorPane;
scroller.addEventListener('scroll', syncScrollToPreview);
const scroller = (document.getElementById('editor-container') || editorPane).querySelector('.w-e-scroll');
if (scroller) scroller.addEventListener('scroll', syncScrollToPreview);
}, 500);
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"> 提取并注入标签
================================================================ */
// 代码块语言标签更新节流定时器
let codeLabelTimer = null;
function updateCodeLabels() {
if (!editor) return;
// 节流:避免 onChange 中频繁调用导致滚动跳动
clearTimeout(codeLabelTimer);
codeLabelTimer = setTimeout(_updateCodeLabelsImpl, 300);
}
function _updateCodeLabelsImpl() {
if (!editor) return;
// wangEditor 编辑区 <code> 不带 class="language-xxx"
// 但 getHtml() 输出带 class="language-xxx"。
// 用 DOMParser 解析 HTML 获取语言,再注入到编辑区对应 pre。
@ -184,6 +237,11 @@ function updateCodeLabels() {
if (!editorEl) return;
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) => {
const domPre = domPres[idx];
if (!domPre || domPre.querySelector('.code-lang-label')) return;
@ -200,6 +258,11 @@ function updateCodeLabels() {
}
}
});
// 恢复滚动位置
if (scroller && scroller.scrollTop !== savedScrollTop) {
scroller.scrollTop = savedScrollTop;
}
} catch (_) { /* 忽略 */ }
}
@ -256,10 +319,16 @@ function togglePreview() {
================================================================ */
let scrollSyncing = false;
function getEditorScroller() {
const el = document.getElementById('editor-container') || editorPane;
return el ? (el.querySelector('.w-e-scroll') || el) : null;
}
function syncScrollToPreview() {
if (!previewVisible || scrollSyncing) return;
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);
previewCont.scrollTop = ratio * Math.max(1, previewCont.scrollHeight - previewCont.clientHeight);
requestAnimationFrame(() => { scrollSyncing = false; });
@ -268,8 +337,9 @@ function syncScrollToPreview() {
function syncScrollToEditor() {
if (!previewVisible || scrollSyncing) return;
scrollSyncing = true;
const scroller = getEditorScroller();
if (!scroller) { scrollSyncing = false; return; }
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);
requestAnimationFrame(() => { scrollSyncing = false; });
}

File diff suppressed because one or more lines are too long