1class Dropdown { 2 constructor(container, options = {}) { 3 this.container = typeof container === 'string' ? document.querySelector(container) : container; 4 this.options = { 5 name: 'dropdown', 6 defaultValue: null, 7 placeholder: 'Select an option', 8 items: [], 9 onChange: null, 10 ...options 11 }; 12 13 this.init(); 14 } 15 16 init() { 17 this.render(); 18 this.bindEvents(); 19 this.setInitialValue(); 20 } 21 22 render() { 23 const { name, placeholder, items } = this.options; 24 25 this.container.innerHTML = ` 26 <div class="minitools-custom-dropdown"> 27 <div class="minitools-custom-dropdown-toggle" tabindex="0"> 28 <span class="selected-text">${placeholder}</span> 29 <div class="minitools-custom-dropdown-arrow"></div> 30 </div> 31 <div class="minitools-custom-dropdown-menu"> 32 ${items.map(item => ` 33 <button class="minitools-custom-dropdown-item" data-value="${item.value}"> 34 ${item.text} 35 <span class="check-icon"> 36<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> 37<path d="M6.48272 12C6.30863 12.0007 6.14119 11.9272 6.01619 11.795L2.86781 8.46198C2.80505 8.39514 2.75498 8.31547 2.72045 8.22752C2.68592 8.13957 2.6676 8.04506 2.66655 7.94939C2.66443 7.75618 2.73274 7.56995 2.85648 7.43169C2.98021 7.29343 3.14921 7.21445 3.32632 7.21213C3.50343 7.20981 3.67412 7.28434 3.80086 7.41933L6.48539 10.2601L12.1983 4.20704C12.3253 4.07206 12.4961 3.9976 12.6733 4.00006C12.8506 4.00251 13.0197 4.08168 13.1434 4.22013C13.2671 4.35859 13.3354 4.545 13.3331 4.73835C13.3309 4.9317 13.2583 5.11616 13.1314 5.25115L6.94925 11.795C6.82425 11.9272 6.65681 12.0007 6.48272 12Z" fill="#8D8D8D"/> 38</svg> 39</span> 40 </button> 41 `).join('')} 42 </div> 43 <select name="${name}" class="form-select w-auto" style="display: none;"> 44 ${items.map(item => ` 45 <option value="${item.value}">${item.text}</option> 46 `).join('')} 47 </select> 48 </div> 49 `; 50 51 // Cache DOM elements 52 this.dropdown = this.container.querySelector('.minitools-custom-dropdown'); 53 this.toggle = this.dropdown.querySelector('.minitools-custom-dropdown-toggle'); 54 this.menu = this.dropdown.querySelector('.minitools-custom-dropdown-menu'); 55 this.selectedText = this.dropdown.querySelector('.selected-text'); 56 this.hiddenSelect = this.dropdown.querySelector('select'); 57 this.items = this.dropdown.querySelectorAll('.minitools-custom-dropdown-item'); 58 } 59 60 bindEvents() { 61 // Toggle dropdown 62 this.toggle.addEventListener('click', () => this.toggleDropdown()); 63 64 // Keyboard support for toggle 65 this.toggle.addEventListener('keydown', (e) => { 66 if (e.key === 'Enter' || e.key === ' ') { 67 e.preventDefault(); 68 this.toggleDropdown(); 69 } else if (e.key === 'ArrowDown') { 70 e.preventDefault(); 71 if (!this.dropdown.classList.contains('open')) { 72 this.toggleDropdown(); 73 } 74 const firstItem = this.items[0]; 75 if (firstItem) firstItem.focus(); 76 } 77 }); 78 79 // Item selection 80 this.items.forEach((item, index) => { 81 item.addEventListener('click', (e) => { 82 e.preventDefault(); 83 this.selectItem(item); 84 }); 85 86 // Keyboard navigation for items 87 item.addEventListener('keydown', (e) => { 88 switch (e.key) { 89 case 'ArrowDown': 90 e.preventDefault(); 91 const nextItem = this.items[index + 1]; 92 if (nextItem) nextItem.focus(); 93 break; 94 case 'ArrowUp': 95 e.preventDefault(); 96 const prevItem = this.items[index - 1]; 97 if (prevItem) { 98 prevItem.focus(); 99 } else { 100 this.toggle.focus(); 101 } 102 break; 103 case 'Enter': 104 case ' ': 105 e.preventDefault(); 106 this.selectItem(item); 107 this.toggle.focus(); 108 break; 109 case 'Escape': 110 this.closeDropdown(); 111 this.toggle.focus(); 112 break; 113 } 114 }); 115 }); 116 117 // Close on outside click 118 document.addEventListener('click', (e) => { 119 if (!this.dropdown.contains(e.target)) { 120 this.closeDropdown(); 121 } 122 }); 123 124 // Recalculate direction on window resize/scroll 125 window.addEventListener('resize', () => { 126 if (this.dropdown.classList.contains('open')) { 127 this.checkDropdownDirection(); 128 } 129 }); 130 131 window.addEventListener('scroll', () => { 132 if (this.dropdown.classList.contains('open')) { 133 this.checkDropdownDirection(); 134 } 135 }); 136 } 137 138 toggleDropdown() { 139 const isOpening = !this.dropdown.classList.contains('open'); 140 141 if (isOpening) { 142 this.dropdown.classList.add('open'); 143 // Check direction after opening to ensure proper positioning 144 this.checkDropdownDirection(); 145 } else { 146 this.closeDropdown(); 147 } 148 } 149 150 closeDropdown() { 151 this.dropdown.classList.remove('open'); 152 this.dropdown.classList.remove('open-upward'); 153 } 154 155 selectItem(item) { 156 const value = item.dataset.value; 157 const text = item.textContent.trim(); 158 159 // Update visual selection
160 this.items.forEach(i => i.classList.remove('selected')); 161 item.classList.add('selected'); 162 163 // Update displayed text 164 this.selectedText.textContent = text; 165 166 // Update hidden select value 167 this.hiddenSelect.value = value; 168 169 // Trigger change event 170 const changeEvent = new Event('change', { bubbles: true }); 171 this.hiddenSelect.dispatchEvent(changeEvent); 172 173 // Call custom onChange if provided 174 if (this.options.onChange) { 175 this.options.onChange(value, text); 176 } 177 178 this.closeDropdown(); 179 } 180 181 setInitialValue() { 182 const { defaultValue } = this.options; 183 if (defaultValue) { 184 const item = Array.from(this.items).find(item => item.dataset.value === defaultValue); 185 if (item) { 186 this.selectItem(item); 187 } 188 } 189 } 190 191 checkDropdownDirection() { 192 const rect = this.toggle.getBoundingClientRect(); 193 const viewportHeight = window.innerHeight; 194 195 // Calculate available space below and above 196 const spaceBelow = viewportHeight - rect.bottom; 197 const spaceAbove = rect.top; 198 199 // Get menu height - we need to temporarily show it to measure 200 const wasVisible = this.menu.style.display !== 'none'; 201 if (!wasVisible) { 202 // Temporarily show menu to measure its height 203 this.menu.style.visibility = 'hidden'; 204 this.menu.style.display = 'block'; 205 } 206 207 const menuHeight = this.menu.getBoundingClientRect().height; 208 209 if (!wasVisible) { 210 // Hide it again 211 this.menu.style.display = 'none'; 212 this.menu.style.visibility = 'visible'; 213 } 214 215 // Add some padding for safety 216 const padding = 20; 217 218 // Determine if dropdown should open upward 219 const shouldOpenUpward = (spaceBelow < menuHeight + padding) && (spaceAbove > menuHeight + padding); 220 221 // Apply or remove the upward class 222 if (shouldOpenUpward) { 223 this.dropdown.classList.add('open-upward'); 224 } else { 225 this.dropdown.classList.remove('open-upward'); 226 } 227 } 228 229 // Public methods 230 getValue() { 231 return this.hiddenSelect.value; 232 } 233 234 setValue(value) { 235 const item = Array.from(this.items).find(item => item.dataset.value === value); 236 if (item) { 237 this.selectItem(item); 238 } 239 } 240 241 destroy() { 242 // Remove event listeners and clean up 243 this.container.innerHTML = ''; 244 } 245}
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.