782 lines
24 KiB
JavaScript
782 lines
24 KiB
JavaScript
/**
|
||
* Sidebar Integration - 侧边栏集成模块
|
||
*
|
||
* 职责:
|
||
* 1. 从原有 TOC 数据同步到侧边栏 TOC
|
||
* 2. 从原有 Dock 统计同步到侧边栏统计
|
||
* 3. 处理侧边栏折叠/展开(桌面端)
|
||
* 4. 处理侧边栏显示/隐藏(移动端)
|
||
* 5. 处理可折叠区域的展开/收起
|
||
* 6. 保持状态到 localStorage
|
||
*
|
||
* 注意:仅在非沉浸模式下工作,沉浸模式不受影响
|
||
*/
|
||
|
||
(function() {
|
||
'use strict';
|
||
|
||
// ==================== 配置 ====================
|
||
|
||
const CONFIG = {
|
||
storageKeys: {
|
||
sidebarCollapsed: 'pbx_sidebar_collapsed',
|
||
tocSectionExpanded: 'pbx_sidebar_toc_expanded'
|
||
},
|
||
selectors: {
|
||
// Sidebar
|
||
appShell: '#app-shell',
|
||
appSidebar: '#appSidebar',
|
||
sidebarOverlay: '#sidebarOverlay',
|
||
sidebarToggleBtn: '#sidebarToggleBtn',
|
||
sidebarToggleIcon: '#sidebarToggleIcon',
|
||
sidebarCloseBtn: '#sidebarCloseBtn',
|
||
mobileMenuBtn: '#mobileMenuBtn',
|
||
sidebarLogo: '#sidebarLogo',
|
||
sidebarSettingsLink: '#sidebarSettingsLink',
|
||
|
||
// TOC Section
|
||
sidebarTocSection: '#sidebarTocSection',
|
||
sidebarTocToggle: '#sidebarTocToggle',
|
||
sidebarTocList: '#sidebarTocList',
|
||
originalTocList: '#toc-list',
|
||
|
||
// Sidebar Footer Stats (紧凑布局)
|
||
sidebarReadingProgress: '#sidebarReadingProgress',
|
||
sidebarHighlightCount: '#sidebarHighlightCount',
|
||
sidebarAnnotationCount: '#sidebarAnnotationCount',
|
||
sidebarImageCount: '#sidebarImageCount',
|
||
sidebarFormulaCount: '#sidebarFormulaCount',
|
||
sidebarTableCount: '#sidebarTableCount',
|
||
sidebarWordCount: '#sidebarWordCount',
|
||
sidebarReferenceCount: '#sidebarReferenceCount',
|
||
|
||
// Original Dock Elements
|
||
originalReadingProgress: '#reading-progress-percentage-verbose',
|
||
originalHighlightCount: '#highlight-count',
|
||
originalAnnotationCount: '#annotation-count',
|
||
originalImageCount: '#image-count',
|
||
originalFormulaCount: '#formula-count',
|
||
originalTableCount: '#table-count',
|
||
originalWordCount: '#total-word-count',
|
||
originalReferenceCount: '#reference-count',
|
||
|
||
// Immersive Mode
|
||
immersiveContainer: '#immersive-layout-container',
|
||
immersiveToggleBtn: '#toggle-immersive-btn',
|
||
|
||
// Settings Link
|
||
originalSettingsLink: '#settings-link'
|
||
},
|
||
logos: {
|
||
full: '../../public/h_with_name.svg',
|
||
pure: '../../public/pure.svg'
|
||
}
|
||
};
|
||
|
||
// ==================== 状态管理 ====================
|
||
|
||
let isImmersiveMode = false;
|
||
let isMobile = window.innerWidth < 768;
|
||
|
||
// ==================== DOM 元素缓存 ====================
|
||
|
||
const elements = {};
|
||
|
||
/**
|
||
* 初始化 DOM 元素缓存
|
||
*/
|
||
function cacheElements() {
|
||
for (const [key, selector] of Object.entries(CONFIG.selectors)) {
|
||
elements[key] = document.querySelector(selector);
|
||
}
|
||
}
|
||
|
||
// ==================== 侧边栏显示/隐藏 ====================
|
||
|
||
/**
|
||
* 显示或隐藏 App Shell(根据沉浸模式)
|
||
*/
|
||
function updateAppShellVisibility() {
|
||
if (!elements.appShell) return;
|
||
|
||
if (isImmersiveMode) {
|
||
elements.appShell.style.display = 'none';
|
||
} else {
|
||
elements.appShell.style.display = 'flex';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 监听沉浸模式切换
|
||
*/
|
||
function watchImmersiveMode() {
|
||
if (!elements.immersiveContainer) return;
|
||
|
||
// 使用 MutationObserver 监听 display 样式变化
|
||
const observer = new MutationObserver(mutations => {
|
||
mutations.forEach(mutation => {
|
||
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
||
const display = elements.immersiveContainer.style.display;
|
||
const newImmersiveState = (display !== 'none');
|
||
|
||
if (newImmersiveState !== isImmersiveMode) {
|
||
isImmersiveMode = newImmersiveState;
|
||
updateAppShellVisibility();
|
||
console.log('[Sidebar] Immersive mode:', isImmersiveMode ? 'ON' : 'OFF');
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
observer.observe(elements.immersiveContainer, {
|
||
attributes: true,
|
||
attributeFilter: ['style']
|
||
});
|
||
|
||
// 初始状态
|
||
const initialDisplay = elements.immersiveContainer.style.display;
|
||
isImmersiveMode = (initialDisplay !== 'none');
|
||
updateAppShellVisibility();
|
||
}
|
||
|
||
// ==================== 桌面端侧边栏折叠 ====================
|
||
|
||
/**
|
||
* 设置侧边栏折叠状态
|
||
* @param {boolean} collapsed - 是否折叠
|
||
*/
|
||
function setSidebarCollapsed(collapsed) {
|
||
if (!elements.appSidebar || !elements.sidebarLogo) return;
|
||
|
||
if (collapsed) {
|
||
elements.appSidebar.classList.add('collapsed');
|
||
elements.sidebarLogo.src = CONFIG.logos.pure;
|
||
// 切换图标为"打开"图标
|
||
if (elements.sidebarToggleIcon) {
|
||
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-open');
|
||
}
|
||
} else {
|
||
elements.appSidebar.classList.remove('collapsed');
|
||
elements.sidebarLogo.src = CONFIG.logos.full;
|
||
// 切换图标为"关闭"图标
|
||
if (elements.sidebarToggleIcon) {
|
||
elements.sidebarToggleIcon.setAttribute('icon', 'carbon:side-panel-close');
|
||
}
|
||
}
|
||
|
||
// 保存状态
|
||
localStorage.setItem(CONFIG.storageKeys.sidebarCollapsed, collapsed.toString());
|
||
}
|
||
|
||
/**
|
||
* 初始化桌面端折叠状态
|
||
*/
|
||
function initDesktopCollapse() {
|
||
if (!elements.sidebarToggleBtn) return;
|
||
|
||
// 从 localStorage 读取状态
|
||
const savedState = localStorage.getItem(CONFIG.storageKeys.sidebarCollapsed);
|
||
const isCollapsed = savedState === 'true';
|
||
setSidebarCollapsed(isCollapsed);
|
||
|
||
// 绑定切换按钮
|
||
elements.sidebarToggleBtn.addEventListener('click', () => {
|
||
const currentlyCollapsed = elements.appSidebar.classList.contains('collapsed');
|
||
setSidebarCollapsed(!currentlyCollapsed);
|
||
});
|
||
}
|
||
|
||
// ==================== 移动端侧边栏显示/隐藏 ====================
|
||
|
||
/**
|
||
* 打开移动端侧边栏
|
||
*/
|
||
function openMobileSidebar() {
|
||
if (!elements.appSidebar || !elements.sidebarOverlay) return;
|
||
|
||
elements.appSidebar.classList.add('mobile-open');
|
||
elements.sidebarOverlay.classList.add('show');
|
||
}
|
||
|
||
/**
|
||
* 关闭移动端侧边栏
|
||
*/
|
||
function closeMobileSidebar() {
|
||
if (!elements.appSidebar || !elements.sidebarOverlay) return;
|
||
|
||
elements.appSidebar.classList.remove('mobile-open');
|
||
elements.sidebarOverlay.classList.remove('show');
|
||
}
|
||
|
||
/**
|
||
* 初始化移动端侧边栏
|
||
*/
|
||
function initMobileSidebar() {
|
||
// 绑定打开按钮
|
||
if (elements.mobileMenuBtn) {
|
||
elements.mobileMenuBtn.addEventListener('click', openMobileSidebar);
|
||
}
|
||
|
||
// 绑定关闭按钮
|
||
if (elements.sidebarCloseBtn) {
|
||
elements.sidebarCloseBtn.addEventListener('click', closeMobileSidebar);
|
||
}
|
||
|
||
// 绑定遮罩层点击关闭
|
||
if (elements.sidebarOverlay) {
|
||
elements.sidebarOverlay.addEventListener('click', closeMobileSidebar);
|
||
}
|
||
}
|
||
|
||
// ==================== 可折叠区域 ====================
|
||
|
||
/**
|
||
* 切换可折叠区域的展开/收起状态
|
||
* @param {HTMLElement} section - 区域元素
|
||
* @param {string} storageKey - localStorage 键名
|
||
*/
|
||
function toggleSection(section, storageKey) {
|
||
if (!section) return;
|
||
|
||
const isExpanded = section.classList.contains('expanded');
|
||
const newState = !isExpanded;
|
||
|
||
if (newState) {
|
||
section.classList.add('expanded');
|
||
} else {
|
||
section.classList.remove('expanded');
|
||
}
|
||
|
||
// 保存状态
|
||
if (storageKey) {
|
||
localStorage.setItem(storageKey, newState.toString());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 初始化可折叠区域
|
||
* @param {string} sectionSelector - 区域选择器
|
||
* @param {string} toggleSelector - 切换按钮选择器
|
||
* @param {string} storageKey - localStorage 键名
|
||
*/
|
||
function initCollapsibleSection(sectionSelector, toggleSelector, storageKey) {
|
||
const section = document.querySelector(sectionSelector);
|
||
const toggle = document.querySelector(toggleSelector);
|
||
|
||
if (!section || !toggle) return;
|
||
|
||
// 从 localStorage 读取状态
|
||
const savedState = localStorage.getItem(storageKey);
|
||
const isExpanded = savedState !== 'false'; // 默认展开
|
||
|
||
if (isExpanded) {
|
||
section.classList.add('expanded');
|
||
} else {
|
||
section.classList.remove('expanded');
|
||
}
|
||
|
||
// 绑定切换按钮
|
||
toggle.addEventListener('click', () => {
|
||
toggleSection(section, storageKey);
|
||
});
|
||
}
|
||
|
||
// ==================== TOC 数据同步 ====================
|
||
|
||
/**
|
||
* 从原有 TOC 同步数据到侧边栏 TOC
|
||
*/
|
||
function syncTocData() {
|
||
if (!elements.originalTocList || !elements.sidebarTocList) return;
|
||
|
||
const originalItems = elements.originalTocList.querySelectorAll('li');
|
||
|
||
if (originalItems.length === 0) {
|
||
// 显示空状态
|
||
elements.sidebarTocList.innerHTML = `
|
||
<li class="sidebar-empty-state">
|
||
<div class="sidebar-empty-icon">📄</div>
|
||
<div>暂无目录</div>
|
||
</li>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// 清空现有内容
|
||
elements.sidebarTocList.innerHTML = '';
|
||
|
||
// 复制 TOC 项目
|
||
originalItems.forEach(item => {
|
||
const originalLink = item.querySelector('a');
|
||
if (!originalLink) return;
|
||
|
||
// 获取并清理文本内容
|
||
const text = originalLink.textContent.trim();
|
||
const href = originalLink.href;
|
||
|
||
// 跳过占位符和空标题
|
||
// 1. 空标题
|
||
// 2. "未命名章节"占位符
|
||
// 3. placeholder- 开头的ID(占位符标识)
|
||
if (!text ||
|
||
text === '未命名章节' ||
|
||
text === 'undefined' ||
|
||
text === 'null' ||
|
||
href.includes('#placeholder-')) {
|
||
return;
|
||
}
|
||
|
||
const li = document.createElement('li');
|
||
li.className = 'sidebar-toc-item';
|
||
|
||
// 复制层级类(toc-h2, toc-h3 等)
|
||
for (const className of item.classList) {
|
||
if (className.startsWith('toc-')) {
|
||
li.classList.add(className);
|
||
}
|
||
}
|
||
|
||
const link = document.createElement('a');
|
||
link.href = href;
|
||
link.className = 'sidebar-toc-link';
|
||
link.textContent = text; // 使用清理后的文本
|
||
|
||
// 复制 active 状态
|
||
if (originalLink.classList.contains('active')) {
|
||
link.classList.add('active');
|
||
}
|
||
|
||
// 绑定点击事件(跳转后自动关闭移动端侧边栏)
|
||
link.addEventListener('click', (e) => {
|
||
// 触发原有 TOC 链接的点击事件(保持原有滚动逻辑)
|
||
originalLink.click();
|
||
e.preventDefault();
|
||
|
||
// 移动端关闭侧边栏
|
||
if (isMobile) {
|
||
closeMobileSidebar();
|
||
}
|
||
});
|
||
|
||
li.appendChild(link);
|
||
elements.sidebarTocList.appendChild(li);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 监听原有 TOC 的变化并同步
|
||
*/
|
||
function watchTocChanges() {
|
||
if (!elements.originalTocList) return;
|
||
|
||
// 使用 MutationObserver 监听 TOC 列表的变化
|
||
const observer = new MutationObserver(() => {
|
||
syncTocData();
|
||
});
|
||
|
||
observer.observe(elements.originalTocList, {
|
||
childList: true,
|
||
subtree: true,
|
||
attributes: true,
|
||
attributeFilter: ['class'] // 监听 active 类变化
|
||
});
|
||
|
||
// 初始同步
|
||
syncTocData();
|
||
}
|
||
|
||
// ==================== Dock 数据同步 ====================
|
||
|
||
/**
|
||
* 同步单个统计数据
|
||
* @param {string} originalSelector - 原始元素选择器
|
||
* @param {string} sidebarSelector - 侧边栏元素选择器
|
||
* @param {string} suffix - 后缀(如 '%')
|
||
*/
|
||
function syncStat(originalSelector, sidebarSelector, suffix = '') {
|
||
const originalEl = document.querySelector(originalSelector);
|
||
const sidebarEl = document.querySelector(sidebarSelector);
|
||
|
||
if (!originalEl || !sidebarEl) return;
|
||
|
||
let value = originalEl.textContent.trim();
|
||
|
||
// 如果需要添加后缀,先检查是否已存在
|
||
if (suffix && !value.endsWith(suffix)) {
|
||
value = value + suffix;
|
||
}
|
||
|
||
sidebarEl.textContent = value;
|
||
}
|
||
|
||
/**
|
||
* 检查 Dock 数据是否已初始化(不全是 0)
|
||
*/
|
||
function isDockDataReady() {
|
||
const wordCountEl = document.querySelector(CONFIG.selectors.originalWordCount);
|
||
const wordCount = wordCountEl?.textContent.trim();
|
||
|
||
// 如果总字数不是 0,说明 Dock 数据已计算完成
|
||
// 总字数是最可靠的指标,因为任何文档都应该有字数
|
||
return wordCount && wordCount !== '0';
|
||
}
|
||
|
||
/**
|
||
* 获取当前滚动容器(从 main 分支的 DockLogic 移植 + 非沉浸模式修正)
|
||
*/
|
||
function getCurrentScrollableElement() {
|
||
if (window.ImmersiveLayout && window.ImmersiveLayout.isActive && window.ImmersiveLayout.isActive()) {
|
||
// 沉浸模式:滚动容器取决于当前活动的标签页
|
||
const immersiveMainArea = document.getElementById('immersive-main-content-area');
|
||
if (immersiveMainArea) {
|
||
// 1. 优先查找带有内联样式的 .tab-content
|
||
const tabContentScroller = immersiveMainArea.querySelector('.tab-content[style*="overflow-y: auto"], .tab-content[style*="overflow: auto"]');
|
||
if (tabContentScroller) return tabContentScroller;
|
||
|
||
// 2. 查找活动的内容包装器
|
||
const activeTabContent = immersiveMainArea.querySelector('.tab-content .content-wrapper, .tab-content .chunk-compare-container');
|
||
if (activeTabContent) {
|
||
// 检查父元素 .tab-content 是否可滚动
|
||
const tabContentParent = activeTabContent.closest('.tab-content');
|
||
if (tabContentParent && (tabContentParent.style.overflowY === 'auto' ||
|
||
tabContentParent.style.overflow === 'auto' ||
|
||
getComputedStyle(tabContentParent).overflowY === 'auto')) {
|
||
return tabContentParent;
|
||
}
|
||
// 回退到内容包装器本身
|
||
return activeTabContent;
|
||
}
|
||
|
||
// 3. 尝试 .container
|
||
const mainContainerInImmersive = immersiveMainArea.querySelector('.container');
|
||
if (mainContainerInImmersive) return mainContainerInImmersive;
|
||
|
||
// 4. 最后回退到 immersiveMainArea
|
||
return immersiveMainArea;
|
||
}
|
||
}
|
||
|
||
// 非沉浸模式:使用 .app-main(侧边栏布局中的主内容区)
|
||
const appMain = document.querySelector('.app-main');
|
||
if (appMain) {
|
||
return appMain;
|
||
}
|
||
|
||
// 最终回退到 document.documentElement(旧布局或特殊情况)
|
||
return document.documentElement;
|
||
}
|
||
|
||
/**
|
||
* 计算并更新侧边栏的阅读进度(直接计算,不依赖 Dock)
|
||
*/
|
||
function updateSidebarReadingProgress() {
|
||
const progressEl = document.querySelector(CONFIG.selectors.sidebarReadingProgress);
|
||
if (!progressEl) return;
|
||
|
||
// 动态获取当前滚动容器
|
||
const scrollableElement = getCurrentScrollableElement();
|
||
if (!scrollableElement) {
|
||
console.warn('[Sidebar] No scrollable element found');
|
||
return;
|
||
}
|
||
|
||
const scrollTop = scrollableElement.scrollTop;
|
||
const scrollHeight = scrollableElement.scrollHeight;
|
||
const clientHeight = scrollableElement.clientHeight;
|
||
|
||
// 如果内容适合视口(无滚动条),显示 100%
|
||
if (scrollHeight <= clientHeight) {
|
||
progressEl.textContent = '100%';
|
||
return;
|
||
}
|
||
|
||
// 计算滚动百分比
|
||
const maxScrollTop = scrollHeight - clientHeight;
|
||
const scrollFraction = maxScrollTop > 0 ? (scrollTop / maxScrollTop) : 0;
|
||
const percentage = Math.min(100, Math.max(0, Math.round(scrollFraction * 100)));
|
||
|
||
progressEl.textContent = percentage + '%';
|
||
}
|
||
|
||
/**
|
||
* 同步所有 Dock 统计数据到侧边栏
|
||
*/
|
||
function syncDockData() {
|
||
console.log('[Sidebar] Syncing Dock data...');
|
||
|
||
// 阅读进度:直接计算,不从 Dock 同步
|
||
updateSidebarReadingProgress();
|
||
|
||
// 其他统计:从 Dock 同步
|
||
syncStat(CONFIG.selectors.originalHighlightCount, CONFIG.selectors.sidebarHighlightCount);
|
||
syncStat(CONFIG.selectors.originalAnnotationCount, CONFIG.selectors.sidebarAnnotationCount);
|
||
syncStat(CONFIG.selectors.originalImageCount, CONFIG.selectors.sidebarImageCount);
|
||
syncStat(CONFIG.selectors.originalFormulaCount, CONFIG.selectors.sidebarFormulaCount);
|
||
syncStat(CONFIG.selectors.originalTableCount, CONFIG.selectors.sidebarTableCount);
|
||
syncStat(CONFIG.selectors.originalWordCount, CONFIG.selectors.sidebarWordCount);
|
||
syncStat(CONFIG.selectors.originalReferenceCount, CONFIG.selectors.sidebarReferenceCount);
|
||
|
||
// 调试:输出同步后的值
|
||
const progressEl = document.querySelector(CONFIG.selectors.sidebarReadingProgress);
|
||
const highlightEl = document.querySelector(CONFIG.selectors.sidebarHighlightCount);
|
||
const wordCountEl = document.querySelector(CONFIG.selectors.sidebarWordCount);
|
||
console.log('[Sidebar] Synced values - Progress:', progressEl?.textContent, 'Highlights:', highlightEl?.textContent, 'Words:', wordCountEl?.textContent);
|
||
}
|
||
|
||
/**
|
||
* 监听原有 Dock 的变化并同步
|
||
*/
|
||
function watchDockChanges() {
|
||
// 使用 MutationObserver 监听 Dock 元素的变化
|
||
const observer = new MutationObserver(() => {
|
||
syncDockData();
|
||
});
|
||
|
||
// 监听所有原始统计元素
|
||
const selectors = [
|
||
CONFIG.selectors.originalReadingProgress,
|
||
CONFIG.selectors.originalHighlightCount,
|
||
CONFIG.selectors.originalAnnotationCount,
|
||
CONFIG.selectors.originalImageCount,
|
||
CONFIG.selectors.originalFormulaCount,
|
||
CONFIG.selectors.originalTableCount,
|
||
CONFIG.selectors.originalWordCount,
|
||
CONFIG.selectors.originalReferenceCount
|
||
];
|
||
|
||
selectors.forEach(selector => {
|
||
const el = document.querySelector(selector);
|
||
if (el) {
|
||
observer.observe(el, {
|
||
childList: true,
|
||
characterData: true,
|
||
subtree: true
|
||
});
|
||
}
|
||
});
|
||
|
||
// 智能初始同步:等待 Dock 数据初始化完成
|
||
let retryCount = 0;
|
||
const maxRetries = 20; // 最多重试 20 次
|
||
const retryDelay = 200; // 每次间隔 200ms
|
||
|
||
function attemptInitialSync() {
|
||
if (isDockDataReady()) {
|
||
console.log('[Sidebar] Dock data is ready, syncing now');
|
||
syncDockData();
|
||
} else {
|
||
retryCount++;
|
||
if (retryCount < maxRetries) {
|
||
console.log(`[Sidebar] Dock data not ready yet, retry ${retryCount}/${maxRetries} in ${retryDelay}ms...`);
|
||
setTimeout(attemptInitialSync, retryDelay);
|
||
} else {
|
||
console.warn('[Sidebar] Dock data still not ready after max retries, syncing anyway');
|
||
syncDockData();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 开始尝试初始同步
|
||
attemptInitialSync();
|
||
}
|
||
|
||
// ==================== 设置链接 ====================
|
||
|
||
/**
|
||
* 绑定侧边栏设置链接到原有设置链接
|
||
*/
|
||
function bindSettingsLink() {
|
||
if (!elements.sidebarSettingsLink || !elements.originalSettingsLink) return;
|
||
|
||
elements.sidebarSettingsLink.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
elements.originalSettingsLink.click();
|
||
|
||
// 移动端关闭侧边栏
|
||
if (isMobile) {
|
||
closeMobileSidebar();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==================== 可点击统计项 ====================
|
||
|
||
/**
|
||
* 绑定可点击统计项(高亮、批注)
|
||
*/
|
||
function bindClickableStats() {
|
||
const clickableStats = document.querySelectorAll('.sidebar-quick-stat');
|
||
|
||
clickableStats.forEach(stat => {
|
||
stat.addEventListener('click', () => {
|
||
const statType = stat.dataset.statType;
|
||
const originalStat = document.querySelector(`.stat-item-clickable[data-stat-type="${statType}"]`);
|
||
|
||
if (originalStat && statType) {
|
||
// 触发原有统计项的点击事件
|
||
originalStat.click();
|
||
|
||
// 移动端关闭侧边栏
|
||
if (isMobile) {
|
||
closeMobileSidebar();
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// ==================== 响应式处理 ====================
|
||
|
||
/**
|
||
* 动态绑定/解绑滚动事件到当前滚动容器
|
||
*/
|
||
let lastScrollableElement = null;
|
||
const debouncedProgressUpdate = debounce(updateSidebarReadingProgress, 100);
|
||
|
||
function bindScrollForCurrentScrollable() {
|
||
// 解绑旧的滚动监听
|
||
if (lastScrollableElement) {
|
||
lastScrollableElement.removeEventListener('scroll', debouncedProgressUpdate);
|
||
lastScrollableElement = null;
|
||
}
|
||
|
||
// 获取当前滚动元素
|
||
const el = getCurrentScrollableElement();
|
||
if (el) {
|
||
console.log(`[Sidebar] 绑定滚动事件到元素:`, el.id || el.className || el.tagName);
|
||
el.addEventListener('scroll', debouncedProgressUpdate);
|
||
lastScrollableElement = el;
|
||
|
||
// 立即更新一次阅读进度
|
||
setTimeout(() => updateSidebarReadingProgress(), 50);
|
||
} else {
|
||
console.warn(`[Sidebar] 未找到可滚动元素,无法绑定滚动事件`);
|
||
}
|
||
}
|
||
|
||
function unbindScrollForCurrentScrollable() {
|
||
if (lastScrollableElement) {
|
||
lastScrollableElement.removeEventListener('scroll', debouncedProgressUpdate);
|
||
lastScrollableElement = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理窗口大小变化
|
||
*/
|
||
function handleResize() {
|
||
const newIsMobile = window.innerWidth < 768;
|
||
|
||
if (newIsMobile !== isMobile) {
|
||
isMobile = newIsMobile;
|
||
|
||
// 切换到桌面端时,关闭移动端侧边栏
|
||
if (!isMobile) {
|
||
closeMobileSidebar();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ==================== 初始化 ====================
|
||
|
||
/**
|
||
* 防抖函数
|
||
*/
|
||
function debounce(func, delay) {
|
||
let timeout;
|
||
return function(...args) {
|
||
clearTimeout(timeout);
|
||
timeout = setTimeout(() => func.apply(this, args), delay);
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 初始化侧边栏集成模块
|
||
*/
|
||
function init() {
|
||
console.log('[Sidebar] Initializing sidebar integration...');
|
||
|
||
// 缓存 DOM 元素
|
||
cacheElements();
|
||
|
||
// 监听沉浸模式切换
|
||
watchImmersiveMode();
|
||
|
||
// 初始化桌面端折叠功能
|
||
initDesktopCollapse();
|
||
|
||
// 初始化移动端侧边栏
|
||
initMobileSidebar();
|
||
|
||
// 初始化可折叠区域
|
||
initCollapsibleSection(
|
||
CONFIG.selectors.sidebarTocSection,
|
||
CONFIG.selectors.sidebarTocToggle,
|
||
CONFIG.storageKeys.tocSectionExpanded
|
||
);
|
||
|
||
// 监听 TOC 变化并同步
|
||
watchTocChanges();
|
||
|
||
// 监听 Dock 变化并同步
|
||
watchDockChanges();
|
||
|
||
// 绑定设置链接
|
||
bindSettingsLink();
|
||
|
||
// 绑定可点击统计项
|
||
bindClickableStats();
|
||
|
||
// 监听窗口大小变化
|
||
window.addEventListener('resize', handleResize);
|
||
|
||
// 动态绑定滚动事件到当前滚动容器
|
||
bindScrollForCurrentScrollable();
|
||
|
||
// 监听标签页切换事件(重新绑定滚动事件)
|
||
// 使用 MutationObserver 监听 .tab-content 的变化
|
||
const tabContent = document.getElementById('tabContent');
|
||
if (tabContent) {
|
||
const tabObserver = new MutationObserver(() => {
|
||
console.log('[Sidebar] Tab content changed, rebinding scroll event');
|
||
bindScrollForCurrentScrollable();
|
||
});
|
||
|
||
tabObserver.observe(tabContent, {
|
||
childList: true,
|
||
subtree: false
|
||
});
|
||
}
|
||
|
||
// 监听沉浸模式切换(重新绑定滚动事件)
|
||
window.addEventListener('immersive-mode-changed', () => {
|
||
console.log('[Sidebar] Immersive mode changed, rebinding scroll event');
|
||
setTimeout(() => bindScrollForCurrentScrollable(), 100);
|
||
});
|
||
|
||
console.log('[Sidebar] Sidebar integration initialized successfully');
|
||
}
|
||
|
||
// ==================== 导出 ====================
|
||
|
||
// DOM 加载完成后初始化
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
|
||
// 导出到全局(供调试使用)
|
||
window.SidebarIntegration = {
|
||
syncTocData,
|
||
syncDockData,
|
||
updateReadingProgress: updateSidebarReadingProgress,
|
||
bindScrollEvent: bindScrollForCurrentScrollable,
|
||
unbindScrollEvent: unbindScrollForCurrentScrollable,
|
||
openMobileSidebar,
|
||
closeMobileSidebar,
|
||
setSidebarCollapsed
|
||
};
|
||
|
||
})();
|