PageSourceSearch

https://equilens.io/assets/eql/nav.js?v=20260921e

js equilens.io collected 2026-10-03 21:16:36 UTC 6,271 bytes, 179 lines download raw bytes

1// Navigation behaviour for the statically baked nav
2// (scripts/content/sync_nav_static.py bakes the markup at build time).
3
4// Runs immediately after the baked navbar, before main is parsed. This keeps
5// enhanced header geometry stable from the first content paint; a failed or
6// disabled script leaves the visible native-link fallback intact.
7function initNavFeatures() {
8  document.querySelector('.navbar')?.classList.add('is-enhanced');
9  // Set active nav link based on current page
10  const currentPath = window.location.pathname;
11  document.querySelectorAll('.nav-link').forEach(link => {
12    const linkPath = new URL(link.href, window.location.origin).pathname;
13    if (linkPath === currentPath || (currentPath !== '/' && linkPath !== '/' && currentPath.startsWith(linkPath))) {
14      link.setAttribute('aria-current', 'page');
15    }
16  });
17
18  // Mobile nav toggle (accessible)
19  const toggle = document.querySelector('.nav-toggle');
20  const links = document.getElementById('nav-links');
21  if (toggle && links) {
22    const setOpen = (open) => {
23      links.setAttribute('data-open', String(open));
24      toggle.setAttribute('aria-expanded', String(open));
25      document.body.classList.toggle('nav-open', open);
26    };
27    toggle.addEventListener('click', () => {
28      const open = links.getAttribute('data-open') === 'true';
29      setOpen(!open);
30    });
31    links.querySelectorAll('a').forEach(link => {
32      link.addEventListener('click', () => setOpen(false));
33    });
34    document.addEventListener('keydown', (e) => {
35      if (e.key === 'Escape' && links.getAttribute('data-open') === 'true') {
36        setOpen(false);
37        toggle.focus();
38      }
39    });
40  }
41
42}
43
44// The page's contents and anchors become available after parsing. Disclosures
45// start closed in HTML; opening a wide sidebar does not shift the reading column.
46function initPageNavigation() {
47  const wideLayout = window.matchMedia('(min-width: 1100px)');
48  document.querySelectorAll('.toc-disclosure').forEach(details => {
49    details.open = wideLayout.matches;
50    wideLayout.addEventListener('change', event => { details.open = event.matches; });
51  });
52
53  // Guarded smooth scroll (hash links only) - respects scroll-padding-top
54  document.querySelectorAll('a[href^="#"]:not(.skip-to-content)').forEach(a => {
55    const h = a.getAttribute('href');
56    if (!h || h === '#') return;
57    a.addEventListener('click', e => {
58      const t = document.getElementById(decodeURIComponent(h.slice(1)));
59      if (t) {
60        e.preventDefault();
61        const disclosure = a.closest('.toc-disclosure');
62        if (disclosure && !wideLayout.matches) disclosure.open = false;
63        const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
64        t.scrollIntoView({ behavior: reduceMotion ? 'instant' : 'smooth', block: 'start' });
65        if (!t.hasAttribute('tabindex')) t.setAttribute('tabindex', '-1');
66        t.focus({ preventScroll: true });
67        // Update URL hash and trigger active state update
68        history.pushState(null, '', h);
69        // Update TOC active state immediately after click
70        updateTocActiveState(h);
71        // Pause scroll spy to prevent it from overriding the click-based update
72        pauseScrollSpy(800);
73      }
74    });
75  });
76}
77
78// Flag to temporarily pause scroll spy after click
79let scrollSpyPaused = false;
80let scrollSpyPauseTimeout = null;
81
82// Update TOC active state for a specific hash
83function updateTocActiveState(hash) {
84  const toc = document.querySelector('.toc');
85  if (!toc) return;
86
87  const links = toc.querySelectorAll('.toc-link[href^="#"]');
88  links.forEach(link => {
89    link.removeAttribute('aria-current');
90    if (link.getAttribute('href') === hash) {
91      link.setAttribute('aria-current', 'location');
92    }
93  });
94}
95
96// Pause scroll spy temporarily (used after click)
97function pauseScrollSpy(duration) {
98  scrollSpyPaused = true;
99  if (scrollSpyPauseTimeout) clearTimeout(scrollSpyPauseTimeout);
100  scrollSpyPauseTimeout = setTimeout(() => {
101    scrollSpyPaused = false;
102  }, duration);
103}
104
105// Scroll-spy for the TOC: highlights active section as user scrolls
106function initScrollSpy() {
107  const subnav = document.querySelector('.toc');
108  if (!subnav) return;
109
110  const links = subnav.querySelectorAll('.toc-link[href^="#"]');
111  if (!links.length) return;
112
113  // Get all target sections
114  const sections = [];
115  links.forEach(link => {
116    const targetId = link.getAttribute('href').substring(1);
117    const section = document.getElementById(targetId);
118    if (section) {
119      sections.push({ id: targetId, el: section, link: link });
120    }
121  });
122
123  if (!sections.length) return;
124
125  // Throttled scroll handler
126  let ticking = false;
127  function onScroll() {
128    if (ticking) return;
129    ticking = true;
130    requestAnimationFrame(() => {
131      updateActiveSection();
132      ticking = false;
133    });
134  }
135
136  function updateActiveSection() {
137    // Skip if scroll spy is paused (e.g., after a click)
138    if (scrollSpyPaused) return;
139
140    const scrollPadding = parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0;
141    const scrollPos = window.scrollY + scrollPadding + 17;
142    let activeSection = sections[0];
143
144    // Check if we're at the bottom of the page
145    const atBottom = (window.innerHeight + window.scrollY) >= (document.body.scrollHeight - 50);
146
147    if (atBottom && sections.length > 0) {
148      // If at bottom, highlight the last section
149      activeSection = sections[sections.length - 1];
150    } else {
151      // Find the section closest to or above the viewport
152      for (const section of sections) {
153        if (section.el.offsetTop <= scrollPos) {
154          activeSection = section;
155        }
156      }
157    }
158
159    // Update aria-current
160    links.forEach(link => link.removeAttribute('aria-current'));
161    if (activeSection) {
162      activeSection.link.setAttribute('aria-current', 'location');
163    }
164  }
165
166  window.addEventListener('scroll', onScroll, { passive: true });
167  updateActiveSection(); // Initial state
168}
169
170initNavFeatures();
171function initPageFeatures() {
172  initPageNavigation();
173  initScrollSpy();
174}
175if (document.readyState === 'loading') {
176  document.addEventListener('DOMContentLoaded', initPageFeatures, { once: true });
177} else {
178  initPageFeatures();
179}

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.