// stickyNavigation.js - Sticky navigation behavior and hamburger menu // Handles: sticky positioning on scroll + collapse/expand toggle + active section tracking /** * Initializes sticky navigation behavior for a page * @param {string} navElementId - ID of the navigation element (without #) */ export function initializeStickyNavigation(navElementId) { const navElement = document.getElementById(navElementId); if (!navElement) { console.warn(`Navigation element #${navElementId} not found`); return; } const navSection = navElement.closest('section.page-navigation'); if (!navSection) { console.warn('Navigation section wrapper not found'); return; } // Calculate sticky threshold: when nav reaches bottom of header const headerHeight = parseInt(getComputedStyle(document.documentElement) .getPropertyValue('--header-height')) || 100; const stickyThreshold = navSection.offsetTop - headerHeight; // Scroll handler with throttling let scrollTimeout; const handleScroll = () => { const scrollPosition = window.pageYOffset || document.documentElement.scrollTop; navSection.classList.toggle('sticky-nav', scrollPosition >= stickyThreshold); }; window.addEventListener('scroll', () => { if (scrollTimeout) cancelAnimationFrame(scrollTimeout); scrollTimeout = requestAnimationFrame(handleScroll); }); handleScroll(); // Initial check // Initialize active section tracking initializeActiveSectionTracking(); } /** * Track visible sections and update active navigation item * Uses Intersection Observer for efficient scroll tracking */ function initializeActiveSectionTracking() { // Get all navigation links const navLinks = document.querySelectorAll('.page-navigation a[href^="/#"], .page-navigation a[href*="#"]'); if (navLinks.length === 0) return; // Extract section IDs from navigation links const sectionIds = Array.from(navLinks) .map(link => { const href = link.getAttribute('href'); const hashIndex = href.indexOf('#'); return hashIndex >= 0 ? href.substring(hashIndex + 1) : null; }) .filter(id => id); if (sectionIds.length === 0) return; // Get all section elements const sections = sectionIds .map(id => document.getElementById(id)) .filter(section => section); if (sections.length === 0) return; // Track which section is most visible const observerOptions = { root: null, rootMargin: '-20% 0px -60% 0px', // Trigger when section is 20% from top threshold: [0, 0.1, 0.2, 0.5, 0.8, 1] }; let activeSection = null; const observer = new IntersectionObserver((entries) => { // Find the most visible section let mostVisible = null; let maxRatio = 0; entries.forEach(entry => { if (entry.isIntersecting && entry.intersectionRatio > maxRatio) { maxRatio = entry.intersectionRatio; mostVisible = entry.target; } }); // Update active section if changed if (mostVisible && mostVisible !== activeSection) { activeSection = mostVisible; updateActiveNavItem(mostVisible.id); } }, observerOptions); // Observe all sections sections.forEach(section => observer.observe(section)); } /** * Update active navigation item based on current section * @param {string} sectionId - ID of the currently visible section */ function updateActiveNavItem(sectionId) { const navLinks = document.querySelectorAll('.page-navigation a'); navLinks.forEach(link => { const href = link.getAttribute('href'); const linkSectionId = href.substring(href.indexOf('#') + 1); if (linkSectionId === sectionId) { // Add active classes link.classList.add('text-blue-600', 'font-semibold'); link.classList.add('underline', 'decoration-2', 'underline-offset-2'); link.classList.remove('text-gray-700'); } else { // Remove active classes link.classList.remove('text-blue-600', 'font-semibold'); link.classList.remove('underline', 'decoration-2', 'underline-offset-2'); link.classList.add('text-gray-700'); } }); } /** * Toggle navigation visibility (hamburger menu) * Exposed globally for onclick handler */ window.toggleNavigation = function() { const navItems = document.getElementById('nav-items'); const navToggle = document.getElementById('nav-toggle'); const navContainer = document.getElementById('nav-container'); if (!navItems || !navToggle) return; const isCollapsed = navItems.classList.contains('nav-collapsed'); // Toggle classes and ARIA attribute navItems.classList.toggle('nav-collapsed', !isCollapsed); navToggle.setAttribute('aria-expanded', isCollapsed ? 'true' : 'false'); navContainer?.classList.toggle('nav-container-collapsed', !isCollapsed); };