1/** 2 * @class StateSelectController 3 * @extends Controller 4 * @description 5 * Enhances state select dropdowns with US state name-to-abbreviation mapping. 6 * Works alongside the ChoicesController to handle autofill scenarios where 7 * password managers type full state names (e.g., "California") but the select 8 * options only contain abbreviations (e.g., "CA"). 9 * 10 * The controller automatically finds the select element within its scope, 11 * so it can be placed on the wrapper div or the select element itself. 12 * 13 * @example <caption>State select with autofill support (on wrapper)</caption> 14 * <div data-controller="choices state-select"> 15 * <select data-choices-target="select" name="state"> 16 * <option value="">Select a state</option> 17 * <option value="CA">CA</option> 18 * <option value="NY">NY</option> 19 * </select> 20 * </div> 21 * 22 * @example <caption>State select with controller on select element</caption> 23 * <div data-controller="choices"> 24 * <select data-controller="state-select" data-choices-target="select" name="state"> 25 * <option value="">Select a state</option> 26 * <option value="CA">CA</option> 27 * </select> 28 * </div> 29 */ 30import { Controller } from "@hotwired/stimulus" 31 32/** 33 * Mapping of lowercase full state names to their 2-character abbreviations. 34 * Includes US states, DC, Puerto Rico, and the Armed Forces (AA, AE, AP) codes. 35 * @constant {Object.<string, string>} 36 */ 37const STATE_NAME_TO_ABBR = { 38 'alabama': 'AL', 39 'alaska': 'AK', 40 'arizona': 'AZ', 41 'arkansas': 'AR', 42 'california': 'CA', 43 'colorado': 'CO', 44 'connecticut': 'CT', 45 'delaware': 'DE', 46 'district of columbia': 'DC', 47 'florida': 'FL', 48 'georgia': 'GA', 49 'hawaii': 'HI', 50 'idaho': 'ID', 51 'illinois': 'IL', 52 'indiana': 'IN', 53 'iowa': 'IA', 54 'kansas': 'KS', 55 'kentucky': 'KY', 56 'louisiana': 'LA', 57 'maine': 'ME', 58 'maryland': 'MD', 59 'massachusetts': 'MA', 60 'michigan': 'MI', 61 'minnesota': 'MN', 62 'mississippi': 'MS', 63 'missouri': 'MO', 64 'montana': 'MT', 65 'nebraska': 'NE', 66 'nevada': 'NV', 67 'new hampshire': 'NH', 68 'new jersey': 'NJ', 69 'new mexico': 'NM', 70 'new york': 'NY', 71 'north carolina': 'NC', 72 'north dakota': 'ND', 73 'ohio': 'OH', 74 'oklahoma': 'OK', 75 'oregon': 'OR', 76 'pennsylvania': 'PA', 77 'puerto rico': 'PR', 78 'rhode island': 'RI', 79 'south carolina': 'SC', 80 'south dakota': 'SD', 81 'tennessee': 'TN', 82 'texas': 'TX', 83 'utah': 'UT', 84 'vermont': 'VT', 85 'virginia': 'VA', 86 'washington': 'WA', 87 'west virginia': 'WV', 88 'wisconsin': 'WI', 89 'wyoming': 'WY', 90 'armed forces americas': 'AA', 91 'armed forces europe': 'AE', 92 'armed forces pacific': 'AP' 93} 94 95export default class StateSelectController extends Controller { 96 /** 97 * Bound reference to the type handler for cleanup. 98 * @type {Function|null} 99 * @private 100 */ 101 boundHandleType = null 102 103 /** 104 * Reference to the select element. 105 * @type {HTMLSelectElement|null} 106 * @private 107 */ 108 selectElement = null 109 110 /** 111 * Initializes the state name mapping on the Tom Select instance. 112 * Waits for Tom Select to be initialized by the ChoicesController. 113 * 114 * @method connect 115 * @returns {void} 116 */ 117 connect() { 118 // Find the select element within this controller's scope 119 this.selectElement = this.element.querySelector('select') || this.element 120 if (this.selectElement.tagName !== 'SELECT') { 121 this.selectElement = null 122 return 123 } 124 125 // Wait for Tom Select to initialize (choices controller runs first) 126 requestAnimationFrame(() => { 127 this.attachTypeListener() 128 }) 129 } 130 131 /** 132 * Attaches the type event listener to the Tom Select instance. 133 * Retries if Tom Select is not yet initialized. 134 * 135 * @method attachTypeListener 136 * @private 137 * @returns {void} 138 */ 139 attachTypeListener() { 140 const ts = this.selectElement?.tomselect 141 if (ts) { 142 this.boundHandleType = this.handleType.bind(this) 143 ts.on('type', this.boundHandleType) 144 } else { 145 // Tom Select not ready yet, try again 146 requestAnimationFrame(() => this.attachTypeListener()) 147 } 148 } 149 150 /** 151 * Handles type events from Tom Select. 152 * If the typed query matches a full state name, selects the corresponding abbreviation. 153 * 154 * @method handleType 155 * @param {string} query - The text being typed into the search input 156 * @returns {void} 157 */ 158 handleType(query) { 159 const ts = this.selectElement?.tomselect 160 if (!ts) return 161 162 const abbr = STATE_NAME_TO_ABBR[query.toLowerCase()] 163 if (abbr && ts.options[abbr]) { 164 ts.setValue(abbr) 165 ts.close() 166 } 167 } 168 169 /** 170 * Cleans up event listeners when the controller disconnects. 171 * 172 * @method disconnect 173 * @returns {void} 174 */ 175 disconnect() { 176 const ts = this.selectElement?.tomselect 177 if (ts && this.boundHandleType) { 178 ts.off('type', this.boundHandleType) 179 } 180 } 181}
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.