PageSourceSearch

https://www.emailmonday.com/wp-content/plugins/emailmonday-megamenu/assets/em-megamenu.js

js emailmonday.com collected 2026-10-01 08:37:34 UTC 8,149 bytes, 285 lines download raw bytes

1/**
2 * emailmonday Megamenu - behaviour
3 *
4 * Mirrors the interaction model on kinsta.com:
5 *   - hover-intent open (short delay in, longer delay out, so diagonal mouse
6 *     travel toward the panel does not close it)
7 *   - only one panel open at a time; moving between top-level items swaps
8 *     panels instantly rather than replaying the open animation
9 *   - click/tap toggles (touch devices never get a hover event)
10 *   - Escape closes and returns focus to the trigger
11 *   - full keyboard support: arrows along the bar, Down opens a panel
12 *   - panels flip to right-aligned if they would overflow the viewport
13 */
14(function () {
15  'use strict';
16
17  var OPEN_DELAY = 90;
18  var CLOSE_DELAY = 220;
19  var MOBILE_QUERY = '(max-width: 1024px)';
20
21  function ready(fn) {
22    if (document.readyState !== 'loading') {
23      fn();
24    } else {
25      document.addEventListener('DOMContentLoaded', fn);
26    }
27  }
28
29  function EmMegaMenu(root) {
30    this.root = root;
31    this.items = Array.prototype.slice.call(
32      root.querySelectorAll('.em-mega__item--has-panel')
33    );
34    this.triggers = Array.prototype.slice.call(
35      root.querySelectorAll('.em-mega__list > .em-mega__item > .em-mega__top')
36    );
37    this.openItem = null;
38    this.openTimer = null;
39    this.closeTimer = null;
40    this.mq = window.matchMedia(MOBILE_QUERY);
41
42    this.onDocPointer = this.onDocPointer.bind(this);
43    this.onKeydown = this.onKeydown.bind(this);
44    this.onFocusIn = this.onFocusIn.bind(this);
45
46    this.init();
47  }
48
49  EmMegaMenu.prototype.isMobile = function () {
50    return this.mq.matches;
51  };
52
53  EmMegaMenu.prototype.init = function () {
54    var self = this;
55
56    this.items.forEach(function (item) {
57      var trigger = item.querySelector(':scope > .em-mega__top');
58      var panel = item.querySelector(':scope > .em-mega__panel');
59      if (!trigger || !panel) {
60        return;
61      }
62
63      // Pointer: hover intent. Skipped entirely on coarse pointers so a tap
64      // does not both hover-open and click-close in the same gesture.
65      item.addEventListener('mouseenter', function (e) {
66        if (self.isMobile() || e.sourceCapabilities && e.sourceCapabilities.firesTouchEvents) {
67          return;
68        }
69        self.scheduleOpen(item);
70      });
71
72      item.addEventListener('mouseleave', function () {
73        if (self.isMobile()) {
74          return;
75        }
76        self.scheduleClose();
77      });
78
79      trigger.addEventListener('click', function (e) {
80        e.preventDefault();
81        clearTimeout(self.openTimer);
82        clearTimeout(self.closeTimer);
83        if (self.openItem === item) {
84          self.close();
85        } else {
86          self.open(item);
87        }
88      });
89
90      // Keep the panel open while the pointer is inside it.
91      panel.addEventListener('mouseenter', function () {
92        clearTimeout(self.closeTimer);
93      });
94      panel.addEventListener('mouseleave', function () {
95        if (!self.isMobile()) {
96          self.scheduleClose();
97        }
98      });
99    });
100
101    document.addEventListener('keydown', this.onKeydown);
102    document.addEventListener('pointerdown', this.onDocPointer);
103    document.addEventListener('focusin', this.onFocusIn);
104
105    // Close when the viewport changes mode, so state never gets stranded.
106    var onModeChange = function () { self.close(true); };
107    if (typeof this.mq.addEventListener === 'function') {
108      this.mq.addEventListener('change', onModeChange);
109    } else if (typeof this.mq.addListener === 'function') {
110      this.mq.addListener(onModeChange);
111    }
112
113    window.addEventListener('resize', function () {
114      if (self.openItem) {
115        self.position(self.openItem);
116      }
117    });
118  };
119
120  EmMegaMenu.prototype.scheduleOpen = function (item) {
121    var self = this;
122    clearTimeout(this.closeTimer);
123    clearTimeout(this.openTimer);
124
125    // A panel is already open: swap immediately, no re-animation delay.
126    if (this.openItem && this.openItem !== item) {
127      this.open(item);
128      return;
129    }
130    if (this.openItem === item) {
131      return;
132    }
133    this.openTimer = setTimeout(function () {
134      self.open(item);
135    }, OPEN_DELAY);
136  };
137
138  EmMegaMenu.prototype.scheduleClose = function () {
139    var self = this;
140    clearTimeout(this.openTimer);
141    clearTimeout(this.closeTimer);
142    this.closeTimer = setTimeout(function () {
143      self.close();
144    }, CLOSE_DELAY);
145  };
146
147  EmMegaMenu.prototype.position = function (item) {
148    if (this.isMobile()) {
149      return;
150    }
151    var panel = item.querySelector(':scope > .em-mega__panel');
152    if (!panel) {
153      return;
154    }
155
156    // Measure against the nav's left edge, then flip if we would spill out.
157    item.removeAttribute('data-em-align');
158    var navLeft = this.root.getBoundingClientRect().left;
159    var width = panel.offsetWidth;
160    if (navLeft + width > window.innerWidth - 20) {
161      item.setAttribute('data-em-align', 'right');
162    }
163  };
164
165  EmMegaMenu.prototype.open = function (item) {
166    if (this.openItem && this.openItem !== item) {
167      this.setState(this.openItem, false);
168    }
169    this.openItem = item;
170    this.position(item);
171    this.setState(item, true);
172
173    // Always flagged, even with the backdrop switched off: the header bar
174    // watches this class to stay pinned while a panel is open.
175    if (!this.isMobile()) {
176      document.body.classList.add('em-mega-open');
177    }
178  };
179
180  EmMegaMenu.prototype.close = function (immediate) {
181    clearTimeout(this.openTimer);
182    clearTimeout(this.closeTimer);
183    if (this.openItem) {
184      this.setState(this.openItem, false);
185      this.openItem = null;
186    }
187    document.body.classList.remove('em-mega-open');
188    if (immediate) {
189      this.items.forEach(function (item) {
190        item.removeAttribute('data-em-align');
191      });
192    }
193  };
194
195  EmMegaMenu.prototype.setState = function (item, isOpen) {
196    var trigger = item.querySelector(':scope > .em-mega__top');
197    item.classList.toggle('is-open', isOpen);
198    if (trigger) {
199      trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
200    }
201  };
202
203  EmMegaMenu.prototype.onDocPointer = function (e) {
204    if (!this.openItem) {
205      return;
206    }
207    if (!this.root.contains(e.target)) {
208      this.close();
209    }
210  };
211
212  EmMegaMenu.prototype.onFocusIn = function (e) {
213    if (!this.openItem) {
214      return;
215    }
216    // Tabbing out of the nav entirely closes the panel.
217    if (!this.root.contains(e.target)) {
218      this.close();
219    }
220  };
221
222  EmMegaMenu.prototype.onKeydown = function (e) {
223    if (e.key === 'Escape' && this.openItem) {
224      var trigger = this.openItem.querySelector(':scope > .em-mega__top');
225      this.close();
226      if (trigger) {
227        trigger.focus();
228      }
229      return;
230    }
231
232    if (!this.root.contains(e.target)) {
233      return;
234    }
235
236    var idx = this.triggers.indexOf(e.target);
237
238    if (e.key === 'ArrowDown' && idx > -1) {
239      var item = e.target.closest('.em-mega__item--has-panel');
240      if (item) {
241        e.preventDefault();
242        this.open(item);
243        var first = item.querySelector('.em-mega__link');
244        if (first) {
245          first.focus();
246        }
247      }
248      return;
249    }
250
251    if ((e.key === 'ArrowRight' || e.key === 'ArrowLeft') && idx > -1) {
252      e.preventDefault();
253      var step = e.key === 'ArrowRight' ? 1 : -1;
254      var next = (idx + step + this.triggers.length) % this.triggers.length;
255      this.triggers[next].focus();
256    }
257  };
258
259  ready(function () {
260    var roots = document.querySelectorAll('.em-mega');
261    if (!roots.length) {
262      return;
263    }
264
265    Array.prototype.forEach.call(roots, function (root) {
266      if (root.dataset.emInit) {
267        return;
268      }
269      root.dataset.emInit = '1';
270
271      if (root.classList.contains('em-mega--no-backdrop')) {
272        document.body.classList.add('em-mega-hide-backdrop');
273      }
274
275      new EmMegaMenu(root);
276    });
277
278    if (!document.querySelector('.em-mega-backdrop')) {
279      var backdrop = document.createElement('div');
280      backdrop.className = 'em-mega-backdrop';
281      backdrop.setAttribute('aria-hidden', 'true');
282      document.body.appendChild(backdrop);
283    }
284  });
285})();

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.