diff --git a/assets/css/comments.css b/assets/css/comments.css
index 4725280..5c8ef99 100644
--- a/assets/css/comments.css
+++ b/assets/css/comments.css
@@ -925,6 +925,45 @@
background: none;
}
+/* 3.2.25 所见即所得编辑区:顶替原 textarea 的可视位置。
+ * 边框 / 底色 / 聚焦态都挂在 .comment-textarea 容器上(focus-within),
+ * 编辑区本体只管排版 —— 整框仍是"一个编辑器"的观感。 */
+.k-main .details .comments .comment-respond .comment-textarea .comment-editor,
+.comment-textarea .comment-editor {
+ min-height: 136px;
+ max-height: 420px;
+ overflow-y: auto;
+ padding: 12px 14px;
+ border: 0;
+ background: none;
+ color: #2b3038;
+ font-family: inherit;
+ font-size: 14px;
+ line-height: 1.5;
+ word-break: break-word;
+ outline: none;
+ cursor: text;
+ -webkit-appearance: none;
+}
+
+.comment-editor:focus {
+ outline: none;
+}
+
+/* 占位符:编辑区为空时显示(:empty 由 input 监听保真空) */
+.comment-editor:empty::before {
+ content: attr(data-placeholder);
+ color: var(--yq-text-3, #6b7280);
+ pointer-events: none;
+}
+
+/* 编辑区里的表情图:跟正文字号走,基线对齐 */
+.comment-editor .yang-editor-emoji {
+ height: 1.25em;
+ vertical-align: -0.25em;
+ margin: 0 1px;
+}
+
/* 工具条:加粗 / 斜体 / 字体颜色 / 表情。
*
* 位于输入框上方,与 textarea 同在 .comment-textarea 这个带边框的
diff --git a/assets/js/comments.js b/assets/js/comments.js
index 4895de2..cd60c80 100644
--- a/assets/js/comments.js
+++ b/assets/js/comments.js
@@ -67,7 +67,7 @@
}
/* ── 提交态开关 ────────────────────────────────────────────
- * 只改按钮和 textarea 的可交互性,不动表单数据。 */
+ * 只改按钮和编辑区的可交互性,不动表单数据。 */
function setSubmitting (on) {
submitting = on
if (submitBtn) {
@@ -75,6 +75,7 @@
submitBtn.classList[on ? 'add' : 'remove']('is-loading')
submitBtn.textContent = on ? (I18N.submitting || '提交中…') : (I18N.submit || '提交评论')
}
+ if (editor) editor.setAttribute('contenteditable', on ? 'false' : 'true')
if (textarea) textarea.readOnly = on
}
@@ -126,8 +127,8 @@
if (parentInput) parentInput.value = String(commentId)
// 聚焦输入框并滚过去
- if (textarea) {
- textarea.focus()
+ if (editor) {
+ editor.focus()
try {
var top = respondBox.getBoundingClientRect().top + window.pageYOffset - 90
window.scrollTo({ top: top, behavior: 'smooth' })
@@ -176,49 +177,45 @@
}
})
- /* ── 富文本工具条(3.2.24)─────────────────────────────────
- * 评论内容仍是纯文本存储;工具条只在光标/选区处插入受控标签:
- * B → … I → …
- * 颜色 → …
- * 后端由 theme-comments.php 的 kses 白名单放行这三种标签
- * (style 属性再经 WP 核心 safecss_filter_attr 消毒,只留安全属性)。
+ /* ── 所见即所得编辑器(3.2.25)─────────────────────────────
+ * 3.2.24 的做法是往 textarea 里插 这类源码:用户看到的
+ * 是代码不是格式;光标点错位置再插表情,代码就被劈成两半 ——
+ * 3.2.25 用户明确要求改掉。
*
- * 选区为空时插入占位文本并**选中**它 —— 用户直接打字就覆盖占位,
- * 这是各家编辑器工具条的通行做法;不选中的话占位文本会留在正文里。
+ * 现在:#yang-editor 是 contentEditable,格式所见即所得;
+ * 提交前由 serializeEditor() 把编辑区内容序列化成**后端白名单
+ * HTML**(theme-comments.php 的 kses 白名单 + safecss_filter_attr
+ * 消毒保持不变):
+ * strong / em / span[style color] 保留
+ * 表情图
转回 :code: 文本(存储不变)
+ * 其余一切标签 只保留文字(粘贴垃圾进不来)
*/
- function wrapSelection (before, after, placeholder) {
- if (!textarea) return
- textarea.focus()
- var s = textarea.selectionStart || 0
- var e = textarea.selectionEnd || 0
- var v = textarea.value
- var sel = v.slice(s, e)
- var usedPlaceholder = false
- if (sel === '') {
- sel = placeholder || ''
- usedPlaceholder = true
- }
- textarea.value = v.slice(0, s) + before + sel + after + v.slice(e)
- // 选中被包裹的内容(或占位文本),光标落回包裹体内
- var a = s + before.length
- var b = a + sel.length
- try { textarea.setSelectionRange(a, b) } catch (ex) { /* 老 IE 忽略 */ }
- return usedPlaceholder
+ var editor = $('#yang-editor', form)
+
+ /* 工具按钮用 mousedown 抢焦点会弄丢编辑区选区 —— 一律挡掉,
+ * 这样「选中一段字 → 点 B」的选区在 click 到来时还在。
+ * ⚠️ 面板内部的元素(色板、表情链接)也要挡:3.2.25 本地探针实测,
+ * 点色板时焦点被抢走,foreColor 落在塌陷光标上,颜色功能整个失效。 */
+ $$('.comment-tool-btn, .comment-color-swatch, .comment-color-panel, .comment-smile-panel, .comment-smile-panel a', form)
+ .forEach(function (b) {
+ b.addEventListener('mousedown', function (ev) { ev.preventDefault() })
+ })
+
+ function exec (cmd, val) {
+ if (!editor) return
+ editor.focus()
+ try { document.execCommand(cmd, false, val || null) } catch (ex) { /* 忽略 */ }
}
- // B / I:按钮上带 data-wrap-before / data-wrap-after,加新按钮不用改 JS
- $$('.comment-tool-btn[data-wrap-before]', form).forEach(function (btn) {
- btn.addEventListener('click', function () {
- wrapSelection(
- btn.getAttribute('data-wrap-before') || '',
- btn.getAttribute('data-wrap-after') || '',
- I18N.boldPlaceholder || '加粗文字'
- )
- })
- })
+ // B / I:浏览器原生 execCommand,Ctrl+B / Ctrl+I 在编辑区内天然可用
+ var boldBtn = $('#yang-tool-bold', form)
+ var italicBtn = $('#yang-tool-italic', form)
+ if (boldBtn) boldBtn.addEventListener('click', function () { exec('bold') })
+ if (italicBtn) italicBtn.addEventListener('click', function () { exec('italic') })
- /* 字体颜色面板:开合 + 色板点击包裹选区。
- * 与表情面板同款交互:点按钮开/关,点外面关,选中后关。 */
+ /* 字体颜色面板:开合逻辑与表情面板同款(点按钮开/关,点外面关)。
+ * styleWithCSS(true) 让 foreColor 产出 ,
+ * 而不是废弃的 —— 正好对齐后端白名单。 */
var colorBtn = $('#yang-tool-color', form)
var colorPanel = $('#yang-color-panel', form)
if (colorBtn && colorPanel) {
@@ -240,16 +237,17 @@
var c = sw.getAttribute('data-color') || '#000000'
// 色值只允许 #rrggbb,面板里写死的,这里再挡一道
if (!/^#[0-9a-fA-F]{6}$/.test(c)) return
- wrapSelection('', '', I18N.colorPlaceholder || '彩色文字')
+ exec('styleWithCSS', true)
+ exec('foreColor', c)
colorPanel.hidden = true
colorBtn.setAttribute('aria-expanded', 'false')
})
}
/* ── 表情面板 ──────────────────────────────────────────────
- * 点表情按钮 → 把对应代码插到光标处(不是简单追加到末尾)。
- * 旧实现是 `myField.value += tag`,光标本在中间也会跑到最后,
- * 用户想在某句话后面插表情就得重新点光标。 */
+ * 插入**可见的表情图片**(img 带 data-code 记住代码),
+ * 不再插 :smile: 这类源码 —— 那正是 3.2.25 要改掉的行为。
+ * 提交时 serializeEditor() 把图片转回代码,存储格式与旧评论兼容。 */
var smileBtn = $('#yang-smile-btn')
var smilePanel = $('#yang-smile-panel')
if (smileBtn && smilePanel) {
@@ -273,26 +271,77 @@
var m = href.match(/grin\('([^']+)'\)/)
if (!m) return
ev.preventDefault()
- insertAtCursor(textarea, ' ' + m[1] + ' ')
+ var img = a.querySelector('img')
+ var src = img ? (img.getAttribute('src') || '') : ''
+ if (src) {
+ exec('insertHTML', '
')
+ } else {
+ exec('insertText', ' ' + m[1] + ' ')
+ }
smilePanel.hidden = true
smileBtn.setAttribute('aria-expanded', 'false')
})
}
- function insertAtCursor (el, text) {
- if (!el) return
- el.focus()
- // 现代浏览器
- if (typeof el.setSelectionRange === 'function') {
- var s = el.selectionStart || 0
- var e = el.selectionEnd || 0
- var v = el.value
- el.value = v.slice(0, s) + text + v.slice(e)
- var pos = s + text.length
- el.setSelectionRange(pos, pos)
- } else {
- el.value += text
+ /* 编辑区被清空后常残留
/
,压成真空,
+ * 否则 :empty 占位符不显示。 */
+ editor.addEventListener('input', function () {
+ var h = editor.innerHTML
+ if (h === '
' || h === '
' || h === '
') {
+ editor.innerHTML = ''
}
+ })
+
+ /* 粘贴只收纯文本:富文本粘贴会把整篇网页的样式标签搬进来,
+ * 序列化时虽然只剩文字,但先按纯文本插入可以少绕一圈。 */
+ editor.addEventListener('paste', function (ev) {
+ ev.preventDefault()
+ var t = (ev.clipboardData || window.clipboardData).getData('text/plain') || ''
+ exec('insertText', t)
+ })
+
+ /* 序列化:编辑区 DOM → 后端白名单 HTML。
+ * 白名单在这里再收一次(不只依赖 kses):提交的是我们生成的串,
+ * 自己保证干净,比"交上去让后端删"更稳。 */
+ function escHtml (s) {
+ return s.replace(/&/g, '&').replace(//g, '>')
+ }
+ function serializeEditor () {
+ if (!editor) return ''
+ var clone = editor.cloneNode(true)
+ // 表情图 → :code: 文本(前后带空格,和旧版纯文本行为一致)
+ $$('img[data-code]', clone).forEach(function (im) {
+ im.parentNode.replaceChild(
+ document.createTextNode(' ' + (im.getAttribute('data-code') || '') + ' '),
+ im)
+ })
+ function walk (n) {
+ var out = ''
+ for (var i = 0; i < n.childNodes.length; i++) {
+ var c = n.childNodes[i]
+ if (c.nodeType === 3) { out += escHtml(c.nodeValue); continue }
+ if (c.nodeType !== 1) continue
+ var tag = c.tagName.toLowerCase()
+ if (tag === 'br') { out += '\n'; continue }
+ var inner = walk(c)
+ /* 颜色不一定长在 span 上:foreColor 作用在斜体/加粗选区时,
+ * 产出的是 /
+ * (3.2.25 本地探针实测)。所以任何允许标签都检查自身 color,
+ * 有就包一层 span 提交 —— 后端白名单只认 span[style]。 */
+ var col = ((c.style && c.style.color) || (c.getAttribute && c.getAttribute('color')) || '').trim()
+ var colSpan = /^(#[0-9a-fA-F]{3}|#[0-9a-fA-F]{6}|rgb\()/.test(col)
+ ? '' + inner + ''
+ : inner
+ if (tag === 'strong' || tag === 'b') { out += '' + colSpan + '' }
+ else if (tag === 'em' || tag === 'i') { out += '' + colSpan + '' }
+ else if (tag === 'span' || tag === 'font') { out += colSpan }
+ else if (tag === 'div' || tag === 'p') { out += inner + '\n' }
+ else { out += inner }
+ }
+ return out
+ }
+ return walk(clone).replace(/\n{3,}/g, '\n\n').trim()
}
/* ── 提交 ──────────────────────────────────────────────────
@@ -304,10 +353,16 @@
if (submitting) return // 双击保护
- // 本地前置校验:空内容不发请求(省一次往返,也避免后端 500)
- if (textarea && textarea.value.trim() === '') {
+ // 编辑区内容序列化进真实提交字段(隐藏的 #comment)
+ var content = serializeEditor()
+ if (textarea) textarea.value = content
+
+ // 本地前置校验:空内容不发请求(省一次往返,也避免后端 500)。
+ // 「空」= 没有文字也没有表情图(只点了个表情也允许提交)。
+ var hasEmoji = editor ? !!editor.querySelector('img[data-code]') : false
+ if (content === '' && !hasEmoji) {
setStatus(I18N.emptyComment || '评论内容不能为空。', 'err')
- textarea.focus()
+ if (editor) editor.focus()
return
}
@@ -352,6 +407,7 @@
insertComment(data)
// 清空输入(保留昵称邮箱)
+ if (editor) editor.innerHTML = ''
if (textarea) textarea.value = ''
/* 顺序很重要:exitReplyMode() 内部最后会 setStatus('','')
diff --git a/comments.php b/comments.php
index 7d797c3..935d8d2 100644
--- a/comments.php
+++ b/comments.php
@@ -241,13 +241,11 @@ if ($yang_per_page <= 0) {
+ hidden aria-hidden="true" tabindex="-1">
+
+ 这类源码。真实内容提交前由
+ comments.js 序列化进上面隐藏的 #comment。
+ required 不能放隐藏 textarea 上(display:none 的
+ 控件无法聚焦,原生校验会报错),空值检查在 JS 做。 */ ?>
+