feat(widgets): wire PostsNavs TOC shell to page-template scrollspy

Match built-in single sidebar .posts-nav-box.toc-widget/.toc-list so the
sidebar widget is filled (≥3 headings), supports multiple TOC shells, and
removes empty chrome.
This commit is contained in:
2026-07-21 08:03:25 +08:00
parent 5ab935f015
commit c0b7cb504b
3 changed files with 108 additions and 25 deletions
@@ -364,6 +364,10 @@ html.dark .article-end-separator span {
color: var(--color-primary);
}
.toc-list .toc-h1 a {
font-weight: 700;
}
.toc-list .toc-h3 a {
padding-left: 12px;
}
@@ -372,6 +376,30 @@ html.dark .article-end-separator span {
padding-left: 24px;
}
/* WidgetPostsNavs + built-in shell share .posts-nav-box.toc-widget */
.widget .posts-nav-box.toc-widget,
.widget_aksrt_posts_navs .posts-nav-box.toc-widget {
margin: 0;
box-shadow: none;
background: transparent;
padding: 0;
border-radius: 0;
}
.widget .posts-nav-box.toc-widget > h3,
.widget .posts-nav-box.toc-widget .toc-widget__title,
.widget_aksrt_posts_navs .posts-nav-box.toc-widget > h3 {
margin: 0 0 10px;
font-size: 15px;
font-weight: 700;
}
body.single .sidebar .posts-nav-box.toc-widget.is-affix,
body.single .sidebar .posts-nav-box.toc-widget[data-affix="true"] {
position: sticky;
top: calc(var(--nav-height, 60px) + 16px);
}
.toc-float-btn {
position: fixed;
right: 18px;
+59 -17
View File
@@ -19,9 +19,31 @@
}
function initPostToc() {
var content = document.querySelector('.wp-posts-content[data-nav="posts"][data-aksrt-toc="1"]');
var tocWidget = document.querySelector('.posts-nav-box.toc-widget');
var tocList = tocWidget ? tocWidget.querySelector('.toc-list') : null;
// Accept either explicit data-aksrt-toc="1" or any [data-nav] article body.
var content =
document.querySelector('.wp-posts-content[data-nav="posts"][data-aksrt-toc="1"]') ||
document.querySelector('.wp-posts-content[data-nav]') ||
document.querySelector('[data-nav="posts"]') ||
document.querySelector('[data-nav]');
// Support multiple TOC shells (built-in single sidebar + WidgetPostsNavs).
var tocWidgets = Array.prototype.slice.call(
document.querySelectorAll('.posts-nav-box.toc-widget, .posts-nav-box[data-title]')
);
var tocLists = [];
tocWidgets.forEach(function (box) {
if (!box.classList.contains('toc-widget')) {
box.classList.add('toc-widget');
}
var list = box.querySelector('.toc-list');
if (!list) {
list = document.createElement('ol');
list.className = 'toc-list';
box.appendChild(list);
}
tocLists.push(list);
});
var mobileToggle = document.querySelector('.toc-float-btn');
var drawer = document.querySelector('.toc-drawer');
var overlay = document.querySelector('.toc-drawer-overlay');
@@ -29,16 +51,25 @@
var closeBtn = drawer ? drawer.querySelector('.toc-drawer__close') : null;
if (!content || content.dataset.aksrtTocBound === '1') return;
if (!tocList && !drawerList && !mobileToggle) return;
if (!tocLists.length && !drawerList && !mobileToggle) return;
content.dataset.aksrtTocBound = '1';
var maxDepth = parseInt(content.dataset.aksrtTocDepth || '3', 10) || 3;
// zibll section-navs: only fill when heading count > 2 (≥3).
var MIN_HEADINGS = 3;
function getHeadings() {
return Array.prototype.slice.call(content.querySelectorAll('h2, h3, h4')).filter(function (heading) {
// zibll scans h1–h4 inside [data-nav]; depth setting still caps lower levels.
return Array.prototype.slice.call(content.querySelectorAll('h1, h2, h3, h4')).filter(function (heading) {
if (heading.closest('.no-nav') || heading.classList.contains('item-heading')) {
return false;
}
var level = parseInt((heading.tagName || 'H2').replace('H', ''), 10) || 2;
return level <= maxDepth;
// toc_depth is typically 2–4 meaning deepest heading level from h2 baseline;
// allow h1 always, then h2..h(1+maxDepth) capped at h4.
if (level === 1) return true;
return level <= Math.min(4, maxDepth + 1);
});
}
@@ -46,7 +77,7 @@
if (!container) return false;
var headings = getHeadings();
if (!headings.length) return false;
if (headings.length < MIN_HEADINGS) return false;
var html = '';
headings.forEach(function (heading) {
@@ -55,7 +86,9 @@
}
var levelClass = '';
if (heading.tagName === 'H3') {
if (heading.tagName === 'H1') {
levelClass = 'toc-h1';
} else if (heading.tagName === 'H3') {
levelClass = 'toc-h3';
} else if (heading.tagName === 'H4') {
levelClass = 'toc-h4';
@@ -68,13 +101,24 @@
return true;
}
var hasToc = buildToc(tocList);
if (drawerList && drawerList !== tocList) {
buildToc(drawerList);
var hasToc = false;
tocLists.forEach(function (list) {
if (buildToc(list)) hasToc = true;
});
if (drawerList && tocLists.indexOf(drawerList) === -1) {
if (buildToc(drawerList)) hasToc = true;
}
if (!hasToc) {
if (tocWidget) tocWidget.remove();
tocWidgets.forEach(function (box) {
var host = box.closest('.widget') || box.parentElement;
// Prefer removing the whole widget chrome when empty.
if (host && host !== document.body && host.querySelectorAll('.posts-nav-box').length <= 1) {
host.remove();
} else {
box.remove();
}
});
if (mobileToggle) mobileToggle.remove();
if (drawer) drawer.remove();
if (overlay) overlay.remove();
@@ -94,7 +138,7 @@
}
function trackActiveHeading() {
var headings = content.querySelectorAll('h2[id], h3[id], h4[id]');
var headings = content.querySelectorAll('h1[id], h2[id], h3[id], h4[id]');
if (!headings.length) return;
var activeId = '';
@@ -117,10 +161,8 @@
if (!target) return;
event.preventDefault();
target.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
var top = target.getBoundingClientRect().top + window.pageYOffset - 105;
window.scrollTo({ top: Math.max(0, top), behavior: 'smooth' });
closeDrawer();
});
});
+21 -8
View File
@@ -2,7 +2,8 @@
/**
* In-article / topic TOC shell (zibll widget_ui_posts_navs parity).
*
* Frontend JS fills `.posts-nav-box` from heading outline when present.
* Shell matches the built-in single sidebar TOC so page-template.js can fill
* `.posts-nav-box.toc-widget > .toc-list` from `[data-nav]` headings.
*
* @package AKSRT
*/
@@ -18,8 +19,8 @@ class WidgetPostsNavs extends \WP_Widget {
'aksrt_posts_navs',
esc_html__('AKSRT 文章目录树', 'aksrt'),
[
'description' => esc_html__('显示当前文章/帖子标题目录,非详情页不输出。标题超过 3 个才显示。', 'aksrt'),
'classname' => 'widget_aksrt_posts_navs',
'description' => esc_html__('显示当前文章/帖子标题目录,非详情页不输出。标题超过 2 个才填充。', 'aksrt'),
'classname' => 'widget_aksrt_posts_navs',
]
);
}
@@ -35,16 +36,28 @@ class WidgetPostsNavs extends \WP_Widget {
$data_title = $title;
if ($mini_title !== '') {
$data_title .= ' · ' . $mini_title;
$data_title .= ($data_title !== '' ? ' · ' : '') . $mini_title;
}
if ($data_title === '') {
$data_title = __('文章目录', 'aksrt');
}
// Mirror aksrt-single-render built-in TOC shell so page-template.js binds.
$classes = 'zib-widget toc-widget posts-nav-box';
if ($in_affix) {
$classes .= ' is-affix';
}
echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo '<div class="widget-container">';
printf(
'<div class="posts-nav-box"%s%s></div>',
'<div class="%s"%s data-title="%s" aria-label="%s">',
esc_attr($classes),
$in_affix ? ' data-affix="true"' : '',
$data_title !== '' ? ' data-title="' . esc_attr($data_title) . '"' : ''
esc_attr($data_title),
esc_attr($data_title)
);
echo '<h3 class="widget-title toc-widget__title">' . esc_html($data_title) . '</h3>';
echo '<ol class="toc-list"></ol>';
echo '</div>';
echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
@@ -68,7 +81,7 @@ class WidgetPostsNavs extends \WP_Widget {
<?php esc_html_e('侧栏随动(仅侧边栏)', 'aksrt'); ?>
</label>
</p>
<p class="description"><?php esc_html_e('请添加到文章/帖子侧栏。正文 H2–H4 超过 3 个时前端才会填充目录。', 'aksrt'); ?></p>
<p class="description"><?php esc_html_e('请添加到文章/帖子侧栏。正文标题 ≥3 个时由前端填充目录;无标题则自动移除。', 'aksrt'); ?></p>
<?php
}