fix: 修复 WYSIWYG 清空内容 + 分屏工具栏 + XSS 防御(bulemondy消毒)
This commit is contained in:
@ -17,10 +17,10 @@
|
||||
</header>
|
||||
|
||||
<div class="post-detail-body">
|
||||
{{.Post.BodyHTML}}
|
||||
{{.PostBodyHTML}}
|
||||
{{/* 如果 BodyHTML 为空,直接显示原文 */}}
|
||||
</div>
|
||||
{{if not .Post.BodyHTML}}
|
||||
{{if not .PostBodyHTML}}
|
||||
<div class="post-detail-body-raw">{{.Post.Body}}</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
var postId = document.getElementById('postId');
|
||||
var titleEl = document.getElementById('postTitle');
|
||||
var bodyEl = document.getElementById('postBody'); // textarea (source of truth)
|
||||
var bodyEl = document.getElementById('postBody'); // textarea (唯一数据源)
|
||||
var wysiwygEl = document.getElementById('wysiwygEditor'); // contenteditable
|
||||
var splitPrev = document.getElementById('splitPreview');
|
||||
var splitCont = splitPrev ? splitPrev.querySelector('.split-preview-content') : null;
|
||||
@ -13,6 +13,7 @@
|
||||
|
||||
var isEdit = postId && postId.value;
|
||||
var currentMode = 'wysiwyg';
|
||||
var wysiwygDirty = false; // WYSIWYG 是否已被用户编辑过(首次进入时不覆盖 textarea)
|
||||
var syncTimer = null;
|
||||
|
||||
// ===================== HELPERS =====================
|
||||
@ -22,19 +23,9 @@
|
||||
return meta ? meta.getAttribute('content') : '';
|
||||
}
|
||||
|
||||
function getBodyValue() {
|
||||
return currentMode === 'wysiwyg' ? bodyEl.value : bodyEl.value;
|
||||
}
|
||||
|
||||
// 保存到 textarea 并同步 WYSIWYG
|
||||
function saveToTextarea(md) {
|
||||
bodyEl.value = md;
|
||||
if (currentMode === 'wysiwyg') syncMarkdownToWysiwyg();
|
||||
}
|
||||
|
||||
// ================== MARKDOWN ↔ HTML ==================
|
||||
|
||||
// 通过 API 渲染 markdown 为 HTML
|
||||
// 通过 API 渲染 markdown 为 HTML(服务端 Goldmark + bluemonday 消毒)
|
||||
function renderMarkdown(md, callback) {
|
||||
fetch('/api/posts/preview', {
|
||||
method: 'POST',
|
||||
@ -55,10 +46,7 @@
|
||||
el.innerHTML = html;
|
||||
|
||||
function walk(node) {
|
||||
var md = '';
|
||||
if (node.nodeType === 3) { // text
|
||||
return node.textContent;
|
||||
}
|
||||
if (node.nodeType === 3) return node.textContent;
|
||||
if (node.nodeType !== 1) return '';
|
||||
var tag = node.tagName.toLowerCase();
|
||||
var children = '';
|
||||
@ -82,8 +70,7 @@
|
||||
case 'pre':
|
||||
var code = node.querySelector('code');
|
||||
var lang = code ? (code.className.replace('language-','') || '') : '';
|
||||
var text = node.textContent;
|
||||
return '\n```' + lang + '\n' + text + '\n```\n';
|
||||
return '\n```' + lang + '\n' + node.textContent + '\n```\n';
|
||||
case 'code':
|
||||
if (node.parentNode.tagName.toLowerCase() !== 'pre')
|
||||
return '`' + children + '`';
|
||||
@ -97,9 +84,7 @@
|
||||
return '> ' + children.replace(/\n$/, '').replace(/\n/g, '\n> ') + '\n\n';
|
||||
case 'hr': return '\n---\n';
|
||||
case 'img':
|
||||
var src = node.getAttribute('src') || '';
|
||||
var alt = node.getAttribute('alt') || '';
|
||||
return '';
|
||||
return ' || '') + ')';
|
||||
case 'div':
|
||||
case 'section':
|
||||
return children;
|
||||
@ -110,87 +95,36 @@
|
||||
return walk(el).replace(/\n{3,}/g, '\n\n').trim();
|
||||
}
|
||||
|
||||
// 将 markdown 渲染到 WYSIWYG
|
||||
// 将 markdown 从 textarea 渲染到 WYSIWYG
|
||||
function syncMarkdownToWysiwyg() {
|
||||
if (!bodyEl.value.trim()) {
|
||||
wysiwygEl.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
renderMarkdown(bodyEl.value, function(html) {
|
||||
var cur = wysiwygEl.innerHTML;
|
||||
// 只在真的变化时更新,避免闪烁
|
||||
if (cur !== html) {
|
||||
wysiwygEl.innerHTML = html;
|
||||
}
|
||||
wysiwygEl.innerHTML = html;
|
||||
});
|
||||
}
|
||||
|
||||
// WYSIWYG 编辑后延迟同步回 textarea
|
||||
// WYSIWYG 编辑后延迟将 HTML 转为 markdown 保存回 textarea
|
||||
function debounceSync() {
|
||||
clearTimeout(syncTimer);
|
||||
syncTimer = setTimeout(function() {
|
||||
var md = htmlToMarkdown(wysiwygEl.innerHTML);
|
||||
bodyEl.value = md;
|
||||
}, 500);
|
||||
if (md !== bodyEl.value) {
|
||||
bodyEl.value = md;
|
||||
}
|
||||
}, 600);
|
||||
}
|
||||
|
||||
// ==================== MODE SWITCHING ====================
|
||||
|
||||
function switchMode(mode) {
|
||||
// 先同步当前状态
|
||||
if (currentMode === 'wysiwyg') {
|
||||
var md = htmlToMarkdown(wysiwygEl.innerHTML);
|
||||
bodyEl.value = md;
|
||||
}
|
||||
|
||||
currentMode = mode;
|
||||
|
||||
// 更新标签
|
||||
modeTabs.forEach(function(tab) {
|
||||
tab.classList.toggle('active', tab.dataset.mode === mode);
|
||||
});
|
||||
|
||||
// 切换布局 class
|
||||
var editorBody = document.querySelector('.editor-body');
|
||||
if (editorBody) {
|
||||
editorBody.classList.toggle('split-mode', mode === 'split');
|
||||
}
|
||||
|
||||
// 切换面板
|
||||
if (mode === 'wysiwyg') {
|
||||
bodyEl.style.display = 'none';
|
||||
wysiwygEl.style.display = '';
|
||||
if (splitPrev) splitPrev.style.display = 'none';
|
||||
if (toolbar) toolbar.style.display = '';
|
||||
syncMarkdownToWysiwyg();
|
||||
} else if (mode === 'source') {
|
||||
bodyEl.style.display = '';
|
||||
wysiwygEl.style.display = 'none';
|
||||
if (splitPrev) splitPrev.style.display = 'none';
|
||||
if (toolbar) toolbar.style.display = '';
|
||||
} else if (mode === 'split') {
|
||||
bodyEl.style.display = '';
|
||||
wysiwygEl.style.display = 'none';
|
||||
if (splitPrev) splitPrev.style.display = '';
|
||||
if (toolbar) toolbar.style.display = 'none';
|
||||
updateSplitPreview();
|
||||
}
|
||||
}
|
||||
|
||||
modeTabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
switchMode(tab.dataset.mode);
|
||||
});
|
||||
});
|
||||
|
||||
// 分屏实时预览(防抖)
|
||||
var splitTimer = null;
|
||||
// markdown 保存回 textarea 后触发分屏预览更新
|
||||
bodyEl.addEventListener('input', function() {
|
||||
if (currentMode === 'split') {
|
||||
clearTimeout(splitTimer);
|
||||
splitTimer = setTimeout(updateSplitPreview, 300);
|
||||
}
|
||||
});
|
||||
var splitTimer = null;
|
||||
|
||||
function updateSplitPreview() {
|
||||
if (!splitCont || !bodyEl.value.trim()) {
|
||||
@ -204,7 +138,65 @@
|
||||
|
||||
// WYSIWYG 输入实时同步
|
||||
wysiwygEl.addEventListener('input', function() {
|
||||
if (currentMode === 'wysiwyg') debounceSync();
|
||||
if (currentMode === 'wysiwyg') {
|
||||
wysiwygDirty = true;
|
||||
debounceSync();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== MODE SWITCHING ====================
|
||||
|
||||
function switchMode(mode) {
|
||||
// 仅在离开 WYSIWYG 时同步 WYSIWYG → textarea
|
||||
if (currentMode === 'wysiwyg' && mode !== 'wysiwyg' && wysiwygDirty) {
|
||||
var md = htmlToMarkdown(wysiwygEl.innerHTML);
|
||||
bodyEl.value = md;
|
||||
}
|
||||
|
||||
currentMode = mode;
|
||||
wysiwygDirty = false; // 切换到新模式下重置 dirty 标记
|
||||
|
||||
// 更新标签
|
||||
modeTabs.forEach(function(tab) {
|
||||
tab.classList.toggle('active', tab.dataset.mode === mode);
|
||||
});
|
||||
|
||||
// 切换布局 class
|
||||
var editorBody = document.querySelector('.editor-body');
|
||||
if (editorBody) {
|
||||
editorBody.classList.toggle('split-mode', mode === 'split');
|
||||
}
|
||||
|
||||
// 工具栏在 split 模式下也显示(源码风格操作)
|
||||
if (toolbar) {
|
||||
toolbar.style.display = (mode === 'source' || mode === 'split') ? '' : '';
|
||||
}
|
||||
|
||||
// 切换面板
|
||||
if (mode === 'wysiwyg') {
|
||||
bodyEl.style.display = 'none';
|
||||
wysiwygEl.style.display = '';
|
||||
if (splitPrev) splitPrev.style.display = 'none';
|
||||
syncMarkdownToWysiwyg();
|
||||
wysiwygEl.focus();
|
||||
} else if (mode === 'source') {
|
||||
bodyEl.style.display = '';
|
||||
wysiwygEl.style.display = 'none';
|
||||
if (splitPrev) splitPrev.style.display = 'none';
|
||||
bodyEl.focus();
|
||||
} else if (mode === 'split') {
|
||||
bodyEl.style.display = '';
|
||||
wysiwygEl.style.display = 'none';
|
||||
if (splitPrev) splitPrev.style.display = '';
|
||||
bodyEl.focus();
|
||||
updateSplitPreview();
|
||||
}
|
||||
}
|
||||
|
||||
modeTabs.forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
switchMode(tab.dataset.mode);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================== TOOLBAR ====================
|
||||
@ -216,7 +208,8 @@
|
||||
var action = btn.dataset.action;
|
||||
if (currentMode === 'wysiwyg') {
|
||||
execWysiwygAction(action);
|
||||
} else if (currentMode === 'source') {
|
||||
} else {
|
||||
// source 和 split 模式都用源码工具栏(操作 textarea 中的 markdown)
|
||||
execSourceAction(action);
|
||||
}
|
||||
});
|
||||
@ -264,7 +257,7 @@
|
||||
document.execCommand('insertHorizontalRule', false, null);
|
||||
break;
|
||||
}
|
||||
debounceSync();
|
||||
wysiwygDirty = true;
|
||||
}
|
||||
|
||||
// WYSIWYG 中包裹选中文本
|
||||
@ -273,18 +266,13 @@
|
||||
if (!sel.rangeCount) return;
|
||||
var range = sel.getRangeAt(0);
|
||||
var text = range.toString();
|
||||
if (!text) {
|
||||
text = 'code';
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(before + text + after));
|
||||
} else {
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(before + text + after));
|
||||
}
|
||||
debounceSync();
|
||||
if (!text) text = 'code';
|
||||
range.deleteContents();
|
||||
range.insertNode(document.createTextNode(before + text + after));
|
||||
wysiwygDirty = true;
|
||||
}
|
||||
|
||||
// 源码模式下插入 markdown 语法
|
||||
// 源码/分屏模式下插入 markdown 语法
|
||||
function execSourceAction(action) {
|
||||
var start = bodyEl.selectionStart;
|
||||
var end = bodyEl.selectionEnd;
|
||||
@ -311,7 +299,10 @@
|
||||
bodyEl.value = bodyEl.value.substring(0, start) + replacement + bodyEl.value.substring(end);
|
||||
bodyEl.focus();
|
||||
var cursor = start + wrap.b.length + (text ? text.length : 0);
|
||||
bodyEl.setSelectionRange(cursor, cursor + (text ? wrap.a.length : 0));
|
||||
bodyEl.setSelectionRange(cursor, cursor + (text ? 0 : replacement.length - wrap.b.length - wrap.a.length));
|
||||
|
||||
// 分屏模式下立刻更新预览
|
||||
if (currentMode === 'split') updateSplitPreview();
|
||||
}
|
||||
}
|
||||
|
||||
@ -320,11 +311,10 @@
|
||||
form.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// WYSIWYG 模式下先同步
|
||||
// WYSIWYG 模式下先同步到 textarea
|
||||
if (currentMode === 'wysiwyg') {
|
||||
var md = htmlToMarkdown(wysiwygEl.innerHTML);
|
||||
bodyEl.value = md;
|
||||
currentMode = 'source';
|
||||
}
|
||||
|
||||
var title = titleEl.value.trim();
|
||||
@ -364,6 +354,7 @@
|
||||
|
||||
// ==================== INIT ====================
|
||||
if (bodyEl.value.trim()) {
|
||||
switchMode('wysiwyg');
|
||||
// 如果已有内容(编辑帖子),渲染到 WYSIWYG 但不写入 textarea
|
||||
syncMarkdownToWysiwyg();
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user