- rehydrate.js 新增 boot()/aksrt:ready 事件,支持 boot 后晚注册立即以 init 触发 - aksrt-runtime.js 初始加载改走 AksrtFrontend.boot,tooltip/倒计时注册为组件 - 约 30 个前端脚本(首页、评论、支付、路由、页面叶子、widgets) 从直接 DOMContentLoaded 迁移为 AksrtFrontend.register,并加幂等标记 - section-navs 依赖改为 aksrt-runtime,保证注册时序 - 清理死代码:删除孤儿 loader.js/main.js 及 runtime/page-navs 遗留副本 - loader.js 移除已迁移模块的 tbquire 路径映射,main.js 相应瘦身 - build-public-assets.mjs 增加 runtime/main.min.js 自动压缩步骤 - 新增 4 个契约测试(homepage-dom/rehydrate-runtime/global-dom-init/ public-bundle-parity),3 个既有 harness 适配注册表初始化方式
153 lines
4.5 KiB
JavaScript
153 lines
4.5 KiB
JavaScript
/**
|
||
* AKSRT Section Navs — legacy section_navs parity (posts-nav-lists + scrollspy).
|
||
*
|
||
* Builds TOC from [data-nav] h1–h4 into .posts-nav-box / .posts-nav-lists.
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
var HEADING_SEL = 'h1, h2, h3, h4';
|
||
var MIN_HEADINGS = 2;
|
||
var NAVBOX = '.posts-nav-box';
|
||
var NAV_CLASS = 'posts-nav-lists';
|
||
|
||
function isExcluded(el) {
|
||
return el.classList.contains('item-heading') || !!el.closest('.no-nav');
|
||
}
|
||
|
||
function ensureId(el, index) {
|
||
if (!el.id) {
|
||
el.id = 'section-nav-' + index;
|
||
}
|
||
return el.id;
|
||
}
|
||
|
||
function buildLists(root) {
|
||
var headings = root.querySelectorAll(HEADING_SEL);
|
||
var html = '';
|
||
var count = 0;
|
||
headings.forEach(function (h, i) {
|
||
if (isExcluded(h)) return;
|
||
var id = ensureId(h, i);
|
||
var tag = h.tagName.toUpperCase();
|
||
var text = (h.textContent || '').trim();
|
||
if (!text) return;
|
||
html += '<li class="n-' + tag + '"><a href="#' + id + '">' + text + '</a></li>';
|
||
count++;
|
||
});
|
||
return count >= MIN_HEADINGS ? html : '';
|
||
}
|
||
|
||
function fillBoxes(listsHtml, scope) {
|
||
var boxes = Array.prototype.slice.call(scope.querySelectorAll(NAVBOX));
|
||
if (scope.nodeType === 1 && scope.matches(NAVBOX)) boxes.unshift(scope);
|
||
boxes.forEach(function (box) {
|
||
var existing = box.querySelector('.' + NAV_CLASS);
|
||
if (existing) {
|
||
existing.innerHTML = listsHtml;
|
||
return;
|
||
}
|
||
var title = box.getAttribute('data-title') || '';
|
||
var titleHtml = title
|
||
? '<div class="box-body notop"><div class="title-theme">' + title + '</div></div>'
|
||
: '';
|
||
box.insertAdjacentHTML(
|
||
'beforeend',
|
||
titleHtml +
|
||
'<div class="aksrt-panel"><div class="' +
|
||
NAV_CLASS +
|
||
' scroll-y mini-scrollbar list-unstyled">' +
|
||
listsHtml +
|
||
'</div></div>'
|
||
);
|
||
});
|
||
}
|
||
|
||
function smoothTo(hash) {
|
||
var el = document.querySelector(hash);
|
||
if (!el) return;
|
||
var offset = 0;
|
||
var header = document.querySelector('.header');
|
||
if (document.body.classList.contains('nav-fixed') && header) {
|
||
offset = header.offsetHeight + 12;
|
||
}
|
||
var top = el.getBoundingClientRect().top + window.pageYOffset - offset;
|
||
window.scrollTo({ top: top, behavior: 'smooth' });
|
||
}
|
||
|
||
var clicksBound = false;
|
||
var scrollspyBound = false;
|
||
var autoHashHandled = false;
|
||
|
||
function bindClicks() {
|
||
if (clicksBound) return;
|
||
clicksBound = true;
|
||
document.addEventListener('click', function (e) {
|
||
var a = e.target && e.target.closest ? e.target.closest('.' + NAV_CLASS + ' a[href^="#"]') : null;
|
||
if (!a) return;
|
||
var href = a.getAttribute('href');
|
||
if (!href || href.length < 2) return;
|
||
e.preventDefault();
|
||
history.replaceState(null, '', href);
|
||
smoothTo(href);
|
||
});
|
||
}
|
||
|
||
function scrollspy(scope) {
|
||
if (scrollspyBound) return;
|
||
var links = scope.querySelectorAll('.' + NAV_CLASS + ' a[href^="#"]');
|
||
if (!links.length) return;
|
||
scrollspyBound = true;
|
||
var map = [];
|
||
links.forEach(function (a) {
|
||
var id = a.getAttribute('href').slice(1);
|
||
var el = document.getElementById(id);
|
||
if (el) map.push({ el: el, a: a, li: a.parentElement });
|
||
});
|
||
function update() {
|
||
var y = window.scrollY + 110;
|
||
var current = null;
|
||
for (var i = 0; i < map.length; i++) {
|
||
if (map[i].el.offsetTop <= y) current = map[i];
|
||
}
|
||
map.forEach(function (m) {
|
||
if (m.li) m.li.classList.toggle('active', m === current);
|
||
});
|
||
}
|
||
window.addEventListener('scroll', update, { passive: true });
|
||
update();
|
||
}
|
||
|
||
function autoHash() {
|
||
if (autoHashHandled || !window.location.hash) return;
|
||
autoHashHandled = true;
|
||
setTimeout(function () {
|
||
smoothTo(window.location.hash);
|
||
}, 40);
|
||
}
|
||
|
||
function init(scope) {
|
||
scope = scope || document;
|
||
if (document.querySelector('.posts-nav-box .toc-list, .posts-nav-box.toc-widget .toc-list')) {
|
||
return;
|
||
}
|
||
var root = scope.nodeType === 1 && scope.matches('[data-nav], .article-content, .entry-content, main.article')
|
||
? scope
|
||
: scope.querySelector('[data-nav], .article-content, .entry-content, main.article');
|
||
if (!root) return;
|
||
var listsHtml = buildLists(root);
|
||
if (!listsHtml) return;
|
||
if (!document.querySelector(NAVBOX) && !document.querySelector('.' + NAV_CLASS)) {
|
||
return;
|
||
}
|
||
fillBoxes(listsHtml, document);
|
||
bindClicks();
|
||
scrollspy(document);
|
||
autoHash();
|
||
}
|
||
|
||
window.AksrtFrontend.register('aksrt-section-navs', init);
|
||
})();
|