From fb12d58067bafc05d4a02fe088681e21cae15a93 Mon Sep 17 00:00:00 2001 From: laoyang Date: Sat, 24 Oct 2026 22:38:10 +0800 Subject: [PATCH] =?UTF-8?q?3.2.25:=20=E8=AF=84=E8=AE=BA=E6=A1=86=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E6=89=80=E8=A7=81=E5=8D=B3=E6=89=80=E5=BE=97=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=EF=BC=88B/I/=E9=A2=9C=E8=89=B2/=E8=A1=A8?= =?UTF-8?q?=E6=83=85=E5=8F=AF=E8=A7=86=E5=8C=96+=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E7=99=BD=E5=90=8D=E5=8D=95+=E7=BA=AF=E6=96=87?= =?UTF-8?q?=E6=9C=AC=E7=B2=98=E8=B4=B4=EF=BC=89=EF=BC=9B=E5=90=8E=E5=8F=B0?= =?UTF-8?q?=E8=B5=84=E6=BA=90=20enqueue=20=E7=89=88=E6=9C=AC=E5=8F=B7?= =?UTF-8?q?=E6=94=B9=E8=B7=9F=20THEME=5FVERSION=EF=BC=88=E4=BF=AE=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E9=A1=B5=E9=87=8D=E6=8E=92=E8=A2=AB=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E7=BC=93=E5=AD=98=E6=8C=A1=E4=BD=8F=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/css/comments.css | 39 ++++ assets/js/comments.js | 180 ++++++++++++------ comments.php | 15 +- .../class-options-framework-admin.php | 7 +- style.css | 2 +- 5 files changed, 174 insertions(+), 69 deletions(-) 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', ' ' + 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 + /* 编辑区被清空后常残留
/

,压成真空, + * 否则 :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 做。 */ ?> +
diff --git a/inc/options-framework/includes/class-options-framework-admin.php b/inc/options-framework/includes/class-options-framework-admin.php index 88c3f90..07f89dc 100644 --- a/inc/options-framework/includes/class-options-framework-admin.php +++ b/inc/options-framework/includes/class-options-framework-admin.php @@ -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 diff --git a/style.css b/style.css index 4477d63..96497e3 100644 --- a/style.css +++ b/style.css @@ -1,7 +1,7 @@ /* Theme Name: Kratos_Yang Text Domain: kratos_Yang -Version: 3.2.24 +Version: 3.2.25 Requires PHP: 7.0 Description: 这是一个Kratos主题的分支版本,用来增加一些定制化功能。 Tags: 博客, 双栏, 响应式, 自定义背景, 自定义颜色, 自定义图标, 自定义菜单, 特色图像, 收录优化, 邮件中心, 主题选项, 小工具, 文章置顶, 无障碍友好