PageSourceSearch

https://wprobo.com/wp-content/themes/wprobo-2026/assets/js/mega-menu-2.js

js wprobo.com collected 2026-09-24 08:46:52 UTC 14,483 bytes, 461 lines download raw bytes

1/**
2 * WPRobo Mega Menu
3 *
4 * Handles panel show/hide on hover, tab switching inside tabbed panels,
5 * outside-click-to-close, keyboard accessibility, and smooth transitions.
6 *
7 * @package WPRobo
8 */
9
10(function () {
11  'use strict';
12
13  var megaParents = document.querySelectorAll('.mega-parent');
14  if (!megaParents.length) {
15    return;
16  }
17
18  // ─── Deferred panel content ─────────────────────────────
19  // Panel innards ship inside inert <template> tags (no style/layout cost
20  // on page load — issue #181). Materialize them on the first pointer,
21  // focus, or touch anywhere in the navigation; the 150ms hover show-delay
22  // comfortably covers the synchronous stamp-out.
23  var materialized = false;
24  function materializePanels() {
25    if (materialized) {
26      return;
27    }
28    materialized = true;
29    document.querySelectorAll('.mega-panel > template.mega-tpl').forEach(function (tpl) {
30      tpl.parentNode.appendChild(tpl.content);
31      tpl.parentNode.removeChild(tpl);
32    });
33    initTabs();
34  }
35  var nav = document.getElementById('site-navigation') || document;
36  ['pointerover', 'focusin', 'touchstart'].forEach(function (evt) {
37    nav.addEventListener(evt, materializePanels, { passive: true, once: true });
38  });
39
40  var SHOW_DELAY = 150;
41  var HIDE_DELAY = 250;
42  var activePanel = null;
43  var showTimer = null;
44  var hideTimer = null;
45
46  // ─── Panel show / hide ──────────────────────────────────
47
48  /**
49   * Show a mega panel.
50   *
51   * @param {HTMLElement} panel The .mega-panel element.
52   * @param {HTMLElement} trigger The parent <li>.
53   */
54  /**
55   * Keep the panel inside the viewport. The panel is centered on the
56   * parent <li> via `left-1/2 -translate-x-1/2`. When the parent sits
57   * near the edge of the navbar the panel can overflow — this helper
58   * shifts it by an inline transform so it always has a 16px gutter
59   * to both edges of the viewport.
60   *
61   * @param {HTMLElement} panel The .mega-panel element.
62   */
63  function adjustPanelPosition(panel) {
64    // Disable transitions while we measure. Without this, the panel is
65    // mid-animation when getBoundingClientRect() reads its position, which
66    // returned the OLD shifted rect on alternate hovers and made the
67    // adjustment toggle on/off every other time.
68    var origTransition = panel.style.transition;
69    panel.style.transition = 'none';
70
71    // Reset any previous adjustment so we measure the natural centered position.
72    panel.style.transform = '';
73
74    // Force layout so the reset takes effect before we measure.
75    var _ = panel.offsetWidth; // eslint-disable-line no-unused-vars
76
77    var rect = panel.getBoundingClientRect();
78    var vw = window.innerWidth;
79    var gutter = 16;
80
81    if (rect.left < gutter) {
82      var shiftRight = Math.ceil(gutter - rect.left);
83      panel.style.transform = 'translateX(calc(-50% + ' + shiftRight + 'px))';
84    } else if (rect.right > vw - gutter) {
85      var shiftLeft = Math.ceil(rect.right - (vw - gutter));
86      panel.style.transform = 'translateX(calc(-50% - ' + shiftLeft + 'px))';
87    }
88
89    // Force layout again so the new transform is committed without animation,
90    // then re-enable transitions for the show/hide opacity fade.
91    var __ = panel.offsetWidth; // eslint-disable-line no-unused-vars
92    panel.style.transition = origTransition;
93  }
94
95  function showPanel(panel, trigger) {
96    clearTimeout(hideTimer);
97    clearTimeout(showTimer);
98
99    // Hide any other open panel first.
100    if (activePanel && activePanel !== panel) {
101      hidePanel(activePanel);
102    }
103
104    showTimer = setTimeout(function () {
105      panel.classList.remove('opacity-0', 'invisible', 'pointer-events-none');
106      panel.classList.add('opacity-100', 'visible', 'pointer-events-auto');
107
108      // Now that the panel has dimensions, reposition it if it would
109      // overflow the viewport on either side.
110      adjustPanelPosition(panel);
111
112      // Rotate chevron.
113      var chevron = trigger.querySelector('.mega-chevron');
114      if (chevron) {
115        chevron.classList.add('rotate-180');
116      }
117
118      activePanel = panel;
119    }, SHOW_DELAY);
120  }
121
122  // Recompute on resize while a panel is open.
123  window.addEventListener('resize', function () {
124    if (activePanel) {
125      adjustPanelPosition(activePanel);
126    }
127  });
128
129  /**
130   * Hide a mega panel.
131   *
132   * @param {HTMLElement} panel The .mega-panel element.
133   */
134  function hidePanel(panel) {
135    clearTimeout(showTimer);
136    clearTimeout(hideTimer);
137
138    hideTimer = setTimeout(function () {
139      panel.classList.add('opacity-0', 'invisible', 'pointer-events-none');
140      panel.classList.remove('opacity-100', 'visible', 'pointer-events-auto');
141
142      // Reset chevron.
143      var parentLi = panel.closest('.mega-parent');
144      if (parentLi) {
145        var chevron = parentLi.querySelector('.mega-chevron');
146        if (chevron) {
147          chevron.classList.remove('rotate-180');
148        }
149      }
150
151      if (activePanel === panel) {
152        activePanel = null;
153      }
154    }, HIDE_DELAY);
155  }
156
157  /**
158   * Hide a panel immediately (no delay).
159   *
160   * @param {HTMLElement} panel The .mega-panel element.
161   */
162  function hidePanelNow(panel) {
163    clearTimeout(showTimer);
164    clearTimeout(hideTimer);
165
166    panel.classList.add('opacity-0', 'invisible', 'pointer-events-none');
167    panel.classList.remove('opacity-100', 'visible', 'pointer-events-auto');
168
169    var parentLi = panel.closest('.mega-parent');
170    if (parentLi) {
171      var chevron = parentLi.querySelector('.mega-chevron');
172      if (chevron) {
173        chevron.classList.remove('rotate-180');
174      }
175    }
176
177    if (activePanel === panel) {
178      activePanel = null;
179    }
180  }
181
182  // ─── Wire up hover events for each mega parent ─────────
183
184  megaParents.forEach(function (parentLi) {
185    var panel = parentLi.querySelector('.mega-panel');
186    if (!panel) {
187      return;
188    }
189
190    // Show on hover.
191    parentLi.addEventListener('mouseenter', function () {
192      showPanel(panel, parentLi);
193    });
194
195    // Hide on leave (with delay so user can move to panel).
196    parentLi.addEventListener('mouseleave', function () {
197      hidePanel(panel);
198    });
199
200    // Keep panel visible while mouse is over it.
201    panel.addEventListener('mouseenter', function () {
202      clearTimeout(hideTimer);
203    });
204
205    panel.addEventListener('mouseleave', function () {
206      hidePanel(panel);
207    });
208
209    // Click trigger.
210    //
211    // Desktop (mouse / fine pointer): the click navigates to the parent's
212    // URL — e.g. /plugins/ or /services/. Hover already shows the mega
213    // panel, so we don't need to hijack click on desktop.
214    //
215    // Touch / coarse pointer: there's no real hover, so click is the only
216    // way to open the panel. On the FIRST tap we open the panel and
217    // preventDefault so the page doesn't navigate; the user can then tap
218    // a card. A SECOND tap on the parent (when the panel is already open)
219    // navigates as a "go to overview page" affordance.
220    var trigger = parentLi.querySelector('.mega-trigger');
221    if (trigger && panel) {
222      // matchMedia returns true for devices with a fine, hovering pointer
223      // (i.e. mouse). Phones/tablets fall into the "else" branch.
224      var hasHover = window.matchMedia &&
225        window.matchMedia('(hover: hover) and (pointer: fine)').matches;
226
227      trigger.addEventListener('click', function (e) {
228        if (hasHover) {
229          // Desktop: let the default navigation happen.
230          return;
231        }
232
233        // Touch device — first tap toggles the panel, second tap navigates.
234        if (activePanel === panel) {
235          // Panel already open — let this tap navigate.
236          return;
237        }
238
239        e.preventDefault();
240        clearTimeout(hideTimer);
241        clearTimeout(showTimer);
242
243        if (activePanel) {
244          hidePanelNow(activePanel);
245        }
246
247        panel.classList.remove('opacity-0', 'invisible', 'pointer-events-none');
248        panel.classList.add('opacity-100', 'visible', 'pointer-events-auto');
249        adjustPanelPosition(panel);
250
251        var chevron = parentLi.querySelector('.mega-chevron');
252        if (chevron) {
253          chevron.classList.add('rotate-180');
254        }
255
256        activePanel = panel;
257      });
258    }
259  });
260
261  // ─── Outside click to close ─────────────────────────────
262
263  document.addEventListener('click', function (e) {
264    if (activePanel && !e.target.closest('.mega-parent')) {
265      hidePanelNow(activePanel);
266    }
267  });
268
269  // ─── Escape key to close ────────────────────────────────
270
271  document.addEventListener('keydown', function (e) {
272    if (e.key === 'Escape' && activePanel) {
273      hidePanelNow(activePanel);
274
275      // Focus the trigger that opened this panel.
276      var parentLi = activePanel ? activePanel.closest('.mega-parent') : null;
277      if (parentLi) {
278        var trigger = parentLi.querySelector('.mega-trigger');
279        if (trigger) {
280          trigger.focus();
281        }
282      }
283    }
284  });
285
286  // ─── Keyboard Navigation: Arrow keys & Focus Trap ───────
287
288  /**
289   * Get all focusable elements within a container.
290   *
291   * @param {HTMLElement} container The container element.
292   * @return {NodeList} Focusable elements.
293   */
294  function getFocusableElements(container) {
295    return container.querySelectorAll(
296      'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
297    );
298  }
299
300  /**
301   * Trap focus within an open mega panel.
302   *
303   * @param {KeyboardEvent} e The keyboard event.
304   */
305  function handleFocusTrap(e) {
306    if (e.key !== 'Tab' || !activePanel) {
307      return;
308    }
309
310    var parentLi = activePanel.closest('.mega-parent');
311    if (!parentLi) {
312      return;
313    }
314
315    var focusable = getFocusableElements(parentLi);
316    if (!focusable.length) {
317      return;
318    }
319
320    var firstEl = focusable[0];
321    var lastEl = focusable[focusable.length - 1];
322
323    if (e.shiftKey) {
324      if (document.activeElement === firstEl) {
325        e.preventDefault();
326        lastEl.focus();
327      }
328    } else {
329      if (document.activeElement === lastEl) {
330        e.preventDefault();
331        firstEl.focus();
332      }
333    }
334  }
335
336  document.addEventListener('keydown', handleFocusTrap);
337
338  // Arrow key navigation between top-level menu items.
339  megaParents.forEach(function (parentLi) {
340    var trigger = parentLi.querySelector('.mega-trigger');
341    if (!trigger) {
342      return;
343    }
344
345    trigger.addEventListener('keydown', function (e) {
346      var allTriggers = [];
347      megaParents.forEach(function (li) {
348        var t = li.querySelector('.mega-trigger');
349        if (t) {
350          allTriggers.push(t);
351        }
352      });
353
354      var currentIndex = allTriggers.indexOf(trigger);
355
356      if (e.key === 'ArrowRight') {
357        e.preventDefault();
358        var nextIndex = (currentIndex + 1) % allTriggers.length;
359        allTriggers[nextIndex].focus();
360      } else if (e.key === 'ArrowLeft') {
361        e.preventDefault();
362        var prevIndex = (currentIndex - 1 + allTriggers.length) % allTriggers.length;
363        allTriggers[prevIndex].focus();
364      } else if (e.key === 'ArrowDown') {
365        e.preventDefault();
366        var panel = parentLi.querySelector('.mega-panel');
367        if (panel) {
368          // Open the panel.
369          clearTimeout(hideTimer);
370          clearTimeout(showTimer);
371          panel.classList.remove('opacity-0', 'invisible', 'pointer-events-none');
372          panel.classList.add('opacity-100', 'visible', 'pointer-events-auto');
373          activePanel = panel;
374
375          // Focus first focusable element in the panel.
376          var firstFocusable = panel.querySelector('a[href], button:not([disabled])');
377          if (firstFocusable) {
378            firstFocusable.focus();
379          }
380        }
381      } else if (e.key === 'Enter' || e.key === ' ') {
382        e.preventDefault();
383        var panel = parentLi.querySelector('.mega-panel');
384        if (panel) {
385          if (activePanel === panel) {
386            hidePanelNow(panel);
387          } else {
388            if (activePanel) {
389              hidePanelNow(activePanel);
390            }
391            panel.classList.remove('opacity-0', 'invisible', 'pointer-events-none');
392            panel.classList.add('opacity-100', 'visible', 'pointer-events-auto');
393            activePanel = panel;
394          }
395        }
396      }
397    });
398  });
399
400  // ─── Tab switching inside tabbed panels ─────────────────
401  // Bound after panel content is materialized from its <template>.
402
403  function initTabs() {
404    var tabButtons = document.querySelectorAll('.mega-tab');
405
406    tabButtons.forEach(function (btn) {
407      // Hover to switch tabs (like the reference design).
408      btn.addEventListener('mouseenter', function () {
409        activateTab(this);
410      });
411
412      // Click also works.
413      btn.addEventListener('click', function () {
414        activateTab(this);
415      });
416
417      // Keyboard: Enter or Space.
418      btn.addEventListener('keydown', function (e) {
419        if (e.key === 'Enter' || e.key === ' ') {
420          e.preventDefault();
421          activateTab(this);
422        }
423      });
424    });
425  }
426
427  /**
428   * Activate a tab button and show its content panel.
429   *
430   * @param {HTMLElement} btn The tab button.
431   */
432  function activateTab(btn) {
433    var panel = btn.closest('.mega-panel-tabbed');
434    if (!panel) {
435      return;
436    }
437
438    var targetId = btn.getAttribute('data-tab');
439
440    // Deactivate all tabs in this panel.
441    panel.querySelectorAll('.mega-tab').forEach(function (t) {
442      t.classList.remove('mega-tab-active', 'bg-wprobo-accent/10', 'text-wprobo-accent');
443      t.classList.add('text-gray-600');
444    });
445
446    // Activate clicked tab.
447    btn.classList.add('mega-tab-active', 'bg-wprobo-accent/10', 'text-wprobo-accent');
448    btn.classList.remove('text-gray-600');
449
450    // Hide all content panels.
451    panel.querySelectorAll('.mega-tab-content').forEach(function (content) {
452      content.classList.add('hidden');
453    });
454
455    // Show target content.
456    var target = document.getElementById(targetId);
457    if (target) {
458      target.classList.remove('hidden');
459    }
460  }
461})();

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.