1/** 2 * Handle the personal info step 3 */ 4 5class PersonalInfoManager { 6 constructor() { 7 this.suggestions = []; 8 this.selectedSuggestionIndex = -1; 9 this.suggestionsContainer = null; 10 this.initialized = false; 11 this.form = null; 12 this.nextButton = null; 13 this.isFormValid = false; 14 15 // Bind methods to maintain 'this' context 16 this.setupAddressAutocomplete = this.setupAddressAutocomplete.bind(this); 17 this.getSuggestionsContainer = this.getSuggestionsContainer.bind(this); 18 this.showAddressSuggestions = this.showAddressSuggestions.bind(this); 19 this.hideSuggestions = this.hideSuggestions.bind(this); 20 this.navigateSuggestions = this.navigateSuggestions.bind(this); 21 this.highlightSuggestion = this.highlightSuggestion.bind(this); 22 this.selectSuggestion = this.selectSuggestion.bind(this); 23 this.validateForm = this.validateForm.bind(this); 24 this.showError = this.showError.bind(this); 25 this.hideError = this.hideError.bind(this); 26 this.handleInput = this.handleInput.bind(this); 27 this.updateNextButtonState = this.updateNextButtonState.bind(this); 28 29 // Initialize when DOM is ready 30 if (document.readyState === "loading") { 31 document.addEventListener("DOMContentLoaded", () => this.init()); 32 } else { 33 this.init(); 34 } 35 } 36 37 init() { 38 console.log("PersonalInfoManager initializing..."); 39 40 // Try to initialize now and retry after a short delay if elements aren't ready 41 this.tryInitialize(); 42 43 // Retry initialization after a delay to ensure DOM elements are ready 44 setTimeout(() => { 45 if (!this.initialized) { 46 console.log("Retrying initialization..."); 47 this.tryInitialize(); 48 } 49 }, 1000); 50 } 51 52 tryInitialize() { 53 const addressInput = document.getElementById("address"); 54 const step1Form = document.getElementById("step1"); 55 const nextButton = document.getElementById("step1-next"); 56 57 if (addressInput && step1Form && nextButton) { 58 console.log("Form elements found, initializing validation"); 59 this.form = step1Form; 60 this.nextButton = nextButton;
61 this.getSuggestionsContainer(); 62 this.setupAddressAutocomplete(); 63 this.setupFormValidation(); 64 this.initialized = true; 65 } else { 66 console.warn("Form elements not found, initialization delayed"); 67 } 68 } 69 70 setupFormValidation() { 71 // Add input event listeners to all form inputs 72 const inputs = this.form.querySelectorAll("input"); 73 inputs.forEach((input) => { 74 input.addEventListener("input", () => { 75 this.handleInput(input); 76 this.updateNextButtonState(); 77 }); 78 input.addEventListener("blur", () => { 79 this.handleInput(input); 80 this.updateNextButtonState(); 81 }); 82 }); 83 84 // Add click event listener to the next button 85 this.nextButton.addEventListener("click", (e) => { 86 e.preventDefault(); 87 if (this.validateForm()) { 88 // If validation passes, proceed to next step 89 document.getElementById("step1").classList.add("hidden"); 90 document.getElementById("step2").classList.remove("hidden"); 91 92 // Scroll to top smoothly 93 window.scrollTo({ 94 top: 0, 95 behavior: 'smooth' 96 }); 97 } 98 }); 99 100 // Initial button state 101 this.updateNextButtonState(); 102 } 103 104 updateNextButtonState() { 105 const inputs = this.form.querySelectorAll("input"); 106 this.isFormValid = Array.from(inputs).every((input) => 107 this.validateInput(input) 108 ); 109 110 if (this.isFormValid) { 111 this.nextButton.classList.remove("opacity-50", "cursor-not-allowed"); 112 this.nextButton.classList.add("hover:bg-red-100"); 113 this.nextButton.disabled = false; 114 } else { 115 this.nextButton.classList.add("opacity-50", "cursor-not-allowed"); 116 this.nextButton.classList.remove("hover:bg-red-100"); 117 this.nextButton.disabled = true; 118 } 119 } 120 121 handleInput(input) { 122 const isValid = this.validateInput(input); 123 if (isValid) { 124 this.hideError(input); 125 input.classList.remove("border-red-500"); 126 input.classList.add("border-gray-300"); 127 } else { 128 this.showError(input); 129 input.classList.remove("border-gray-300"); 130 input.classList.add("border-red-500"); 131 } 132 } 133 134 validateInput(input) { 135 const value = input.value.trim(); 136 137 // Check if required field is empty 138 if (input.required && !value) { 139 return false; 140 } 141 142 // Validate based on input type and pattern 143 switch (input.type) { 144 case "email": 145 return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); 146 case "tel": 147 return /^(?:(?:\+|00)33|0)\s*[1-9](?:[\s.-]*\d{2}){4}$/.test(value); 148 default: 149 if (input.pattern) { 150 return new RegExp(input.pattern).test(value); 151 } 152 return true; 153 } 154 } 155 156 validateForm() { 157 let isValid = true; 158 const inputs = this.form.querySelectorAll("input"); 159 160 inputs.forEach((input) => { 161 if (!this.validateInput(input)) { 162 this.showError(input); 163 input.classList.remove("border-gray-300"); 164 input.classList.add("border-red-500"); 165 isValid = false; 166 } else { 167 this.hideError(input); 168 input.classList.remove("border-red-500"); 169 input.classList.add("border-gray-300"); 170 } 171 }); 172 173 return isValid; 174 } 175 176 showError(input) { 177 const errorMessage = input.nextElementSibling; 178 if (errorMessage && errorMessage.classList.contains("error-message text-sm")) { 179 errorMessage.classList.remove("hidden"); 180 } 181 } 182 183 hideError(input) { 184 const errorMessage = input.nextElementSibling; 185 if (errorMessage && errorMessage.classList.contains("error-message text-sm")) { 186 errorMessage.classList.add("hidden"); 187 } 188 } 189 190 getSuggestionsContainer() { 191 // Get the existing suggestions container 192 this.suggestionsContainer = document.getElementById("address-suggestions"); 193 194 if (!this.suggestionsContainer) { 195 console.error("Suggestions container not found, creating one"); 196 // Create new suggestions container as fallback 197 this.suggestionsContainer = document.createElement("div"); 198 this.suggestionsContainer.id = "address-suggestions"; 199 this.suggestionsContainer.className = 200 "absolute z-10 w-full bg-white border border-gray-300 rounded-md shadow-lg max-h-60 overflow-y-auto mt-1 hidden"; 201 202 // Find the address field container and insert suggestions container after it 203 const addressField = document.getElementById("address"); 204 if (addressField) { 205 console.log("Attaching suggestions container to address field parent"); 206 const parentDiv = addressField.parentNode; 207 parentDiv.style.position = "relative"; 208 parentDiv.appendChild(this.suggestionsContainer); 209 } else { 210 console.error('Address field not found with ID "address"'); 211 } 212 } 213 } 214 215 setupAddressAutocomplete() { 216 const addressInput = document.getElementById("address"); 217 218 if (!addressInput) { 219 console.error("Address input not found"); 220 return; 221 } 222 223 if (!this.suggestionsContainer) { 224 console.error("Suggestions container not created"); 225 return; 226 } 227 228 console.log("Setting up event listeners for address autocomplete"); 229 230 // Handle input changes 231 addressInput.addEventListener( 232 "input", 233 this.debounce(async (e) => { 234 console.log("Input event fired"); 235 const query = e.target.value.trim(); 236 this.selectedSuggestionIndex = -1; 237 238 if (query.length < 3) { 239 this.hideSuggestions(); 240 return; 241 } 242 243 try { 244 console.log(`Fetching address suggestions for query: ${query}`); 245 const response = await fetch( 246 `https://api-adresse.data.gouv.fr/search/?q=${encodeURIComponent( 247 query 248 )}`, 249 { method: "GET" } 250 ); 251 252 if (!response.ok) throw new Error("Network response was not ok"); 253 254 const data = await response.json(); 255 console.log(`Received ${data.features?.length || 0} suggestions`); 256 this.suggestions = data.features || []; 257 this.showAddressSuggestions(); 258 } catch (error) { 259 console.error("Error fetching address suggestions:", error); 260 this.hideSuggestions(); 261 } 262 }, 300) 263 ); 264 265 // Handle keyboard navigation 266 addressInput.addEventListener("keydown", (e) => { 267 if (["ArrowDown", "ArrowUp", "Enter", "Escape"].includes(e.ke
267y)) { 268 console.log(`Key pressed: ${e.key}`); 269 } 270 271 switch (e.key) { 272 case "ArrowDown": 273 e.preventDefault(); 274 this.navigateSuggestions("down"); 275 break; 276 case "ArrowUp": 277 e.preventDefault(); 278 this.navigateSuggestions("up"); 279 break; 280 case "Enter": 281 e.preventDefault(); 282 if (this.selectedSuggestionIndex >= 0) { 283 this.selectSuggestion(this.selectedSuggestionIndex); 284 } 285 break; 286 case "Escape": 287 this.hideSuggestions(); 288 break; 289 } 290 }); 291 292 // Hide suggestions when clicking outside 293 document.addEventListener("click", (e) => { 294 if ( 295 !addressInput.contains(e.target) && 296 !this.suggestionsContainer.contains(e.target) 297 ) { 298 this.hideSuggestions(); 299 } 300 }); 301 302 console.log("Address autocomplete setup complete"); 303 } 304 305 showAddressSuggestions() { 306 if (!this.suggestions.length) { 307 this.hideSuggestions(); 308 return; 309 } 310 311 console.log(`Displaying ${this.suggestions.length} suggestions`); 312 this.suggestionsContainer.innerHTML = ""; 313 314 this.suggestions.forEach((suggestion, index) => { 315 const item = document.createElement("div"); 316 item.className = 317 "px-4 py-2 hover:bg-gray-100 cursor-pointer font-inter text-sm"; 318 item.textContent = suggestion.properties.label; 319 320 item.addEventListener("click", () => { 321 this.selectSuggestion(index); 322 }); 323 324 item.addEventListener("mouseenter", () => { 325 this.selectedSuggestionIndex = index; 326 this.highlightSuggestion(); 327 }); 328 329 this.suggestionsContainer.appendChild(item); 330 }); 331 332 // Use Tailwind's hidden class - remove it to show the container 333 this.suggestionsContainer.classList.remove("hidden"); 334 } 335 336 hideSuggestions() { 337 if (this.suggestionsContainer) { 338 // Use Tailwind's hidden class - add it to hide the container 339 this.suggestionsContainer.classList.add("hidden"); 340 } 341 } 342 343 navigateSuggestions(direction) { 344 if (!this.suggestions.length) return; 345 346 if (direction === "down") { 347 this.selectedSuggestionIndex = Math.min( 348 this.selectedSuggestionIndex + 1, 349 this.suggestions.length - 1 350 ); 351 } else { 352 this.selectedSuggestionIndex = Math.max( 353 this.selectedSuggestionIndex - 1, 354 -1 355 ); 356 } 357 358 this.highlightSuggestion(); 359 } 360 361 highlightSuggestion() { 362 const items = this.suggestionsContainer.querySelectorAll("div"); 363 items.forEach((item, index) => { 364 if (index === this.selectedSuggestionIndex) { 365 item.classList.add("bg-gray-100"); 366 } else { 367 item.classList.remove("bg-gray-100"); 368 } 369 }); 370 371 // Ensure the highlighted item is visible 372 if (this.selectedSuggestionIndex >= 0) { 373 const selectedItem = items[this.selectedSuggestionIndex]; 374 if (selectedItem) { 375 selectedItem.scrollIntoView({ block: "nearest" }); 376 } 377 } 378 } 379 380 selectSuggestion(index) { 381 const suggestion = this.suggestions[index]; 382 if (!suggestion) return; 383 384 const properties = suggestion.properties; 385 console.log("Selected address:", properties); 386 387 // Fill in the form fields 388 document.getElementById("address").value = properties.name || ""; 389 document.getElementById("city").value = properties.city || ""; 390 document.getElementById("zipCode").value = properties.postcode || ""; 391 392 // Ensure suggestions are hidden after selection 393 this.hideSuggestions(); 394 395 // Clear the suggestions array to prevent them from showing again 396 this.suggestions = []; 397 398 // Update the next button state after autocomplete 399 this.updateNextButtonState(); 400 } 401 402 // Utility function to debounce API calls 403 debounce(func, wait) { 404 let timeout; 405 return function executedFunction(...args) { 406 const later = () => { 407 clearTimeout(timeout); 408 func(...args); 409 }; 410 clearTimeout(timeout); 411 timeout = setTimeout(later, wait); 412 }; 413 } 414} 415 416// Initialize the personal info manager 417console.log("Initializing PersonalInfoManager"); 418const manager = new PersonalInfoManager();
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.