(function () { 'use strict'; const DATA = window.__WEBOOK__; const navTree = DATA.nav || []; const defaultFile = DATA.defaultFile; const initialContent = DATA.initialContent; // === DOM Elements === const sidebarNav = document.getElementById('sidebarNav'); const searchInput = document.getElementById('searchInput'); const btnToggleSidebar = document.getElementById('btnToggleSidebar'); const btnToggleTheme = document.getElementById('btnToggleTheme'); const contentBody = document.getElementById('contentBody'); const emptyState = document.getElementById('emptyState'); const markdownContent = document.getElementById('markdownContent'); const articleTitle = document.getElementById('articleTitle'); const articleMeta = document.getElementById('articleMeta'); const articleBody = document.getElementById('articleBody'); const tocNav = document.getElementById('tocNav'); const tocPanel = document.getElementById('tocPanel'); const loadingBar = document.getElementById('loadingBar'); const sidebar = document.getElementById('sidebar'); const mobileNavBtn = document.getElementById('mobileNavBtn'); let currentFile = null; // === Theme === function getTheme() { return localStorage.getItem('webook-theme') || 'light'; } function setTheme(theme) { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('webook-theme', theme); btnToggleTheme.textContent = theme === 'dark' ? '☀︎' : '☾'; // Swap highlight theme const hlLink = document.querySelector('.theme-highlight'); if (hlLink) { hlLink.href = theme === 'dark' ? '/_webook/highlight-dark.css' : '/_webook/highlight-light.css'; } } function toggleTheme() { const current = getTheme(); setTheme(current === 'light' ? 'dark' : 'light'); } // Initialize theme setTheme(getTheme()); // === Sidebar Toggle === function getSidebarVisible() { return localStorage.getItem('webook-sidebar') !== 'hidden'; } function setSidebarVisible(visible) { if (visible) { sidebar.classList.remove('collapsed'); localStorage.setItem('webook-sidebar', 'visible'); btnToggleSidebar.textContent = '☰'; } else { sidebar.classList.add('collapsed'); localStorage.setItem('webook-sidebar', 'hidden'); btnToggleSidebar.textContent = '☷'; } } function toggleSidebar() { setSidebarVisible(sidebar.classList.contains('collapsed')); } // Initialize sidebar state setSidebarVisible(getSidebarVisible()); // === Mobile Sidebar === let sidebarOverlay = null; function createOverlay() { if (sidebarOverlay) return; sidebarOverlay = document.createElement('div'); sidebarOverlay.className = 'sidebar-overlay'; sidebarOverlay.addEventListener('click', hideMobileSidebar); document.body.appendChild(sidebarOverlay); } function showMobileSidebar() { createOverlay(); sidebar.classList.add('mobile-visible'); sidebar.classList.remove('collapsed'); sidebarOverlay.classList.add('visible'); } function hideMobileSidebar() { sidebar.classList.remove('mobile-visible'); if (sidebarOverlay) { sidebarOverlay.classList.remove('visible'); } // Restore collapsed state if needed if (!getSidebarVisible()) { sidebar.classList.add('collapsed'); } } // === Navigation Rendering === function renderNavTree(items, level = 0) { if (!items || items.length === 0) return ''; let html = ''; return html; } function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } // Render nav sidebarNav.innerHTML = renderNavTree(navTree); // === Navigation Events === sidebarNav.addEventListener('click', function (e) { // Directory toggle const dirHeader = e.target.closest('.nav-dir-header'); if (dirHeader) { const dirItem = dirHeader.parentElement; dirItem.classList.toggle('collapsed'); return; } // File click const fileItem = e.target.closest('.nav-file'); if (fileItem) { const filePath = fileItem.getAttribute('data-file-path'); loadFile(filePath); setActiveFile(fileItem); // On mobile, hide sidebar after selection if (window.innerWidth <= 768) { hideMobileSidebar(); } } }); function setActiveFile(fileItem) { const prev = sidebarNav.querySelector('.nav-file.active'); if (prev) prev.classList.remove('active'); if (fileItem) fileItem.classList.add('active'); } // === Search === searchInput.addEventListener('input', function () { const query = this.value.trim().toLowerCase(); if (!query) { // Show all sidebarNav.querySelectorAll('.nav-hidden').forEach(el => el.classList.remove('nav-hidden')); removeEmptyMessage(); return; } let hasVisible = false; // Process directories const dirs = sidebarNav.querySelectorAll('.nav-dir'); dirs.forEach(dir => { let dirHasVisible = false; const files = dir.querySelectorAll('.nav-file'); files.forEach(file => { const name = file.querySelector('.nav-file-name').textContent.toLowerCase(); if (name.includes(query)) { file.classList.remove('nav-hidden'); dirHasVisible = true; } else { file.classList.add('nav-hidden'); } }); // Show/hide directory if (dirHasVisible) { dir.classList.remove('nav-hidden', 'collapsed'); hasVisible = true; } else { dir.classList.add('nav-hidden'); } }); // Root-level files const rootFiles = sidebarNav.querySelectorAll(':scope > .nav-tree > .nav-file'); rootFiles.forEach(file => { const name = file.querySelector('.nav-file-name').textContent.toLowerCase(); if (name.includes(query)) { file.classList.remove('nav-hidden'); hasVisible = true; } else { file.classList.add('nav-hidden'); } }); if (!hasVisible) { showEmptyMessage('未找到匹配的文件'); } else { removeEmptyMessage(); } }); function showEmptyMessage(msg) { removeEmptyMessage(); const div = document.createElement('div'); div.className = 'nav-empty'; div.id = 'navEmptyMsg'; div.textContent = msg; sidebarNav.appendChild(div); } function removeEmptyMessage() { const existing = document.getElementById('navEmptyMsg'); if (existing) existing.remove(); } // === File Loading === async function loadFile(filePath) { if (currentFile === filePath) return; currentFile = filePath; showLoading(); try { const resp = await fetch('/api/file?path=' + encodeURIComponent(filePath)); const json = await resp.json(); if (!json.success) { throw new Error(json.error || 'Failed to load file'); } const data = json.data; // Update content emptyState.style.display = 'none'; markdownContent.style.display = ''; articleTitle.textContent = data.title; articleBody.innerHTML = data.html; buildTOC(); // Update meta const created = formatDate(data.meta.created); const modified = formatDate(data.meta.modified); const wordCount = data.meta.wordCount; articleMeta.innerHTML = ` 📅 创建: ${created} ✏️ 修改: ${modified} 📝 字数: ${wordCount.toLocaleString()} `; // Scroll to top contentBody.scrollTop = 0; // Update URL hash if (history.pushState) { history.pushState(null, '', '#' + encodeURIComponent(filePath)); } // Apply syntax highlighting to code blocks in content applyHighlighting(); } catch (err) { emptyState.style.display = 'flex'; emptyState.querySelector('p').textContent = '加载失败: ' + err.message; markdownContent.style.display = 'none'; } finally { hideLoading(); } } function showLoading() { loadingBar.classList.add('visible'); loadingBar.classList.remove('done'); } function hideLoading() { loadingBar.classList.add('done'); setTimeout(() => { loadingBar.classList.remove('visible', 'done'); }, 350); } function formatDate(isoStr) { if (!isoStr) return '-'; const d = new Date(isoStr); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); const h = String(d.getHours()).padStart(2, '0'); const min = String(d.getMinutes()).padStart(2, '0'); return `${y}-${m}-${day} ${h}:${min}`; } function applyHighlighting() { // highlight.js is loaded via CDN or bundled, but we handle it server-side. // If any code blocks need client-side highlighting, do it here. // For now, server-side rendering handles highlighting. } function slugifyHeading(text, index) { return 'toc-' + text .toLowerCase() .replace(/['"`]/g, '') .replace(/[^a-z0-9\u4e00-\u9fff]+/g, '-') .replace(/^-+|-+$/g, '') + '-' + index; } function buildTOC() { if (!tocNav || !tocPanel) return; const headings = articleBody.querySelectorAll('h2, h3, h4'); if (!headings.length) { tocPanel.classList.add('toc-empty'); tocNav.innerHTML = '
当前页面没有可用目录
'; return; } tocPanel.classList.remove('toc-empty'); const items = []; headings.forEach((heading, index) => { const level = Number(heading.tagName.slice(1)); if (!heading.id) { heading.id = slugifyHeading(heading.textContent || 'section', index); } items.push({ id: heading.id, text: heading.textContent.trim(), level }); }); tocNav.innerHTML = items.map((item) => ` ${escapeHtml(item.text)} `).join(''); } // === Initial Load === if (initialContent) { emptyState.style.display = 'none'; markdownContent.style.display = ''; articleTitle.textContent = initialContent.title; articleBody.innerHTML = initialContent.html; buildTOC(); const created = formatDate(initialContent.meta.created); const modified = formatDate(initialContent.meta.modified); const wordCount = initialContent.meta.wordCount; articleMeta.innerHTML = ` 📅 创建: ${created} ✏️ 修改: ${modified} 📝 字数: ${wordCount.toLocaleString()} `; currentFile = defaultFile; if (defaultFile && history.replaceState) { history.replaceState(null, '', '#' + encodeURIComponent(defaultFile)); } } // === Hash-based navigation === function handleHashChange() { const hash = decodeURIComponent(window.location.hash.slice(1)); if (hash && hash !== currentFile) { loadFile(hash); // Find and activate the nav item const fileItem = sidebarNav.querySelector(`[data-file-path="${CSS.escape(hash)}"]`); if (fileItem) { setActiveFile(fileItem); } } } window.addEventListener('hashchange', handleHashChange); // === Event Listeners === btnToggleTheme.addEventListener('click', toggleTheme); btnToggleSidebar.addEventListener('click', toggleSidebar); mobileNavBtn.addEventListener('click', showMobileSidebar); // === Keyboard Shortcuts === document.addEventListener('keydown', function (e) { // Ctrl+K or Cmd+K: focus search if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); searchInput.focus(); searchInput.select(); } // Escape: clear search if (e.key === 'Escape' && document.activeElement === searchInput) { searchInput.value = ''; searchInput.dispatchEvent(new Event('input')); searchInput.blur(); } }); // Close mobile sidebar on Escape document.addEventListener('keydown', function (e) { if (e.key === 'Escape' && sidebar.classList.contains('mobile-visible')) { hideMobileSidebar(); } }); })();