diff --git a/assets/css/frontend/single-post/related-toc.css b/assets/css/frontend/single-post/related-toc.css index 277cd053..827f1d6e 100644 --- a/assets/css/frontend/single-post/related-toc.css +++ b/assets/css/frontend/single-post/related-toc.css @@ -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; diff --git a/assets/js/page-template.js b/assets/js/page-template.js index b532e3cd..d87794c2 100644 --- a/assets/js/page-template.js +++ b/assets/js/page-template.js @@ -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(); }); }); diff --git a/src/AKSRT/Widgets/WidgetPostsNavs.php b/src/AKSRT/Widgets/WidgetPostsNavs.php index c8873898..3567c6e5 100644 --- a/src/AKSRT/Widgets/WidgetPostsNavs.php +++ b/src/AKSRT/Widgets/WidgetPostsNavs.php @@ -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 '