const nested = require('../nested'); module.exports = function initRouting(state, dom, api) { const { ui, search } = api; const { searchInput, content, sidebar } = dom; function showPage(pageId, skipSearchReset = false) { if (state.currentPageId === pageId && !skipSearchReset && !state.isInitialLoad) return; state.currentPageId = pageId; // 使用 RAF 确保动画流畅 requestAnimationFrame(() => { if (!state.pages) { state.pages = document.querySelectorAll('.page'); } state.pages.forEach((page) => { const shouldBeActive = page.id === pageId; if (shouldBeActive !== page.classList.contains('active')) { page.classList.toggle('active', shouldBeActive); } }); // 通知:页面已切换(供按需组件初始化,如 github-calendar) // 注意:必须在 active class 切换之后触发,否则监听方可能认为页面仍不可见。 try { document.dispatchEvent( new CustomEvent('menav:pageChanged', { detail: { pageId, }, }) ); } catch (error) { // ignore } // 初始加载完成后设置标志 if (state.isInitialLoad) { state.isInitialLoad = false; document.body.classList.add('loaded'); } }); // 重置滚动位置并更新进度条 content.scrollTop = 0; // 只有在非搜索状态下才重置搜索 if (!skipSearchReset) { searchInput.value = ''; search.resetSearch(); } } // 初始化(在 window load 时执行) window.addEventListener('load', () => { // 获取可能在 HTML 生成后才存在的 DOM 元素 const categories = document.querySelectorAll('.category'); const navItems = document.querySelectorAll('.nav-item'); const navItemWrappers = document.querySelectorAll('.nav-item-wrapper'); const submenuItems = document.querySelectorAll('.submenu-item'); state.pages = document.querySelectorAll('.page'); // 方案 A:用 ?page= 作为页面深链接(兼容 GitHub Pages 静态托管) const normalizeText = (value) => String(value === null || value === undefined ? '' : value).trim(); // 侧边栏子菜单面板:将“当前页面的分类列表”放到独立区域滚动,避免挤压“页面列表” const submenuPanel = document.querySelector('.sidebar-submenu-panel'); const submenuByPageId = new Map(); let submenuPanelPageId = ''; navItemWrappers.forEach((wrapper) => { const nav = wrapper.querySelector('.nav-item'); const pageId = nav ? normalizeText(nav.getAttribute('data-page')) : ''; const submenu = wrapper.querySelector('.submenu'); if (!pageId || !submenu) return; submenuByPageId.set(pageId, { wrapper, submenu }); }); const isSidebarCollapsed = () => Boolean(sidebar && sidebar.classList.contains('collapsed')); const clearSubmenuPanel = () => { if (!submenuPanel) return; const pageId = normalizeText(submenuPanelPageId); if (pageId) { const entry = submenuByPageId.get(pageId); if (entry && entry.wrapper && entry.submenu) { entry.wrapper.appendChild(entry.submenu); } } submenuPanel.textContent = ''; submenuPanelPageId = ''; }; const renderSubmenuPanelForPage = (pageId) => { if (!submenuPanel) return; const id = normalizeText(pageId); if (!id) { clearSubmenuPanel(); return; } // 折叠态:子菜单使用 hover 弹出,不使用面板 if (isSidebarCollapsed()) { clearSubmenuPanel(); return; } const entry = submenuByPageId.get(id); if (!entry || !entry.wrapper || !entry.submenu) { clearSubmenuPanel(); return; } // 仅当 wrapper 处于 expanded 时展示(与 UI 行为保持一致) if (!entry.wrapper.classList.contains('expanded')) { clearSubmenuPanel(); return; } if (normalizeText(submenuPanelPageId) === id && submenuPanel.contains(entry.submenu)) { return; } clearSubmenuPanel(); submenuPanel.appendChild(entry.submenu); submenuPanelPageId = id; }; // 监听侧边栏折叠状态变化:折叠时归还子菜单;展开时渲染当前页子菜单 if (sidebar && typeof MutationObserver === 'function') { const observer = new MutationObserver(() => { const activeNav = document.querySelector('.nav-item.active'); const activePageId = activeNav ? normalizeText(activeNav.getAttribute('data-page')) : ''; renderSubmenuPanelForPage(activePageId); }); observer.observe(sidebar, { attributes: true, attributeFilter: ['class'] }); } const isValidPageId = (pageId) => { const id = normalizeText(pageId); if (!id) return false; const el = document.getElementById(id); return Boolean(el && el.classList && el.classList.contains('page')); }; const getRawPageIdFromUrl = () => { try { const url = new URL(window.location.href); return normalizeText(url.searchParams.get('page')); } catch (error) { return ''; } }; const getPageIdFromUrl = () => { try { const url = new URL(window.location.href); const pageId = normalizeText(url.searchParams.get('page')); return isValidPageId(pageId) ? pageId : ''; } catch (error) { return ''; } }; const setUrlState = (next, options = {}) => { const { replace = true } = options; try { const url = new URL(window.location.href); if (next && typeof next.pageId === 'string') { const pageId = normalizeText(next.pageId); if (pageId) { url.searchParams.set('page', pageId); } else { url.searchParams.delete('page'); } } if (next && Object.prototype.hasOwnProperty.call(next, 'hash')) { const hash = normalizeText(next.hash); url.hash = hash ? `#${hash}` : ''; } const nextUrl = `${url.pathname}${url.search}${url.hash}`; const fn = replace ? history.replaceState : history.pushState; fn.call(history, null, '', nextUrl); } catch (error) { // 忽略 URL/History API 异常,避免影响主流程 } }; const setActiveNavByPageId = (pageId) => { const id = normalizeText(pageId); let activeItem = null; navItems.forEach((nav) => { const isActive = nav.getAttribute('data-page') === id; nav.classList.toggle('active', isActive); if (isActive) activeItem = nav; }); // 同步子菜单展开状态:只展开当前激活项 navItemWrappers.forEach((wrapper) => { const nav = wrapper.querySelector('.nav-item'); if (!nav) return; const pageId = normalizeText(nav.getAttribute('data-page')); const hasSubmenu = pageId ? submenuByPageId.has(pageId) : false; const shouldExpand = hasSubmenu && nav === activeItem; wrapper.classList.toggle('expanded', shouldExpand); }); renderSubmenuPanelForPage(id); }; const escapeSelector = (value) => { if (value === null || value === undefined) return ''; const text = String(value); if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text); // 回退:尽量避免打断选择器(不追求完全覆盖所有边界字符) return text.replace(/[^a-zA-Z0-9_\u00A0-\uFFFF-]/g, '\\$&'); }; const escapeAttrValue = (value) => { if (value === null || value === undefined) return ''; return String(value).replace(/\\/g, '\\\\').replace(/\"/g, '\\"'); }; const getHashFromUrl = () => { const rawHash = window.location.hash ? String(window.location.hash).slice(1) : ''; if (!rawHash) return ''; try { return decodeURIComponent(rawHash).trim(); } catch (error) { return rawHash.trim(); } }; const scrollToCategoryInPage = (pageId, options = {}) => { const id = normalizeText(pageId); if (!id) return false; const targetPage = document.getElementById(id); if (!targetPage) return false; const categoryId = normalizeText(options.categoryId); const categoryName = normalizeText(options.categoryName); let targetCategory = null; // 优先使用 slug/data-id 精准定位(解决重复命名始终命中第一个的问题) if (categoryId) { const escapedId = escapeSelector(categoryId); targetCategory = targetPage.querySelector(`#${escapedId}`) || targetPage.querySelector( `[data-type="category"][data-id="${escapeAttrValue(categoryId)}"]` ); } // 回退:旧逻辑按文本包含匹配(兼容旧页面/旧数据) if (!targetCategory && categoryName) { targetCategory = Array.from(targetPage.querySelectorAll('.category h2')).find((heading) => heading.textContent.trim().includes(categoryName) ); } if (!targetCategory) return false; // 优化的滚动实现:滚动到使目标分类位于视口 1/4 处(更靠近顶部位置) try { // 直接获取所需元素和属性,减少重复查询 const contentElement = document.querySelector('.content'); if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) { // 获取目标元素相对于内容区域的位置 const rect = targetCategory.getBoundingClientRect(); const containerRect = contentElement.getBoundingClientRect(); // 计算目标应该在视口中的位置(视口高度的 1/4 处) const desiredPosition = containerRect.height / 4; // 计算需要滚动的位置 const scrollPosition = contentElement.scrollTop + rect.top - containerRect.top - desiredPosition; // 执行滚动 contentElement.scrollTo({ top: scrollPosition, behavior: 'smooth', }); } else { // 回退到基本滚动方式 targetCategory.scrollIntoView({ behavior: 'smooth', block: 'center' }); } } catch (error) { console.error('Error during scroll:', error); // 回退到基本滚动方式 targetCategory.scrollIntoView({ behavior: 'smooth', block: 'center' }); } return true; }; // 初始化主题 ui.initTheme(); // 初始化侧边栏状态 ui.initSidebarState(); // 初始化搜索引擎选择 search.initSearchEngine(); // 初始化 MeNav 对象版本信息 try { const config = window.MeNav.getConfig(); if (config && config.version) { window.MeNav.version = config.version; console.log('MeNav API initialized with version:', config.version); } } catch (error) { console.error('Error initializing MeNav API:', error); } // 立即执行初始化,不再使用 requestAnimationFrame 延迟 // 支持 ?page= 直接打开对应页面;无效时回退到首页 const rawPageIdFromUrl = getRawPageIdFromUrl(); const validatedPageIdFromUrl = getPageIdFromUrl(); const initialPageId = validatedPageIdFromUrl || (isValidPageId(state.homePageId) ? state.homePageId : 'home'); setActiveNavByPageId(initialPageId); showPage(initialPageId); // 当输入了不存在的 page id 时,自动纠正 URL,避免“内容回退但地址栏仍错误” if (rawPageIdFromUrl && !validatedPageIdFromUrl) { setUrlState({ pageId: initialPageId }, { replace: true }); } // 初始深链接:支持 ?page=# const initialHash = getHashFromUrl(); if (initialHash) { setTimeout(() => { const found = scrollToCategoryInPage(initialPageId, { categoryId: initialHash, categoryName: initialHash, }); // hash 存在但未命中时,不做强制修正,避免误伤其他用途的 hash if (!found) return; }, 50); } // 添加载入动画 categories.forEach((category, index) => { setTimeout(() => { category.style.opacity = '1'; }, index * 100); }); // 导航项点击效果 navItems.forEach((item) => { item.addEventListener('click', (e) => { if (item.getAttribute('target') === '_blank') return; e.preventDefault(); // 获取当前项的父级 wrapper const wrapper = item.closest('.nav-item-wrapper'); const pageId = normalizeText(item.getAttribute('data-page')); const hasSubmenu = Boolean(wrapper && pageId && submenuByPageId.has(pageId)); if (!pageId) return; // 处理子菜单展开/折叠 if (hasSubmenu && item.classList.contains('active')) { // 当前页:保持子菜单展开状态,不做任何操作 return; } else { // 切换页面:统一由 setActiveNavByPageId 管理 active/expanded setActiveNavByPageId(pageId); } const prevPageId = state.currentPageId; showPage(pageId); // 切换页面时同步 URL(清空旧 hash,避免跨页残留) if (normalizeText(prevPageId) !== normalizeText(pageId)) { setUrlState({ pageId, hash: '' }, { replace: true }); } // 在移动端视图下点击导航项后自动收起侧边栏 if (ui.isMobile() && state.isSidebarOpen && !hasSubmenu) { ui.closeAllPanels(); } }); }); // 子菜单项点击效果 submenuItems.forEach((item) => { item.addEventListener('click', (e) => { e.preventDefault(); // 获取页面 ID 和分类名称 const pageId = item.getAttribute('data-page'); const categoryName = item.getAttribute('data-category'); const categoryId = item.getAttribute('data-category-id'); if (pageId) { // 清除所有子菜单项的激活状态 submenuItems.forEach((subItem) => { subItem.classList.remove('active'); }); // 激活当前子菜单项 item.classList.add('active'); // 激活导航项并同步子菜单展开状态 setActiveNavByPageId(pageId); // 显示对应页面 showPage(pageId); // 先同步 page 参数并清空旧 hash,避免跨页残留;后续若找到分类再写入新的 hash setUrlState({ pageId, hash: '' }, { replace: true }); // 等待页面切换完成后滚动到对应分类 setTimeout(() => { const found = scrollToCategoryInPage(pageId, { categoryId, categoryName }); if (!found) return; // 由于对子菜单 click 做了 preventDefault,这里手动同步 hash(不触发浏览器默认跳转) const nextHash = normalizeText(categoryId) || normalizeText(categoryName); if (nextHash) { setUrlState({ pageId, hash: nextHash }, { replace: true }); } }, 25); // 延迟时间 // 在移动端视图下点击子菜单项后自动收起侧边栏 if (ui.isMobile() && state.isSidebarOpen) { ui.closeAllPanels(); } } }); }); // 初始化嵌套分类功能 nested.initializeNestedCategories(); // 初始化分类切换按钮 const categoryToggleBtn = document.getElementById('category-toggle'); if (categoryToggleBtn) { categoryToggleBtn.addEventListener('click', function () { window.MeNav.toggleCategories(); }); } else { console.error('Category toggle button not found'); } // 初始化搜索索引(使用 requestIdleCallback 或 setTimeout 延迟初始化,避免影响页面加载) if ('requestIdleCallback' in window) { requestIdleCallback(() => search.initSearchIndex()); } else { setTimeout(search.initSearchIndex, 1000); } }); return { showPage }; };