1// Main script 2(function() { 3 "use strict"; 4 5 document.addEventListener("DOMContentLoaded", function() { 6 loadTemplateSafeguards(); 7 repairGlobalHeaderControlMarkup(); 8 initWesternBrandingSafeguard(); 9 initSearchUI(); 10 initOffcanvasCloseControls(); 11 initLegacyMobileMenuResets(); 12 initGlobalMenuBodyCleanup(); 13 initGlobalMenuMutualExclusion(); 14 initHiddenRibbonIconSwap(); 15 initLeftNavDisabledLinkStyling(); 16 initVideoAccordionSafety(); 17 initResponsiveMediaEmbeds(); 18 makeTablesResponsive(); 19 setPdfLinksToBlankSafe(); 20 initMatchHeights(); 21 initHeroVideo(); 22 initLeftNavClone(); 23 initGlobalMenuDropdowns(); 24 initOffcanvasAccordionStates(); 25 initCustomCarousels(); 26 initViewportCloseBehavior(); 27 initPagination(); 28 ensureDefaultAccordionTargets(); 29 initHiddenRibbonFocusManagement(); 30 31 document.body.style.overflowX = "hidden"; 32 document.body.style.overflowY = "auto"; 33 34 }); 35 36 // Change Focus when Hidden Ribbon is revealed 37 function initHiddenRibbonFocusManagement() { 38 var hiddenRibbon = document.getElementById("hidden-ribbon"); 39 if (!hiddenRibbon) return; 40 41 var lastTrigger = null; 42 43 var triggerSelector = [ 44 '[data-bs-toggle="collapse"][data-bs-target="#hidden-ribbon"]', 45 '[data-bs-toggle="collapse"][href="#hidden-ribbon"]', 46 '[aria-controls="hidden-ribbon"]' 47 ].join(","); 48 49 var focusableSelector = [ 50 'a[href]', 51 'button:not([disabled])', 52 'input:not([type="hidden"]):not([disabled])', 53 'select:not([disabled])', 54 'textarea:not([disabled])', 55 '[tabindex]:not([tabindex="-1"])' 56 ].join(","); 57 58 function getFirstFocusable() { 59 return hiddenRibbon.querySelector(focusableSelector); 60 } 61 62 function moveFocusIntoRibbon() { 63 var firstFocusable = getFirstFocusable(); 64 65 if (firstFocusable) { 66 firstFocusable.focus(); 67 return; 68 } 69 70 hiddenRibbon.setAttribute("tabindex", "-1"); 71 hiddenRibbon.focus(); 72 } 73 74 document.addEventListener("click", function(e) { 75 var trigger = e.target.closest(triggerSelector); 76 if (!trigger) return; 77 78 lastTrigger = trigger; 79 }, true); 80 81 document.addEventListener("keydown", function(e) { 82 var trigger = e.target.closest(triggerSelector); 83 if (!trigger) return; 84 85 if (e.key === "Enter" || e.key === " ") { 86 lastTrigger = trigger; 87 } 88 }, true); 89 90 hiddenRibbon.addEventListener("shown.bs.collapse", function() { 91 moveFocusIntoRibbon(); 92 }); 93 94 hiddenRibbon.addEventListener("hidden.bs.collapse", function() { 95 if (lastTrigger && typeof lastTrigger.focus === "function") { 96 lastTrigger.focus(); 97 } 98 }); 99} 100 // ===================================================== 101 // TABLET / MOBILE HEADER DOM ORDER FIX 102 // Makes keyboard tab order match intended reading order: 103 // main logo â department â search â hamburger 104 // ===================================================== 105 function initTabletMobileHeaderDomOrder() { 106 var pageTitleContainer = document.getElementById("page-title-container"); 107 var toggleDiv = document.getElementById("tablet-menu-toggle-div"); 108 var mainLogo = document.getElementById("main-logo"); 109 110 if (!pageTitleContainer || !toggleDiv || !mainLogo) return; 111 112 var tabletMobileQuery = window.matchMedia("(max-width: 1199.98px)"); 113 114 function moveToggleAfterMainLogo() { 115 if (!tabletMobileQuery.matches) return; 116 117 // Only move if it is not already directly after #main-logo 118 if (mainLogo.nextElementSibling !== toggleDiv) { 119 mainLogo.insertAdjacentElement("afterend", toggleDiv); 120 } 121 } 122 123 moveToggleAfterMainLogo(); 124 125 if (tabletMobileQuery.addEventListener) { 126 tabletMobileQuery.addEventListener("change", moveToggleAfterMainLogo); 127 } else if (tabletMobileQuery.addListener) { 128 tabletMobileQuery.addListener(moveToggleAfterMainLogo); 129 } 130 131 new MutationObserver(function() { 132 moveToggleAfterMainLogo(); 133 }).observe(pageTitleContainer, { 134 childList: true, 135 subtree: false 136 }); 137 } 138 139 // ===================================================== 140 // REPAIR GLOBAL HEADER CONTROL MARKUP 141 // Converts icon <a> controls to <button> controls 142 // and removes nested interactive markup from hamburger. 143 // ===================================================== 144 function repairGlobalHeaderControlMarkup() { 145 var searchTriggerSelector = 'a[data-bs-toggle="collapse"][data-bs-target="#search-container"]'; 146 var offcanvasCloseSelector = 'a.offcanvas-close'; 147 var hamburgerSelector = '#wu-toggle-mobile, button.navbar-toggler[data-bs-target="#global-menu
147-offcanvas"]'; 148 149 function getLabelFromControl(control, fallback) { 150 if (!control) return fallback; 151 152 var ariaLabel = control.getAttribute("aria-label"); 153 if (ariaLabel && ariaLabel.trim()) return ariaLabel.trim(); 154 155 var hiddenText = control.querySelector(".visually-hidden"); 156 if (hiddenText && hiddenText.textContent.trim()) { 157 return hiddenText.textContent.trim(); 158 } 159 160 var img = control.querySelector("img[alt]"); 161 if (img && img.getAttribute("alt").trim()) { 162 return img.getAttribute("alt").trim(); 163 } 164 165 return fallback; 166 } 167 168 function copyAttributes(fromEl, toEl, skip) { 169 Array.prototype.slice.call(fromEl.attributes).forEach(function(attr) { 170 if (skip.indexOf(attr.name) !== -1) return; 171 toEl.setAttribute(attr.name, attr.value); 172 }); 173 } 174 175 function cleanIconImages(control) { 176 if (!control) return; 177 178 control.querySelectorAll("img").forEach(function(img) { 179 img.setAttribute("alt", ""); 180 img.setAttribute("aria-hidden", "true"); 181 img.removeAttribute("aria-label"); 182 img.removeAttribute("type"); 183 }); 184 185 control.querySelectorAll(".navbar-toggler-icon").forEach(function(icon) { 186 icon.setAttribute("aria-hidden", "true"); 187 }); 188 } 189 190 function convertAnchorToButton(anchor, fallbackLabel) { 191 if (!anchor || anchor.tagName !== "A") return anchor; 192 if (anchor.dataset.repairedControl === "1") return anchor; 193 194 var button = document.createElement("button"); 195 196 copyAttributes(anchor, button, [ 197 "href", 198 "role", 199 "tabindex" 200 ]); 201 202 button.setAttribute("type", "button"); 203 204 if (button.getAttribute("aria-expanded") === "flase") { 205 button.setAttribute("aria-expanded", "false"); 206 } 207 208 if (!button.getAttribute("aria-label")) { 209 button.setAttribute("aria-label", getLabelFromControl(anchor, fallbackLabel)); 210 } 211 212 while (anchor.firstChild) { 213 button.appendChild(anchor.firstChild); 214 } 215 216 cleanIconImages(button); 217 218 button.dataset.repairedControl = "1"; 219 anchor.replaceWith(button); 220 221 return button; 222 } 223 224 function repairSearchTriggers(root) { 225 var context = root || document; 226 227 if (context.matches && context.matches(searchTriggerSelector)) { 228 addSearchButtonClasses(convertAnchorToButton(context, "Open search")); 229 } 230 231 if (!context.querySelectorAll) return; 232 233 context.querySelectorAll(searchTriggerSelector).forEach(function(anchor) { 234 var button = convertAnchorToButton(anchor, "Open search"); 235 236 addSearchButtonClasses(button); 237 238 if (!button.getAttribute("aria-controls")) { 239 button.setAttribute("aria-controls", "search-container"); 240 } 241 242 if (!button.getAttribute("aria-expanded")) { 243 button.setAttribute("aria-expanded", "false"); 244 } 245 }); 246 } 247 248 function repairHamburger(root) { 249 var context = root || document; 250 251 function processButton(button) { 252 if (!button || button.dataset.repairedHamburger === "1") return; 253 254 button.setAttribute("type", "button"); 255 256 if (!button.getAttribute("aria-controls")) { 257 button.setAttribute("aria-controls", "global-menu
257-offcanvas"); 258 } 259 260 if (!button.getAttribute("aria-expanded")) { 261 button.setAttribute("aria-expanded", "false"); 262 } 263 264 if (!button.getAttribute("aria-label")) { 265 button.setAttribute("aria-label", getLabelFromControl(button, "Open menu")); 266 } 267 268 // Fix invalid structure: 269 // <button><a>...</a></button> 270 // becomes: 271 // <button>...</button> 272 button.querySelectorAll("a").forEach(function(innerLink) { 273 if (!button.getAttribute("data-bs-toggle") && innerLink.getAttribute("data-bs-toggle")) { 274 button.setAttribute("data-bs-toggle", innerLink.getAttribute("data-bs-toggle")); 275 } 276 277 if (!button.getAttribute("data-bs-target") && innerLink.getAttribute("data-bs-target")) { 278 button.setAttribute("data-bs-target", innerLink.getAttribute("data-bs-target")); 279 } 280 281 while (innerLink.firstChild) { 282 button.insertBefore(innerLink.firstChild, innerLink); 283 } 284 285 innerLink.remove(); 286 }); 287 288 cleanIconImages(button); 289 button.dataset.repairedHamburger = "1"; 290 } 291 292 if (context.matches && context.matches(hamburgerSelector)) { 293 processButton(context); 294 } 295 296 if (!context.querySelectorAll) return; 297 298 context.querySelectorAll(hamburgerSelector).forEach(processButton); 299 } 300 301 function repairOffcanvasClose(root) { 302 var context = root || document; 303 304 if (context.matches && context.matches(offcanvasCloseSelector)) { 305 convertAnchorToButton(context, "Close menu"); 306 } 307 308 if (!context.querySelectorAll) return; 309 310 context.querySelectorAll(offcanvasCloseSelector).forEach(function(anchor) { 311 var button = convertAnchorToButton(anchor, "Close menu"); 312 313 if (!button.getAttribute("data-bs-dismiss")) { 314 button.setAttribute("data-bs-dismiss", "offcanvas"); 315 } 316 317 if (!button.getAttribute("aria-label")) { 318 button.setAttribute("aria-label", "Close menu"); 319 } 320 }); 321 } 322 323 function repairAll(root) { 324 repairSearchTriggers(root); 325 repairHamburger(root); 326 repairOffcanvasClose(root); 327 } 328 329 function addSearchButtonClasses(button) { 330 if (!button) return; 331 332 ["btn", "bg-transparent", "border-0", "p-0", "m-0"].forEach(function(className) { 333 button.classList.add(className); 334 }); 335 } 336 337 repairAll(document); 338 339 new MutationObserver(function(mutations) { 340 mutations.forEach(function(mutation) { 341 mutation.addedNodes.forEach(function(node) { 342 if (node.nodeType !== 1) return; 343 repairAll(node); 344 }); 345 }); 346 }).observe(document.body, { 347 childList: true, 348 subtree: true 349 }); 350 351 var searchContainer = document.getElementById("search-container"); 352 var globalMenuOffcanvas = document.getElementById("global-menu-offcanvas"); 353 354 if (searchContainer) { 355 searchContainer.addEventListener("shown.bs.collapse", function() { 356 document 357 .querySelectorAll('[data-bs-toggle="collapse"][data-bs-target="#search-container"]') 358 .forEach(function(trigger) { 359 trigger.setAttribute("aria-expanded", "true"); 360 trigger.setAttribute("aria-label", "Close search"); 361 }); 362 }); 363 364 searchContainer.addEventListener("hidden.bs.collapse", function() { 365 document 366 .querySelectorAll('[data-bs-toggle="collapse"][data-bs-target="#search-container"]') 367 .forEach(function(trigger) { 368 trigger.setAttribute("aria-expanded", "false"); 369 trigger.setAttribute("aria-label", "Open search"); 370 }); 371 }); 372 } 373 374 if (globalMenuOffcanvas) { 375 globalMenuOffcanvas.addEventListener("shown.bs.offcanvas", function() { 376 document.querySelectorAll(hamburgerSelector).forEach(function(button) { 377 button.setAttribute("aria-expanded", "true"); 378 button.setAttribute("aria-label", "Close menu"); 379 }); 380 }); 381 382 globalMenuOffcanvas.addEventListener("hidden.bs.offcanvas", function() { 383 document.querySelectorAll(hamburgerSelector).forEach(function(button) { 384 button.setAttribute("aria-expanded", "false"); 385 button.setAttribute("aria-label", "Open menu"); 386 }); 387 }); 388 } 389 } 390 391 // ===================================================== 392 // KEYBOARD ACCESSIBILITY FIXES FOR ICON CONTROLS
393 // Fixes current markup without changing HTML structure 394 // ===================================================== 395 function initGlobalIconKeyboardAccessibility() { 396 var searchTriggerSelector = '[data-bs-toggle="collapse"][data-bs-target="#search-container"]'; 397 var mobileMenuButtonSelector = '#wu-toggle-mobile, button.navbar-toggler[data-bs-target="#global-menu-offcanvas"]'; 398 var offcanvasCloseSelector = 'a.offcanvas-close, button.offcanvas-close'; 399 400 function getTextFromVisuallyHidden(el) { 401 var hiddenText = el ? el.querySelector(".visually-hidden") : null; 402 return hiddenText ? hiddenText.textContent.trim() : ""; 403 } 404 405 function getAltText(el) { 406 var img = el ? el.querySelector("img[alt]") : null; 407 return img ? img.getAttribute("alt").trim() : ""; 408 } 409 410 function setButtonLikeAnchor(el, fallbackLabel) { 411 if (!el || el.dataset.keyboardA11yNormalized === "1") return; 412 413 el.setAttribute("role", "button"); 414 el.setAttribute("tabindex", "0"); 415 416 if (!el.getAttribute("aria-label")) { 417 el.setAttribute( 418 "aria-label", 419 getTextFromVisuallyHidden(el) || getAltText(el) || fallbackLabel 420 ); 421 } 422 423 el.addEventListener("keydown", function(e) { 424 if (e.key !== "Enter" && e.key !== " ") return; 425 426 e.preventDefault(); 427 el.click(); 428 }); 429 430 el.dataset.keyboardA11yNormalized = "1"; 431 } 432 433 function normalizeSearchTrigger(trigger) { 434 if (!trigger) return; 435 436 if (trigger.getAttribute("aria-expanded") === "flase") { 437 trigger.setAttribute("aria-expanded", "false"); 438 } 439 440 if (!trigger.getAttribute("aria-controls")) { 441 trigger.setAttribute("aria-controls", "search-container"); 442 } 443 444 if (trigger.tagName === "A") { 445 setButtonLikeAnchor(trigger, "Open search"); 446 } 447 448 if (!trigger.getAttribute("aria-label")) { 449 trigger.setAttribute("aria-label", "Open search"); 450 } 451 } 452 453 function normalizeMobileMenuButton(button) { 454 if (!button) return; 455 456 button.setAttribute("type", "button"); 457 button.setAttribute("tabindex", "0"); 458 459 if (!button.getAttribute("aria-controls")) { 460 button.setAttribute("aria-controls", "global-menu
460-offcanvas"); 461 } 462 463 if (!button.getAttribute("aria-expanded")) { 464 button.setAttribute("aria-expanded", "false"); 465 } 466 467 if (!button.getAttribute("aria-label")) { 468 button.setAttribute( 469 "aria-label", 470 getTextFromVisuallyHidden(button) || "Open menu" 471 ); 472 } 473 474 // The current HTML has an <a> inside the <button>. 475 // Do not let the nested anchor become a competing interactive control. 476 button.querySelectorAll("a").forEach(function(innerLink) { 477 innerLink.removeAttribute("data-bs-toggle"); 478 innerLink.removeAttribute("data-bs-target"); 479 innerLink.removeAttribute("href"); 480 innerLink.setAttribute("tabindex", "-1"); 481 innerLink.setAttribute("aria-hidden", "true"); 482 innerLink.setAttribute("role", "presentation"); 483 }); 484 } 485 486 function normalizeOffcanvasClose(control) { 487 if (!control) return; 488 489 if (control.tagName === "A") { 490 setButtonLikeAnchor(control, "Close menu"); 491 } 492 493 if (!control.getAttribute("aria-label")) { 494 control.setAttribute("aria-label", "Close menu"); 495 } 496 } 497 498 function normalizeAll(root) { 499 var context = root || document; 500 501 if (context.matches && context.matches(searchTriggerSelector)) { 502 normalizeSearchTrigger(context); 503 } 504 505 if (context.matches && context.matches(mobileMenuButtonSelector)) { 506 normalizeMobileMenuButton(context); 507 } 508 509 if (context.matches && context.matches(offcanvasCloseSelector)) { 510 normalizeOffcanvasClose(context); 511 } 512 513 if (!context.querySelectorAll) return; 514 515 context.querySelectorAll(searchTriggerSelector).forEach(normalizeSearchTrigger); 516 context.querySelectorAll(mobileMenuButtonSelector).forEach(normalizeMobileMenuButton); 517 context.querySelectorAll(offcanvasCloseSelector).forEach(normalizeOffcanvasClose); 518 } 519 520 normalizeAll(document); 521 522 new MutationObserver(function(mutations) { 523 mutations.forEach(function(mutation) { 524 mutation.addedNodes.forEach(function(node) { 525 if (node.nodeType !== 1) return; 526 normalizeAll(node); 527 }); 528 }); 529 }).observe(document.body, { 530 childList: true, 531 subtree: true 532 }); 533 534 var searchContainer = document.getElementById("search-container"); 535 var globalMenuOffcanvas = document.getElementById("global-menu-offcanvas"); 536 537 if (searchContainer) { 538 searchContainer.addEventListener("shown.bs.collapse", function() { 539 document.querySelectorAll(searchTriggerSelector).forEach(function(trigger) { 540 trigger.setAttribute("aria-expanded", "true"); 541 trigger.setAttribute("aria-label", "Close search"); 542 }); 543 }); 544 545 searchContainer.addEventListener("hidden.bs.collapse", function() { 546 document.querySelectorAll(searchTriggerSelector).forEach(function(trigger) { 547 trigger.setAttribute("aria-expanded", "false"); 548 trigger.setAttribute("aria-label", "Open search"); 549 }); 550 }); 551 } 552 553 if (globalMenuOffcanvas) { 554 globalMenuOffcanvas.addEventListener("shown.bs.offcanvas", function() { 555 document.querySelectorAll(mobileMenuButtonSelector).forEach(function(button) { 556 button.setAttribute("aria-expanded", "true"); 557 button.setAttribute("aria-label", "Close menu"); 558 }); 559 }); 560 561 globalMenuOffcanvas.addEventListener("hidden.bs.offcanvas", function() { 562 document.querySelectorAll(mobileMenuButtonSelector).forEach(function(button) { 563 button.setAttribute("aria-expanded", "false"); 564 button.setAttribute("aria-label", "Open menu"); 565 }); 566 }); 567 } 568 } 569 570 571 572 // ===================================================== 573 // SEARCH UI 574 // Consolidates: 575 // - href stripping for search triggers 576 // - hash cleanup 577 // - icon swap 578 // - body lock / scrollbar compensation 579 // - input focus 580 // - mobile search from offcanvas 581 // ===================================================== 582 function initSearchUI() { 583 var searchCollapse = document.getElementById("search-container"); 584 if (!searchCollapse || !window.bootstrap || !bootstrap.Collapse) return; 585 586 var inputEl = document.querySelector("#input-cse input"); 587 var desktopSearchIcon = document.getElementById("desktop-search-icon"); 588 var defaultSearchIcon = document.getElementById("default-search-1"); 589 var triggerSelector = '[data-bs-toggle="collapse"][data-bs-target="#search-container"]'; 590 var currentSearchAnimation = null; 591 592 function getScrollbarWidth() { 593 return window.innerWidth - document.documentElement.clientWidth; 594 } 595 596 function removeSearchHash() { 597 if (window.location.hash === "#search-container") { 598 history.replaceState(null, "", window.location.pathname + window.location.search); 599 } 600 } 601 602 function cancelSearchAnimation() { 603 if (currentSearchAnimation) { 604 currentSearchAnimation.cancel(); 605 currentSearchAnimation = null; 606 }
607 searchCollapse.getAnimations().forEach(function(animation) { 608 animation.cancel(); 609 }); 610 } 611 612 function setSearchIconState(isOpen) { 613 [desktopSearchIcon, defaultSearchIcon].forEach(function(icon) { 614 if (!icon || !icon.src) return; 615 616 icon.src = isOpen ? 617 icon.src.replace("search.svg", "search-X.svg") : 618 icon.src.replace("search-X.svg", "search.svg"); 619 }); 620 } 621 622 function lockBody() { 623 document.body.classList.add("no-scroll"); 624 document.body.style.paddingRight = getScrollbarWidth() + "px"; 625 } 626 627 function unlockBody() { 628 document.body.classList.remove("no-scroll"); 629 document.body.style.paddingRight = ""; 630 } 631 632 function normalizeSearchTriggers(root) { 633 var context = root || document; 634 635 if (context.matches && context.matches(triggerSelector)) { 636 if ( 637 context.tagName === "A" && 638 context.getAttribute("href") === "#search-container" 639 ) { 640 context.removeAttribute("href"); 641 } 642 context.dataset.searchNormalized = "1"; 643 } 644 645 if (!context.querySelectorAll) return; 646 647 context.querySelectorAll(triggerSelector).forEach(function(trigger) { 648 if (trigger.dataset.searchNormalized === "1") return; 649 650 if ( 651 trigger.tagName === "A" && 652 trigger.getAttribute("href") === "#search-container" 653 ) { 654 trigger.removeAttribute("href"); 655 } 656 657 trigger.dataset.searchNormalized = "1"; 658 }); 659 } 660 661 normalizeSearchTriggers(); 662 663 new MutationObserver(function(mutations) { 664 mutations.forEach(function(mutation) { 665 mutation.addedNodes.forEach(function(node) { 666 if (node.nodeType !== 1) return; 667 normalizeSearchTriggers(node); 668 }); 669 }); 670 }).observe(document.body, { 671 childList: true, 672 subtree: true 673 }); 674 675 // Only special-case the mobile search button inside an open offcanvas. 676 // All other search triggers are left to Bootstrap's normal data API. 677 document.addEventListener( 678 "click", 679 function(e) { 680 var trigger = e.target.closest(triggerSelector); 681 if (!trigger) return; 682 683 if (trigger.id !== "mobile-search") return; 684 685 var offcanvasEl = trigger.closest(".offcanvas"); 686 if (!offcanvasEl || !offcanvasEl.classList.contains("show")) return; 687 688 e.preventDefault(); 689 e.stopPropagation(); 690 691 offcanvasEl.addEventListener( 692 "hidden.bs.offcanvas", 693 function onHidden() { 694 bootstrap.Collapse.getOrCreateInstance(searchCollapse).show(); 695 }, { 696 once: true 697 } 698 ); 699 700 bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl).hide(); 701 }, 702 true 703 ); 704 705 searchCollapse.addEventListener("show.bs.collapse", function() { 706 closeOpenBootstrapUi([searchCollapse]); 707 cancelSearchAnimation(); 708 searchCollapse.style.opacity = "0"; 709 lockBody(); 710 removeSearchHash(); 711 }); 712 713 searchCollapse.addEventListener("shown.bs.collapse", function() { 714 cancelSearchAnimation(); 715 setSearchIconState(true); 716 removeSearchHash(); 717 718 currentSearchAnimation = searchCollapse.animate( 719 [{ 720 opacity: 0 721 }, { 722 opacity: 1 723 }], { 724 duration: 250, 725 easing: "ease" 726 } 727 ); 728 729 currentSearchAnimation.onfinish = function() { 730 searchCollapse.style.opacity = "1"; 731 currentSearchAnimation = null; 732 }; 733 734 if (inputEl) { 735 setTimeout(function() { 736 inputEl.value = ""; 737 inputEl.focus(); 738 inputEl.select(); 739 }, 75); 740 } 741 }); 742 743 searchCollapse.addEventListener("hide.bs.collapse", function() { 744 cancelSearchAnimation(); 745 searchCollapse.style.opacity = "1"; 746 setSearchIconState(false); 747 }); 748 749 searchCollapse.addEventListener("hidden.bs.collapse", function() { 750 cancelSearchAnimation(); 751 searchCollapse.style.opacity = "0"; 752 unlockBody(); 753 removeSearchHash(); 754 }); 755 } 756 757 // ===================================================== 758 // HIDDEN RIBBON ICON SWAP 759 // ===================================================== 760 function initHiddenRibbonIconSwap() { 761 var hiddenRibbonCollapse = document.getElementById("hidden-ribbon"); 762 if (!hiddenRibbonCollapse) return; 763 764 ["ribbon.svg", "ribbon-open.svg"].forEach(function(name) { 765 var img = new Image();
766 img.src = "https://www.uwo.ca/web_standards/2023-hp/assets/img/assets/icons/" + name; 767 }); 768 769 function bindImage(popularLinksImage) { 770 if (!popularLinksImage || popularLinksImage.dataset.ribbonBound === "1") return; 771 popularLinksImage.dataset.ribbonBound = "1"; 772 773 hiddenRibbonCollapse.addEventListener("shown.bs.collapse", function() { 774 if (popularLinksImage.src.includes("ribbon-open.svg")) { 775 popularLinksImage.src = "https://www.uwo.ca/web_standards/2023-hp/assets/img/assets/icons/ribbon.svg"; 776 } 777 }); 778 779 hiddenRibbonCollapse.addEventListener("hidden.bs.collapse", function() { 780 if (popularLinksImage.src.includes("ribbon.svg") && !popularLinksImage.src.includes("ribbon-open.svg")) { 781 popularLinksImage.src = "https://www.uwo.ca/web_standards/2023-hp/assets/img/assets/icons/ribbon-open.svg"; 782 } 783 }); 784 } 785 786 var existingImage = document.getElementById("popular-links-image"); 787 if (existingImage) bindImage(existingImage); 788 789 new MutationObserver(function() { 790 var popularLinksImage = document.getElementById("popular-links-image"); 791 if (popularLinksImage) bindImage(popularLinksImage); 792 }).observe(document.body, { 793 childList: true, 794 subtree: true 795 }); 796 } 797 798 // ===================================================== 799 // LEFT NAV DISABLED LINK STYLING 800 // ===================================================== 801 function initLeftNavDisabledLinkStyling() { 802 document.querySelectorAll("#leftNavSource a.disabled").forEach(function(link) { 803 var span = link.querySelector("span"); 804 if (!span) return; 805 span.classList.remove("bs-book"); 806 span.classList.add("bs-medium"); 807 }); 808 } 809 810 // ===================================================== 811 // VIDEO ACCORDION SAFETY 812 // ===================================================== 813 function initVideoAccordionSafety() { 814 document.querySelectorAll(".accordion iframe").forEach(function(iframe) { 815 var videoSrc = iframe.src || ""; 816 if (!videoSrc.includes("youtube.com") && !videoSrc.includes("panopto.com")) return; 817 818 var accordionItem = iframe.closest(".accordion-collapse"); 819 if (!accordionItem) return; 820 821 accordionItem.addEventListener("hide.bs.collapse", function(event) { 822 if (document.fullscreenElement) { 823 event.preventDefault(); 824 } 825 }); 826 827 accordionItem.addEventListener("hidden.bs.collapse", function() { 828 pauseVideo(iframe); 829 }); 830 831 document.addEventListener("fullscreenchange", function() { 832 if (!document.fullscreenElement && !accordionItem.classList.contains("show")) { 833 pauseVideo(iframe); 834 } 835 }); 836 }); 837 838 function pauseVideo(iframe) { 839 var src = iframe.src; 840 iframe.src = ""; 841 setTimeout(function() { 842 iframe.src = src; 843 }, 200); 844 } 845 } 846 847 // ===================================================== 848 // RESPONSIVE MEDIA EMBEDS 849 // Wrap supported video iframes / HTML5 video in Bootstrap ratio containers 850 // while excluding homepage hero, wu-youtube sections, and social modal media 851 // ===================================================== 852 function initResponsiveMediaEmbeds() { 853 var supportedRatioClasses = ["ratio-21x9", "ratio-16x9", "ratio-4x3", "ratio-1x1"]; 854 var requiredWrapperClasses = ["ratio", "mb-3"]; 855 var defaultRatioClass = "ratio-16x9"; 856 var responsiveVideoBlockClass = "responsive-video-block"; 857 858 function isVideoIframe(el) { 859 if (!el || el.tagName.toLowerCase() !== "iframe") return false; 860 861 var src = (el.getAttribute("src") || "").toLowerCase(); 862 863 return ( 864 src.indexOf("youtube.com/embed/") !== -1 || 865 src.indexOf("youtube-nocookie.com/embed/") !== -1 || 866 src.indexOf("player.vimeo.com/video/") !== -1 || 867 src.indexOf("fast.wistia.net/embed/") !== -1 || 868 src.indexOf("wistia.net/embed/") !== -1 || 869 src.indexOf("players.brightcove.net/") !== -1 || 870 src.indexOf("kaltura.com/") !== -1 || 871 src.indexOf("panopto.com/panopto/pages/embed.aspx") !== -1 || 872 src.indexOf("stream.office.com/embed/") !== -1 || 873 src.indexOf("microsoftstream.com/") !== -1 874 ); 875 } 876 877 function isHtml5Video(el) { 878 return el && el.tagName && el.tagName.toLowerCase() === "video"; 879 } 880 881 function isResponsiveMedia(el) { 882 return isVideoIframe(el) || isHtml5Video(el); 883 } 884 885 function isExcludedHomepageHero(el) { 886 return ( 887 document.body.classList.contains("wu-homepage") && 888 el && 889 el.closest("#hp-video-hero") 890 ); 891 } 892 893 function isExcludedWuYoutubeSection(el) { 894 return el && el.closest("section.wu-section.section-capped.wu-youtube"); 895 } 896 897 function isExcludedSocialModal(el) { 898 return el && el.closest("#social-modals"); 899 } 900 901 function isExcludedMediaArea(el) { 902 return ( 903 isExcludedHomepageHero(el) || 904 isExcludedWuYoutubeSection(el) || 905 isExcludedSocialModal(el) 906 ); 907 } 908 909 function getRatioClass(wrapper) { 910 if (!wrapper) return null; 911 912 for (var i = 0; i < supportedRatioClasses.length; i++) { 913 if (wrapper.classList.contains(supportedRatioClasses[i])) { 914 return supportedRatioClasses[i]; 915 } 916 } 917 918 return null; 919 } 920 921 function isValidRatioWrapper(wrapper) { 922 return !!( 923 wrapper && 924 wrapper.classList && 925 wrapper.classList.contains("ratio") && 926 getRatioClass(wrapper) 927 ); 928 } 929 930 function getClosestValidRatioWrapper(el) { 931 var current = el ? el.parentElement : null; 932 933 while (current) { 934 if (isValidRatioWrapper(current)) { 935 return current; 936 } 937 current = current.parentElement; 938 } 939 940 return null; 941 } 942 943 function applyRequiredWrapperClasses(wrapper) { 944 var i; 945 946 if (!wrapper) return; 947 948 for (i = 0; i < requiredWrapperClasses.length; i++) { 949 wrapper.classList.add(requiredWrapperClasses[i]); 950 } 951 952 if (!getRatioClass(wrapper)) { 953 wrapper.classList.add(defaultRatioClass); 954 } 955 } 956 957 function createWrapperFromClassName(className) { 958 var wrapper = document.createElement("div"); 959 wrapper.className = className || ""; 960 applyRequiredWrapperClasses(wrapper); 961 return wrapper; 962 } 963 964 function createDefaultWrapper() { 965 var wrapper = document.createElement("div"); 966 applyRequiredWrapperClasses(wrapper); 967 wrapper.classList.add(defaultRatioClass); 968 return wrapper; 969 } 970 971 function isMeaningfulTextNode(node) { 972 return node && node.nodeType === 3 && /\S/.test(node.textContent || ""); 973 } 974 975 function isDirectResponsiveMediaNode(node) { 976 return node && node.nodeType === 1 && isResponsiveMedia(node); 977 } 978 979 function isNonMediaContentNode(node) { 980 if (!node) return false; 981 982 if (node.nodeType === 1) { 983 return !isResponsiveMedia(node); 984 } 985 986 if (node.nodeType === 3) { 987 return isMeaningfulTextNode(node); 988 } 989 990 return false; 991 } 992 993 function splitMultiMediaWrapper(wrapper) { 994 var mediaItems = Array.prototype.filter.call( 995 wrapper.children, 996 function(child) { 997 return isResponsiveMedia(child) && !isExcludedMediaArea(child); 998 } 999 ); 1000 1001 if (mediaItems.length <= 1) return; 1002 1003 applyRequiredWrapperClasses(wrapper); 1004 1005 var insertAfterNode = wrapper; 1006 var originalClassName = wrapper.className; 1007
1008 mediaItems.forEach(function(media, index) { 1009 if (index === 0) return; 1010 1011 var newWrapper = createWrapperFromClassName(originalClassName); 1012 insertAfterNode.parentNode.insertBefore(newWrapper, insertAfterNode.nextSibling); 1013 newWrapper.appendChild(media); 1014 insertAfterNode = newWrapper; 1015 }); 1016 } 1017 1018 function rebuildWrapperIntoResponsiveBlock(wrapper) { 1019 var childNodes = Array.prototype.slice.call(wrapper.childNodes); 1020 var parent = wrapper.parentNode; 1021 var originalClassName = wrapper.className; 1022 var block = document.createElement("div"); 1023 var mediaCount = 0; 1024 1025 if (!parent) return; 1026 1027 block.className = responsiveVideoBlockClass; 1028 1029 parent.insertBefore(block, wrapper); 1030 parent.removeChild(wrapper); 1031 1032 while (wrapper.firstChild) { 1033 wrapper.removeChild(wrapper.firstChild); 1034 } 1035 1036 childNodes.forEach(function(node) { 1037 if (isDirectResponsiveMediaNode(node) && !isExcludedMediaArea(node)) { 1038 var mediaWrapper; 1039 1040 if (mediaCount === 0) { 1041 mediaWrapper = wrapper; 1042 mediaWrapper.className = originalClassName; 1043 applyRequiredWrapperClasses(mediaWrapper); 1044 } else { 1045 mediaWrapper = createWrapperFromClassName(originalClassName); 1046 } 1047 1048 mediaWrapper.appendChild(node); 1049 block.appendChild(mediaWrapper); 1050 mediaCount++; 1051 } else if (isNonMediaContentNode(node)) { 1052 block.appendChild(node); 1053 } 1054 }); 1055 1056 if (mediaCount === 0) { 1057 while (block.firstChild) { 1058 parent.insertBefore(block.firstChild, block); 1059 } 1060 parent.removeChild(block); 1061 } 1062 } 1063 1064 function processExistingRatioWrappers(context) { 1065 var scope = context || document; 1066 var wrappers = scope.querySelectorAll(".ratio"); 1067 1068 wrappers.forEach(function(wrapper) { 1069 if (!isValidRatioWrapper(wrapper)) return; 1070 if (isExcludedMediaArea(wrapper)) return; 1071 1072 var directMediaChildren = Array.prototype.filter.call( 1073 wrapper.children, 1074 function(child) { 1075 return isResponsiveMedia(child) && !isExcludedMediaArea(child); 1076 } 1077 ); 1078 1079 if (!directMediaChildren.length) return; 1080 1081 var hasNonMediaContent = Array.prototype.some.call( 1082 wrapper.childNodes, 1083 function(node) { 1084 return isNonMediaContentNode(node); 1085 } 1086 ); 1087 1088 if (hasNonMediaContent) { 1089 rebuildWrapperIntoResponsiveBlock(wrapper); 1090 return; 1091 } 1092 1093 if (directMediaChildren.length > 1) { 1094 splitMultiMediaWrapper(wrapper); 1095 return; 1096 } 1097 1098 applyRequiredWrapperClasses(wrapper); 1099 }); 1100 } 1101 1102 function wrapUnwrappedMedia(context) { 1103 var scope = context || document; 1104 var mediaItems = scope.querySelectorAll("iframe, video"); 1105 1106 mediaItems.forEach(function(media) { 1107 if (!isResponsiveMedia(media)) return; 1108 if (isExcludedMediaArea(media)) return; 1109 1110 var existingWrapper = getClosestValidRatioWrapper(media); 1111 1112 if (existingWrapper) { 1113 applyRequiredWrapperClasses(existingWrapper); 1114 return; 1115 } 1116 1117 if (!media.parentNode) return; 1118 1119 var wrapper = createDefaultWrapper(); 1120 media.parentNode.insertBefore(wrapper, media); 1121 wrapper.appendChild(media); 1122 }); 1123 } 1124 1125 function normalizeResponsiveMedia(context) { 1126 processExistingRatioWrappers(context); 1127 wrapUnwrappedMedia(context); 1128 } 1129 1130 normalizeResponsiveMedia(document); 1131 } 1132 1133 // ===================================================== 1134 // TABLES 1135 // ===================================================== 1136 function makeTablesResponsive() { 1137 document.querySelectorAll("table").forEach(function(table) { 1138 if (!table.classList.contains("table")) { 1139 table.classList.add("table"); 1140 } 1141 1142 if (table.classList.contains("table-responsive")) { 1143 table.classList.remove("table-responsive"); 1144 } 1145 1146 if (!table.parentElement || !table.parentElement.classList.contains("table-responsive")) { 1147 var wrapper = document.createElement("div"); 1148 wrapper.classList.add("table-responsive"); 1149 table.parentNode.insertBefore(wrapper, table); 1150 wrapper.appendChild(table); 1151 } 1152 }); 1153 } 1154 1155 // ===================================================== 1156 // PDF LINKS 1157 // ===================================================== 1158 function setPdfLinksToBlankSafe() { 1159 document.querySelectorAll('a[href$=".pdf"]').forEach(function(link) { 1160 link.setAttribute("target", "_blank"); 1161 link.setAttribute("rel", "noopener noreferrer"); 1162 }); 1163 } 1164 1165 // ===================================================== 1166 // MATCH HEIGHTS 1167 // ===================================================== 1168 function initMatchHeights() { 1169 function matchHeightsInContainer(containerSelector, elementClass) { 1170 var containers = document.querySelectorAll(containerSelector); 1171
1172 containers.forEach(function(container) { 1173 var elements = container.querySelectorAll(elementClass); 1174 if (!elements.length) return; 1175 1176 elements.forEach(function(element) { 1177 element.style.height = "auto"; 1178 }); 1179 1180 var maxHeight = 0; 1181 elements.forEach(function(element) { 1182 maxHeight = Math.max(maxHeight, element.offsetHeight); 1183 }); 1184 1185 elements.forEach(function(element) { 1186 element.style.height = maxHeight + "px"; 1187 }); 1188 }); 1189 } 1190 1191 function adjustHeights() { 1192 matchHeightsInContainer(".social-group", ".card-body"); 1193 matchHeightsInContainer(".western-card", ".card-body"); 1194 matchHeightsInContainer(".western-card", ".card-footer"); 1195 matchHeightsInContainer(".factoid-container", ".factoid"); 1196 matchHeightsInContainer(".call-out-container", ".call-out"); 1197 matchHeightsInContainer(".cta-button-group", ".btn-western-purple-light"); 1198 } 1199 1200 adjustHeights(); 1201 window.addEventListener("resize", adjustHeights); 1202 window.addEventListener("orientationchange", adjustHeights); 1203 } 1204 1205 // ===================================================== 1206 // HERO VIDEO 1207 // ===================================================== 1208 function initHeroVideo() { 1209 var heroVideos = document.querySelectorAll(".hero-video"); 1210 1211 for (var i = 0; i < heroVideos.length; i++) { 1212 setupHeroVideo(heroVideos[i]); 1213 } 1214 1215 function setupHeroVideo(heroVideo) { 1216 var playButton = heroVideo.querySelector(".hero-play-btn"); 1217 var videoElement = heroVideo.querySelector("video"); 1218 1219 if (!playButton || !videoElement) return; 1220 1221 function updateButtonState(isPaused) { 1222 if (isPaused) { 1223 playButton.classList.add("paused"); 1224 playButton.setAttribute("aria-pressed", "true"); 1225 playButton.setAttribute("aria-label", "Play hero video"); 1226 } else { 1227 playButton.classList.remove("paused"); 1228 playButton.setAttribute("aria-pressed", "false"); 1229 playButton.setAttribute("aria-label", "Pause hero video"); 1230 } 1231 } 1232 1233 function toggleVideoPlayback(e) { 1234 e.preventDefault(); 1235 e.stopPropagation(); 1236 1237 if (videoElement.paused) { 1238 videoElement.play(); 1239 updateButtonState(false); 1240 } else { 1241 videoElement.pause(); 1242 updateButtonState(true); 1243 } 1244 } 1245 1246 playButton.addEventListener("click", toggleVideoPlayback); 1247 1248 videoElement.addEventListener("pause", function() { 1249 updateButtonState(true); 1250 }); 1251 1252 videoElement.addEventListener("play", function() { 1253 updateButtonState(false); 1254 }); 1255 1256 updateButtonState(videoElement.paused); 1257 } 1258 } 1259 1260 1261 // ===================================================== 1262 // MOBILE LEFT-HAND NAV CLONE 1263 // ===================================================== 1264 function initLeftNavClone() { 1265 const sourceElement = document.getElementById("leftNavSource"); 1266 const destinationElement = document.getElementById("leftNavDestination"); 1267 1268 if (!sourceElement || !destinationElement) return; 1269 1270 const mobileWrapper = document.getElementById("left-nav-wrapper-mobile"); 1271 1272 destinationElement.innerHTML = sourceElement.innerHTML; 1273 1274 if (!mobileWrapper) return; 1275 1276 const legacyHeader = mobileWrapper.querySelector("h1.accordion-header"); 1277 1278 if (legacyHeader) { 1279 const replacement = document.createElement("p"); 1280 replacement.className = legacyHeader.className; 1281 replacement.classList.add("h1"); 1282 replacement.innerHTML = legacyHeader.innerHTML; 1283 legacyHeader.replaceWith(replacement); 1284 } 1285 1286 const accordionButton = mobileWrapper.querySelector('[data-bs-target="#left-nav-content"]'); 1287 1288 if (accordionButton && !accordionButton.classList.contains("collapsed")) { 1289 accordionButton.classList.add("collapsed"); 1290 accordionButton.setAttribute("aria-expanded", "false"); 1291 } 1292 } 1293 1294 // ===================================================== 1295 // GLOBAL MENU DROPDOWNS 1296 // ===================================================== 1297 function initGlobalMenuDropdowns() { 1298 function bindDropdowns() { 1299 document
1300 .querySelectorAll("#global-menu-nav .nav-item.dropdown .dropdown-toggle") 1301 .forEach(function(toggle) { 1302 if (toggle.dataset.dropdownBound === "1") return; 1303 toggle.dataset.dropdownBound = "1"; 1304 1305 toggle.addEventListener("click", function(event) { 1306 event.stopPropagation(); 1307 event.preventDefault(); 1308 1309 var targetUrl = toggle.getAttribute("href"); 1310 if (!targetUrl) return; 1311 1312 if (window.self !== window.top) { 1313 window.top.location.href = targetUrl; 1314 } else { 1315 window.location.href = targetUrl; 1316 } 1317 }); 1318 }); 1319 } 1320 1321 bindDropdowns(); 1322 1323 var targetNode = document.getElementById("global-menu-nav"); 1324 if (!targetNode) return; 1325 1326 new MutationObserver(function(mutations) { 1327 if ( 1328 mutations.some(function(mutation) { 1329 return mutation.type === "childList" && mutation.addedNodes.length > 0; 1330 }) 1331 ) { 1332 bindDropdowns(); 1333 } 1334 }).observe(targetNode, { 1335 childList: true, 1336 subtree: true 1337 }); 1338 } 1339 1340 // ===================================================== 1341 // MOBILE MAIN NAV ACTIVE / INACTIVE STATES 1342 // ===================================================== 1343 function initOffcanvasAccordionStates() { 1344 var accordion = document.getElementById("offcanvas-global-menu-accordion"); 1345 if (!accordion) return; 1346 1347 function setupAccordionListeners() { 1348 var buttons = accordion.querySelectorAll(".accordion-button"); 1349 var collapseElements = accordion.querySelectorAll(".accordion-collapse"); 1350 if (!buttons.length) return; 1351 1352 collapseElements.forEach(function(collapseElement, index) { 1353 var button = buttons[index]; 1354 if (!button || collapseElement.dataset.stateBound === "1") return; 1355 collapseElement.dataset.stateBound = "1"; 1356 1357 collapseElement.addEventListener("shown.bs.collapse", function() { 1358 buttons.forEach(function(btn) { 1359 btn.classList.remove("active", "inactive"); 1360 }); 1361 1362 button.classList.add("active"); 1363 1364 buttons.forEach(function(btn) { 1365 if (btn !== button) btn.classList.add("inactive"); 1366 }); 1367 }); 1368 1369 collapseElement.addEventListener("hidden.bs.collapse", function() { 1370 button.classList.remove("active"); 1371 1372 var anyOpen = Array.from(collapseElements).some(function(el) { 1373 return el.classList.contains("show"); 1374 }); 1375 1376 if (!anyOpen) { 1377 buttons.forEach(function(btn) { 1378 btn.classList.remove("inactive"); 1379 }); 1380 } 1381 }); 1382 }); 1383 } 1384 1385 setupAccordionListeners(); 1386 1387 new MutationObserver(function(mutations) { 1388 if ( 1389 mutations.some(function(mutation) { 1390 return mutation.type === "childList" && mutation.addedNodes.length > 0; 1391 }) 1392 ) { 1393 setupAccordionListeners(); 1394 } 1395 }).observe(accordion, { 1396 childList: true, 1397 subtree: true 1398 }); 1399 1400 document.addEventListener("click", function(event) { 1401 if (!event.target.closest("#offcanvas-global-menu-accordion .accordion-button")) { 1402 accordion.querySelectorAll(".accordion-button").forEach(function(btn) { 1403 btn.classList.remove("active", "inactive"); 1404 }); 1405 } 1406 }); 1407 } 1408 1409 // ===================================================== 1410 // PAGINATION 1411 // ===================================================== 1412 function initPagination() { 1413 var paginationNumbers = document.getElementById("pagination-numbers"); 1414 var paginatedList = document.getElementById("paginated-list"); 1415 var pageInfo = document.getElementById("page-info"); 1416 1417 if (!paginationNumbers || !paginatedList || !pageInfo) return; 1418 1419 var listItems = paginatedList.querySelectorAll(".col"); 1420 var paginationLimit = 10;
1421 var pageCount = Math.ceil(listItems.length / paginationLimit); 1422 var currentPage = 1; 1423 1424 if (!pageCount) { 1425 pageInfo.textContent = "Page 0 of 0"; 1426 return; 1427 } 1428 1429 var prevButton = document.createElement("li"); 1430 prevButton.className = "page-item"; 1431 prevButton.id = "prev-button"; 1432 prevButton.innerHTML = '<a class="page-link" href="#"><<</a>'; 1433 1434 var nextButton = document.createElement("li"); 1435 nextButton.className = "page-item"; 1436 nextButton.id = "next-button"; 1437 nextButton.innerHTML = '<a class="page-link" href="#">>></a>'; 1438 1439 function clampPage(pageNum) { 1440 return Math.max(1, Math.min(pageNum, pageCount)); 1441 } 1442 1443 function appendPageNumber(index) { 1444 var pageNumber = document.createElement("li"); 1445 pageNumber.className = "page-item"; 1446 pageNumber.setAttribute("page-index", index); 1447 pageNumber.innerHTML = '<a class="page-link" href="#">' + index + "</a>"; 1448 paginationNumbers.appendChild(pageNumber); 1449 } 1450 1451 function renderPaginationNumbers() { 1452 paginationNumbers.innerHTML = ""; 1453 paginationNumbers.appendChild(prevButton); 1454 1455 for (var i = 1; i <= pageCount; i++) { 1456 appendPageNumber(i); 1457 } 1458 1459 paginationNumbers.appendChild(nextButton); 1460 } 1461 1462 function handlePageButtonsStatus() { 1463 prevButton.classList.toggle("disabled", currentPage === 1); 1464 nextButton.classList.toggle("disabled", currentPage === pageCount); 1465 } 1466 1467 function setCurrentPage(pageNum) { 1468 currentPage = clampPage(pageNum); 1469 1470 paginationNumbers.querySelectorAll(".page-item").forEach(function(button) { 1471 var pageIndex = Number(button.getAttribute("page-index")); 1472 button.classList.toggle("active", pageIndex === currentPage); 1473 }); 1474 1475 handlePageButtonsStatus(); 1476 pageInfo.textContent = "Page " + currentPage + " of " + pageCount; 1477 1478 var prevRange = (currentPage - 1) * paginationLimit; 1479 var currRange = currentPage * paginationLimit; 1480 1481 listItems.forEach(function(item, index) { 1482 item.classList.toggle("d-none", !(index >= prevRange && index < currRange)); 1483 }); 1484 1485 var wuNewsSection = document.querySelector("section.wu-news"); 1486 var header = document.querySelector("header"); 1487 var headerHeight = header ? header.offsetHeight : 0; 1488 1489 if (wuNewsSection) { 1490 window.scrollTo({ 1491 top: wuNewsSection.offsetTop - headerHeight, 1492 behavior: "smooth" 1493 }); 1494 } 1495 } 1496 1497 renderPaginationNumbers(); 1498 setCurrentPage(1); 1499 1500 prevButton.addEventListener("click", function(event) { 1501 event.preventDefault(); 1502 if (currentPage > 1) setCurrentPage(currentPage - 1); 1503 }); 1504 1505 nextButton.addEventListener("click", function(event) { 1506 event.preventDefault(); 1507 if (currentPage < pageCount) setCurrentPage(currentPage + 1); 1508 }); 1509 1510 paginationNumbers.addEventListener("click", function(event) { 1511 var pageItem = event.target.closest(".page-item"); 1512 if (!pageItem) return; 1513 1514 var pageIndex = Number(pageItem.getAttribute("page-index")); 1515 if (!pageIndex) return; 1516 1517 event.preventDefault(); 1518 setCurrentPage(pageIndex); 1519 }); 1520 } 1521 1522 // ===================================================== 1523 // HELPER FUNCTION FOR MISSING SLUGS IN DEFAULT ACCORDIONS 1524 // ===================================================== 1525 function ensureDefaultAccordionTargets() { 1526 document.querySelectorAll(".default-accordion .accordion-item").forEach(function(itemEl, idx) { 1527 var collapseEl = itemEl.querySelector(".accordion-collapse"); 1528 var btn = itemEl.querySelector(".accordion-button"); 1529 if (!collapseEl || !btn) return; 1530 1531 var hasValidId = typeof collapseEl.id === "string" && collapseEl.id.trim().length > 0;
1532 if (!hasValidId) { 1533 var itemId = itemEl.id && itemEl.id.trim() ? itemEl.id.trim() : "accordion-item-" + idx; 1534 collapseEl.id = itemId + "-panel"; 1535 } 1536 1537 btn.setAttribute("data-bs-target", "#" + collapseEl.id); 1538 btn.setAttribute("aria-controls", collapseEl.id); 1539 }); 1540 } 1541})(); 1542 1543// ===================================================== 1544// Smooth-Scroll system with sticky header offset & Bootstrap Accordion support 1545// - updates URL hash on collapse/open for accordion items 1546// ===================================================== 1547(function() { 1548 "use strict"; 1549 1550 var CONFIG = { 1551 HEADER_SELECTOR: "header.sticky-top", 1552 EXTRA_PAD_PX: 12, 1553 INITIAL_HASH_DELAY_MS: 80, 1554 RESIZE_REAPPLY_DEBOUNCE_MS: 150, 1555 ACCORDION_SCROLL_TO_TRIGGER: true, 1556 UPDATE_HASH_ON_COLLAPSE_OPEN: false, 1557 USE_REPLACE_STATE_FOR_HASH: true 1558 }; 1559 1560 function prefersReducedMotion() { 1561 return window.matchMedia("(prefers-reduced-motion: reduce)").matches; 1562 } 1563 1564 function getHeaderOffsetPx() { 1565 var header = document.querySelector(CONFIG.HEADER_SELECTOR); 1566 return header ? header.offsetHeight : 0; 1567 } 1568 1569 function getHashTarget(hash) { 1570 if (!hash || hash === "#") return null; 1571 1572 var id = decodeURIComponent(hash.slice(1)); 1573 var byId = document.getElementById(id); 1574 if (byId) return byId; 1575 1576 try { 1577 return document.querySelector(hash); 1578 } catch (e) { 1579 return null; 1580 } 1581 } 1582 1583 function setTemporaryTabIndexAndFocus(el) { 1584 if (!el || typeof el.focus !== "function") return; 1585 1586 var hadTabIndex = el.hasAttribute("tabindex"); 1587 if (!hadTabIndex) el.setAttribute("tabindex", "-1"); 1588 1589 el.focus({ 1590 preventScroll: true 1591 }); 1592 1593 if (!hadTabIndex) el.removeAttribute("tabindex"); 1594 } 1595 1596 function scrollToElement(el) { 1597 if (!el) return; 1598 1599 var extra = Number(el.getAttribute("data-anchor-offset") || 0) || 0; 1600 var offset = getHeaderOffsetPx() + CONFIG.EXTRA_PAD_PX + extra; 1601 var top = el.getBoundingClientRect().top + window.pageYOffset - offset; 1602 1603 window.scrollTo({ 1604 top: top, 1605 behavior: prefersReducedMotion() ? "auto" : "smooth" 1606 }); 1607 1608 setTemporaryTabIndexAndFocus(el); 1609 } 1610 1611 function findClosestCollapse(el) { 1612 if (!el || !el.closest) return null; 1613 if (el.classList && el.classList.contains("accordion-collapse")) return el; 1614 return el.closest(".accordion-collapse, .collapse"); 1615 } 1616 1617 function findTriggerForCollapse(collapseEl) { 1618 if (!collapseEl || !collapseEl.id) return null; 1619 1620 var safeId = window.CSS && CSS.escape ? CSS.escape(collapseEl.id) : collapseEl.id.replace(/([ #;?%&,.+*~\\':\"!^$\[\]()=>|\/@])/g, "\\$1"); 1621 var selector = '[data-bs-target="#' + safeId + '"]' + ', a[href="#' + safeId + '"]'; 1622 return document.querySelector(selector); 1623 } 1624 1625 function openCollapseIfNeeded(collapseEl) { 1626 if (!collapseEl || !window.bootstrap || !bootstrap.Collapse) { 1627 return { 1628 opened: false 1629 }; 1630 } 1631 1632 var isShown = collapseEl.classList.contains("show"); 1633 if (isShown) return { 1634 opened: false 1635 }; 1636 1637 bootstrap.Collapse.getOrCreateInstance(collapseEl).show(); 1638 return { 1639 opened: true 1640 }; 1641 } 1642 1643 function updateHash(hash) { 1644 if (!hash || hash === "#") return; 1645 if (window.location.hash === hash) return; 1646 1647 if (CONFIG.USE_REPLACE_STATE_FOR_HASH) { 1648 history.replaceState(null, "", hash); 1649 } else { 1650 history.pushState(null, "", hash); 1651 } 1652 } 1653 1654 function scrollToHash(hash) { 1655 var target = getHashTarget(hash); 1656 if (!target) return; 1657 1658 var collapseEl = findClosestCollapse(target); 1659 1660 if (!collapseEl) { 1661 scrollToElement(target); 1662 return; 1663 } 1664 1665 var result = openCollapseIfNeeded(collapseEl); 1666 1667 function doScroll() { 1668 if (CONFIG.ACCORDION_SCROLL_TO_TRIGGER && collapseEl.classList.contains("accordion-collapse")) { 1669 var trigger = findTriggerForCollapse(collapseEl); 1670 scrollToElement(trigger || target); 1671 } else { 1672 scrollToElement(target); 1673 } 1674 } 1675 1676 if (!result.opened) { 1677 doScroll(); 1678 return; 1679 } 1680 1681 collapseEl.addEventListener("shown.bs.collapse", doScroll, { 1682 once: true 1683 }); 1684 } 1685 1686 function isSameDocumentUrl(url) { 1687 if (!url.hash || url.hash === "#") return false; 1688 return ( 1689 url.origin === window.location.origin && 1690 url.pathname === window.location.pathname && 1691 url.search === window.location.search 1692 ); 1693 } 1694 1695 function interceptSamePageAnchorClicks() { 1696 document.querySelectorAll('a[href*="#"]').forEach(function(link) { 1697 var url; 1698 try { 1699 url = new URL(link.getAttribute("href"), window.location.href); 1700 } catch (e) { 1701 return; 1702 } 1703 1704 var isDownload = link.hasAttribute("download"); 1705 var isNewTab = (link.target || "").toLowerCase() === "_blank"; 1706 if (!isSameDocumentUrl(url) || isDownload || isNewTab) return; 1707 1708 link.addEventListener("click", function(e) { 1709 var target = getHashTarget(url.hash); 1710 if (!target) return; 1711 1712 e.preventDefault(); 1713 1714 if (window.location.hash !== url.hash) { 1715 history.pushState(null, "", url.hash); 1716 scrollToHash(url.hash); 1717 } else { 1718 scrollToHash(url.hash); 1719 } 1720 }); 1721 }); 1722 } 1723 1724 function handleInitialHash() { 1725 if (!window.location.hash) return; 1726 setTimeout(function() { 1727 scrollToHash(window.location.hash); 1728 }, CONFIG.INITIAL_HASH_DELAY_MS); 1729 } 1730 1731 function attachHashNavigationHandlers() { 1732 window.addEventListener("hashchange", function() { 1733 scrollToHash(window.location.hash); 1734 }); 1735 } 1736 1737 function attachResizeRepositionHandler() { 1738 var timer = null; 1739 1740 window.addEventListener("resize", function() { 1741 if (!window.location.hash) return; 1742 1743 clearTimeout(timer); 1744 timer = setTimeout(function() { 1745 var target = getHashTarget(window.location.hash); 1746 if (!target) return; 1747 1748 var collapseEl = findClosestCollapse(target); 1749 if (collapseEl) openCollapseIfNeeded(collapseEl); 1750 1751 scrollToHash(window.location.hash); 1752 }, CONFIG.RESIZE_REAPPLY_DEBOUNCE_MS);
1753 }); 1754 } 1755 1756 function attachDefaultAccordionHashSync() { 1757 if (!CONFIG.UPDATE_HASH_ON_COLLAPSE_OPEN) return; 1758 1759 function isDefaultAccordionCollapse(collapseEl) { 1760 return !!collapseEl && !!collapseEl.closest && !!collapseEl.closest(".default-accordion") && collapseEl.classList.contains("accordion-collapse"); 1761 } 1762 1763 function setHashToCollapse(collapseEl) { 1764 if (!collapseEl || !collapseEl.id) return; 1765 updateHash("#" + collapseEl.id); 1766 } 1767 1768 function clearHashFully() { 1769 var url = new URL(window.location.href); 1770 url.hash = ""; 1771 history.replaceState(null, "", url.toString()); 1772 } 1773 1774 document.addEventListener( 1775 "shown.bs.collapse", 1776 function(e) { 1777 var collapseEl = e.target; 1778 if (!isDefaultAccordionCollapse(collapseEl)) return; 1779 setHashToCollapse(collapseEl); 1780 }, 1781 true 1782 ); 1783 1784 document.addEventListener( 1785 "hidden.bs.collapse", 1786 function(e) { 1787 var closedEl = e.target; 1788 if (!isDefaultAccordionCollapse(closedEl)) return; 1789 if (decodeURIComponent(window.location.hash) !== "#" + closedEl.id) return; 1790 1791 var accordionEl = closedEl.closest(".accordion"); 1792 if (!accordionEl) { 1793 clearHashFully(); 1794 return; 1795 } 1796 1797 var openSibling = accordionEl.querySelector(".accordion-collapse.show"); 1798 if (openSibling && openSibling.id) { 1799 setHashToCollapse(openSibling); 1800 } else { 1801 clearHashFully(); 1802 } 1803 }, 1804 true 1805 ); 1806 } 1807 1808 document.addEventListener("DOMContentLoaded", function() { 1809 interceptSamePageAnchorClicks(); 1810 attachHashNavigationHandlers(); 1811 attachResizeRepositionHandler(); 1812 attachDefaultAccordionHashSync(); 1813 handleInitialHash(); 1814 }); 1815})(); 1816 1817// Stops default behaviour when closing mobile menu 1818function initOffcanvasCloseControls() { 1819 function normalizeOffcanvasCloseControls(root) { 1820 var context = root || document; 1821 1822 function processControl(control) { 1823 if (!control || control.dataset.offcanvasCloseNormalized === "1") return; 1824 1825 var isAnchor = control.tagName === "A"; 1826 var dismissChild = control.querySelector('[data-bs-dismiss="offcanvas"]'); 1827 1828 // If old structure has data-bs-dismiss on child image, move/copy it to parent control 1829 if ( 1830 dismissChild && 1831 dismissChild !== control && 1832 !control.hasAttribute("data-bs-dismiss") 1833 ) { 1834 control.setAttribute("data-bs-dismiss", "offcanvas"); 1835 } 1836 1837 // Old anchor structure: prevent # from being added to the URL 1838 if (isAnchor && control.getAttribute("href") === "#") { 1839 control.removeAttribute("href"); 1840 control.setAttribute("role", "button"); 1841 } 1842 1843 // Optional accessibility cleanup 1844 if (!control.getAttribute("aria-label")) { 1845 var labelledChild = control.querySelector("[aria-label]"); 1846 if (labelledChild) { 1847 control.setAttribute("aria-label", labelledChild.getAttribute("aria-label")); 1848 } 1849 } 1850 1851 control.dataset.offcanvasCloseNormalized = "1"; 1852 } 1853 1854 if ( 1855 context.matches && 1856 context.matches("a.offcanvas-close, button.offcanvas-close") 1857 ) { 1858 processControl(context); 1859 } 1860 1861 if (!context.querySelectorAll) return; 1862 1863 context 1864 .querySelectorAll("a.offcanvas-close, button.offcanvas-close") 1865 .forEach(processControl); 1866 } 1867 1868 normalizeOffcanvasCloseControls(); 1869 1870 // Covers dynamically inserted/re-rendered offcanvas markup too 1871 new MutationObserver(function(mutations) { 1872 mutations.forEach(function(mutation) {
1873 mutation.addedNodes.forEach(function(node) { 1874 if (node.nodeType !== 1) return; 1875 normalizeOffcanvasCloseControls(node); 1876 }); 1877 }); 1878 }).observe(document.body, { 1879 childList: true, 1880 subtree: true 1881 }); 1882 1883 // Safety net for any legacy anchors that still somehow keep href="#" 1884 document.addEventListener( 1885 "click", 1886 function(e) { 1887 var closeLink = e.target.closest("a.offcanvas-close"); 1888 if (!closeLink) return; 1889 1890 if (closeLink.getAttribute("href") === "#") { 1891 e.preventDefault(); 1892 } 1893 }, 1894 true 1895 ); 1896} 1897 1898// ===================================================== 1899// ACCORDION GUARD (platform-agnostic) 1900// Keep panels in .default-accordion from auto-closing 1901// on scroll/resize/blur across iOS + Android. 1902// Allows only user-intended closes (header click/keyboard). 1903// ===================================================== 1904document.addEventListener("DOMContentLoaded", function() { 1905 var CONTAINER = ".default-accordion"; 1906 1907 // Only run this guard on touch / mobile-like devices 1908 var isTouchLike = 1909 window.matchMedia("(pointer: coarse)").matches || 1910 navigator.maxTouchPoints > 0; 1911 1912 if (!isTouchLike) return; 1913 1914 var lastIntentTs = 0; 1915 var intentWindowMs = 450; 1916 1917 function markIntentIfHeader(el) { 1918 if (el && el.closest && el.closest(CONTAINER + " .accordion-button")) { 1919 lastIntentTs = Date.now(); 1920 } 1921 } 1922 1923 document.addEventListener( 1924 "pointerdown", 1925 function(e) { 1926 markIntentIfHeader(e.target); 1927 }, 1928 true 1929 ); 1930 1931 document.addEventListener( 1932 "touchstart", 1933 function(e) { 1934 markIntentIfHeader(e.target); 1935 }, { 1936 passive: true, 1937 capture: true 1938 } 1939 ); 1940 1941 document.addEventListener( 1942 "keydown", 1943 function(e) { 1944 if ((e.key === "Enter" || e.key === " ") && e.target) { 1945 markIntentIfHeader(e.target); 1946 } 1947 if (e.key === "Escape" && e.target && e.target.closest(CONTAINER)) { 1948 lastIntentTs = Date.now(); 1949 } 1950 }, 1951 true 1952 ); 1953 1954 var openingByAccordion = new WeakMap(); 1955 1956 document.addEventListener( 1957 "show.bs.collapse", 1958 function(e) { 1959 var collapseEl = e.target; 1960 if (!collapseEl.closest(CONTAINER)) return; 1961 1962 var accordionEl = collapseEl.closest(".accordion"); 1963 if (accordionEl) openingByAccordion.set(accordionEl, collapseEl); 1964 }, 1965 true 1966 ); 1967 1968 document.addEventListener( 1969 "shown.bs.collapse", 1970 function(e) { 1971 var collapseEl = e.target; 1972 if (!collapseEl.closest(CONTAINER)) return; 1973 1974 var accordionEl = collapseEl.closest(".accordion"); 1975 if (accordionEl) openingByAccordion.delete(accordionEl); 1976 }, 1977 true 1978 ); 1979 1980 document.addEventListener( 1981 "hide.bs.collapse", 1982 function(e) { 1983 var collapseEl = e.target; 1984 if (!collapseEl.closest(CONTAINER)) return; 1985 1986 var accordionEl = collapseEl.closest(".accordion"); 1987 if (accordionEl) { 1988 var opening = openingByAccordion.get(accordionEl); 1989 if (opening && opening !== collapseEl) return; 1990 } 1991 1992 var justUserIntended = Date.now() - lastIntentTs < intentWindowMs; 1993 if (!justUserIntended) { 1994 e.preventDefault(); 1995 } 1996 }, 1997 true 1998 ); 1999}); 2000 2001// ===================================================== 2002// Normalize absolute uwo.ca links to https://www.uwo.ca/.../index.html 2003// ===================================================== 2004(function() { 2005 "use strict"; 2006 2007 var DEBUG = false; 2008 var SKIP_RE = /^(mailto:|tel:|javascript:|#)/i; 2009 var UWO_RE = /^https?:\/\/(?:www\.)?uwo\.ca(\/|$)/i; 2010 2011 function isFolderPath(pathname) { 2012 if (pathname === "/" || pathname.endsWith("/")) return true; 2013 var last = pathname.split("/").pop() || ""; 2014 return !last.includes("."); 2015 } 2016 2017 function addIndexHtml(urlObj) { 2018 if (urlObj.pathname.endsWith("/index.html")) return; 2019 2020 if (urlObj.pathname === "/") { 2021 urlObj.pathname = "/index.html"; 2022 return; 2023 } 2024 2025 if (urlObj.pathname.endsWith("/")) { 2026 urlObj.pathname = urlObj.pathname + "index.html"; 2027 return; 2028 } 2029 2030 urlObj.pathname = urlObj.pathname + "/index.html"; 2031 } 2032 2033 function normalizeUwoHref(rawHref) { 2034 if (!rawHref) return null; 2035 if (SKIP_RE.test(rawHref)) return null; 2036 if (!UWO_RE.test(rawHref)) return null; 2037 2038 var u = new URL(rawHref); 2039 u.protocol = "https:"; 2040 u.hostname = "www.uwo.ca"; 2041 2042 if (isFolderPath(u.pathname)) { 2043 addIndexHtml(u); 2044 } 2045 2046 return u.toString(); 2047 } 2048 2049 function processAnchors(root) { 2050 var scope = root && root.querySelectorAll ? root : document; 2051 var anchors = scope.querySelectorAll ? scope.querySelectorAll("a[href]") : []; 2052
2053 anchors.forEach(function(a) { 2054 if (a.dataset.uwoNormalized === "1") return; 2055 2056 var original = a.getAttribute("href"); 2057 var normalized = normalizeUwoHref(original); 2058 2059 if (normalized && normalized !== original) { 2060 a.setAttribute("href", normalized); 2061 2062 if (DEBUG) { 2063 console.log("[UWO Normalize]", original, "â", normalized); 2064 } 2065 } 2066 2067 a.dataset.uwoNormalized = "1"; 2068 }); 2069 } 2070 2071 if (document.readyState === "loading") { 2072 document.addEventListener("DOMContentLoaded", function() { 2073 processAnchors(document); 2074 }); 2075 } else { 2076 processAnchors(document); 2077 } 2078 2079 var observer = new MutationObserver(function(mutations) { 2080 mutations.forEach(function(m) { 2081 m.addedNodes.forEach(function(node) { 2082 if (node.nodeType !== 1) return; 2083 2084 if (node.tagName === "A") { 2085 processAnchors(node.parentNode || document); 2086 } else { 2087 processAnchors(node); 2088 } 2089 }); 2090 2091 if (m.type === "attributes" && m.target && m.target.tagName === "A") { 2092 m.target.dataset.uwoNormalized = "0"; 2093 processAnchors(m.target.parentNode || document); 2094 } 2095 }); 2096 }); 2097 2098 observer.observe(document.documentElement, { 2099 subtree: true, 2100 childList: true, 2101 attributes: true, 2102 attributeFilter: ["href"] 2103 }); 2104})(); 2105 2106// ===================================================== 2107// Image Gallery Carousel 2108// ===================================================== 2109function initCustomCarousels(root) { 2110 var context = root || document; 2111 2112 function setupCarousel(carousel) { 2113 if (!carousel || carousel.dataset.carouselInitialized === "1") return; 2114 carousel.dataset.carouselInitialized = "1"; 2115 2116 var items = carousel.querySelectorAll(".carousel-item"); 2117 var total = items.length; 2118 2119 var totalMobile = carousel.querySelector(".carousel-total"); 2120 var totalDesktop = carousel.querySelector(".carousel-total-desktop"); 2121 2122 if (totalMobile) { 2123 totalMobile.textContent = total; 2124 } 2125 2126 if (totalDesktop) { 2127 totalDesktop.textContent = total; 2128 } 2129 2130 if (total <= 1) { 2131 carousel.classList.add("is-single-slide"); 2132 } 2133 2134 function updateCarouselMeta(activeIndex) { 2135 var activeItem = items[activeIndex]; 2136 var slideNumber = activeIndex + 1; 2137 var title = activeItem ? activeItem.getAttribute("data-title") : ""; 2138 var caption = activeItem ? activeItem.getAttribute("data-caption") : ""; 2139 2140 var currentMobile = carousel.querySelector(".carousel-current"); 2141 var currentDesktop = carousel.querySelector(".carousel-current-desktop"); 2142 var titleEl = carousel.querySelector(".carousel-title"); 2143 var textEl = carousel.querySelector(".carousel-text"); 2144 2145 if (currentMobile) { 2146 currentMobile.textContent = slideNumber; 2147 } 2148 2149 if (currentDesktop) { 2150 currentDesktop.textContent = slideNumber; 2151 } 2152 2153 if (titleEl) { 2154 titleEl.textContent = title || ""; 2155 } 2156 2157 if (textEl) { 2158 textEl.textContent = caption || ""; 2159 } 2160 } 2161 2162 var activeIndex = 0; 2163 2164 items.forEach(function(item, index) { 2165 if (item.classList.contains("active")) { 2166 activeIndex = index; 2167 } 2168 }); 2169 2170 updateCarouselMeta(activeIndex); 2171 2172 var carouselInstance = bootstrap.Carousel.getInstance(carou
2172sel); 2173 2174 if (!carouselInstance) { 2175 carouselInstance = new bootstrap.Carousel(carousel, { 2176 interval: false, 2177 ride: false, 2178 touch: true, 2179 wrap: true 2180 }); 2181 } 2182 2183 carousel.addEventListener("slid.bs.carousel", function(event) { 2184 updateCarouselMeta(event.to); 2185 }); 2186 } 2187 2188 if (context.matches && context.matches(".custom-carousel")) { 2189 setupCarousel(context); 2190 } 2191 2192 if (!context.querySelectorAll) return; 2193 2194 context.querySelectorAll(".custom-carousel").forEach(setupCarousel); 2195} 2196 2197 2198// ===================================================== 2199// Collapse of Bootstrap Elements 2200// ===================================================== 2201function shouldSkipBootstrapElement(element, exceptions) { 2202 return exceptions.some(function(exception) { 2203 return ( 2204 exception === element || 2205 exception.contains(element) || 2206 element.contains(exception) 2207 ); 2208 }); 2209} 2210 2211function closeVisibleCollapses(exceptions) { 2212 var keepOpen = Array.isArray(exceptions) ? exceptions.filter(Boolean) : []; 2213 2214 document.querySelectorAll(".collapse.show").forEach(function(collapseEl) { 2215 if (shouldSkipBootstrapElement(collapseEl, keepOpen)) return; 2216 2217 var instance = 2218 bootstrap.Collapse.getInstance(collapseEl) || 2219 bootstrap.Collapse.getOrCreateInstance(collapseEl, { 2220 toggle: false 2221 }); 2222 2223 instance.hide(); 2224 }); 2225} 2226 2227function closeVisibleOffcanvases(exceptions) { 2228 var keepOpen = Array.isArray(exceptions) ? exceptions.filter(Boolean) : []; 2229 2230 document.querySelectorAll(".offcanvas.show").forEach(function(offcanvasEl) { 2231 if (shouldSkipBootstrapElement(offcanvasEl, keepOpen)) return; 2232 2233 var instance = 2234 bootstrap.Offcanvas.getInstance(offcanvasEl) || 2235 bootstrap.Offcanvas.getOrCreateInstance(offcanvasEl); 2236 2237 instance.hide(); 2238 }); 2239} 2240 2241function closeOpenBootstrapUi(exceptions) { 2242 closeVisibleCollapses(exceptions); 2243 closeVisibleOffcanvases(exceptions); 2244} 2245 2246 2247// ===================================================== 2248// Global Menu Watcher 2249// ===================================================== 2250function initGlobalMenuMutualExclusion() { 2251 document.addEventListener( 2252 "show.bs.offcanvas", 2253 function(e) { 2254 var offcanvasEl = e.target; 2255 if (!offcanvasEl || !offcanvasEl.classList.contains("offcanvas")) return; 2256 2257 var isGlobalMenu = 2258 (offcanvasEl.id && offcanvasEl.id.indexOf("global-menu") !== -1) || 2259 offcanvasEl.querySelector("#offcanvas-global-menu-accordion"); 2260 2261 if (!isGlobalMenu) return; 2262 2263 closeOpenBootstrapUi([offcanvasEl]); 2264 }, 2265 true 2266 ); 2267 2268 document.addEventListener( 2269 "show.bs.collapse", 2270 function(e) { 2271 var collapseEl = e.target; 2272 if (!collapseEl || !collapseEl.classList.contains("collapse")) return; 2273 2274 var isGlobalMenuCollapse = 2275 (collapseEl.id && collapseEl.id.indexOf("global-menu") !== -1) ||
2276 collapseEl.closest("#global-menu-accordion") || 2277 collapseEl.closest("#offcanvas-global-menu-accordion"); 2278 2279 if (!isGlobalMenuCollapse) return; 2280 2281 closeOpenBootstrapUi([collapseEl]); 2282 }, 2283 true 2284 ); 2285} 2286// ===================================================== 2287// Resize / Orientation Close Behaviour 2288// ===================================================== 2289function initViewportCloseBehavior() { 2290 var resizeTimer = null; 2291 2292 function getSearchCollapseIfOpenOrOpening() { 2293 var searchCollapse = document.getElementById("search-container"); 2294 2295 if ( 2296 searchCollapse && 2297 ( 2298 searchCollapse.classList.contains("show") || 2299 searchCollapse.classList.contains("collapsing") 2300 ) 2301 ) { 2302 return searchCollapse; 2303 } 2304 2305 return null; 2306 } 2307 2308 function scheduleClose() { 2309 clearTimeout(resizeTimer); 2310 2311 resizeTimer = setTimeout(function() { 2312 var searchCollapse = getSearchCollapseIfOpenOrOpening(); 2313 2314 if (searchCollapse) { 2315 closeOpenBootstrapUi([searchCollapse]); 2316 return; 2317 } 2318 2319 closeOpenBootstrapUi([]); 2320 }, 150); 2321 } 2322 2323 window.addEventListener("resize", scheduleClose); 2324 window.addEventListener("orientationchange", scheduleClose); 2325 2326 if (window.matchMedia) { 2327 var orientationMedia = window.matchMedia("(orientation: portrait)"); 2328 2329 if (orientationMedia.addEventListener) { 2330 orientationMedia.addEventListener("change", scheduleClose); 2331 } else if (orientationMedia.addListener) { 2332 orientationMedia.addListener(scheduleClose); 2333 } 2334 } 2335} 2336// ===================================================== 2337// Body Cleanup 2338// ===================================================== 2339function initGlobalMenuBodyCleanup() { 2340 document.addEventListener("shown.bs.offcanvas", function(e) { 2341 var offcanvasEl = e.target; 2342 if (!offcanvasEl || !offcanvasEl.classList.contains("offcanvas")) return; 2343 2344 var isGlobalMenu = 2345 (offcanvasEl.id && offcanvasEl.id.indexOf("global-menu") !== -1) || 2346 offcanvasEl.id === "offcanvas-1" || 2347 offcanvasEl.querySelector("#offcanvas-global-menu-accordion"); 2348 2349 if (!isGlobalMenu) return; 2350 2351 document.body.style.overflow = "hidden"; 2352 }); 2353 2354 document.addEventListener("hidden.bs.offcanvas", function(e) { 2355 var offcanvasEl = e.target; 2356 if (!offcanvasEl || !offcanvasEl.classList.contains("offcanvas")) return; 2357 2358 var isGlobalMenu = 2359 (offcanvasEl.id && offcanvasEl.id.indexOf("global-menu") !== -1) || 2360 offcanvasEl.id === "offcanvas-1" || 2361 offcanvasEl.querySelector("#offcanvas-global-menu-accordion"); 2362 2363 if (!isGlobalMenu) return; 2364 2365 resetMobileMenuVisualState(); 2366 2367 var anotherOffcanvasOpen = document.querySelector(".offcanvas.show"); 2368 // var searchOpen = document.querySelector("#search-container.show"); 2369 var searchOpen = document.querySelector("#search-container.show, #search-container.collapsing"); 2370 2371 if (!anotherOffcanvasOpen && !searchOpen) { 2372 document.body.style.overflowX = "hidden"; 2373 document.body.style.overflowY = "auto"; 2374 document.body.style.paddingRight = ""; 2375 document.body.classList.remove("no-scroll"); 2376 } 2377 }); 2378} 2379 2380// ===================================================== 2381// Reset Mobile Menu 2382// ===================================================== 2383function resetMobileMenuVisualState() { 2384 document.querySelectorAll(".offcanvas-slide").forEach(function(el) { 2385 el.classList.add("d-none"); 2386 }); 2387 2388 document.querySelectorAll(".menu-accordion").forEach(function(el) { 2389 el.classList.remove("d-none"); 2390 }); 2391 2392 document.querySelectorAll(".menu-accordion-button").forEach(function(el) { 2393 el.classList.remove("inactive"); 2394 });
2395} 2396 2397// ===================================================== 2398// Resetting the menu visuals 2399// ===================================================== 2400function initLegacyMobileMenuResets() { 2401 document.addEventListener("click", function(e) { 2402 var mobileSearch = e.target.closest(".mobile-search, #mobile-search"); 2403 if (mobileSearch) { 2404 resetMobileMenuVisualState(); 2405 } 2406 2407 var closeControl = e.target.closest(".offcanvas-close"); 2408 if (closeControl) { 2409 resetMobileMenuVisualState(); 2410 } 2411 }, true); 2412} 2413 2414// ===================================================== 2415// WESTERN BRANDING SAFEGUARD 2416// Ensures a Western-designated logo uses the approved 2417// image and links to the Western homepage. 2418// ===================================================== 2419function initWesternBrandingSafeguard() { 2420 var approvedLogo = 2421 "https://www.uwo.ca/web_standards/2025-global/assets/img/page-titles/svg/full-western-logo.svg"; 2422 2423 var approvedUrl = 2424 "https://www.uwo.ca/index.html"; 2425 2426 function enforceWesternBranding() { 2427 var mainLogo = document.getElementById("main-logo"); 2428 2429 if (!mainLogo) return; 2430 2431 var westernLogos = mainLogo.querySelectorAll( 2432 'img[data-wu-faculty="Western University"]' 2433 ); 2434 2435 westernLogos.forEach(function(image) { 2436 if (image.getAttribute("src") !== approvedLogo) { 2437 image.setAttribute("src", approvedLogo); 2438 } 2439 2440 var logoLink = image.closest("a"); 2441 2442 if ( 2443 logoLink && 2444 logoLink.getAttribute("href") !== approvedUrl 2445 ) { 2446 logoLink.setAttribute("href", approvedUrl); 2447 } 2448 }); 2449 } 2450 2451 // Enforce immediately if the logo has already been rendered. 2452 enforceWesternBranding(); 2453 2454 var pageTitleContainer = 2455 document.getElementById("page-title-container"); 2456 2457 if (!pageTitleContainer) return; 2458 2459 // The JSON rendering script may create or replace #main-logo later. 2460 var observer = new MutationObserver(function(mutations) { 2461 var brandingMayHaveChanged = mutations.some(function(mutation) { 2462 if (mutation.type === "attributes") { 2463 return true; 2464 } 2465 2466 return mutation.addedNodes.length > 0; 2467 }); 2468 2469 if (brandingMayHaveChanged) { 2470 enforceWesternBranding(); 2471 } 2472 }); 2473 2474 observer.observe(pageTitleContainer, { 2475 subtree: true, 2476 childList: true, 2477 attributes: true, 2478 attributeFilter: [ 2479 "src", 2480 "href", 2481 "data-wu-faculty" 2482 ] 2483 }); 2484} 2485 2486// ===================================================== 2487// TEMPLATE SAFEGUARD STYLESHEET FALLBACK 2488// Dynamically loads the safeguard stylesheet only when 2489// the template has not already included it. 2490// ===================================================== 2491function loadTemplateSafeguards() { 2492 var stylesheetId = "template-safeguards-css"; 2493 var stylesheetUrl = "https://www.uwo.ca/web_standards/2025-global/assets/css/template-safeguards.css"; 2494 2495 // The template normally provides the stylesheet with this ID. 2496 if (document.getElementById(stylesheetId)) { 2497 return; 2498 } 2499 2500 // Secondary check in case the stylesheet is present without its ID. 2501 var stylesheetAlreadyLoaded = Array.prototype.some.call( 2502 document.querySelectorAll('link[rel="stylesheet"]'), 2503 function(link) { 2504 return link.href.indexOf("/template-safeguards.css") !== -1; 2505 } 2506 ); 2507 2508 if (stylesheetAlreadyLoaded) { 2509 return; 2510 } 2511 2512 // The stylesheet is missing, so create and load it. 2513 var link = document.createElement("link"); 2514 2515 link.id = stylesheetId; 2516 link.rel = "stylesheet"; 2517 link.href = stylesheetUrl; 2518 2519 link.addEventListener( 2520 "load", 2521 function() { 2522 console.info( 2523 "Template safeguard stylesheet loaded by JavaScript fallback." 2524 ); 2525 }, 2526 { once: true } 2527 ); 2528 2529 link.addEventListener( 2530 "error", 2531 function() { 2532 console.error( 2533 "Template safeguard stylesheet could not be loaded:", 2534 stylesheetUrl 2535 ); 2536 }, 2537 { once: true } 2538 ); 2539 2540 document.head.appendChild(link); 2541}
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.