PageSourceSearch

https://mys-cdn.net/js/adresse-autocomplete.js?20261008b?v=20261005g

js mys-cdn.net collected 2026-10-09 04:18:33 UTC 10,948 bytes, 217 lines download raw bytes

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.