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