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:
@@ -245,6 +245,214 @@ body.admin-bar .k-main .details .content h6[id] {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 正文内嵌槽(自动插入时用) ──────────────────────────────
|
||||
* 自动插入的目录外面包一层 slot。加这层不是为了好看,是为了让
|
||||
* 「目录在哪」这件事在 DOM 里有痕迹:插入位置变了、或者根本没插进去,
|
||||
* 看 slot 的 class / 位置就能判断,不用去猜正文里的 <details> 是谁输出的。
|
||||
*
|
||||
* 边距放在 slot 上、把 .yang-toc 自身的 margin 归零:
|
||||
* 两层都带 margin 时外边距会叠加,插在开头/结尾的那几种位置尤其难看
|
||||
* (开头会顶出一块空白)。
|
||||
*/
|
||||
.yang-toc-slot {
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
.yang-toc-slot > .yang-toc {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ── 侧边浮动目录 ─────────────────────────────────────────────
|
||||
* 结构:贴边的一条细标签 + 一块可滑出的面板。
|
||||
*
|
||||
* 关键几何约束(容易被后来的改动破坏):
|
||||
* · 容器是 0 宽的定位锚点,不可见、不吃点击(pointer-events:none);
|
||||
* 真正可点的只有标签和面板。
|
||||
* · 收起态面板必须**完全**移出视口(translateX(±100%)),
|
||||
* 不能只做透明度。留一点"露头"看着舒服,但它会盖住正文列边缘,
|
||||
* 窄屏上正好压掉正文的第一个字 —— 那种问题肉眼很难定位。
|
||||
* · z-index 低于导航(Bootstrap navbar 是 1030),
|
||||
* 万一两者在高度上相遇,导航仍然压在上面。
|
||||
*/
|
||||
.yang-toc-rail {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.yang-toc-rail-left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.yang-toc-rail-right {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.yang-toc-rail > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
/* 贴边标签:竖排文字,主色底 */
|
||||
.yang-toc-rail-tab {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
width: 34px;
|
||||
padding: 14px 0;
|
||||
border: 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
background-color: #00a2ff;
|
||||
color: #fff;
|
||||
box-shadow: 1px 0 6px rgba(0, 0, 0, 0.12);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
transform: translateY(-50%);
|
||||
transition: opacity 0.18s ease, background-color 0.18s ease;
|
||||
}
|
||||
|
||||
.yang-toc-rail-left .yang-toc-rail-tab {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.yang-toc-rail-right .yang-toc-rail-tab {
|
||||
right: 0;
|
||||
border-radius: 6px 0 0 6px;
|
||||
box-shadow: -1px 0 6px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.yang-toc-rail-tab:hover,
|
||||
.yang-toc-rail-tab:focus {
|
||||
background-color: #0089d8;
|
||||
}
|
||||
|
||||
.yang-toc-rail-tab-text {
|
||||
display: block;
|
||||
letter-spacing: 3px;
|
||||
writing-mode: vertical-rl;
|
||||
text-orientation: upright;
|
||||
}
|
||||
|
||||
/* 展开时标签让位给面板(面板已经贴边盖住它了,留着只是碍眼) */
|
||||
.yang-toc-rail.is-open .yang-toc-rail-tab {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 面板 */
|
||||
.yang-toc-rail-panel {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 288px;
|
||||
max-height: 72vh;
|
||||
border: 1px solid #ebeef5;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.14);
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s ease, transform 0.28s ease, visibility 0s linear 0.28s;
|
||||
}
|
||||
|
||||
.yang-toc-rail-left .yang-toc-rail-panel {
|
||||
left: 0;
|
||||
border-left: 3px solid #00a2ff;
|
||||
border-radius: 0 4px 4px 0;
|
||||
transform: translate(-100%, -50%);
|
||||
}
|
||||
|
||||
.yang-toc-rail-right .yang-toc-rail-panel {
|
||||
right: 0;
|
||||
border-right: 3px solid #00a2ff;
|
||||
border-radius: 4px 0 0 4px;
|
||||
transform: translate(100%, -50%);
|
||||
}
|
||||
|
||||
.yang-toc-rail.is-open .yang-toc-rail-panel {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translate(0, -50%);
|
||||
transition: opacity 0.2s ease, transform 0.28s ease, visibility 0s;
|
||||
}
|
||||
|
||||
.yang-toc-rail-head {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
padding: 12px 12px 8px 14px;
|
||||
border-bottom: 1px solid #f3f5f8;
|
||||
}
|
||||
|
||||
.yang-toc-rail-title {
|
||||
color: #303133;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.yang-toc-rail-close {
|
||||
flex: 0 0 auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-left: auto;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 3px;
|
||||
background: none;
|
||||
color: #909399;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.yang-toc-rail-close:hover,
|
||||
.yang-toc-rail-close:focus {
|
||||
background-color: #f2f4f8;
|
||||
color: #00a2ff;
|
||||
}
|
||||
|
||||
.yang-toc-rail-body {
|
||||
flex: 1 1 auto;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px 12px;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
/* ── 窄屏:面板按视口宽度收,标签再窄一点 ── */
|
||||
@media screen and (max-width: 768px) {
|
||||
.yang-toc-rail-panel {
|
||||
width: min(78vw, 300px);
|
||||
max-height: 70vh;
|
||||
}
|
||||
|
||||
.yang-toc-rail-tab {
|
||||
width: 30px;
|
||||
padding: 12px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.yang-toc-rail-head {
|
||||
padding: 10px 10px 8px 12px;
|
||||
}
|
||||
|
||||
.yang-toc-rail-body {
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动效敏感的用户:不要滑出动画,直接显隐 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.yang-toc-rail-panel,
|
||||
.yang-toc-rail.is-open .yang-toc-rail-panel,
|
||||
.yang-toc-rail-tab {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 打印:目录展开、去背景、不折行 ────────────────────────── */
|
||||
@media print {
|
||||
.yang-toc {
|
||||
@@ -260,6 +468,11 @@ body.admin-bar .k-main .details .content h6[id] {
|
||||
.yang-toc-link {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/* 侧栏是屏幕上的快捷入口,纸上没有意义,还会盖住正文 */
|
||||
.yang-toc-rail {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 暗色主题适配 ──────────────────────────────────────────
|
||||
@@ -300,4 +513,39 @@ body.admin-bar .k-main .details .content h6[id] {
|
||||
html.yang-dark .yang-toc-num {
|
||||
color: #767d88;
|
||||
}
|
||||
|
||||
/* 侧边目录:面板底色/边框/文字跟内嵌目录同一套暗色值,
|
||||
否则暗色站点上会凭空出现一块白板。标签本身是主色底白字,
|
||||
两种模式下都不用改。 */
|
||||
html.yang-dark .yang-toc-rail-panel {
|
||||
border-color: #303640;
|
||||
background-color: #1d2026;
|
||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
html.yang-dark .yang-toc-rail-left .yang-toc-rail-panel {
|
||||
border-left-color: #00a2ff;
|
||||
}
|
||||
|
||||
html.yang-dark .yang-toc-rail-right .yang-toc-rail-panel {
|
||||
border-right-color: #00a2ff;
|
||||
}
|
||||
|
||||
html.yang-dark .yang-toc-rail-head {
|
||||
border-bottom-color: #2a2f38;
|
||||
}
|
||||
|
||||
html.yang-dark .yang-toc-rail-title {
|
||||
color: #d5d9e0;
|
||||
}
|
||||
|
||||
html.yang-dark .yang-toc-rail-close {
|
||||
color: #8b929d;
|
||||
}
|
||||
|
||||
html.yang-dark .yang-toc-rail-close:hover,
|
||||
html.yang-dark .yang-toc-rail-close:focus {
|
||||
background-color: #252b34;
|
||||
color: #00a2ff;
|
||||
}
|
||||
}
|
||||
+85
-3
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user