/** * Glossary Navigation Active State Manager * Handles highlighting active terms as they scroll into view * and syncing navigation with scroll position */ class GlossaryNavigator { constructor(glossarySelector = 'dl.glossary', navSelector = 'nav.glossary-index') { this.glossary = document.querySelector(glossarySelector); this.nav = document.querySelector(navSelector); if (!this.glossary || !this.nav) return; this.terms = this.glossary.querySelectorAll('dt[id]'); this.navList = this.nav.querySelector('ul'); this.activeClass = 'active'; this.currentActive = null; this.init(); this.setupResizeHandler(); } init() { // Set up Intersection Observer with screen-size appropriate margins const observerOptions = { root: null, // viewport rootMargin: this.getRootMargin(), threshold: 0 }; this.observer = new IntersectionObserver( (entries) => this.handleIntersection(entries), observerOptions ); // Observe all terms this.terms.forEach(term => this.observer.observe(term)); // Also handle manual scroll for edge cases this.handleScroll = this.debounce(() => this.checkActiveTerm(), 100); window.addEventListener('scroll', this.handleScroll, { passive: true }); } getRootMargin() { // On larger screens: centered (50% from top and bottom) return '-50% 0px -50% 0px'; } setupResizeHandler() { let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { // Reinitialize observer with new margins on resize this.reinitialize(); }, 250); }); } reinitialize() { // Disconnect old observer if (this.observer) { this.observer.disconnect(); } // Create new observer with updated margins this.init(); } handleIntersection(entries) { // Find the entry that's intersecting const intersecting = entries.find(entry => entry.isIntersecting); if (intersecting) { this.setActive(intersecting.target); } } checkActiveTerm() { // Fallback method to find which term is in the trigger zone const remInPixels = parseFloat(getComputedStyle(document.documentElement).fontSize); const margin = remInPixels * 4; let closestTerm = null; let closestDistance = Infinity; this.terms.forEach(term => { const rect = term.getBoundingClientRect(); const isInZone = rect.top + rect.height / 2 >= 0 && rect.top + rect.height / 2 <= window.innerHeight; if (isInZone) { // Find closest to the trigger point const triggerPoint = window.innerHeight / 2; const distance = Math.abs(rect.top - triggerPoint); if (distance < closestDistance) { closestDistance = distance; closestTerm = term; } } }); if (closestTerm) { this.setActive(closestTerm); } } setActive(term) { if (this.currentActive === term) return; // Remove active class from previous term if (this.currentActive) { this.currentActive.classList.remove(this.activeClass); } // Add active class to current term term.classList.add(this.activeClass); this.currentActive = term; // Update navigation this.updateNavigation(term.id); } updateNavigation(termId) { // Remove active from all nav links const navLinks = this.nav.querySelectorAll('a'); navLinks.forEach(link => link.classList.remove(this.activeClass)); // Find and activate corresponding nav link const activeLink = this.nav.querySelector(`a[href="#${termId}"]`); if (activeLink) { activeLink.classList.add(this.activeClass); // Scroll the nav list to center the active link this.centerNavItem(activeLink); } } centerNavItem(link) { const listRect = this.navList.getBoundingClientRect(); const linkRect = link.getBoundingClientRect(); // Calculate position to center the link in the nav container const scrollTop = this.navList.scrollTop; const linkOffset = linkRect.top - listRect.top; const centerOffset = (listRect.height / 2) - (linkRect.height / 2); this.navList.scrollTo({ top: scrollTop + linkOffset - centerOffset, behavior: 'smooth' }); } debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } destroy() { if (this.observer) { this.observer.disconnect(); } window.removeEventListener('scroll', this.handleScroll); } } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new GlossaryNavigator(); }); } else { new GlossaryNavigator(); }