1import { qs, qsa } from "../utils/dom.js"; 2import { NAV_LINKS } from "../components/nav.js"; 3 4function initScrollProgress() { 5 const bar = qs("#scrollProgressBar"); 6 if (!bar) return; 7 const update = () => { 8 const { scrollTop, scrollHeight, clientHeight } = document.documentElement; 9 const max = scrollHeight - clientHeight; 10 bar.style.width = `${max > 0 ? (scrollTop / max) * 100 : 0}%`; 11 }; 12 document.addEventListener("scroll", update, { passive: true }); 13 update(); 14} 15 16function initNavShrink() { 17 const nav = qs("#siteNav"); 18 if (!nav) return; 19 const update = () => nav.classList.toggle("is-scrolled", window.scrollY > 24); 20 document.addEventListener("scroll", update, { passive: true }); 21 update(); 22} 23 24function initBackToTop() { 25 const btn = qs("#backToTop"); 26 if (!btn) return; 27 const update = () => btn.classList.toggle("is-visible", window.scrollY > 480); 28 document.addEventListener("scroll", update, { passive: true }); 29 btn.addEventListener("click", () => { 30 window.scrollTo({ top: 0, behavior: "smooth" }); 31 }); 32 update(); 33} 34 35function initMobileMenu() { 36 const toggle = qs("#navMobileToggle"); 37 const menu = qs("#navMobileMenu"); 38 if (!toggle || !menu) return; 39 40 const close = () => { 41 menu.classList.remove("is-open"); 42 menu.setAttribute("aria-hidden", "true"); 43 toggle.setAttribute("aria-expanded", "false"); 44 toggle.querySelector("i").className = "fa-solid fa-bars"; 45 }; 46 47 const open = () => { 48 menu.classList.add("is-open"); 49 menu.setAttribute("aria-hidden", "false"); 50 toggle.setAttribute("aria-expanded", "true"); 51 toggle.querySelector("i").className = "fa-solid fa-xmark"; 52 }; 53 54 toggle.addEventListener("click", () => { 55 menu.classList.contains("is-open") ? close() : open(); 56 }); 57 58 menu.addEventListener("click", (e) => { 59 if (e.target.closest("a")) close(); 60 }); 61 62 document.addEventListener("keydown", (e) => { 63 if (e.key === "Escape") close(); 64 }); 65} 66 67function initScrollspy() { 68 const sections = NAV_LINKS.map((link) => document.getElementById(link.id)).filter(Boolean); 69 if (!sections.length) return; 70 71 const setActive = (id) => { 72 qsa("[data-nav-link]").forEach((link) => { 73 const isActive = link.dataset.navLink === id; 74 link.classList.toggle("is-active", isActive); 75 if (isActive) link.setAttribute("aria-current", "true"); 76 else link.removeAttribute("aria-current"); 77 }); 78 }; 79 80 const observer = new IntersectionObserver( 81 (entries) => { 82 const visible = entries 83 .filter((entry) => entry.isIntersecting) 84 .sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]; 85 if (visible) setActive(visible.target.id); 86 }, 87 { rootMargin: "-40% 0px -50% 0px", threshold: [0, 0.25, 0.5, 0.75, 1] } 88 ); 89 90 sections.forEach((section) => observer.observe(section)); 91} 92 93export function initNavScroll() { 94 initScrollProgress(); 95 initNavShrink(); 96 initBackToTop(); 97 initMobileMenu(); 98 initScrollspy(); 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.