PageSourceSearch

https://www.paleophilatelie.eu/components/header/header.js

js paleophilatelie.eu collected 2026-10-02 12:28:11 UTC 10,891 bytes, 298 lines download raw bytes

1/**
2 * Header Component JavaScript
3 * Smithsonian Magazine Style - Handles mobile menu, search overlay, and scroll behavior
4 */
5
6(function() {
7    'use strict';
8
9    // DOM Elements
10    let header, menuToggle, mainNav, navClose, searchToggle, searchOverlay, searchClose, searchInput;
11    let isMenuOpen = false;
12
13    /**
14     * Initialize header functionality
15     */
16    function init() {
17        // Get DOM elements
18        header = document.querySelector('.site-header');
19        menuToggle = document.getElementById('menu-toggle');
20        mainNav = document.getElementById('main-nav');
21        navClose = document.getElementById('nav-close');
22        searchToggle = document.getElementById('search-toggle');
23        searchOverlay = document.getElementById('search-overlay');
24        searchClose = document.getElementById('search-close');
25        searchInput = searchOverlay ? searchOverlay.querySelector('.search-overlay-input') : null;
26
27        if (!header) return;
28
29        // Initialize features
30        initMobileMenu();
31        initMobileDropdowns();
32        initSearchOverlay();
33        initStickyHeader();
34        highlightActiveNavLink();
35    }
36
37    /**
38     * Mobile Menu Toggle
39     */
40    function initMobileMenu() {
41        if (!menuToggle || !mainNav) return;
42
43        menuToggle.addEventListener('click', toggleMenu);
44
45        // Close button inside drawer
46        if (navClose) {
47            navClose.addEventListener('click', closeMenu);
48        }
49
50        // Close menu when clicking overlay (the ::before pseudo-element)
51        mainNav.addEventListener('click', (e) => {
52            if (e.target === mainNav) {
53                closeMenu();
54            }
55        });
56
57        // Close on escape key
58        document.addEventListener('keydown', (e) => {
59            if (e.key === 'Escape' && isMenuOpen) {
60                closeMenu();
61            }
62        });
63
64        // Close menu on resize to desktop
65        window.addEventListener('resize', debounce(() => {
66            if (window.innerWidth >= 992 && isMenuOpen) {
67                closeMenu();
68            }
69        }, 100));
70    }
71
72    function toggleMenu() {
73        isMenuOpen = !isMenuOpen;
74
75        menuToggle.setAttribute('aria-expanded', isMenuOpen);
76        mainNav.classList.toggle('is-open', isMenuOpen);
77        document.body.classList.toggle('menu-open', isMenuOpen);
78    }
79
80    function closeMenu() {
81        isMenuOpen = false;
82        menuToggle.setAttribute('aria-expanded', 'false');
83        mainNav.classList.remove('is-open');
84        document.body.classList.remove('menu-open');
85
86        // Close all mobile dropdowns when menu closes
87        const openDropdowns = document.querySelectorAll('.nav-item.has-dropdown.is-open');
88        openDropdowns.forEach(item => item.classList.remove('is-open'));
89    }
90
91    /**
92     * Mobile Dropdown Toggle (for touch/click on mobile)
93     * Creates separate toggle buttons so that links remain clickable for navigation
94     * while the arrow button expands/collapses the submenu.
95     */
96    function initMobileDropdowns() {
97        // Guard: don't add toggles twice (init may run multiple times)
98        if (document.querySelector('.nav-dropdown-toggle')) return;
99
100        var chevronSvg = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>';
101        var chevronSmallSvg = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M6 9l6 6 6-6"/></svg>';
102
103        // Top-level dropdowns
104        var dropdownItems = document.querySelectorAll('.nav-item.has-dropdown');
105
106        dropdownItems.forEach(function(item) {
107            var link = item.querySelector(':scope > .nav-link');
108            if (!link) return;
109
110            // Create a separate toggle button for expanding/collapsing submenu
111            var toggle = document.createElement('button');
112            toggle.className = 'nav-dropdown-toggle';
113            toggle.setAttribute('aria-label', 'Toggle submenu');
114            toggle.setAttribute('type', 'button');
115            toggle.innerHTML = chevronSvg;
116            link.after(toggle);
117
118            toggle.addEventListener('click', function(e) {
119                e.preventDefault();
120                e.stopPropagation();
121
122                if (window.innerWidth < 992) {
123                    // Close other open dropdowns
124                    dropdownItems.forEach(function(otherItem) {
125                        if (otherItem !== item && otherItem.classList.contains('is-open')) {
126                            otherItem.classList.remove('is-open');
127                        }
128                    });
129                    item.classList.toggle('is-open');
130                }
131            });
132        });
133
134        // Nested sub-dropdowns
135        var subdropdownItems = document.querySelectorAll('.has-subdropdown');
136
137        subdropdownItems.forEach(function(item) {
138            var link = item.querySelector(':scope > .nav-dropdown__link');
139            if (!link) return;
140
141            var toggle = document.createElement('button');
142            toggle.className = 'nav-subdropdown-toggle';
143            toggle.setAttribute('aria-label', 'Toggle submenu');
144            toggle.setAttribute('type', 'button');
145            toggle.innerHTML = chevronSmallSvg;
146            link.after(toggle);
147
148            toggle.addEventListener('click', function(e) {
149                e.preventDefault();
150                e.stopPropagation();
151
152                if (window.innerWidth < 992) {
153                    subdropdownItems.forEach(function(otherItem) {
154                        if (otherItem !== item && otherItem.classList.contains('is-open')) {
155                            otherItem.classList.remove('is-open');
156                        }
157                    });
158                    item.classList.toggle('is-open');
159                }
160            });
161        });
162    }
163
164    /**
165     * Search Overlay Toggle
166     */
167    function initSearchOverlay() {
168        if (!searchToggle || !searchOverlay) return;
169
170        searchToggle.addEventListener('click', (e) => {
171            e.preventDefault();
172            openSearchOverlay();
173        });
174
175        if (searchClose) {
176            searchClose.addEventListener('click', closeSearchOverlay);
177        }
178
179        // Close when clicking overlay background
180        searchOverlay.addEventListener('click', (e) => {
181            if (e.target === searchOverlay) {
182                closeSearchOverlay();
183            }
184        });
185
186        // Close on escape key
187        document.addEventListener('keydown', (e) => {
188            if (e.key === 'Escape' && searchOverlay.classList.contains('is-open')) {
189                closeSearchOverlay();
190            }
191        });
192    }
193
194    function openSearchOverlay() {
195        searchOverlay.classList.add('is-open');
196        document.body.style.overflow = 'hidden';
197
198        // Focus on input
199        if (searchInput) {
200            setTimeout(() => searchInput.focus(), 100);
201        }
202    }
203
204    function closeSearchOverlay() {
205        searchOverlay.classList.remove('is-open');
206        document.body.style.overflow = '';
207    }
208
209    /**
210     * Sticky header with scroll behavior
211     */
212    function initStickyHeader() {
213        const headerTop = document.querySelector('.header-top');
214        if (!headerTop) return;
215
216        window.addEventListener('scroll', () => {
217            const currentScrollY = window.pageYOffset;
218
219            // Add shadow when scrolled
220            if (currentScrollY > 10) {
221                headerTop.classList.add('scrolled');
222            } else {
223                headerTop.classList.remove('scrolled');
224            }
225        }, { passive: true });
226    }
227
228    /**
229     * Highlight active navigation link based on centralized config
230     */
231    function highlightActiveNavLink() {
232        var rule = window.getNavHighlightRule ? window.getNavHighlightRule() : null;
233        if (!rule) return;
234
235        // Build a set of target hrefs (lowercased, no hash)
236        var targets = {};
237        for (var i = 0; i < rule.nav.length; i++) {
238            targets[rule.nav[i].toLowerCase().split('#')[0]] = true;
239        }
240
241        // The section parent is the first nav item (used for top-level nav highlighting).
242        // Dropdown items matching ONLY the section parent should NOT be highlighted
243        // (e.g., "About Website" shouldn't highlight when on "News" page).
244        var sectionParent = rule.nav.length > 1 ? rule.nav[0].toLowerCase().split('#')[0] : null;
245
246        // Helper: normalize a link's href to site-root-relative form
247        function normalizeHref(href) {
248            if (!href) return '';
249            return href.replace(/\.\.\//g, '').split('#')[0].toLowerCase();
250        }
251
252        // Scan all nav links at every level
253        var allLinks = document.querySelectorAll('.nav-link, .nav-dropdown__link, .nav-subdropdown__link');
254        allLinks.forEach(function(link) {
255            var href = link.getAttribute('href');
256            if (!href || href === '#') return;
257            var norm = normalizeHref(href);
258            if (!targets[norm]) return;
259
260            // Determine link type and apply the correct active class
261            if (link.classList.contains('nav-subdropdown__link')) {
262                link.classList.add('nav-subdropdown__link--active');
263            } else if (link.classList.contains('nav-dropdown__link')) {
264                // Skip dropdown items that only match the section parent
265                // (e.g. don't highlight "About Website" when on "News" page)
266                if (sectionParent && norm === sectionParent) return;
267                link.classList.add('nav-dropdown__link--active');
268            } else if (link.classList.contains('nav-link')) {
269                link.classList.add('nav-link--active');
270            }
271        });
272    }
273
274    /**
275     * Debounce utility
276     */
277    function debounce(func, wait) {
278        let timeout;
279        return function executedFunction(...args) {
280            const later = () => {
281                clearTimeout(timeout);
282                func(...args);
283            };
284            clearTimeout(timeout);
285            timeout = setTimeout(later, wait);
286        };
287    }
288
289    // Initialize when DOM is ready
290    if (document.readyState === 'loading') {
291        document.addEventListener('DOMContentLoaded', init);
292    } else {
293        init();
294    }
295
296    // Re-initialize after components are loaded
297    document.addEventListener('componentsLoaded', init);
298})();

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.