1(function() { 2 'use strict'; 3 4 const HOVER_DELAY = 150; 5 const CLOSE_DELAY = 100; 6 7 let openDropdown = null; 8 let hoverTimeout = null; 9 let closeTimeout = null; 10 11 function adjustDropdownPosition(navItem) { 12 const dropdownMenu = navItem.querySelector('.dropdown-menu'); 13 if (!dropdownMenu) return; 14 15 // Reset position first 16 dropdownMenu.style.left = '-30px'; 17 dropdownMenu.style.right = 'auto'; 18 19 // Force reflow to get accurate measurements 20 dropdownMenu.offsetHeight; 21 22 // Check if it goes off the right edge (with 20px padding) 23 const rect = dropdownMenu.getBoundingClientRect(); 24 const viewportWidth = document.documentElement.clientWidth; 25 26 if (rect.right > viewportWidth - 20) { 27 // Align to right edge instead 28 dropdownMenu.style.left = 'auto'; 29 dropdownMenu.style.right = '0'; 30 } 31 } 32 33 function openDropdownMenu(navItem) { 34 // Close any other open dropdown 35 if (openDropdown && openDropdown !== navItem) { 36 closeDropdownMenu(openDropdown); 37 } 38 39 navItem.classList.add('is-open'); 40 const trigger = navItem.querySelector('a[aria-haspopup="true"]'); 41 if (trigger) { 42 trigger.setAttribute('aria-expanded', 'true'); 43 } 44 openDropdown = navItem; 45 46 // Adjust dropdown position 47 adjustDropdownPosition(navItem); 48 } 49 50 function closeDropdownMenu(navItem) { 51 navItem.classList.remove('is-open'); 52 const trigger = navItem.querySelector('a[aria-haspopup="true"]'); 53 if (trigger) { 54 trigger.setAttribute('aria-expanded', 'false'); 55 } 56 if (openDropdown === navItem) { 57 openDropdown = null; 58 } 59 } 60 61 function closeAllDropdowns() { 62 document.querySelectorAll('.nav-item.has-dropdown.is-open').forEach(function(item) { 63 closeDropdownMenu(item); 64 }); 65 } 66 67 function initDropdowns() { 68 const dropdownItems = document.querySelectorAll('.nav-item.has-dropdown'); 69 70 dropdownItems.forEach(function(navItem) { 71 const trigger = navItem.querySelector('a[aria-haspopup="true"]'); 72 const dropdownMenu = navItem.querySelector('.dropdown-menu'); 73 74 if (!trigger || !dropdownMenu) return; 75 76 // Hover: mouse enter with delay 77 navItem.addEventListener('mouseenter', function() { 78 clearTimeout(closeTimeout); 79 hoverTimeout = setTimeout(function() { 80 openDropdownMenu(navItem); 81 }, HOVER_DELAY); 82 }); 83 84 // Hover: mouse leave with grace period 85 navItem.addEventListener('mouseleave', function() { 86 clearTimeout(hoverTimeout); 87 closeTimeout = setTimeout(function() { 88 closeDropdownMenu(navItem); 89 }, CLOSE_DELAY); 90 }); 91 92 // Keep dropdown open when hovering over the full-width dropdown bar 93 dropdownMenu.addEventListener('mouseenter', function() { 94 clearTimeout(closeTimeout); 95 }); 96 97 dropdownMenu.addEventListener('mouseleave', function() { 98 closeTimeout = setTimeout(function() { 99 closeDropdownMenu(navItem); 100 }, CLOSE_DELAY); 101 }); 102 103 // Keyboard: Enter to open 104 trigger.addEventListener('keydown', function(e) { 105 if (e.key === 'Enter') { 106 e.preventDefault(); 107 if (navItem.classList.contains('is-open')) { 108 closeDropdownMenu(navItem); 109 } else { 110 openDropdownMenu(navItem); 111 // Focus first dropdown item 112 const firstItem = dropdownMenu.querySelector('.dropdown-item'); 113 if (firstItem) { 114 firstItem.focus(); 115 } 116 } 117 } 118 }); 119 120 // Keyboard: Escape to close 121 navItem.addEventListener('keydown', function(e) { 122 if (e.key === 'Escape' && navItem.classList.contains('is-open')) { 123 e.preventDefault(); 124 closeDropdownMenu(navItem); 125 trigger.focus(); 126 } 127 }); 128 129 // Keyboard: Arrow navigation within dropdown 130 dropdownMenu.addEventListener('keydown', function(e) { 131 const items = Array.from(dropdownMenu.querySelectorAll('.dropdown-item')); 132 const currentIndex = items.indexOf(document.activeElement); 133 134 if (e.key === 'ArrowDown' || e.key === 'ArrowRight') { 135 e.preventDefault(); 136 const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; 137 items[nextIndex].focus(); 138 } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') { 139 e.preventDefault(); 140 const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; 141 items[prevIndex].focus(); 142 } 143 }); 144 }); 145 146 // Close dropdowns when clicking outside 147 document.addEventListener('click', function(e) { 148 if (!e.target.closest('.nav-item.has-dropdown') && !e.target.closest('.dropdown-menu')) { 149 closeAllDropdowns(); 150 } 151 }); 152 153 // Re-adjust dropdown position on window resize 154 let resizeTimeout; 155 window.addEventListener('resize', function() { 156 clearTimeout(resizeTimeout); 157 resizeTimeout = setTimeout(function() { 158 if (openDropdown) { 159 adjustDropdownPosition(openDropdown); 160 } 161 }, 100); 162 }); 163 } 164 165 // Initialize when DOM is ready 166 if (document.readyState === 'loading') { 167 document.addEventListener('DOMContentLoaded', initDropdowns); 168 } else { 169 initDropdowns(); 170 } 171})();
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.