/** * 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 = ` `; 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 }; })();