1const FOCUSABLE = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'; 2 3export function initNav() { 4 const toggle = document.getElementById('nav-toggle'); 5 const nav = document.getElementById('site-nav'); 6 if (!toggle || !nav) return; 7 8 const links = nav.querySelectorAll('a[href^="#"]'); 9 const sections = [...links] 10 .map((link) => { 11 const id = link.getAttribute('href')?.slice(1); 12 return id ? document.getElementById(id) : null; 13 }) 14 .filter(Boolean); 15 16 const close = () => { 17 toggle.setAttribute('aria-expanded', 'false'); 18 nav.classList.remove('is-open'); 19 document.body.style.overflow = ''; 20 }; 21 22 const open = () => { 23 toggle.setAttribute('aria-expanded', 'true'); 24 nav.classList.add('is-open'); 25 document.body.style.overflow = 'hidden'; 26 const first = nav.querySelector(FOCUSABLE); 27 if (first instanceof HTMLElement) first.focus(); 28 }; 29 30 toggle.addEventListener('click', () => { 31 if (nav.classList.contains('is-open')) close(); 32 else open(); 33 }); 34 35 document.addEventListener('keydown', (event) => { 36 if (event.key === 'Escape' && nav.classList.contains('is-open')) { 37 close(); 38 toggle.focus(); 39 } 40 }); 41 42 links.forEach((link) => { 43 link.addEventListener('click', () => close()); 44 }); 45 46 if ('IntersectionObserver' in window && sections.length) { 47 const observer = new IntersectionObserver( 48 (entries) => { 49 entries.forEach((entry) => { 50 if (!entry.isIntersecting) return; 51 const id = entry.target.id; 52 links.forEach((link) => { 53 const match = link.getAttribute('href') === `#${id}`; 54 link.setAttribute('aria-current', match ? 'true' : 'false'); 55 }); 56 }); 57 }, 58 { rootMargin: '-40% 0px -50% 0px', threshold: 0 } 59 ); 60 sections.forEach((section) => observer.observe(section)); 61 } 62}
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.