1/** 2 * Talassa GF Citychoice â dropdown dynamique CP â Commune â id_ville 3 * 4 * Attend, sur chaque form GF concerné : 5 * - un champ text avec la classe CSS "gf-citychoice-cp" (utilisateur y tape son CP) 6 * - un champ text avec la classe CSS "gf-citychoice-ville-display" (rempli après pick, affiche la commune choisie) 7 * - un hidden field avec la classe CSS "gf-citychoice-ville-id" (stocke l'id_ville pour le backend) 8 * 9 * Le JS injecte un <select> juste après le champ CP dès que 5 chiffres sont saisis. 10 * Le select est scopé au form courant (fonctionne pour plusieurs forms GF sur la même page). 11 */ 12(function ($) { 13 'use strict'; 14 15 var cfg = window.talassaGfCitychoice; 16 if (!cfg || !cfg.ajaxUrl || !cfg.action) { 17 if (window.console) { 18 console.warn('[GF citychoice] Configuration talassaGfCitychoice absente ou incomplète. Le mu-plugin PHP est-il bien chargé et déclenche-t-il wp_localize_script ?', cfg); 19 } 20 return; 21 } 22 console.info('[GF citychoice] Init OK â config chargée.', cfg); 23 24 // Init initial + après chaque render GF (multi-pages, AJAX, conditional logic reveal) 25 $(function () { bindAll(); }); 26 $(document).on('gform_post_render', function () { 27 console.info('[GF citychoice] Ãvénement gform_post_render reçu.'); 28 bindAll(); 29 }); 30 31 function bindAll() { 32 var $cpFields = $('.' + cfg.classCp); 33 if (!$cpFields.length) { 34 console.info('[GF citychoice] Aucun champ CP (classe "' + cfg.classCp + '") détecté sur cette page.'); 35 return; 36 } 37 console.info('[GF citychoice] ' + $cpFields.length + ' wrapper(s) CP trouvé(s), tentative de binding.'); 38 39 $cpFields.each(function () { 40 var $cpField = $(this); 41 var $cpInput = $cpField.is('input') ? $cpField : $cpField.find('input').first(); 42 if (!$cpInput.length) { 43 console.warn('[GF citychoice] Wrapper CP sans input détecté :', $cpField[0]); 44 return; 45 } 46 if ($cpInput.data('ntBound')) { 47 return; 48 } 49 $cpInput.data('ntBound', true); 50 console.info('[GF citychoice] Handler attaché sur input CP :', $cpInput[0]); 51 52 var $formWrap = $cpInput.closest('.gform_wrapper'); 53 if (!$formWrap.length) { 54 $formWrap = $cpInput.closest('form'); 55 } 56 57 // Injecter le select à côté du champ CP, à l'intérieur du même .gfield 58 var $gfield = $cpInput.closest('.gfield'); 59 var $selectWrap = $('<div class="nt-citychoice-select-wrap" style="margin-top:8px;"></div>'); 60 var $select = $('<select class="large" style="width:100%;"></select>') 61 .append('<option value="">' + escapeHtml(cfg.labelSelect) + '</option>'); 62 $selectWrap.append($select); 63 $gfield.append($selectWrap); 64 65 // Cacher le select tant qu'on n'a pas de CP valide 66 $selectWrap.hide(); 67 68 var debounceTimer = null; 69 var lastFetchedCp = null; 70 $cpInput.on('input change', function () { 71 clearTimeout(debounceTimer); 72 var cp = ($cpInput.val() || '').replace(/\D/g, '').slice(0, 5); 73 if (cp.length === 5) { 74 debounceTimer = setTimeout(function () { 75 if (cp === lastFetchedCp) { 76 return; // Même CP que la dernière requête, on ne relance rien 77 } 78 lastFetchedCp = cp; 79 fetchVilles(cp, $select, $selectWrap, $formWrap); 80 }, 250); 81 } else { 82 lastFetchedCp = null; 83 $selectWrap.hide(); 84 clearVilleFields($formWrap); 85 } 86 }); 87 88 // Trigger initial si le champ est déjà pré-rempli (page reload avec valeurs) 89 var preFill = ($cpInput.val() || '').replace(/\D/g, '').slice(0, 5); 90 if (preFill.length === 5) { 91 lastFetchedCp = preFill; 92 fetchVilles(preFill, $select, $selectWrap, $formWrap); 93 } 94 95 $select.on('change', function () { 96 var idVille = $(this).val(); 97 var displayText = $(this).find('option:selected').data('display') || ''; 98 setVilleFields($formWrap, idVille, displayText); 99 }); 100 }); 101 } 102 103 function fetchVilles(cp, $select, $selectWrap, $formWrap) { 104 console.info('[GF citychoice] Requête AJAX pour CP=' + cp); 105 $select.prop('disabled', true).html( 106 '<option value="">' + escapeHtml(cfg.labelLoading) + '</option>' 107 ); 108 $selectWrap.show(); 109 110 $.ajax({ 111 url: cfg.ajaxUrl, 112 type: 'POST', 113 dataType: 'json', 114 data: { 115 action: cfg.action, 116 nonce: cfg.nonce, 117 cp: cp 118 } 119 }).done(function (res) { 120 console.info('[GF citychoice] Réponse AJAX :', res); 121 if (!res || !res.success) { 122 $select.html('<option value="">' + escapeHtml(cfg.labelEmpty) + '</option>').prop('disabled', true); 123 return; 124 } 125 renderOptions($select, $selectWrap, $formWrap, res.data.villes || [], cp); 126 }).fail(function (xhr, status, err) { 127 console.error('[GF citychoice] AJAX échouée :', status, err, xhr); 128 $select.html('<option value="">' + escapeHtml(cfg.labelEmpty) + '</option>').prop('disabled', true); 129 }); 130 } 131 132 function renderOptions($select, $selectWrap, $formWrap, villes, cp) { 133 if (!villes.length) { 134 $select.html('<option value="">' + escapeHtml(cfg.labelEmpty) + '</option>').prop('disabled', true); 135 return; 136 } 137 138 // Alimente le select avec toutes les options (sert de fallback + de rappel après pick modal) 139 var html = '<option value="">' + escapeHtml(cfg.labelSelect) + '</option>'; 140 $.each(villes, function (i, v) { 141 // Label visible utilisateur = ville seule (installateur masqué) 142 var label = v.nom; 143 html += '<option value="' + escapeHtml(String(v.id)) + '" data-display="' + escapeHtml(label) + '">' + escapeHtml(label) + '</option>'; 144 }); 145 $select.html(html).prop('disabled', false); 146 147 if (villes.length === 1) { 148 // Auto-select silencieux si un seul résultat 149 $select.val(String(villes[0].id)).trigger('change'); 150 return; 151 } 152 153 // Plusieurs résultats : ouvrir une modale qui force le choix 154 openVilleModal(cp, villes, function (id) { 155 $select.val(String(id)).trigger('change'); 156 }); 157 } 158 159 function openVilleModal(cp, villes, onConfirm) { 160 // Purge d'une éventuelle modale précédente 161 $('#nt-ville-modal').remove(); 162
163 var $overlay = $('<div id="nt-ville-modal" class="nt-modal-overlay" role="dialog" aria-modal="true"></div>'); 164 var $modal = $('<div class="nt-modal"></div>'); 165 166 $modal.append('<h3 class="nt-modal-title">Plusieurs communes pour ' + escapeHtml(cp) + '</h3>'); 167 $modal.append('<p class="nt-modal-subtitle">Sélectionnez la vôtre :</p>'); 168 169 var $list = $('<div class="nt-modal-options"></div>'); 170 $.each(villes, function (i, v) { 171 var $option = $( 172 '<label class="nt-modal-option">' + 173 '<input type="radio" name="nt-ville-choice" value="' + escapeHtml(String(v.id)) + '">' + 174 '<span>' + escapeHtml(v.nom) + '</span>' + 175 '</label>' 176 ); 177 $list.append($option); 178 }); 179 $modal.append($list); 180 181 var $confirmBtn = $('<button type="button" class="nt-modal-confirm" disabled>Confirmer</button>'); 182 $modal.append($confirmBtn); 183 184 $overlay.append($modal); 185 $('body').append($overlay); 186 187 // Active le bouton dès qu'un choix est fait 188 $list.on('change', 'input[type="radio"]', function () { 189 $confirmBtn.prop('disabled', false); 190 }); 191 192 // Double-clic sur une option = confirmation directe 193 $list.on('dblclick', '.nt-modal-option', function () { 194 $(this).find('input[type="radio"]').prop('checked', true).trigger('change'); 195 $confirmBtn.trigger('click'); 196 }); 197 198 $confirmBtn.on('click', function () { 199 var $sel = $list.find('input[type="radio"]:checked'); 200 if (!$sel.length) return; 201 var id = $sel.val(); 202 $overlay.remove(); 203 onConfirm(id); 204 }); 205 206 // Focus premier radio pour accessibilité 207 $list.find('input[type="radio"]').first().focus(); 208 } 209 210 function setVilleFields($formWrap, idVille, displayText) { 211 var $idField = findFieldInput($formWrap, cfg.classVilleId); 212 var $dispField = findFieldInput($formWrap, cfg.classVilleDisp); 213 214 if ($idField.length) { 215 $idField.val(idVille || '').trigger('change'); 216 } else if (window.console) { 217 console.warn( 218 '[GF citychoice] Aucun champ trouvé avec la classe CSS "' + cfg.classVilleId + '". ' + 219 'Vérifie dans l\'éditeur du form GF que le champ Hidden a bien cette classe dans Advanced > CSS Class Name.' 220 ); 221 } 222 223 if ($dispField.length) { 224 $dispField.val(displayText || '').trigger('change').trigger('input'); 225 } else if (window.console) { 226 console.warn( 227 '[GF citychoice] Aucun champ trouvé avec la classe CSS "' + cfg.classVilleDisp + '". ' + 228 'Vérifie dans l\'éditeur du form GF que le champ Commune a bien cette classe dans Advanced > CSS Class Name.' 229 ); 230 } 231 } 232 233 function clearVilleFields($formWrap) { 234 setVilleFields($formWrap, '', ''); 235 } 236 237 function findFieldInput($formWrap, cls) { 238 // Cherche l'élément portant la classe : soit un wrapper .gfield, soit directement un input. 239 var $target = $formWrap.find('.' + cls); 240 if (!$target.length) return $(); 241 // Si c'est déjà un input/select/textarea, on renvoie tel quel. 242 var $inputs = $target.filter('input, select, textarea'); 243 if ($inputs.length) return $inputs.first(); 244 // Sinon c'est un wrapper : on cherche le premier input à l'intérieur. 245 return $target.find('input, select, textarea').first(); 246 } 247 248 function escapeHtml(str) { 249 return String(str) 250 .replace(/&/g, '&') 251 .replace(/</g, '<') 252 .replace(/>/g, '>') 253 .replace(/"/g, '"') 254 .replace(/'/g, '''); 255 } 256})(jQuery);
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.