1import { Controller } from "@hotwired/stimulus" 2 3export default class extends Controller { 4 /** 5 * GetAddress API configuration 6 * @private 7 */ 8 API_KEY = 'm-Xt3XwMO0CKb4duGF6QoA45555'; 9 10 connect() { 11 this.initializePostcodeLookup() 12 this.observeRepresentativeChanges() 13 this.addPostcodeLookupClasses() 14 } 15 16 /** 17 * Initialize the postcode lookup functionality 18 * This method sets up both the getAddress plugin and enhances its UI 19 */ 20 initializePostcodeLookup() { 21 this.initializeGetAddressPlugin(); 22 this.initializePostcodeLookupStyling(); 23 } 24 25 /** 26 * Initialize the getAddress plugin for both billing and delivery addresses 27 * This sets up the API connection and field mappings 28 */ 29 initializeGetAddressPlugin() { 30 // Initialize Patient billing address lookup 31 this.initializeLookupForElement('#patient_billing_postcode_lookup', { 32 line_1: '#patient_form_billing_address_line1', 33 line_2: '#patient_form_billing_address_line2', 34 post_town: '#patient_form_billing_address_city', 35 county: '#patient_form_billing_address_county', 36 postcode: '#patient_form_billing_address_postal_code' 37 }); 38 39 // Initialize Patient delivery address lookup 40 this.initializeLookupForElement('#patient_delivery_postcode_lookup', { 41 line_1: '#patient_form_delivery_address_line1', 42 line_2: '#patient_form_delivery_address_line2', 43 post_town: '#patient_form_delivery_address_city', 44 county: '#patient_form_delivery_address_county', 45 postcode: '#patient_form_delivery_address_postal_code' 46 }); 47 48 // Initialize billing address lookup 49 this.initializeLookupForElement('#billing_postcode_lookup', { 50 line_1: '#addresses_form_billing_address_line1', 51 line_2: '#addresses_form_billing_address_line2', 52 post_town: '#addresses_form_billing_address_city', 53 county: '#addresses_form_billing_address_county', 54 postcode: '#addresses_form_billing_address_postal_code' 55 }); 56 57 // Initialize delivery address lookup 58 this.initializeLookupForElement('#delivery_postcode_lookup', { 59 line_1: '#addresses_form_delivery_address_line1', 60 line_2: '#addresses_form_delivery_address_line2', 61 post_town: '#addresses_form_delivery_address_city', 62 county: '#addresses_form_delivery_address_county', 63 postcode: '#addresses_form_delivery_address_postal_code' 64 }); 65 66 67 this.initializeLookupForElement('#partner_delivery_postcode_lookup', { 68 line_1: '#partner_delivery_address_attributes_line1', 69 line_2: '#partner_delivery_address_attributes_line2', 70 post_town: '#partner_delivery_address_attributes_city', 71 county: '#partner_delivery_address_attributes_county', 72 postcode: '#partner_delivery_address_attributes_postal_code' 73 }); 74 75 this.initializeLookupForElement('#patient_modal_billing_postcode_lookup', { 76 line_1: '#addresses_form_billing_address_line1', 77 line_2: '#addresses_form_billing_address_line2', 78 post_town: '#addresses_form_billing_address_city', 79 county: '#addresses_form_billing_address_county', 80 postcode: '#addresses_form_billing_address_postal_code' 81 }); 82 83 // Initialize prescription delivery address lookup (admin modal) 84 this.initializeLookupForElement('#postcode_lookup_container', { 85 line_1: '#prescription_delivery_line1', 86 line_2: '#prescription_delivery_line2', 87 post_town: '#prescription_delivery_city', 88 county: '#prescription_delivery_county', 89 postcode: '#prescription_delivery_postal_code' 90 }); 91 92 // Initialize all existing representative lookups 93 this.initializeAllRepresentativeLookups(); 94 } 95 96 /** 97 * Initialize a single postcode lookup element 98 * @param {string} selector - CSS selector for the lookup element 99 * @param {Object} outputFields - Mapping of output fields 100 */ 101 initializeLookupForElement(selector, outputFields) { 102 const element = document.querySelector(selector); 103 if (element && !element.hasAttribute('data-getaddress-initialized')) { 104 $(selector).getAddress({ 105 api_key: this.API_KEY, 106 output_fields: outputFields 107 }); 108 element.setAttribute('data-getaddress-initialized', 'true'); 109 } 110 } 111 112 /** 113 * Initialize all representative postcode lookups (both existing and new) 114 */ 115 initializeAllRepresentativeLookups() { 116 // Find all representative rows 117 const representativeRows = document.querySelectorAll('.representative-row'); 118 119 representativeRows.forEach((row, index) => { 120 this.initializeRepresentativeLookup(row, index); 121 }); 122 } 123 124 /** 125 * Initialize postcode lookup for a specific representative row 126 * @param {HTMLElement} representativeRow - The representative row element 127 * @param {number} index - Index or timestamp for unique identification 128 */ 129 initializeRepresentativeLookup(representativeRow, index = null) { 130 const postcodeDiv = representativeRow.querySelector('[id*="representative_postcode_lookup"]'); 131 if (!postcodeDiv) return; 132 133 // If no unique ID exists, create one 134 if (!postcodeDiv.id || postcodeDiv.id === 'representative_postcode_lookup') { 135 const uniqueId = index !== null ? index : new Date().getTime(); 136 postcodeDiv.id = `representative_postcode_lookup_${uniqueId}`; 137 } 138 139 // Skip if already initialized 140 if (postcodeDiv.hasAttribute('data-getaddress-initialized')) return; 141 142 // Find the address fields within this representative row 143 const addressFields = this.getRepresentativeAddressFields(representativeRow); 144 145 if (addressFields.line1 && addressFields.postcode) { 146 // Initialize the getAddress plugin 147 $(`#${postcodeDiv.id}`).getAddress({ 148 api_key: this.API_KEY, 149 output_fields: { 150 line_1: `#${addressFields.line1.id}`, 151 line_2: `#${addressFields.line2.id}`, 152 post_town: `#${addressFields.city.id}`, 153 county: `#${addressFields.county.id}`, 154 postcode: `#${addressFields.postcode.id}` 155 } 156 }); 157 158 // Mark as initialized 159 postcodeDiv.setAttribute('data-getaddress-initialized', 'true'); 160 } 161 } 162 163 /** 164 * Get address field elements within a representative row 165 * @param {HTMLElement} representativeRow - The representative row element 166 * @returns {Object} Object containing address field elements 167 */ 168 getRepresentativeAddressFields(representativeRow) { 169 const fields = {}; 170 171 // Find fields by name patterns or labels 172 fields.line1 = representativeRow.querySelector('input[name*="[line1]"]'); 173 fields.line2 = representativeRow.querySelector('input[name*="[line2]"]'); 174 fields.city = representativeRow.querySelector('input[name*="[city]"]'); 175 fields.county = representativeRow.querySelector('input[name*="[county]"]'); 176 fields.postcode = representativeRow.querySelector('input[name*="[postal_code]"]'); 177 178 // Ensure all fields have IDs for getAddress to target
179 Object.keys(fields).forEach(key => { 180 if (fields[key] && !fields[key].id) { 181 const timestamp = new Date().getTime(); 182 fields[key].id = `representative_${key}_${timestamp}`; 183 } 184 }); 185 186 return fields; 187 } 188 189 /** 190 * Observe changes in the representatives container to initialize new lookups 191 */ 192 observeRepresentativeChanges() { 193 const container = document.getElementById('representatives-container'); 194 if (!container) return; 195 196 const observer = new MutationObserver((mutations) => { 197 mutations.forEach((mutation) => { 198 if (mutation.addedNodes.length) { 199 mutation.addedNodes.forEach((node) => { 200 if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('representative-row')) { 201 // Wait a bit for the DOM to settle, then initialize 202 setTimeout(() => { 203 this.initializeRepresentativeLookup(node); 204 this.initializePostcodeLookupStyling(); 205 }, 100); 206 } 207 }); 208 } 209 }); 210 }); 211 212 observer.observe(container, { childList: true, subtree: true }); 213 } 214 215 /** 216 * Add CSS classes to postcode lookup containers for easier styling 217 */ 218 addPostcodeLookupClasses() { 219 // Add class to static lookups 220 const staticLookups = [ 221 'patient_billing_postcode_lookup', 222 'patient_delivery_postcode_lookup', 223 'billing_postcode_lookup', 224 'delivery_postcode_lookup', 225 'partner_delivery_postcode_lookup', 226 'patient_modal_billing_postcode_lookup', 227 'postcode_lookup_container' 228 ]; 229 230 staticLookups.forEach(id => { 231 const element = document.getElementById(id); 232 if (element) { 233 element.classList.add('postcode-lookup-container'); 234 } 235 }); 236 237 // Add class to all representative lookups (both existing and dynamic) 238 const representativeLookups = document.querySelectorAll('[id^="representative_postcode_lookup"]'); 239 representativeLookups.forEach(element => { 240 element.classList.add('postcode-lookup-container'); 241 }); 242 } 243 244 /** 245 * Initialize the styling enhancements for the postcode lookup 246 * This wraps the input and button in a container and handles placeholder text 247 */ 248 initializePostcodeLookupStyling() { 249 // Handle static lookups 250 const staticIds = ['patient_billing_postcode_lookup', 'patient_delivery_postcode_lookup', 'billing_postcode_lookup', 'delivery_postcode_lookup', 'partner_delivery_postcode_lookup', 'patient_modal_billing_postcode_lookup', 'postcode_lookup_container']; 251 staticIds.forEach((id) => { 252 this.stylePostcodeLookup(id); 253 }); 254 255 // Handle all representative lookups 256 const representativeDivs = document.querySelectorAll('[id*="representative_postcode_lookup"]'); 257 representativeDivs.forEach((div) => { 258 this.stylePostcodeLookup(div.id); 259 }); 260 } 261 262 /** 263 * Style a specific postcode lookup element 264 * @param {string} id - The ID of the postcode lookup element 265 */ 266 stylePostcodeLookup(id) { 267 const postcodeDiv = document.getElementById(id); 268 if (postcodeDiv) { 269 if (postcodeDiv.querySelector('input') && postcodeDiv.querySelector('button')) { 270 this.wrapSearchElements(postcodeDiv); 271 } else { 272 this.observeAndWrapElements(postcodeDiv); 273 } 274 } 275 } 276 277 /** 278 * Wrap the search elements (input and button) in a container div 279 * This is needed to: 280 * 1. Apply consistent styling to the input and button 281 * 2. Handle the placeholder text properly 282 * 3. Prevent the default "Search.." text from appearing 283 * @param {HTMLElement} postcodeDiv - The container div for the postcode lookup 284 */ 285 wrapSearchElements(postcodeDiv) { 286 // Check if already wrapped 287 if (postcodeDiv.querySelector('.getaddress-search-box')) return; 288 289 const input = postcodeDiv.querySelector('input'); 290 const button = postcodeDiv.querySelector('button'); 291 if (input && button) { 292 // Set placeholder text 293 input.placeholder = "Enter your postal code"; 294 295 // Watch for value changes to prevent "Search.." text 296 this.observeInputValue(input); 297 298 // Create wrapper and move elements into it 299 const wrapper = document.createElement('div'); 300 wrapper.className = 'getaddress-search-box'; 301 input.parentNode.insertBefore(wrapper, input); 302 wrapper.appendChild(input); 303 wrapper.appendChild(button); 304 } 305 } 306 307 /** 308 * Observe the postcode div for changes and wrap elements when they appear 309 * This is needed because the getAddress plugin creates its elements asynchronously 310 * @param {HTMLElement} postcodeDiv - The container div to observe 311 */ 312 observeAndWrapElements(postcodeDiv) { 313 const observer = new MutationObserver((mutations) => { 314 mutations.forEach((mutation) => { 315 if (mutation.addedNodes.length) { 316 if (postcodeDiv.querySelector('input') && postcodeDiv.querySelector('button')) { 317 this.wrapSearchElements(postcodeDiv); 318 observer.disconnect(); 319 } 320 } 321 }); 322 }); 323 324 observer.observe(postcodeDiv, { childList: true }); 325 } 326 327 /** 328 * Observe input value changes to prevent "Search.." text from appearing 329 * This is needed because the getAddress plugin tries to set this text by default 330 * @param {HTMLInputElement} input - The input element to observe 331 */ 332 observeInputValue(input) { 333 // Create a MutationObserver to watch for value changes 334 const observer = new MutationObserver((mutations) => { 335 mutations.forEach((mutation) => { 336 if (mutation.type === 'attributes' && mutation.attributeName === 'value') { 337 if (input.value === 'Search..') { 338 input.value = ''; 339 } 340 } 341 }); 342 }); 343 344 // Observe the input for value changes 345 observer.observe(input, { attributes: true }); 346 347 // Also set up an interval as backup 348 const intervalId = setInterval(() => { 349 if (input.value === 'Search..') { 350 input.value = ''; 351 } 352 // Clean up interval if input is removed from DOM 353 if (!document.contains(input)) { 354 clearInterval(intervalId); 355 } 356 }, 100); 357 } 358 359 /** 360 * Method to be called after a new representative is added 361 * This should be called from your addRepresentative method 362 */ 363 reinitializeRepresentativeLookups() { 364 // Small delay to ensure DOM is updated 365 setTimeout(() => { 366 this.initializeAllRepresentativeLookups(); 367 this.initializePostcodeLookupStyling(); 368 this.addPostcodeLookupClasses(); 369 }, 100); 370 } 371};
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.