3.2.25: 评论框改为所见即所得编辑器(B/I/颜色/表情可视化+序列化白名单+纯文本粘贴);后台资源 enqueue 版本号改跟 THEME_VERSION(修设置页重排被浏览器缓存挡住的问题)
This commit is contained in:
@@ -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 这个带边框的
|
||||
|
||||
+118
-62
@@ -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 → <strong>…</strong> I → <em>…</em>
|
||||
* 颜色 → <span style="color:#xxx">…</span>
|
||||
* 后端由 theme-comments.php 的 kses 白名单放行这三种标签
|
||||
* (style 属性再经 WP 核心 safecss_filter_attr 消毒,只留安全属性)。
|
||||
/* ── 所见即所得编辑器(3.2.25)─────────────────────────────
|
||||
* 3.2.24 的做法是往 textarea 里插 <strong> 这类源码:用户看到的
|
||||
* 是代码不是格式;光标点错位置再插表情,代码就被劈成两半 ——
|
||||
* 3.2.25 用户明确要求改掉。
|
||||
*
|
||||
* 选区为空时插入占位文本并**选中**它 —— 用户直接打字就覆盖占位,
|
||||
* 这是各家编辑器工具条的通行做法;不选中的话占位文本会留在正文里。
|
||||
* 现在:#yang-editor 是 contentEditable,格式所见即所得;
|
||||
* 提交前由 serializeEditor() 把编辑区内容序列化成**后端白名单
|
||||
* HTML**(theme-comments.php 的 kses 白名单 + safecss_filter_attr
|
||||
* 消毒保持不变):
|
||||
* strong / em / span[style color] 保留
|
||||
* 表情图 <img data-code> 转回 :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)
|
||||
|
||||
// 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 || '加粗文字'
|
||||
)
|
||||
})
|
||||
/* 工具按钮用 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:浏览器原生 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 产出 <span style="color">,
|
||||
* 而不是废弃的 <font color> —— 正好对齐后端白名单。 */
|
||||
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('<span style="color:' + c + '">', '</span>', 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', ' <img class="yang-editor-emoji" src="' + src +
|
||||
'" data-code="' + m[1] + '" alt="' + m[1] + '"> ')
|
||||
} 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
|
||||
/* 编辑区被清空后常残留 <br>/<div><br></div>,压成真空,
|
||||
* 否则 :empty 占位符不显示。 */
|
||||
editor.addEventListener('input', function () {
|
||||
var h = editor.innerHTML
|
||||
if (h === '<br>' || h === '<div><br></div>' || h === '<div><br></div><br>') {
|
||||
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, '<').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 作用在斜体/加粗选区时,
|
||||
* 产出的是 <i style="color:..."> / <b style="color:...">
|
||||
* (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)
|
||||
? '<span style="color:' + col + '">' + inner + '</span>'
|
||||
: inner
|
||||
if (tag === 'strong' || tag === 'b') { out += '<strong>' + colSpan + '</strong>' }
|
||||
else if (tag === 'em' || tag === 'i') { out += '<em>' + colSpan + '</em>' }
|
||||
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('','')
|
||||
|
||||
+11
-4
@@ -241,13 +241,11 @@ if ($yang_per_page <= 0) {
|
||||
<div class="comment-toolbar" role="toolbar"
|
||||
aria-label="<?php esc_attr_e('评论格式工具条', 'kratos'); ?>">
|
||||
<button type="button" class="comment-tool-btn" id="yang-tool-bold"
|
||||
data-wrap-before="<strong>" data-wrap-after="</strong>"
|
||||
title="<?php esc_attr_e('加粗', 'kratos'); ?>"
|
||||
aria-label="<?php esc_attr_e('加粗', 'kratos'); ?>">
|
||||
<span class="comment-tool-glyph comment-tool-glyph-bold" aria-hidden="true">B</span>
|
||||
</button>
|
||||
<button type="button" class="comment-tool-btn" id="yang-tool-italic"
|
||||
data-wrap-before="<em>" data-wrap-after="</em>"
|
||||
title="<?php esc_attr_e('斜体', 'kratos'); ?>"
|
||||
aria-label="<?php esc_attr_e('斜体', 'kratos'); ?>">
|
||||
<span class="comment-tool-glyph comment-tool-glyph-italic" aria-hidden="true">I</span>
|
||||
@@ -304,8 +302,17 @@ if ($yang_per_page <= 0) {
|
||||
</div>
|
||||
|
||||
<textarea class="comment-input" id="comment" name="comment" rows="6"
|
||||
placeholder="<?php esc_attr_e('说点什么…(支持 :smile: 这类表情代码)', 'kratos'); ?>"
|
||||
required="required" maxlength="65525"></textarea>
|
||||
hidden aria-hidden="true" tabindex="-1"></textarea>
|
||||
|
||||
<?php /* 3.2.25 所见即所得编辑区:格式/表情所见即所得,
|
||||
不再往框里插 <strong> 这类源码。真实内容提交前由
|
||||
comments.js 序列化进上面隐藏的 #comment。
|
||||
required 不能放隐藏 textarea 上(display:none 的
|
||||
控件无法聚焦,原生校验会报错),空值检查在 JS 做。 */ ?>
|
||||
<div class="comment-editor" id="yang-editor" contenteditable="true"
|
||||
role="textbox" aria-multiline="true"
|
||||
aria-label="<?php esc_attr_e('评论内容', 'kratos'); ?>"
|
||||
data-placeholder="<?php esc_attr_e('说点什么…(加粗、颜色、表情点上方按钮)', 'kratos'); ?>"></div>
|
||||
|
||||
<div class="comment-bar">
|
||||
<div class="comment-bar-right">
|
||||
|
||||
@@ -116,7 +116,10 @@ class Options_Framework_Admin {
|
||||
if ( $this->options_screen != $hook )
|
||||
return;
|
||||
|
||||
wp_enqueue_style( 'optionsframework', get_stylesheet_directory_uri() . '/inc/options-framework/css/optionsframework.css', array(), Options_Framework::VERSION );
|
||||
// ⚠️ 版本号必须跟主题版本(THEME_VERSION)走:3.2.24 踩坑——
|
||||
// 原来用框架常量 Options_Framework::VERSION(1.9.1 永不变),主题升级后
|
||||
// 资源 URL 不变,浏览器拿缓存旧 CSS,后台「设置页重排」看起来像没做。
|
||||
wp_enqueue_style( 'optionsframework', get_stylesheet_directory_uri() . '/inc/options-framework/css/optionsframework.css', array(), THEME_VERSION );
|
||||
wp_enqueue_style( 'wp-color-picker' );
|
||||
}
|
||||
|
||||
@@ -135,7 +138,7 @@ class Options_Framework_Admin {
|
||||
'options-custom',
|
||||
get_stylesheet_directory_uri() . '/inc/options-framework/js/options-custom.js',
|
||||
array( 'jquery','wp-color-picker' ),
|
||||
Options_Framework::VERSION
|
||||
THEME_VERSION
|
||||
);
|
||||
|
||||
// Inline scripts from options-interface.php
|
||||
|
||||
Reference in New Issue
Block a user