3.2.30: 文章目录进后台(新增「自动目录」标签页)—— 位置/插入点/默认展开可配,手写 [TOC] 永远优先

后台「主题设置」新增第 5 个标签页「自动目录」,7 个选项:
  目录位置   正文中间 / 页面左侧(浮动)/ 页面右侧(浮动)
  添加方式   手动(只认 [TOC])/ 自动
  自动插入位置  文章开头 / 第一个 H1、H2 之前 / 文章结尾
  默认状态   默认显示 / 默认隐藏
  另外:目录标题、收录层级(H1~H4)、是否显示序号

设计要点:
- 插入方式拆成 toc_mode(manual/auto) x toc_anchor(start/first/end) 两个正交
  字段,不合成一个四值下拉:侧栏模式下插入位置毫无意义,合并会产生歧义。
- 手写 [TOC] 绝对优先:yang_toc_plan() 里 $has_marker 最先返回,inline_at
  ='marker' 且 rail=false —— 否则同一篇会出现两份目录,老文章升级后突然多
  出一个目录。这条是刻意守住的。
- 纯函数 + 薄壳分层:normalize_settings / plan / inject / build_list 全部不查库,
  单进程可喂几十种组合;settings() 只负责读库 + 调纯函数 + static 缓存。
- <details open> 是唯一可靠的「默认收起」手段:展开态不接受 CSS 覆盖,只能
  由 open 属性决定,所以隐藏时必须不输出该属性。
- 侧栏收起态必须 translateX(±100%) 完全移出视口:只做 opacity 会让透明面板
  留在原位吃掉正文点击(探针 elementFromPoint 才抓得到)。
- yang_toc_rail_ready() 开关:侧栏在 wp_footer 输出,那时正文可能没走
  the_content(缓存插件/OEmbed),标题没 id ⇒ 侧栏链接全跳不动却看起来正常。
  宁可不出侧栏。
- 后台联动隐藏「插入位置」不用 jQuery show()/hide():会把 .section 的行内
  display 写成 block,悄悄回退 3.2.28 的 display:flow-root BFC 修复。改用
  class + display:none!important。

验证(全部在本轮重跑):
  _test_toc.php       53/0   旧 TOC 行为回归
  _toc330_logic.php   88/0   逻辑判据
  _toc330_falsify     13/13  注入全变红且红对地方
  _geo330_rail.js     73/0   侧栏真实几何(左/右 x 展开/收起 x 宽窄屏)
  _geo330_falsify     8/8
  _guard330.php       46/0   后台控件与运行时默认值双向一致
  _guard330_falsify   8/8
  _admin330_tab_check 36/0   新标签页几何 + 联动 + 载入态
  _admin330_falsify   8/8
  _admin328_check     13/0   3.2.28 布局回归

