fix(widgets): 分类挂件改成「点开才占一行」的下拉面板 #24
需求改判:3.2.54 做的「父级常显 + 子级折叠树」被否,用户要的是 「不点只有一行,点开把所有一级分类拉出来」的真下拉面板。 ## 改了什么 - widget() 重写:`<details class="yang-cats-drop">` + `<summary>` 触发条 + `position:absolute` 浮层面板。收起时整个挂件只占一行(40.5px), 展开时面板盖在下方内容上,不把后面的挂件推走。 - children 兜底值 true → false。线上实例的配置里本来没有这个键, 所以走 pick() 兜底 ⇒ 改兜底就等于改线上行为,用户零操作即变成 「只列一级分类」,不用进后台勾一次。 - 子级不再嵌套 <details>(3.2.54 是嵌套)。现在父级与子级的 DOM 结构完全一致,3.2.54 那个「space-between 推不动、数字右边界差 7.1px」的缺陷从结构上消失了,不需要 flex:1 补丁。 - 补一段渐进增强脚本,只做「点外部收起 / Esc 收起并还焦点」两件事, 不接管开合本身(关掉 JS 仍能正常展开跳转)。 - CSS:.w-cats 区块整块重写。修掉子级 li 右内边距叠加的缺陷 —— 子级 ul 嵌在已 padding:0 12px 的父级 li 里,子级 li 又写了一遍 右内边距 ⇒ 叠加成 24px,导致子级数字比父级多缩进 12px、父子排不成 一列,竖着比大小时会错位。改成 0 后父子数字同列。 - style.css 版本号 3.2.54 → 3.2.55(3.2.54 已落过 update.json, 同号重发会让 WP 的 version_compare 判「没有新版本」)。 ## 验证 - 端到端(线上真实 WordPress,96 项):全通过 - 反证:内容判据 12 组 + 端到端 3 组,全部真红,无一组空跑 - 视觉几何(真实浏览器 50 项):全通过 - 上一版(3.2.53)老代码跑同一套端到端判据:88 项失败 - 包内 PHP lint 96 个全过;zip 无 1980 时间戳(防 OPcache 误判) ## 发布 只写 theme-update/:zip + readme + update.json → 3.2.55。 线上主题目录仍是 3.2.54,等用户在后台点升级(红线)。
This commit is contained in:
+146
-53
@@ -1101,15 +1101,28 @@ class widget_posts extends WP_Widget
|
||||
* 沿用 `categories` 则 option 名不变,存量实例继续有效。
|
||||
* 这也意味着 update() **必须容忍核心那套老字段**(dropdown 等),见下方说明。
|
||||
*
|
||||
* ── 「父级常显 + 子级可折叠」的呈现(用户 2026-10-27 选定)──────────────
|
||||
* 本站实测 29 个分类(9 父 + 20 子)。全展开侧栏要拉长约 1100px,
|
||||
* 所以默认只列父级,有子级的父级带一个折叠箭头,点开才展开。
|
||||
* 🔴 折叠用 `<details>/<summary>` 原生实现,**不写一行 JS**:
|
||||
* ── 呈现形态:收起一行 + 点击弹出面板(3.2.55 按用户反馈改判)────────────
|
||||
* 3.2.54 先做成「父级常显 + 子级折叠树」,8 个父级铺开占约 300px 侧栏高度。
|
||||
* 用户否决:「不点击的话就一行,这样占用的空间小,点击后把所有的一级分类
|
||||
* 拉出来,类似于之前那样(指原生的 <select>),但样式要好看」。
|
||||
*
|
||||
* 所以现在是**真下拉面板**:
|
||||
* 收起 → 一行触发条(当前分类名,不在分类页时显示「选择分类」+ 箭头)
|
||||
* 展开 → 浮层面板,`position:absolute` 贴在触发条下方,**不推开下面的挂件**
|
||||
* 内容 → 默认只列一级分类(本站 9 个);子分类是可选项(默认关)
|
||||
*
|
||||
* 🔴 仍然用 `<details>/<summary>` 做开合,**核心逻辑不写 JS**:
|
||||
* - 不依赖 jQuery/Bootstrap 版本,不会跟主题其它脚本抢事件;
|
||||
* - 键盘可操作、读屏软件能正确朗读,自带 `aria-expanded` 语义;
|
||||
* - 关掉 JS 也能用(分类导航属于基础功能,不该依赖脚本)。
|
||||
* `<summary>` 里同时放"父级链接 + 箭头按钮"会有嵌套点击问题,
|
||||
* 所以箭头用 `::marker` 之外的独立 span,父级链接本身仍是普通 `<a>`。
|
||||
* - 键盘可操作、读屏软件能正确朗读,自带展开/收起语义;
|
||||
* - 关掉 JS 也能展开、也能点分类跳转(分类导航是基础功能)。
|
||||
* JS 只做两件 `<details>` 原生没有的事:**点面板外部关闭、按 Esc 关闭**。
|
||||
* 这段是渐进增强:脚本没跑,功能不缺,只是要再点一次触发条才收起来。
|
||||
*
|
||||
* 🔴 为什么面板能做绝对定位浮层而不被裁掉(实测过,不是猜的):
|
||||
* 链路 `.k-main .sidebar .widget` → `.w-cats` → 面板,`sidebar` 与 `widget`
|
||||
* 都**没有** `overflow` 声明(`body` 只有 `overflow-x:hidden`,不影响纵向),
|
||||
* 所以面板可以溢出挂件边界。若将来谁给 `.widget` 加了 `overflow:hidden`,
|
||||
* 面板会被整块裁掉 —— 这条写在这里就是给那时候看的。
|
||||
*
|
||||
* ── 父级数字为什么要 pad_counts(这条最容易做错)────────────────────────
|
||||
* 核心 `wp_list_categories()` 在 `show_count && hierarchical` 时**自动**打开
|
||||
@@ -1152,7 +1165,7 @@ class widget_categories extends WP_Widget
|
||||
* 名字走第 2 参(工单 #23,理由见 widget_ad 的注释)。 */
|
||||
$widget_ops = array(
|
||||
'classname' => 'widget_categories',
|
||||
'description' => __('按父子层级展示文章分类,父级常显、子级可折叠', 'kratos'),
|
||||
'description' => __('以「点击展开」的下拉面板展示文章分类,收起时只占一行', 'kratos'),
|
||||
);
|
||||
|
||||
parent::__construct('categories', __('文章分类', 'kratos'), $widget_ops);
|
||||
@@ -1332,7 +1345,15 @@ class widget_categories extends WP_Widget
|
||||
{
|
||||
$show_count = $this->pick($instance, 'count', true);
|
||||
$hide_empty = $this->pick($instance, 'hide_empty', true);
|
||||
$show_children = $this->pick($instance, 'children', true);
|
||||
/* 🔴 children 默认值 3.2.55 改成 **false**(3.2.54 是 true)。
|
||||
*
|
||||
* 用户的诉求原话是「点击后把所有**一级**分类拉出来」—— 默认就该
|
||||
* 只有一级。线上那个实例 `categories-3` 是核心老配置,压根没有
|
||||
* children 这个键 ⇒ 走 fallback ⇒ 改默认值就等于改线上呈现,
|
||||
* 不需要用户去后台勾一下,也不用动数据库。
|
||||
*
|
||||
* 想连子级一起看的人,在后台勾上即可(update() 会存成 1)。 */
|
||||
$show_children = $this->pick($instance, 'children', false);
|
||||
$orderby = $this->pick($instance, 'orderby', 'name');
|
||||
$order = $this->pick($instance, 'order', 'ASC');
|
||||
|
||||
@@ -1379,7 +1400,57 @@ class widget_categories extends WP_Widget
|
||||
echo '<div class="' . esc_attr(yang_widget_class('w-cats')) . '">';
|
||||
yang_widget_title($args, $instance, __('文章分类', 'kratos'), $this->id_base);
|
||||
|
||||
echo '<ul class="yang-cat-list">';
|
||||
/* 🔴 当前分类名要显示在触发条上,让收起状态也能看出"我人在哪个分类"。
|
||||
*
|
||||
* 这里必须先把它找出来(而不是边遍历边拼),因为触发条**在面板之前**
|
||||
* 输出 —— 面板内容才能边遍历边 echo。
|
||||
*
|
||||
* 只在 is_category() 时取,空串表示"不在分类页" ⇒ 触发条显示占位文案。
|
||||
* 父级分类被访问时,$current_id 就是那个父级(不是它的子级),
|
||||
* 所以不必再往祖先链上找。 */
|
||||
$current_name = '';
|
||||
if ($current_id > 0) {
|
||||
foreach ($tree as $node) {
|
||||
if ((int) $node['cat']->term_id === $current_id) {
|
||||
$current_name = (string) $node['cat']->name;
|
||||
break;
|
||||
}
|
||||
foreach ($node['children'] as $kid) {
|
||||
if ((int) $kid->term_id === $current_id) {
|
||||
$current_name = (string) $kid->name;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 面板 DOM id:一页上可能有多个实例(虽然实际很少),必须唯一,
|
||||
* 否则 `<summary>` 里若要写 aria-controls 会指到第一个。
|
||||
* 用 yang_widget_uid() 拿 `widget_categories-3` 这类稳定串。 */
|
||||
$panel_id = yang_widget_uid($this) . '-panel';
|
||||
|
||||
echo '<details class="yang-cats-drop">';
|
||||
|
||||
/* 🔴 触发条用 <summary>,**里面不放 <a>**。
|
||||
*
|
||||
* 3.2.54 那版把父级链接塞进 <summary> 里靠"<a> 是 interactive
|
||||
* content 所以不触发展开"来分工(实测确实可行)。但这一版
|
||||
* <summary> 的职责变了:它整条就是"开合开关",不该再带导航。
|
||||
* 把 <a> 放进来只会让"点空白=开合、点文字=跳转"这种
|
||||
* 半吊子分工回来,反而更难用。
|
||||
*
|
||||
* 所以这里纯文本 + 箭头,箭头用 CSS 三角(不依赖字体图标)。 */
|
||||
echo '<summary class="yang-cats-trigger">';
|
||||
echo '<span class="yang-cats-label">'
|
||||
. ($current_name !== ''
|
||||
? $this->name_html($current_name)
|
||||
: esc_html__('选择分类', 'kratos'))
|
||||
. '</span>';
|
||||
echo '<span class="yang-cats-caret" aria-hidden="true"></span>';
|
||||
echo '</summary>';
|
||||
|
||||
echo '<div class="yang-cats-panel" id="' . esc_attr($panel_id) . '">';
|
||||
echo '<ul class="yang-cats-list">';
|
||||
|
||||
foreach ($tree as $node) {
|
||||
$root = $node['cat'];
|
||||
@@ -1387,7 +1458,7 @@ class widget_categories extends WP_Widget
|
||||
$kids = $node['children'];
|
||||
$has_kids = ($show_children && count($kids) > 0);
|
||||
|
||||
$root_cls = 'yang-cat-item';
|
||||
$root_cls = 'yang-cats-item';
|
||||
if ($has_kids) {
|
||||
$root_cls .= ' has-children';
|
||||
}
|
||||
@@ -1400,58 +1471,36 @@ class widget_categories extends WP_Widget
|
||||
}
|
||||
|
||||
echo '<li class="' . esc_attr($root_cls) . '">';
|
||||
echo '<a class="yang-cats-link" href="' . esc_url(get_category_link($rid)) . '">'
|
||||
. $this->name_html($root->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cats-count">' . (int) $root->count . '</span>';
|
||||
}
|
||||
|
||||
/* 子分类:只有用户显式勾了「显示子分类」才输出。
|
||||
*
|
||||
* 🔴 这里**不再用 <details> 嵌套**(3.2.54 是那样)。原因:
|
||||
* 面板本身就是浮层,展开状态只由触发条一个开关管;
|
||||
* 再套一层折叠,用户点开面板后仍要再点一次才能看见子级,
|
||||
* 「点击后把所有的一级分类拉出来」这个诉求就没满足。
|
||||
* 子级直接平铺在父级下面、缩进显示 —— 一目了然,少一次点击。 */
|
||||
if ($has_kids) {
|
||||
/* 有子级:<details> 原生折叠。
|
||||
*
|
||||
* 🔴 为什么父级链接可以放在 <summary> 里(这不是偷懒):
|
||||
* HTML 规范规定,`<summary>` 的"激活即折叠"行为只在该
|
||||
* 点击目标**不是** interactive content 时才触发,而 `<a>`
|
||||
* 本身属于 interactive content。所以点名字是**纯导航**、
|
||||
* 不会顺带展开;点箭头/数字/行内空白才折叠。
|
||||
* 两个动作各归各的,不需要一行 JS。
|
||||
* (若哪天有人把 `<a>` 换成 `<span onclick>`,这条就失效了,
|
||||
* 所以这里写清楚原因,避免后人改错。)
|
||||
*
|
||||
* 🔴 为什么用 <details> 而不是 JS 手风琴:
|
||||
* · 不依赖 jQuery/Bootstrap 版本,也不跟主题其它脚本抢事件;
|
||||
* · 键盘 Tab + 回车天然可用,读屏软件正确朗读展开状态;
|
||||
* · 关掉 JS 仍能展开(分类导航是基础功能,不该依赖脚本)。 */
|
||||
echo '<details class="yang-cat-group">';
|
||||
echo '<summary class="yang-cat-summary">';
|
||||
echo '<span class="yang-cat-caret" aria-hidden="true"></span>';
|
||||
echo '<a class="yang-cat-link" href="' . esc_url(get_category_link($rid)) . '">'
|
||||
. $this->name_html($root->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cat-count">' . (int) $root->count . '</span>';
|
||||
}
|
||||
echo '</summary>';
|
||||
|
||||
echo '<ul class="yang-cat-children">';
|
||||
echo '<ul class="yang-cats-children">';
|
||||
foreach ($kids as $kid) {
|
||||
$kid_id = (int) $kid->term_id;
|
||||
$kid_cls = 'yang-cat-item';
|
||||
$kid_cls = 'yang-cats-item';
|
||||
if ($current_id === $kid_id) {
|
||||
$kid_cls .= ' current-cat';
|
||||
}
|
||||
echo '<li class="' . esc_attr($kid_cls) . '">';
|
||||
echo '<a class="yang-cat-link" href="' . esc_url(get_category_link($kid_id)) . '">'
|
||||
echo '<a class="yang-cats-link" href="' . esc_url(get_category_link($kid_id)) . '">'
|
||||
. $this->name_html($kid->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cat-count">' . (int) $kid->count . '</span>';
|
||||
echo '<span class="yang-cats-count">' . (int) $kid->count . '</span>';
|
||||
}
|
||||
echo '</li>';
|
||||
}
|
||||
echo '</ul>';
|
||||
|
||||
echo '</details>';
|
||||
} else {
|
||||
// 无子级:普通一行
|
||||
echo '<a class="yang-cat-link" href="' . esc_url(get_category_link($rid)) . '">'
|
||||
. $this->name_html($root->name) . '</a>';
|
||||
if ($show_count) {
|
||||
echo '<span class="yang-cat-count">' . (int) $root->count . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
echo '</li>';
|
||||
@@ -1459,6 +1508,49 @@ class widget_categories extends WP_Widget
|
||||
|
||||
echo '</ul>';
|
||||
echo '</div>';
|
||||
echo '</details>';
|
||||
echo '</div>';
|
||||
|
||||
/* ── 渐进增强:点面板外部 / 按 Esc 收起 ──────────────────────────
|
||||
*
|
||||
* 🔴 为什么这段必须写在挂件里而不是 kratos.js:
|
||||
* 面板是 `<details>`,只有挂件知道它在哪;写进主脚本就得靠类名
|
||||
* 全局查询,一旦页面上没有这个挂件就是一次空查询 + 一个常驻事件
|
||||
* 监听。挂在挂件输出处,没挂件就没这段代码。
|
||||
*
|
||||
* 🔴 为什么用 `once` 标志:挂件可以有多个实例,每实例输出一段脚本,
|
||||
* 全局 document 上的监听会被装 N 次。用 window 上的标志位保证
|
||||
* 只装一次(监听器内部用 `closest('.w-cats')` 定位到自己)。
|
||||
*
|
||||
* 🔴 为什么不用内联 onclick 属性:CSP 里 `unsafe-inline` 一收紧
|
||||
* 就全废。这里用 addEventListener,属性里只有 data-* 标记。
|
||||
*
|
||||
* 🔴 无 JS 时行为:点触发条展开、点分类跳转都正常,只是
|
||||
* "点外部不自动收起"。功能不缺,所以不写成必要条件。 */
|
||||
echo '<script>'
|
||||
. '(function(){'
|
||||
/* 已经装过一次就直接退出(多实例场景) */
|
||||
. 'if(window.__yangCatsBound){return;}window.__yangCatsBound=1;'
|
||||
/* 点外部关闭:只处理"点到的不是这个 details 内部"的情况 */
|
||||
. 'document.addEventListener("click",function(e){'
|
||||
. 'var t=e.target;'
|
||||
. 'var list=document.querySelectorAll(".yang-cats-drop[open]");'
|
||||
. 'for(var i=0;i<list.length;i++){'
|
||||
. 'if(!list[i].contains(t)){list[i].removeAttribute("open");}'
|
||||
. '}'
|
||||
. '},false);'
|
||||
/* Esc 关闭:键盘用户 */
|
||||
. 'document.addEventListener("keydown",function(e){'
|
||||
. 'if(e.key!=="Escape"&&e.keyCode!==27){return;}'
|
||||
. 'var list=document.querySelectorAll(".yang-cats-drop[open]");'
|
||||
. 'for(var i=0;i<list.length;i++){'
|
||||
. 'list[i].removeAttribute("open");'
|
||||
. 'var s=list[i].querySelector(".yang-cats-trigger");'
|
||||
. 'if(s&&s.focus){s.focus();}'
|
||||
. '}'
|
||||
. '},false);'
|
||||
. '})();'
|
||||
. '</script>';
|
||||
}
|
||||
|
||||
public function update($new_instance, $old_instance)
|
||||
@@ -1501,7 +1593,8 @@ class widget_categories extends WP_Widget
|
||||
$title = isset($instance['title']) ? (string) $instance['title'] : '';
|
||||
$show_count = $this->pick($instance, 'count', true);
|
||||
$hide_empty = $this->pick($instance, 'hide_empty', true);
|
||||
$show_children = $this->pick($instance, 'children', true);
|
||||
// 与 widget() 保持同一默认值(理由见 widget() 里的注释)
|
||||
$show_children = $this->pick($instance, 'children', false);
|
||||
$orderby = $this->pick($instance, 'orderby', 'name');
|
||||
$order = $this->pick($instance, 'order', 'ASC');
|
||||
if ($orderby !== 'count') {
|
||||
@@ -1520,8 +1613,8 @@ class widget_categories extends WP_Widget
|
||||
|
||||
<p>
|
||||
<input class="checkbox" type="checkbox" id="<?php echo esc_attr($this->get_field_id('children')); ?>" name="<?php echo esc_attr($this->get_field_name('children')); ?>" <?php checked($show_children); ?>>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('children')); ?>"><?php _e('显示子分类(父级可展开)', 'kratos'); ?></label>
|
||||
<span class="description"><?php _e('关掉则只列父分类。', 'kratos'); ?></span>
|
||||
<label for="<?php echo esc_attr($this->get_field_id('children')); ?>"><?php _e('展开时同时列出子分类', 'kratos'); ?></label>
|
||||
<span class="description"><?php _e('默认只列一级分类。勾上后,有子分类的父级下面会缩进列出它的子分类。', 'kratos'); ?></span>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
|
||||
Reference in New Issue
Block a user