/** * 統一導航加載器 v7 - 移除所有複製代碼 * 在所有頁面的 末尾一次性加載 * * 功能: * - 從Firebase Firestore動態加載導航菜單 * - 支持LocalStorage快取(24小時) * - 自動構建桌面和行動版本導航 * - 提供降級支持 */ (function () { 'use strict'; const CK = 'nav_v7'; const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24小時 // 清除舊版本cache ['nav_cache', 'nav_cache_v2', 'nav_cache_v3', 'nav_cache_v4', 'nav_cache_v5', 'nav_v5', 'nav_v6'] .forEach(k => { try { localStorage.removeItem(k); } catch(e) {} }); function isIndex() { const p = window.location.pathname; return p.endsWith('index.html') || p === '/' || p.endsWith('/'); } // ── XSS 防線:escape HTML 與 URL ── function escHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&').replace(//g, '>') .replace(/"/g, '"').replace(/'/g, '''); } function safeUrl(u) { const s = String(u == null ? '' : u).trim(); if (/^\s*(javascript|data|vbscript):/i.test(s)) return '#'; return s; } function fixLink(link) { if (!link) return '#'; const safe = safeUrl(link); return safe.startsWith('#') && !isIndex() ? 'index.html' + safe : safe; } function buildNavHTML(items) { if (!items || !items.length) return { desktop: '', mobile: '' }; const desktopHTML = items.map(item => { if (item.type === 'dropdown' && item.subItems?.length > 0) { const subHTML = item.subItems.map(sub => `${escHtml(sub.title)}` ).join(''); return `
${escHtml(item.title)}
`; } return `${escHtml(item.title)}`; }).join(''); const mobileHTML = items.map(item => { if (item.type === 'dropdown' && item.subItems?.length > 0) { const subHTML = item.subItems.map(sub => `${escHtml(sub.title)}` ).join(''); return `
${escHtml(item.title)}
${subHTML}
`; } return `${escHtml(item.title)}`; }).join(''); return { desktop: desktopHTML, mobile: mobileHTML }; } function loadCachedNav() { try { const cached = JSON.parse(localStorage.getItem(CK)); if (cached && (Date.now() - cached.timestamp) < CACHE_EXPIRY) { return cached.items; } } catch (e) {} return null; } function saveNav(items) { try { localStorage.setItem(CK, JSON.stringify({ items: items, timestamp: Date.now() })); } catch (e) {} } function renderNav(items) { const { desktop, mobile } = buildNavHTML(items); const desktopContainer = document.getElementById('dynamic-nav-container'); const mobileContainer = document.getElementById('dynamic-mobile-nav'); if (desktopContainer) desktopContainer.innerHTML = desktop; if (mobileContainer) mobileContainer.innerHTML = mobile; } // 立即用cache渲染(無閃爍) const cached = loadCachedNav(); if (cached) { document.addEventListener('DOMContentLoaded', () => renderNav(cached)); } // 暴露API給Firebase module使用 window.sharedNav = { load: async function(getPublicCol, getDocs, query, getDocsFromServer) { try { const fetcher = getDocsFromServer || getDocs; const snap = await fetcher(query(getPublicCol("nav_menus"))); const items = []; snap.forEach(doc => { const data = doc.data(); if (data.status !== 'draft') items.push(data); }); if (items.length > 0) { items.sort((a, b) => (a.sortOrder || 99) - (b.sortOrder || 99)); saveNav(items); renderNav(items); } } catch (e) { console.error('Failed to load nav from server:', e); // 回退到cached版本 const fallback = loadCachedNav(); if (fallback) renderNav(fallback); } } }; })();