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.