发布:只发 zip + readme + update.json 到 theme-update/,不动服务器主题目录,
用户在 WP 后台自行升级(沿用 3.2.24 起的策略)。
This commit is contained in:
2026-10-25 12:18:52 +08:00
parent 677de0f24e
commit 6f51d50a58
7 changed files with 1060 additions and 91 deletions
+85 -3
View File
@@ -4,9 +4,11 @@
*
* ── 设计原则:没有 JS 也要能用 ────────────────────────────────
* 目录的**核心功能是跳转**,那由 <a href="#id"> + CSS 的
* scroll-margin-top 完成,完全不需要 JS。这个文件只做两件锦上添花的事:
* scroll-margin-top 完成,完全不需要 JS。这个文件只做三件锦上添花的事:
* ① 平滑滚动(不支持的浏览器直接跳,不影响功能)
* ② 高亮当前阅读位置的条目
* ③ 侧边目录面板的展开 / 收起(默认状态由 HTML class 决定,
* JS 不参与"初始长什么样",只负责切换)
* 所以这里所有增强都必须能安全失败 —— 任何异常都不该让跳转失效。
*
* 为什么不用 jQuery:主题里虽然有 jQuery,但这两件事原生 API 足够,
@@ -114,7 +116,11 @@
/* ── ② 滚动时高亮当前章节 ──────────────────────────────── */
function bindScrollSpy () {
var toc = document.querySelector('.yang-toc')
/* 目录可能在两处:正文里的 .yang-toc,或侧边的 .yang-toc-rail-panel。
两处不会同时出现(正文有手写标记时不出侧栏),所以取第一个找到的
就够;用逗号选择器而不是两个 if,是为了将来加第三种形态时
不用再改这里。 */
var toc = document.querySelector('.yang-toc, .yang-toc-rail-panel')
if (!toc) return
var links = [].slice.call(toc.querySelectorAll('.yang-toc-link'))
@@ -175,7 +181,82 @@
update()
}
/* ── ③ 带 #hash 直接进入页面时,校正落点 ──────────────────
/* ── ③ 侧边目录:展开 / 收起 ─────────────────────────────
* 目录内容本身(链接、href、锚点)全部由 PHP 输出,浏览器不点也能
* 跳(每个 <a> 都是真链接)。这个函数只负责"把面板滑出来"。
*
* 默认展开/收起的状态**已经写在 HTML 的 class 上**(is-open),
* 所以 JS 挂了也只是不能切换,不会把"默认显示"变成"什么都没有"。
*/
function bindRail () {
var rails = document.querySelectorAll('.yang-toc-rail')
if (!rails.length) return
[].forEach.call(rails, function (rail) {
var tab = rail.querySelector('.yang-toc-rail-tab')
var panel = rail.querySelector('.yang-toc-rail-panel')
var closeBtn = rail.querySelector('.yang-toc-rail-close')
if (!tab || !panel) return
if (rail.getAttribute('data-bound') === '1') return
rail.setAttribute('data-bound', '1')
function isOpen () {
return rail.classList.contains('is-open')
}
function setOpen (open) {
if (open) {
rail.classList.add('is-open')
} else {
rail.classList.remove('is-open')
}
tab.setAttribute('aria-expanded', open ? 'true' : 'false')
rail.setAttribute('data-state', open ? 'open' : 'closed')
}
tab.addEventListener('click', function (ev) {
ev.preventDefault()
setOpen(!isOpen())
})
/* 点 × 收起后把焦点还给标签 —— 否则焦点留在被隐藏的面板里,
键盘用户会"走丢"(Tab 下一个跑到页面开头)。 */
if (closeBtn) {
closeBtn.addEventListener('click', function () {
setOpen(false)
tab.focus()
})
}
/* 点了目录项:宽屏上留着面板方便继续翻(用户多半还要点下一节),
窄屏上它正好盖在刚跳到的正文上,必须收起来。 */
panel.addEventListener('click', function (ev) {
var a = ev.target.closest ? ev.target.closest('.yang-toc-link') : null
if (!a) return
if (window.innerWidth < 1200) {
setOpen(false)
}
})
document.addEventListener('keydown', function (ev) {
// Esc 收起。只处理 Esc,别拦别的键(会和浏览器快捷键打架)
if ((ev.key === 'Escape' || ev.key === 'Esc') && isOpen()) {
setOpen(false)
tab.focus()
}
})
/* 点面板外面收起。用 document 上的冒泡而不是遮罩层:
加一层半透明遮罩会挡住正文,目录只是辅助导航,不该锁住页面。 */
document.addEventListener('click', function (ev) {
if (!isOpen()) return
if (rail.contains(ev.target)) return
setOpen(false)
})
})
}
/* ── ④ 带 #hash 直接进入页面时,校正落点 ──────────────────
* 浏览器原生的锚点跳转发生在页面加载早期,那时图片还没加载完、
* CSS 也可能没应用完,落点常常偏上或偏下(尤其是有图片的页面)。
* 这里在 load 之后再做一次校正。
@@ -194,6 +275,7 @@
function init () {
bindClick()
bindScrollSpy()
bindRail()
fixHashOnLoad()
}