paper-burner/js/ui/toc_scroll_sync.js

173 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @file js/ui/toc_scroll_sync.js
* @description 负责监听页面滚动,自动高亮当前视口中对应的 TOC 目录项。
* 支持普通视图和沉浸式视图的自动切换。
*/
(function() {
let scrollTimeout = null;
let lastActiveId = null;
/**
* 获取当前的滚动容器
* @returns {HTMLElement|Window}
*/
function getScrollContainer() {
if (document.body.classList.contains('immersive-active')) {
return document.querySelector('#immersive-main-content-area .tab-content') || window;
}
return window;
}
/**
* 核心逻辑:计算当前应该高亮的标题
*/
function highlightActiveTocItem() {
// 依赖 toc_logic.js 暴露的全局函数
if (typeof window.getTocNodes !== 'function') return;
const tocNodes = window.getTocNodes();
if (!tocNodes || tocNodes.length === 0) return;
const container = getScrollContainer();
// 在沉浸模式下,容器顶部可能有偏移
const containerTop = (container === window) ? 0 : container.getBoundingClientRect().top;
// 定义“激活区域”:视口顶部向下 150px 的范围
const activeZoneTop = containerTop + 150;
let currentActiveNode = null;
// 倒序遍历,找到第一个在激活区域之上的标题
for (let i = tocNodes.length - 1; i >= 0; i--) {
const node = tocNodes[i];
const rect = node.getBoundingClientRect();
if (rect.top <= activeZoneTop) {
currentActiveNode = node;
break;
}
}
// 如果页面刚打开,可能都在视口下方,默认高亮第一个
if (!currentActiveNode && tocNodes.length > 0) {
// 可选currentActiveNode = tocNodes[0];
}
if (currentActiveNode) {
const activeId = currentActiveNode.id;
if (activeId !== lastActiveId) {
updateTocUi(activeId);
lastActiveId = activeId;
}
} else if (lastActiveId) {
// 如果没有找到激活节点(例如滚动到最顶部之前),清除高亮
clearTocUi();
lastActiveId = null;
}
}
/**
* 更新 TOC UI 的高亮状态
* @param {string} activeId
*/
function updateTocUi(activeId) {
// 1. 移除所有旧的激活状态
document.querySelectorAll('#toc-list a.active').forEach(link => {
link.classList.remove('active');
});
// 2. 找到新的激活链接
// 可能有多个链接指向同一个ID虽然不常见但为了健壮性
// 使用属性选择器匹配 href="#id"
const activeLinks = document.querySelectorAll(`#toc-list a[href="#${CSS.escape(activeId)}"]`);
activeLinks.forEach(link => {
link.classList.add('active');
// 可选:自动展开父级目录
// ensureParentExpanded(link);
// 可选:确保高亮的目录项在 TOC 视口中可见
ensureTocItemVisible(link);
});
}
function clearTocUi() {
document.querySelectorAll('#toc-list a.active').forEach(link => {
link.classList.remove('active');
});
}
/**
* 确保激活的 TOC 项在滚动容器中可见
* @param {HTMLElement} link
*/
function ensureTocItemVisible(link) {
const tocPopup = document.getElementById('toc-popup');
// 仅当 TOC 弹窗显示时才自动滚动
if (tocPopup && (getComputedStyle(tocPopup).display !== 'none' || document.body.classList.contains('immersive-active'))) {
// 简单的 scrollIntoView使用 nearest 避免剧烈跳动
link.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}
/**
* 节流滚动事件处理器
*/
function handleScroll() {
if (!scrollTimeout) {
scrollTimeout = requestAnimationFrame(() => {
highlightActiveTocItem();
scrollTimeout = null;
});
}
}
/**
* 初始化监听器
*/
function initScrollSync() {
// 监听全局滚动(普通模式)
window.addEventListener('scroll', handleScroll, { passive: true });
// 监听可能的内部容器滚动(沉浸模式或其他特定 Tab
// 使用事件代理或定期检查可能更健壮,这里先尝试直接绑定常见容器
const potentialContainers = document.querySelectorAll('.tab-content');
potentialContainers.forEach(c => {
c.addEventListener('scroll', handleScroll, { passive: true });
});
// 监听沉浸模式切换,重新绑定或触发一次计算
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.attributeName === 'class' && mutation.target === document.body) {
// 模式切换后,稍等布局稳定再计算一次
setTimeout(highlightActiveTocItem, 300);
// 如果进入沉浸模式,可能需要重新绑定新的滚动容器
if (document.body.classList.contains('immersive-active')) {
const immersiveContainer = document.querySelector('#immersive-main-content-area .tab-content');
if (immersiveContainer) {
immersiveContainer.removeEventListener('scroll', handleScroll); // 避免重复
immersiveContainer.addEventListener('scroll', handleScroll, { passive: true });
}
}
}
}
});
observer.observe(document.body, { attributes: true });
// 初始执行一次
setTimeout(highlightActiveTocItem, 500);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initScrollSync);
} else {
initScrollSync();
}
// 暴露一个全局方法以便在内容重新渲染后手动触发
window.syncTocScroll = highlightActiveTocItem;
})();