1var html=document.documentElement,body=document.body;const toggleLight=document.querySelector(".toggle-light"),toggleDark=document.querySelector(".toggle-dark");toggleLight&&toggleLight.addEventListener("click",()=>{document.documentElement.classList.remove("dark"),localStorage.setItem("dark-mode",!1)}),toggleDark&&toggleDark.addEventListener("click",()=>{document.documentElement.classList.add("dark"),localStorage.setItem("dark-mode",!0)}); 2 3// Enhanced Navigation JavaScript with Accessibility Features 4document.addEventListener('DOMContentLoaded', function() { 5 const liveRegion = document.getElementById('live-region'); 6 7 // Utility function to announce to screen readers 8 function announce(message) { 9 if (liveRegion) { 10 liveRegion.textContent = message; 11 setTimeout(() => { 12 liveRegion.textContent = ''; 13 }, 1000); 14 } 15 } 16 17 // Set active states based on current URL 18 function setActiveNavigationState() { 19 const currentPath = window.location.pathname; 20 const navLinks = document.querySelectorAll('nav a[href*="/tag/"], nav a[href*="/t/"], nav a[href="/"]'); 21 22 navLinks.forEach(link => { 23 const linkPath = new URL(link.href).pathname; 24 25 // Remove existing active states 26 link.classList.remove('nav-active'); 27 link.removeAttribute('aria-current'); 28 29 if (currentPath === linkPath || 30 (currentPath.includes('/t/') && linkPath.includes(currentPath.split('/t/')[1])) || 31 (currentPath.includes('/tag/') && linkPath.includes(currentPath.split('/tag/')[1]))) { 32 33 link.classList.add('nav-active'); 34 link.setAttribute('aria-current', 'page'); 35 36 // If it's in a dropdown, also highlight the parent 37 const parentDropdown = link.closest('.dropdown-container'); 38 if (parentDropdown) { 39 const parentTrigger = parentDropdown.querySelector('.dropdown-trigger'); 40 if (parentTrigger) { 41 parentTrigger.classList.add('nav-active'); 42 parentTrigger.setAttribute('aria-current', 'page'); 43 } 44 } 45 } 46 }); 47 } 48 49 // Enhanced Desktop Dropdown Functionality 50 function initializeAccessibleDropdowns() { 51 const dropdowns = document.querySelectorAll('.dropdown-container'); 52 53 dropdowns.forEach(dropdown => { 54 const trigger = dropdown.querySelector('.dropdown-trigger'); 55 const menu = dropdown.querySelector('.dropdown-menu'); 56 57 if (!trigger || !menu) return; 58 59 const menuItems = menu.querySelectorAll('a[role="menuitem"]'); 60 let currentFocus = -1; 61 let closeTimeout; 62 63 // Keyboard event handler for dropdown triggers 64 function handleKeyDown(e) { 65 switch(e.key) { 66 case 'Enter': 67 case ' ': 68 e.preventDefault(); 69 toggleDropdown(); 70 break; 71 case 'ArrowDown': 72 e.preventDefault(); 73 if (isOpen()) { 74 focusFirstItem(); 75 } else { 76 openDropdown(); 77 focusFirstItem(); 78 } 79 break; 80 case 'ArrowUp': 81 e.preventDefault(); 82 if (isOpen()) { 83 focusLastItem(); 84 } else { 85 openDropdown(); 86 focusLastItem(); 87 } 88 break; 89 case 'Escape': 90 if (isOpen()) { 91 closeDropdown(); 92 trigger.focus(); 93 } 94 break; 95 } 96 } 97 98 // Menu item keyboard navigation 99 function handleMenuItemKeyDown(e, index) { 100 currentFocus = index; 101 102 switch(e.key) { 103 case 'ArrowDown': 104 e.preventDefault(); 105 focusNextItem(); 106 break; 107 case 'ArrowUp': 108 e.preventDefault(); 109 focusPreviousItem(); 110 break; 111 case 'Home': 112 e.preventDefault(); 113 focusFirstItem(); 114 break; 115 case 'End': 116 e.preventDefault(); 117 focusLastItem(); 118 break; 119 case 'Escape': 120 closeDropdown(); 121 trigger.focus(); 122 break; 123 case 'Tab': 124 // Allow normal tab behavior to close menu 125 closeDropdown(); 126 break; 127 } 128 } 129 130 function isOpen() { 131 return trigger.getAttribute('aria-expanded') === 'true'; 132 } 133 134 function openDropdown() { 135 clearTimeout(closeTimeout); 136 trigger.setAttribute('aria-expanded', 'true'); 137 menu.setAttribute('aria-expanded', 'true'); 138 currentFocus = -1; 139 140 const menuLabel = trigger.querySelector('span').textContent.trim(); 141 announce(`${menuLabel} menu opened`); 142 143 // Close other dropdowns 144 dropdowns.forEach(otherDropdown => { 145 if (otherDropdown !== dropdown) { 146 closeDropdownImmediate(otherDropdown); 147 } 148 }); 149 } 150 151 function closeDropdown() { 152 closeTimeout = setTimeout(() => { 153 trigger.setAttribute('aria-expanded', 'false'); 154 menu.setAttribute('aria-expanded', 'false'); 155 currentFocus = -1; 156 157 const menuLabel = trigger.querySelector('span').textContent.trim(); 158 announce(`${menuLabel} menu closed`); 159 }, 150); 160 } 161 162 function closeDropdownImmediate(targetDropdown) { 163 const targetTrigger = targetDropdown.querySelector('.dropdown-trigger'); 164 const targetMenu = targetDropdown.querySelector('.dropdown-menu'); 165 if (targetTrigger && targetMenu) { 166 targetTrigger.setAttribute('aria-expanded', 'false'); 167 targetMenu.setAttribute('aria-expanded', 'false'); 168 } 169 } 170 171 function toggleDropdown() { 172 if (isOpen()) { 173 closeDropdown(); 174 } else { 175 openDropdown(); 176 } 177 } 178 179 function focusItem(index) { 180 if (index >= 0 && index < menuItems.length) { 181 currentFocus = index; 182 menuItems[index].focus(); 183 } 184 } 185 186 function focusFirstItem() { 187 focusItem(0); 188 } 189 190 function focusLastItem() { 191 focusItem(menuItems.length - 1); 192 } 193 194 function focusNextItem() { 195 currentFocus = (currentFocus + 1) % menuItems.length; 196 focusItem(currentFocus); 197 } 198 199 function focusPreviousItem() { 200 currentFocus = currentFocus <= 0 ? menuItems.length - 1 : currentFocus - 1; 201 focusItem(currentFocus); 202 } 203 204 // Event listeners for triggers 205 trigger.addEventListener('keydown', handleKeyDown); 206 trigger.addEventListener('click', (e) => { 207 e.preventDefault(); 208 toggleDropdown(); 209 }); 210 211 // Event listeners for menu items
212 menuItems.forEach((item, index) => { 213 item.addEventListener('keydown', (e) => { 214 handleMenuItemKeyDown(e, index); 215 }); 216 217 item.addEventListener('click', () => { 218 closeDropdown(); 219 }); 220 221 // Keep menu open when focusing on items 222 item.addEventListener('mouseenter', () => { 223 clearTimeout(closeTimeout); 224 currentFocus = index; 225 }); 226 }); 227 228 // Mouse events for desktop (hover to open/close) 229 if (window.innerWidth >= 768) { 230 dropdown.addEventListener('mouseenter', openDropdown); 231 dropdown.addEventListener('mouseleave', closeDropdown); 232 233 // Keep dropdown open when focusing on menu items 234 menu.addEventListener('mouseenter', () => { 235 clearTimeout(closeTimeout); 236 }); 237 238 menu.addEventListener('mouseleave', closeDropdown); 239 } 240 }); 241 242 // Global event listeners 243 document.addEventListener('click', (e) => { 244 if (!e.target.closest('.dropdown-container')) { 245 dropdowns.forEach(dropdown => { 246 const trigger = dropdown.querySelector('.dropdown-trigger'); 247 const menu = dropdown.querySelector('.dropdown-menu'); 248 if (trigger && menu) { 249 trigger.setAttribute('aria-expanded', 'false'); 250 menu.setAttribute('aria-expanded', 'false'); 251 } 252 }); 253 } 254 }); 255 256 document.addEventListener('keydown', (e) => { 257 if (e.key === 'Escape') { 258 dropdowns.forEach(dropdown => { 259 const trigger = dropdown.querySelector('.dropdown-trigger'); 260 const menu = dropdown.querySelector('.dropdown-menu'); 261 if (trigger && menu) { 262 trigger.setAttribute('aria-expanded', 'false'); 263 menu.setAttribute('aria-expanded', 'false'); 264 } 265 }); 266 } 267 }); 268 } 269 270 // Enhanced Mobile Menu Functionality 271 function initializeAccessibleMobileMenu() { 272 const mobileMenuToggles = document.querySelectorAll('.mobile-menu-toggle'); 273 const mobileMenuClose = document.querySelector('.mobile-menu-close'); 274 const mobileMenuOverlay = document.querySelector('.mobile-menu-overlay'); 275 const mobileMenuContent = document.querySelector('.mobile-menu-content'); 276 const submenuToggles = document.querySelectorAll('.mobile-submenu-toggle'); 277 278 // Store the last focused element before opening menu 279 let lastFocusedElement = null; 280 // Store scroll position before opening menu 281 let savedScrollPosition = 0; 282 283 function openMobileMenu() { 284 lastFocusedElement = document.activeElement; 285 286 // Save current scroll position before applying position: fixed 287 savedScrollPosition = window.scrollY; 288 289 if (mobileMenuOverlay) { 290 mobileMenuOverlay.style.display = 'block'; 291 setTimeout(() => { 292 mobileMenuOverlay.classList.add('show'); 293 }, 10); 294 } 295 296 document.body.classList.add('mobile-menu-open'); 297 // Offset the body to maintain visual position 298 document.body.style.top = `-${savedScrollPosition}px`; 299 300 if (mobileMenuToggles.length > 0) { 301 mobileMenuToggles.forEach(toggle => { 302 toggle.setAttribute('aria-expanded', 'true'); 303 }); 304 } 305 306 // Focus the close button for better accessibility 307 if (mobileMenuClose) { 308 mobileMenuClose.focus(); 309 } 310 311 announce('Main menu opened'); 312 313 // Set focus trap 314 if (mobileMenuContent) { 315 trapFocus(mobileMenuContent); 316 } 317 } 318 319 function closeMobileMenu() { 320 if (mobileMenuOverlay) { 321 mobileMenuOverlay.classList.remove('show'); 322 setTimeout(() => { 323 mobileMenuOverlay.style.display = 'none'; 324 }, 300); 325 } 326 327 document.body.classList.remove('mobile-menu-open'); 328 document.body.style.top = ''; 329 // Restore scroll position after removing position: fixed 330 window.scrollTo(0, savedScrollPosition); 331 332 if (mobileMenuToggles.length > 0) { 333 mobileMenuToggles.forEach(toggle => { 334 toggle.setAttribute('aria-expanded', 'false'); 335 }); 336 } 337 338 // Return focus to the element that opened the menu 339 if (lastFocusedElement) { 340 lastFocusedElement.focus(); 341 } else if (mobileMenuToggles.length > 0) { 342 mobileMenuToggles[0].focus(); 343 } 344 345 announce('Main menu closed'); 346 347 // Close all submenus
348 submenuToggles.forEach(toggle => { 349 const submenu = toggle.nextElementSibling; 350 const arrow = toggle.querySelector('.mobile-submenu-arrow'); 351 352 toggle.setAttribute('aria-expanded', 'false'); 353 toggle.classList.remove('active'); 354 355 if (submenu) { 356 submenu.classList.remove('show'); 357 } 358 if (arrow) { 359 arrow.style.transform = 'rotate(0deg)'; 360 } 361 }); 362 363 // Remove focus trap 364 removeFocusTrap(); 365 } 366 367 // Focus trap functionality 368 function trapFocus(element) { 369 const focusableElements = element.querySelectorAll( 370 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' 371 ); 372 const firstFocusableElement = focusableElements[0]; 373 const lastFocusableElement = focusableElements[focusableElements.length - 1]; 374 375 function handleTabKey(e) { 376 if (e.key === 'Tab') { 377 if (e.shiftKey) { 378 if (document.activeElement === firstFocusableElement) { 379 lastFocusableElement.focus(); 380 e.preventDefault(); 381 } 382 } else { 383 if (document.activeElement === lastFocusableElement) { 384 firstFocusableElement.focus(); 385 e.preventDefault(); 386 } 387 } 388 } 389 } 390 391 element.addEventListener('keydown', handleTabKey); 392 element.setAttribute('data-focus-trapped', 'true'); 393 } 394 395 function removeFocusTrap() { 396 const trappedElement = document.querySelector('[data-focus-trapped]'); 397 if (trappedElement) { 398 trappedElement.removeAttribute('data-focus-trapped'); 399 } 400 } 401 402 // Event listeners 403 if (mobileMenuToggles.length > 0) { 404 mobileMenuToggles.forEach(toggle => { 405 toggle.addEventListener('click', openMobileMenu); 406 }); 407 } 408 409 if (mobileMenuClose) { 410 mobileMenuClose.addEventListener('click', closeMobileMenu); 411 } 412 413 // Close on overlay click 414 if (mobileMenuOverlay) { 415 mobileMenuOverlay.addEventListener('click', (e) => { 416 if (e.target === mobileMenuOverlay) { 417 closeMobileMenu(); 418 } 419 }); 420 } 421 422 // Close on escape key 423 document.addEventListener('keydown', (e) => { 424 if (e.key === 'Escape' && mobileMenuOverlay && mobileMenuOverlay.classList.contains('show')) { 425 closeMobileMenu(); 426 } 427 }); 428 429 // Submenu toggle functionality 430 submenuToggles.forEach(toggle => { 431 // Set initial ARIA attributes 432 toggle.setAttribute('aria-expanded', 'false'); 433 toggle.setAttribute('aria-haspopup', 'true'); 434 435 toggle.addEventListener('click', function() { 436 const submenu = this.nextElementSibling; 437 const arrow = this.querySelector('.mobile-submenu-arrow'); 438 const isOpen = this.getAttribute('aria-expanded') === 'true'; 439 const menuLabel = this.querySelector('span').textContent.trim(); 440 441 // Close other submenus 442 submenuToggles.forEach(otherToggle => { 443 if (otherToggle !== toggle) { 444 const otherSubmenu = otherToggle.nextElementSibling; 445 const otherArrow = otherToggle.querySelector('.mobile-submenu-arrow'); 446 447 otherToggle.setAttribute('aria-expanded', 'false'); 448 otherToggle.classList.remove('active'); 449 450 if (otherSubmenu) { 451 otherSubmenu.classList.remove('show'); 452 } 453 if (otherArrow) { 454 otherArrow.style.transform = 'rotate(0deg)'; 455 } 456 } 457 }); 458 459 // Toggle current submenu 460 if (isOpen) { 461 this.setAttribute('aria-expanded', 'false'); 462 this.classList.remove('active'); 463 if (submenu) { 464 submenu.classList.remove('show'); 465 } 466 if (arrow) { 467 arrow.style.transform = 'rotate(0deg)'; 468 } 469 announce(`${menuLabel} submenu closed`); 470 } else { 471 this.setAttribute('aria-expanded', 'true'); 472 this.classList.add('active'); 473 if (submenu) { 474 submenu.classList.add('show'); 475 } 476 if (arrow) { 477 arrow.style.transform = 'rotate(180deg)'; 478 } 479 announce(`${menuLabel} submenu opened`); 480 } 481 }); 482 483 // Keyboard support for submenu toggles 484 toggle.addEventListener('keydown', (e) => { 485 if (e.key === 'Enter' || e.key === ' ') { 486 e.preventDefault(); 487 toggle.click(); 488 } 489 }); 490 }); 491 492 // Close mobile menu when navigation links are clicked 493 const mobileNavLinks = document.querySelectorAll('.mobile-menu-overlay a'); 494 mobileNavLinks.forEach(link => { 495 link.addEventListener('click', () => { 496 setTimeout(closeMobileMenu, 100); 497 }); 498 }); 499 } 500 501 // Skip link functionality 502 function initializeSkipLink() { 503 const skipLink = document.querySelector('.skip-link'); 504 const mainContent = document.getElementById('main-content'); 505 506 if (skipLink && mainContent) { 507 skipLink.addEventListener('click', (e) => { 508 e.preventDefault(); 509 mainContent.focus(); 510 announce('Skipped to main content'); 511 }); 512 } 513 } 514 515 // Smooth scroll for footer "back to top" link 516 function initializeBackToTop() { 517 const backToTopLink = document.querySelector('a[href="#"]'); 518 if (backToTopLink) { 519 backToTopLink.addEventListener('click', function(e) { 520 e.preventDefault(); 521 window.scrollTo({ 522 top: 0, 523 behavior: 'smooth' 524 }); 525 }); 526 } 527 } 528 529 // Initialize all functionality 530 setActiveNavigationState(); 531 initializeAccessibleDropdowns(); 532 initializeAccessibleMobileMenu(); 533 initializeSkipLink(); 534 initializeBackToTop(); 535 536 // Re-initialize on window resize for responsive behavior 537 let resizeTimeout; 538 window.addEventListener('resize', function() { 539 clearTimeout(resizeTimeout); 540 resizeTimeout = setTimeout(function() {
541 // Re-initialize dropdowns with new window size 542 initializeAccessibleDropdowns(); 543 544 // Close mobile menu if window becomes large 545 if (window.innerWidth >= 768) { 546 const mobileMenuOverlay = document.querySelector('.mobile-menu-overlay'); 547 const mobileMenuToggles = document.querySelectorAll('.mobile-menu-toggle'); 548 549 if (mobileMenuOverlay && mobileMenuOverlay.classList.contains('show')) { 550 mobileMenuOverlay.classList.remove('show'); 551 document.body.classList.remove('mobile-menu-open'); 552 553 if (mobileMenuToggles.length > 0) { 554 mobileMenuToggles.forEach(toggle => { 555 toggle.setAttribute('aria-expanded', 'false'); 556 }); 557 } 558 559 // Close all submenus 560 const submenuToggles = document.querySelectorAll('.mobile-submenu-toggle'); 561 submenuToggles.forEach(toggle => { 562 const submenu = toggle.nextElementSibling; 563 const arrow = toggle.querySelector('.mobile-submenu-arrow'); 564 565 toggle.setAttribute('aria-expanded', 'false'); 566 toggle.classList.remove('active'); 567 568 if (submenu) { 569 submenu.classList.remove('show'); 570 } 571 if (arrow) { 572 arrow.style.transform = 'rotate(0deg)'; 573 } 574 }); 575 } 576 } 577 }, 250); 578 }); 579 580 // Announce page changes for SPA-like behavior 581 let lastUrl = location.href; 582 new MutationObserver(() => { 583 const url = location.href; 584 if (url !== lastUrl) { 585 lastUrl = url; 586 setTimeout(() => { 587 setActiveNavigationState(); 588 const pageTitle = document.title; 589 announce(`Navigated to ${pageTitle}`); 590 }, 100); 591 } 592 }).observe(document, { subtree: true, childList: true }); 593});
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.