| 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; |
|
|
| |
| 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); |
| } |
| }); |
|
|
| |
| |
| try { |
| document.dispatchEvent( |
| new CustomEvent('menav:pageChanged', { |
| detail: { |
| pageId, |
| }, |
| }) |
| ); |
| } catch (error) { |
| |
| } |
|
|
| |
| if (state.isInitialLoad) { |
| state.isInitialLoad = false; |
| document.body.classList.add('loaded'); |
| } |
| }); |
|
|
| |
| content.scrollTop = 0; |
|
|
| |
| if (!skipSearchReset) { |
| searchInput.value = ''; |
| search.resetSearch(); |
| } |
| } |
|
|
| |
| window.addEventListener('load', () => { |
| |
| 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'); |
|
|
| |
| 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; |
| } |
|
|
| |
| if (isSidebarCollapsed()) { |
| clearSubmenuPanel(); |
| return; |
| } |
|
|
| const entry = submenuByPageId.get(id); |
| if (!entry || !entry.wrapper || !entry.submenu) { |
| clearSubmenuPanel(); |
| return; |
| } |
|
|
| |
| 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) { |
| |
| } |
| }; |
|
|
| 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; |
|
|
| |
| 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; |
|
|
| |
| try { |
| |
| const contentElement = document.querySelector('.content'); |
|
|
| if (contentElement && contentElement.scrollHeight > contentElement.clientHeight) { |
| |
| const rect = targetCategory.getBoundingClientRect(); |
| const containerRect = contentElement.getBoundingClientRect(); |
|
|
| |
| 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(); |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| const rawPageIdFromUrl = getRawPageIdFromUrl(); |
| const validatedPageIdFromUrl = getPageIdFromUrl(); |
| const initialPageId = |
| validatedPageIdFromUrl || (isValidPageId(state.homePageId) ? state.homePageId : 'home'); |
|
|
| setActiveNavByPageId(initialPageId); |
| showPage(initialPageId); |
|
|
| |
| if (rawPageIdFromUrl && !validatedPageIdFromUrl) { |
| setUrlState({ pageId: initialPageId }, { replace: true }); |
| } |
|
|
| |
| const initialHash = getHashFromUrl(); |
| if (initialHash) { |
| setTimeout(() => { |
| const found = scrollToCategoryInPage(initialPageId, { |
| categoryId: initialHash, |
| categoryName: initialHash, |
| }); |
|
|
| |
| 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(); |
|
|
| |
| 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(pageId); |
| } |
|
|
| const prevPageId = state.currentPageId; |
| showPage(pageId); |
|
|
| |
| 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(); |
|
|
| |
| 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); |
| |
| setUrlState({ pageId, hash: '' }, { replace: true }); |
|
|
| |
| setTimeout(() => { |
| const found = scrollToCategoryInPage(pageId, { categoryId, categoryName }); |
| if (!found) return; |
|
|
| |
| 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'); |
| } |
|
|
| |
| if ('requestIdleCallback' in window) { |
| requestIdleCallback(() => search.initSearchIndex()); |
| } else { |
| setTimeout(search.initSearchIndex, 1000); |
| } |
| }); |
|
|
| return { showPage }; |
| }; |
|
|