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:
1
.gitignore
vendored
1
.gitignore
vendored
@ -25,3 +25,4 @@ storage/logs/
|
|||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
.codebuddy
|
||||||
|
|||||||
@ -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%;
|
||||||
|
|||||||
@ -273,6 +273,7 @@
|
|||||||
#editor-container {
|
#editor-container {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* wangEditor 内部文本容器撑满 */
|
/* wangEditor 内部文本容器撑满 */
|
||||||
|
|||||||
@ -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
Reference in New Issue
Block a user