1(() => { 2 const initMegaMenu = () => { 3 const menu = document.querySelector("[data-sp-mega-menu]"); 4 5 if (!(menu instanceof HTMLElement) || menu.dataset.spMegaReady === "true") { 6 return; 7 } 8 9 menu.dataset.spMegaReady = "true"; 10 11 const menuItems = Array.from(menu.querySelectorAll("[data-sp-mega-item]")); 12 const desktopMedia = window.matchMedia("(min-width: 981px)"); 13 const drawer = menu.querySelector("[data-sp-mega-drawer]"); 14 const mobilePanel = menu.querySelector(".sp-mega-menu__mobile-panel"); 15 const mobileToggle = menu.querySelector("[data-sp-mega-toggle]"); 16 const mobileBack = menu.querySelector("[data-sp-mega-back]"); 17 const closeButtons = Array.from(menu.querySelectorAll("[data-sp-mega-close]")); 18 const mobileLevels = Array.from(menu.querySelectorAll("[data-sp-mega-level]")); 19 const mobileStack = ["root"]; 20 let activeItem = null; 21 let closeTimer = 0; 22 let mobileCloseTimer = 0; 23 24 const clearMobileCloseTimer = () => { 25 if (mobileCloseTimer) { 26 window.clearTimeout(mobileCloseTimer); 27 mobileCloseTimer = 0; 28 } 29 }; 30 31 const clearCloseTimer = () => { 32 if (closeTimer) { 33 window.clearTimeout(closeTimer); 34 closeTimer = 0; 35 } 36 }; 37 38 const closeDesktopMenu = () => { 39 clearCloseTimer(); 40 41 menuItems.forEach((item) => { 42 const trigger = item.querySelector("[data-sp-mega-trigger]"); 43 const panel = item.querySelector("[data-sp-mega-panel]"); 44 45 item.classList.remove("is-open"); 46 47 if (trigger instanceof HTMLElement) { 48 trigger.setAttribute("aria-expanded", "false"); 49 } 50 51 if (panel instanceof HTMLElement) { 52 panel.hidden = true; 53 } 54 }); 55 56 activeItem = null; 57 }; 58 59 const positionDesktopPanel = (item, panel) => { 60 if (!(item instanceof HTMLElement) || !(panel instanceof HTMLElement)) { 61 return; 62 } 63 64 const itemRect = item.getBoundingClientRect(); 65 const viewportWidth = document.documentElement.clientWidth; 66 const safeMargin = 16; 67 const previousVisibility = panel.style.visibility; 68 69 panel.style.left = "0px"; 70 panel.style.right = "auto"; 71 panel.style.visibility = "hidden"; 72 73 const panelRect = panel.getBoundingClientRect(); 74 const preferredLeft = itemRect.left + itemRect.width / 2 - panelRect.width / 2; 75 const clampedLeft = Math.max( 76 safeMargin, 77 Math.min(preferredLeft, Math.max(safeMargin, viewportWidth - safeMargin - panelRect.width)) 78 ); 79 const relativeLeft = clampedLeft - itemRect.left; 80 const arrowOffset = Math.max(20, Math.min(panelRect.width - 20, itemRect.width / 2 - relativeLeft)); 81 82 panel.style.left = `${relativeLeft}px`; 83 panel.style.setProperty("--sp-panel-arrow-offset", `${arrowOffset}px`); 84 panel.style.visibility = previousVisibility; 85 }; 86 87 const openDesktopMenu = (item) => { 88 if (!desktopMedia.matches || !(item instanceof HTMLElement)) { 89 return; 90 } 91 92 const trigger = item.querySelector("[data-sp-mega-trigger]"); 93 const panel = item.querySelector("[data-sp-mega-panel]"); 94 95 if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) { 96 return; 97 } 98 99 clearCloseTimer(); 100 101 if (activeItem && activeItem !== item) { 102 closeDesktopMenu(); 103 } 104 105 item.classList.add("is-open"); 106 trigger.setAttribute("aria-expanded", "true"); 107 panel.hidden = false; 108 positionDesktopPanel(item, panel); 109 activeItem = item; 110 }; 111 112 const scheduleDesktopClose = () => { 113 clearCloseTimer(); 114 closeTimer = window.setTimeout(closeDesktopMenu, 120); 115 }; 116 117 menuItems.forEach((item) => { 118 const trigger = item.querySelector("[data-sp-mega-trigger]"); 119 const panel = item.querySelector("[data-sp-mega-panel]"); 120 121 if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) { 122 return; 123 } 124 125 item.addEventListener("mouseenter", () => openDesktopMenu(item)); 126 item.addEventListener("mouseleave", scheduleDesktopClose); 127 item.addEventListener("focusin", () => openDesktopMenu(item)); 128 item.addEventListener("focusout", (event) => { 129 const nextTarget = event.relatedTarget; 130 131 if (!(nextTarget instanceof Node) || !item.contains(nextTarget)) { 132 scheduleDesktopClose(); 133 } 134 }); 135 136 trigger.addEventListener("click", (event) => { 137 if (!desktopMedia.matches) { 138 return; 139 } 140 141 event.preventDefault(); 142 143 if (activeItem === item) { 144 closeDesktopMenu(); 145 return; 146 } 147 148 openDesktopMenu(item); 149 }); 150 }); 151 152 menu.addEventListener("keydown", (event) => { 153 if (event.key === "Escape") { 154 closeDesktopMenu(); 155 } 156 }); 157 158 document.addEventListener("click", (event) => { 159 if (!(event.target instanceof Node) || menu.contains(event.target)) { 160 return; 161 } 162 163 closeDesktopMenu(); 164 }); 165 166 const getLevelById = (levelId) => menu.querySelector(`[data-sp-mega-level="${levelId}"]`); 167 const mobileFocusableSelector = [ 168 'a[href]:not([tabindex="-1"])', 169 'button:not([disabled]):not([tabindex="-1"])', 170 'input:not([disabled]):not([tabindex="-1"])', 171 'select:not([disabled]):not([tabindex="-1"])', 172 'textarea:not([disabled]):not([tabindex="-1"])',
173 '[tabindex]:not([tabindex="-1"])' 174 ].join(","); 175 176 const getMobileFocusableElements = () => { 177 if (!(mobilePanel instanceof HTMLElement)) { 178 return []; 179 } 180 181 return Array.from(mobilePanel.querySelectorAll(mobileFocusableSelector)).filter( 182 (element) => element instanceof HTMLElement && element.getClientRects().length > 0 183 ); 184 }; 185 186 const focusCurrentMobileLevel = () => { 187 const currentLevel = getLevelById(mobileStack[mobileStack.length - 1]); 188 const firstControl = currentLevel instanceof HTMLElement 189 ? Array.from(currentLevel.querySelectorAll(mobileFocusableSelector)).find( 190 (element) => element instanceof HTMLElement && element.getClientRects().length > 0 191 ) 192 : null; 193 194 if (firstControl instanceof HTMLElement) { 195 firstControl.focus({ preventScroll: true }); 196 } 197 }; 198 199 const syncMobileState = (direction = "forward") => { 200 if (!(drawer instanceof HTMLElement) || !(mobileToggle instanceof HTMLElement)) { 201 return; 202 } 203 204 const currentLevelId = mobileStack[mobileStack.length - 1]; 205 const currentIndex = mobileStack.length - 1; 206
207 mobileLevels.forEach((level) => { 208 if (!(level instanceof HTMLElement)) { 209 return; 210 } 211 212 const levelId = level.dataset.spMegaLevel; 213 const levelIndex = mobileStack.indexOf(levelId); 214 const isActive = levelId === currentLevelId; 215 216 level.classList.toggle("is-active", isActive); 217 level.classList.remove("is-before", "is-after"); 218 219 if (!isActive && levelIndex !== -1 && levelIndex < currentIndex) { 220 level.classList.add("is-before"); 221 } else if (!isActive) { 222 level.classList.add("is-after"); 223 } 224 }); 225 226 if (mobileBack instanceof HTMLElement) { 227 mobileBack.hidden = mobileStack.length <= 1; 228 } 229 230 drawer.classList.toggle("is-sublevel", mobileStack.length > 1); 231 drawer.dataset.spMegaDirection = direction; 232 }; 233 234 const openMobileDrawer = () => { 235 if (!(drawer instanceof HTMLElement) || !(mobileToggle instanceof HTMLElement)) { 236 return; 237 } 238 239 clearMobileCloseTimer(); 240 drawer.hidden = false; 241 window.requestAnimationFrame(() => { 242 drawer.classList.add("is-open"); 243 244 const initialFocus = mobilePanel instanceof HTMLElement 245 ? mobilePanel.querySelector("[data-sp-mega-close]") 246 : null; 247 248 if (initialFocus instanceof HTMLElement) { 249 initialFocus.focus({ preventScroll: true }); 250 } 251 }); 252 mobileToggle.setAttribute("aria-expanded", "true"); 253 document.body.classList.add("sp-mega-menu-open"); 254 document.body.style.overflow = "hidden"; 255 mobileStack.splice(0, mobileStack.length, "root"); 256 syncMobileState("forward"); 257 }; 258 259 const closeMobileDrawer = (restoreFocus = true) => { 260 if (!(drawer instanceof HTMLElement) || !(mobileToggle instanceof HTMLElement)) { 261 return; 262 } 263 264 const wasOpen = !drawer.hidden; 265 266 clearMobileCloseTimer(); 267 drawer.classList.remove("is-open"); 268 mobileToggle.setAttribute("aria-expanded", "false"); 269 document.body.classList.remove("sp-mega-menu-open"); 270 document.body.style.overflow = ""; 271 mobileStack.splice(0, mobileStack.length, "root"); 272 syncMobileState("forward"); 273 mobileCloseTimer = window.setTimeout(() => { 274 drawer.hidden = true; 275 }, 220); 276 277 if (restoreFocus && wasOpen) { 278 mobileToggle.focus({ preventScroll: true }); 279 } 280 }; 281 282 if (drawer instanceof HTMLElement) { 283 drawer.addEventListener("keydown", (event) => { 284 if (drawer.hidden) { 285 return; 286 } 287 288 if (event.key === "Escape") { 289 event.preventDefault(); 290 event.stopPropagation(); 291 closeMobileDrawer(); 292 return; 293 } 294 295 if (event.key !== "Tab") { 296 return; 297 } 298 299 const focusableElements = getMobileFocusableElements(); 300 301 if (focusableElements.length === 0) { 302 event.preventDefault(); 303 return; 304 } 305 306 const firstElement = focusableElements[0]; 307 const lastElement = focusableElements[focusableElements.length - 1]; 308 const activeElement = document.activeElement; 309 310 if (event.shiftKey && activeElement === firstElement) { 311 event.preventDefault(); 312 lastElement.focus(); 313 } else if (!event.shiftKey && activeElement === lastElement) { 314 event.preventDefault(); 315 firstElement.focus(); 316 } 317 }); 318 } 319 320 if (mobileToggle instanceof HTMLElement) { 321 mobileToggle.addEventListener("click", () => { 322 if (drawer instanceof HTMLElement && !drawer.hidden) { 323 closeMobileDrawer(); 324 return; 325 } 326 327 openMobileDrawer(); 328 }); 329 } 330 331 closeButtons.forEach((button) => { 332 button.addEventListener("click", () => closeMobileDrawer()); 333 }); 334 335 if (mobileBack instanceof HTMLElement) { 336 mobileBack.addEventListener("click", () => { 337 if (mobileStack.length > 1) { 338 mobileStack.pop(); 339 syncMobileState("backward"); 340 window.requestAnimationFrame(focusCurrentMobileLevel); 341 } 342 }); 343 } 344 345 menu.addEventListener("click", (event) => { 346 const nextButton = event.target instanceof Element ? event.target.closest("[data-sp-mega-next]") : null; 347 348 if (!(nextButton instanceof HTMLElement)) { 349 return; 350 } 351 352 const nextLevel = nextButton.dataset.spMegaNext; 353 354 if (!nextLevel || !getLevelById(nextLevel)) { 355 return; 356 } 357 358 event.preventDefault(); 359 mobileStack.push(nextLevel); 360 syncMobileState("forward"); 361 window.requestAnimationFrame(focusCurrentMobileLevel); 362 }); 363 364 desktopMedia.addEventListener("change", (event) => { 365 if (event.matches) { 366 closeMobileDrawer(false); 367 return; 368 } 369 370 closeDesktopMenu(); 371 }); 372 373 window.addEventListener("resize", () => { 374 if (!(activeItem instanceof HTMLElement) || !desktopMedia.matches) { 375 return; 376 } 377 378 const panel = activeItem.querySelector("[data-sp-mega-panel]"); 379 380 if (panel instanceof HTMLElement && !panel.hidden) {
381 positionDesktopPanel(activeItem, panel); 382 } 383 }); 384 385 syncMobileState(); 386 }; 387 388 if (document.readyState === "loading") { 389 document.addEventListener("DOMContentLoaded", initMegaMenu, { once: true }); 390 return; 391 } 392 393 initMegaMenu(); 394})();
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.