1/** 2 * @file 3 * Global header custom scripts. 4 * 5 */ 6(function (Drupal) { 7 'use strict'; 8 Drupal.behaviors.pgi_global_header = { 9 attach: function (context, settings) { 10 // Cache DOM queries that are used multiple times 11 const domCache = { 12 overlay: document.querySelector('.pgi_global_overlay'), 13 closeButton: document.querySelector('.pgi-coveo-search-close'), 14 coveoSearchContainer: document.querySelector('.utility_search_droplet'), 15 searchDropdownTrigger: document.querySelector('.pgi-search-dropdown-trigger'), 16 dropDownSignIn: document.querySelector('pds-global-header-dropdown-item.mobile-utility-signin')?.shadowRoot?.querySelector('.pds-c-global-header-dropdown-item__label'), 17 mobileNavExternalLink: document.querySelector('pds-global-header')?.querySelector('[slot="mobile-nav-menu-footer"]')?.querySelector('pds-list-item pds-link.external-link-utility-nav-mobile'), 18 regionListItem: document.querySelector('pds-list-item.region-switcher-list-item'), 19 globalHeader: document.querySelector('.block-pgi-global-header'), 20 utilityHeader: document.querySelector('.block-pgi-utility-header pds-layout-container'), 21 globalHeaderDropdowns: document.querySelectorAll('.global-header-dropdown'), 22 mobileUtilitySignIn: document.querySelector('pds-global-header-dropdown-item.mobile-utility-signin'), 23 }; 24 domCache.regionButtonElement = domCache.regionListItem?.querySelector('pds-button'); 25 domCache.region_button = domCache.regionButtonElement?.shadowRoot?.querySelector('button'); 26 if (!domCache.utilityHeader && !domCache.globalHeader) return; 27 // Handle main navigation dropdown open event 28 function handleDropdownOpen(event) { 29 domCache.overlay?.setAttribute('aria-hidden', false); 30 // Close all other open dropdown items except the one that just opened 31 document.querySelectorAll('pds-global-header-dropdown-item').forEach((dropdownItem) => { 32 if (dropdownItem.open && dropdownItem !== event.target) { 33 dropdownItem.toggleOpen(); 34 } 35 }); 36 } 37 38 // Handle main navigation dropdown close event 39 function handleDropdownClose() { 40 domCache.overlay?.setAttribute('aria-hidden', true); 41 } 42 // Set up event listeners for PDS component dropdown events 43 document.addEventListener('pds-global-header-dropdown-item-open', handleDropdownOpen); 44 document.addEventListener('pds-global-header-dropdown-item-close', handleDropdownClose); 45 // Create a synthetic Escape key event for programmatic dispatch 46 const escapeEventOptions = { 47 key: 'Escape', 48 keyCode: 27, 49 which: 27, 50 code: 'Escape', 51 bubbles: true, 52 cancelable: true, 53 composed: true, 54 }; 55 function createEscapeEvent() { 56 return new KeyboardEvent('keydown', escapeEventOptions); 57 } 58 // Handle clicks on the Coveo search close button 59 function handleCoveoCloseClick(event) { 60 event.preventDefault(); 61 event.currentTarget.dispatchEvent(createEscapeEvent()); 62 } 63 64 // Handle keyboard events on the close button (Enter/Space) 65 function handleCloseButtonKeydown(event) { 66 if (event.key === 'Enter' || event.key === ' ') { 67 event.preventDefault(); 68 handleCoveoCloseClick(event); 69 } 70 } 71 // Set up event listeners for the Coveo close button 72 function setupCloseButtonListeners() { 73 if (domCache.closeButton) { 74 domCache.closeButton.addEventListener('click', handleCoveoCloseClick); 75 domCache.closeButton.addEventListener('keydown', handleCloseButtonKeydown); 76 } 77 } 78 // Handle keydown events on the search dropdown trigger 79 function handleSearchDropdownTriggerKeydown(event) { 80 if (event.key === 'Escape') { 81 domCache.coveoSearchContainer?.dispatchEvent(createEscapeEvent()); 82 } 83 } 84 // Focus the search input inside the Coveo search container 85 function searchFocus() { 86 const searchInput = domCache.coveoSearchContainer?.querySelector('.coveo-omnibox .search_focus'); 87 if (searchInput) { 88 searchInput.setAttribute('aria-labelledby', 'searchHeading'); 89 searchInput.setAttribute('placeholder', ''); 90 setTimeout(() => searchInput.focus(), 600); 91 } 92 } 93 // Set up event listeners for the search dropdown trigger 94 function setupSearchDropdownTriggerListeners() { 95 if (domCache.searchDropdownTrigger) { 96 domCache.searchDropdownTrigger.addEventListener('keydown', handleSearchDropdownTriggerKeydown); 97 domCache.searchDropdownTrigger.addEventListener('click', searchFocus); 98 } 99 } 100 // Handle viewport changes using media query 101 const mobileQuery = window.matchMedia('(width < 1024px)'); 102 mobileQuery.addEventListener('change', (mediaQuery) => { 103 if (mediaQuery.matches) { 104 const closeEvent = new CustomEvent( 105 'pds-global-header-dropdown-item-close', 106 { 107 bubbles: true, 108 cancelable: true, 109 }, 110 ); 111 document.dispatchEvent(closeEvent); 112 } 113 }); 114 function setHeaderBlockPosition() { 115 // Set overflow to visible for the utility menu dropdown item. 116 const utilityMenuDropdown = document.querySelector('pds-global-header-utility-menu')?.shadowRoot?.querySelector('pds-global-header-dropdown-item')?.shadowRoot?.querySelector('.pds-c-global-header-dropdown-item__menu'); 117 if (utilityMenuDropdown) utilityMenuDropdown.setAttribute('style', 'overflow: visible;'); 118 const regionHeader = document.querySelector('.region-header'); 119 if (!regionHeader) return; 120 121 // Hide both headers visually during load. The headers start hidden 122 // via CSS (visibility:hidden). Both are revealed together after 123 // PDS elements are upgraded and heights are correctly applied. 124 const globalHeaderBlock = regionHeader.querySelector('.block-pgi-global-header'); 125 const utilityLayoutContainer = regionHeader.querySelector('.block-pgi-utility-header pds-layout-container'); 126 127 const applyHeights = () => { 128 const blocks = regionHeader.querySelectorAll('.block-pgi-utility-header, .block-pgi-global-header'); 129 let cumulativeHeight = 0; 130 blocks.forEach(block => { 131 if (block.classList.contains('block-pgi-utility-header')) { 132 const layoutContainer = block.querySelector('pds-layout-container'); 133 if (layoutContainer) { 134 layoutContainer.style.top = `${cumulativeHeight}px`; 135 cumulativeHeight += layoutContainer.getBoundingClientRect().height; 136 return; 137 } 138 } else { 139 block.style.top = `${cumulativeHeight}px`; 140 cumulativeHeight += block.getBoundingClientRect().height; 141 } 142 }); 143 if (cumulativeHeight > 0) { 144 regionHeader.style.height = `${cumulativeHeight}px`; 145 // Clear the CSS min-height placeholder once JS has the real height, 146 // so it no longer overrides on mobile where utility header is hidden. 147 regionHeader.style.minHeight = '0'; 148 } 149 }; 150 151 let initialMeasurementDone = false;
152 let rafId; 153 154 const observer = new ResizeObserver(() => { 155 if (!initialMeasurementDone) return; 156 cancelAnimationFrame(rafId); 157 rafId = requestAnimationFrame(applyHeights); 158 }); 159 160 regionHeader.querySelectorAll('.block-pgi-utility-header, .block-pgi-global-header').forEach(block => { 161 observer.observe(block); 162 }); 163 const pdsHeader = regionHeader.querySelector('pds-global-header'); 164 if (pdsHeader) observer.observe(pdsHeader); 165 166 Promise.all([ 167 customElements.whenDefined('pds-global-header'), 168 customElements.whenDefined('pds-layout-container'), 169 ]).then(() => { 170 requestAnimationFrame(() => { 171 requestAnimationFrame(() => { 172 applyHeights(); 173 initialMeasurementDone = true; 174 // Reveal both headers simultaneously after correct positioning. 175 if (globalHeaderBlock) globalHeaderBlock.classList.add('pgi-header-ready'); 176 if (utilityLayoutContainer) utilityLayoutContainer.classList.add('pgi-utility-ready'); 177 }); 178 }); 179 }); 180 181 // Debounced resize listener for window resize events. 182 let resizeTimer; 183 window.addEventListener('resize', () => { 184 clearTimeout(resizeTimer); 185 resizeTimer = setTimeout(applyHeights, 100); 186 }); 187 } 188 // Adjust the sign-in icon and label in the dropdown 189 function setupSignInIcon() { 190 if (domCache.dropDownSignIn) { 191 const setupIcon = () => { 192 // Save original text 193 const dropDownSignIn = domCache.dropDownSignIn; 194 const originalText = dropDownSignIn.textContent.trim(); 195 196 // Create pds-icon-user element 197 const iconElement = document.createElement('pds-icon-user'); 198 iconElement.setAttribute('color', 'inverted-default'); 199 iconElement.style.marginRight = "8px"; 200 dropDownSignIn.textContent = ""; 201 // Apply inline-flex styling to the shadow DOM label element 202 dropDownSignIn.style.display = "inline-flex"; 203 dropDownSignIn.style.flexDirection = "unset"; 204 dropDownSignIn.style.alignItems = "center"; 205 dropDownSignIn.style.whiteSpace = "nowrap"; 206 207 // Append icon and text directly to label 208 dropDownSignIn.appendChild(iconElement); 209 dropDownSignIn.appendChild(document.createTextNode(originalText)); 210 }; 211 212 if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { 213 setTimeout(setupIcon, 100); 214 } else { 215 setupIcon(); 216 } 217 } 218 } 219 // Apply styles to external link in mobile nav 220 function externalLinkStyles(link) { 221 const applyStyles = () => { 222 if (link?.shadowRoot) { 223 const shadowRoot = link.shadowRoot; 224 shadowRoot.querySelector('.pds-c-link__wrapper')?.setAttribute('style', 'padding: calc(var(--pds-base-unit) * 2) calc(var(--pds-base-unit) * 3.5); border-left: calc(var(--pds-base-unit) * .5) solid var(--pds-component-global-header-dropdown-item-border); width: 100%;'); 225 shadowRoot.querySelector('.pds-c-link')?.setAttribute('style', 'display: flex; width: 100%; text-decoration: none;'); 226 shadowRoot.querySelector('.pds-c-link .pds-c-link__text')?.setAttribute('style', 'width: 100%; color: #FFFFFF;'); 227 link.querySelector('pds-icon-external-link')?.shadowRoot?.querySelector('svg')?.setAttribute('style', 'stroke: #FFFFFF !important;'); 228 } 229 }; 230 231 if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { 232 setTimeout(applyStyles, 100); 233 } else { 234 applyStyles(); 235 } 236 } 237 238 // Set scroll-margin-top for pds-footnote-item:target to avoid header overlap 239 // This function calculates the height of the global and utility headers 240 // and sets the scroll-margin-top CSS property accordingly. 241 const setFootnoteScrollMargin = () => { 242 if (!domCache.globalHeader && !domCache.utilityHeader) return; 243 const globalHeaderHeight = domCache.globalHeader ? domCache.globalHeader.offsetHeight : 0; 244 const utilityHeaderHeight = domCache.utilityHeader ? domCache.utilityHeader.offsetHeight : 0; 245 if (domCache.utilityHeader) { 246 document.documentElement.style.setProperty('--pds-utility-header-height', `${utilityHeaderHeight}px`); 247 } 248 if (domCache.globalHeader) { 249 document.documentElement.style.setProperty('--pds-global-header-height', `${globalHeaderHeight}px`); 250 } 251 }; 252 // Apply box shadow to the global header dropdown menu 253 function setupGlobalHeaderDropdownBoxShadow() { 254 if (domCache.globalHeaderDropdowns) { 255 domCache.globalHeaderDropdowns.forEach(globalHeaderDropdown => { 256 function setBoxShadow() { 257 const menu = globalHeaderDropdown.shadowRoot?.querySelector('.pds-c-global-header-dropdown-item__menu'); 258 if (menu) { 259 menu.style.boxShadow = 'inset 0 calc(var(--pds-base-unit) * -.125) 0 var(--pds-component-global-header-dropdown-menu-border)'; 260 } 261 } 262 if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { 263 setTimeout(setBoxShadow, 0); 264 } else { 265 setBoxShadow(); 266 } 267 }); 268 } 269 if (domCache.mobileUtilitySignIn) { 270 const setMobileSignInBoxShadow = () => { 271 const menu = domCache.mobileUtilitySignIn.shadowRoot?.querySelector('.pds-c-global-header-dropdown-item__menu'); 272 if (menu) { 273 menu.style.boxShadow = 'inset 0 0 0 var(--pds-component-global-header-dropdown-menu-border)'; 274 } 275 }; 276 if (/iPad|iPhone|iPod/.test(navigator.userAgent)) { 277 setTimeout(setMobileSignInBoxShadow, 0); 278 } else { 279 setMobileSignInBoxShadow(); 280 } 281 } 282 } 283 // On mobile view, close the mobile tray when tabbing out of the search submit button. 284 // Only execute this code on mobile view (width < 1024px) 285 if (window.matchMedia('(max-width: 1023px)').matches) { 286 const coveoOmniboxButton = document.querySelector('.coveo-omnibox'); 287 if (coveoOmniboxButton) { 288 coveoOmniboxButton.addEventListener('keydown', function (event) { 289 if (event.key === 'Tab') { 290 if (document.activeElement && document.activeElement.tagName === 'BUTTON' && document.activeElement.type === 'submit') { 291 document.querySelector('pds-global-header-utility-menu').shadowRoot.querySelector('pds-global-header-mobile-tray').removeAttribute('open'); 292 } 293 } 294 }); 295 } 296 } 297 // setHeaderBlockPosition installs a ResizeObserver that self-corrects 298 // whenever PDS components change size, so no deferred Promise needed. 299 setHeaderBlockPosition(); 300 setupCloseButtonListeners(); 301 setupSearchDropdownTriggerListeners(); 302 setupSignInIcon(); 303 if (domCache.mobileNavExternalLink) { 304 externalLinkStyles(domCache.mobileNavExternalLink); 305 } 306 setFootnoteScrollMargin(); 307 setupGlobalHeaderDropdownBoxShadow(); 308 309 } 310 }; 311 312})(Drupal);
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.