1class SiteFooterLists { 2 constructor(root = document) { 3 this.root = root; 4 this.selector = '.SiteFooterList__trigger'; 5 this.mq = window.matchMedia('(min-width: 768px)'); 6 this.handleClick = this.handleClick.bind(this); 7 this.handleKeydown = this.handleKeydown.bind(this); 8 this.handleBreakpointChange = this.handleBreakpointChange.bind(this); 9 this.init(); 10 } 11 12 init() { 13 this.root.addEventListener('click', this.handleClick); 14 this.root.addEventListener('keydown', this.handleKeydown); 15 this.mq.addEventListener('change', this.handleBreakpointChange); 16 17 this.applyState(); 18 } 19 20 isDesktop() { 21 return this.mq.matches; 22 } 23 24 handleBreakpointChange() { 25 this.applyState(); 26 } 27 28 applyState() { 29 const triggers = this.root.querySelectorAll(this.selector); 30 31 triggers.forEach(trigger => { 32 const panelId = trigger.getAttribute('aria-controls'); 33 if (!panelId) return; 34 35 const panel = document.getElementById(panelId); 36 if (!panel) return; 37 38 const icon = trigger.querySelector('.SiteFooterList__Icon i'); 39 40 if (this.isDesktop()) { 41 // ALWAYS override DOM defaults 42 trigger.setAttribute('aria-expanded', 'true'); 43 trigger.setAttribute('aria-disabled', 'true'); 44 45 panel.removeAttribute('hidden'); 46 47 if (icon) { 48 icon.classList.remove('icon-circle-chevron-down'); 49 icon.classList.add('icon-circle-chevron-up'); 50 51 const use = icon.querySelector('use'); 52 if (use) { 53 use.setAttribute('xlink:href', '#icon-circle-chevron-up'); 54 } 55 } 56 } else { 57 trigger.setAttribute('aria-expanded', 'false'); 58 trigger.removeAttribute('aria-disabled'); 59 60 panel.setAttribute('hidden', ''); 61 62 if (icon) { 63 icon.classList.remove('icon-circle-chevron-up'); 64 icon.classList.add('icon-circle-chevron-down'); 65 66 const use = icon.querySelector('use'); 67 if (use) { 68 use.setAttribute('xlink:href', '#icon-circle-chevron-down'); 69 } 70 } 71 } 72 }); 73 } 74 75 handleClick(e) { 76 if (this.isDesktop()) return; 77 78 const trigger = e.target.closest(this.selector); 79 if (!trigger) return; 80 81 this.toggle(trigger); 82 } 83 84 handleKeydown(e) { 85 if (this.isDesktop()) return; 86 if (e.key !== 'Enter' && e.key !== ' ') return; 87 88 const trigger = e.target.closest(this.selector); 89 if (!trigger) return; 90 91 e.preventDefault(); 92 this.toggle(trigger); 93 } 94 95 toggle(trigger) { 96 const panelId = trigger.getAttribute('aria-controls'); 97 if (!panelId) return; 98 99 const panel = document.getElementById(panelId); 100 if (!panel) return; 101 102 const icon = trigger.querySelector('.SiteFooterList__Icon i'); 103 104 const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; 105 const nextState = !isExpanded; 106 107 trigger.setAttribute('aria-expanded', String(nextState)); 108 109 if (nextState) { 110 panel.hidden = false; 111 112 if (icon) { 113 icon.classList.remove('icon-circle-chevron-down'); 114 icon.classList.add('icon-circle-chevron-up'); 115 116 const use = icon.querySelector('use'); 117 if (use) { 118 use.setAttribute('xlink:href', '#icon-circle-chevron-up'); 119 } 120 } 121 } else { 122 panel.hidden = true; 123 124 if (icon) { 125 icon.classList.remove('icon-circle-chevron-up'); 126 icon.classList.add('icon-circle-chevron-down'); 127 128 const use = icon.querySelector('use'); 129 if (use) { 130 use.setAttribute('xlink:href', '#icon-circle-chevron-down'); 131 } 132 } 133 } 134 } 135} 136 137new SiteFooterLists(); 138 139class ResolveFooterOverlaps { 140 constructor(options = {}) { 141 this.footerId = options.footerId || 'SiteFooter'; 142 this.targets = options.targets || []; 143 144 this.footerEl = null; 145 this.items = []; 146 this.ticking = false;
147 148 this.onScroll = this.onScroll.bind(this); 149 this.onResize = this.onResize.bind(this); 150 } 151 152 init() { 153 this.footerEl = document.getElementById(this.footerId); 154 if (!this.footerEl) return; 155 156 this.collectTargets(); 157 if (this.items.length === 0) return; 158 159 this.injectStyles(); 160 this.bindEvents(); 161 this.update(); 162 } 163 164 collectTargets() { 165 this.items = this.targets.reduce((acc, t, i) => { 166 if (!t || !t.selector) return acc; 167 168 const el = document.querySelector(t.selector); 169 if (!el) return acc; 170 171 const id = `rfo-${i}`; 172 el.dataset.rfoId = id; 173 174 acc.push({ 175 el, 176 mode: t.mode === 'offset' ? 'offset' : 'hide', 177 offset: Number.isFinite(t.offset) ? t.offset : 16, 178 className: `rfo-hidden-${id}` 179 }); 180 181 return acc; 182 }, []); 183 } 184 185 injectStyles() { 186 const styleId = 'resolve-footer-overlaps'; 187 if (document.getElementById(styleId)) return; 188 189 const css = this.items 190 .map(item => `[data-rfo-id="${item.el.dataset.rfoId}"].${item.className}{display:none!important;}`) 191 .join(''); 192 193 if (!css) return; 194 195 const style = document.createElement('style'); 196 style.id = styleId; 197 style.textContent = css; 198 document.head.appendChild(style); 199 } 200 201 bindEvents() { 202 window.addEventListener('scroll', this.onScroll, { passive: true }); 203 window.addEventListener('resize', this.onResize); 204 } 205 206 onScroll() { 207 this.requestTick(); 208 } 209 210 onResize() { 211 this.requestTick(); 212 } 213 214 requestTick() { 215 if (this.ticking) return; 216 217 this.ticking = true; 218 requestAnimationFrame(() => { 219 this.update(); 220 this.ticking = false; 221 }); 222 } 223 224 update() { 225 if (!this.footerEl || this.items.length === 0) return; 226 227 const footerRect = this.footerEl.getBoundingClientRect(); 228 if (!footerRect) return; 229 230 const viewportHeight = window.innerHeight; 231 const isOverlapping = footerRect.top < viewportHeight; 232 233 this.items.forEach(item => { 234 if (!item || !item.el) return; 235 236 if (item.mode === 'hide') { 237 item.el.classList.toggle(item.className, isOverlapping); 238 return; 239 } 240 241 if (item.mode === 'offset') { 242 const bottom = isOverlapping 243 ? (viewportHeight - footerRect.top) + item.offset 244 : item.offset; 245 246 item.el.style.setProperty('bottom', bottom + 'px', 'important'); 247 } 248 }); 249 } 250} 251 252(function () { 253 const init = () => { 254 const instance = new ResolveFooterOverlaps({ 255 targets: [ 256 { selector: '.jwPlayer--floatingContainer', mode: 'hide' }, 257 { selector: '.social-share-sticky-menu', mode: 'hide' } 258 ] 259 }); 260 261 if (!instance.targets.length) return; 262 instance.init(); 263 }; 264 265 if (document.readyState === 'loading') { 266 document.addEventListener('DOMContentLoaded', init, { once: true }); 267 } else { 268 init(); 269 } 270})();; 271/* eslint-disable no-undef */ 272class GetManager { 273 static PUBLISHER_ALIASES = new Map([ 274 ['arnnet', ['arn']], 275 ['channelasia', ['ca']], 276 ['reseller', ['rsl']], 277 ]); 278 279 static VALID_PUBLISHERS = new Set([ 280 'nww', 281 'iw', 282 'cw', 283 'arnnet', 284 'reseller', 285 'cso', 286 'cio', 287 'channelasia', 288 ]); 289 290 static EDITION_ALIASES = new Map([ 291 ['uk', ['gb']], 292 ['middle-east', ['me']], 293 ['asean', ['as']], 294 ['africa', ['af']], 295 ]); 296 297 static VALID_EDITIONS = new Set([ 298 'africa', 299 'asean', 300 'au', 301 'ca', 302 'es', 303 'ie', 304 'in', 305 'it', 306 'jp', 307 'kr', 308 'middle-east', 309 'nl', 310 'nz', 311 'uk', 312 'us', 313 'de', 314 'fr', 315 'pl', 316 'se', 317 ]); 318 319 constructor() { 320 window.foundry_is_language = GetManager.foundry_is_language; 321 window.foundry_is_publisher = GetManager.foundry_is_publisher; 322 window.foundry_is_edition = GetManager.foundry_is_edition; 323 window.foundry_get_publisher = GetManager.foundry_get_publisher; 324 window.foundry_get_site = GetManager.foundry_get_site; 325 } 326 327 static getCanonicalPublisher(name) { 328 if (!name) return null; 329 const lowerName = name.toLowerCase(); 330 331 const found = Array.from(GetManager.PUBLISHER_ALIASES.entries()).find( 332 ([canonical, aliases]) =>
332 canonical === lowerName || aliases.includes(lowerName), 333 ); 334 if (found) return found[0]; 335 336 return GetManager.VALID_PUBLISHERS.has(lowerName) ? lowerName : null; 337 } 338 339 static getDocumentLanguage() { 340 const langAttr = document.documentElement.getAttribute('lang') || ''; 341 return langAttr.split('-')[0].toLowerCase(); // Extracts the language without the region (e.g., "en" from "en-US") 342 } 343 344 static getDocumentEdition() { 345 const editionAttr = document.documentElement.getAttribute('data-edition') || ''; 346 return editionAttr.toLowerCase(); 347 } 348 349 static getCanonicalEdition(name) { 350 if (!name) return null; 351 const lowerName = name.toLowerCase(); 352 353 const found = Array.from(GetManager.EDITION_ALIASES.entries()).find( 354 ([canonical, aliases]) => canonical === lowerName || aliases.includes(lowerName), 355 ); 356 if (found) return found[0]; 357 358 return GetManager.VALID_EDITIONS.has(lowerName) ? lowerName : null; 359 } 360 361 static foundry_is_publisher(publisherNames) { 362 const brandAttr = document.documentElement.getAttribute('data-brand') || ''; 363 if (!brandAttr) return false; 364 365 const canonicalBrand = brandAttr.toLowerCase(); 366 367 if (Array.isArray(publisherNames)) { 368 return publisherNames.some(name => GetManager.getCanonicalPublisher(name) === canonicalBrand); 369 } 370 371 return GetManager.getCanonicalPublisher(publisherNames) === canonicalBrand; 372 } 373 374 static foundry_is_language(languages) { 375 const currentLang = GetManager.getDocumentLanguage(); 376 if (!currentLang) return false; 377 378 const languageList = Array.isArray(languages) 379 ? languages.map(lang => lang.toLowerCase()) 380 : languages.split(',').map(lang => lang.trim().toLowerCase()); 381 382 return languageList.includes(currentLang); 383 } 384 385 static foundry_is_edition(editionNames) { 386 const currentEdition = GetManager.getDocumentEdition(); 387 if (!currentEdition) return false; 388 389 if (Array.isArray(editionNames)) { 390 return editionNames.some(name => GetManager.getCanonicalEdition(name) === currentEdition); 391 } 392 393 return GetManager.getCanonicalEdition(editionNames) === currentEdition; 394 } 395 396 static foundry_get_publisher() { 397 const brandAttr = document.documentElement.getAttribute('data-brand') || ''; 398 if (!brandAttr) return null; 399 400 return brandAttr.trim().toLowerCase(); 401 } 402 403 static foundry_get_site() { 404 return (window.siteData && window.siteData.site) ? window.siteData.site : ''; 405 } 406 407} 408 409new GetManager(); // eslint-disable-line no-new; 410function ToggleContentVisibilityParagraph() { 411 const elements = document.querySelectorAll('[data-readmore-length]'); 412 413 if (elements.length === 0) return; 414 415 elements.forEach((element, index) => { 416 const uniqueId = `readmore-${index}`; 417 const modifiedElement = element; 418 modifiedElement.id = uniqueId; 419 420 const readMoreText = element.getAttribute('data-readmore-txt') || 'Read more'; 421 const hasReadLess = element.hasAttribute('data-readless'); 422 const readLessText = hasReadLess 423 ? element.getAttribute('data-readless-txt') || readMoreText 424 : ''; 425 426 const readMoreButton = document.createElement('button'); 427 readMoreButton.textContent = readMoreText; 428 readMoreButton.classList.add('read-more-toggle', 'reset-button'); 429 readMoreButton.setAttribute('aria-controls', uniqueId); 430 readMoreButton.setAttribute('aria-expanded', 'false'); 431 432 element.insertAdjacentElement('afterend', readMoreButton); 433 434 readMoreButton.addEventListener('click', function () { 435 const isExpanded = readMoreButton.getAttribute('aria-expanded') === 'true'; 436 readMoreButton.setAttribute('aria-expanded', !isExpanded); 437 element.setAttribute('data-expanded', !isExpanded); 438 439 if (!hasReadLess) { 440 readMoreButton.remove(); 441 } 442 443 if (isExpanded) { 444 readMoreButton.textContent = readMoreText; 445 } else { 446 readMoreButton.textContent = readLessText; 447 } 448 }); 449 }); 450} 451 452document.addEventListener('DOMContentLoaded', ToggleContentVisibilityParagraph); 453; 454 455class PrimaryNavDropdown { 456 static init(selector = ".PrimaryNav") { 457 document.querySelectorAll(selector).forEach((root) => { 458 if (root._primaryNavDropdown) return; 459 460 if (!root.querySelector('[data-nav-item="parent"]')) return; 461 462 const list = root.querySelector(".PrimaryNav__list"); 463 if (!list || !list.getClientRects().length) return; 464 465 root._primaryNavDropdown = new PrimaryNavDropdown(root); 466 }); 467 } 468 469 constructor(root) { 470 this.root = root; 471 this.header = root.closest("#SiteHeader") || root; 472 this.fadeBound = new WeakSet(); 473 474 this.items = Array.from( 475 root.querySelectorAll('[data-nav-item="parent"]') 476 ) 477 .map((item) => this._hydrateItem(item)) 478 .filter(Boolean); 479 480 this.activeItem = null; 481 482 this._buildHorizontalRail(); 483 this._bindItems(); 484 this._bindGlobal(); 485 } 486 487 _buildHorizontalRail() { 488 489 const elements = this.header.querySelectorAll( 490 'a[href], button:not([disabled])' 491 ); 492 493 this.horizontal = Array.from(elements); 494 495 this.horizontalIndex = new Map( 496 this.horizontal.map((el, i) => [el, i]) 497 ); 498 499 this.horizontalSet = new Set(this.horizontal); 500 } 501 502 _hydrateItem(item) { 503 504 const toggle = item.querySelector("[data-nav-toggle]"); 505 const link = item.querySelector("[data-nav-link]"); 506 if (!toggle) return null; 507 508 const key = toggle.dataset.navToggle; 509 510 const dropdown = item.querySelector( 511 `[data-nav-dropdown="${key}"]` 512 ); 513 if (!dropdown) return null; 514 515 const icon = toggle.querySelector("i"); 516 const use = icon?.querySelector("use"); 517 518 const submenuLinks = Array.from( 519 dropdown.querySelectorAll(".PrimaryNav__sublink") 520 ); 521 522 submenuLinks.forEach(a => a.tabIndex = -1); 523 524 return { 525 item, 526 toggle, 527 link, 528 dropdown, 529 icon, 530 use, 531 submenuLinks 532 }; 533 } 534 535 _bindItems() {
536 this.items.forEach((entry) => { 537 const { item, toggle, link, submenuLinks } = entry; 538 539 const open = () => this._open(entry); 540 const close = () => this._close(entry); 541 542 toggle.addEventListener("click", (e) => { 543 e.preventDefault(); 544 this.activeItem === entry ? close() : open(); 545 }); 546 547 [toggle, link].forEach((el) => { 548 if (!el) return; 549 550 el.addEventListener("keydown", (e) => { 551 switch (e.key) { 552 case " ": 553 e.preventDefault(); 554 this.activeItem === entry ? close() : open(); 555 break; 556 557 case "ArrowDown": 558 e.preventDefault(); 559 open(); 560 submenuLinks[0]?.focus(); 561 break; 562 } 563 }); 564 565 el.addEventListener("pointerenter", open); 566 }); 567 568 item.addEventListener("pointerleave", close); 569 570 item.addEventListener("focusout", (e) => { 571 if (!item.contains(e.relatedTarget)) close(); 572 }); 573 574 submenuLinks.forEach((subLink, index) => { 575 subLink.addEventListener("keydown", (e) => { 576 switch (e.key) { 577 case "ArrowDown": 578 e.preventDefault(); 579 submenuLinks[index + 1]?.focus(); 580 break; 581 582 case "ArrowUp": 583 e.preventDefault(); 584 index === 0 585 ? toggle.focus() 586 : submenuLinks[index - 1]?.focus(); 587 break; 588 } 589 }); 590 }); 591 }); 592 } 593 594 _bindGlobal() { 595 596 this.header.addEventListener("keydown", (e) => { 597 598 const target = e.target; 599 600 if (!this.horizontalSet.has(target)) return; 601 602 if (e.key === "ArrowRight") { 603 e.preventDefault(); 604 this._focusHorizontal(target, 1); 605 } 606 607 if (e.key === "ArrowLeft") { 608 e.preventDefault(); 609 this._focusHorizontal(target, -1); 610 } 611 612 if (e.key === "Escape" && this.activeItem) { 613 const { toggle } = this.activeItem; 614 this._close(this.activeItem); 615 toggle.focus(); 616 } 617 }); 618 619 document.addEventListener("pointerdown", (e) => { 620 621 if (this.activeItem && !this.header.contains(e.target)) { 622 this._close(this.activeItem); 623 } 624 }); 625 } 626 627 _bindScrollFade(entry) { 628 const { dropdown, submenuLinks } = entry; 629 630 if (submenuLinks.length <= 5) { 631 dropdown.classList.remove("has-fade"); 632 return; 633 } 634 635 const update = () => { 636 637 const progress = dropdown.scrollTop / (dropdown.scrollHeight - dropdown.clientHeight); 638 const passedThreshold = progress >= 0.6; 639 640 dropdown.classList.toggle("has-fade", !passedThreshold); 641 }; 642 643 if (!this.fadeBound.has(dropdown)) { 644 dropdown.addEventListener("scroll", update); 645 this.fadeBound.add(dropdown); 646 } 647 648 update(); 649 } 650 651 _focusHorizontal(current, delta) { 652 const i = this.horizontalIndex.get(current); 653 if (i === undefined) return; 654 655 let nextIndex = i + delta; 656 657 if (nextIndex < 0) nextIndex = this.horizontal.length - 1; 658 if (nextIndex >= this.horizontal.length) nextIndex = 0; 659 660 this.horizontal[nextIndex]?.focus(); 661 } 662 663 _open(entry) { 664 665 666 const html = document.documentElement; 667 // if html has data-lightbox-open="topics", return 668 if (html.dataset.lightboxOpen === "topics") { 669 return; 670 } 671 672 if (html.dataset.sitenavState === "open" && html.dataset.lightboxOpen !== "topics") { 673 return; 674 } 675 676 if (this.activeItem && this.activeItem !== entry) { 677 this._close(this.activeItem); 678 } 679 680 const { toggle, dropdown, submenuLinks, icon, use } = entry; 681 682 toggle.setAttribute("aria-expanded", "true"); 683 dropdown.setAttribute("aria-hidden", "false"); 684 toggle.classList.add("is-open"); 685 686 requestAnimationFrame(() => { 687 dropdown.scrollTop = 0; 688 }); 689 690 if (icon) { 691 icon.classList.remove("icon-header-angle-down"); 692 icon.classList.add("icon-header-angle-up"); 693 } 694 695 if (use) { 696 use.setAttribute("xlink:href", "#icon-header-angle-up"); 697 } 698 699 submenuLinks.forEach(a => a.tabIndex = 0); 700 701 this._bindScrollFade(entry); 702 703 this.activeItem = entry; 704 } 705 706 _close(entry) { 707 708 const { toggle, dropdown, submenuLinks, icon, use } = entry; 709 710 toggle.setAttribute("aria-expanded", "false"); 711 dropdown.setAttribute("aria-hidden", "true"); 712 toggle.classList.remove("is-open"); 713 714 dropdown.scrollTop = 0; 715 716 if (icon) { 717 icon.classList.remove("icon-header-angle-up"); 718 icon.classList.add("icon-header-angle-down"); 719 } 720 721 if (use) { 722 use.setAttribute("xlink:href", "#icon-header-angle-down"); 723 } 724 725 submenuLinks.forEach(a => a.tabIndex = -1); 726 727 if (this.activeItem === entry) { 728 this.activeItem = null; 729 } 730 } 731} 732 733class PrimaryNavPersistent { 734 735 constructor({ 736 primarySelector = '#SiteHeader', 737 persistentSelector = '#SiteHeaderPersistent',
738 offset = 300, 739 hideDelta = 30, 740 recalcLimit = 500 741 } = {}) { 742 743 this.primary = document.querySelector(primarySelector); 744 if (!this.primary) return; 745 746 this.persistent = this.resolvePersistent(persistentSelector); 747 if (!this.persistent) return; 748 749 this.offset = offset; 750 this.hideDelta = hideDelta; 751 this.recalcLimit = recalcLimit; 752 753 this.lastScrollY = window.scrollY || 0; 754 this.threshold = 0; 755 this.thresholdLocked = false; 756 757 this.ticking = false; 758 this.resizeTicking = false; 759 760 this.visible = false; 761 this.hideStartY = null; 762 763 this.init(); 764 this.bind(); 765 } 766 767 resolvePersistent(selector) { 768 769 let existing = document.querySelector(selector); 770 if (existing) return existing; 771 772 const clone = this.primary.cloneNode(true); 773 774 clone.id = selector.replace('#', ''); 775 clone.classList.add('is-persistent'); 776 777 const list = clone.querySelector('.PrimaryNav__list'); 778 779 if (list) { 780 781 // remove all children 782 list.innerHTML = ''; 783 784 // look for subscribe button inside TrendingBar 785 const trending = document.querySelector('.TrendingBar'); 786 const subscribe = trending 787 ? trending.querySelector('.SubscribeLinkBtn') 788 : null; 789 790 if (subscribe) { 791 792 const li = document.createElement('li'); 793 li.className = 'PrimaryNav__item PrimaryNav__item--subscribe'; 794 795 li.appendChild(subscribe.cloneNode(true)); 796 797 list.appendChild(li); 798 } 799 } 800 801 if (!clone.parentNode) { 802 this.primary.insertAdjacentElement('afterend', clone); 803 } 804 805 return clone; 806 } 807 808 isStickyAdActive() { 809 810 const ad = document.querySelector('.advert-sticky'); 811 if (!ad) return false; 812 813 const rect = ad.getBoundingClientRect(); 814 815 // ad is occupying top of viewport 816 return rect.top <= 0 && rect.bottom > 0; 817 } 818 819 calculateThreshold() { 820 821 this.threshold = 822 this.primary.offsetTop + 823 this.primary.offsetHeight + 824 this.offset; 825 } 826 827 init() { 828 829 if (!this.primary || !this.persistent) return; 830 831 this.calculateThreshold(); 832 833 this.thresholdLocked = false; 834 835 this.persistent.classList.remove('is-visible'); 836 this.visible = false; 837 } 838 839 bind() { 840 841 window.addEventListener('scroll', () => this.onScroll(), { passive: true }); 842 window.addEventListener('resize', () => this.onResize()); 843 } 844 845 onResize() { 846 847 if (this.resizeTicking) return; 848 849 this.resizeTicking = true; 850 851 requestAnimationFrame(() => { 852 this.init(); 853 this.resizeTicking = false; 854 }); 855 } 856 857 onScroll() { 858 859 if (this.ticking) return; 860 861 this.ticking = true; 862 863 requestAnimationFrame(() => { 864 this.update(); 865 this.ticking = false; 866 }); 867 } 868 869 update() { 870 871 const currentY = Math.max(0, window.scrollY || window.pageYOffset); 872 873 if (this.isStickyAdActive()) { 874 if (this.visible) this.hide(); 875 } 876 877 if (!this.thresholdLocked) { 878 879 this.calculateThreshold(); 880 881 if (currentY > this.recalcLimit) { 882 this.thresholdLocked = true; 883 } 884 } 885 886 const delta = currentY - this.lastScrollY; 887 888 const isScrollingUp = delta < -1; 889 const isScrollingDown = delta > 1; 890 891 if (currentY <= this.threshold) { 892 893 if (this.visible) this.hide(); 894 895 this.lastScrollY = currentY <= 0 ? 0 : currentY; 896 return; 897 } 898 899 if (!this.visible) { 900 901 if (isScrollingUp) { 902 this.show(); 903 } 904 905 } else { 906 907 if (isScrollingDown) { 908 909 if (this.hideStartY === null) { 910 this.hideStartY = currentY; 911 } 912 913 if (currentY - this.hideStartY >= this.hideDelta) { 914 this.hide(); 915 } 916 917 } else { 918 this.hideStartY = null; 919 } 920 } 921 922 this.lastScrollY = currentY <= 0 ? 0 : currentY; 923 } 924 925 show() { 926 927 if (!this.persistent) return; 928 929 // block if sticky ad is active 930 if (this.isStickyAdActive()) return; 931 932 this.persistent.style.top = '0px'; 933 this.persistent.classList.add('is-visible'); 934 935 this.visible = true; 936 this.hideStartY = null; 937 } 938 939 hide() { 940 941 if (!this.persistent) return; 942 943 this.persistent.classList.remove('is-visible'); 944 945 this.visible = false; 946 this.hideStartY = null; 947 948 this.persistent.style.top = '0px'; 949 } 950} 951 952class MenuAsideController { 953 954 constructor() { 955 956 this.html = document.documentElement; 957 958 this.aside = document.getElementById('MenuAside'); 959 this.overlay = document.getElementById('MenuOverlay'); 960 961 if (!this.aside || !this.overlay) return; 962 963 this.closeButtons = document.querySelectorAll('.MenuAside__close'); 964 this.openButtons = document.querySelectorAll('[data-menu-open]'); 965 this.sections = [...this.aside.querySelectorAll('[data-menu-section]')]; 966 967 this.focusable = []; 968 this.activeTrigger = null; 969 970 this.init(); 971 this.bind(); 972 } 973
974 init() { 975 976 this.html.dataset.sitenavState = 'closed'; 977 978 this.aside.setAttribute('aria-hidden', 'true'); 979 this.overlay.setAttribute('aria-hidden', 'true'); 980 981 this.hideSections(); 982 983 this.sections.forEach(section => { 984 section.setAttribute('inert', ''); 985 this.setTabIndex(section, -1); 986 }); 987 } 988 989 setTabIndex(section, value) { 990 991 const elements = section.querySelectorAll( 992 'a[href], button:not(.MenuGroup__notoggle), textarea, input, select, [tabindex]' 993 ); 994 995 elements.forEach(el => { 996 el.setAttribute('tabindex', value); 997 }); 998 } 999 1000 bind() { 1001 1002 this.openButtons.forEach(btn => { 1003 btn.addEventListener('click', () => { 1004 this.toggle(btn); 1005 }); 1006 }); 1007 1008 this.closeButtons.forEach(btn => { 1009 btn.addEventListener('click', () => this.close()); 1010 }); 1011 1012 this.overlay.addEventListener('click', () => this.close()); 1013 1014 document.addEventListener('keydown', e => { 1015 1016 if (e.key === 'Escape' && this.html.dataset.sitenavState === 'open') { 1017 this.close(); 1018 } 1019 1020 if (e.key === 'Tab' && this.html.dataset.sitenavState === 'open') { 1021 this.trapFocus(e); 1022 } 1023 }); 1024 1025 /* HARD FOCUS LOCK */ 1026 document.addEventListener('focusin', e => { 1027 1028 if (this.html.dataset.sitenavState !== 'open') return; 1029 1030 if (!this.aside.contains(e.target)) { 1031 const first = this.focusable[0] || this.aside; 1032 first.focus({ preventScroll: true }); 1033 } 1034 }); 1035 } 1036 toggle(trigger) { 1037 1038 const name = trigger.dataset.menuOpen; 1039 1040 const target = this.sections.find( 1041 section => section.dataset.menuSection === name 1042 ); 1043 1044 if (!target) return; 1045 1046 const isOpen = this.html.dataset.sitenavState === 'open'; 1047 const current = this.aside.dataset.sectionCurrent; 1048 1049 if (!isOpen) { 1050 this.open(trigger, target, name); 1051 return; 1052 } 1053 1054 if (current === name) { 1055 this.close(); 1056 return; 1057 } 1058 1059 this.open(trigger, target, name); 1060 } 1061 1062 open(trigger, target, name) { 1063 1064 document.dispatchEvent(new CustomEvent('sitenav:aside-open')); 1065 1066 this.activeTrigger = trigger; 1067 1068 const previous = this.aside.dataset.sectionCurrent || 'default'; 1069 1070 if (name === 'default') { 1071 this.aside.dataset.backBtn = 'false'; 1072 1073 } else if (name === 'topics') { 1074 this.aside.dataset.backBtn = 'true'; 1075 1076 } else if ( 1077 trigger.classList.contains('MenuAside__trigger') && 1078 previous === 'default' 1079 ) { 1080 this.aside.dataset.backBtn = 'true'; 1081 } 1082 1083 if (trigger.classList.contains('HeaderActions__trigger')) { 1084 this.aside.dataset.backBtn = 'false'; 1085 } 1086 1087 this.hideSections(); 1088 1089 this.sections.forEach(section => { 1090 section.setAttribute('inert', ''); 1091 this.setTabIndex(section, -1); 1092 }); 1093 1094 target.removeAttribute('inert'); 1095 target.setAttribute('aria-hidden', 'false'); 1096 this.setTabIndex(target, 0); 1097 1098 target.querySelectorAll('.MenuGroup__notoggle') 1099 .forEach(el => el.setAttribute('tabindex', '-1')); 1100 1101 target.classList.remove('is-visible'); 1102 requestAnimationFrame(() => { 1103 target.classList.add('is-visible'); 1104 }); 1105 1106 this.aside.classList.add('is-open'); 1107 this.overlay.classList.add('is-open'); 1108 1109 this.aside.setAttribute('aria-hidden', 'false'); 1110 this.overlay.setAttribute('aria-hidden', 'false'); 1111 1112 this.html.dataset.sitenavState = 'open'; 1113 this.html.dataset.sitenavSection = name; 1114 this.aside.dataset.sectionCurrent = name; 1115 1116 document.getElementById('SiteHeader')?.setAttribute('inert', ''); 1117 1118 this.syncTriggers(true, name); 1119 1120 if (!target.hasAttribute('tabindex')) { 1121 target.setAttribute('tabindex', '-1'); 1122 } 1123 1124 this.buildFocusable(); 1125 1126 target.focus({ preventScroll: true }); 1127 } 1128 1129 close() { 1130 1131 this.hideSections(); 1132 1133 this.sections.forEach(section => { 1134 section.setAttribute('inert', ''); 1135 this.setTabIndex(section, -1); 1136 }); 1137 1138 this.aside.classList.remove('is-open'); 1139 this.aside.dataset.backBtn = 'false'; 1140 1141 this.overlay.classList.remove('is-open'); 1142 1143 this.aside.setAttribute('aria-hidden', 'true'); 1144 this.overlay.setAttribute('aria-hidden', 'true'); 1145 1146 this.html.dataset.sitenavState = 'closed'; 1147 delete this.html.dataset.sitenavSection; 1148 delete this.aside.dataset.sectionCurrent; 1149 1150 document.getElementById('SiteHeader')?.removeAttribute('inert'); 1151 1152 this.syncTriggers(false); 1153 1154 this.focusable = []; 1155 1156 this.activeTrigger = null; 1157 } 1158 1159 hideSections() { 1160 1161 this.sections.forEach(section => { 1162 section.setAttribute('aria-hidden', 'true'); 1163 section.classList.remove('is-visible'); 1164 }); 1165 } 1166 1167 buildFocusable() { 1168 1169 this.focusable = Array.from( 1170 this.aside.querySelectorAll( 1171 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not
1171([tabindex="-1"])' 1172 ) 1173 ).filter(el => el.getClientRects().length); 1174 } 1175 1176 trapFocus(e) { 1177 1178 const focusable = this.focusable; 1179 if (!focusable.length) return; 1180 1181 const first = focusable[0]; 1182 const last = focusable[focusable.length - 1]; 1183 const active = document.activeElement; 1184 1185 if (e.shiftKey && active === first) { 1186 e.preventDefault(); 1187 last.focus(); 1188 } 1189 1190 if (!e.shiftKey && active === last) { 1191 e.preventDefault(); 1192 first.focus(); 1193 } 1194 } 1195 1196 syncTriggers(expanded, active = null) { 1197 1198 this.openButtons.forEach(btn => { 1199 1200 if (!expanded) { 1201 btn.setAttribute('aria-expanded', 'false'); 1202 return; 1203 } 1204 1205 btn.setAttribute( 1206 'aria-expanded', 1207 btn.dataset.menuOpen === active ? 'true' : 'false' 1208 ); 1209 }); 1210 } 1211} 1212 1213class MenuGroupController { 1214 1215 constructor(root) { 1216 1217 this.root = root; 1218 this.nav = root.querySelector('.MenuAside__nav'); 1219 1220 this.groups = [...root.querySelectorAll('.MenuGroup')]; 1221 this.toggles = [...root.querySelectorAll('.MenuGroup__toggle')]; 1222 this.links = [...root.querySelectorAll('.MenuGroup__link')]; 1223 1224 this.topLevelItems = this.nav 1225 ? [...this.nav.querySelectorAll(':scope > .MenuGroup > a, :scope > .MenuGroup > button, :scope > button')] 1226 : []; 1227 1228 1229 this.panels = new Map(); 1230 this.icons = new Map(); 1231 1232 this.toggles.forEach(toggle => { 1233 1234 const panelId = toggle.getAttribute('aria-controls'); 1235 const panel = document.getElementById(panelId); 1236 1237 if (panel) { 1238 this.panels.set(toggle, panel); 1239 1240 if (!panel.hasAttribute('tabindex')) { 1241 panel.setAttribute('tabindex', '-1'); 1242 } 1243 } 1244 1245 const icon = toggle.querySelector('.MenuGroup__icon i'); 1246 const use = icon?.querySelector('use'); 1247 1248 this.icons.set(toggle, { icon, use }); 1249 }); 1250 1251 this.init(); 1252 this.bind(); 1253 } 1254
1255 init() { 1256 1257 const isEditions = 1258 this.root.dataset.menuSection === 'editions'; 1259 1260 this.toggles.forEach(toggle => { 1261 1262 const group = toggle.closest('.MenuGroup'); 1263 const panel = this.panels.get(toggle); 1264 if (!group || !panel) return; 1265 1266 const isActive = 1267 isEditions || group.classList.contains('is-active'); 1268 1269 toggle.setAttribute('aria-expanded', isActive ? 'true' : 'false'); 1270 panel.setAttribute('aria-hidden', isActive ? 'false' : 'true'); 1271 1272 if (isActive) { 1273 this.enableFocus(panel); 1274 } else { 1275 this.disableFocus(panel); 1276 } 1277 }); 1278 1279 this.root.querySelectorAll('.MenuGroup__notoggle').forEach(el => el.setAttribute('tabindex', '-1')); 1280 } 1281 1282 updateAsideFocusables() { 1283 const aside = this.root.closest('#MenuAside'); 1284 const controller = aside?._controller; 1285 if (controller) controller.buildFocusable(); 1286 } 1287 1288 getFocusable() { 1289 1290 return [...this.root.querySelectorAll( 1291 'a[href], button:not(.MenuGroup__notoggle):not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' 1292 )] 1293 .filter(el => 1294 !el.closest('[inert]') && 1295 el.getClientRects().length 1296 ); 1297 } 1298 1299 firstLink(panel) { 1300 return panel?.querySelector('.MenuGroup__link'); 1301 } 1302 1303 nextToggle(current) { 1304 1305 const i = this.toggles.indexOf(current); 1306 if (i === -1) return null; 1307 1308 return this.toggles[i + 1] || this.toggles[0]; 1309 } 1310 1311 focusNextTopLevel(current) { 1312 1313 const i = this.topLevelItems.indexOf(current); 1314 if (i === -1) return; 1315 1316 const next = this.topLevelItems[i + 1] || this.topLevelItems[0]; 1317 next.focus(); 1318 } 1319 1320 focusPrevTopLevel(current) { 1321 1322 const i = this.topLevelItems.indexOf(current); 1323 if (i === -1) return; 1324 1325 const prev = this.topLevelItems[i - 1] || this.topLevelItems[this.topLevelItems.length - 1]; 1326 prev.focus(); 1327 } 1328 1329 bind() { 1330 1331 this.root.addEventListener('click', e => { 1332 1333 const toggle = e.target.closest('.MenuGroup__toggle'); 1334 if (!toggle || !this.root.contains(toggle)) return; 1335 1336 const panel = this.panels.get(toggle); 1337 if (!panel) return; 1338 1339 this.toggle(toggle, panel); 1340 }); 1341 1342 this.root.addEventListener('keydown', e => { 1343 1344 const focusables = this.getFocusable(); 1345 const current = document.activeElement; 1346 const i = focusables.indexOf(current); 1347 1348 const toggle = current?.closest('.MenuGroup__toggle'); 1349 const link = current?.closest('.MenuGroup__link'); 1350 const panel = current?.closest('.MenuGroup__panel'); 1351 1352 if (e.key === 'ArrowDown') { 1353 1354 if (i === -1) return; 1355 1356 e.preventDefault(); 1357 (focusables[i + 1] || focusables[0]).focus(); 1358 return; 1359 } 1360 1361 if (e.key === 'ArrowUp') { 1362 1363 if (i === -1) return; 1364 1365 e.preventDefault(); 1366 (focusables[i - 1] || focusables[focusables.length - 1]).focus(); 1367 return; 1368 } 1369 1370 if (toggle) { 1371 1372 const panelEl = this.panels.get(toggle); 1373 if (!panelEl) return; 1374 1375 switch (e.key) { 1376 1377 case 'Enter': 1378 case ' ': 1379 e.preventDefault(); 1380 this.toggle(toggle, panelEl); 1381 break; 1382 1383 case 'ArrowRight': 1384 1385 if (toggle.getAttribute('aria-expanded') !== 'true') { 1386 this.toggle(toggle, panelEl); 1387 } 1388 1389 this.firstLink(panelEl)?.focus(); 1390 break; 1391 1392 case 'ArrowLeft': 1393 1394 if (toggle.getAttribute('aria-expanded') === 'true') { 1395 this.close(toggle, panelEl); 1396 } 1397 1398 toggle.focus(); 1399 break; 1400 } 1401 1402 return; 1403 } 1404 1405 if (link) { 1406 1407 if (e.key === 'ArrowLeft') { 1408 1409 const parentToggle = 1410 link.closest('.MenuGroup')?.querySelector('.MenuGroup__toggle'); 1411 1412 if (parentToggle) { 1413 e.preventDefault(); 1414 parentToggle.focus(); 1415 } 1416 } 1417 } 1418 1419 if (panel && e.key === 'ArrowLeft') { 1420 1421 e.preventDefault(); 1422 1423 panel 1424 .closest('.MenuGroup') 1425 ?.querySelector('.MenuGroup__toggle') 1426 ?.focus(); 1427 } 1428 }); 1429 } 1430 1431 toggle(button, panel) { 1432 1433 const expanded = button.getAttribute('aria-expanded') === 'true'; 1434 const isEditions = 1435 document.documentElement.dataset.sitenavSection === 'editions'; 1436 1437 if (!isEditions) { 1438 this.closeAll(); 1439 } 1440 1441 if (expanded) { 1442 1443 if (isEditions) { 1444 this.close(button, panel); 1445 } else { 1446 button.focus(); 1447 } 1448 1449 this.updateAsideFocusables(); 1450 return; 1451 } 1452 1453 button.setAttribute('aria-expanded', 'true'); 1454 panel.setAttribute('aria-hidden', 'false'); 1455 1456 this.enableFocus(panel); 1457 1458 const { icon, use } = this.icons.get(button) || {}; 1459 1460 if (icon) { 1461 icon.classList.remove('icon-header-angle-down'); 1462 icon.classList.add('icon-header-angle-up'); 1463 } 1464 1465 if (use) { 1466 use.setAttribute('xlink:href', '#icon-header-angle-up'); 1467 } 1468 1469 this.updateAsideFocusables(); 1470 1471 panel.focus({ preventScroll: true }); 1472 } 1473 1474 close(button, panel) { 1475 1476 button.setAttribute('aria-expanded', 'false'); 1477 panel.setAttribute('aria-hidden', 'true'); 1478 1479 this.disableFocus(panel); 1480 1481 const { icon, use } = this.icons.get(button) || {}; 1482 1483 if (icon) { 1484 icon.classList.remove('icon-header-angle-up'); 1485 icon.classList.add('icon-header-angle-down'); 1486 } 1487 1488 if (use) { 1489 use.setAttribute('xlink:href', '#icon-header-angle-down'); 1490 } 1491 1492 this.updateAsideFocusables(); 1493 } 1494 1495 closeAll() { 1496
1497 this.toggles.forEach(toggle => { 1498 1499 const panel = this.panels.get(toggle); 1500 if (!panel) return; 1501 1502 toggle.setAttribute('aria-expanded', 'false'); 1503 panel.setAttribute('aria-hidden', 'true'); 1504 1505 this.disableFocus(panel); 1506 1507 const { icon, use } = this.icons.get(toggle) || {}; 1508 1509 if (icon) { 1510 icon.classList.remove('icon-header-angle-up'); 1511 icon.classList.add('icon-header-angle-down'); 1512 } 1513 1514 if (use) { 1515 use.setAttribute('xlink:href', '#icon-header-angle-down'); 1516 } 1517 }); 1518 } 1519 1520 disableFocus(container) { 1521 1522 const items = container.querySelectorAll( 1523 'a[href], button:not(.MenuGroup__notoggle):not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])' 1524 ); 1525 1526 items.forEach(el => { 1527 1528 if (!('prevTabindex' in el.dataset)) { 1529 el.dataset.prevTabindex = el.getAttribute('tabindex') ?? ''; 1530 } 1531 1532 el.setAttribute('tabindex', '-1'); 1533 }); 1534 } 1535 1536 enableFocus(container) { 1537 1538 const items = container.querySelectorAll( 1539 'a[href], button:not(.MenuGroup__notoggle):not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])' 1540 ); 1541 1542 items.forEach(el => { 1543 1544 if (!('prevTabindex' in el.dataset)) { 1545 el.removeAttribute('tabindex'); 1546 return; 1547 } 1548 1549 const prev = el.dataset.prevTabindex; 1550 1551 if (prev === '') { 1552 el.removeAttribute('tabindex'); 1553 } else { 1554 el.setAttribute('tabindex', prev); 1555 } 1556 1557 delete el.dataset.prevTabindex; 1558 }); 1559 } 1560 1561 focusNext(current) { 1562 1563 const i = this.toggles.indexOf(current); 1564 if (i === -1) return; 1565 1566 const next = this.toggles[i + 1] || this.toggles[0]; 1567 next.focus(); 1568 } 1569 1570 focusPrev(current) { 1571 1572 const i = this.toggles.indexOf(current); 1573 if (i === -1) return; 1574 1575 const prev = this.toggles[i - 1] || this.toggles[this.toggles.length - 1]; 1576 prev.focus(); 1577 } 1578} 1579 1580class LightboxController { 1581 1582 constructor() { 1583 1584 this.html = document.documentElement; 1585 1586 this.overlay = document.getElementById('LightboxOverlay'); 1587 this.lightboxes = [...document.querySelectorAll('.Lightbox')]; 1588 1589 if (!this.lightboxes.length) return; 1590 1591 this.openButtons = document.querySelectorAll('[data-lightbox-open]'); 1592 this.closeButtons = document.querySelectorAll('.Lightbox__close'); 1593 1594 this.primary = document.getElementById('primary'); 1595 this.header = document.getElementById('SiteHeader'); 1596 1597 this.panels = new Map( 1598 this.lightboxes.map(panel => [panel.dataset.lightbox, panel]) 1599 ); 1600 1601 this.activePanel = null; 1602 this.activeTrigger = null; 1603 this.focusable = []; 1604 1605 this.resizeRAF = null; 1606 1607 this.init(); 1608 this.bind(); 1609 } 1610
1611 init() { 1612 1613 this.lightboxes.forEach(panel => { 1614 panel.setAttribute('aria-hidden', 'true'); 1615 panel.setAttribute('inert', ''); 1616 this.setTabIndex(panel, -1); 1617 }); 1618 1619 if (this.overlay) { 1620 this.overlay.setAttribute('aria-hidden', 'true'); 1621 } 1622 } 1623 1624 setTabIndex(panel, value) { 1625 const elements = panel.querySelectorAll( 1626 'a[href], button, textarea, input, select, [tabindex]' 1627 ); 1628 1629 elements.forEach(el => { 1630 el.setAttribute('tabindex', value); 1631 }); 1632 } 1633 1634 bind() { 1635 1636 this.openButtons.forEach(btn => { 1637 btn.addEventListener('click', () => { 1638 this.toggle(btn); 1639 }); 1640 }); 1641 1642 this.closeButtons.forEach(btn => { 1643 btn.addEventListener('click', () => this.close()); 1644 }); 1645 1646 if (this.overlay) { 1647 this.overlay.addEventListener('click', () => this.close()); 1648 } 1649 1650 document.addEventListener('keydown', e => { 1651 1652 if (e.key === 'Escape' && this.html.dataset.lightboxOpen) { 1653 this.close(); 1654 } 1655 1656 if (e.key === 'Tab' && this.html.dataset.lightboxOpen) { 1657 this.trapFocus(e); 1658 } 1659 }); 1660 1661 window.addEventListener('resize', () => { 1662 1663 if (this.resizeRAF) return; 1664 1665 this.resizeRAF = requestAnimationFrame(() => { 1666 this.handleTopicsOverflow(); 1667 this.resizeRAF = null; 1668 }); 1669 1670 }); 1671 1672 document.addEventListener('sitenav:aside-open', () => { 1673 this.close(); 1674 }); 1675 } 1676 1677 toggle(trigger) { 1678 1679 const name = trigger.dataset.lightboxOpen; 1680 const panel = this.panels.get(name); 1681 1682 if (!panel) return; 1683 1684 const expanded = trigger.getAttribute('aria-expanded') === 'true'; 1685 1686 if (expanded) { 1687 this.close(); 1688 return; 1689 } 1690 1691 if (window.scrollY > 0) { 1692 1693 window.scrollTo({ 1694 top: 0, 1695 behavior: 'smooth' 1696 }); 1697 1698 const waitForTop = () => { 1699 1700 if (window.scrollY <= 0) { 1701 window.removeEventListener('scroll', waitForTop); 1702 this.open(trigger, panel, name); 1703 } 1704 }; 1705 1706 window.addEventListener('scroll', waitForTop, { passive: true }); 1707 return; 1708 } 1709 1710 this.open(trigger, panel, name); 1711 } 1712 1713 open(trigger, panel, name) { 1714 1715 document.dispatchEvent(new CustomEvent('sitenav:lightbox-open')); 1716 1717 this.close(); 1718 1719 this.activeTrigger = trigger; 1720 this.activePanel = panel; 1721 1722 panel.classList.add('is-open'); 1723 panel.setAttribute('aria-hidden', 'false'); 1724 panel.removeAttribute('inert'); 1725 this.setTabIndex(panel, 0); 1726 1727 if (!panel.hasAttribute('tabindex')) { 1728 panel.setAttribute('tabindex', '-1'); 1729 } 1730 1731 if (this.overlay) { 1732 this.overlay.classList.add('is-open'); 1733 this.overlay.setAttribute('aria-hidden', 'false'); 1734 } 1735 1736 // inert everything except the lightbox + overlay 1737 [...document.body.children].forEach(el => { 1738 if (!el.contains(panel) && el !== this.overlay) { 1739 el.setAttribute('inert', ''); 1740 } 1741 }); 1742 1743 this.html.dataset.lightboxOpen = name; 1744 1745 this.syncTriggers(true, name); 1746 1747 this.buildFocusable(panel); 1748 1749 if (this.focusable.length) { 1750 this.focusable[0].focus(); 1751 } else { 1752 panel.focus({ preventScroll: true }); 1753 } 1754 } 1755 1756 close() { 1757 1758 this.lightboxes.forEach(panel => { 1759 panel.classList.remove('is-open'); 1760 panel.setAttribute('aria-hidden', 'true'); 1761 panel.setAttribute('inert', ''); 1762 this.setTabIndex(panel, -1); 1763 }); 1764 1765 if (this.overlay) { 1766 this.overlay.classList.remove('is-open'); 1767 this.overlay.setAttribute('aria-hidden', 'true'); 1768 } 1769 1770 [...document.body.children].forEach(el => { 1771 el.removeAttribute('inert'); 1772 }); 1773 1774 delete this.html.dataset.lightboxOpen; 1775 1776 this.syncTriggers(false); 1777 1778 document.body.style.overflow = ''; 1779 1780 this.focusable = []; 1781 1782 if (this.activeTrigger) { 1783 this.activeTrigger.focus({ preventScroll: true }); 1784 this.activeTrigger = null; 1785 } 1786 1787 this.activePanel = null; 1788 } 1789 1790 buildFocusable(panel) { 1791 1792 this.focusable = Array.from( 1793 panel.querySelectorAll( 1794 'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])' 1795 ) 1796 ).filter(el => el.getClientRects().length); 1797 } 1798 1799 trapFocus(e) { 1800 1801 const focusable = this.focusable; 1802 if (!focusable.length) return; 1803 1804 const first = focusable[0]; 1805 const last = focusable[focusable.length - 1]; 1806 const active = document.activeElement; 1807 1808 if (e.shiftKey && active === first) { 1809 e.preventDefault(); 1810 last.focus(); 1811 } 1812 1813 if (!e.shiftKey && active === last) { 1814 e.preventDefault(); 1815 first.focus(); 1816 } 1817 } 1818 1819 syncTriggers(expanded, active = null) { 1820 1821 this.openButtons.forEach(btn => { 1822 1823 if (!expanded) { 1824 btn.setAttribute('aria-expanded', 'false'); 1825 return; 1826 } 1827 1828 btn.setAttribute( 1829 'aria-expanded', 1830 btn.dataset.lightboxOpen === active ? 'true' : 'false' 1831 ); 1832 }); 1833 } 1834 1835 handleTopicsOverflow() { 1836 1837 if (this.html.dataset.lightboxOpen !== 'topics') { 1838 document.body.style.overflow = ''; 1839 return; 1840 } 1841 1842 const panel = this.activePanel; 1843 1844 if (!this.header || !panel) { 1845 document.body.style.overflow = ''; 1846 return; 1847 } 1848 1849 const windowHeight = window.innerHeight; 1850 const headerHeight = this.header.offsetHeight; 1851 const panelHeight = panel.offsetHeight; 1852 1853 if (headerHeight + panelHeight < windowHeight) { 1854 document.body.style.overflow = 'hidden'; 1855 } else { 1856 document.body.style.overflow = ''; 1857 } 1858 } 1859} 1860 1861class PrimaryNavCoordinator { 1862 1863 constructor() { 1864 1865 this.aside = document.getElementById('MenuAside'); 1866 this.lightboxes = [...document.querySelectorAll('.Lightbox')]; 1867 1868 document.addEventListener('sitenav:aside-open', () => { 1869 this.closeLightboxes(); 1870 }); 1871 1872 document.addEventListener('sitenav:lightbox-open', () => { 1873 this.closeAside(); 1874 }); 1875 } 1876 1877 closeAside() { 1878 1879 const aside = this.aside; 1880 if (!aside || !aside.classList.contains('is-open')) return; 1881 1882 const controller = aside._controller; 1883 if (controller) controller.close(); 1884 } 1885 1886 closeLightboxes() { 1887 1888 for (const panel of this.lightboxes) { 1889 1890 if (!panel.classList.contains('is-open')) continue; 1891 1892 const controller = panel._controller; 1893 if (controller) controller.close(); 1894 1895 break; 1896 } 1897 } 1898} 1899 1900function priorityNav() { 1901 const runPriority = () => { 1902 const primaryList = document.querySelector('.PrimaryNav__list'); 1903 1904 if (!primaryList) return; 1905 1906 const items = [...primaryList.children]; 1907
1908 items.forEach(item => { 1909 item.style.position = ''; 1910 item.style.top = ''; 1911 item.style.visibility = 'visible'; 1912 }); 1913 1914 if (window.innerWidth <= 768) return; 1915 1916 const totalWidth = primaryList.getBoundingClientRect().width; 1917 const logoBuffer = 120; // Change this number to adjust the gap 1918 const availableWidth = totalWidth - logoBuffer; 1919 1920 const gap = parseFloat(window.getComputedStyle(primaryList).gap) || 0; 1921 let usedWidth = 0; 1922 1923 items.forEach((item, index) => { 1924 usedWidth += item.getBoundingClientRect().width; 1925 1926 if (index > 0) { 1927 usedWidth += gap; 1928 } 1929 1930 if (usedWidth > availableWidth) { 1931 item.style.position = 'absolute'; 1932 item.style.top = '-9999px'; 1933 item.style.visibility = 'hidden'; 1934 } 1935 }); 1936 }; 1937 1938 let resizeFrame; 1939 1940 const schedulePriority = () => { 1941 cancelAnimationFrame(resizeFrame); 1942 resizeFrame = requestAnimationFrame(runPriority); 1943 }; 1944 1945 runPriority(); 1946 window.addEventListener('resize', schedulePriority); 1947} 1948 1949document.addEventListener('DOMContentLoaded', () => { 1950 priorityNav(); 1951 1952 const audience = ( 1953 document.documentElement.getAttribute('data-audience') || 1954 document.body.getAttribute('data-audience') || 1955 '' 1956 ) 1957 .trim() 1958 .toLowerCase() 1959 .replace(/\s+/g, ''); 1960 1961 if ( audience !== 'consumer' ) { 1962 new PrimaryNavPersistent(); 1963 } 1964 1965 // PrimaryNavDropdown.init(); 1966 new PrimaryNavCoordinator(); 1967 1968 let aside; 1969 let lightbox; 1970 1971 document.addEventListener('click', (e) => { 1972 1973 const asideTrigger = e.target.closest('[data-menu-open]'); 1974 if (asideTrigger && !aside) { 1975 1976 aside = new MenuAsideController(); 1977 document.getElementById('MenuAside')._controller = aside; 1978 1979 document 1980 .querySelectorAll('.MenuAside__section') 1981 .forEach(nav => new MenuGroupController(nav)); 1982 1983 asideTrigger.click(); 1984 return; 1985 } 1986 1987 const lightboxTrigger = e.target.closest('[data-lightbox-open]'); 1988 if (lightboxTrigger && !lightbox) { 1989 1990 lightbox = new LightboxController(); 1991 1992 document 1993 .querySelectorAll('.Lightbox') 1994 .forEach(lb => lb._controller = lightbox); 1995 1996 lightboxTrigger.click(); 1997 } 1998 1999 }); 2000}); 2001 2002; 2003/** 2004 * A class to handle the toggling of content visibility with options. 2005 */ 2006 2007class ToggleContentVisibility { 2008 /** 2009 * @param {Element} node - The DOM element to apply the toggle behavior. 2010 * @param {Object} options - Configuration options for the class. 2011 */ 2012 constructor(node, options = {}) { 2013 if (!(node instanceof Element)) { 2014 throw new Error('Input node is not a valid DOM element.'); 2015 } 2016 2017 this.node = node; 2018 this.options = this.setOptions(options); 2019 this.elements = node.querySelectorAll( 2020 Array.isArray(this.options.targetElements) ? this.options.targetElements.join(', ') : '', 2021 ); 2022 this.showingMore = false; 2023 this.readMoreButton = this.node.querySelector('.read-more'); 2024 this.readLessButton = this.node.querySelector('.read-less'); 2025 2026 this.toggleContentBound = this.toggleContent.bind(this); // Bind method once 2027 2028 this.initialize(); 2029 } 2030 2031 setOptions(options) { 2032 const defaults = { 2033 initialIndex: 2, 2034 showReadLess: false, 2035 targetElements: [ 2036 'h1', 2037 'h2', 2038 'h3', 2039 'h4', 2040 'h5', 2041 'h6', 2042 'p', 2043 'blockquote', 2044 'pre', 2045 'address', 2046 'figcaption', 2047 'details', 2048 'ul', 2049 'ol', 2050 'dl', 2051 'table', 2052 'img', 2053 'video', 2054 'audio', 2055 'canvas', 2056 'iframe', 2057 'figure', 2058 'script', 2059 'noscript', 2060 'style', 2061 'form', 2062 'hr', 2063 'svg', 2064 ], 2065 position: 'after', 2066 readMoreText: 'Read more', 2067 readLessText: 'Read less', 2068 transitionEffect: null, 2069 }; 2070 2071 const readMoreAttribute = this.node.getAttribute('data-readmore'); 2072 const readLessAttribute = this.node.getAttribute('data-readless'); 2073 const readMoreCountAttribute = this.node.getAttribute('data-readmore-count'); 2074 2075 const parsedCount = parseInt(readMoreCountAttribute, 10); 2076 const initialIndex = Number.isNaN(parsedCount) ? defaults.initialIndex : parsedCount; 2077 2078 return { 2079 ...defaults, 2080 ...options, 2081 initialIndex, 2082 showReadLess: readLessAttribute !== 'false', 2083 readMoreText: readMoreAttribute || defaults.readMoreText, 2084 readLessText: readLessAttribute || defaults.readLessText, 2085 }; 2086 } 2087 2088 initialize() { 2089 if (this.elements.length === 0) return; // No elements, nothing to toggle 2090 if (this.options.initialIndex >= this.elements.length) { 2091 this.showingMore = true; 2092 return; 2093 } 2094 2095 if (!this.node.id) { 2096 this.node.id = `readmore-${Math.random().toString(36).substr(2, 9)}`; 2097 } 2098
2099 Array.from(this.elements).forEach((element, i) => { 2100 element.classList.toggle('hidden', i >= this.options.initialIndex); 2101 element.setAttribute('aria-hidden', i >= this.options.initialIndex); 2102 }); 2103 2104 this.showingMore = false; 2105 2106 if (!this.readMoreButton) { 2107 this.readMoreButton = ToggleContentVisibility.createControlButton( 2108 this.options.readMoreText, 2109 ['read-more'], 2110 this.toggleContentBound, 2111 ); 2112 this.appendControlButton(this.readMoreButton); 2113 } 2114 2115 if (this.options.showReadLess && !this.readLessButton) { 2116 this.readLessButton = ToggleContentVisibility.createControlButton( 2117 this.options.readLessText, 2118 ['read-less'], 2119 this.toggleContentBound, 2120 ); 2121 this.appendControlButton(this.readLessButton); 2122 } 2123 2124 this.node.setAttribute('aria-expanded', this.showingMore); 2125 this.manageButtonState(); 2126 } 2127 2128 static initAll() { 2129 const nodes = document.querySelectorAll('[data-readmore]'); 2130 nodes.forEach(node => { 2131 new ToggleContentVisibility(node); // eslint-disable-line no-new 2132 }); 2133 } 2134 2135 refresh() { 2136 this.destroy(); 2137 this.initialize(); 2138 } 2139 2140 update(newOptions) { 2141 this.options = this.setOptions({ ...this.options, ...newOptions }); 2142 this.refresh(); 2143 } 2144 2145 destroy() { 2146 this.showingMore = false; 2147 if (this.readMoreButton) { 2148 this.readMoreButton.removeEventListener('click', this.toggleContentBound); 2149 this.readMoreButton.remove(); 2150 this.readMoreButton = null; 2151 } 2152 2153 if (this.readLessButton) { 2154 this.readLessButton.removeEventListener('click', this.toggleContentBound); 2155 this.readLessButton.remove(); 2156 this.readLessButton = null; 2157 } 2158 2159 Array.from(this.elements).forEach(element => { 2160 element.classList.remove('hidden'); 2161 element.removeAttribute('aria-hidden'); 2162 }); 2163 2164 this.node.removeAttribute('aria-expanded'); 2165 } 2166 2167 toggleContent() { 2168 if (this.node.getAttribute('data-readless') === null) { 2169 this.showContent(); 2170 this.readMoreButton.remove(); 2171 if (this.readLessButton) { 2172 this.readLessButton.remove(); 2173 this.readLessButton = null; 2174 } 2175 } else if (this.showingMore) { 2176 this.hideContent(); 2177 } else { 2178 this.showContent(); 2179 } 2180 } 2181 2182 showContent() { 2183 this.showingMore = true; 2184 this.toggleElementsVisibility(true); 2185 this.manageButtonState(); 2186 } 2187 2188 hideContent() { 2189 this.showingMore = false; 2190 this.toggleElementsVisibility(false); 2191 this.manageButtonState(); 2192 } 2193 2194 toggleElementsVisibility(isVisible) { 2195 Array.from(this.elements) 2196 .slice(this.options.initialIndex) 2197 .forEach(element => { 2198 element.classList.toggle('hidden', !isVisible); 2199 element.setAttribute('aria-hidden', !isVisible); 2200 }); 2201 2202 this.node.setAttribute('aria-expanded', isVisible); 2203 } 2204 2205 manageButtonState() { 2206 if (this.showingMore) { 2207 this.configureReadLessButton(); 2208 } else { 2209 this.configureReadMoreButton(); 2210 } 2211 } 2212 2213 configureReadMoreButton() { 2214 if (this.readMoreButton) { 2215 this.readMoreButton.classList.remove('hidden'); 2216 this.readMoreButton.setAttribute('aria-hidden', 'false'); 2217 } 2218 if (this.readLessButton) { 2219 this.readLessButton.classList.add('hidden'); 2220 this.readLessButton.setAttribute('aria-hidden', 'true'); 2221 } 2222 } 2223 2224 configureReadLessButton() { 2225 if (this.readMoreButton) { 2226 this.readMoreButton.classList.add('hidden'); 2227 this.readMoreButton.setAttribute('aria-hidden', 'true'); 2228 } 2229 if (this.readLessButton) { 2230 this.readLessButton.classList.remove('hidden'); 2231 this.readLessButton.setAttribute('aria-hidden', 'false'); 2232 } 2233 } 2234 2235 static createControlButton(text, classNames, handler) { 2236 const button = document.createElement('button'); 2237 button.textContent = text; 2238 button.classList.add(...classNames); 2239 button.addEventListener('click', handler); 2240 return button; 2241 } 2242 2243 appendControlButton(button) { 2244 if (this.node.parentNode) { 2245 this.node.appendChild(button); 2246 } 2247 } 2248 } 2249 2250 2251ToggleContentVisibility.initAll(); 2252 ; 2253(() => { 2254 class dataLayerTracker { 2255 static VERSION = "1.0.0"; 2256 2257 constructor({ 2258 debug = false, 2259 prefix = "[DATA LAYER TRACKER]", 2260 analyticsVendor = "Google Analytics", 2261 trackers = [], 2262 dimensions = {} 2263 } = {}) { 2264 this.debug = debug === true; 2265 this.prefix = prefix; 2266 this.analyticsVendor = analyticsVendor; 2267 this.trackers = Array.isArray(trackers) ? trackers : []; 2268 2269 this.dimensions = ( 2270 dimensions && 2271 typeof dimensions === "object" && 2272 !Array.isArray(dimensions) 2273 ) 2274 ? { ...dimensions } 2275 : {}; 2276 2277 this.initialized = false;
2278 this.destroyed = false; 2279 2280 this.firedEvents = new Set(); 2281 2282 this.handleConsentManagerReady = this.handleConsentManagerReady.bind(this); 2283 this.handleDocumentClick = this.handleDocumentClick.bind(this); 2284 } 2285 2286 log(...args) { 2287 if (!this.debug) { 2288 return; 2289 } 2290 2291 console.log(this.prefix, ...args); 2292 } 2293 2294 warn(...args) { 2295 if (!this.debug) { 2296 return; 2297 } 2298 2299 console.warn(this.prefix, ...args); 2300 } 2301 2302 error(...args) { 2303 if (!this.debug) { 2304 return; 2305 } 2306 2307 console.error(this.prefix, ...args); 2308 } 2309 2310 group(label, callback) { 2311 if (!this.debug || typeof callback !== "function") { 2312 return; 2313 } 2314 2315 console.groupCollapsed(`${this.prefix} ${label}`); 2316 2317 try { 2318 callback(); 2319 } catch (error) { 2320 console.error(this.prefix, "Debug group failed.", error); 2321 } finally { 2322 console.groupEnd(); 2323 } 2324 } 2325 2326 hasAnalyticsConsent() { 2327 if ( 2328 typeof window.consentManager?.checkConsentByVendor !== "function" 2329 ) { 2330 this.log("Consent Manager is unavailable."); 2331 return false; 2332 } 2333 2334 try { 2335 return ( 2336 window.consentManager.checkConsentByVendor( 2337 this.analyticsVendor 2338 ) === true 2339 ); 2340 } catch (error) { 2341 this.error("Consent check failed.", error); 2342 return false; 2343 } 2344 } 2345 2346 getDataLayer() { 2347 if (!Array.isArray(window.dataLayer)) { 2348 this.log("window.dataLayer is unavailable."); 2349 return null; 2350 } 2351 2352 return window.dataLayer; 2353 } 2354 2355 getDimensions() { 2356 return { ...this.dimensions }; 2357 } 2358 2359 setDimensions(dimensions = {}) { 2360 if ( 2361 !dimensions || 2362 typeof dimensions !== "object" || 2363 Array.isArray(dimensions) 2364 ) { 2365 this.warn("Invalid dimensions.", dimensions); 2366 return false; 2367 } 2368 2369 if (!this.hasAnalyticsConsent()) { 2370 this.log("Dimensions blocked because analytics consent is unavailable."); 2371 return false; 2372 } 2373 2374 const dataLayer = this.getDataLayer(); 2375 2376 if (!dataLayer) { 2377 return false; 2378 } 2379 2380 const normalizedDimensions = Object.fromEntries( 2381 Object.entries(dimensions).filter(([key]) => ( 2382 typeof key === "string" && 2383 key.trim().length > 0 2384 )) 2385 ); 2386 2387 if (!Object.keys(normalizedDimensions).length) { 2388 return false; 2389 } 2390 2391 this.dimensions = { 2392 ...this.dimensions, 2393 ...normalizedDimensions 2394 }; 2395 2396 try { 2397 dataLayer.push({ ...normalizedDimensions }); 2398 } catch (error) { 2399 this.error("Data layer dimension push failed.", { 2400 dimensions: normalizedDimensions, 2401 error 2402 }); 2403 2404 return false; 2405 } 2406 2407 this.log("Data layer dimensions updated.", normalizedDimensions); 2408 2409 return true; 2410 } 2411 2412 2413 isValidEventName(eventName) { 2414 return ( 2415 typeof eventName === "string" && 2416 eventName.trim().length > 0 2417 ); 2418 } 2419 2420 isValidPayload(payload) { 2421 return ( 2422 payload === undefined || 2423 ( 2424 payload !== null && 2425 typeof payload === "object" && 2426 !Array.isArray(payload) 2427 ) 2428 ); 2429 } 2430 2431 hasEventFired(eventKey) { 2432 return this.firedEvents.has(eventKey); 2433 } 2434 2435 markEventAsFired(eventKey) { 2436 if (typeof eventKey !== "string" || !eventKey) { 2437 return; 2438 } 2439 2440 this.firedEvents.add(eventKey); 2441 } 2442 2443 resetEvent(eventKey) { 2444 if (typeof eventKey !== "string" || !eventKey) { 2445 return; 2446 } 2447 2448 this.firedEvents.delete(eventKey); 2449 } 2450 2451 resetEvents() { 2452 this.firedEvents.clear(); 2453 } 2454 2455 buildEventKey(eventName, key = "") { 2456 return key 2457 ? `${eventName}:${key}` 2458 : eventName; 2459 } 2460 2461 buildPayload(eventName, payload = {}) { 2462 return { 2463 ...this.dimensions, 2464 ...payload, 2465 event: eventName 2466 }; 2467 } 2468 2469 pushEvent(eventName, payload = {}, { 2470 once = true, 2471 key = "" 2472 } = {}) { 2473 if (!this.isValidEventName(eventName)) { 2474 this.warn("Invalid event name.", eventName); 2475 return false;
2476 } 2477 2478 if (!this.isValidPayload(payload)) { 2479 this.warn("Invalid event payload.", payload); 2480 return false; 2481 } 2482 2483 const normalizedEventName = eventName.trim(); 2484 const eventKey = this.buildEventKey(normalizedEventName, key); 2485 2486 if (once && this.hasEventFired(eventKey)) { 2487 return false; 2488 } 2489 2490 if (!this.hasAnalyticsConsent()) { 2491 this.log( 2492 "Event blocked because analytics consent is unavailable.", 2493 normalizedEventName 2494 ); 2495 2496 return false; 2497 } 2498 2499 const dataLayer = this.getDataLayer(); 2500 2501 if (!dataLayer) { 2502 return false; 2503 } 2504 2505 const eventPayload = this.buildPayload( 2506 normalizedEventName, 2507 payload 2508 ); 2509 2510 try { 2511 dataLayer.push(eventPayload); 2512 } catch (error) { 2513 this.error("Data layer push failed.", { 2514 eventName: normalizedEventName, 2515 payload: eventPayload, 2516 error 2517 }); 2518 2519 return false; 2520 } 2521 2522 if (once) { 2523 this.markEventAsFired(eventKey); 2524 } 2525 2526 this.group("Data layer event pushed", () => { 2527 console.log("Event:", normalizedEventName); 2528 console.log("Key:", eventKey); 2529 console.log("Payload:", eventPayload); 2530 console.log("Data layer length:", dataLayer.length); 2531 }); 2532 2533 return true; 2534 } 2535 2536 validateSelector(selector) { 2537 if (typeof selector !== "string" || !selector.trim()) { 2538 return false; 2539 } 2540 2541 try { 2542 document.querySelector(selector); 2543 return true; 2544 } catch (error) { 2545 this.error("Invalid selector.", { 2546 selector, 2547 error 2548 }); 2549 2550 return false; 2551 } 2552 } 2553 2554 getElements(selector) { 2555 if (!this.validateSelector(selector)) { 2556 return []; 2557 } 2558 2559 try { 2560 return Array.from(document.querySelectorAll(selector)); 2561 } catch (error) { 2562 this.error("Element lookup failed.", { 2563 selector, 2564 error 2565 }); 2566 2567 return []; 2568 } 2569 } 2570 2571 isElementVisible(element) { 2572 if (!(element instanceof HTMLElement)) { 2573 return false; 2574 } 2575 2576 if (!element.isConnected) { 2577 return false; 2578 } 2579 2580 if ( 2581 element.hidden || 2582 element.getAttribute("aria-hidden") === "true" || 2583 element.closest('[hidden], [inert], [aria-hidden="true"]') 2584 ) { 2585 return false; 2586 } 2587 2588 const style = window.getComputedStyle(element); 2589 2590 if ( 2591 style.display === "none" || 2592 style.visibility === "hidden" || 2593 style.visibility === "collapse" || 2594 Number.parseFloat(style.opacity) <= 0 2595 ) { 2596 return false; 2597 } 2598 2599 const rect = element.getBoundingClientRect(); 2600 2601 if (rect.width <= 0 || rect.height <= 0) { 2602 return false; 2603 } 2604 2605 return ( 2606 rect.bottom > 0 && 2607 rect.right > 0 && 2608 rect.top < window.innerHeight && 2609 rect.left < window.innerWidth 2610 ); 2611 } 2612 2613 handleVisibleElement(element, tracker) { 2614 if ( 2615 !tracker || 2616 !this.isValidEventName(tracker.visibleEvent) || 2617 !this.isElementVisible(element) 2618 ) { 2619 return false; 2620 } 2621 2622 const eventKey = this.buildEventKey( 2623 tracker.visibleEvent, 2624 tracker.visibleSelector 2625 ); 2626 2627 if (this.hasEventFired(eventKey)) { 2628 return false;
2629 } 2630 2631 if (tracker.visibleDimensions) { 2632 this.setDimensions(tracker.visibleDimensions); 2633 } 2634 2635 const pushed = this.pushEvent( 2636 tracker.visibleEvent, 2637 tracker.visiblePayload || {}, 2638 { 2639 once: true, 2640 key: tracker.visibleSelector 2641 } 2642 ); 2643 2644 if (!pushed) { 2645 return false; 2646 } 2647 2648 this.intersectionObserver?.unobserve(element); 2649 2650 this.log("Visible element tracked.", { 2651 selector: tracker.visibleSelector, 2652 element, 2653 event: tracker.visibleEvent 2654 }); 2655 2656 return true; 2657 } 2658 2659 handleClickedElement(element, tracker, event) { 2660 if ( 2661 !tracker || 2662 !this.isValidEventName(tracker.clickEvent) || 2663 !(element instanceof HTMLElement) || 2664 !this.isElementVisible(element) 2665 ) { 2666 return false; 2667 } 2668 2669 const pushed = this.pushEvent( 2670 tracker.clickEvent, 2671 tracker.clickPayload || {}, 2672 { 2673 once: true, 2674 key: tracker.clickSelector 2675 } 2676 ); 2677 2678 if (pushed) { 2679 this.log("Clicked element tracked.", { 2680 selector: tracker.clickSelector, 2681 element, 2682 event: tracker.clickEvent, 2683 originalEvent: event 2684 }); 2685 } 2686 2687 return pushed; 2688 } 2689 2690 checkVisibleElements() { 2691 for (const tracker of this.trackers) { 2692 if (!this.validateSelector(tracker?.visibleSelector)) { 2693 continue; 2694 } 2695 2696 const eventKey = this.buildEventKey( 2697 tracker.visibleEvent, 2698 tracker.visibleSelector 2699 ); 2700 2701 if (this.hasEventFired(eventKey)) { 2702 continue; 2703 } 2704 2705 const elements = this.getElements(tracker.visibleSelector); 2706 2707 for (const element of elements) { 2708 if (this.handleVisibleElement(element, tracker)) { 2709 break; 2710 } 2711 } 2712 } 2713 } 2714 2715 observeElements() { 2716 if (typeof MutationObserver !== "function") { 2717 this.warn("MutationObserver is unavailable."); 2718 return; 2719 } 2720 2721 this.mutationObserver = new MutationObserver(() => { 2722 this.observeCurrentElements(); 2723 this.checkVisibleElements(); 2724 }); 2725 2726 this.mutationObserver.observe(document.documentElement, { 2727 childList: true, 2728 subtree: true, 2729 attributes: true, 2730 attributeFilter: [ 2731 "class", 2732 "style", 2733 "hidden", 2734 "aria-hidden" 2735 ] 2736 }); 2737 2738 if (typeof IntersectionObserver !== "function") { 2739 this.warn("IntersectionObserver is unavailable."); 2740 return; 2741 } 2742 2743 this.intersectionObserver = new IntersectionObserver(entries => { 2744 for (const entry of entries) { 2745 if (!entry.isIntersecting) { 2746 continue; 2747 } 2748 2749 for (const tracker of this.trackers) { 2750 if ( 2751 this.validateSelector(tracker?.visibleSelector) && 2752 entry.target.matches(tracker.visibleSelector) 2753 ) { 2754 this.handleVisibleElement(entry.target, tracker); 2755 } 2756 } 2757 } 2758 }, { 2759 threshold: 0.01 2760 }); 2761 2762 this.observeCurrentElements(); 2763 } 2764 2765 observeCurrentElements() { 2766 if (!this.intersectionObserver) { 2767 return; 2768 } 2769 2770 for (const tracker of this.trackers) { 2771 if (!this.validateSelector(tracker?.visibleSelector)) { 2772 continue; 2773 } 2774 2775 const eventKey = this.buildEventKey( 2776 tracker.visibleEvent, 2777 tracker.visibleSelector 2778 ); 2779 2780 if (this.hasEventFired(eventKey)) { 2781 continue; 2782 } 2783 2784 const elements = this.getElements(tracker.visibleSelector); 2785 2786 for (const element of elements) { 2787 this.intersectionObserver.observe(element); 2788 } 2789 } 2790 } 2791 2792 disconnectObservers() { 2793 this.mutationObserver?.disconnect(); 2794 this.intersectionObserver?.disconnect(); 2795 2796 this.mutationObserver = null; 2797 this.intersectionObserver = null; 2798 } 2799 2800 handleDocumentClick(event) { 2801 if (!(event.target instanceof Element)) { 2802 return; 2803 } 2804 2805 for (const tracker of this.trackers) { 2806 if (!this.validateSelector(tracker?.clickSelector)) { 2807 continue; 2808 } 2809 2810 const clickedElement = event.target.closest(tracker.clickSelector); 2811 2812 if (!(clickedElement instanceof HTMLElement)) { 2813 continue; 2814 } 2815 2816 this.handleClickedElement(clickedElement, tracker, event); 2817 } 2818 } 2819 2820 attachEventListeners() { 2821 document.addEventListener( 2822 "click", 2823 this.handleDocumentClick, 2824 true 2825 ); 2826 } 2827 2828 detachEventListeners() { 2829 document.removeEventListener( 2830 "click", 2831 this.handleDocumentClick, 2832 true 2833 ); 2834 } 2835 2836 handleConsentManagerReady() { 2837 this.initialize(); 2838 } 2839 2840 initialize() { 2841 if (this.initialized || this.destroyed) { 2842 return false;
2843 } 2844 2845 if (!this.hasAnalyticsConsent()) { 2846 this.log("Analytics consent was not granted."); 2847 return false; 2848 } 2849 2850 this.initialized = true; 2851 2852 this.setDimensions(this.dimensions); 2853 2854 this.attachEventListeners(); 2855 this.observeElements(); 2856 this.checkVisibleElements(); 2857 2858 this.log( 2859 `Tracking initialized v${dataLayerTracker.VERSION}.`, 2860 { 2861 trackers: this.trackers.length, 2862 dimensions: this.getDimensions() 2863 } 2864 ); 2865 2866 return true; 2867 } 2868 2869 registerConsentManager() { 2870 document.addEventListener( 2871 "consentManagerReady", 2872 this.handleConsentManagerReady 2873 ); 2874 2875 if (window.consentManager?.eventDispatched === true) { 2876 this.initialize(); 2877 return; 2878 } 2879 2880 this.log("Waiting for consentManagerReady."); 2881 } 2882 2883 destroy() { 2884 if (this.destroyed) { 2885 return; 2886 } 2887 2888 document.removeEventListener( 2889 "consentManagerReady", 2890 this.handleConsentManagerReady 2891 ); 2892 2893 this.detachEventListeners(); 2894 this.disconnectObservers(); 2895 this.resetEvents(); 2896 2897 this.initialized = false; 2898 this.destroyed = true; 2899 2900 this.log("Tracking destroyed."); 2901 } 2902 } 2903 2904 const welcomeAdTracker = { 2905 visibleSelector: '.ad-overlay[data-ad-template="overlay"]', 2906 visibleEvent: "welcome_ad_shown", 2907 visiblePayload: { 2908 welcome_ad_shown: true, 2909 welcome_ad_closed: false 2910 }, 2911 2912 clickSelector: '.ad-overlay[data-ad-template="overlay"] #superstitial-text', 2913 clickEvent: "welcome_ad_closed", 2914 clickPayload: { 2915 welcome_ad_shown: true, 2916 welcome_ad_closed: true 2917 } 2918 }; 2919 2920 const topScrollerTracker = { 2921 visibleSelector: '[data-adnm-fid="adnami-canvas-desktop-topscroll"]', 2922 visibleEvent: "topscroller_ad_shown", 2923 2924 visibleDimensions: { 2925 topscroller_ad_shown: true 2926 } 2927 }; 2928 2929 const tracker = new dataLayerTracker({ 2930 debug: true, 2931 prefix: "[DATA LAYER TRACKER]", 2932 analyticsVendor: "Google Analytics", 2933 2934 dimensions: { 2935 topscroller_ad_shown: false 2936 }, 2937 2938 trackers: [ 2939 welcomeAdTracker, 2940 topScrollerTracker 2941 ] 2942 }); 2943 2944 tracker.registerConsentManager(); 2945 2946})();;
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.