1/** 2 * Autocomplétion d'adresse Google Maps (Places) sur les champs de profil. 3 * 4 * Les champs sont marqués côté serveur (App\Service\ProfilChamp\AdresseAutocomplete) : 5 * data-adresse-auto="adresse|voie" â suggestions Google pendant la saisie 6 * (+ data-adresse-key, data-adresse-lang) 7 * data-adresse-auto="cp|ville|cp_ville|pays" â rempli à la sélection d'une adresse, 8 * dans le même formulaire. 9 * 10 * Le script Google n'est chargé que si la page contient un champ source. 11 * S'auto-initialise ; window.initAdresseAutocomplete(root) pour un contenu chargé en AJAX. 12 */ 13(function () { 14 'use strict'; 15 // Script inclus par la base ET par certains templates : une seule instance. 16 if (window.initAdresseAutocomplete) { return; } 17 18 // Uniquement des champs de saisie : sur la fiche contact, le <span> d'affichage porte aussi 19 // l'attribut (recopié sur l'<input> créé ensuite) et Google plante sur un élément non-input. 20 var SOURCES = 'input[data-adresse-auto="adresse"],input[data-adresse-auto="voie"]'; 21 var CIBLES = 'input[data-adresse-auto],select[data-adresse-auto],textarea[data-adresse-auto]'; 22 var loading = null; 23 24 function loadGoogle(key, lang) { 25 if (window.google && window.google.maps && window.google.maps.places) { 26 return Promise.resolve(); 27 } 28 if (loading) { return loading; } 29 // La page charge déjà Google Maps (ex. localisation job board de la fiche) : 30 // on attend ce chargement au lieu d'en lancer un second (conflit d'API). 31 if (document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]')) { 32 loading = new Promise(function (resolve, reject) { 33 var essais = 0; 34 (function attendre() { 35 if (window.google && window.google.maps && window.google.maps.places) { resolve(); return; } 36 if (++essais > 150) { reject(); return; } 37 setTimeout(attendre, 100); 38 })(); 39 }); 40 return loading; 41 } 42 loading = new Promise(function (resolve, reject) { 43 window.__mysAdresseAutoReady = function () { resolve(); }; 44 var s = document.createElement('script'); 45 s.src = 'https://maps.googleapis.com/maps/api/js?key=' + encodeURIComponent(key) 46 + '&libraries=places&language=' + encodeURIComponent(lang || 'fr') 47 + '&callback=__mysAdresseAutoReady'; 48 s.async = true; 49 s.defer = true; 50 s.onerror = function () { s.remove(); reject(new Error('Chargement Google impossible')); }; 51 document.head.appendChild(s); 52 }); 53 return loading; 54 } 55 56 function norm(v) { 57 return String(v || '').normalize('NFD').replace(/[Ì-ͯ]/g, '') 58 .toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim(); 59 } 60 61 function composants(place) { 62 var c = {}; 63 (place.address_components || []).forEach(function (comp) { 64 (comp.types || []).forEach(function (t) { 65 if (!c[t]) { c[t] = comp; } 66 }); 67 }); 68 var get = function (t, court) { return c[t] ? (court ? c[t].short_name : c[t].long_name) : ''; }; 69 var voie = [get('street_number'), get('route')].filter(Boolean).join(' '); 70 var ville = get('locality') || get('postal_town') || get('sublocality') || get('administrative_area_level_2'); 71 return { 72 voie: voie || place.name || '', 73 adresse: place.formatted_address || voie, 74 cp: get('postal_code'), 75 ville: ville, 76 pays: get('country'), 77 paysCode: get('country', true) 78 }; 79 } 80 81 function declencher(el) { 82 // Pas d'« input » sur un champ à suggestions : Google le prend pour une frappe, 83 // relance la recherche et rouvre la liste sur l'adresse qui vient d'être choisie. 84 if (!el.matches(SOURCES)) { 85 el.dispatchEvent(new Event('input', { bubbles: true })); 86 } 87 el.dispatchEvent(new Event('change', { bubbles: true })); 88 if (window.jQuery) { 89 // Selects enrichis (chosen / select2) 90 window.jQuery(el).trigger('chosen:updated'); 91 } 92 } 93 94 function remplir(el, valeur, adr) { 95 if (el.tagName === 'SELECT') { 96 var cibleTexte = norm(valeur); 97 var trouve = null; 98 Array.prototype.forEach.call(el.options, function (o) { 99 if (trouve || o.value === '') { return; } 100 if (norm(o.textContent) === cibleTexte || norm(o.value) === cibleTexte 101 || (adr && adr.paysCode && o.value.toUpperCase() === adr.paysCode.toUpperCase())) { 102 trouve = o; 103 } 104 }); 105 if (!trouve) { return; } 106 el.value = trouve.value; 107 } else { 108 if (!valeur) { return; } 109 el.value = valeur; 110 } 111 declencher(el); 112 } 113 114 function brancher(input) { 115 if (input.tagName !== 'INPUT' || input.dataset.adresseAutoInit === '1') { return; } 116 var role = input.getAttribute('data-adresse-auto'); 117 var ac = new google.maps.places.Autocomplete(input, { 118 fields: ['address_components', 'formatted_address', 'name'], 119 types: ['address'] 120 }); 121 122 // Entrée dans la liste de suggestions : choisir l'adresse, pas soumettre le formulaire. 123 input.addEventListener('keydown', function (e) { 124 if (e.key === 'Enter') { 125 var pac = document.querySelector('.pac-container'); 126 if (pac && pac.offsetParent !== null && pac.querySelector('.pac-item')) { 127 e.preventDefault(); 128 } 129 } 130 }); 131 132 ac.addListener('place_changed', function () { 133 var place = ac.getPlace(); 134 if (!place || !place.address_components) { return; } 135 var adr = composants(place); 136 input.value = (role === 'voie') ? adr.voie : adr.adresse; 137 declencher(input); 138 139 var portee = input.closest('form') || document;
140 portee.querySelectorAll(CIBLES).forEach(function (el) { 141 if (el === input) { return; } 142 switch (el.getAttribute('data-adresse-auto')) { 143 // Même champ affiché deux fois (fiche contact) : on recopie la valeur. 144 case 'adresse': remplir(el, adr.adresse); break; 145 case 'voie': remplir(el, adr.voie); break; 146 case 'cp': remplir(el, adr.cp); break; 147 case 'ville': remplir(el, adr.ville); break; 148 case 'cp_ville': remplir(el, [adr.cp, adr.ville].filter(Boolean).join(' ')); break; 149 case 'pays': 150 // Fiche contact : un champ pays (type country) stocke le code ISO. 151 remplir(el, el.getAttribute('data-adresse-format') === 'code' ? adr.paysCode : adr.pays, adr); 152 break; 153 } 154 }); 155 }); 156 input.dataset.adresseAutoInit = '1'; 157 } 158 159 function init(root) { 160 root = root || document; 161 var sources = Array.prototype.slice.call(root.querySelectorAll(SOURCES)); 162 if (root.matches && root.matches(SOURCES)) { sources.unshift(root); } 163 if (!sources.length) { return; } 164 var key = sources[0].getAttribute('data-adresse-key'); 165 if (!key) { return; } 166 if (!document.getElementById('mys-adresse-auto-style')) { 167 var st = document.createElement('style'); 168 st.id = 'mys-adresse-auto-style'; 169 // Liste de suggestions Google : au-dessus des modales, habillée comme nos menus. 170 // La mention « Google » (pied de liste) est conservée : elle est exigée par Google. 171 // Style « Flottant » (choix du 01/10/2026) : sans bordure, ombre portée, lignes arrondies. 172 st.textContent = [ 173 '.pac-container{z-index:100000;min-width:340px;margin-top:8px;padding:6px;border:0;border-radius:14px;background:#fff;', 174 'box-shadow:0 16px 40px rgba(27,37,51,.14),0 2px 6px rgba(27,37,51,.06);font-family:inherit;overflow:hidden}', 175 '.pac-container:after{margin:6px 10px 2px;opacity:.55;background-position:right center}', 176 '.pac-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border:0;border-radius:9px;cursor:pointer;', 177 'font-size:13px;line-height:1.35;color:#6a7686;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;transition:background .12s}', 178 '.pac-item:hover,.pac-item-selected,.pac-item-selected:hover{background:#f2f5f9}', 179 '.pac-item-query{font-size:13px;font-weight:600;color:#1b2533;padding-right:6px}', 180 '.pac-matched{font-weight:700}', 181 '.pac-container .pac-icon,.pac-container .pac-item-selected .pac-icon{flex:0 0 16px;width:16px;height:16px;margin:0;', 182 'background:url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%236a7686\' stroke-width=\'2\' stroke-linecap=\'round\' stroke-linejoin=\'round\'%3E%3Cpath d=\'M12 22s7-6.2 7-12a7 7 0 1 0-14 0c0 5.8 7 12 7 12z\'/%3E%3Ccircle cx=\'12\' cy=\'10\' r=\'2.5\'/%3E%3C/svg%3E") no-repeat center / 16px 16px !important}' 183 ].join(''); 184 document.head.appendChild(st); 185 } 186 loadGoogle(key, sources[0].getAttribute('data-adresse-lang')).then(function () { 187 sources.forEach(function (input) { 188 try { brancher(input); } catch (e) { /* un champ en échec reste réinitialisable */ } 189 }); 190 }).catch(function () { loading = null; /* prochain appel : réessayer le chargement */ }); 191 } 192 193 window.initAdresseAutocomplete = init; 194 195 // Champs créés après coup (édition en ligne de la fiche, modales, AJAX). 196 function observer() { 197 if (!window.MutationObserver || !document.body) { return; } 198 var timer = null; 199 new MutationObserver(function (mutations) { 200 var utile = mutations.some(function (m) { 201 return Array.prototype.some.call(m.addedNodes, function (n) { 202 return n.nodeType === 1 && (n.matches(SOURCES) || n.querySelector(SOURCES)); 203 }); 204 }); 205 if (!utile) { return; } 206 clearTimeout(timer); 207 timer = setTimeout(function () { init(document); }, 50); 208 }).observe(document.body, { childList: true, subtree: true }); 209 } 210 211 if (document.readyState === 'loading') { 212 document.addEventListener('DOMContentLoaded', function () { init(document); observer(); }); 213 } else { 214 init(document); 215 observer(); 216 } 217})();
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.