PageSourceSearch

https://www.uwo.ca/web_standards/2025-global/assets/js/global-template.js

js uwo.ca collected 2026-09-24 08:24:24 UTC 89,805 bytes, 2,541 lines download raw bytes

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="#">&lt;&lt;</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="#">&gt;&gt;</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.