1// public/js/layout.js 2 3async function loadFragment(targetId, url) { 4 const container = document.getElementById(targetId); 5 if (!container) return; 6 7 try { 8 const res = await fetch(url, { cache: 'no-store' }); 9 if (!res.ok) return; 10 const html = await res.text(); 11 container.innerHTML = html; 12 13 if (window.i18n) { 14 await window.i18n.ready; 15 window.i18n.applyTranslations(container); 16 } 17 18 if (targetId === 'app-header') { 19 setupHeaderNavActiveState(); 20 setupHeaderNavToggle(); 21 setupLangSwitch(); 22 setupAdminNavVisibility(); 23 } 24 } catch (err) { 25 console.error('loadFragment error', targetId, url, err); 26 } 27} 28 29async function setupAdminNavVisibility() { 30 const items = document.querySelectorAll('[data-admin-nav]'); 31 if (!items.length) return; 32 33 try { 34 const res = await fetch('/api/public/admin-nav', { cache: 'no-store' }); 35 if (!res.ok) return; 36 const data = await res.json(); 37 if (data.ok && !data.showAdminLogin) { 38 items.forEach((el) => el.remove()); 39 } 40 } catch (err) { 41 console.error('setupAdminNavVisibility error', err); 42 } 43} 44 45function setupLangSwitch() { 46 const buttons = document.querySelectorAll('.lang-btn[data-lang]'); 47 if (!buttons.length) return; 48 49 const current = window.i18n ? window.i18n.lang : 'zh-Hant'; 50 buttons.forEach((btn) => { 51 btn.classList.toggle('is-active', btn.dataset.lang === current); 52 btn.addEventListener('click', () => { 53 if (btn.dataset.lang === current) return; 54 window.i18n?.setLang(btn.dataset.lang); 55 }); 56 }); 57} 58 59function setupHeaderNavToggle() { 60 const toggle = document.querySelector('[data-nav-toggle]'); 61 const nav = document.querySelector('.site-nav'); 62 if (!toggle || !nav) return; 63 64 toggle.addEventListener('click', () => { 65 const expanded = toggle.getAttribute('aria-expanded') === 'true'; 66 const next = !expanded; 67 toggle.setAttribute('aria-expanded', String(next)); 68 if (next) { 69 nav.classList.add('is-open'); 70 } else { 71 nav.classList.remove('is-open'); 72 } 73 }); 74} 75 76function setupHeaderNavActiveState() { 77 const path = window.location.pathname || '/'; 78 const links = document.querySelectorAll('.site-nav a[href]'); 79 links.forEach((link) => { 80 const href = link.getAttribute('href'); 81 if (!href) return; 82 83 // match exact, or prefix match for sub-paths (with a '/' boundary so 84 // e.g. '/booking.html' doesn't false-match '/booking-history.html') 85 if ( 86 href === path || 87 (href !== '/' && 88 path.startsWith(href) && 89 (path.length === href.length || path[href.length] === '/')) 90 ) { 91 link.classList.add('is-active'); 92 } 93 }); 94} 95 96document.addEventListener('DOMContentLoaded', () => { 97 loadFragment('app-header', '/partials/app-header.html'); 98 loadFragment('app-footer', '/partials/app-footer.html'); 99});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.