1(function () { 2 'use strict'; 3 4 const header = document.querySelector('.sej-header-v2'); 5 6 if (!header || header.dataset.megaMenuReady === 'true') { 7 return; 8 } 9 10 header.dataset.megaMenuReady = 'true'; 11 12 const mobileQuery = window.matchMedia('(max-width: 1199px)'); 13 const menu = header.querySelector('.sej-mega-v2'); 14 const openButton = header.querySelector('.sej-mega-v2__open'); 15 const closeButton = header.querySelector('.sej-mega-v2__close'); 16 const overlay = header.querySelector('.sej-mega-v2__overlay'); 17 const panelItems = Array.from(header.querySelectorAll('.sej-mega-v2__item.has-panel')); 18 let pointerCloseTimer = 0; 19 20 if (!menu || !openButton || !closeButton || !overlay) { 21 return; 22 } 23 24 menu.dataset.enhanced = 'true'; 25 26 // Above 1600px the header container is viewport-centred, so the tuned 27 // per-variant anchors in CSS track their triggers and reproduce the design. 28 // Below that the container goes full-bleed and left-anchored, and a fixed 29 // anchor drifts away from its trigger -- far enough on the right-hand items 30 // that the panel opens to the left of the item you are pointing at. There, 31 // centre each panel on its own trigger instead. The CSS clamp still keeps it 32 // inside the viewport. One rect read per open; nothing on the move path. 33 const anchorQuery = window.matchMedia('(min-width: 100rem)'); 34 35 const positionPanel = (item) => { 36 const panel = item.querySelector(':scope > .sej-mega-v2__panel'); 37 38 if (!panel) { 39 return; 40 } 41 42 if (mobileQuery.matches || anchorQuery.matches) { 43 panel.style.removeProperty('--sej-panel-anchor'); 44 return; 45 } 46 47 const link = item.querySelector(':scope > .sej-mega-v2__top-row > .sej-mega-v2__top-link'); 48 49 if (!link) { 50 return; 51 } 52 53 const rect = link.getBoundingClientRect(); 54 panel.style.setProperty('--sej-panel-anchor', ((rect.left + rect.right) / 2) + 'px'); 55 }; 56 57 const setItemOpen = (item, open) => { 58 const toggle = item.querySelector(':scope > .sej-mega-v2__top-row > .sej-mega-v2__submenu-toggle'); 59 60 if (open) { 61 positionPanel(item); 62 } 63 64 item.dataset.open = open ? 'true' : 'false'; 65 if (toggle) { 66 toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); 67 } 68 }; 69 70 const closeOtherItems = (currentItem) => { 71 panelItems.forEach((item) => { 72 if (item !== currentItem) { 73 setItemOpen(item, false); 74 } 75 }); 76 }; 77 78 const closeAllItems = () => { 79 panelItems.forEach((item) => setItemOpen(item, false)); 80 }; 81 82 // ---- Hover intent ----------------------------------------------------- 83 // Moving diagonally from a trigger into its open panel crosses the 84 // neighbouring triggers and the row itself. Untreated, a neighbour 85 // steals the panel instantly and the short leave-grace closes it. Two 86 // rules fix both without hurting deliberate use: 87 // 1. a sibling only takes over after the pointer DWELLS on it â 88 // crossing at speed never switches; 89 // 2. while the pointer is still travelling inside the header row (or 90 // has already reached the panel), the open panel is kept alive up 91 // to a budget; leaving the row anywhere else closes it quickly. 92 // A geometric pointer-path triangle was tried first and rejected: with 93 // the panel edge only ~40px below the triggers, any horizontal travel 94 // along the row falls outside every cone. 95 const GRACE_RECHECK_MS = 100; 96 const GRACE_MAX_MS = 650; 97 98 let currPointer = null; 99 let pendingSwitchTimer = 0; 100 101 document.addEventListener('pointermove', (event) => { 102 currPointer = { x: event.clientX, y: event.clientY, t: event.timeStamp }; 103 }, { passive: true }); 104 105 const pointerInsidePanel = (item) => { 106 const panel = item.querySelector(':scope > .sej-mega-v2__panel'); 107 if (!panel || !currPointer) { 108 return false;
109 } 110 const r = panel.getBoundingClientRect(); 111 return r.width > 0 && currPointer.x >= r.left && currPointer.x <= r.right && 112 currPointer.y >= r.top && currPointer.y <= r.bottom; 113 }; 114 115 // Coordinate checks, not :hover â timers racing multi-step pointer moves 116 // read stale :hover state (Firefox), while currPointer is always the last 117 // dispatched position. 118 const pointerInsideItem = (item) => { 119 if (!currPointer) { 120 return false; 121 } 122 const r = item.getBoundingClientRect(); 123 return r.width > 0 && currPointer.x >= r.left && currPointer.x <= r.right && 124 currPointer.y >= r.top && currPointer.y <= r.bottom; 125 }; 126 127 const pointerReached = (item) => pointerInsideItem(item) || pointerInsidePanel(item); 128 129 // Still on the way? True while the pointer travels within the header 130 // row band â sibling triggers and the gaps between them. 131 const pointerInHeaderRow = () => { 132 if (!currPointer) { 133 return false; 134 } 135 const r = header.getBoundingClientRect(); 136 return currPointer.y >= r.top && currPointer.y <= r.bottom; 137 }; 138 139 const cancelPendingSwitch = () => { 140 window.clearTimeout(pendingSwitchTimer); 141 pendingSwitchTimer = 0; 142 }; 143 144 // A sibling only takes the panel over after the pointer DWELLS on it. 145 // Crossing triggers mid-flight (the diagonal swoop into the open panel) 146 // never counts: by the time the timer fires the pointer has either moved 147 // on or reached the open item's panel, and both cancel the switch. 148 const DWELL_SWITCH_MS = 200; 149 150 const schedulePendingSwitch = (target, open) => { 151 cancelPendingSwitch(); 152 pendingSwitchTimer = window.setTimeout(() => { 153 pendingSwitchTimer = 0; 154 if (pointerReached(open)) { 155 return; // pointer reached the open item or its panel 156 } 157 if (!pointerInsideItem(target)) { 158 return; // pointer kept travelling 159 } 160 closeOtherItems(target); 161 setItemOpen(target, true); 162 }, DWELL_SWITCH_MS); 163 }; 164 165 const setMobileMenuOpen = (open, returnFocus) => { 166 const mobileOpen = mobileQuery.matches && open; 167 168 menu.classList.toggle('is-open', open); 169 overlay.classList.toggle('is-open', open); 170 openButton.setAttribute('aria-expanded', open ? 'true' : 'false'); 171 document.body.classList.toggle('sej-mega-v2-lock', open); 172 173 if (mobileQuery.matches) { 174 menu.toggleAttribute('inert', !mobileOpen); 175 menu.setAttribute('aria-hidden', mobileOpen ? 'false' : 'true'); 176 } else { 177 menu.removeAttribute('inert'); 178 menu.removeAttribute('aria-hidden'); 179 } 180 181 if (open) { 182 window.requestAnimationFrame(() => closeButton.focus()); 183 } else { 184 closeAllItems(); 185 if (returnFocus) { 186 openButton.focus(); 187 } 188 } 189 }; 190 191 openButton.addEventListener('click', () => setMobileMenuOpen(true, false)); 192 closeButton.addEventListener('click', () => setMobileMenuOpen(false, true)); 193 overlay.addEventListener('click', () => setMobileMenuOpen(false, true)); 194 195 panelItems.forEach((item) => { 196 const toggle = item.querySelector(':scope > .sej-mega-v2__top-row > .sej-mega-v2__submenu-toggle'); 197 const topLink = item.querySelector(':scope > .sej-mega-v2__top-row > .sej-mega-v2__top-link'); 198 199 if (!toggle || !topLink) { 200 return; 201 } 202 203 const toggleItem = () => { 204 const willOpen = item.dataset.open !== 'true'; 205 closeOtherItems(item); 206 setItemOpen(item, willOpen); 207 }; 208 209 toggle.addEventListener('click', toggleItem); 210 211 // Drawer parity with the legacy menu: below the breakpoint, tapping the 212 // parent row expands it rather than navigating. Each panel leads with an 213 // "All <section>" link, so the parent destination stays reachable. 214 topLink.addEventListener('click', (event) => { 215 if (!mobileQuery.matches) { 216 return; 217 } 218 219 event.preventDefault(); 220 toggleItem(); 221 }); 222 223 item.addEventListener('pointerenter', (event) => { 224 if (mobileQuery.matches || event.pointerType === 'touch') { 225 return; 226 } 227 228 window.clearTimeout(pointerCloseTimer); 229 230 const openNeighbour = panelItems.find( 231 (other) => other !== item && other.dataset.open === 'true' 232 ); 233 234 if (openNeighbour) { 235 // Another panel is open â take over only after a dwell 236 schedulePendingSwitch(item, openNeighbour); 237 return; 238 } 239 240 cancelPendingSwitch(); 241 closeOtherItems(item); 242 setItemOpen(item, true); 243 }); 244 245 item.addEventListener('pointerleave', (event) => { 246 if (mobileQuery.matches || event.pointerType === 'touch') { 247 return; 248 } 249 250 window.clearTimeout(pointerCloseTimer); 251 const started = performance.now(); 252 const attemptClose = () => { 253 if (pointerReached(item)) { 254 return; // re-entered the item or its panel 255 } 256 if (item.dataset.open === 'true' && pointerInHeaderRow() && 257 performance.now() - started < GRACE_MAX_MS) { 258 pointerCloseTimer = window.setTimeout(attemptClose, GRACE_RECHECK_MS); 259 return; 260 } 261 setItemOpen(item, false); 262 }; 263 pointerCloseTimer = window.setTimeout(attemptClose, 90); 264 }); 265 266 item.addEventListener('focusin', () => { 267 if (mobileQuery.matches) { 268 return; 269 } 270 271 closeOtherItems(item); 272 setItemOpen(item, true); 273 }); 274 275 item.addEventListener('focusout', (event) => { 276 if (mobileQuery.matches || item.contains(event.relatedTarget)) { 277 return; 278 } 279 280 setItemOpen(item, false); 281 }); 282 283 item.addEventListener('keydown', (event) => { 284 if (event.key !== 'Escape') { 285 return; 286 } 287 288 event.preventDefault(); 289 topLink.focus(); 290 setItemOpen(item, false); 291 }); 292 }); 293 294 menu.addEventListener('keydown', (event) => { 295 if (event.key !== 'Tab' || !mobileQuery.matches || !menu.classList.contains('is-open')) { 296 return; 297 } 298 299 const focusableItems = Array.from( 300 menu.querySelectorAll('a[href], button:not([disabled]), input:not([disabled])') 301 ).filter((element) => element.getClientRects().length > 0); 302 303 if (!focusableItems.length) { 304 return; 305 } 306 307 const firstItem = focusableItems[0]; 308 const lastItem = focusableItems[focusableItems.length - 1]; 309 310 if (event.shiftKey && document.activeElement === firstItem) { 311 event.preventDefault(); 312 lastItem.focus(); 313 } else if (!event.shiftKey && document.activeElement === lastItem) { 314 event.preventDefault(); 315 firstItem.focus(); 316 } 317 }); 318 319 document.addEventListener('keydown', (event) => { 320 if (event.key !== 'Escape') { 321 return; 322 } 323 324 if (menu.classList.contains('is-open')) { 325 setMobileMenuOpen(false, true); 326 return; 327 } 328 329 closeAllItems(); 330 }); 331 332 document.addEventListener('pointerdown', (event) => { 333 if (!mobileQuery.matches && !header.contains(event.target)) { 334 closeAllItems(); 335 } 336 }); 337
338 // The header hides on scroll (.desktop-noscrolled-body -> .desktop-scrolled-body), 339 // but panels are fixed to the viewport rather than to the header, so an open 340 // panel would keep hanging in mid-air after its trigger scrolled away. The 341 // legacy menu opened on CSS :hover, so losing the header closed it implicitly; 342 // this restores that. Panels keep focus-opened state so keyboard users are not 343 // interrupted mid-scroll. Passive, with no layout reads, to keep scrolling cheap. 344 window.addEventListener('scroll', () => { 345 if (mobileQuery.matches || header.contains(document.activeElement)) { 346 return; 347 } 348 349 if (panelItems.some((item) => item.dataset.open === 'true')) { 350 closeAllItems(); 351 } 352 }, { passive: true }); 353 354 const handleViewportChange = () => { 355 setMobileMenuOpen(false, false); 356 closeAllItems(); 357 }; 358 359 // Crossing 1600px swaps which anchor applies, so close panels there too 360 // rather than leaving one open with an anchor from the other mode. 361 [mobileQuery, anchorQuery].forEach((query) => { 362 if (typeof query.addEventListener === 'function') { 363 query.addEventListener('change', handleViewportChange); 364 } else { 365 query.addListener(handleViewportChange); 366 } 367 }); 368 369 setMobileMenuOpen(false, false); 370}());
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.