1document.addEventListener("DOMContentLoaded", function () { 2 // Always start with menu closed 3 if (document.body.classList.contains("menu-open")) { 4 document.body.classList.remove("menu-open"); 5 console.log("[MobileMenu] Removed menu-open from body on load"); 6 } 7 console.log( 8 "[MobileMenu] Initial menu-open:", 9 document.body.classList.contains("menu-open"), 10 ); 11 //updateNavClass(); 12 const menuBtn = document.querySelector(".search-toggle-btn-mobile"); 13 const menuCloseBtn = document.querySelector(".mobile-menu-close-btn"); 14 const mobileMenu = document.getElementById("mobileMenu"); 15 16 function openMobileMenu() { 17 document.body.classList.add("menu-open"); 18 menuBtn.setAttribute("aria-expanded", "true"); 19 20 // Trigger SVG animation 21 const startSvg = menuBtn.querySelector("#start"); 22 if (startSvg && startSvg.beginElement) { 23 startSvg.beginElement(); 24 } 25 26 // Move focus to close button after menu opens 27 setTimeout(() => { 28 if (menuCloseBtn) { 29 menuCloseBtn.focus(); 30 } 31 }, 100); 32 } 33 34 function closeMobileMenu() { 35 document.body.classList.remove("menu-open"); 36 menuBtn.setAttribute("aria-expanded", "false"); 37 38 // Trigger SVG reverse animation 39 const reverseSvg = menuBtn.querySelector("#reverse"); 40 if (reverseSvg && reverseSvg.beginElement) { 41 reverseSvg.beginElement(); 42 } 43 44 // Return focus to toggle button 45 menuBtn.focus(); 46 } 47 48 if (menuBtn) { 49 menuBtn.addEventListener("click", function (e) { 50 e.preventDefault(); 51 const isOpen = document.body.classList.contains("menu-open"); 52 53 if (isOpen) { 54 closeMobileMenu(); 55 } else { 56 openMobileMenu(); 57 } 58 }); 59 } 60 61 if (menuCloseBtn) { 62 menuCloseBtn.addEventListener("click", function (e) { 63 e.preventDefault(); 64 closeMobileMenu(); 65 }); 66 } 67 68 // Focus trap for mobile menu 69 if (mobileMenu && menuBtn) { 70 mobileMenu.addEventListener("keydown", function (e) { 71 if (e.key === "Tab" && document.body.classList.contains("menu-open")) { 72 const focusableElements = mobileMenu.querySelectorAll( 73 'button, a[href], input, select, textarea, [tabindex]:not([tabindex="-1"])', 74 ); 75 const focusableArray = Array.from(focusableElements); 76 const firstFocusable = focusableArray[0]; 77 const lastFocusable = focusableArray[focusableArray.length - 1]; 78 79 if (e.shiftKey) { 80 // Shift + Tab 81 if (document.activeElement === firstFocusable) { 82 e.preventDefault(); 83 lastFocusable.focus(); 84 } 85 } else { 86 // Tab 87 if (document.activeElement === lastFocusable) { 88 e.preventDefault(); 89 firstFocusable.focus(); 90 } 91 } 92 } 93 }); 94 } 95 96 document.querySelectorAll(".main-menu-dropdown-toggle").forEach((button) => { 97 button.addEventListener("click", function () { 98 const groupItems = this.nextElementSibling; 99 100 // Close all other open dropdowns 101 document.querySelectorAll(".main-menu-dropdown-menu").forEach((menu) => { 102 if (menu !== groupItems) { 103 menu.classList.remove("show"); 104 menu.style.top = ""; 105 menu.style.left = ""; 106 } 107 }); 108 109 document.querySelectorAll(".main-menu-dropdown-toggle").forEach((btn) => { 110 if (btn !== this) { 111 btn.classList.remove("active"); 112 btn.setAttribute("aria-expanded", "false"); 113 } 114 }); 115 116 const isOpen = groupItems.classList.toggle("show"); 117 this.classList.toggle("active", isOpen); 118 this.setAttribute("aria-expanded", isOpen); 119 120 if (isOpen) { 121 const button = this; 122 const dropdown = groupItems; 123 124 const parent = button.offsetParent; // positioned ancestor 125 const parentWidth = parent.offsetWidth; 126 const buttonLeft = button.offsetLeft; 127 const dropdownWidth = dropdown.offsetWidth; 128 129 let left; 130 131 // Check if it would overflow the right edge of the parent 132 if (buttonLeft + dropdownWidth > parentWidth) { 133 // Shift left so it doesn't overflow 134 left = parentWidth - dropdownWidth - 10; // 10px padding from edge 135 left = Math.max(0, left); // Make sure it doesn't go off the left side 136 } else { 137 // Align to button 138 left = buttonLeft; 139 } 140 141 // Set vertical position relative to document 142 const rect = button.getBoundingClientRect(); 143 const scrollTop = 144 window.pageYOffset || document.documentElement.scrollTop; 145 const top = rect.bottom + scrollTop + 10; 146 147 dropdown.style.top = `${top}px`; 148 dropdown.style.left = `${left}px`; 149 150 console.log( 151 `Dropdown position: top ${dropdown.style.top}, left ${dropdown.style.left}`, 152 ); 153 } 154 });
155 }); 156 157 // Close dropdown if clicking outside 158 document.addEventListener("click", function (e) { 159 document.querySelectorAll(".main-menu-dropdown-menu").forEach((group) => { 160 const toggleButton = group.previousElementSibling; 161 162 if (!toggleButton.contains(e.target) && !group.contains(e.target)) { 163 group.classList.remove("show"); 164 toggleButton.classList.remove("active"); 165 group.style.top = ""; 166 group.style.left = ""; 167 toggleButton.setAttribute("aria-expanded", "false"); 168 } 169 }); 170 }); 171 172 // Keyboard support: Escape to close dropdowns and mobile menu 173 document.addEventListener("keydown", function (e) { 174 if (e.key === "Escape") { 175 // Close mobile menu 176 if (document.body.classList.contains("menu-open")) { 177 closeMobileMenu(); 178 } 179 180 // Close dropdown menus 181 const openDropdown = document.querySelector( 182 ".main-menu-dropdown-menu.show", 183 ); 184 if (openDropdown) { 185 const toggleButton = openDropdown.previousElementSibling; 186 openDropdown.classList.remove("show"); 187 toggleButton.classList.remove("active"); 188 openDropdown.style.top = ""; 189 openDropdown.style.left = ""; 190 toggleButton.setAttribute("aria-expanded", "false"); 191 toggleButton.focus(); 192 } 193 } 194 }); 195 196 // Inline search functionality 197 const searchToggleBtns = document.querySelectorAll(".search-toggle-btn"); 198 const searchToggleBtnMobile = document.querySelector( 199 ".search-toggle-btn-mobile", 200 ); 201 const inlineSearchForm = document.querySelector(".inline-search-form"); 202 const inlineSearchInput = document.getElementById("inline-search-input"); 203 const inlineSearchClose = document.querySelector( 204 ".inline-search-form .inline-search-close", 205 ); 206 const inlineSearchFormMobile = document.querySelector( 207 ".inline-search-form-mobile", 208 ); 209 const inlineSearchInputMobile = document.getElementById( 210 "inline-search-input-mobile", 211 ); 212 const inlineSearchCloseMobile = document.querySelector( 213 ".inline-search-form-mobile .inline-search-close", 214 ); 215 const mainMenuNav = document.querySelector(".main-menu"); 216 217 function openInlineSearch() { 218 if (!inlineSearchForm || !mainMenuNav) return; 219 220 // Remove hidden first, then add active class on next frame for smooth transition 221 inlineSearchForm.removeAttribute("hidden"); 222 inlineSearchForm.style.display = "flex"; 223 224 requestAnimationFrame(() => { 225 requestAnimationFrame(() => { 226 inlineSearchForm.classList.add("active"); 227 searchToggleBtns.forEach((btn) => 228 btn.setAttribute("aria-expanded", "true"), 229 ); 230 mainMenuNav.classList.add("search-active"); 231 }); 232 }); 233 234 // Focus on search input after animation starts 235 setTimeout(() => { 236 inlineSearchInput.focus(); 237 }, 150); 238 } 239 240 function closeInlineSearch() { 241 if (!inlineSearchForm || !mainMenuNav) return; 242 243 inlineSearchForm.classList.remove("active"); 244 searchToggleBtns.forEach((btn) => 245 btn.setAttribute("aria-expanded", "false"), 246 ); 247 mainMenuNav.classList.remove("search-active"); 248 249 // Clear input 250 inlineSearchInput.value = ""; 251 252 // Wait for transition to complete before hiding 253 setTimeout(() => { 254 inlineSearchForm.setAttribute("hidden", ""); 255 inlineSearchForm.style.display = ""; 256 }, 400); 257 } 258 259 function openInlineSearchMobile() { 260 if (!inlineSearchFormMobile) return; 261 262 // Remove hidden first, then add active class on next frame for smooth transition 263 inlineSearchFormMobile.removeAttribute("hidden"); 264 inlineSearchFormMobile.style.display = "flex"; 265 266 requestAnimationFrame(() => { 267 requestAnimationFrame(() => { 268 inlineSearchFormMobile.classList.add("active"); 269 searchToggleBtnMobile.setAttribute("aria-expanded", "true"); 270 }); 271 }); 272 273 // Focus on search input after animation starts 274 setTimeout(() => { 275 inlineSearchInputMobile.focus(); 276 }, 150); 277 } 278 279 function closeInlineSearchMobile() { 280 if (!inlineSearchFormMobile) return; 281 282 inlineSearchFormMobile.classList.remove("active"); 283 searchToggleBtnMobile.setAttribute("aria-expanded", "false"); 284 285 // Clear input 286 inlineSearchInputMobile.value = ""; 287 288 // Wait for transition to complete before hiding 289 setTimeout(() => { 290 inlineSearchFormMobile.setAttribute("hidden", ""); 291 inlineSearchFormMobile.style.display = ""; 292 }, 400); 293 294 // Return focus to toggle button 295 searchToggleBtnMobile.focus(); 296 } 297 298 // Open search 299 searchToggleBtns.forEach((btn) => { 300 btn.addEventListener("click", openInlineSearch); 301 }); 302 303 // Open mobile search 304 if (searchToggleBtnMobile) { 305 searchToggleBtnMobile.addEventListener("click", openInlineSearchMobile); 306 } 307 308 // Close search 309 if (inlineSearchClose) { 310 inlineSearchClose.addEventListener("click", closeInlineSearch); 311 } 312 313 // Close mobile search 314 if (inlineSearchCloseMobile) { 315 inlineSearchCloseMobile.addEventListener("click", closeInlineSearchMobile); 316 } 317 318 // Close on Escape key 319 document.addEventListener("keydown", function (e) { 320 if (e.key === "Escape") { 321 if (inlineSearchForm && !inlineSearchForm.hasAttribute("hidden")) { 322 closeInlineSearch(); 323 } 324 if ( 325 inlineSearchFormMobile && 326 !inlineSearchFormMobile.hasAttribute("hidden") 327 ) { 328 closeInlineSearchMobile(); 329 } 330 } 331 }); 332});
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.