3.2.54 主题接管「文章分类」挂件:父子折叠树替代核心下拉框(#24)

原来侧栏那个分类挂件是 WP 核心的 WP_Widget_Categories,实例配置
dropdown=1,输出的是 <select> 下拉框。主题 CSS 里那套
`.widget_categories ul li` 只匹配 ul li,下拉框里没有 ul
⇒ 写了但一条都没生效。不是"没写样式",是样式和渲染形态对不上。

做法:沿用核心的 id_base `categories` 直接顶替(不是新建独立挂件)。
- option 名不变,_register_widgets() 照旧认领存量实例 categories-3
- 用户升级后什么都不用做:就地变新样式、位置不变、标题沿用
- 代价:update() 需容忍核心的 dropdown/hierarchical 老字段
  (不读也不显式删,用户重存表单时自然收敛掉)

呈现:父级常显 + 子级可折叠,用 <details>/<summary> 原生实现,零 JS。
父级链接放在 <summary> 里不会误触折叠 —— HTML 规范规定 <summary>
的"激活即折叠"只在点击目标非 interactive content 时触发。

显式 pad_counts:核心是在 show_count && hierarchical 时隐式打开的
(category-template.php:392)。不显式给的话父级数字 220→188,
用户会以为丢了 32 篇文章。

orderby=count 不交给 SQL:WP_Term_Query 翻成 ORDER BY tt.count
(原始值),而 pad_counts 是查询后由 PHP 补的 ⇒ 数字大的排在小的
后面。改成本地取回后按补过的 count 排,并做 strnatcasecmp tie-break。

分类名不 decode:实测 esc_html() 对 & 的输出恒为 &amp;,所以库里
存 & 还是存 &amp;,最终 HTML 字节一致、浏览器显示也一致。decode
是冗余且有多一层绕过风险。(建单时判断错了,此处自我纠正。)

删掉失效的 .widget_categories ul li 整套规则及 @media 里的宽度规则。

样式三个缺陷(线上真实页面量坐标才发现,不是看代码能看出来的):
1. 主题全局 ul>li{margin-bottom:6px;padding-left:2px;list-style:circle}
   (kratos.css:74) 未被重置 ⇒ 每行多 6px,8 行累积 42px 行距发虚
2. 同上 padding-left:2px 只覆盖了 :not(.has-children) ⇒ 带箭头行错位 2px
3. 有子级行的 li 里只有 <details> 一个 flex 子项,space-between 推不动;
   <details> 默认按内容收缩 ⇒ 数字右边界差 7.1px(实测
   [220.5,220.5,227.6,227.6,220.5])

验证:
- 端到端(真实 WP 加载待测副本)77 通过 / 0 失败
- 反证 12 组全部命中预期判据,无一组空跑
- CSS 视觉反向验证 3 组:三处缺陷逐个注回,全部被量出来
- 视觉几何 18 项全绿(行距/胶囊/对齐/留白/悬停/箭头)
- 用 3.2.53 老代码跑同一套判据:11 通过 / 57 失败

过程中修掉 5 条假判据(全在我自己的测试脚本里)。用老代码跑时它
居然"通过 16 条",逐条查全是假绿,根因同一个:否定式断言作用在
空靶上(与 pitfalls #424 同一族)。最典型的一条:
  /@media[^{]*\{[^}]*widget_categories/ 的 [^}]* 遇到块内第一个 }
  就截断 ⇒ 永远匹配不到 ⇒ 恒真。老代码 2354 行明明有那条规则。
改成按花括号配对切块后,老代码从「16 通过/50 失败」变「11/57」。

包 kratos_yang-3.2.54.zip:4783259 字节 / 237 成员 / 0 个 1980 时间戳
readme 45 个版本标题 · update.json → 3.2.54
服务器主题目录仍 3.2.53,未被覆盖
This commit is contained in:
2026-10-27 12:18:53 +08:00
parent 75848f2ba2
commit 4c1d67b86b
4 changed files with 828 additions and 10 deletions
+499
View File
@@ -27,6 +27,19 @@ function widgets_init()
'WP_Widget_Search',
'WP_Widget_Tag_Cloud',
'WP_Nav_Menu_Widget',
/* 3.2.54 / 工单 #24:分类挂件由主题接管(widget_categories,
* id_base 沿用 'categories',理由见该类的注释)。
* 🔴 为什么必须在这里注销核心的那个:
* `WP_Widget_Factory::register()` 是按**类名**存的,不是按 id_base。
* 只 register 自己的类,核心 `WP_Widget_Categories` 仍在工厂里,
* 后台「可用挂件」列表会同时出现「分类」和「文章分类」两项 ——
* 用户根本分不清该拖哪个。注销掉核心的,列表里只剩一个。
* 注销不影响存量实例:`_register_widgets()` 是按 id_base 读
* option `widget_categories`,本类 id_base 相同,照样认领全部实例。
* 执行顺序没问题:本函数挂在 `widgets_init` 优先级 10,而工厂的
* `_register_widgets` 挂在优先级 100(wp-includes/widgets.php),
* 所以「先注销 + 再注册」一定在真正注册实例之前完成。 */
'WP_Widget_Categories',
);
foreach ($wp_widget as $wp_widget) {
@@ -1061,6 +1074,491 @@ class widget_posts extends WP_Widget
}
}
/* ==========================================================================
* 「文章分类」挂件(3.2.54 · 工单 #24)
*
* ── 为什么这次是「接管」而不是「另起一个」 ──────────────────────────────
* WP 核心自带的 `WP_Widget_Categories` 本来就在跑,主题从没管过它。
* 线上侧栏那个实例是 `categories-3`,配置里 **`dropdown` = 1**:
*
* array( 'title' => '文章分类', 'count' => 1, 'hierarchical' => 1, 'dropdown' => 1 )
*
* 也就是说访客实际看到的是一个 `<select>` 下拉框(`wp_dropdown_categories`),
* 不是列表。主题 CSS 里那套 `.widget_categories ul li`(kratos.css 2293 行起)
* 因此**一条都没生效** —— 它只匹配 `ul li`,下拉框里根本没有 `ul`。
* 「丑」的直接原因就在这里:不是样式没写,是**样式和渲染形态对不上**。
*
* 所以本挂件用核心的同一个 id_base `categories` 顶替它(主题的
* `widgets_init` 在核心 `init` 之后跑,`register_widget` 覆盖同 id_base 的项)。
* 好处是**用户升级后什么都不同做**:
* · 侧栏里那个 `categories-3` 实例直接变成新样式,位置不变;
* · 已存的 `title` = 「文章分类」原样沿用,不用重新填;
* · 不存在"旧的还在旁边、要手动删掉"的迁移步骤。
*
* 🔴 为什么 id_base 必须**沿用** `categories` 而不是换新名字:
* 换新 id_base 会得到新的 option 名(`widget_新名字`),旧实例 `categories-3`
* 的配置留在 `widget_categories` 里没人读 —— 用户看到的是"挂件凭空消失"。
* 沿用 `categories` 则 option 名不变,存量实例继续有效。
* 这也意味着 update() **必须容忍核心那套老字段**(dropdown 等),见下方说明。
*
* ── 「父级常显 + 子级可折叠」的呈现(用户 2026-10-27 选定)──────────────
* 本站实测 29 个分类(9 父 + 20 子)。全展开侧栏要拉长约 1100px,
* 所以默认只列父级,有子级的父级带一个折叠箭头,点开才展开。
* 🔴 折叠用 `<details>/<summary>` 原生实现,**不写一行 JS**:
* - 不依赖 jQuery/Bootstrap 版本,不会跟主题其它脚本抢事件;
* - 键盘可操作、读屏软件能正确朗读,自带 `aria-expanded` 语义;
* - 关掉 JS 也能用(分类导航属于基础功能,不该依赖脚本)。
* `<summary>` 里同时放"父级链接 + 箭头按钮"会有嵌套点击问题,
* 所以箭头用 `::marker` 之外的独立 span,父级链接本身仍是普通 `<a>`。
*
* ── 父级数字为什么要 pad_counts(这条最容易做错)────────────────────────
* 核心 `wp_list_categories()` 在 `show_count && hierarchical` 时**自动**打开
* `pad_counts`(wp-includes/category-template.php:392-393)。实测本站:
*
* 生活记录 原始 count = 188
* 生活记录 带 pad_counts = 220 (188 + 博士6 + 美国22 + 游泳4)
*
* 线上那个下拉框显示的也是 220。如果这里取原始 count,同一个分类会在
* 升级前后显示成两个不同的数字(220 → 188),用户会以为文章丢了。
* 所以这里显式 `'pad_counts' => true`,与核心行为保持一致。
*
* ── 分类名里的 HTML 实体:实测结论是「不用管」(记一次自我纠正)──────
* 线上有一个分类名在库里存的就是 `Ubuntu&amp;Debian`(id=122),
* 不是 `Ubuntu&Debian`。我一开始认定必须 `html_entity_decode()` 一层,
* 理由是"下拉框里会显示成 `Ubuntu&amp;Debian`"。**实测证明这个判断是错的**:
*
* raw = [Ubuntu&amp;Debian]
* decode = [Ubuntu&Debian]
* esc(raw) = [Ubuntu&amp;Debian]
* esc(decode) = [Ubuntu&amp;Debian] ← 两者完全相同
*
* 关键点:`esc_html()` 对 `&` 的输出**恒为** `&amp;`。所以
* · 库里存 `&` → esc_html 变 `&amp;` → 浏览器显示 `&`
* · 库里存 `&amp;` → esc_html 不动它 → 浏览器显示 `&`
* 两种数据经 esc_html 后**字节完全一致**,浏览器里都是 `Ubuntu&Debian`,
* 本来就是正常的。加 decode 不会改变任何输出,只是个看着"更讲究"的冗余。
*
* ⇒ 所以这里**不 decode**,直接用 `esc_html()`。少一层处理 = 少一处
* 可能出错的地方(decode 反而有风险:若将来分类名里有人写了
* `&lt;script&gt;` 这种真·字面量,decode 会把它变成真标签)。
* 这个教训和工单 #23 那次一样:**先实测,再写注释**。
* ====================================================================== */
class widget_categories extends WP_Widget
{
public function __construct()
{
/* id_base 沿用核心的 'categories'(接管,理由见上方),
* 名字走第 2 参(工单 #23,理由见 widget_ad 的注释)。 */
$widget_ops = array(
'classname' => 'widget_categories',
'description' => __('按父子层级展示文章分类,父级常显、子级可折叠', 'kratos'),
);
parent::__construct('categories', __('文章分类', 'kratos'), $widget_ops);
}
/**
* 取实例字段;没填就用默认值。
*
* 🔴 判空一律「trim 后是不是空串」,不用 empty():
* `!empty('0')` 是 false。本挂件里的 `count` 是 0/1 开关、
* 标题也可能就叫「0」,用 empty() 会被当"没填"。
* 与 widget_about::pick() / widget_recent_comments::pick() 同一套判据。
*
* @param array $instance
* @param string $key
* @param mixed $fallback
* @return mixed
*/
private function pick($instance, $key, $fallback)
{
if (!isset($instance[$key])) {
return $fallback;
}
$v = $instance[$key];
// 布尔开关:checkbox 没勾时不提交,所以「缺键 = 关」由上面的 isset 处理,
// 键在则按真值判定(核心老实例存的是 1/0 整数,也能走这里)
if (is_bool($fallback)) {
return (bool) $v;
}
if (is_int($fallback)) {
$s = is_scalar($v) ? trim((string) $v) : '';
if ($s === '' || !preg_match('/^-?\d+$/', $s)) {
return $fallback;
}
return (int) $s;
}
if (!is_string($v)) {
return $fallback;
}
$v = trim($v);
return ($v === '') ? $fallback : $v;
}
/**
* 分类名转成可安全输出的文本。
*
* 只做 `esc_html()`,**不做** html_entity_decode —— 原因见类注释里的
* 实测数据:esc_html 对 `&` 的输出恒为 `&amp;`,所以库里存 `&` 还是
* 存 `&amp;`,最终 HTML 字节一致、浏览器显示也一致。decode 是冗余。
* 反过来 decode 有真实风险:分类名里若有人写了字面量 `&lt;script&gt;`,
* decode 会把它还原成真标签再 esc 一遍 —— 白白多一层可被绕过的路径。
*
* @param string $name
* @return string
*/
private function name_html($name)
{
return esc_html((string) $name);
}
/**
* 取分类树。
*
* @param bool $show_count 是否带上文章数(pad_counts 规则见类注释)
* @param bool $hide_empty 是否隐藏空分类
* @param string $orderby 'name' | 'count'
* @param string $order 'ASC' | 'DESC'
* @return array
*/
private function get_tree($show_count, $hide_empty, $orderby, $order)
{
$args = array(
'hide_empty' => (bool) $hide_empty,
'orderby' => $orderby,
'order' => $order,
/* 🔴 pad_counts 必须显式给:核心 wp_list_categories 是靠
* 「show_count && hierarchical」**隐式**打开的
* (category-template.php:392)。这里若不给,父级数字
* 会比升级前小(220 → 188),看着像丢了文章。 */
'pad_counts' => (bool) $show_count,
);
/* 🔴🔴 「按文章数排序」不能交给 SQL —— 这条实测踩过。
*
* `orderby=count` 在 WP_Term_Query::parse_orderby() 里被翻成
* `ORDER BY tt.count`(class-wp-term-query.php:926-927),
* 排的是数据库里的**原始** count 列;而 pad_counts 是在查询
* 之后由 PHP 补的(父级 = 自己 + 所有子孙)。
*
* 两者不一致的直接后果(实测本站,orderby=count&order=DESC):
*
* 生活记录 220 ← 排第 1(原始 188)
* 评论杂谈 168
* 信息技术 233 ← 排第 3(原始 233,最大!)
* 情感随笔 113
* 人生感悟 120
*
* 界面上看就是"数字大的排在小的后面"—— 用户会认为排序坏了。
* 所以 count 排序改成「先按 name 取回、再在 PHP 里按补过的
* count 排」。取回本来就要全量(要建树),不会多一次查询。 */
if ($orderby === 'count') {
$args['orderby'] = 'name';
$args['order'] = 'ASC';
}
$cats = get_categories($args);
if (!is_array($cats)) {
return array();
}
/* get_categories 返回的是平铺数组(按 orderby 排好),
* 这里按 parent 归拢成树。不用 Walker,因为要输出 <details>,
* 而 Walker_Category 只认 <ul>/<li>。 */
$by_parent = array();
foreach ($cats as $c) {
$pid = (int) $c->parent;
if (!isset($by_parent[$pid])) {
$by_parent[$pid] = array();
}
$by_parent[$pid][] = $c;
}
// 只保留「根(parent=0)→ 直接子级」两层。实测本站最深就是 2 层;
// 若将来有第三层,递归会把 <details> 套娃,侧栏观感反而更差。
$tree = array();
if (!isset($by_parent[0])) {
return $tree;
}
foreach ($by_parent[0] as $root) {
$rid = (int) $root->term_id;
$children = isset($by_parent[$rid]) ? $by_parent[$rid] : array();
$tree[] = array(
'cat' => $root,
'children' => $children,
);
}
/* 按文章数排序:在 PHP 里按「补过的 count」排,两边都排。
* 用 usort + 显式 tie-break(count 相同时按名称),
* 否则相等项的先后顺序取决于 usort 的实现,每次请求可能不同。 */
if ($orderby === 'count') {
$desc = ($order === 'DESC');
$cmp = function ($a, $b) use ($desc) {
$ca = (int) $a->count;
$cb = (int) $b->count;
if ($ca !== $cb) {
return $desc ? ($cb <=> $ca) : ($ca <=> $cb);
}
// 数量相同:按名称稳定排序(strnatcasecmp 对 "C#笔记" 之类更自然)
return strnatcasecmp($a->name, $b->name);
};
usort($tree, function ($x, $y) use ($cmp) {
return $cmp($x['cat'], $y['cat']);
});
foreach ($tree as $i => $node) {
if (count($node['children']) > 1) {
usort($tree[$i]['children'], $cmp);
}
}
}
return $tree;
}
public function widget($args, $instance)
{
$show_count = $this->pick($instance, 'count', true);
$hide_empty = $this->pick($instance, 'hide_empty', true);
$show_children = $this->pick($instance, 'children', true);
$orderby = $this->pick($instance, 'orderby', 'name');
$order = $this->pick($instance, 'order', 'ASC');
// 夹紧非法值(老实例里可能存着任意字符串)
if ($orderby !== 'count') {
$orderby = 'name';
}
if ($order !== 'DESC') {
$order = 'ASC';
}
$tree = $this->get_tree($show_count, $hide_empty, $orderby, $order);
// 🔴 一个分类都没有就整块不输出 —— 不留「标题 + 空 ul」的空壳
if (count($tree) === 0) {
return;
}
/* 当前分类页高亮用。is_category() 为真时查当前 term_id。
*
* 🔴 为什么不看 get_queried_object() 是不是 term 就完事:
* 单篇文章页上它是 WP_Post,用 in_category() 反查会把一篇
* 属于多个分类的文章算成多处高亮,高亮就不唯一了。
* 所以只在 is_category() 为真时取,其余页面一律不高亮。 */
$current_id = 0;
if (function_exists('is_category') && is_category()) {
$qo = get_queried_object();
if (is_object($qo) && isset($qo->term_id)) {
$current_id = (int) $qo->term_id;
}
}
/* 当前分类的祖先链:访问子分类时父级要一起标记,
* 否则用户看不出"我在哪个大类下面"。get_ancestors() 返回的是
* 从近到远的父级 id 数组。 */
$current_ancestors = array();
if ($current_id > 0 && function_exists('get_ancestors')) {
$anc = get_ancestors($current_id, 'category');
if (is_array($anc)) {
$current_ancestors = array_map('intval', $anc);
}
}
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">';
foreach ($tree as $node) {
$root = $node['cat'];
$rid = (int) $root->term_id;
$kids = $node['children'];
$has_kids = ($show_children && count($kids) > 0);
$root_cls = 'yang-cat-item';
if ($has_kids) {
$root_cls .= ' has-children';
}
if ($current_id === $rid) {
$root_cls .= ' current-cat';
} elseif (in_array($rid, $current_ancestors, true)) {
/* 当前在看这个父级底下的子分类 —— 父级挂 current-cat-parent,
* 与核心 Walker_Category 的类名保持一致,便于写样式。 */
$root_cls .= ' current-cat-parent';
}
echo '<li class="' . esc_attr($root_cls) . '">';
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">';
foreach ($kids as $kid) {
$kid_id = (int) $kid->term_id;
$kid_cls = 'yang-cat-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)) . '">'
. $this->name_html($kid->name) . '</a>';
if ($show_count) {
echo '<span class="yang-cat-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>';
}
echo '</ul>';
echo '</div>';
}
public function update($new_instance, $old_instance)
{
$instance = array();
/* 🔴 标题用「键存在就存原值」的写法,不是 `!empty`:
* 用户把标题清空是合法操作(表示不显示标题),
* `!empty` 会把它存成 ''、看着"存住了",但读的时候
* 又要区分「没设过」和「设成空」—— 这里统一成存空串即可,
* yang_widget_title() 对空串会回落到默认名「文章分类」。
* 这是本挂件与核心行为的一处有意差异:核心是空则回落
* __('Categories'),我们回落到 __('文章分类')。 */
$instance['title'] = isset($new_instance['title'])
? sanitize_text_field($new_instance['title'])
: '';
$instance['count'] = !empty($new_instance['count']) ? 1 : 0;
$instance['hide_empty'] = !empty($new_instance['hide_empty']) ? 1 : 0;
$instance['children'] = !empty($new_instance['children']) ? 1 : 0;
$orderby = isset($new_instance['orderby']) ? $new_instance['orderby'] : 'name';
$instance['orderby'] = ($orderby === 'count') ? 'count' : 'name';
$order = isset($new_instance['order']) ? $new_instance['order'] : 'ASC';
$instance['order'] = ($order === 'DESC') ? 'DESC' : 'ASC';
/* 🔴 核心的 dropdown / hierarchical 字段不在这里保存。
* 接管后它们没有意义,但**不能显式删掉**:本类只在
* 「用户再次保存这个挂件」时才跑 update(),而 update() 的返回值
* 会整体替换旧实例。如果用户从没打开过新表单,旧实例里那三个
* 字段还在(无害:widget() 不读它们)。
* 一旦用户保存了,字段就自然消失 —— 这正是我们想要的收敛方向。 */
return $instance;
}
public function form($instance)
{
$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);
$orderby = $this->pick($instance, 'orderby', 'name');
$order = $this->pick($instance, 'order', 'ASC');
if ($orderby !== 'count') {
$orderby = 'name';
}
if ($order !== 'DESC') {
$order = 'ASC';
}
?>
<div class="media-widget-control">
<p>
<label for="<?php echo esc_attr($this->get_field_id('title')); ?>"><?php _e('标题:', 'kratos'); ?></label>
<input class="widefat" id="<?php echo esc_attr($this->get_field_id('title')); ?>" name="<?php echo esc_attr($this->get_field_name('title')); ?>" type="text" value="<?php echo esc_attr($title); ?>" placeholder="<?php esc_attr_e('文章分类', 'kratos'); ?>">
<span class="description"><?php _e('留空则显示「文章分类」。', 'kratos'); ?></span>
</p>
<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>
</p>
<p>
<input class="checkbox" type="checkbox" id="<?php echo esc_attr($this->get_field_id('count')); ?>" name="<?php echo esc_attr($this->get_field_name('count')); ?>" <?php checked($show_count); ?>>
<label for="<?php echo esc_attr($this->get_field_id('count')); ?>"><?php _e('显示文章数', 'kratos'); ?></label>
<span class="description"><?php _e('父分类的数字包含其子分类的文章。', 'kratos'); ?></span>
</p>
<p>
<input class="checkbox" type="checkbox" id="<?php echo esc_attr($this->get_field_id('hide_empty')); ?>" name="<?php echo esc_attr($this->get_field_name('hide_empty')); ?>" <?php checked($hide_empty); ?>>
<label for="<?php echo esc_attr($this->get_field_id('hide_empty')); ?>"><?php _e('隐藏没有文章的分类', 'kratos'); ?></label>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('orderby')); ?>"><?php _e('排序依据:', 'kratos'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('orderby')); ?>" name="<?php echo esc_attr($this->get_field_name('orderby')); ?>">
<option value="name" <?php selected($orderby, 'name'); ?>><?php _e('分类名称', 'kratos'); ?></option>
<option value="count" <?php selected($orderby, 'count'); ?>><?php _e('文章数量', 'kratos'); ?></option>
</select>
</p>
<p>
<label for="<?php echo esc_attr($this->get_field_id('order')); ?>"><?php _e('排序方向:', 'kratos'); ?></label>
<select class="widefat" id="<?php echo esc_attr($this->get_field_id('order')); ?>" name="<?php echo esc_attr($this->get_field_name('order')); ?>">
<option value="ASC" <?php selected($order, 'ASC'); ?>><?php _e('升序', 'kratos'); ?></option>
<option value="DESC" <?php selected($order, 'DESC'); ?>><?php _e('降序', 'kratos'); ?></option>
</select>
</p>
<p class="description">
<?php _e('本挂件已接管 WordPress 自带的「分类」挂件:已有的分类挂件会直接在原位置换成新样式,标题也会保留。', 'kratos'); ?>
</p>
</div>
<?php
}
}
/* ==========================================================================
* 「最近评论」挂件(3.2.52 · 工单 #22)
*
@@ -1505,6 +2003,7 @@ function register_widgets()
register_widget('widget_tags');
register_widget('widget_search');
register_widget('widget_posts');
register_widget('widget_categories');
register_widget('widget_recent_comments');
}
add_action('widgets_init', 'register_widgets');