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);
};