1(function () { 2 const autonavBP = matchMedia("(min-width: 1440px)"); 3 const autonavSemiBP = matchMedia("(min-width: 840px)"); 4 let layout = "desktop"; 5 let hamburgerActive = false; 6 let $cartLink = null; 7 let $cartCount = null; 8 let $hamburger = null; 9 let $searchToggle = null; 10 let $searchDropdown = null; 11 let trapFocus = false; 12 let $autonav, $topBar, $hamburgerMenu, $dropdownToggles, $main; 13 14 window.addEventListener("DOMContentLoaded", init); 15 16 function init() { 17 // Cache DOM queries 18 const $wrapper = document.querySelector(".autonav-wrapper"); 19 $autonav = $wrapper.querySelector(".autonav"); 20 $topBar = $autonav.querySelector(".autonav-top-bar"); 21 $hamburgerMenu = $autonav.querySelector("#autonav-page-list"); 22 $dropdownToggles = $hamburgerMenu.querySelectorAll( 23 ".autonav-dropdown-toggle", 24 ); 25 $main = document.querySelector("#main"); 26 27 // Use requestAnimationFrame for layout-related operations 28 requestAnimationFrame(() => { 29 initMarkup(); 30 initHamburger(); 31 initDropdowns(); 32 initDropdownSubemnus(); 33 trapFocusInit($autonav); 34 buildLayout(); 35 forceSubDropdownsOpen(); 36 }); 37 38 // Use passive event listeners where possible 39 autonavBP.addEventListener("change", buildLayout, { passive: true }); 40 autonavSemiBP.addEventListener("change", buildLayout, { passive: true }); 41 autonavSemiBP.addEventListener("change", forceSubDropdownsOpen, { 42 passive: true, 43 }); 44 45 document.querySelector("#autonav-page-list")?.classList.remove("hidden"); 46 } 47 48 function initMarkup() { 49 const $home = $autonav.querySelector(".autonav-home"); 50 const $homeLink = $home.querySelector("a"); 51 $homeLink.classList.add(..."inline-block mr-16 pl-0".split(" ")); 52 $topBar.append($homeLink); 53 $home.remove(); 54 55 const $tickets = $autonav.querySelector(".autonav-tickets"); 56 if ($tickets !== null) { 57 const $ticketsLink = $tickets.querySelector("a"); 58 // Store the original text content immediately 59 window.__ticketsText = $tickets.innerText; 60 61 $ticketsLink.setAttribute( 62 "class", 63 "autonav-tickets-link button button--primary max-[420px]:hidden pl-20", 64 ); 65 $ticketsLink.setAttribute("title", window.__ticketsText); 66 const icon = $ticketsLink.querySelector("svg"); 67 68 $ticketsLink.innerHTML = `${ 69 icon !== null ? icon.outerHTML : "" 70 }<span class="max-[580px]:sr-only">${window.__ticketsText}</span>`; 71 // Store tickets link for later use 72 window.__ticketsLink = $ticketsLink; 73 appendToTopBar($tickets); 74 } 75 76 const $cart = $autonav.querySelector(".autonav-cart"); 77 if ($cart !== null) { 78 $cartLink = $cart.querySelector("a"); 79 $cartLink.setAttribute( 80 "class", 81 "autonav-cart-link inline-flex gap-8 py-12 pl-16 pr-20 font-bold no-underline bg-white text-black hover:bg-white/90 focus:bg-white/90", 82 ); 83 $cartLink.hidden = true; 84 $cartLink.setAttribute("title", $cartLink.innerText); 85 $cartLink.innerHTML = 86 `<span class="sr-only">${$cartLink.innerHTML}</span>` + 87 '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 32 32"><circle cx="10" cy="28" r="2" fill="currentColor"/><circle cx="24" cy="28" r="2" fill="currentColor"/><path fill="currentColor" d="M28 7H5.82L5 2.8A1 1 0 0 0 4 2H0v2h3.18L7 23.2a1 1 0 0 0 1 .8h18v-2H8.82L8 18h18a1 1 0 0 0 1-.78l2-9A1 1 0 0 0 28 7m-2.8 9H7.62l-1.4-7h20.53Z"/></svg>'; // carbon:cart 88 appendToTopBar($cart); 89 initCartCount(); 90 } 91 92 initSearchDropdown(); 93 } 94 95 function initCartCount() { 96 $cartCount = document.createElement("span"); 97 $cartLink.append($cartCount); 98 99 window.__updateCartCount = (count = null) => { 100 const displayCount = 101 count !== null 102 ? count 103 : (JSON.parse(localStorage.getItem("tickets")) ?? []).length; 104 105 if (displayCount) { 106 $cartLink.hidden = false; 107 $cartCount.innerHTML = `${displayCount} <abbr class="no-underline" title="${window.__i18n.autonav.pieces}">${window.__i18n.autonav.pcs}</abbr>`; 108 109 // Hide tickets label when cart is visible (desktop only) 110 if (window.__ticketsLink && autonavBP.matches) { 111 const $textSpan = window.__ticketsLink.querySelector("span"); 112 if ($textSpan) { 113 $textSpan.classList.add("cart-visible-hide"); // Custom class for cart-based hiding 114 } 115 } 116 } else { 117 $cartLink.hidden = true; 118 $cartCount.innerHTML = ""; 119 120 // Show tickets label when cart is hidden (desktop only) 121 if (window.__ticketsLink && autonavBP.matches) { 122 const $textSpan = window.__ticketsLink.querySelector("span"); 123 if ($textSpan) { 124 $textSpan.classList.remove("cart-visible-hide"); // Remove our custom class 125 } 126 } 127 }
128 return displayCount; 129 }; 130 window.__updateCartCount(); 131 } 132 133 function initDropdowns() { 134 const handleDropdownToggle = (e) => { 135 e.preventDefault(); 136 toggleDropdown(e.currentTarget); 137 }; 138 139 $dropdownToggles.forEach(($toggle) => { 140 $toggle.addEventListener("click", handleDropdownToggle); 141 setDropdownExpanded($toggle, false); 142 $toggle.disabled = false; 143 }); 144 145 // Use event delegation for body click and keydown events 146 document.body.addEventListener("click", handleBodyClick); 147 document.body.addEventListener("keydown", handleBodyKeydown); 148 } 149 150 function handleBodyClick(e) { 151 if (e.target.closest(".autonav") === null) { 152 collapseAllTopBarDropdowns(); 153 } 154 } 155 156 function handleBodyKeydown(e) { 157 if (e.key == "Escape") { 158 if (hamburgerActive) { 159 if (document.activeElement.closest("#autonav-page-list") !== null) { 160 $hamburger.focus(); 161 } 162 setHamburgerExpanded(false); 163 } else { 164 const closestDropdown = document.activeElement.closest( 165 '[id^="autonav-dropdown-"][data-level="1"]', 166 ); 167 if (closestDropdown !== null) { 168 closestDropdown.previousElementSibling.focus(); 169 } 170 collapseAllTopBarDropdowns(); 171 } 172 } 173 } 174 175 function toggleDropdown($toggle) { 176 const expand = $toggle.getAttribute("aria-expanded") == "false"; 177 if ( 178 $toggle.getAttribute("data-level") == "1" && 179 $toggle.closest(".autonav-top-bar") !== null 180 ) { 181 collapseAllTopBarDropdowns(); 182 183 if (expand) { 184 trapFocus = true; 185 blurBackground(true); 186 } 187 } 188 189 // Mobile hamburger: only one top-level section open at a time (accordion). 190 if ( 191 !autonavSemiBP.matches && 192 expand && 193 $toggle.getAttribute("data-level") == "1" && 194 $hamburgerMenu.contains($toggle) 195 ) { 196 collapseMobileHamburgerLevel1DropdownsExcept($toggle); 197 } 198 199 setDropdownExpanded($toggle, expand); 200 } 201 202 function setDropdownExpanded($toggle, expanded) { 203 $toggle.setAttribute("aria-expanded", expanded ? "true" : "false"); 204 document.querySelector(`#${$toggle.getAttribute("aria-controls")}`).hidden = 205 !expanded; 206 } 207 208 function collapseAllTopBarDropdowns() { 209 for (const $toggle of $dropdownToggles) { 210 if ( 211 $toggle.closest(".autonav-top-bar") !== null && 212 $toggle.getAttribute("data-level") == "1" 213 ) { 214 setDropdownExpanded($toggle, false); 215 } 216 } 217 218 const $search = $autonav.querySelector(".autonav-top-bar .autonav-search"); 219 if ($search !== null) { 220 $searchToggle.setAttribute("aria-expanded", "false"); 221 $searchDropdown.hidden = true; 222 } 223 224 setHamburgerExpanded(false); 225 226 trapFocus = false; 227 blurBackground(false); 228 } 229 230 function blurBackground(blur) { 231 const $footer = document.querySelector("#footer-areas"); 232 if (blur) { 233 $main.classList.add("blur", "pointer-events-none"); 234 if ($footer !== null) { 235 $footer.classList.add("blur", "pointer-events-none"); 236 } 237 } else { 238 $main.classList.remove("blur", "pointer-events-none"); 239 if ($footer !== null) { 240 $footer.classList.remove("blur", "pointer-events-none"); 241 } 242 } 243 } 244 245 function initDropdownSubemnus() { 246 for (const $submenu of $hamburgerMenu.querySelectorAll( 247 ".autonav-dropdown-submenu", 248 ) ?? []) { 249 const $list = $submenu.querySelector(".autonav-dropdown-submenu-list"); 250 const items = $list.children.length; 251 $submenu.classList.add( 252 ...`autonav:row-span-${ 253 items + 1 254 } autonav:grid autonav:grid-cols-subgrid autonav:grid-rows-subgrid`.split( 255 " ", 256 ), 257 ); 258 $list.classList.add( 259 ...`autonav:row-span-${items} autonav:grid autonav:grid-cols-subgrid autonav:grid-rows-subgrid`.split( 260 " ", 261 ), 262 ); 263 } 264 } 265 266 function buildLayout() { 267 const $topLevelMenuItems = $autonav.querySelectorAll( 268 ".autonav-top-level-menu-item", 269 ); 270 const $langSwitch = $autonav.querySelector(".autonav-switch-language"); 271 const $search = $autonav.querySelector(".autonav-search"); 272 const $tickets = $autonav.querySelector(".autonav-tickets"); 273 const $userAccount = $autonav.querySelector(".autonav-user-account"); 274 275 // Clean up any existing tickets in hamburger menu 276 const $existingTickets = $hamburgerMenu.querySelector(".autonav-tickets"); 277 if ($existingTickets) { 278 $existingTickets.remove(); 279 } 280 281 // Clean up any existing user account in hamburger menu 282 const $existingUserAccount = $hamburgerMenu.querySelector(".autonav-user-account"); 283 if ($existingUserAccount) { 284 $existingUserAccount.remove(); 285 } 286 287 if (document.activeElement.closest(".autonav") !== null) {
288 $autonav.focus(); 289 } 290 291 if (autonavBP.matches) { 292 for (const $item of $topLevelMenuItems) { 293 appendToTopBar($item); 294 } 295 appendToTopBar($tickets); 296 appendToTopBar($autonav.querySelector(".autonav-cart")); 297 appendToTopBar($langSwitch); 298 appendToTopBar($search); 299 appendToTopBar($userAccount); 300 displaySearchToggle(true); 301 $hamburger.hidden = true; 302 } else { 303 // For both semi-desktop and mobile layouts 304 if (autonavSemiBP.matches) { 305 appendToTopBar($langSwitch); 306 appendToTopBar($search); 307 appendToTopBar($userAccount); 308 displaySearchToggle(true); 309 } else { 310 appendToHamburgerMenu($search); 311 displaySearchToggle(false); 312 } 313 314 for (const $item of $topLevelMenuItems) { 315 appendToHamburgerMenu($item); 316 } 317 318 if (!autonavSemiBP.matches) { 319 appendToHamburgerMenu($langSwitch); 320 appendToHamburgerMenu($userAccount); 321 } 322 323 324 // Add tickets button to hamburger menu 325 if ($tickets) { 326 const $ticketsClone = $tickets.cloneNode(true); 327 $ticketsClone.classList.remove("ml-auto"); 328 const $ticketsLink = $ticketsClone.querySelector("a"); 329 if ($ticketsLink) { 330 $ticketsLink.classList.remove("max-[420px]:hidden"); 331 $ticketsLink.classList.add("w-full", "justify-center", "my-16"); 332 const $textSpan = $ticketsLink.querySelector("span"); 333 if ($textSpan) { 334 $textSpan.classList.remove("max-[580px]:sr-only"); 335 // Use the stored text content 336 $textSpan.innerHTML = window.__ticketsText; 337 } 338 } 339 appendToHamburgerMenu($ticketsClone); 340 } 341 342 $topBar.append($hamburger); 343 $hamburger.hidden = false; 344 } 345 346 collapseAllTopBarDropdowns(); 347 348 // Keep the first mobile section (visit) expanded by default. 349 ensureMobileVisitDefaultOpen(); 350 351 // Update tickets label visibility based on cart state and screen size 352 updateTicketsLabelVisibility(); 353 } 354 355 function setHamburgerExpanded(expanded) { 356 $hamburger.setAttribute("aria-expanded", expanded); 357 $hamburgerMenu.hidden = !expanded; 358 if (expanded) { 359 ensureMobileVisitDefaultOpen(); 360 } 361 } 362 363 function initHamburger() { 364 const $label = $autonav.querySelector("#autonav-label"); 365 $hamburger = document.createElement("button"); 366 $hamburger.classList.add("autonav-hamburger"); 367 $hamburger.setAttribute("type", "button"); 368 $hamburger.setAttribute("aria-expanded", "false"); 369 $hamburger.setAttribute("aria-controls", "autonav-page-list"); 370 $hamburger.setAttribute("title", $label.innerText); 371 $hamburger.innerHTML = 372 `<span>${$label.innerHTML}</span>` + 373 '<svg class="autonav-hamburger-icon" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 32 32"><path fill="currentColor" d="M4 6h24v2H4zm0 18h24v2H4zm0-12h24v2H4zm0 6h24v2H4z"/></svg>' + // carbon:menu 374 '<svg class="autonav-hamburger-icon is-close" xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 32 32"><path fill="currentColor" d="M17.414 16L24 9.414L22.586 8L16 14.586L9.414 8L8 9.414L14.586 16L8 22.586L9.414 24L16 17.414L22.586 24L24 22.586z"/></svg>'; // carbon:close 375 $hamburger.addEventListener("click", (e) => { 376 e.preventDefault(); 377 const expand = $hamburger.getAttribute("aria-expanded") == "false"; 378 collapseAllTopBarDropdowns(); 379 setHamburgerExpanded(expand); 380 }); 381 382 $topBar.append($hamburger); 383 $hamburgerMenu.hidden = true; 384 } 385 386 function initSearchDropdown() { 387 $searchDropdown = $autonav.querySelector(".autonav-search search"); 388 $searchToggle = document.createElement("button"); 389 $searchToggle.classList.add( 390 ..."mb-1 flex items-center px-12 py-28 aria-expanded:bg-gray-10 hover:bg-gray-10 focus:bg-gray-10".split( 391 " ", 392 ), 393 ); 394 $searchToggle.setAttribute("type", "button"); 395 $searchToggle.setAttribute("aria-expanded", "false"); 396 $searchToggle.setAttribute( 397 "aria-controls", 398 $searchDropdown.getAttribute("id"), 399 ); 400 $searchToggle.setAttribute("title", window.__i18n?.autonav.search); 401 $searchToggle.innerHTML = 402 `<span class="sr-only">${window.__i18n?.autonav.search}</span>` + 403 '<svg aria-hidden="true" width="24" height="24" viewBox="0 0 32 32">
403<path fill="currentColor" d="m29 27.586l-7.552-7.552a11.018 11.018 0 1 0-1.414 1.414L27.586 29ZM4 13a9 9 0 1 1 9 9a9.01 9.01 0 0 1-9-9"/></svg>'; // carbon:search 404 $searchToggle.addEventListener("click", (e) => { 405 e.preventDefault(); 406 const expand = $searchDropdown.hidden; 407 if (expand) { 408 collapseAllTopBarDropdowns(); 409 } 410 trapFocus = expand; 411 blurBackground(expand); 412 $searchToggle.setAttribute("aria-expanded", expand ? "true" : "false"); 413 $searchDropdown.hidden = !expand; 414 }); 415 $searchDropdown.hidden = true; 416 $searchDropdown.before($searchToggle); 417 } 418 419 function displaySearchToggle(displayToggle) { 420 $searchToggle.hidden = !displayToggle; 421 if (!displayToggle) { 422 $searchDropdown.hidden = false; 423 } 424 } 425 426 function appendToTopBar($old) { 427 const $new = document.createElement("div"); 428 if ($old.hasAttribute("class")) { 429 $new.setAttribute("class", $old.getAttribute("class")); 430 } 431 while ($old.children.length > 0) { 432 $new.insertBefore($old.children[0], null); 433 } 434 $topBar.append($new); 435 $old.remove(); 436 } 437 438 function appendToHamburgerMenu($old) { 439 const $new = document.createElement("li"); 440 if ($old.hasAttribute("class")) { 441 $new.setAttribute("class", $old.getAttribute("class")); 442 } 443 while ($old.children.length > 0) { 444 $new.insertBefore($old.children[0], null); 445 } 446 $hamburgerMenu.append($new); 447 $old.remove(); 448 } 449 450 function updateTicketsLabelVisibility() { 451 if (window.__ticketsLink) { 452 const $textSpan = window.__ticketsLink.querySelector("span"); 453 if ($textSpan) { 454 // Only manage our cart-based hiding on desktop 455 const cartVisible = !$cartLink.hidden; 456 const isDesktop = autonavBP.matches; 457 458 if (cartVisible && isDesktop) { 459 $textSpan.classList.add("cart-visible-hide"); // Custom class for cart-based hiding 460 } else if (isDesktop) { 461 $textSpan.classList.remove("cart-visible-hide"); // Remove our custom class 462 } 463 // On mobile, do nothing - let the existing responsive behavior work 464 } 465 } 466 } 467 468 function forceSubDropdownsOpen() { 469 for (const $toggle of $dropdownToggles) { 470 if (autonavSemiBP.matches && $toggle.getAttribute("data-level") != "1") { 471 setDropdownExpanded($toggle, true); 472 $toggle.disabled = true; 473 } else { 474 setDropdownExpanded($toggle, false); 475 $toggle.disabled = false; 476 } 477 } 478 } 479 480 function collapseMobileHamburgerLevel1DropdownsExcept($exceptToggle) { 481 if (autonavSemiBP.matches) { 482 return; 483 } 484 for (const $t of $dropdownToggles) { 485 if ( 486 $t.getAttribute("data-level") == "1" && 487 $hamburgerMenu.contains($t) && 488 $t !== $exceptToggle 489 ) { 490 setDropdownExpanded($t, false); 491 } 492 } 493 } 494 495 function ensureMobileVisitDefaultOpen() { 496 if (autonavSemiBP.matches) { 497 return; 498 } 499 500 const $visitDropdown = $hamburgerMenu.querySelector( 501 ".autonav-dropdown.is-visit", 502 ); 503 if (!$visitDropdown) { 504 return; 505 } 506 507 const visitDropdownId = $visitDropdown.getAttribute("id"); 508 if (!visitDropdownId) { 509 return; 510 } 511 512 const $visitToggle = $hamburgerMenu.querySelector( 513 `.autonav-dropdown-toggle[data-level="1"][aria-controls="${visitDropdownId}"]`, 514 ); 515 if (!$visitToggle) { 516 return; 517 } 518 519 collapseMobileHamburgerLevel1DropdownsExcept($visitToggle); 520 setDropdownExpanded($visitToggle, true); 521 } 522 523 /** 524 * https://github.com/jashkenas/underscore/blob/master/modules/debounce.js 525 */ 526 function debounce(func, wait, immediate) { 527 function restArguments(func, startIndex) { 528 startIndex = startIndex == null ? func.length - 1 : +startIndex; 529 return function () { 530 var length = Math.max(arguments.length - startIndex, 0), 531 rest = Array(length), 532 index = 0; 533 for (; index < length; index++) { 534 rest[index] = arguments[index + startIndex]; 535 } 536 switch (startIndex) { 537 case 0: 538 return func.call(this, rest); 539 case 1: 540 return func.call(this, arguments[0], rest); 541 case 2: 542 return func.call(this, arguments[0], arguments[1], rest); 543 } 544 var args = Array(startIndex + 1); 545 for (index = 0; index < startIndex; index++) { 546 args[index] = arguments[index]; 547 } 548 args[startIndex] = rest; 549 return func.apply(this, args); 550 }; 551 } 552 553 var timeout, previous, args, result, context; 554 555 var later = function () { 556 var passed = Date.now() - previous; 557 if (wait > passed) { 558 timeout = setTimeout(later, wait - passed); 559 } else { 560 timeout = null;
561 if (!immediate) result = func.apply(context, args); 562 // This check is needed because `func` can recursively invoke `debounced`. 563 if (!timeout) args = context = null; 564 } 565 }; 566 567 var debounced = restArguments(function (_args) { 568 context = this; 569 args = _args; 570 previous = Date.now(); 571 if (!timeout) { 572 timeout = setTimeout(later, wait); 573 if (immediate) result = func.apply(context, args); 574 } 575 return result; 576 }); 577 578 debounced.cancel = function () { 579 clearTimeout(timeout); 580 timeout = args = context = null; 581 }; 582 583 return debounced; 584 } 585 586 /** 587 * Trap focus (pressing tab) inside `$element`. 588 */ 589 function trapFocusInit($element) { 590 // all focusable elements (https://classroom.udacity.com/courses/ud891/lessons/7962031279/concepts/79621414230923) 591 const focusableSelector = 592 'a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), iframe, object, embed, [tabindex="0"], [contenteditable]'; 593 594 function focusOverride(e) { 595 if (!trapFocus) return; 596 597 if (e.keyCode === 9) { 598 // Tab 599 // DOM could be updated, check new elements every time 600 let $focusable = $element.querySelectorAll(focusableSelector); 601 $focusable = Array.from($focusable).filter((node) => { 602 if (node.closest("[hidden]") !== null) return false; 603 return ( 604 node.getAttribute("tabindex") !== null || 605 node.getAttribute("tabindex") >= 0 606 ); 607 }); 608 609 // last -> first 610 if (e.target == $focusable[$focusable.length - 1] && !e.shiftKey) { 611 e.preventDefault(); 612 $focusable[0].focus(); 613 } 614 // first -> last 615 else if (e.target == $focusable[0] && e.shiftKey) { 616 e.preventDefault(); 617 $focusable[$focusable.length - 1].focus(); 618 } 619 } 620 } 621 622 $element.addEventListener("keydown", focusOverride); 623 return () => { 624 $element.removeEventListener("keydown", focusOverride); 625 }; 626 } 627})();
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.