PageSourceSearch

https://getagency.com/js/mega-menu.js

js getagency.com collected 2026-10-03 19:39:23 UTC 8,337 bytes, 238 lines download raw bytes

1/* ============================================================
2   Mega-Menu Interactions — 2026 Update
3   Handles Offerings and Platform mega-menus.
4   Coexists with Webflow's site.js which manages .w-dropdown.
5   ============================================================ */
6(function () {
7  'use strict';
8
9  var DELAY = 200;
10  var triggers = document.querySelectorAll('[data-mega-menu]');
11  var closeTimers = {};
12  var desktopMQ = window.matchMedia('(min-width: 992px)');
13
14  // Add ARIA roles and keyboard attributes to triggers
15  triggers.forEach(function (trigger) {
16    trigger.setAttribute('role', 'button');
17    trigger.setAttribute('tabindex', '0');
18    trigger.setAttribute('aria-haspopup', 'true');
19    if (!trigger.getAttribute('aria-expanded')) {
20      trigger.setAttribute('aria-expanded', 'false');
21    }
22  });
23
24  function openPanel(trigger) {
25    var id = trigger.getAttribute('data-mega-menu');
26    var panel = document.getElementById(id);
27    if (!panel) return;
28    clearTimeout(closeTimers[id]);
29    // Close all other panels first
30    triggers.forEach(function (t) {
31      if (t !== trigger) closePanel(t);
32    });
33    panel.classList.add('is-open');
34    trigger.setAttribute('aria-expanded', 'true');
35    // Nudge scroll so mobile browsers recalculate scrollable area
36    if (!desktopMQ.matches) {
37      requestAnimationFrame(function () {
38        var overlay = document.querySelector('.w-nav-overlay');
39        if (overlay && overlay.scrollHeight > overlay.clientHeight && overlay.scrollTop === 0) {
40          overlay.scrollTop = 1;
41          overlay.scrollTop = 0;
42        }
43      });
44    }
45  }
46
47  function closePanel(trigger) {
48    var id = trigger.getAttribute('data-mega-menu');
49    var panel = document.getElementById(id);
50    if (!panel) return;
51    panel.classList.remove('is-open');
52    trigger.setAttribute('aria-expanded', 'false');
53    // Clamp scroll position when panel collapses to avoid blank space
54    if (!desktopMQ.matches) {
55      requestAnimationFrame(function () {
56        var overlay = document.querySelector('.w-nav-overlay');
57        if (overlay && overlay.scrollTop > overlay.scrollHeight - overlay.clientHeight) {
58          overlay.scrollTop = Math.max(0, overlay.scrollHeight - overlay.clientHeight);
59        }
60      });
61    }
62  }
63
64  function scheduledClose(trigger) {
65    var id = trigger.getAttribute('data-mega-menu');
66    closeTimers[id] = setTimeout(function () {
67      closePanel(trigger);
68    }, DELAY);
69  }
70
71  function cancelClose(trigger) {
72    var id = trigger.getAttribute('data-mega-menu');
73    clearTimeout(closeTimers[id]);
74  }
75
76  triggers.forEach(function (trigger) {
77    var id = trigger.getAttribute('data-mega-menu');
78    var panel = document.getElementById(id);
79    var wrapper = trigger.closest('.mega-menu-wrapper');
80    if (!panel) return;
81
82    // Desktop hover — only when above mobile breakpoint
83    if (wrapper) {
84      wrapper.addEventListener('mouseenter', function () {
85        if (desktopMQ.matches) openPanel(trigger);
86      });
87      wrapper.addEventListener('mouseleave', function () {
88        if (desktopMQ.matches) scheduledClose(trigger);
89      });
90    } else {
91      trigger.addEventListener('mouseenter', function () {
92        if (desktopMQ.matches) openPanel(trigger);
93      });
94      trigger.addEventListener('mouseleave', function () {
95        if (desktopMQ.matches) scheduledClose(trigger);
96      });
97      panel.addEventListener('mouseenter', function () {
98        if (desktopMQ.matches) cancelClose(trigger);
99      });
100      panel.addEventListener('mouseleave', function () {
101        if (desktopMQ.matches) scheduledClose(trigger);
102      });
103    }
104
105    // Click toggle (mobile)
106    trigger.addEventListener('click', function (e) {
107      e.preventDefault();
108      if (panel.classList.contains('is-open')) {
109        closePanel(trigger);
110      } else {
111        openPanel(trigger);
112      }
113    });
114
115    // Keyboard: Enter/Space to toggle, arrow keys to navigate
116    trigger.addEventListener('keydown', function (e) {
117      if (e.key === 'Enter' || e.key === ' ') {
118        e.preventDefault();
119        if (panel.classList.contains('is-open')) {
120          closePanel(trigger);
121        } else {
122          openPanel(trigger);
123          // Focus first link in panel
124          var firstLink = panel.querySelector('a');
125          if (firstLink) firstLink.focus();
126        }
127      }
128      if (e.key === 'ArrowDown') {
129        e.preventDefault();
130        if (!panel.classList.contains('is-open')) {
131          openPanel(trigger);
132        }
133        var firstLink = panel.querySelector('a');
134        if (firstLink) firstLink.focus();
135      }
136    });
137
138    // Allow keyboard navigation within panel and return focus
139    panel.addEventListener('keydown', function (e) {
140      var links = panel.querySelectorAll('a');
141      var idx = Array.prototype.indexOf.call(links, document.activeElement);
142
143      if (e.key === 'ArrowDown') {
144        e.preventDefault();
145        if (idx < links.length - 1) links[idx + 1].focus();
146      }
147      if (e.key === 'ArrowUp') {
148        e.preventDefault();
149        if (idx > 0) {
150          links[idx - 1].focus();
151        } else {
152          trigger.focus();
153        }
154      }
155      if (e.key === 'Escape') {
156        closePanel(trigger);
157        trigger.focus();
158      }
159      if (e.key === 'Tab') {
160        // On last link Tab, or shift+tab on first, close panel
161        if (!e.shiftKey && idx === links.length - 1) {
162          closePanel(trigger);
163        }
164        if (e.shiftKey && idx === 0) {
165          closePanel(trigger);
166          trigger.focus();
167          e.preventDefault();
168        }
169      }
170    });
171  });
172
173  // Close on outside click
174  document.addEventListener('click', function (e) {
175    triggers.forEach(function (trigger) {
176      var id = trigger.getAttribute('data-mega-menu');
177      var panel = document.getElementById(id);
178      if (!panel) return;
179      if (!trigger.contains(e.target) && !panel.contains(e.target)) {
180        closePanel(trigger);
181      }
182    });
183  });
184
185  // Close on Escape (global)
186  document.addEventListener('keydown', function (e) {
187    if (e.key === 'Escape') {
188      triggers.forEach(function (trigger) { closePanel(trigger); });
189    }
190  });
191
192  // Body scroll lock and panel reset for mobile nav
193  var navButton = document.querySelector('.w-nav-button');
194  if (navButton) {
195    var mobileMQ = window.matchMedia('(max-width: 991px)');
196    var observer = new MutationObserver(function (mutations) {
197      mutations.forEach(function (mutation) {
198        if (mutation.attributeName !== 'class') return;
199        if (!mobileMQ.matches) return;
200        var isOpen = navButton.classList.contains('w--open');
201        if (isOpen) {
202          document.body.style.overflow = 'hidden';
203          document.body.style.position = 'fixed';
204          document.body.style.width = '100%';
205          document.body.style.top = '-' + window.scrollY + 'px';
206          // Fix overlay height — Webflow's $(window).height() can be wrong on mobile
207          var navEl = document.querySelector('.navbar.w-nav');
208          var animDuration = parseInt(navEl && navEl.getAttribute('data-duration') || '400', 10);
209          setTimeout(function () {
210            var overlay = document.querySelector('.w-nav-overlay');
211            if (!overlay || overlay.style.display === 'none') return;
212            var navHeight = navEl ? navEl.getBoundingClientRect().height : 0;
213            overlay.style.setProperty('height', (window.innerHeight - navHeight) + 'px', 'important');
214          }, animDuration + 50);
215        } else {
216          // Close all mega-menu panels
217          triggers.forEach(function (t) { closePanel(t); });
218          // Restore scroll position
219          var scrollY = document.body.style.top;
220          document.body.style.overflow = '';
221          document.body.style.position = '';
222          document.body.style.width = '';
223          document.body.style.top = '';
224          window.scrollTo(0, parseInt(scrollY || '0') * -1);
225        }
226      });
227    });
228    observer.observe(navButton, { attributes: true, attributeFilter: ['class'] });
229  }
230
231  // Backup: listen for Webflow's jQuery w-close event
232  var wNav = document.querySelector('.w-nav');
233  if (wNav && window.jQuery) {
234    window.jQuery(wNav).on('w-close', function () {
235      triggers.forEach(function (t) { closePanel(t); });
236    });
237  }
238})();

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.