1/*jslint browser: true, white: true, this: true, long: true */ 2/*global console,jQuery,megamenu,window,navigator*/ 3 4/*! Max Mega Menu jQuery Plugin */ 5(function ( $ ) { 6 "use strict"; 7 8 let instanceCounter = 0; 9 10 $.maxmegamenu = function(menu, options) { 11 12 // --- DOM references --- 13 const plugin = this; 14 const $menu = $(menu); 15 const $wrap = $menu.parent(); 16 const $toggle_bar = $menu.siblings(".mega-menu-toggle"); 17 18 // --- Instance identifiers --- 19 const menuId = $menu.attr("id"); 20 const instanceId = menuId + '-' + (++instanceCounter); 21 const docEventNamespace = '.megamenu-' + instanceId; 22 23 // --- Cached element sets (captured at init) --- 24 const items_with_submenus = $([ 25 "li.mega-menu-megamenu.mega-menu-item-has-children", 26 "li.mega-menu-flyout.mega-menu-item-has-children", 27 "li.mega-menu-tabbed > ul.mega-sub-menu > li.mega-menu-item-has-children", 28 "li.mega-menu-flyout li.mega-menu-item-has-children" 29 ].join(","), $menu); 30 31 const collapse_children_parents = $("li.mega-menu-megamenu li.mega-menu-item-has-children.mega-collapse-children > a.mega-menu-link", $menu); 32 33 // --- Keyboard key identifiers (KeyboardEvent.key values) --- 34 const tab_key = "Tab"; 35 const escape_key = "Escape"; 36 const enter_key = "Enter"; 37 const space_key = " "; 38 const left_arrow_key = "ArrowLeft"; 39 const up_arrow_key = "ArrowUp"; 40 const right_arrow_key = "ArrowRight"; 41 const down_arrow_key = "ArrowDown"; 42 43 // --- Settings defaults (read from data attributes) --- 44 const defaults = { 45 event: $menu.attr("data-event"), 46 effect: $menu.attr("data-effect"), 47 effect_speed: parseInt($menu.attr("data-effect-speed")), 48 effect_mobile: $menu.attr("data-effect-mobile"), 49 effect_speed_mobile: parseInt($menu.attr("data-effect-speed-mobile")), 50 panel_width: $menu.attr("data-panel-width"), 51 panel_inner_width: $menu.attr("data-panel-inner-width"), 52 mobile_force_width: $menu.attr("data-mobile-force-width"), 53 mobile_overlay: $menu.attr("data-mobile-overlay"), 54 mobile_state: $menu.attr("data-mobile-state"), 55 mobile_direction: $menu.attr("data-mobile-direction"), 56 second_click: $menu.attr("data-second-click"), 57 vertical_behaviour: $menu.attr("data-vertical-behaviour"), 58 document_click: $menu.attr("data-document-click"), 59 breakpoint: $menu.attr("data-breakpoint"), 60 unbind_events: $menu.attr("data-unbind"), 61 hover_intent_timeout: $menu.attr("data-hover-intent-timeout") ?? 300, 62 hover_intent_interval: $menu.attr("data-hover-intent-interval") ?? 100 63 }; 64 65 // --- Mutable state --- 66 plugin.settings = {}; 67 let html_body_class_timeout; 68 69 plugin.addAnimatingClass = function(element) { 70 if (plugin.settings.effect === "disabled") { 71 return; 72 } 73 74 $(".mega-animating", $wrap).removeClass("mega-animating"); 75 76 const timeout = plugin.settings.effect_speed + parseInt(plugin.settings.hover_intent_timeout, 10); 77 78 element.addClass("mega-animating"); 79 80 setTimeout(function() { 81 element.removeClass("mega-animating"); 82 }, timeout ); 83 }; 84 85 plugin.hideAllPanels = function() { 86 $(".mega-toggle-on > a.mega-menu-link", $menu).each(function() { 87 plugin.hidePanel($(this), false); 88 }); 89 }; 90 91 plugin.expandMobileSubMenus = function() { 92 if (plugin.settings.mobile_direction !== 'vertical') { 93 return; 94 } 95 96 $(".mega-menu-item-has-children.mega-expand-on-mobile > a.mega-menu-link", $menu).each(function() { 97 plugin.showPanel($(this), true); 98 }); 99 100 if ( plugin.settings.mobile_state === 'expand_all' ) { 101 $(".mega-menu-item-has-children:not(.mega-toggle-on) > a.mega-menu-link", $menu).each(function() { 102 plugin.showPanel($(this), true); 103 }); 104 } 105 106 if ( plugin.settings.mobile_state === 'expand_active' ) { 107 const activeItemSelectors = [ 108 "li.mega-current-menu-ancestor.mega-menu-item-has-children > a.mega-menu-link", 109 "li.mega-current-menu-item.mega-menu-item-has-children > a.mega-menu-link", 110 "li.mega-current-menu-parent.mega-menu-item-has-children > a.mega-menu-link", 111 "li.mega-current_page_ancestor.mega-menu-item-has-children > a.mega-menu-link", 112 "li.mega-current_page_item.mega-menu-item-has-children > a.mega-menu-link" 113 ]; 114 115 $menu.find(activeItemSelectors.join(', ')).each(function() { 116 plugin.showPanel($(this), true); 117 }); 118 } 119 }; 120 121 plugin.hideSiblingPanels = function(anchor, immediate) { 122 anchor.parent().parent().find(".mega-toggle-on").children("a.mega-menu-link").each(function() { // all open children of open siblings 123 plugin.hidePanel($(this), immediate); 124 }); 125 }; 126 127 plugin.isDesktopView = function() { 128 const width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0); 129 return width > plugin.settings.breakpoint; 130 }; 131 132 plugin.isMobileView = function() { 133 return !plugin.isDesktopView(); 134 }; 135 136 plugin.isHorizontalMobileSubmenuMode = function() { 137 return plugin.isMobileView() && plugin.isMobileOffCanvas() && plugin.settings.mobile_direction === "horizontal"; 138 }; 139 140 plugin.getFocusableItemsInSubmenu = function($submenu, include_back_link = true) { 141 let $focusable = $submenu 142 .find("a.mega-menu-link, button.mega-menu-link, .mega-search span[role=button]") 143 .filter(":visible"); 144 145 if (!include_back_link) { 146 $focusable = $focusable.not(".mega-mobile-back-link"); 147 } 148 149 return $focusable; 150 }; 151 152 plugin.focusFirstItemInOpenedSubmenu = function($item) { 153 if ( ! plugin.isHorizontalMobileSubmenuMode() || ! $wrap.hasClass("mega-keyboard-navigation")) { 154 return; 155 } 156 157 const $submenu = $item.children("ul.mega-sub-menu"); 158 159 if (!$submenu.length) { 160 return; 161 } 162 163 // Search all descendants, not just direct li.mega-menu-item children, so that 164 // megamenu panels (where links are nested inside li.mega-menu-row > li.mega-menu-column) 165 // are handled the same as flat flyout panels. 166 const $firstFocusable = $submenu 167 .find("a.mega-menu-link, button.mega-menu-link, .mega-search span[role=button]") 168 .not(".mega-mobile-back-link") 169 .filter(":visible") 170 .first(); 171 172 if ($firstFocusable.length) { 173 $firstFocusable.trigger("focus"); 174 } else { 175 $submenu.find("> li.mega-mobile-back:visible > button.mega-menu-link.mega-mobile-back-link").first().trigger("focus"); 176 } 177 }; 178 179 plugin.deferFocusFirstItemInOpenedSubmenu = function($item) {
180 const delay = Math.min(120, parseInt(plugin.settings.effect_speed_mobile, 10) || 0); 181 182 setTimeout(function() { 183 plugin.focusFirstItemInOpenedSubmenu($item); 184 185 // Retry once after the first paint in case CSS visibility/transform has not applied yet. 186 setTimeout(function() { 187 const focusedInSubmenu = $item.find("ul.mega-sub-menu").has(document.activeElement).length !== 0; 188 189 if (!focusedInSubmenu) { 190 plugin.focusFirstItemInOpenedSubmenu($item); 191 } 192 }, 40); 193 }, delay); 194 }; 195 196 plugin.showPanel = function(anchor, immediate) { 197 if ( typeof anchor === 'number' || ( typeof anchor === 'string' && anchor.trim() !== '' && !isNaN(anchor) ) ) { 198 anchor = $("li.mega-menu-item-" + anchor, $menu).find("a.mega-menu-link").first(); 199 } else if ( anchor.is("li.mega-menu-item") ) { 200 anchor = anchor.find("a.mega-menu-link").first(); 201 } 202 203 const $item = anchor.parent(); 204 const isDesktop = plugin.isDesktopView(); 205 const isMobile = !isDesktop; 206 207 $item.triggerHandler("before_open_panel"); 208 209 $item.find("[aria-expanded]").first().attr("aria-expanded", "true"); 210 211 $(".mega-animating", $wrap).removeClass("mega-animating"); 212 213 if (isMobile && $item.hasClass("mega-hide-sub-menu-on-mobile")) { 214 return; 215 } 216 217 if (isDesktop && ( $menu.hasClass("mega-menu-horizontal") || $menu.hasClass("mega-menu-vertical") ) && !$item.hasClass("mega-collapse-children")) { 218 plugin.hideSiblingPanels(anchor, true); 219 } 220 221 if ((isMobile && $wrap.hasClass("mega-keyboard-navigation")) || plugin.settings.vertical_behaviour === "accordion") { 222 plugin.hideSiblingPanels(anchor, false); 223 } 224 225 plugin.calculateDynamicSubmenuWidths(anchor); 226 227 // apply jQuery transition (only if the effect is set to "slide", other transitions are CSS based) 228 if ( plugin.shouldUseSlideAnimation(anchor, immediate) ) { 229 const speed = isMobile ? plugin.settings.effect_speed_mobile : plugin.settings.effect_speed; 230 231 anchor.siblings(".mega-sub-menu").css("display", "none").animate({"height":"show", "paddingTop":"show", "paddingBottom":"show", "minHeight":"show"}, speed, function() { 232 $(this).css("display", ""); 233 }); 234 } 235 236 $item.addClass("mega-toggle-on").triggerHandler("open_panel"); 237 plugin.deferFocusFirstItemInOpenedSubmenu($item); 238 }; 239 240 241 plugin.shouldUseSlideAnimation = function(anchor, immediate) { 242 243 if (immediate === true) { 244 return false; 245 } 246 247 if (anchor.parent().hasClass("mega-collapse-children")) { 248 return true; 249 } 250 251 const isDesktop = plugin.isDesktopView(); 252 253 if (isDesktop && plugin.settings.effect === "slide") { 254 return true; 255 } 256 257 if (!isDesktop) { 258 if (plugin.settings.effect_mobile === "slide") { 259 return true; 260 } 261 262 if ( plugin.isMobileOffCanvas() ) { 263 return plugin.settings.mobile_direction !== "horizontal"; 264 } 265 } 266 267 return false; 268 }; 269 270 271 plugin.hidePanel = function(anchor, immediate) { 272 if ( typeof anchor === 'number' || ( typeof anchor === 'string' && anchor.trim() !== '' && !isNaN(anchor) ) ) { 273 anchor = $("li.mega-menu-item-" + anchor, $menu).find("a.mega-menu-link").first(); 274 } else if ( anchor.is("li.mega-menu-item") ) { 275 anchor = anchor.find("a.mega-menu-link").first(); 276 } 277 278 const $item = anchor.parent(); 279 const $submenu = anchor.siblings(".mega-sub-menu"); 280 const isMobile = plugin.isMobileView(); 281 282 $item.triggerHandler("before_close_panel"); 283 284 $item.find("[aria-expanded]").first().attr("aria-expanded", "false"); 285 286 if ( plugin.shouldUseSlideAnimation(anchor) ) { 287 const speed = isMobile ? plugin.settings.effect_speed_mobile : plugin.settings.effect_speed; 288 289 $submenu.animate({"height":"hide", "paddingTop":"hide", "paddingBottom":"hide", "minHeight":"hide"}, speed, function() { 290 $submenu.css("display", ""); 291 $item.removeClass("mega-toggle-on").triggerHandler("close_panel"); 292 }); 293 294 return; 295 } 296 297 if (immediate) { 298 $submenu.css("display", "none").delay(plugin.settings.effect_speed).queue(function(){ 299 $(this).css("display", "").dequeue(); 300 }); 301 } 302 303 // pause video widget videos 304 $submenu.find(".widget_media_video video").each(function() { 305 if ( this.player ) { 306 this.player.pause(); 307 } 308 }); 309 310 $item.removeClass("mega-toggle-on").triggerHandler("close_panel"); 311 plugin.addAnimatingClass($item); 312 }; 313 314 plugin.calculateDynamicSubmenuWidths = function(anchor) { 315 const $item = anchor.parent(); 316 const $submenu = anchor.siblings(".mega-sub-menu"); 317 const isDesktop = plugin.isDesktopView(); 318 const isTopLevelMegamenu = $item.hasClass("mega-menu-megamenu") && $item.parent().hasClass("max-mega-menu"); 319 320 // apply dynamic width and sub menu position (only to top level mega menus) 321 if (isTopLevelMegamenu && plugin.settings.panel_width) { 322 if (isDesktop) { 323 const submenu_offset = $menu.offset(); 324 325 if ( plugin.settings.panel_width === '100vw' ) { 326 const target_offset = $('body').offset(); 327 328 $submenu.css({ 329 left: (target_offset.left - submenu_offset.left) + "px" 330 }); 331 } else { 332 const $panel_width_el = plugin.settings.panel_width === '$menu' ? $menu : plugin.settings.panel_width === '$wrap' ? $wrap : $(plugin.settings.panel_width); 333 334 if ( $panel_width_el.length > 0 ) { 335 $submenu.css({ 336 width: $panel_width_el.outerWidth(), 337 left: ($panel_width_el.offset().left - submenu_offset.left) + "px" 338 }); 339 } 340 } 341 } else { 342 $submenu.css({ 343 width: "", 344 left: "" 345 }); 346 } 347 } 348 349 // apply inner width to sub menu by adding padding to the left and right of the mega menu 350 if (isTopLevelMegamenu && plugin.settings.panel_inner_width) { 351 const $panel_inner_width_el = plugin.settings.panel_inner_width === '$menu' ? $menu : plugin.settings.panel_inner_width === '$wrap' ? $wrap : $(plugin.settings.panel_inner_width); 352 353 if ($panel_inner_width_el.length > 0) { 354 const target_width = parseInt($panel_inner_width_el.width(), 10); 355 356 $submenu.css({ 357 "paddingLeft": "", 358 "paddingRight": "" 359 }); 360 361 const submenu_width = parseInt($submenu.innerWidth(), 10); 362 363 if (isDesktop && target_width > 0 && target_width < submenu_width) { 364 $submenu.css({ 365 "paddingLeft": (submenu_width - target_width) / 2 + "px", 366 "paddingRight": (submenu_width - target_width) / 2 + "px" 367 }); 368 } 369 } 370 } 371 }; 372 373 plugin.bindClickEvents = function() { 374 375 plugin.unbindClickEvents(); 376 377 let dragging = false;
378 379 $(document).on({ 380 ["touchmove" + docEventNamespace]: function() { dragging = true; }, 381 ["touchstart" + docEventNamespace]: function() { dragging = false; } 382 }); 383 384 $(document).on("click" + docEventNamespace + " touchend" + docEventNamespace, function(e) { // hide menu when clicked away from 385 if (!dragging && plugin.settings.document_click === "collapse" && ! $(e.target).closest(".mega-menu-wrap").length ) { 386 plugin.hideAllPanels(); 387 plugin.hideMobileMenu(); 388 } 389 dragging = false; 390 }); 391 392 collapse_children_parents.off("click.megamenu touchend.megamenu"); 393 const clickable_parents = $("> a.mega-menu-link", items_with_submenus).add(collapse_children_parents); 394 395 clickable_parents.on("touchend.megamenu", function(e) { 396 if (plugin.settings.event === "hover_intent") { 397 plugin.unbindHoverIntentEvents(); 398 } 399 400 if (plugin.settings.event === "hover") { 401 plugin.unbindHoverEvents(); 402 } 403 }); 404 405 clickable_parents.on("click.megamenu", function(e) { 406 if ( $(e.target).hasClass('mega-indicator') ) { 407 return; 408 } 409 410 if (plugin.isDesktopView() && $(this).parent().hasClass("mega-toggle-on") && $(this).closest("ul.mega-sub-menu").parent().hasClass("mega-menu-tabbed") ) { 411 if (plugin.settings.second_click === "go") { 412 return; 413 } else { 414 e.preventDefault(); 415 return; 416 } 417 } 418 if (dragging) { 419 return; 420 } 421 if (plugin.isMobileView() && $(this).parent().hasClass("mega-hide-sub-menu-on-mobile")) { 422 return; // allow all clicks on parent items when sub menu is hidden on mobile 423 } 424 if ((plugin.settings.second_click === "go" || $(this).parent().hasClass("mega-click-click-go")) && $(this).attr("href") !== undefined) { // check for second click 425 if (!$(this).parent().hasClass("mega-toggle-on")) { 426 e.preventDefault(); 427 plugin.showPanel($(this)); 428 } 429 } else { 430 e.preventDefault(); 431 432 if ($(this).parent().hasClass("mega-toggle-on")) { 433 plugin.hidePanel($(this), false); 434 } else { 435 plugin.showPanel($(this)); 436 } 437 } 438 }); 439 440 if ( plugin.settings.second_click === "disabled" ) { 441 clickable_parents.off("click.megamenu"); 442 } 443 444 $(".mega-close-after-click:not(.mega-menu-item-has-children) > a.mega-menu-link", $menu).on("click.megamenu", function() { 445 plugin.hideAllPanels(); 446 plugin.hideMobileMenu(); 447 }); 448 449 $("button.mega-close", $wrap).on("click.megamenu", function(e) { 450 plugin.hideMobileMenu(); 451 }); 452 }; 453 454 plugin.bindHoverEvents = function() { 455 items_with_submenus.on({ 456 "mouseenter.megamenu" : function() { 457 plugin.unbindClickEvents(); 458 if (! $(this).hasClass("mega-toggle-on")) { 459 plugin.showPanel($(this).children("a.mega-menu-link")); 460 } 461 }, 462 "mouseleave.megamenu" : function() { 463 if ($(this).hasClass("mega-toggle-on") && ! $(this).hasClass("mega-disable-collapse") && ! $(this).parent().parent().hasClass("mega-menu-tabbed")) { 464 plugin.hidePanel($(this).children("a.mega-menu-link"), false); 465 } 466 } 467 }); 468 }; 469 470 plugin.bindHoverIntentEvents = function() { 471 items_with_submenus.hoverIntent({ 472 over: function () { 473 plugin.unbindClickEvents(); 474 if (! $(this).hasClass("mega-toggle-on")) { 475 plugin.showPanel($(this).children("a.mega-menu-link")); 476 } 477 }, 478 out: function () { 479 if ($(this).hasClass("mega-toggle-on") && ! $(this).hasClass("mega-disable-collapse") && ! $(this).parent().parent().hasClass("mega-menu-tabbed")) { 480 plugin.hidePanel($(this).children("a.mega-menu-link"), false); 481 } 482 }, 483 timeout: plugin.settings.hover_intent_timeout, 484 interval: plugin.settings.hover_intent_interval 485 }); 486 }; 487 488 plugin.isMobileOffCanvas = function() { 489 return plugin.settings.effect_mobile === 'slide_left' || plugin.settings.effect_mobile === 'slide_right';
490 }; 491 492 plugin.shouldGoToNextTopLevelItem = function(key) { 493 return ( ( key === right_arrow_key && plugin.isDesktopView() ) || ( key === down_arrow_key && plugin.isMobileView() ) ) && $menu.hasClass("mega-menu-horizontal"); 494 }; 495 496 plugin.shouldGoToPreviousTopLevelItem = function(key) { 497 return ( ( key === left_arrow_key && plugin.isDesktopView() ) || ( key === up_arrow_key && plugin.isMobileView() ) ) && $menu.hasClass("mega-menu-horizontal"); 498 }; 499 500 plugin.bindKeyboardEvents = function() { 501 const $firstFocusable = $menu.find("a.mega-menu-link").first(); 502 const $lastFocusable = $wrap.find("button.mega-close").first(); 503 // Matches the focusable link element inside a visible menu list item. 504 // Must stay in sync with getFocusableItemsInSubmenu. 505 const focusableLinkSelector = "> a.mega-menu-link, > button.mega-menu-link, > .mega-search span[role=button]"; 506 507 const getActiveSubmenuBackLink = function() { 508 const $activeSubmenu = $("li.mega-toggle-on:not(.mega-collapse-children) > ul.mega-sub-menu", $menu).last(); 509 return $activeSubmenu.find("> li.mega-mobile-back:visible > button.mega-menu-link.mega-mobile-back-link").first(); 510 }; 511 512 const togglePanelForAnchor = function(anchor) { 513 if ( !anchor || !anchor.length ) return; 514 if ( anchor.parent().hasClass("mega-toggle-on") && !anchor.closest("ul.mega-sub-menu").parent().hasClass("mega-menu-tabbed") ) { 515 plugin.hidePanel(anchor); 516 } else { 517 plugin.showPanel(anchor); 518 } 519 }; 520 521 const closeNearestOpenPanelAndRefocus = function() { 522 const $focused = $menu[0].contains(document.activeElement) ? $(document.activeElement) : $(); 523 const $parentAnchor = $("> a.mega-menu-link", $focused.closest(".mega-toggle-on")); 524 if ( $parentAnchor.length ) { 525 plugin.hidePanel($parentAnchor); 526 $parentAnchor.trigger("focus"); 527 return true; 528 } 529 return false; 530 }; 531 532 // ââ Key Handlers âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 533 534 const handleTabKey = function(e, $active, isOffCanvasHorizontal, isMobileOffCanvas) { 535 // Close button: hand focus off to the menu, or receive it back from the menu 536 if ( $active.is($lastFocusable) && isMobileOffCanvas ) { 537 if ( !e.shiftKey ) { 538 // Tab â forward to back link (if a flyout is open) or first menu item 539 e.preventDefault(); 540 const $backLink = isOffCanvasHorizontal ? getActiveSubmenuBackLink() : $(); 541 ( $backLink.length ? $backLink : $firstFocusable ).trigger('focus'); 542 } else if ( isOffCanvasHorizontal ) { 543 // Shift+Tab â last item in the active submenu 544 const $last = plugin.getFocusableItemsInSubmenu( 545 $("li.mega-toggle-on:not(.mega-collapse-children) > ul.mega-sub-menu", $menu).last(), false 546 ).last(); 547 if ( $last.length ) { e.preventDefault(); $last.trigger('focus'); } 548 } 549 return; 550 } 551 552 // First top-level item: Shift+Tab wraps around to the close button 553 if ( isMobileOffCanvas && e.shiftKey && $active.is($firstFocusable) ) { 554 e.preventDefault(); 555 $lastFocusable.trigger('focus'); 556 return; 557 } 558 559 // Inside an offcanvas horizontal submenu: keep focus trapped within the panel 560 if ( isOffCanvasHorizontal ) { 561 // Walk up to the nearest ul.mega-sub-menu whose parent li has mega-toggle-on. 562 // For mega menus, $active may be inside a column ul rather than the panel ul itself. 563 const $submenu = $active.parentsUntil($menu, "ul.mega-sub-menu") 564 .filter(function() { 565 const $p = $(this).parent(); 566 return $p.hasClass("mega-toggle-on") && !$p.hasClass("mega-collapse-children"); 567 }) 568 .first(); 569 if ( $submenu.length ) { 570 if ( !e.shiftKey ) { 571 // Tab forward: when on the last item, hand off to the close button 572 const $items = plugin.getFocusableItemsInSubmenu($submenu, false); 573 if ( $items.length && $active.is($items.last(
573)) ) { 574 e.preventDefault(); 575 $lastFocusable.trigger('focus'); 576 } 577 } else if ( $active.hasClass("mega-mobile-back-link") ) { 578 // Shift+Tab on back link â close button 579 e.preventDefault(); 580 $lastFocusable.trigger('focus'); 581 } else { 582 // Shift+Tab on first item â back link 583 const $items = plugin.getFocusableItemsInSubmenu($submenu, false); 584 const $backLink = $submenu.find("> li.mega-mobile-back:visible > button.mega-menu-link.mega-mobile-back-link").first(); 585 if ( $items.length && $backLink.length && $active.is($items.first()) ) { 586 e.preventDefault(); 587 $backLink.trigger('focus'); 588 } 589 } 590 } 591 } 592 }; 593 594 const handleToggleBarTrigger = function(e) { 595 e.preventDefault(); 596 if ( $toggle_bar.hasClass("mega-menu-open") ) { 597 plugin.hideMobileMenu(); 598 } else { 599 plugin.showMobileMenu(); 600 html_body_class_timeout = setTimeout(function() { 601 $menu.find("a.mega-menu-link").first().trigger('focus'); 602 }, plugin.settings.effect_speed_mobile); 603 } 604 }; 605 606 const handleSpaceKey = function(e, $active) { 607 if ( $active.is("a.mega-menu-link") ) { 608 e.preventDefault(); // prevent page scroll on any menu link 609 if ( $active.parent().is(items_with_submenus) ) togglePanelForAnchor($active); 610 } else if ( $active.is(".mega-indicator") ) { 611 e.preventDefault(); 612 togglePanelForAnchor($active.parent()); 613 } 614 }; 615 616 const handleEscapeKey = function() { 617 const submenu_open = $(".mega-toggle-on", $menu).length !== 0; 618 if ( submenu_open && closeNearestOpenPanelAndRefocus() ) return; 619 if ( plugin.isMobileView() && !submenu_open ) plugin.hideMobileMenu(); 620 }; 621 622 const handleEnterKey = function(e, $active) { 623 if ( $active.is(".mega-indicator") ) { 624 togglePanelForAnchor($active.parent()); 625 return; 626 } 627 if ( $active.parent().is(items_with_submenus) ) { 628 if ( plugin.isMobileView() && $active.parent().is(".mega-hide-sub-menu-on-mobile") ) return; 629 if ( !$active.is("[href]") ) { togglePanelForAnchor($active); return; } 630 if ( $active.parent().hasClass("mega-toggle-on") && !$active.closest("ul.mega-sub-menu").parent().hasClass("mega-menu-tabbed") ) return; 631 e.preventDefault(); 632 plugin.showPanel($active); 633 } 634 }; 635 636 const handleArrowUpDown = function(e, goingDown) { 637 e.preventDefault(); 638 const $activeSubmenu = $("li.mega-toggle-on:not(.mega-collapse-children) > ul.mega-sub-menu", $menu).last(); 639 640 if ( $activeSubmenu.length ) { 641 // Inside a flyout panel: cycle through the same sequence as Tab/Shift+Tab â 642 // back-link â items â close-button â using an explicit ordered array so 643 // the back button (li.mega-mobile-back, not li.mega-menu-item) is included. 644 const $backLink = $activeSubmenu.find("> li.mega-mobile-back:visible > button.mega-menu-link.mega-mobile-back-link").first(); 645 const $items = plugin.getFocusableItemsInSubmenu($activeSubmenu, false); 646 const focusOrder = []; 647 if ( $backLink.length ) focusOrder.push($backLink[0]); 648 $items.each(function() { focusOrder.push(this); }); 649 if ( $lastFocusable.length ) focusOrder.push($lastFocusable[0]); 650 651 const idx = focusOrder.indexOf(document.activeElement); 652 const nextIdx = (idx < 0)
653 ? (goingDown ? 0 : focusOrder.length - 1) 654 : (idx + (goingDown ? 1 : -1) + focusOrder.length) % focusOrder.length; 655 $(focusOrder[nextIdx]).trigger('focus'); 656 } else { 657 // Top level: cycle through the visible top-level links. 658 const $topLinks = $menu.children("li.mega-menu-item:visible").find(focusableLinkSelector); 659 const idx = $topLinks.index(document.activeElement); 660 if ( idx >= 0 ) { 661 const nextIdx = (idx + (goingDown ? 1 : -1) + $topLinks.length) % $topLinks.length; 662 $topLinks.eq(nextIdx).trigger('focus'); 663 } 664 } 665 }; 666 667 const handleArrowLeftRight = function(e, goingToNext) { 668 e.preventDefault(); 669 // Use find(focusableLinkSelector) with > so only direct-child links are 670 // matched â prevents picking up nested submenu links as top-level items. 671 const $topLinks = $menu.children("li.mega-menu-item:visible").find(focusableLinkSelector); 672 const $topLink = $(document.activeElement) 673 .closest($menu.children("li.mega-menu-item")) 674 .find(focusableLinkSelector).first(); 675 const idx = $topLinks.index($topLink); 676 const nextIdx = goingToNext ? idx + 1 : idx - 1; 677 // Guard: nextIdx < 0 would let jQuery's eq(-1) silently wrap to the last item 678 if ( idx >= 0 && nextIdx >= 0 ) { 679 plugin.hideAllPanels(); 680 $topLinks.eq(nextIdx).trigger('focus'); // eq(length) = empty set = no-op at boundary 681 } 682 }; 683 684 // ââ Event Bindings âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 685 686 $wrap.on("keyup.megamenu", ".max-mega-menu, .mega-menu-toggle", function(e) { 687 if ( e.key !== tab_key ) return; 688 $wrap.addClass("mega-keyboard-navigation"); 689 plugin.bindClickEvents(); // Windows Narrator ignores Enter, so ensure click events are bound on tab 690 const $target = $(e.target); 691 if ( plugin.isDesktopView() && $target.is(".mega-menu-link") && $target.parent().parent().hasClass('max-mega-menu') ) { 692 plugin.hideAllPanels(); 693 } 694 }); 695 696 $wrap.on("keydown.megamenu", "a.mega-menu-link, button.mega-menu-link, .mega-indicator, .mega-menu-toggle-block, .mega-menu-toggle-animated-block button, button.mega-close", function(e) { 697 if ( !$wrap.hasClass("mega-keyboard-navigation") ) return; 698 699 const key = e.key; 700 const $active = $(e.target); 701 const isOffCanvasHorizontal = plugin.isHorizontalMobileSubmenuMode(); 702 const isMobileOffCanvas = plugin.isMobileView() && plugin.isMobileOffCanvas(); 703 704 switch (key) { 705 case tab_key: 706 handleTabKey(e, $active, isOffCanvasHorizontal, isMobileOffCanvas); 707 break; 708 709 case space_key: 710 if ( $active.is(".mega-menu-toggle-block button, .mega-menu-toggle-animated-block button") ) { 711 handleToggleBarTrigger(e); 712 } else { 713 handleSpaceKey(e, $active); 714 } 715 break; 716 717 case enter_key: 718 if ( $active.is(".mega-menu-toggle-block button, .mega-menu-toggle-animated-block button") ) { 719 handleToggleBarTrigger(e); 720 } else { 721 handleEnterKey(e, $active); 722 } 723 break; 724 725 case escape_key: 726 handleEscapeKey(); 727 break; 728 729 case up_arrow_key: 730 case down_arrow_key: 731 if ( isOffCanvasHorizontal ) { 732 handleArrowUpDown(e, key === down_arrow_key); 733 } 734 break; 735 736 case left_arrow_key: 737 case right_arrow_key: 738 const goingToNext = plugin.shouldGoToNextTopLevelItem(key); 739 const goingToPrev = plugin.shouldGoToPreviousTopLevelItem(key); 740 if ( goingToNext || goingToPrev ) { 741 handleArrowLeftRight(e, goingToNext); 742 } 743 break; 744 } 745 }); 746 747 $wrap.on("focusout.megamenu", function(e) { 748 if ( $wrap.hasClass("mega-keyboard-navigation") ) { 749 setTimeout(function() {
750 if ( !$wrap[0].contains(document.activeElement) ) { 751 $wrap.removeClass("mega-keyboard-navigation"); 752 plugin.hideAllPanels(); 753 plugin.hideMobileMenu(); 754 } 755 }, 10); 756 } 757 }); 758 }; 759 760 plugin.unbindAllEvents = function() { 761 $(document).off(docEventNamespace); 762 $("ul.mega-sub-menu, li.mega-menu-item, li.mega-menu-row, li.mega-menu-column, a.mega-menu-link, .mega-indicator", $menu).off(); 763 }; 764 765 plugin.unbindClickEvents = function() { 766 if ( $wrap.hasClass('mega-keyboard-navigation') ) { 767 return; 768 } 769 770 $(document).off(docEventNamespace); 771 772 // collapsable parents always have a click event 773 $("> a.mega-menu-link", items_with_submenus).not(collapse_children_parents).off("click.megamenu touchend.megamenu"); 774 }; 775 776 plugin.unbindHoverEvents = function() { 777 items_with_submenus.off("mouseenter.megamenu mouseleave.megamenu"); 778 }; 779 780 plugin.unbindHoverIntentEvents = function() { 781 items_with_submenus.off("mouseenter mouseleave").removeProp("hoverIntent_t").removeProp("hoverIntent_s"); // hoverintent does not allow namespaced events 782 }; 783 784 plugin.unbindKeyboardEvents = function() { 785 $wrap.off("keyup.megamenu keydown.megamenu focusout.megamenu"); 786 }; 787 788 plugin.unbindMegaMenuEvents = function() { 789 if (plugin.settings.event === "hover_intent") { 790 plugin.unbindHoverIntentEvents(); 791 } 792 793 if (plugin.settings.event === "hover") { 794 plugin.unbindHoverEvents(); 795 } 796 797 plugin.unbindClickEvents(); 798 plugin.unbindKeyboardEvents(); 799 }; 800 801 plugin.bindMegaMenuEvents = function() { 802 plugin.unbindMegaMenuEvents(); 803 804 const isDesktop = plugin.isDesktopView(); 805 806 if (isDesktop && plugin.settings.event === "hover_intent") { 807 plugin.bindHoverIntentEvents(); 808 } 809 810 if (isDesktop && plugin.settings.event === "hover") { 811 plugin.bindHoverEvents(); 812 } 813 814 plugin.bindClickEvents(); // always bind click events for touch screen devices 815 plugin.bindKeyboardEvents(); 816 }; 817 818 plugin.checkWidth = function() { 819 if ( plugin.isMobileView() && $menu.data("view") === "desktop" ) { 820 plugin.switchToMobile(); 821 } 822 823 if ( plugin.isDesktopView() && $menu.data("view") === "mobile" ) { 824 plugin.switchToDesktop(); 825 } 826 827 plugin.calculateDynamicSubmenuWidths($("> li.mega-menu-megamenu > a.mega-menu-link", $menu)); 828 }; 829 830 plugin.reverseRightAlignedItems = function() { 831 if ( ! $("body").hasClass("rtl") && $menu.hasClass("mega-menu-horizontal") && $menu.css("display") !== 'flex' ) { 832 $menu.append($menu.children("li.mega-item-align-right").get().reverse()); 833 } 834 }; 835 836 plugin.addClearClassesToMobileItems = function() { 837 $(".mega-menu-row", $menu).each(function() { 838 $("> .mega-sub-menu > .mega-menu-column:not(.mega-hide-on-mobile)", $(this)).filter(":even").addClass("mega-menu-clear"); // :even is 0 based 839 }); 840 }; 841 842 plugin.initDesktop = function() { 843 $menu.data("view", "desktop"); 844 plugin.bindMegaMenuEvents(); 845 plugin.initIndicators(); 846 $menu.trigger("mmm:switchToDesktop"); 847 }; 848 849 plugin.initMobile = function() { 850 plugin.switchToMobile(); 851 }; 852 853 plugin.switchToDesktop = function() { 854 $menu.data("view", "desktop"); 855 plugin.bindMegaMenuEvents(); 856 plugin.reverseRightAlignedItems(); 857 plugin.hideAllPanels(); 858 plugin.hideMobileMenu(true); 859 $menu.removeAttr('role'); 860 $menu.removeAttr('aria-modal'); 861 $menu.removeAttr('aria-hidden'); 862 $menu.trigger("mmm:switchToDesktop"); 863 }; 864 865 plugin.switchToMobile = function() { 866 $menu.data("view", "mobile"); 867 868 if (plugin.isMobileOffCanvas() && $toggle_bar.is(":visible") ) { 869 $menu.attr('role', 'dialog'); 870 $menu.attr('aria-modal', 'true'); 871 $menu.attr('aria-hidden', 'true'); 872 } 873 874 plugin.bindMegaMenuEvents(); 875 plugin.initIndicators(); 876 plugin.reverseRightAlignedItems(); 877 plugin.addClearClassesToMobileItems(); 878 plugin.hideAllPanels(); 879 plugin.expandMobileSubMenus(); 880 881 $menu.trigger("mmm:switchToMobile"); 882 883 }; 884 885 plugin.initToggleBar = function() { 886 $toggle_bar.on("click", function(e) { 887 const isToggleTrigger = $(e.target).closest(".mega-menu-toggle-block, button.mega-toggle-animated, .mega-menu-toggle-custom-block", this).length; 888 889 if ( isToggleTrigger ) { 890 e.preventDefault(); 891 if ($(this).hasClass("mega-menu-open")) { 892 plugin.hideMobileMenu(); 893 } else { 894 plugin.showMobileMenu(); 895 } 896 } else if ( e.target === this && plugin.isMobileOffCanvas() ) { 897 plugin.hideMobileMenu(); 898 } 899 }); 900 }; 901 902 plugin.initIndicators = function() { 903 $menu.off('click.megamenu', '.mega-indicator'); 904 905 $menu.on('click.megamenu', '.mega-indicator', function(e) { 906 e.preventDefault(); 907 e.stopPropagation(); 908 909 if ( $(this).closest(".mega-menu-item").hasClass("mega-toggle-on") ) { 910 if ( ! $(this).closest("ul.mega-sub-menu").parent().hasClass("mega-menu-tabbed") || plugin.isMobileView() ) { 911 plugin.hidePanel($(this).parent(), false); 912 } 913 } else { 914 plugin.showPanel($(this).parent(), false); 915 } 916 }); 917 }; 918 919 plugin.hideMobileMenu = function(force = false) { 920 921 if ( ! $toggle_bar.is(":visible") && ! force ) { 922 return; 923 } 924 925 $menu.attr("aria-hidden", "true"); 926 927 clearTimeout(html_body_class_timeout); 928 html_body_class_timeout = setTimeout(function() { 929 $("body").removeClass(menuId + "-mobile-open"); 930 $("html").removeClass(menuId + "-off-canvas-open"); 931 }, plugin.settings.effect_speed_mobile); 932 933 if ($wrap.hasClass("mega-keyboard-navigation")) { 934 $(".mega-menu-toggle-block button, button.mega-toggle-animated", $toggle_bar).first().trigger('focus'); 935 } 936 937 $(".mega-toggle-label, .mega-toggle-animated", $toggle_bar).attr("aria-expanded", "false"); 938 939 if (plugin.settings.effect_mobile === "slide" && ! force ) { 940 $menu.animate({"height":"hide"}, plugin.settings.effect_speed_mobile, function() { 941 $menu.css({ 942 width: "", 943 left: "", 944 display: "" 945 }); 946 947 $toggle_bar.removeClass("mega-menu-open"); 948 }); 949 } else { 950 $menu.css({ 951 width: "", 952 left: "", 953 display: "" 954 }); 955 956 $toggle_bar.removeClass("mega-menu-open"); 957 } 958 959 960 $menu.triggerHandler("mmm:hideMobileMenu"); 961 }; 962 963 plugin.showMobileMenu = function() { 964 if ( ! $toggle_bar.is(":visible") ) { 965 return; 966 } 967 968 clearTimeout(html_body_class_timeout); 969 970 $("body").addClass(menuId + "-mobile-open"); 971 972 plugin.expandMobileSubMenus(); 973 974 if ( plugin.isMobileOffCanvas() ) { 975 $("html").addClass(menuId + "-off-canvas-open"); 976 } 977 978 if (plugin.settings.effect_mobile === "slide") { 979 $menu.animate({"height":"show"}, plugin.settings.effect_speed_mobile, function() { 980 $(this).css("display", ""); 981 }); 982 } 983 984 $(".mega-toggle-label, .mega-toggle-animated", $toggle_bar).attr("aria-expanded", "true"); 985 986 $toggle_bar.addClass("mega-menu-open"); 987 988 plugin.toggleBarForceWidth(); 989 990 $menu.attr("aria-hidden", "false"); 991 $menu.triggerHandler("mmm:showMobileMenu"); 992 }; 993 994 plugin.toggleBarForceWidth = function() { 995 const $force_width_el = $(plugin.settings.mobile_force_width); 996 997 if ($force_width_el.length && ( plugin.settings.effect_mobile === "slide" || plugin.settings.effect_mobile === "disabled" ) ) { 998 const submenu_offset = $toggle_bar.offset(); 999 const target_offset = $force_width_el.offset(); 1000 1001 $menu.css({ 1002 width: $force_width_el.outerWidth(), 1003 left: (target_offset.left - submenu_offset.left) + "px" 1004 }); 1005 } 1006 }; 1007 1008 plugin.doConsoleChecks = function() {
1009 if (plugin.settings.mobile_force_width !== "false" && ! $(plugin.settings.mobile_force_width).length && ( plugin.settings.effect_mobile === "slide" || plugin.settings.effect_mobile === "disabled" ) ) { 1010 console.warn('Max Mega Menu #' + $wrap.attr('id') + ': Mobile Force Width element (' + plugin.settings.mobile_force_width + ') not found'); 1011 } 1012 1013 const cssWidthRegex = /^((\d+(\.\d+)?(px|%|em|rem|vw|vh|ch|ex|cm|mm|in|pt|pc))|auto)$/i; 1014 1015 const resolveWidthEl = (val) => val === '$menu' ? $menu : val === '$wrap' ? $wrap : $(val); 1016 1017 if (plugin.settings.panel_width !== undefined && ! cssWidthRegex.test(plugin.settings.panel_width) && ! resolveWidthEl(plugin.settings.panel_width).length ) { 1018 console.warn('Max Mega Menu #' + $wrap.attr('id') + ': Panel Width (Outer) element (' + plugin.settings.panel_width + ') not found'); 1019 } 1020 1021 if (plugin.settings.panel_inner_width !== undefined && ! cssWidthRegex.test(plugin.settings.panel_inner_width) && ! resolveWidthEl(plugin.settings.panel_inner_width).length ) { 1022 console.warn('Max Mega Menu #' + $wrap.attr('id') + ': Panel Width (Inner) element (' + plugin.settings.panel_inner_width + ') not found'); 1023 } 1024 }; 1025 1026 plugin.init = function() { 1027 $menu.triggerHandler("before_mega_menu_init"); 1028 plugin.settings = $.extend({}, defaults, options); 1029 1030 if (window.console) { 1031 plugin.doConsoleChecks(); 1032 } 1033 1034 $menu.removeClass("mega-no-js"); 1035 1036 plugin.initToggleBar(); 1037 1038 if (plugin.settings.unbind_events === "true") { 1039 plugin.unbindAllEvents(); 1040 } 1041 1042 if ( document.readyState === 'complete' ) { 1043 plugin.calculateDynamicSubmenuWidths($("> li.mega-menu-megamenu > a.mega-menu-link", $menu)); 1044 } else { 1045 $(window).on("load", function() { 1046 plugin.calculateDynamicSubmenuWidths($("> li.mega-menu-megamenu > a.mega-menu-link", $menu)); 1047 }); 1048 } 1049 1050 if ( plugin.isDesktopView() ) { 1051 plugin.initDesktop(); 1052 } else { 1053 plugin.initMobile(); 1054 } 1055 1056 $(window).on("resize", function() { 1057 plugin.checkWidth(); 1058 }); 1059 1060 $menu.triggerHandler("after_mega_menu_init"); 1061 }; 1062 1063 plugin.init(); 1064 }; 1065 1066 $.fn.maxmegamenu = function(options) { 1067 return this.each(function() { 1068 if (undefined === $(this).data("maxmegamenu")) { 1069 const plugin = new $.maxmegamenu(this, options); 1070 $(this).data("maxmegamenu", plugin); 1071 } 1072 }); 1073 }; 1074 1075 $(function() { 1076 $(".max-mega-menu").maxmegamenu(); 1077 }); 1078}( jQuery ));
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.