// ----------------------------------------------------- // Global Variables and Constants // ----------------------------------------------------- //let mobileSearchPanel; // declare the variable to give it global scope so we can use it in the toggleMobileSearchPanel() //let menuPanel; //let menuToggle; //let isTransitioning = false; // state flag to prevent multiple clicks during transition // ----------------------------------------------------- // Global Utility Functions - necessary due to scope issues across multiple functions // ----------------------------------------------------- //set all links that open in a new window with an aria-label that tells the user the link opens in a new window document.addEventListener('DOMContentLoaded', function() { // Select all anchor tags with target="_blank" const externalLinks = document.querySelectorAll('a[target="_blank"]'); externalLinks.forEach(link => { // Check if the link already has an aria-label to avoid overwriting if (!link.hasAttribute('aria-label')) { // Get the existing text content of the link const linkText = link.textContent.trim(); // Construct the aria-label value // You can customize this message as needed const ariaLabelValue = `${linkText} (opens in a new tab)`; // Set the aria-label attribute link.setAttribute('aria-label', ariaLabelValue); } }); }); //Ensure the current year for the copyright text is correct (in the event a page has not been republished in a while) var theCurrentYear = new Date().getFullYear(); $('.the-current-year').html(" " + theCurrentYear); // MM - 06-10-26 - Apply active state to top-level main navigation links only. function initMainNavActiveState() { const navLists = document.querySelectorAll('.primary-links, .sticky-links'); if (!navLists.length) return; function normalizePath(input) { if (!input) return '/'; let value = String(input).trim(); if (!value) return '/'; try { const parsed = new URL(value, window.location.origin); value = parsed.pathname || '/'; } catch (e) { value = value.split('#')[0].split('?')[0]; } if (!value.startsWith('/')) value = '/' + value; value = value.replace(/\/{2,}/g, '/'); value = value.replace(/\/index\.(php|html?|pcf)$/i, '/'); if (value.length > 1) value = value.replace(/\/+$/, '/'); return value || '/'; } function isNavigableHref(rawHref) { if (!rawHref) return false; const href = rawHref.trim(); if (!href || href === '#') return false; if (/^(mailto:|tel:|javascript:)/i.test(href)) return false; return true; } const currentPath = normalizePath(window.location.pathname); navLists.forEach((navList) => { const topLinks = Array.from( navList.querySelectorAll(':scope > li:not(.search-box) > a[href]'), ).filter((link) => isNavigableHref(link.getAttribute('href'))); if (!topLinks.length) return; topLinks.forEach((link) => { link.classList.remove('is-active'); if (link.getAttribute('aria-current') === 'page') { link.removeAttribute('aria-current'); } }); let bestLink = null; let bestLen = -1; let bestExact = false; topLinks.forEach((link) => { const linkPath = normalizePath(link.getAttribute('href')); const isHome = linkPath === '/'; const isExact = currentPath === linkPath; const isSection = !isHome && currentPath.startsWith(linkPath); if (!isExact && !isSection) return; if (isExact && !bestExact) { bestLink = link; bestLen = linkPath.length; bestExact = true; return; } if ( (isExact === bestExact && linkPath.length > bestLen) || (isExact && bestExact && linkPath.length > bestLen) ) { bestLink = link; bestLen = linkPath.length; bestExact = isExact || bestExact; } }); if (!bestLink) return; bestLink.classList.add('is-active'); if (normalizePath(bestLink.getAttribute('href')) === currentPath) { bestLink.setAttribute('aria-current', 'page'); } }); } // Apply active state to top-level mobile menu items without auto-expanding panels. function initMainMobileNavActiveState() { const menuPanels = document.querySelectorAll('.menu-panel'); if (!menuPanels.length) return; function normalizePath(input) { if (!input) return '/'; let value = String(input).trim(); if (!value) return '/'; try { const parsed = new URL(value, window.location.origin); value = parsed.pathname || '/'; } catch (e) { value = value.split('#')[0].split('?')[0]; } if (!value.startsWith('/')) value = '/' + value; value = value.replace(/\/{2,}/g, '/'); value = value.replace(/\/index\.(php|html?|pcf)$/i, '/'); if (value.length > 1) value = value.replace(/\/+$/, '/'); return value || '/'; } function isNavigableHref(rawHref) { if (!rawHref) return false; const href = rawHref.trim(); if (!href || href === '#') return false; if (/^(mailto:|tel:|javascript:)/i.test(href)) return false; return true; } const currentPath = normalizePath(window.location.pathname); menuPanels.forEach((panel) => { const topItems = Array.from(panel.querySelectorAll('.primary-menu > li')); if (!topItems.length) return; const candidates = []; topItems.forEach((li) => { const link = li.querySelector( ':scope > a.mobile-nav-link-top, :scope > .mobile-nav-item-top > a.mobile-nav-link-top', ); if (!link) return; const href = link.getAttribute('href'); if (!isNavigableHref(href)) return; const toggle = li.querySelector(':scope > .mobile-nav-item-top > .accordion-toggle'); candidates.push({ li, link, toggle, path: normalizePath(href), }); }); if (!candidates.length) return; candidates.forEach(({ li, link, toggle }) => { li.classList.remove('is-active'); link.classList.remove('is-active'); toggle?.classList.remove('is-active'); if (link.getAttribute('aria-current') === 'page') { link.removeAttribute('aria-current'); } }); let best = null; candidates.forEach((candidate) => { const isHome = candidate.path === '/'; const isExact = currentPath === candidate.path; const isSection = !isHome && currentPath.startsWith(candidate.path); if (!isExact && !isSection) return; const exactRank = isExact ? 1 : 0; const lengthRank = candidate.path.length; if ( !best || exactRank > best.exactRank || (exactRank === best.exactRank && lengthRank > best.lengthRank) ) { best = { ...candidate, isExact, exactRank, lengthRank, }; } }); if (!best) return; best.li.classList.add('is-active'); best.toggle?.classList.add('is-active'); if (best.isExact) { best.link.setAttribute('aria-current', 'page'); } }); } document.addEventListener('DOMContentLoaded', function () { initMainNavActiveState(); initMainMobileNavActiveState(); }); // Menu Opener ////////////////////////////////////////////// document.addEventListener("DOMContentLoaded", function () { // Mobile & Tablet Menu Opener const mobileHamburger = document.querySelector(".hamburger-menu"); const tabletToggle = document.querySelector(".tablet-menu-toggle"); const mobileMenu = document.querySelector(".mobile-menu"); const tabletMenu = document.querySelector(".tablet-menu"); const closeButtons = document.querySelectorAll(".close-menu"); function closeAllMenus() { if (mobileMenu) mobileMenu.classList.remove("open"); if (tabletMenu) tabletMenu.classList.remove("open"); } if (mobileHamburger) { mobileHamburger.addEventListener("click", function () { if (mobileMenu) mobileMenu.classList.add("open"); }); } if (tabletToggle) { tabletToggle.addEventListener("click", function () { if (tabletMenu) tabletMenu.classList.add("open"); }); } if (closeButtons.length > 0) { closeButtons.forEach(button => button.addEventListener("click", closeAllMenus)); } // Close menu if clicking outside document.addEventListener("click", function (event) { if (mobileMenu && mobileHamburger && !mobileMenu.contains(event.target) && !mobileHamburger.contains(event.target)) { mobileMenu.classList.remove("open"); } if (tabletMenu && tabletToggle && !tabletMenu.contains(event.target) && !tabletToggle.contains(event.target)) { tabletMenu.classList.remove("open"); } }); // Close menu on ESC key document.addEventListener("keydown", function (event) { if (event.key === "Escape") { if (mobileMenu) mobileMenu.classList.remove("open"); if (tabletMenu) tabletMenu.classList.remove("open"); } }); }); // Sticky Navigation ////////////////////////////////////////////// document.addEventListener("DOMContentLoaded", function () { const stickyNav = document.querySelector(".sticky-nav"); const heroSection = document.querySelector('.homepage-hero'); const desktopBreakpoint = 992; // Only show sticky nav on desktop let scrollThreshold = 100; // Default threshold function calculateAndSetThreshold() { // Check if the hero section exists and we are on a desktop view if (heroSection && window.innerWidth >= desktopBreakpoint) { scrollThreshold = heroSection.offsetHeight; } else { // Fallback for pages without a hero or on smaller screens scrollThreshold = 100; } } // Function to update sticky nav visibility based on scroll position and viewport width function updateStickyNav() { // Ensure stickyNav exists before proceeding if (!stickyNav) { return; } const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const viewportWidth = window.innerWidth; // If we're on mobile/tablet or not past the threshold, hide the sticky nav if (viewportWidth < desktopBreakpoint || scrollTop < scrollThreshold) { stickyNav.classList.remove("visible"); } else { // Otherwise, if we've scrolled down past the threshold, show it stickyNav.classList.add("visible"); } } // Initial calculation and visibility check on page load calculateAndSetThreshold(); updateStickyNav(); // Listen for scroll events to update visibility window.addEventListener("scroll", updateStickyNav); // Recalculate threshold and update visibility on resize window.addEventListener("resize", function() { calculateAndSetThreshold(); updateStickyNav(); }); }); // Drop Down Menu ////////////////////////////////////////////// document.addEventListener("DOMContentLoaded", function () { const dropdowns = document.querySelectorAll(".dropdown > a"); // Ensure all dropdown menus are hidden on page load using CSS class document.querySelectorAll(".dropdown:not(.news-dropdown) > .dropdown-menu").forEach((el) => { el.classList.remove("dropdown-open"); // Ensures they start hidden el.setAttribute("aria-hidden", "true"); }); dropdowns.forEach((dropdown) => { const menu = dropdown.nextElementSibling; // Open/Close on Click dropdown.addEventListener("click", function (event) { event.preventDefault(); const isExpanded = dropdown.getAttribute("aria-expanded") === "true"; if (isExpanded) { closeDropdown(dropdown, menu); } else { closeAllMenus(); openDropdown(dropdown, menu); } }); // Open on Hover dropdown.addEventListener("mouseenter", function () { closeAllMenus(); openDropdown(dropdown, menu); }); menu.addEventListener("mouseenter", function () { openDropdown(dropdown, menu); }); // Close on Mouse Leave dropdown.addEventListener("mouseleave", function () { setTimeout(() => { if (!menu.matches(":hover") && !dropdown.matches(":hover")) { closeDropdown(dropdown, menu); } }, 300); }); menu.addEventListener("mouseleave", function () { setTimeout(() => { if (!menu.matches(":hover") && !dropdown.matches(":hover")) { closeDropdown(dropdown, menu); } }, 300); }); // Enable Space Bar Activation for Dropdowns dropdown.addEventListener("keydown", function (event) { if (event.key === " ") { event.preventDefault(); dropdown.click(); } }); // Close dropdown when clicking outside document.addEventListener("click", function (event) { if (!dropdown.contains(event.target) && !menu.contains(event.target)) { closeDropdown(dropdown, menu); } }); // Close dropdown on Escape Key menu.addEventListener("keydown", function (event) { if (event.key === "Escape") { closeDropdown(dropdown, menu); dropdown.focus(); } }); // Fix Keyboard Navigation (Allow Tab to exit menu) menu.addEventListener("keydown", function (event) { const focusableElements = menu.querySelectorAll("a"); if (!focusableElements.length) return; const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; if (event.key === "Tab") { if (event.shiftKey && document.activeElement === firstElement) { event.preventDefault(); closeDropdown(dropdown, menu); dropdown.focus(); } else if (!event.shiftKey && document.activeElement === lastElement) { event.preventDefault(); closeDropdown(dropdown, menu); const allFocusable = [...document.querySelectorAll("a, button, input, [tabindex]:not([tabindex='-1'])")]; const nextElement = allFocusable[allFocusable.indexOf(lastElement) + 1]; if (nextElement) { nextElement.focus(); } } } }); function openDropdown(trigger, target) { target.classList.add("dropdown-open"); target.setAttribute("aria-hidden", "false"); trigger.setAttribute("aria-expanded", "true"); // Only set focus if the trigger was focused (i.e., opened via keyboard) if (document.activeElement === trigger) { target.querySelector("a")?.focus(); } } function closeDropdown(trigger, target) { target.classList.remove("dropdown-open"); target.setAttribute("aria-hidden", "true"); trigger.setAttribute("aria-expanded", "false"); } function closeAllMenus() { document.querySelectorAll(".dropdown:not(.news-dropdown) > .dropdown-menu").forEach((el) => { el.classList.remove("dropdown-open"); el.setAttribute("aria-hidden", "true"); el.previousElementSibling.setAttribute("aria-expanded", "false"); }); } }); }); // Mega Menu ////////////////////////////////////////////// // Utility functions to open and close the mega menu // Refactored Mega Menu Functions for Both Primary and Sticky Nav function openMegaMenu(link) { // Find the containing nav element so we scope the behavior to just that nav. const navContainer = link.closest("nav"); // Close any other open mega menus within this nav. navContainer.querySelectorAll("a.has-mega-menu").forEach(l => { l.classList.remove("open"); l.setAttribute("aria-expanded", "false"); }); navContainer.querySelectorAll(".mega-menu").forEach(menu => { menu.classList.remove("open"); }); // Open the current mega menu. link.classList.add("open"); link.setAttribute("aria-expanded", "true"); const megaMenu = link.nextElementSibling; if (megaMenu && megaMenu.classList.contains("mega-menu")) { megaMenu.classList.add("open"); // Use a short delay to allow event processing before shifting focus. setTimeout(function () { const firstFocusable = megaMenu.querySelector('a, button, input, [tabindex]:not([tabindex="-1"])'); if (firstFocusable) { firstFocusable.focus(); } }, 10); } } function closeMegaMenu(link) { link.classList.remove("open"); link.setAttribute("aria-expanded", "false"); const megaMenu = link.nextElementSibling; if (megaMenu && megaMenu.classList.contains("mega-menu")) { megaMenu.classList.remove("open"); } } // Bind click and keyboard events for all mega menu triggers in both navs. document.querySelectorAll( ".primary-links > li > a.has-mega-menu, .sticky-links > li > a.has-mega-menu" ).forEach(link => { // Click handler: if not open, prevent navigation and open; if already open, let link work. link.addEventListener("click", function (e) { const isOpen = this.classList.contains("open"); if (!isOpen) { e.preventDefault(); openMegaMenu(this); } // If already open, allow navigation on the second click. }); // Keyboard handler: trigger on Enter or Space. link.addEventListener("keydown", function (e) { if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { const isOpen = this.classList.contains("open"); if (!isOpen) { e.preventDefault(); openMegaMenu(this); } } }); }); // Bind hover and focus-out events for list items containing mega menus. // This applies to both primary and sticky nav items. document.querySelectorAll( ".primary-links > li, .sticky-links > li" ).forEach(function (item) { let closeTimer; const link = item.querySelector("a.has-mega-menu"); // On mouseenter, cancel any pending close and open the menu if not already open. item.addEventListener("mouseenter", function () { if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; } if (link && !link.classList.contains("open")) { openMegaMenu(link); } }); // On mouseleave, set a timer to close the menu. item.addEventListener("mouseleave", function () { closeTimer = setTimeout(function () { if (link) { closeMegaMenu(link); } }, 300); // Adjust delay as needed }); // Close the menu if focus leaves the item entirely. item.addEventListener("focusout", function () { setTimeout(function () { if (!item.contains(document.activeElement) && link) { closeMegaMenu(link); } }, 0); }); }); // Optional: Update a CSS variable for scrollbar width (if your mega menu widths depend on it) function setScrollbarWidthVar() { const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth; document.documentElement.style.setProperty('--scrollbar-width', `${scrollbarWidth}px`); } document.addEventListener('DOMContentLoaded', setScrollbarWidthVar); window.addEventListener('resize', setScrollbarWidthVar); // ================================ // MOBILE & TABLET MENU + SEARCH PANEL LOGIC // ================================ // // - Handles mobile hamburger menu // - Handles mobile search panel toggle // - Handles focus trapping // - Ensures one panel is open at a time // Mobile search panel uses `max-height` for animation instead of `transform` // like the Desktop and Sticky search panel does below. This is by design. // This allows the panel to expand smoothly from its dynamic content height // (e.g. 260px) and avoids layout conflicts present in transform-based logic. // // This is intentionally separate from the desktop and sticky search logic, // which use `transform` because their layout and position are fixed. // // This toggle script also handles the following: // - Closes the mobile search panel if open to open the mobile panel // - Closes the mobile menu if open to open the search panel // - Smoothly expands/collapses the mobile search panel // - Dynamically calculates height based on scrollHeight // - Hides the panel from desktop view via media query // MOBILE AND TABLET MEGA MENU ////////////////////////////////////////////// document.addEventListener("DOMContentLoaded", function () { // Mobile/Tablet Menu variables const menuToggle = document.querySelector(".mobile-header .menu-toggle"); const menuPanel = document.querySelector(".menu-panel"); const closeMenuButton = document.querySelector(".close-menu"); const overlay = document.querySelector(".overlay"); const mobileSearchToggle = document.querySelector(".search-toggle-mobile"); const mobileSearchPanel = document.querySelector(".mobile-search-panel"); const mobileSearchInput = mobileSearchPanel?.querySelector('.sb-search-form input[name="q"], .sb-search-form input[type="search"], .sb-search-form input[type="text"]'); let focusableElements, firstFocusable, lastFocusable; let isTransitioning = false; let mobileSearchReturnFocus = mobileSearchToggle || null; function trapFocus(element) { focusableElements = element.querySelectorAll('a, button, input, [tabindex]:not([tabindex="-1"])'); if (focusableElements.length > 0) { firstFocusable = focusableElements[0]; lastFocusable = focusableElements[focusableElements.length - 1]; firstFocusable.focus(); element.addEventListener("keydown", handleTrapFocus); } } function handleTrapFocus(e) { if (e.key === "Tab") { if (e.shiftKey && document.activeElement === firstFocusable) { e.preventDefault(); lastFocusable.focus(); } else if (!e.shiftKey && document.activeElement === lastFocusable) { e.preventDefault(); firstFocusable.focus(); } } } function releaseFocus() { menuPanel.removeEventListener("keydown", handleTrapFocus); } function focusMobileSearchInput() { if (!mobileSearchInput) return; requestAnimationFrame(() => { if (!mobileSearchPanel?.classList.contains("is-open")) return; mobileSearchInput.focus(); }); } function closeMenu(callback) { menuPanel.classList.remove("open"); menuToggle.classList.remove("active"); menuToggle.setAttribute("aria-expanded", "false"); menuPanel.setAttribute("aria-hidden", "true"); releaseFocus(); if (typeof callback === "function") { setTimeout(callback, 10); // Allow repaint before callback } } function openMenu() { // Close the mobile search panel if it's open if (mobileSearchPanel?.classList.contains("is-open")) { closeSearchPanel(); } menuPanel.classList.add("open"); menuToggle.classList.add("active"); menuToggle.setAttribute("aria-expanded", "true"); menuPanel.setAttribute("aria-hidden", "false"); trapFocus(menuPanel); } function doOpenSearchPanel(triggerEl = mobileSearchToggle) { mobileSearchPanel.style.visibility = "visible"; mobileSearchPanel.classList.add("is-open"); mobileSearchPanel.style.maxHeight = mobileSearchPanel.scrollHeight + "px"; mobileSearchReturnFocus = triggerEl || mobileSearchToggle || mobileSearchReturnFocus; focusMobileSearchInput(); } function closeSearchPanel(callback, options = {}) { const { returnFocus = false, focusTarget = mobileSearchReturnFocus } = options; mobileSearchPanel.style.maxHeight = mobileSearchPanel.scrollHeight + "px"; mobileSearchPanel.offsetHeight; mobileSearchPanel.style.maxHeight = "0px"; mobileSearchPanel.classList.remove("is-open"); setTimeout(() => { mobileSearchPanel.style.visibility = "hidden"; if (returnFocus && focusTarget && typeof focusTarget.focus === "function") { focusTarget.focus(); } if (typeof callback === "function") callback(); }, 400); } function toggleMobileSearchPanel() { if (isTransitioning) return; const isOpen = mobileSearchPanel.classList.contains("is-open"); isTransitioning = true; if (isOpen) { closeSearchPanel(() => { isTransitioning = false; }, { returnFocus: true, focusTarget: mobileSearchToggle }); } else if (menuPanel?.classList.contains("open")) { closeMenu(() => { doOpenSearchPanel(mobileSearchToggle); isTransitioning = false; }); } else { doOpenSearchPanel(mobileSearchToggle); isTransitioning = false; } } if (menuToggle) { menuToggle.addEventListener("click", function () { if (menuPanel.classList.contains("open")) { closeMenu(); } else { openMenu(); } }); } if(closeMenuButton) { closeMenuButton.addEventListener("click", () => closeMenu()); } if(overlay) { overlay.addEventListener("click", () => closeMenu()); } mobileSearchToggle?.addEventListener("click", function (e) { e.preventDefault(); toggleMobileSearchPanel(); }); // ------------------------------- // Accordion Functionality for Nested Submenus // ------------------------------- /** * setPanelFocusable(panel, isOpen) * Sets tabindex="-1" on all focusable children if closed, removes if open */ function setPanelFocusable(panel, isOpen) { if (!panel) return; const focusableSelectors = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'; const focusableEls = panel.querySelectorAll(focusableSelectors); focusableEls.forEach(el => { if (isOpen) { el.removeAttribute('tabindex'); } else { el.setAttribute('tabindex', '-1'); } }); } /** * updateAllAncestors(childSub) * Recursively update every open .accordion-panel (or primary-menu) * so they expand to fit newly visible content. */ function updateAllAncestors(childSub) { let panel = childSub.closest("ul.accordion-panel.is-open") || childSub.closest(".primary-menu"); while (panel) { // Force a full reflow before measuring and setting max-height panel.style.maxHeight = "none"; panel.offsetHeight; // force reflow panel.style.maxHeight = panel.scrollHeight + "px"; panel = panel.parentElement ? panel.parentElement.closest("ul.accordion-panel.is-open") || panel.parentElement.closest(".primary-menu") : null; } } /** * setupAccordion(selector, animate) * @param {string} selector - e.g. ".accordion-toggle" or ".sub-accordion-toggle" * @param {boolean} animate - true => animate using max-height, false => instant toggle */ function setupAccordion(selector, animate) { const toggles = document.querySelectorAll(selector); toggles.forEach((toggle) => { toggle.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); const submenuId = this.getAttribute("aria-controls"); const submenu = document.getElementById(submenuId); const isExpanded = this.getAttribute("aria-expanded") === "true"; // For sibling closing, try first to get an ancestor accordion-panel, // or if not found, fallback to the primary-menu container. const parentUl = this.closest("ul.accordion-panel") || this.closest(".primary-menu"); if (animate) { // SECOND-LEVEL: Animate via max-height if (isExpanded) { // Closing: animate max-height to 0 submenu.style.maxHeight = submenu.scrollHeight + "px"; submenu.offsetHeight; // force reflow submenu.classList.remove("is-open"); submenu.style.maxHeight = "0"; submenu.setAttribute("aria-hidden", "true"); this.setAttribute("aria-expanded", "false"); setPanelFocusable(submenu, false); } else { // Optionally close sibling toggles at this level if (parentUl) { const siblingToggles = parentUl.querySelectorAll(selector); siblingToggles.forEach((sibToggle) => { if (sibToggle !== this && sibToggle.getAttribute("aria-expanded") === "true") { const sibId = sibToggle.getAttribute("aria-controls"); const sibPanel = document.getElementById(sibId); sibPanel.style.maxHeight = sibPanel.scrollHeight + "px"; sibPanel.offsetHeight; sibPanel.classList.remove("is-open"); sibPanel.style.maxHeight = "0"; sibPanel.setAttribute("aria-hidden", "true"); sibToggle.setAttribute("aria-expanded", "false"); setPanelFocusable(sibPanel, false); } }); } // Open this submenu submenu.classList.add("is-open"); submenu.setAttribute("aria-hidden", "false"); this.setAttribute("aria-expanded", "true"); submenu.offsetHeight; // force reflow submenu.style.maxHeight = submenu.scrollHeight + "px"; setPanelFocusable(submenu, true); } } else { // THIRD-LEVEL: Instant toggle (display none/block) if (isExpanded) { submenu.classList.remove("is-open"); submenu.setAttribute("aria-hidden", "true"); this.setAttribute("aria-expanded", "false"); setPanelFocusable(submenu, false); updateAllAncestors(submenu); } else { if (parentUl) { const siblingToggles = parentUl.querySelectorAll(selector); siblingToggles.forEach((sibToggle) => { if (sibToggle !== this && sibToggle.getAttribute("aria-expanded") === "true") { const sibId = sibToggle.getAttribute("aria-controls"); const sibPanel = document.getElementById(sibId); sibPanel.classList.remove("is-open"); sibPanel.setAttribute("aria-hidden", "true"); sibToggle.setAttribute("aria-expanded", "false"); setPanelFocusable(sibPanel, false); updateAllAncestors(sibPanel); } }); } submenu.classList.add("is-open"); submenu.setAttribute("aria-hidden", "false"); this.setAttribute("aria-expanded", "true"); setPanelFocusable(submenu, true); updateAllAncestors(submenu); } } return false; }); }); } // On DOMContentLoaded, set tabindex="-1" for all closed panels document.querySelectorAll('ul.accordion-panel[aria-hidden="true"]').forEach(panel => { setPanelFocusable(panel, false); }); // Then bind the accordions: setupAccordion(".accordion-toggle", true); // For second-level (animate) setupAccordion(".sub-accordion-toggle", false); // For third-level (instant) }); // ----------------------------------------------------- // SEARCH PANEL FUNCTIONALITY // ----------------------------------------------------- document.addEventListener("DOMContentLoaded", function () { // Select all search toggles and panels const searchToggles = { desktop: document.querySelector(".desktop-nav .search-toggle"), sticky: document.querySelector(".sticky-nav .search-toggle"), mobile: document.querySelector(".search-toggle-mobile"), }; const searchPanels = { desktop: document.querySelector(".desktop-search-panel"), sticky: document.querySelector(".sticky-search-panel"), mobile: document.querySelector(".mobile-search-panel"), }; const searchCloseButtons = document.querySelectorAll(".sb-search-panel-close"); const searchInputs = { desktop: searchPanels.desktop?.querySelector('.sb-search-form input[name="q"], .sb-search-form input[type="search"], .sb-search-form input[type="text"]'), sticky: searchPanels.sticky?.querySelector('.sb-search-form input[name="q"], .sb-search-form input[type="search"], .sb-search-form input[type="text"]'), mobile: searchPanels.mobile?.querySelector('.sb-search-form input[name="q"], .sb-search-form input[type="search"], .sb-search-form input[type="text"]'), }; let lastSearchToggle = null; function focusSearchInput(panelKey) { const panel = searchPanels[panelKey]; const input = searchInputs[panelKey]; if (!panel || !input) return; requestAnimationFrame(() => { if ((panelKey === "desktop" || panelKey === "sticky") && !panel.classList.contains("is-open")) return; if (panelKey === "mobile" && !panel.classList.contains("is-open")) return; input.focus(); }); } // Function to close all search panels function closeAllSearchPanels(options = {}) { const { returnFocus = false, focusTarget = null } = options; const mobileWasOpen = searchPanels.mobile && searchPanels.mobile.classList.contains("is-open"); if (searchPanels.desktop) searchPanels.desktop.classList.remove("is-open"); if (searchPanels.sticky) searchPanels.sticky.classList.remove("is-open"); if (searchPanels.mobile && searchPanels.mobile.classList.contains("is-open")) { searchPanels.mobile.style.maxHeight = "0px"; searchPanels.mobile.classList.remove("is-open"); setTimeout(() => { searchPanels.mobile.style.visibility = "hidden"; }, 400); } if (returnFocus) { const target = focusTarget || lastSearchToggle; const focusDelay = mobileWasOpen ? 410 : 0; if (target && typeof target.focus === "function") { setTimeout(() => { target.focus(); }, focusDelay); } } } // Toggle desktop search panel if (searchToggles.desktop && searchPanels.desktop) { searchToggles.desktop.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); const isOpen = searchPanels.desktop.classList.contains("is-open"); searchPanels.desktop.classList.toggle("is-open"); if (isOpen) { searchToggles.desktop.focus(); } else { lastSearchToggle = searchToggles.desktop; focusSearchInput("desktop"); } }); } // Toggle sticky search panel if (searchToggles.sticky && searchPanels.sticky) { searchToggles.sticky.addEventListener("click", function (e) { e.preventDefault(); e.stopPropagation(); const isOpen = searchPanels.sticky.classList.contains("is-open"); searchPanels.sticky.classList.toggle("is-open"); if (isOpen) { searchToggles.sticky.focus(); } else { lastSearchToggle = searchToggles.sticky; focusSearchInput("sticky"); } }); } // Mobile search panel is handled in the mobile menu section due to its complexity // Add event listeners to close buttons searchCloseButtons.forEach(button => { button.addEventListener("click", function(e) { e.stopPropagation(); const parentPanel = button.closest(".desktop-search-panel, .sticky-search-panel, .mobile-search-panel"); let focusTarget = null; if (parentPanel?.classList.contains("desktop-search-panel")) focusTarget = searchToggles.desktop; else if (parentPanel?.classList.contains("sticky-search-panel")) focusTarget = searchToggles.sticky; else if (parentPanel?.classList.contains("mobile-search-panel")) focusTarget = searchToggles.mobile; closeAllSearchPanels({ returnFocus: true, focusTarget }); }); }); // Add click away listener document.addEventListener("click", function(e) { let clickedInside = false; // Check if click is inside any of the panels or on any of the toggles for (const panelKey in searchPanels) { if (searchPanels[panelKey] && searchPanels[panelKey].contains(e.target)) { clickedInside = true; break; } } if (!clickedInside) { for (const toggleKey in searchToggles) { if (searchToggles[toggleKey] && searchToggles[toggleKey].contains(e.target)) { clickedInside = true; break; } } } if (!clickedInside) { closeAllSearchPanels(); } }); // The rest of the mobile search panel logic is handled in the mobile menu section // It's complex because it interacts with the main mobile nav panel }); // NOTE: The "People / Website" toggle buttons inside the search panel were removed on 4/14/25. // Their JavaScript functionality MUST also been removed for cleanup. // LEAVING HERE COMMENTED OUT FOR NOW IN CASE WE WANT TO RE-ADD LATER. // ----------------------------------------------------- // Search Panel Interior Toggle Buttons Functionality // ----------------------------------------------------- /* document.querySelectorAll('.sb-search-toggle').forEach(toggleGroup => { toggleGroup.querySelectorAll('.btn').forEach(button => { button.addEventListener('click', () => { toggleGroup.querySelectorAll('.btn').forEach(btn => { btn.classList.remove('active'); btn.setAttribute('aria-pressed', 'false'); }); button.classList.add('active'); button.setAttribute('aria-pressed', 'true'); }); }); }); */ // ----------------------------------------------------- // ALERT AND ANNOUNCEMENT BANNER CLOSE BUTTONS (AND TEMP SHOW/HIDE BUTTONS FOR TESTING IN DEV) // ----------------------------------------------------- document.addEventListener('DOMContentLoaded', function () { // Banner and main content elements const alertBanner = document.querySelector('.sb-alert-banner'); const announcementBanner = document.querySelector('.sb-announcement-banner'); const mainContent = document.querySelector('main.main-content'); // DEV ONLY: Toggle buttons inside main content area - used during development to toggle banners manually const alertBtn = document.getElementById('toggle-alert'); const announcementBtn = document.getElementById('toggle-announcement'); // '×' close buttons inside each banner const closeButtons = document.querySelectorAll('.sb-banner-close'); // Utility: Check layout spacing when banners are shown on mobile function checkBannerSpacing() { const isMobile = window.innerWidth <= 991.98; const isAlertVisible = alertBanner && alertBanner.style.display === 'block'; const isAnnouncementVisible = announcementBanner && announcementBanner.style.display === 'block'; if (isMobile && (isAlertVisible || isAnnouncementVisible)) { mainContent?.classList.add('remove-margin-top'); } else { mainContent?.classList.remove('remove-margin-top'); } } // DEV ONLY: Toggle buttons to show/hide banners (used in dev only — safe to remove in production) if (alertBtn && alertBanner) { alertBtn.addEventListener('click', () => { const isVisible = alertBanner.style.display === 'block'; alertBanner.style.display = isVisible ? 'none' : 'block'; alertBtn.textContent = isVisible ? 'Show Alert' : 'Hide Alert'; checkBannerSpacing(); }); } if (announcementBtn && announcementBanner) { announcementBtn.addEventListener('click', () => { const isVisible = announcementBanner.style.display === 'block'; announcementBanner.style.display = isVisible ? 'none' : 'block'; announcementBtn.textContent = isVisible ? 'Show Announcement' : 'Hide Announcement'; checkBannerSpacing(); }); } // LIVE CLOSE BUTTONS: '×' inside banner (removes the banner on click) closeButtons.forEach(button => { button.addEventListener('click', function () { const banner = this.closest('.sb-alert-banner') || this.closest('.sb-announcement-banner'); if (banner) { banner.style.display = 'none'; checkBannerSpacing(); // Recalculate spacing when banner is closed } }); }); // Run on page load — in case banners are visible by default checkBannerSpacing(); // Also run on window resize — to adjust spacing if device switches between breakpoints window.addEventListener('resize', checkBannerSpacing); }); /* ============================================================================= CAMPUS LIFE SECTION LAYOUT MANAGER ============================================================================= This code manages the responsive layout of the Campus Life section circles. The section contains two rows: - Top row: 2 circles (Student Life, Arts & Culture) - Bottom row: 3 circles (Health & Safety, Student Knowledge, Explore Campus Life) Key features: - Dynamically calculates and sets circle sizes based on viewport width - Maintains perfect circular shapes using aspect ratio - Handles responsive layout above 768px (desktop) - Deactivates for mobile view (below 768px) where CSS takes over - Uses debouncing to optimize resize performance - Preserves gaps between circles for consistent spacing Related files: - campus-life.html: Contains the circle markup structure - _campus-life.scss: Handles styling and mobile layout ============================================================================= */ document.addEventListener('DOMContentLoaded', () => { const wrapper = document.querySelector('.sb-campuslife-section .section-content-wrapper'); const topRow = document.querySelector('.sb-campuslife-section .top-row'); const bottomRow = document.querySelector('.sb-campuslife-section .bottom-row'); const circles = document.querySelectorAll('.sb-campuslife-section .sb-campuslife-circle'); const mobileBreakpoint = 767; // px - Changed from 768 to 767 to match CSS breakpoints if (!wrapper || !topRow || !bottomRow || circles.length === 0) { console.warn('Campus life elements not found.'); return; } // --- Debounce function --- function debounce(func, wait, immediate) { let timeout; return function executedFunction() { const context = this; const args = arguments; const later = function() { timeout = null; if (!immediate) func.apply(context, args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func.apply(context, args); }; } // --- Layout calculation function --- function updateCampusLifeLayout() { if (window.innerWidth <= mobileBreakpoint) { circles.forEach(circle => { circle.style.width = ''; // Don't reset height if using aspect ratio }); // Reset row gaps if they were set inline topRow.style.gap = ''; bottomRow.style.gap = ''; return; } const computedWrapperStyle = getComputedStyle(wrapper); // We need padding from the ROWS now, not the wrapper const computedRowStyle = getComputedStyle(bottomRow); // Assume rows have same padding const rowPaddingLeft = parseFloat(computedRowStyle.paddingLeft); const rowPaddingRight = parseFloat(computedRowStyle.paddingRight); // Use clientWidth of the ROW for available space calculation const availableWidth = bottomRow.clientWidth - rowPaddingLeft - rowPaddingRight; const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); // Target gap is only used conceptually now for initial diameter calculation // const targetGapPx = rootFontSize * 2; const numItems = 3; const numGaps = numItems - 1; // Use a default/fallback gap for the initial diameter calculation if needed const conceptualGapPx = rootFontSize * 2; let calculatedDiameter = (availableWidth - (numGaps * conceptualGapPx)) / numItems; const minDiameter = 170; const maxDiameter = 322; const finalDiameter = Math.max(minDiameter, Math.min(maxDiameter, calculatedDiameter)); const spaceForBottomGaps = availableWidth - (numItems * finalDiameter); const actualGapPx = spaceForBottomGaps > 0 ? spaceForBottomGaps / numGaps : 0; // Apply styles circles.forEach(circle => { circle.style.width = `${finalDiameter}px`; }); // Set the SAME calculated gap for BOTH rows topRow.style.gap = `${actualGapPx}px`; bottomRow.style.gap = `${actualGapPx}px`; } // --- Event Listeners --- const debouncedLayoutUpdate = debounce(updateCampusLifeLayout, 150); updateCampusLifeLayout(); window.addEventListener('resize', debouncedLayoutUpdate); }); /* ============================================================================= WYSIWYG table responsible class accessibility fix / Mobile stacking fix - Added by Ying ============================================================================= This code Converts thead td to th Add scope="col" to Make stacking cards in mobile view ============================================================================= */ function initResponsiveTables() { document.querySelectorAll('table[class*="responsive-table"]').forEach((table) => { const thead = table.querySelector('thead'); if (!thead) return; // Existing: convert td to th[scope="col"] thead.querySelectorAll('tr td').forEach((td) => { const th = document.createElement('th'); th.innerHTML = td.innerHTML; Array.from(td.attributes).forEach((attr) => th.setAttribute(attr.name, attr.value)); th.setAttribute('scope', 'col'); td.replaceWith(th); }); thead.querySelectorAll('tr th').forEach((th) => { if (!th.hasAttribute('scope')) { th.setAttribute('scope', 'col'); } }); // New: stamp data-label on each tbody td for stacking const headerLabels = Array.from(thead.querySelectorAll('th')) .map((th) => th.textContent.trim()); const tbody = table.querySelector('tbody'); if (!tbody) return; Array.from(tbody.rows).forEach((row) => { Array.from(row.cells).forEach((cell, idx) => { if (headerLabels[idx]) { cell.setAttribute('data-label', headerLabels[idx]); } }); }); }); } document.addEventListener('DOMContentLoaded', initResponsiveTables); /* ============================================================================= People profile card - Added by Ying ============================================================================= This code convert raw phone number into (XXX)XXX - XXXX format Strip off illegal character in case user didn't follow instruction Hide divider and description when there is none ============================================================================= */ document.addEventListener('DOMContentLoaded', function () { document.querySelectorAll('.dept-people-profile-card-phone').forEach(function (el) { var link = el.querySelector('a'); if (!link) return; var href = link.getAttribute('href') || ''; var raw = href.replace('tel:', '').replace(/\D/g, ''); // Strip leading country code if (raw.length === 11 && raw.charAt(0) === '1') { raw = raw.slice(1); } if (raw.length !== 10) { // No valid number — hide the whole phone row el.style.display = 'none'; } else { // Valid number — format and display it var formatted = '(' + raw.slice(0, 3) + ') ' + raw.slice(3, 6) + '-' + raw.slice(6); var textNode = null; link.childNodes.forEach(function (node) { if (node.nodeType === 3) textNode = node; }); if (textNode) { textNode.textContent = ' ' + formatted; } else { link.appendChild(document.createTextNode(' ' + formatted)); } } }); document.querySelectorAll('.dept-people-profile-card').forEach(function (card) { var desc = card.querySelector('.dept-people-profile-card-desc'); var divider = card.querySelector('.dept-people-profile-card-divider'); if (desc && desc.textContent.trim() === '') { desc.style.display = 'none'; if (divider) divider.style.display = 'none'; } }); }); // Added by Modern Campus for Ticket 285323 // This is the logic that takes the values from the check boxes on the News Listing page and then ultimately // places that on the URL for the PHP to filter out articles document.addEventListener('DOMContentLoaded', function () { const applyButton = document.querySelector('#apply-filters'); if (!applyButton) { return; } applyButton.addEventListener('click', function (event) { event.preventDefault(); const url = new URL(window.location.href); url.searchParams.delete('page'); url.searchParams.delete('search_phrase'); url.searchParams.delete('year[]'); url.searchParams.delete('category[]'); document.querySelectorAll('#year-filter-menu input[name="years"]:checked').forEach(function (checkbox) { url.searchParams.append('year[]', checkbox.value); }); document.querySelectorAll('#category-filter-menu input[name="categories"]:checked').forEach(function (checkbox) { url.searchParams.append('category[]', checkbox.value); }); window.location.href = url.toString(); }); }); document.addEventListener('DOMContentLoaded', function () { const queryString = window.location.search; const urlParams = new URLSearchParams(queryString); cats = urlParams.getAll('category[]'); years = urlParams.getAll('year[]'); if(cats.length > 0){ for(var i = 0; i < cats.length; ++i){ var redone = "category-" + cats[i].replaceAll("&", "and").replaceAll(" ", "-").toLowerCase(); var catele = document.getElementById(redone); if(catele) catele.checked = true; } } if(years.length > 0){ for(var i = 0; i < years.length; ++i){ var redone2 = "year-" + years[i].replaceAll("&", "and").replaceAll(" ", "-").toLowerCase(); var yearele = document.getElementById(redone2); if(yearele) yearele.checked = true; } } }); document.addEventListener('DOMContentLoaded', function () { const resetButton = document.querySelector('#reset-filters'); if (!resetButton) { return; } resetButton.addEventListener('click', function (event) { event.preventDefault(); document.querySelectorAll('#year-filter-menu input[name="years"]:checked').forEach(function (checkbox) { checkbox.checked = false; }); document.querySelectorAll('#category-filter-menu input[name="categories"]:checked').forEach(function (checkbox) { checkbox.checked = false; }); document.getElementById("news-search").value = ""; }); }); document.addEventListener('DOMContentLoaded', function () { document.querySelectorAll('.news-dropdown .dropdown-toggle').forEach(function (button) { new bootstrap.Dropdown(button, { popperConfig: function (defaultConfig) { return { ...defaultConfig, placement: 'bottom-start', modifiers: [ ...(defaultConfig.modifiers || []).filter(function (modifier) { return modifier.name !== 'flip'; }), { name: 'flip', enabled: false } ] }; } }); }); });