PageSourceSearch

https://vinaysomawat.github.io/src/services/nav-scroll.js

js vinaysomawat.github.io collected 2026-10-03 10:08:45 UTC 2,963 bytes, 99 lines download raw bytes

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.