1export default function initFauxSelects(context = document) { 2 // CHANGE 1: Only target select elements with multiple attribute, scope to context 3 const selects = context.querySelectorAll(".gdm-form select"); 4 const maxOptionsForFauxSelect = 15; 5 6 // Guard clause: exit if no multi-select elements are found 7 if (selects.length === 0) { 8 return; 9 } 10 11 selects.forEach((select, index) => { 12 // Skip if select is already wrapped in a faux-select 13 if (select.closest(".faux-select")) { 14 return; 15 } 16 17 // Allow explicit opt-out for fields that should stay native. 18 if ( 19 select.hasAttribute("data-native-select") || 20 select.hasAttribute("data-no-faux-select") 21 ) { 22 return; 23 } 24 25 // Avoid duplicating very large option sets in the DOM unless explicitly opted in. 26 if ( 27 select.options.length > maxOptionsForFauxSelect && 28 !select.hasAttribute("data-force-faux-select") 29 ) { 30 return; 31 } 32 33 // Create container for faux select 34 const wrapper = document.createElement("div"); 35 wrapper.className = "faux-select"; 36 wrapper.setAttribute("data-select-id", `faux-select-${index}`); 37 38 // Mirror native select hooks onto the wrapper for styling/targeting. 39 if (select.classList.length > 0) { 40 wrapper.classList.add(...Array.from(select.classList)); 41 } 42 43 // Keep a safe id on the wrapper and preserve the original select id as metadata. 44 if (select.id) { 45 wrapper.id = `${select.id}--faux-select`; 46 wrapper.setAttribute("data-source-id", select.id); 47 } 48 49 // Create display element for selected options 50 const display = document.createElement("div"); 51 display.className = "faux-select__display"; 52 display.tabIndex = 0; 53 display.setAttribute("role", "combobox"); 54 display.setAttribute("aria-haspopup", "listbox"); 55 display.setAttribute("aria-expanded", "false"); 56 57 // Transfer aria-label from original select if it exists 58 if (select.hasAttribute("aria-label")) { 59 display.setAttribute("aria-label", select.getAttribute("aria-label")); 60 } else { 61 // Fallback: use title or generate one 62 const label = 63 select.getAttribute("title") || `Select ${select.name || "option"}`; 64 display.setAttribute("aria-label", label); 65 } 66 67 // CHANGE 2: Default display text for multi-select 68 display.textContent = select.options[0].text; 69 70 // Create options list 71 const optionsList = document.createElement("ul"); 72 optionsList.className = "faux-select__options"; 73 optionsList.setAttribute("role", "listbox"); 74 optionsList.setAttribute("aria-hidden", "true"); 75 76 // Populate options 77 Array.from(select.options).forEach((option, optionIndex) => { 78 const li = document.createElement("li"); 79 li.className = "faux-select__option"; 80 li.setAttribute("data-value", option.value); 81 li.setAttribute("role", "option"); 82 83 // CHANGE 3: Reflect initial selected state from native select 84 li.setAttribute("aria-selected", option.selected); 85 86 li.textContent = option.text; 87 li.tabIndex = -1; 88 89 // CHANGE 4: Modified click handler for multi-select 90 li.addEventListener("click", () => { 91 // Check if this is a single or multi-select 92 const isMultiple = select.hasAttribute("multiple"); 93 94 if (isMultiple) { 95 // Multi-select: toggle selection 96 option.selected = !option.selected; 97 } else { 98 // Single-select: clear all selections and select this one 99 Array.from(select.options).forEach((opt) => (opt.selected = false)); 100 option.selected = true; 101 102 // Close dropdown for single-select 103 optionsList.setAttribute("aria-hidden", "true"); 104 display.setAttribute("aria-expanded", "false"); 105 } 106 107 // Update ARIA attribute 108 li.setAttribute("aria-selected", option.selected); 109 110 // Get selected options 111 const selectedOptions = Array.from(select.options) 112 .filter((opt) => opt.selected) 113 .map((opt) => opt.text); 114 115 // Update display 116 display.textContent = 117 selectedOptions.length > 0 118 ? selectedOptions.join(", ") 119 : select.options[0].text; 120 121 // Dispatch change event 122 select.dispatchEvent(new Event("change", { bubbles: true })); 123 124 // Maintain focus 125 display.focus(); 126 }); 127 128 optionsList.appendChild(li); 129 }); 130 131 // Sync faux select with native select changes 132 select.addEventListener("change", () => { 133 // Get all selected options from native select 134 const selectedOptions = Array.from(select.options) 135 .filter((opt) => opt.selected) 136 .map((opt) => opt.text); 137 138 // CHANGE 6: Update display text with all selections 139 display.textContent = 140 selectedOptions.length > 0 141 ? selectedOptions.join(", ") 142 : select.options[0].text; 143 144 // Update ARIA states for all options 145 optionsList.querySelectorAll("li").forEach((item) => { 146 const correspondingOption = Array.from(select.options).find( 147 (opt) => opt.value === item.getAttribute("data-value"), 148 ); 149 item.setAttribute( 150 "aria-selected", 151 correspondingOption?.selected || false, 152 ); 153 }); 154 }); 155 156 // Toggle options visibility (unchanged) 157 display.addEventListener("click", () => { 158 const isHidden = optionsList.getAttribute("aria-hidden") === "true";
159 optionsList.setAttribute("aria-hidden", !isHidden); 160 display.setAttribute("aria-expanded", String(isHidden)); 161 if (!isHidden) { 162 display.focus(); 163 } else { 164 optionsList.querySelector("li").focus(); 165 positionOptionsList(wrapper, display, optionsList); 166 } 167 }); 168 169 // Close options when clicking outside (unchanged) 170 document.addEventListener("click", (e) => { 171 if (!wrapper.contains(e.target)) { 172 optionsList.setAttribute("aria-hidden", "true"); 173 display.setAttribute("aria-expanded", "false"); 174 } 175 }); 176 177 // Keyboard accessibility for display (unchanged) 178 display.addEventListener("keydown", (e) => { 179 if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { 180 e.preventDefault(); 181 optionsList.setAttribute("aria-hidden", "false"); 182 display.setAttribute("aria-expanded", "true"); 183 optionsList.querySelector("li").focus(); 184 positionOptionsList(wrapper, display, optionsList); 185 } else if (e.key === "ArrowUp") { 186 e.preventDefault(); 187 optionsList.setAttribute("aria-hidden", "false"); 188 display.setAttribute("aria-expanded", "true"); 189 optionsList.querySelector("li:last-child").focus(); 190 positionOptionsList(wrapper, display, optionsList); 191 } else if ( 192 e.key === "Tab" && 193 !optionsList.getAttribute("aria-hidden") === "true" 194 ) { 195 e.preventDefault(); 196 optionsList.querySelector("li").focus(); 197 positionOptionsList(wrapper, display, optionsList); 198 } 199 }); 200 201 // Keyboard accessibility for options (unchanged) 202 optionsList.addEventListener("keydown", (e) => { 203 const currentOption = document.activeElement; 204 if (e.key === "ArrowDown") { 205 e.preventDefault(); 206 const nextOption = 207 currentOption.nextElementSibling || optionsList.firstElementChild; 208 nextOption.focus(); 209 } else if (e.key === "ArrowUp") { 210 e.preventDefault(); 211 const prevOption = 212 currentOption.previousElementSibling || optionsList.lastElementChild; 213 prevOption.focus(); 214 } else if (e.key === "Enter" || e.key === " ") { 215 e.preventDefault(); 216 currentOption.click(); 217 } else if (e.key === "Escape") { 218 e.preventDefault(); 219 optionsList.setAttribute("aria-hidden", "true"); 220 display.setAttribute("aria-expanded", "false"); 221 display.focus(); 222 } else if (e.key === "Tab") { 223 e.preventDefault(); 224 optionsList.setAttribute("aria-hidden", "true"); 225 display.setAttribute("aria-expanded", "false"); 226 display.focus(); 227 } 228 }); 229 230 // Helper: Position options list (unchanged) 231 function positionOptionsList(wrapper, display, optionsList) { 232 optionsList.classList.remove("faux-select__options--above"); 233 optionsList.style.bottom = ""; 234 optionsList.style.top = ""; 235 236 if (optionsList.getAttribute("aria-hidden") === "true") return; 237 const rect = display.getBoundingClientRect(); 238 const optionsHeight = optionsList.offsetHeight; 239 const form = display.closest("form"); 240 let formBottom = window.innerHeight; 241 if (form) { 242 const formRect = form.getBoundingClientRect(); 243 formBottom = formRect.bottom; 244 } 245 const spaceBelow = formBottom - rect.bottom; 246 const spaceAbove = 247 rect.top - (form ? form.getBoundingClientRect().top : 0); 248 const viewportBottomDistance = window.innerHeight - rect.bottom; 249 250 if ( 251 (spaceBelow < optionsHeight && spaceAbove > optionsHeight) || 252 viewportBottomDistance < 320 253 ) { 254 optionsList.classList.add("faux-select__options--above"); 255 optionsList.style.bottom = `${display.offsetHeight}px`; 256 optionsList.style.top = "auto"; 257 } else { 258 optionsList.classList.remove("faux-select__options--above"); 259 optionsList.style.top = `${display.offsetHeight}px`; 260 optionsList.style.bottom = "auto"; 261 } 262 } 263 264 // Window event listeners (unchanged) 265 window.addEventListener("resize", () => {
266 if (optionsList.getAttribute("aria-hidden") === "false") { 267 positionOptionsList(wrapper, display, optionsList); 268 } 269 }); 270 window.addEventListener( 271 "scroll", 272 () => { 273 if (optionsList.getAttribute("aria-hidden") === "false") { 274 positionOptionsList(wrapper, display, optionsList); 275 } 276 }, 277 true, 278 ); 279 280 // Hide original select but keep it in DOM 281 select.style.display = "none"; 282 283 // Assemble faux select 284 wrapper.appendChild(display); 285 wrapper.appendChild(optionsList); 286 select.parentNode.insertBefore(wrapper, select); 287 wrapper.appendChild(select); 288 }); 289}
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.