PageSourceSearch

https://www.marq.com/wp-content/themes/marq/js/responsive-navigation.js?ver=1.0.0

js marq.com collected 2026-09-24 08:37:15 UTC 13,737 bytes, 424 lines download raw bytes

1/**
2 * Mobile Navigation - Vanilla JavaScript
3 */
4
5(function() {
6  'use strict';
7
8  // Mobile Menu Controller
9  const MobileMenu = {
10    menuButton: null,
11    responsiveMenu: null,
12    menuOverlay: null,
13    menuItems: null,
14    lastFocusedElement: null,
15    
16    init: function() {
17      this.menuButton = document.getElementById('menu-button');
18      this.responsiveMenu = document.getElementById('responsive-menu');
19      this.menuOverlay = document.querySelector('.menu-overlay');
20      this.menuItems = document.querySelector('.responsive-menu-items');
21      
22      if (!this.menuButton || !this.responsiveMenu || !this.menuItems) {
23        return; // Exit if required elements don't exist
24      }
25      
26      // Ensure all submenus are completely hidden on init
27      this.initializeSubmenus();
28      
29      this.bindEvents();
30      this.setupKeyboardNavigation();
31    },
32    
33    initializeSubmenus: function() {
34      // Force all submenus to be completely hidden on page load
35      const submenus = document.querySelectorAll('.responsive-menu-items .sub-menu');
36      submenus.forEach(submenu => {
37        // Remove active class and let CSS handle hiding
38        submenu.classList.remove('active');
39        submenu.setAttribute('aria-hidden', 'true');
40      });
41      
42      // Ensure parent items are marked as closed
43      const parentItems = document.querySelectorAll('.responsive-menu-items .menu-item-has-children');
44      parentItems.forEach(item => {
45        item.classList.remove('open');
46      });
47      
48      // Set proper ARIA attributes on all submenu toggles
49      const parentLinks = document.querySelectorAll('.responsive-menu-items .menu-item-has-children > a');
50      parentLinks.forEach(link => {
51        link.setAttribute('aria-expanded', 'false');
52      });
53    },
54    
55    bindEvents: function() {
56      const self = this;
57      
58      // Menu button click (toggles open/close)
59      this.menuButton.addEventListener('click', function(e) {
60        e.preventDefault();
61        self.toggleMenu();
62      });
63      
64      // Overlay click to close
65      if (this.menuOverlay) {
66        this.menuOverlay.addEventListener('click', function() {
67          self.closeMenu();
68        });
69      }
70      
71      // Menu link clicks
72      const menuLinks = this.menuItems.querySelectorAll('a');
73      menuLinks.forEach(link => {
74        link.addEventListener('click', function(e) {
75          const parent = this.parentElement;
76          
77          if (parent.classList.contains('menu-item-has-children')) {
78            e.preventDefault();
79            self.toggleSubMenu(this);
80          } else {
81            self.closeMenu();
82          }
83        });
84      });
85      
86      // ESC key to close menu
87      document.addEventListener('keydown', function(e) {
88        if (e.key === 'Escape' && self.responsiveMenu.classList.contains('open')) {
89          self.closeMenu();
90        }
91      });
92    },
93    
94    setupKeyboardNavigation: function() {
95      const self = this;
96      
97      // Arrow key navigation
98      const menuLinks = this.menuItems.querySelectorAll('a');
99      menuLinks.forEach(link => {
100        link.addEventListener('keydown', function(e) {
101          const currentItem = this.parentElement;
102          const allLinks = Array.from(self.menuItems.querySelectorAll('a')).filter(l => {
103            return l.offsetParent !== null; // Only visible links
104          });
105          const currentIndex = allLinks.indexOf(this);
106          
107          switch(e.key) {
108            case 'ArrowDown':
109              e.preventDefault();
110              self.focusNextItem(allLinks, currentIndex);
111              break;
112              
113            case 'ArrowUp':
114              e.preventDefault();
115              self.focusPreviousItem(allLinks, currentIndex);
116              break;
117              
118            case 'ArrowRight':
119              if (currentItem.classList.contains('menu-item-has-children') && 
120                  !currentItem.classList.contains('open')) {
121                e.preventDefault();
122                self.openSubMenu(this);
123              }
124              break;
125              
126            case 'ArrowLeft':
127              if (currentItem.classList.contains('open')) {
128                e.preventDefault();
129                self.closeSubMenu(this);
130              } else if (currentItem.closest('.sub-menu')) {
131                e.preventDefault();
132                const parentItem = currentItem.closest('.menu-item-has-children');
133                if (parentItem) {
134                  const parentLink = parentItem.querySelector(':scope > a');
135                  if (parentLink) parentLink.focus();
136                }
137              }
138              break;
139              
140            case 'Enter':
141            case ' ':
142              if (currentItem.classList.contains('menu-item-has-children')) {
143                e.preventDefault();
144                self.toggleSubMenu(this);
145              }
146              break;
147              
148            case 'Home':
149              e.preventDefault();
150              if (allLinks.length > 0) allLinks[0].focus();
151              break;
152              
153            case 'End':
154              e.preventDefault();
155              if (allLinks.length > 0) allLinks[allLinks.length - 1].focus();
156              break;
157          }
158        });
159      });
160      
161      // Tab trap within open menu
162      this.responsiveMenu.addEventListener('keydown', function(e) {
163        if (e.key === 'Tab' && self.responsiveMenu.classList.contains('open')) {
164          self.handleTabKey(e);
165        }
166      });
167    },
168    
169    toggleMenu: function() {
170      if (this.responsiveMenu.classList.contains('open')) {
171        this.closeMenu();
172      } else {
173        this.openMenu();
174      }
175    },
176    
177    openMenu: function() {
178    this.lastFocusedElement = document.activeElement;
179
180    this.responsiveMenu.classList.add('open');
181    this.menuButton.classList.add('open');
182    document.body.classList.add('responsive-menu-open');
183
184    this.menuButton.setAttribute('aria-expanded', 'true');
185    this.menuItems.setAttribute('aria-hidden', 'false');
186
187    setTimeout(() => {
188        const firstLink = this.menuItems.querySelector('a');
189        if (firstLink) firstLink.focus();
190    }, 300);
191
192    this.announce('Menu opened');
193},
194    
195    closeMenu: function() {
196    const openItems = this.menuItems.querySelectorAll('.menu-item-has-children.open');
197    openItems.forEach(item => {
198        const link = item.querySelector(':scope > a');
199        if (link) this.closeSubMenu(link, false);
200    });
201
202    this.responsiveMenu.classList.remove('open');
203    this.menuButton.classList.remove('open');
204    document.body.classList.remove('responsive-menu-open');
205
206    this.menuButton.setAttribute('aria-expanded', 'false');
207    this.menuItems.setAttribute('aria-hidden', 'true');
208
209    if (this.lastFocusedElement) {
210        this.lastFocusedElement.focus();
211    } else {
212        this.menuButton.focus();
213    }
214
215    this.announce('Menu closed');
216},
217    
218    toggleSubMenu: function(link) {
219      const parent = link.parentElement;
220      
221      if (parent.classList.contains('open')) {
222        this.closeSubMenu(link);
223      } else {
224        this.openSubMenu(link);
225      }
226    },
227    
228    openSubMenu: function(link) {
229      const parent = link.parentElement;
230      const submenu = link.nextElementSibling;
231      
232      if (!submenu || !submenu.classList.contains('sub-menu')) {
233        return;
234      }
235      
236      // Close sibling submenus
237      const siblings = Array.from(parent.parentElement.children);
238      siblings.forEach(sibling => {
239        if (sibling !== parent && sibling.classList.contains('menu-item-has-children') && 
240            sibling.classList.contains('open')) {
241          const siblingLink = sibling.querySelector(':scope > a');
242          if (siblingLink) this.closeSubMenu(siblingLink, false);
243        }
244      });
245      
246      // Open this submenu - add active class (CSS handles animation)
247      submenu.classList.add('active');
248      parent.classList.add('open');
249      
250      // Remove any inline max-height that might be blocking
251      submenu.style.maxHeight = '';
252      
253      // Update ARIA attributes
254      link.setAttribute('aria-expanded', 'true');
255      submenu.setAttribute('aria-hidden', 'false');
256      
257      // Focus first submenu item after animation
258      setTimeout(() => {
259        const firstLink = submenu.querySelector('a');
260        if (firstLink) firstLink.focus();
261      }, 400);
262      
263      // Announce to screen readers
264      const label = link.getAttribute('aria-label') || link.textContent;
265      this.announce(label + ' submenu opened');
266    },
267    
268    closeSubMenu: function(link, shouldFocus = true) {
269      const parent = link.parentElement;
270      const submenu = link.nextElementSibling;
271      
272      if (!submenu || !submenu.classList.contains('sub-menu')) {
273        return;
274      }
275      
276      // Remove classes (CSS handles the collapse animation)
277      submenu.classList.remove('active');
278      parent.classList.remove('open');
279      
280      // Update ARIA attributes
281      link.setAttribute('aria-expanded', 'false');
282      submenu.setAttribute('aria-hidden', 'true');
283      
284      // Return focus to parent link
285      if (shouldFocus) {
286        link.focus();
287      }
288      
289      // Announce to screen readers
290      const label = link.getAttribute('aria-label') || link.textContent;
291      this.announce(label + ' submenu closed');
292    },
293    
294    focusNextItem: function(allLinks, currentIndex) {
295      const nextIndex = (currentIndex + 1) % allLinks.length;
296      allLinks[nextIndex].focus();
297    },
298    
299    focusPreviousItem: function(allLinks, currentIndex) {
300      const prevIndex = currentIndex > 0 ? currentIndex - 1 : allLinks.length - 1;
301      allLinks[prevIndex].focus();
302    },
303    
304    handleTabKey: function(e) {
305      const focusableElements = Array.from(
306        this.responsiveMenu.querySelectorAll('a:not([style*="display: none"]), button:not([style*="display: none"])')
307      ).filter(el => el.offsetParent !== null);
308      
309      const firstElement = focusableElements[0];
310      const lastElement = focusableElements[focusableElements.length - 1];
311      
312      if (e.shiftKey) {
313        // Shift + Tab
314        if (document.activeElement === firstElement) {
315          e.preventDefault();
316          lastElement.focus();
317        }
318      } else {
319        // Tab
320        if (document.activeElement === lastElement) {
321          e.preventDefault();
322          firstElement.focus();
323        }
324      }
325    },
326    
327    // Simple animation function (replaces jQuery's animate)
328    animate: function(element, properties, duration, callback) {
329      const start = performance.now();
330      const initialStyles = {};
331      
332      // Get initial values
333      for (let prop in properties) {
334        const computed = window.getComputedStyle(element);
335        if (prop === 'height') {
336          initialStyles[prop] = parseFloat(computed.height) || 0;
337        } else if (prop === 'opacity') {
338          initialStyles[prop] = parseFloat(computed.opacity) || 0;
339        } else if (prop === 'padding') {
340          initialStyles[prop] = parseFloat(computed.padding) || 0;
341        } else if (prop === 'margin') {
342          initialStyles[prop] = parseFloat(computed.margin) || 0;
343        }
344      }
345      
346      const animate = (currentTime) => {
347        const elapsed = currentTime - start;
348        const progress = Math.min(elapsed / duration, 1);
349        
350        // Easing function (ease-in-out)
351        const eased = progress < 0.5 
352          ? 2 * progress * progress 
353          : -1 + (4 - 2 * progress) * progress;
354        
355        // Apply animated values
356        for (let prop in properties) {
357          const target = parseFloat(properties[prop]);
358          const initial = initialStyles[prop] || 0;
359          const current = initial + (target - initial) * eased;
360          
361          if (prop === 'height') {
362            element.style.height = current + 'px';
363          } else if (prop === 'opacity') {
364            element.style.opacity = current;
365          } else if (prop === 'padding') {
366            element.style.padding = current + 'px';
367          } else if (prop === 'margin') {
368            element.style.margin = current + 'px';
369          }
370        }
371        
372        if (progress < 1) {
373          requestAnimationFrame(animate);
374        } else {
375          if (callback) callback();
376        }
377      };
378      
379      requestAnimationFrame(animate);
380    },
381    
382    announce: function(message) {
383      // Create or update live region for screen reader announcements
384      let liveRegion = document.getElementById('mobile-menu-announcer');
385      
386      if (!liveRegion) {
387        liveRegion = document.createElement('div');
388        liveRegion.id = 'mobile-menu-announcer';
389        liveRegion.className = 'sr-only';
390        liveRegion.setAttribute('aria-live', 'polite');
391        liveRegion.setAttribute('aria-atomic', 'true');
392        document.body.appendChild(liveRegion);
393      }
394      
395      // Clear and announce
396      liveRegion.textContent = '';
397      setTimeout(() => {
398        liveRegion.textContent = message;
399      }, 100);
400    }
401  };
402  
403  // Initialize on DOM ready
404  if (document.readyState === 'loading') {
405    document.addEventListener('DOMContentLoaded', function() {
406      MobileMenu.init();
407    });
408  } else {
409    MobileMenu.init();
410  }
411  
412  // Handle window resize - close menu if switching to desktop
413  let resizeTimer;
414  window.addEventListener('resize', function() {
415    clearTimeout(resizeTimer);
416    resizeTimer = setTimeout(function() {
417      const menu = document.getElementById('responsive-menu');
418      if (menu && window.innerWidth > 1024 && menu.classList.contains('open')) {
419        MobileMenu.closeMenu();
420      }
421    }, 250);
422  });
423
424})();

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.