1(($) => { 2'use strict'; 3 4/** @description document.documentElement.clientWidth <= 992 */ 5const megaMenuBreakpoint = window.matchMedia('(max-width: 992px)'); 6//SMBWMGR-7625- hide mmenu before init on theme level 7try { 8 /** @type {HTMLStyleElement}*/ 9 const style = document.createElement('STYLE'); 10 document.head.append(style); 11 style.sheet.insertRule(`.mm-menu.mm-offcanvas { display: none; }`); 12} catch (e) { 13 console.error(e); 14} 15 16 17let firstMenuItemResizeObserve; 18const menuItemResizeObserver = new ResizeObserver((_entries, o) => { 19 // ignore the event when it fires upon adding items 20 if (firstMenuItemResizeObserve) { 21 firstMenuItemResizeObserve = false; 22 return; 23 } 24 // re-initialize navigations, no matter what item caused the event 25 schedulePositionNavs(); 26}); 27 28let positionNavsTimer = 0; 29 30function schedulePositionNavs() { 31 if (positionNavsTimer) return; 32 const myTimer = positionNavsTimer = requestAnimationFrame(() => { 33 if (myTimer !== positionNavsTimer) return; 34 positionNavs(); 35 positionNavsTimer = 0; 36 }); 37} 38 39function positionNavs() { 40 NavigationAMoreList.reposition(); 41 // navigations 42 MegaMenu.reposition(); 43} 44 45function addMMenuTitleAttributes() { 46 // add title attributes to mobile menu (mm-menu) no text links - SMBWMGR-3784 47 for (const el of document.querySelectorAll('nav.mm-menu a.mm-prev:not([title])')) { 48 const title = el.parentElement.querySelector('a.mm-title')?.textContent; 49 if (title) el.setAttribute('title', title); 50 } 51 52 for (const el of document.querySelectorAll('nav.mm-menu a.mm-next:not([title])')) { 53 /** @type {null|HTMLElement} */ 54 const titleEl = el.parentElement.querySelector(':scope > a[data-title]'); 55 const title = titleEl?.dataset.title; 56 if (title) el.setAttribute('title', title); 57 } 58 59 for (const el of document.querySelectorAll('nav.mm-menu a.mm-close:not([title])')) { 60 el.setAttribute('title', 'Close'); 61 } 62} 63 64class NavigationAMoreList { 65 static Templates = new class { 66 #style1; 67 /** @type {DocumentFragment} */ 68 get style1() { 69 return this.#style1(); 70 }; 71 72 #style2; 73 /** @type {DocumentFragment} */ 74 get style2() { 75 return this.#style2(); 76 } 77 78 #styleDefault; 79 /** @type {DocumentFragment} */ 80 get styleDefault() { 81 return this.#styleDefault(); 82 } 83 84 constructor() { 85 const lazyFragment = (templateString) => { 86 /** @type {DocumentFragment} */ 87 let cache; 88 return () => { 89 if (cache) return cache; 90 /** @type {HTMLTemplateElement} */ 91 const templateEl = document.createElement('TEMPLATE'); 92 templateEl.innerHTML = templateString; 93 cache = templateEl.content; 94 return cache; 95 }; 96 }; 97 98 this.#style1 = lazyFragment('<li class="navigation__item navigation__item--default has-drop-down show-drop-down drop-down-style--1 more"> <a href="#" class="navigation__item--link navigation__item--expand_more">More<span class="navigation__item--expand" title="View more" aria-expanded="false"></span> </a><ul class="navigation__list navigation__list--default navigation__list--sub"></ul> </li>'); 99 this.#style2 = lazyFragment('<li class="navigation__item navigation__item--default has-drop-down show-drop-down drop-down-style--2 more"> <a href="#" class="navigation__item--link navigation__item--expand_more">More<span class="navigation__item--expand" title="View more" aria-expanded="false"></span> </a><ul class="navigation__list navigation__list--default navigation__list--sub"></ul> </li>'); 100 this.#styleDefault = lazyFragment('<li class="navigation__item navigation__item--default has-drop-down more"> <a href="#" class="navigation__item--link navigation__item--expand_more">More<span class="navigation__item--expand" title="View more" aria-expanded="false"></span> </a><ul class="navigation__list navigation__list--default navigation__list--sub"></ul> </li>'); 101 } 102 }; 103 104 static unmask() { 105 const loadingMask = document.querySelectorAll('.navigation-a > .navigation__list.more_nav--loading'); 106 for (const el of loadingMask) { 107 el.classList.remove('more_nav--loading'); 108 } 109 return this; 110 } 111 112 /** 113 * @param {HTMLElement} el 114 * @param {Partial<typeof NavigationAMoreList.Defaults>} [defaults] 115 * @return {NavigationAMoreList} 116 */ 117 static create(el, defaults) { 118 const instance = new this(el, defaults); 119 this.active.add(instance); 120 return instance; 121 } 122 123 /** 124 * A Set of the Navigation Menus with More enabled, populated post ready 125 * @type {Set<NavigationAMoreList>} 126 */ 127 static active = new Set(); 128 129 static reposition() { 130 // Handle Nav A with More button repositioning 131 for (const navAMore of this.active) { 132 if (navAMore.arrange() === false) this.active.delete(navAMore); 133 } 134 } 135 136 /** 137 * @type {WeakRef<HTMLElement>} 138 */ 139 ref; 140 /** 141 * @type {{el: HTMLElement; initalStyles: string; initalClasses: string; reset():void; hide():void;}} 142 */ 143 more; 144 145 get el() { 146 return this.ref.deref(); 147 } 148 149 /**
150 * @description The Cached settings of the initial direct children used for tracking and restoring positioning of 151 * the Menu items in Navigation A 152 * @type {Array<NavigationAMoreMenuItem>} 153 */ 154 initalState; 155 156 /** 157 * 158 * @param {HTMLElement} el 159 * @param {Partial<typeof NavigationAMoreList.Defaults>} [defaults] 160 */ 161 constructor(el, defaults = {}) { 162 this.ref = new WeakRef(el); 163 const parentClassList = el.parentElement.classList; 164 // Create More Button. 165 const {Templates} = this.constructor; 166 167 this.initalState = Array.from(el.children).map( 168 (el) => new NavigationAMoreMenuItem(el) 169 ); 170 const moreElTemplate = parentClassList.contains('drop-down-style--1') 171 ? Templates.style1 172 : parentClassList.contains('drop-down-style--2') 173 ? Templates.style2 174 : Templates.styleDefault; 175 this.more = new class { 176 /** @type {HTMLElement} */ 177 el; 178 /** @type {string} */ 179 initalClasses; 180 /** @type {string} */ 181 initalStyles; 182 183 /** 184 * @param {HTMLElement} el 185 */ 186 constructor(el) { 187 this.el = el; 188 this.initalClasses = this.el.getAttribute('class') || ''; 189 this.initalStyles = this.el.getAttribute('styles') || ''; 190 191 el.querySelector('a.navigation__item.more')?.addEventListener('click', (e) => { 192 e.preventDefault(); 193 }); 194 } 195 196 reset() { 197 this.el.setAttribute('class', this.initalClasses); 198 this.el.setAttribute('style', this.initalStyles); 199 this.el.hidden = false; 200 } 201 202 hide() { 203 this.el.hidden = true; 204 } 205 }(moreElTemplate.cloneNode(true).firstElementChild); 206 this.more.hide(); 207 el.append(this.more.el); 208 209 this.defaults = {...this.constructor.Defaults, ...defaults}; 210 } 211 212 /** 213 * @description arranges the menu's children 214 * @return {void|boolean} false if the html element has lost all references 215 */ 216 arrange() { 217 const menuEl = this.el; 218 // Has the element been GCed? 219 if (!menuEl) return false; 220 if (!$(this.el).is(':visible')) return; 221 222 // Pause the observers 223 for (const item of menuEl.children) { 224 menuItemResizeObserver.unobserve(item); 225 } 226 227 const liveItems = []; 228 // Restore initial order state. 229 for (const menuItemState of this.initalState.slice()) { 230 const menuItem = menuItemState.reset(); 231 if (menuItem) { 232 liveItems.push(menuItem); 233 continue; 234 } 235 236 // Reset failed because it has been removed. Delete it. 237 const index = this.initalState.indexOf(menuItemState); 238 if (index !== -1) this.initalState.splice(index, 1); 239 } 240 241 // Remove more button, and restore the order 242 menuEl.replaceChildren(...liveItems); 243 244 if (menuEl.children.length) this.tryToFitMore(); 245 246 // while adding items to the resize observer, the event fires... we ignore that one time 247 firstMenuItemResizeObserve = true; 248 for (const item of menuEl.children) { 249 menuItemResizeObserver.observe(item, {box: 'border-box'}); 250 } 251 } 252 253 static Defaults = { 254 menuWidthBuffer: 4 255 , menuMaxOtherItemsWhenMore: 6 256 }; 257 /** 258 * @type {typeof NavigationAMoreList.Defaults} 259 */ 260 defaults; 261 262 tryToFitMore() { 263 const menuEl = this.el; 264 const $menu = $(menuEl); 265 const {menuWidthBuffer, menuMaxOtherItemsWhenMore} = this.defaults; 266 const itemsCount = menuEl.children.length; 267 const $closestAlias = $menu.closest("div[class*='alias-']"); 268 const availableWidth = ($closestAlias.is("div[class*='alias-partial-header']")) ? 269 Math.floor($menu.closest('.header-partial-navigation').width()) : 270 Math.floor($closestAlias.width()); 271 if (availableWidth < menuWidthBuffer) { 272 // if menu width could not be established, leave all items on it, and do not add the More menu 273 // this should never happen, the menu's container should always be resolvable 274 // we're outputting a "soft error" here -- this should be looked at by a developer 275 console.error("Navigation Error: Menu container width could not be established. Skipped adding the More menu."); 276 return; 277 } 278 279 // Check to see if everything is A-OK with the initial state! 280 if ( 281 itemsCount <= menuMaxOtherItemsWhenMore 282 && (Math.ceil(calculateMenuWidth($menu)) + menuWidthBuffer) <= availableWidth 283 ) return; 284 285 286 this.more.reset(); 287 const moreListEl = this.more.el.querySelector(':scope > ul'); 288 const itemsOverflowed = document.createDocumentFragment(); 289 290 const menuItems = $menu.children('li:not(.more)'); 291 menuEl.append(this.more.el); 292 if ((itemsCount > menuMaxOtherItemsWhenMore)) { 293 // Take the excess items, and detach them from the dom, this is now our starter set 294 itemsOverflowed.append(...menuItems.slice(Math.max(menuMaxOtherItemsWhenMore, 0)).detach().toArray()); 295 } 296 297 // Try to make enough room for the more button. 298 if ((Math.ceil(calculateMenuWidth($menu)) + menuWidthBuffer) <= availableWidth) { 299 // Finally append the items that did not fit to the more button. 300 moreListEl?.append(itemsOverflowed); 301 return; 302 } 303 /** 304 * @type {HTMLLIElement[]} 305 */ 306 const remainingItems = menuItems.slice(0, menuMaxOtherItemsWhenMore).toArray().reverse(); 307 // Check width 308 for (const next of remainingItems) { 309 // Drop next item until there's enough width 310 itemsOverflowed.prepend(next); 311 312 if ((Math.ceil(calculateMenuWidth($menu)) + menuWidthBuffer) <= availableWidth) break; 313 // Try again. still not fitting. 314 } 315 moreListEl?.append(itemsOverflowed); 316 } 317} 318 319function calculateMenuWidth($menu) { 320 let width = 0; 321 $menu.children().each(function (i) { 322 width += Math.ceil($(this).outerWidth(true)); 323 }); 324 325 return width; 326} 327 328/** 329 * 330 * @param {number} maxWidth 331 * @param {HTMLElement} el 332 * @return {number} 333 */ 334const largestWidth = (maxWidth, el) => { 335 const width = $(el).outerWidth(true); 336 if (width > maxWidth) return width; 337 return maxWidth; 338}; 339 340class MegaMenu { 341 /** 342 * @param {NodeListOf<HTMLElement>} [menuList] 343 */ 344 static reposition(menuList) { 345 menuList ??= document.querySelectorAll( 346 '.navigation__item.mega-menu > ul, .navigation-b .navigation__menu--container, .navigation-c .navigation__menu--container' 347 ); 348 const windowWidth = $(window).width(); 349 for (const el of menuList) this._position(el, windowWidth); 350 } 351 352 /** 353 * 354 * @param {HTMLElement} el 355 * @param {number} windowWidth 356 * @protected 357 */ 358 static _position(el, windowWidth) { 359 const el$ref = $(el); 360 let itemOffset = 0; 361 if (el.parentElement?.classList.contains('mega-menu')) { 362 itemOffset = 15; 363 } 364 365 // Get the left position of the module container. 366 const parentOffset = el$ref.closest('[class*="alias-"]').offset().left; 367 368 /* setting the width of the flyout to be the width of the window. max-width rule from the css stops it to become larger than 1140px 369 On mobile and tablet and partialy on desktop it will be from edge to edge */ 370 el$ref.width(windowWidth); 371 372 /* the magic. the left position is always the widht of the window minus the current width of the flyout divided by 2, 373 and then substruct the flyouts parent offset and add 15px (15px is the padding of the parent)*/ 374 el$ref.css('left', (((windowWidth - el$ref.width()) / 2) - parentOffset + itemOffset)); 375 } 376} 377 378 379class NavigationAMoreMenuItem { 380 /** @type {WeakRef<HTMLElement>} */ 381 ref; 382 /** @type {string} */ 383 classes; 384 /** @type {string} */ 385 styles; 386 387 /** 388 * @param {HTMLElement} el 389 */ 390 constructor(el) { 391 this.ref = new WeakRef(el); 392 this.classes = el.getAttribute('class') || ''; 393 this.styles = el.getAttribute('style') || ''; 394 } 395 396 get el() { 397 return this.ref.deref(); 398 } 399 400 reset() { 401 const el = this.el; 402 if (!el) return false;
403 el.setAttribute('class', this.classes); 404 el.setAttribute('style', this.styles); 405 return el; 406 } 407} 408 409class NavigationA { 410 411 /** 412 * @type {WeakSet<HTMLElement>} 413 */ 414 static initialized = new WeakSet(); 415 416 static initializeAll() { 417 const navigationAList = document.querySelectorAll( 418 '.component.navigation .navigation-a' 419 ); 420 for (const navA of navigationAList) { 421 this.initialize(navA); 422 } 423 return this; 424 } 425 426 static initialize(navA) { 427 // Don't add duplicate event listeners 428 if (this.initialized.has(navA)) return; 429 this.initialized.add(navA); 430 431 const navA$ref = $(navA); 432 // FIXME: This code does not provide any means of managing the menus in desktop mode or tablet mode to produce keyboard navigation. 433 434 const expandDropDown = (el) => { 435 $(el).toggleClass('expanded').parent().next('ul').slideToggle(); 436 }; 437 /** 438 * @param {IJQueryEvent<MouseEvent>} e 439 */ 440 const onExpandMore = (e) => { 441 e.preventDefault(); 442 e.stopImmediatePropagation(); 443 // Trigger the child expand more element (run onExpand) 444 expandDropDown(e.currentTarget.querySelector(':scope > .navigation__item--expand')); 445 }; 446 447 /** 448 * @param {IJQueryEvent<MouseEvent>} e 449 */ 450 const onExpandClick = (e) => { 451 e.preventDefault(); 452 e.stopImmediatePropagation(); 453 expandDropDown(e.currentTarget); 454 }; 455 456 /** @type {(()=>void)[]} */ 457 const mediaResizeDestructors = []; 458 /** 459 * @param {MediaQueryListEvent|MediaQueryList} event 460 */ 461 const onMediaChange = (event) => { 462 for (const fn of mediaResizeDestructors.splice(0, mediaResizeDestructors.length)) try { 463 fn(); 464 } catch (e) { 465 console.error(e); 466 } 467 468 if (!document.contains(navA)) { 469 megaMenuBreakpoint.removeEventListener('change', onMediaChange); 470 return; 471 } 472 473 // document.documentElement.clientWidth <= 992 474 const target$ref = event.matches ? navA$ref : navA$ref.find('.mega-menu'); 475 target$ref.on('click', '.navigation__item--expand_more', onExpandMore); 476 target$ref.on('click', 'a.navigation__item--link .navigation__item--expand', onExpandClick); 477 mediaResizeDestructors.push(() => { 478 target$ref.off('click', '.navigation__item--expand_more', onExpandMore); 479 target$ref.off('click', 'a.navigation__item--link .navigation__item--expand', onExpandClick); 480 481 // slideToggle sets the display to block/none on expand/hide 482 // remove the CSS display value, so the element will use the default value specified in theme 483 target$ref.find('.navigation__list--sub').css('display', ''); 484 }); 485 }; 486 487 megaMenuBreakpoint.addEventListener('change', onMediaChange); 488 onMediaChange(megaMenuBreakpoint); 489 490 // If this is expected to have more, then we need to add a subscription on reposition by adding to the `navAMenuList` 491 if (navA.dataset.moreButton === '1') { 492 /** 493 * @type {NodeListOf<HTMLElement>} 494 */ 495 const navAWithMoreList = navA.querySelectorAll('.navigation__list.more_nav:not(.mm-listview)'); 496 for (const el of navAWithMoreList) { 497 // Hook in the Behavior for the repositioning 498 NavigationAMoreList.create(el); 499 } 500 } 501 502 // Drop down adjustments 503 /* Add sub item class if too close to edges */ 504 /** 505 * @param {IJQueryEvent<MouseEvent>} event 506 */ 507 const onHover = (event) => { 508 const el = event.currentTarget; 509 const $this = $(el); 510 const $navListChildItem = $this.children('.navigation__list--sub'); 511 const $navListChildSubItem = $navListChildItem.find('> .navigation__item.has-drop-down > .navigation__list--sub'); 512 const $navListGrandChildSubItem = $navListChildSubItem.find('> .navigation__item.has-drop-down > .navigation__list--sub'); 513 const widthChildItem = $navListChildItem.outerWidth(true); 514 const navListSub_offsetLeft = $this.offset().left; 515 const buffer = 60; 516 517 const maxWidthChildSubItem = $navListChildSubItem.toArray().reduce(largestWidth, 0); 518 const maxWidthGrandChildSubItem = $navListGrandChildSubItem.toArray().reduce(largestWidth, 0); 519 520 const navListSub_width = (widthChildItem + maxWidthChildSubItem + maxWidthGrandChildSubItem) - $this.outerWidth(true); 521 522 const offsetRight = $(document).width() - (navListSub_offsetLeft + $this.outerWidth(true) + buffer); 523 const isEntirelyVisible = (offsetRight >
523 navListSub_width); 524 525 el.classList.toggle('near-edge', !isEntirelyVisible); 526 }; 527 528 // feature/SMBWMGR-14028: .navigation-a > .navigation__list > .navigation__item.has-drop-down 529 navA$ref.children('.navigation__list').on( 530 'mouseenter mouseleave' 531 , '> .navigation__item.has-drop-down' 532 , onHover 533 ); 534 } 535} 536 537// TODO: Manage State 538class NavigationOther { 539 static initializeAll() { 540 const navigationOtherList = document.querySelectorAll( 541 '.component.navigation .navigation-b, .component.navigation .navigation-c' 542 ); 543 for (const el of navigationOtherList) { 544 this.initialize(el); 545 } 546 } 547 548 static initialize(nav) { 549 const navButton = $(nav).children('.navigation__button'); 550 const navMenu = $(nav).children('.navigation__menu--container'); 551 const expandButton = $(nav).find('.navigation__item--expand'); 552 553 navButton.click(function() { 554 if (!(navButton.hasClass('animating'))) { 555 navButton.addClass('animating'); 556 navButton.children('.navigation__button--icon').toggleClass('is-active'); 557 558 if (!(navMenu.hasClass('animating'))) { 559 navMenu.addClass('animating'); 560 navMenu.toggleClass('expanded'); 561 setTimeout(function(){ 562 navMenu.removeClass('animating'); 563 navButton.removeClass('animating'); 564 }, 500); 565 } 566 } 567 568 }); 569 570 $(document).click(function(e) { 571 if ((!navMenu.is(e.target) && navMenu.has(e.target).length === 0) && (!navButton.is(e.target) && navButton.has(e.target).length === 0)) { 572 navButton.children('.navigation__button--icon').removeClass('is-active'); 573 if (!(navMenu.hasClass('animating'))) { 574 navMenu.addClass('animating'); 575 navMenu.removeClass('expanded'); 576 setTimeout(function(){ 577 navMenu.removeClass('animating'); 578 navButton.removeClass('animating'); 579 }, 500); 580 } 581 } 582 }); 583 584 expandButton.click(function() { 585 $(this).toggleClass('expanded').next().slideToggle(); 586 }); 587 } 588} 589 590$(() => { 591 //sticky nav 592 $('[data-sticky-header]').closest('.js-sticky-nav').sticky({topSpacing: 0}); 593 594 595 NavigationOther.initializeAll(); 596 NavigationAMoreList.unmask(); 597 NavigationA.initializeAll(); 598 599 addMMenuTitleAttributes(); 600 601 positionNavs(); 602 603 window.addEventListener('resize', () => { 604 schedulePositionNavs(); 605 }); 606}); 607})(window.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.