1// Autocomplete pour le champ de recherche de ville 2(function() { 3 const input = document.getElementById('citySearch'); 4 if (!input) return; 5 6 const API_URL = '/comparator-new/api/api_autocomplete_cities.php'; 7 const MIN_CHARS = 3; 8 const LISTBOX_ID = 'autocomplete-listbox'; 9 10 let debounceTimer = null; 11 let autocompleteList = null; 12 13 14 // ARIA : accessibilité pour le seo 15 // ARIA : Configurer l'input comme combobox 16 input.setAttribute('role', 'combobox'); 17 input.setAttribute('aria-autocomplete', 'list'); 18 input.setAttribute('aria-expanded', 'false'); 19 input.setAttribute('aria-controls', LISTBOX_ID); 20 input.setAttribute('aria-haspopup', 'listbox'); 21 22 // Créer la liste de suggestions (flottante) 23 function createAutocompleteList() { 24 // Supprimer l'ancienne liste si elle existe 25 removeAutocompleteList(); 26 27 autocompleteList = document.createElement('div'); 28 autocompleteList.setAttribute('id', LISTBOX_ID); 29 autocompleteList.setAttribute('class', 'autocomplete-items'); 30 autocompleteList.setAttribute('role', 'listbox'); 31 autocompleteList.setAttribute('aria-label', 'Suggestions de villes'); 32 33 // Ajouter au body pour qu'elle flotte 34 document.body.appendChild(autocompleteList); 35 36 // ARIA : Marquer la liste comme ouverte 37 input.setAttribute('aria-expanded', 'true'); 38 39 // Positionner sous l'input 40 positionAutocompleteList(); 41 } 42 43 // Positionner la liste sous l'input 44 function positionAutocompleteList() { 45 if (!autocompleteList) return; 46 47 const rect = input.getBoundingClientRect(); 48 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 49 const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; 50 51 autocompleteList.style.cssText = ` 52 position: absolute; 53 top: ${rect.bottom + scrollTop}px; 54 left: ${rect.left + scrollLeft}px; 55 width: ${Math.max(rect.width, 300)}px; 56 z-index: 9999; 57 background: #fff; 58 border: 1px solid #ddd; 59 border-radius: 8px; 60 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 61 max-height: 250px; 62 overflow-y: auto; 63 `; 64 } 65 66 // Supprimer la liste de suggestions 67 function removeAutocompleteList() { 68 if (autocompleteList) { 69 autocompleteList.remove(); 70 autocompleteList = null; 71 } 72 // ARIA : Marquer la liste comme fermée 73 input.setAttribute('aria-expanded', 'false'); 74 input.removeAttribute('aria-activedescendant'); 75 } 76 77 // Afficher les suggestions 78 function showSuggestions(cities) { 79 createAutocompleteList(); 80 81 if (cities.length === 0) { 82 removeAutocompleteList(); 83 return; 84 } 85 86 cities.forEach(function(city, index) { 87 const item = document.createElement('div'); 88 const itemId = 'autocomplete-option-' + index; 89 item.setAttribute('id', itemId); 90 item.setAttribute('role', 'option'); 91 item.setAttribute('aria-selected', 'false'); 92 item.classList.add('autocomplete-item'); 93 item.style.cssText = ` 94 padding: 10px 15px; 95 cursor: pointer; 96 border-bottom: 1px solid #f0f0f0; 97 `; 98 item.innerHTML = '<strong>' + city.postalCode + '</strong> - ' + city.ville; 99 100 // Hover effect 101 item.addEventListener('mouseenter', function() { 102 this.style.backgroundColor = '#e9f7f6'; 103 }); 104 item.addEventListener('mouseleave', function() { 105 if (!this.classList.contains('autocomplete-active')) { 106 this.style.backgroundColor = ''; 107 } 108 }); 109 110 // Stocker les données 111 item.dataset.ville = city.ville; 112 item.dataset.postalCode = city.postalCode; 113 114 // Au clic sur une suggestion 115 item.addEventListener('click', function() { 116 input.value = this.dataset.ville; 117 118 // Remplir le champ CP s'il existe 119 const cpField = document.getElementById('cp'); 120 if (cpField) { 121 cpField.value = this.dataset.postalCode; 122 } 123 124 removeAutocompleteList(); 125 126 // Déclencher un événement change pour notifier d'autres scripts 127 input.dispatchEvent(new Event('change', { bubbles: true })); 128 }); 129
130 autocompleteList.appendChild(item); 131 }); 132 } 133 134 // Appeler l'API 135 function fetchCities(query) { 136 fetch(API_URL + '?mot=' + encodeURIComponent(query)) 137 .then(function(response) { 138 return response.json(); 139 }) 140 .then(function(data) { 141 showSuggestions(data); 142 }) 143 .catch(function(error) { 144 console.error('Erreur autocomplete:', error); 145 removeAutocompleteList(); 146 }); 147 } 148 149 // Ãcouter les frappes clavier 150 input.addEventListener('input', function() { 151 const query = this.value.trim(); 152 153 // Annuler le timer précédent 154 clearTimeout(debounceTimer); 155 156 // Minimum 3 caractères 157 if (query.length < MIN_CHARS) { 158 removeAutocompleteList(); 159 return; 160 } 161 162 // Debounce de 300ms pour éviter trop de requêtes 163 debounceTimer = setTimeout(function() { 164 fetchCities(query); 165 }, 300); 166 }); 167 168 // Fermer la liste si on clique ailleurs 169 document.addEventListener('click', function(e) { 170 if (e.target !== input && !autocompleteList?.contains(e.target)) { 171 removeAutocompleteList(); 172 } 173 }); 174 175 // Repositionner lors du scroll ou resize 176 window.addEventListener('scroll', positionAutocompleteList); 177 window.addEventListener('resize', positionAutocompleteList); 178 179 // Navigation au clavier 180 input.addEventListener('keydown', function(e) { 181 if (!autocompleteList) return; 182 183 const items = autocompleteList.querySelectorAll('.autocomplete-item'); 184 let currentIndex = -1; 185 186 items.forEach(function(item, index) { 187 if (item.classList.contains('autocomplete-active')) { 188 currentIndex = index; 189 } 190 }); 191 192 if (e.key === 'ArrowDown') { 193 e.preventDefault(); 194 currentIndex = (currentIndex + 1) % items.length;
195 setActiveItem(items, currentIndex); 196 } 197 else if (e.key === 'ArrowUp') { 198 e.preventDefault(); 199 currentIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1; 200 setActiveItem(items, currentIndex); 201 } 202 else if (e.key === 'Enter') { 203 e.preventDefault(); 204 if (currentIndex > -1 && items[currentIndex]) { 205 items[currentIndex].click(); 206 } 207 } 208 else if (e.key === 'Escape') { 209 removeAutocompleteList(); 210 } 211 }); 212 213 // Mettre en surbrillance l'élément actif 214 function setActiveItem(items, index) { 215 items.forEach(function(item) { 216 item.classList.remove('autocomplete-active'); 217 item.setAttribute('aria-selected', 'false'); 218 item.style.backgroundColor = ''; 219 }); 220 if (items[index]) { 221 items[index].classList.add('autocomplete-active'); 222 items[index].setAttribute('aria-selected', 'true'); 223 items[index].style.backgroundColor = '#e9f7f6'; 224 // ARIA : Indiquer l'élément actif au lecteur d'écran 225 input.setAttribute('aria-activedescendant', items[index].id); 226 } 227 } 228})();
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.