File size: 5,204 Bytes
f07b5d3 a08c68d f07b5d3 a08c68d f07b5d3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
// 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);
};
|