PageSourceSearch

https://www.talassa.fr/wp-content/mu-plugins/gf-installateur-routing/gf-citychoice.js?ver=1.2.0

js talassa.fr collected 2026-09-28 15:00:07 UTC 9,210 bytes, 256 lines download raw bytes

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, '&amp;')
251			.replace(/</g, '&lt;')
252			.replace(/>/g, '&gt;')
253			.replace(/"/g, '&quot;')
254			.replace(/'/g, '&#039;');
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.