PageSourceSearch

https://rousseau5.ch/wp-content/themes/rousseau5/assets/js/forms.js?ver=1789562778

js rousseau5.ch collected 2026-10-06 12:23:13 UTC 19,020 bytes, 439 lines download raw bytes

1/**
2 * R5 — formulaires de contact/RDV → prospects Apimo.
3 *
4 * Deux mécanismes :
5 *   1. les <form class="r5-lead-form"> posés dans les pages (envoi
6 *      intercepté, repli HTML natif si le script manque) ;
7 *   2. la MODALE « créneau » — reproduction fidèle du modèle d'origine
8 *      (index.html, rappelOpen) validé par Gary le 13/08 : kicker or,
9 *      titre serif, champs en deux colonnes sur fond crème, bouton navy
10 *      pleine largeur, note de réassurance signée du broker. Elle
11 *      s'adapte au sujet et à la page via les data-attributes des
12 *      déclencheurs (.r5-lead-trigger) :
13 *        data-lead-form     visite | rappel | avant-premiere | adresse | search
14 *                           | message | contact | actualites
15 *        data-lead-subject  contexte affiché sous le titre + transmis au lead
16 *        data-lead-property référence du bien
17 *        data-lead-broker   nom du broker (ligne de contexte + note)
18 *
19 * Règle d'or n°2 : contrairement au stub d'origine, ici CHAQUE envoi
20 * part réellement vers r5/v1/lead (journal local + Apimo) — l'écran de
21 * remerciement n'apparaît qu'une fois l'envoi accepté.
22 */
23(function () {
24	'use strict';
25
26	var config = window.r5FormsConfig || {};
27
28	/* Les textes de la modale suivent la langue de la page : PHP les
29	   passe traduits dans r5FormsConfig.i18n (inc/enqueue.php). Le
30	   français écrit ici ne sert que de repli si la config manque. */
31	var I18N = config.i18n || {};
32	function t(key, fallback) { return I18N[key] || fallback; }
33	function tpl(key, fallback) {
34		var out = t(key, fallback);
35		[].slice.call(arguments, 2).forEach(function (value, i) {
36			out = out.replace('%' + (i + 1) + '$s', value).replace('%s', value);
37		});
38		return out;
39	}
40
41	/* ═══ 1. Formulaires en page ═══════════════════════════════════ */
42
43	document.querySelectorAll('form.r5-lead-form').forEach(function (form) {
44		form.addEventListener('submit', function (event) {
45			if (!config.restUrl) return;
46			event.preventDefault();
47			submitPayload(collect(form), {
48				onDone: function (ok, message) {
49					var status = statusElement(form);
50					status.textContent = '';
51					status.classList.toggle('is-error', !ok);
52					status.classList.toggle('is-success', ok);
53					if (ok) {
54						status.textContent = t('success', 'Merci, votre demande est bien enregistrée. Un courtier Rousseau 5 vous recontacte.');
55						form.reset();
56						/* Il a parlé : le pop-up de sortie (dernier-mot.js) ne se montrera plus. */
57						try { localStorage.setItem('r5_form_envoye', '1'); } catch (e) {}
58					} else {
59						status.append(message);
60					}
61				},
62				button: form.querySelector('[type="submit"]')
63			});
64		});
65	});
66
67	function collect(form) {
68		/* Un champ coché plusieurs fois (communes[]) devient un tableau ;
69		   écrasé valeur par valeur, seule la dernière commune partirait. */
70		var payload = {};
71		new FormData(form).forEach(function (value, key) {
72			var nom = key.replace(/\[\]$/, '');
73			if (nom !== key || Object.prototype.hasOwnProperty.call(payload, nom)) {
74				if (!Array.isArray(payload[nom])) { payload[nom] = payload[nom] === undefined ? [] : [payload[nom]]; }
75				payload[nom].push(value);
76			} else {
77				payload[nom] = value;
78			}
79		});
80		return payload;
81	}
82
83	function statusElement(form) {
84		var el = form.querySelector('.r5-form-status');
85		if (!el) {
86			el = document.createElement('p');
87			el.className = 'r5-form-status';
88			el.setAttribute('role', 'status');
89			el.setAttribute('aria-live', 'polite');
90			form.appendChild(el);
91		}
92		return el;
93	}
94
95	/* ═══ Envoi commun ═════════════════════════════════════════════ */
96
97	function fallbackContact() {
98		var span = document.createElement('span');
99		span.append(t('failure', "Votre demande n'a pas pu être envoyée. Réessayez, ou contactez-nous :") + ' ');
100		var tel = document.createElement('a');
101		tel.href = 'tel:+41227328000';
102		tel.textContent = '+41 22 732 8000';
103		var mail = document.createElement('a');
104		mail.href = 'mailto:[email protected]';
105		mail.textContent = '[email protected]';
106		span.append(tel, ' · ', mail);
107		return span;
108	}
109
110	function submitPayload(payload, opts) {
111		var button = opts.button;
112		var original = button ? button.textContent : '';
113		if (button) { button.disabled = true; button.textContent = t('sending', 'Envoi…'); }
114		payload.r5_js = config.js || ''; /* preuve d'exécution du script : un robot qui poste directement ne l'a pas */
115
116		fetch(config.restUrl, {
117			method: 'POST',
118			headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': config.nonce || '' },
119			body: JSON.stringify(payload)
120		})
121			.then(function (response) {
122				return response.json().then(function (data) { return { ok: response.ok && data && data.ok }; });
123			})
124			.then(function (result) {
125				if (result.ok) {
126					/* Un envoi accepté vaut un événement de mesure (inc/matomo.php l'écoute). */
127					try { document.dispatchEvent(new CustomEvent('r5:evenement', { detail: { categorie: 'formulaire', action: String(payload.form || 'inconnu') } })); } catch (e) {}
128				}
129				opts.onDone(result.ok, result.ok ? null : fallbackContact());
130			})
131			.catch(function () { opts.onDone(false, fallbackContact()); })
132			.finally(function () {
133				if (button) { button.disabled = false; button.textContent = original; }
134			});
135	}
136
137	/* ═══ 2. Modale « créneau » (modèle d'origine) ═════════════════ */
138
139	function brokerOr(broker) { return broker || t('yourBrokerUc', 'Votre courtier Rousseau 5'); }
140
141	var VARIANTS = {
142		visite: {
143			kicker: t('momentKicker', 'Vous choisissez le moment'),
144			title: t('visiteTitle', 'Proposer un créneau de visite'),
145			slot: true,
146			submit: t('visiteSubmit', 'PROPOSER CE CRÉNEAU →'),
147			placeholder: t('visitePh', 'Ex : demande de plans de vente, questions sur les finitions…'),
148			note: function (broker) { return tpl('visiteNote', '%s vous confirme le créneau rapidement — ou vous en propose un autre.', brokerOr(broker)); }
149		},
150		rappel: {
151			kicker: t('momentKicker', 'Vous choisissez le moment'),
152			title: t('rappelTitle', 'Demander un rappel'),
153			slot: true,
154			submit: t('rappelSubmit', 'DEMANDER CE RAPPEL →'),
155			placeholder: t('rappelPh', 'Ex : questions sur les lots, le calendrier, le financement…'),
156			note: function (broker) { return tpl('rappelNote', '%s vous rappelle au créneau choisi — ou vous en propose un autre.', brokerOr(broker)); }
157		},
158		'avant-premiere': {
159			kicker: t('avpKicker', 'Projets neufs · Rive Gauche'),
160			title: t('avpTitle', 'Recevoir les avant-premières'),
161			slot: false,
162			submit: t('avpSubmit', "M'INSCRIRE →"),
163			placeholder: t('avpPh', 'Ex : type de bien recherché, communes, budget…'),
164			note: function () { return t('avpNote', 'Vous serez informé avant la mise en marché des prochains projets.'); }
165		},
166		adresse: {
167			kicker: t('adrKicker', 'Demande qualifiée'),
168			title: t('adrTitle', "Recevoir l'adresse exacte"),
169			slot: false,
170			submit: t('adrSubmit', 'ENVOYER MA DEMANDE →'),
171			placeholder: t('adrPh', 'Ex : votre projet, votre délai…'),
172			note: function (broker) { return tpl('perso24Note', '%s vous répond personnellement, sous 24h.', brokerOr(broker)); }
173		},
174		search: {
175			kicker: t('searchKicker', 'Recherches actives'),
176			title: t('searchTitle', 'Mon bien correspond à une recherche'),
177			slot: false,
178			placeholder: t('searchPh', 'Ex : type de bien, commune, caractéristiques…'),
179			submit: t('searchSubmit', 'CONTACTER UN BROKER →'),
180			note: function () { return t('broker24Note', 'Un broker Rousseau 5 vous répond sous 24h.'); }
181		},
182		message: {
183			kicker: t('msgKicker', 'Échange direct'),
184			title: t('msgTitle', 'Écrire au broker'),
185			slot: false,
186			submit: t('msgSubmit', 'ENVOYER MON MESSAGE →'),
187			placeholder: t('msgPh', 'Votre message au broker…'),
188			note: function (broker) { return tpl('perso24Note', '%s vous répond personnellement, sous 24h.', brokerOr(broker)); }
189		},
190		/* « Recevoir le dossier » d'un site dédié (ALLURE, Florissant…).
191		   La demande arrive dans Apimo sous « Demande de dossier complet »,
192		   pas sous « Demande de visite » : c'est ce que le courtier lit.
193		   Pas de créneau : on demande un document, pas un rendez-vous.
194		   La brochure n'est jamais en téléchargement libre — Rousseau 5
195		   l'envoie (doctrine BOTANIA). Demandé par Gary le 03/09/2026. */
196		brochure: {
197			kicker: t('dossierKicker', 'Dossier complet'),
198			title: t('dossierTitle', 'Recevoir le dossier complet'),
199			slot: false,
200			submit: t('dossierSubmit', 'RECEVOIR LE DOSSIER →'),
201			placeholder: t('dossierPh', 'Ex : le lot qui vous intéresse, votre projet, votre délai…'),
202			note: function (broker) { return tpl('dossierNote', '%s vous envoie le dossier personnellement, sous 24h.', brokerOr(broker)); }
203		},
204		/* La vignette « activité du marché » de l'accueil ouvre celle-ci.
205		   Le libellé promet d'ÊTRE INFORMÉ, la note ne promet rien de plus
206		   que ce que le site tient réellement : la demande arrive en
207		   prospect Apimo et un broker donne suite. Aucun bulletin
208		   d'information n'est annoncé — on ne promet pas un service qui
209		   n'existe pas encore. Demandé par Gary le 01/09/2026. */
210		actualites: {
211			kicker: t('actuKicker', 'Marché de la Rive Gauche'),
212			title: t('actuTitle', 'Soyez informé des dernières actualités'),
213			slot: false,
214			submit: t('avpSubmit', "M'INSCRIRE →"),
215			placeholder: t('actuPh', 'Ex : les communes qui vous intéressent, le type de bien, votre projet…'),
216			note: function () { return t('broker24Note', 'Un broker Rousseau 5 vous répond sous 24h.'); }
217		},
218		contact: {
219			kicker: t('ctKicker', 'Rousseau 5 · Genève'),
220			title: t('ctTitle', 'Nous contacter'),
221			slot: false,
222			submit: t('adrSubmit', 'ENVOYER MA DEMANDE →'),
223			placeholder: t('ctPh', 'Votre message…'),
224			note: function () { return t('broker24Note', 'Un broker Rousseau 5 vous répond sous 24h.'); }
225		}
226	};
227
228	var overlay = null;
229	var context = {};
230
231	/* Une landing page rattachée à un bien (gabarit landing.php) porte la
232	   référence une seule fois, sur le <main> : chaque bouton de la page
233	   en hérite s'il n'en a pas. Sans ça il faudrait répéter la référence
234	   dans le HTML collé, et l'oubli d'un seul bouton enverrait un
235	   prospect sans bien dans Apimo. */
236	var landing = document.querySelector('[data-landing-property]');
237
238	document.querySelectorAll('.r5-lead-trigger').forEach(function (trigger) {
239		trigger.addEventListener('click', function (event) {
240			/* Un déclencheur peut être un <a> — sur une landing page, garder
241			   le lien permet à la page de rester utilisable sans JS (il
242			   descend vers la section contact). Dès que la modale s'ouvre,
243			   le saut n'a plus lieu d'être. */
244			if ('A' === trigger.tagName) {
245				event.preventDefault();
246			}
247			var d = trigger.dataset;
248			openModal({
249				form: d.leadForm || 'contact',
250				subject: d.leadSubject || (landing ? landing.dataset.landingSubject : '') || '',
251				property: d.leadProperty || (landing ? landing.dataset.landingProperty : '') || '',
252				broker: d.leadBroker || (landing ? landing.dataset.landingBroker : '') || ''
253			});
254		});
255	});
256
257	function el(tag, attrs, children) {
258		var node = document.createElement(tag);
259		Object.keys(attrs || {}).forEach(function (k) {
260			if (k === 'text') node.textContent = attrs[k];
261			else node.setAttribute(k, attrs[k]);
262		});
263		(children || []).forEach(function (child) { node.appendChild(child); });
264		return node;
265	}
266
267	function field(id, label, input) {
268		return el('div', {}, [el('label', { for: id, text: label }), input]);
269	}
270
271	function openModal(ctx) {
272		context = ctx;
273		var variant = VARIANTS[ctx.form] || VARIANTS.contact;
274
275		if (!overlay) {
276			overlay = el('div', { class: 'rappel-overlay', role: 'dialog', 'aria-modal'
276: 'true' });
277			overlay.addEventListener('click', function (event) {
278				if (event.target === overlay) closeModal();
279			});
280			document.addEventListener('keydown', function (event) {
281				if (event.key === 'Escape' && overlay.classList.contains('open')) closeModal();
282			});
283			document.body.appendChild(overlay);
284		}
285
286		var card = el('div', { class: 'rappel-card' });
287		overlay.textContent = '';
288		overlay.appendChild(card);
289
290		var closeBtn = el('button', { type: 'button', class: 'rappel-close', 'aria-label': t('close', 'Fermer'), text: '×' });
291		closeBtn.addEventListener('click', closeModal);
292		card.appendChild(closeBtn);
293
294		card.appendChild(el('span', { class: 'r5m-kicker', text: variant.kicker }));
295		card.appendChild(el('h3', { class: 'r5m-title', text: ctx.subject && !variant.slot ? ctx.subject : variant.title }));
296
297		var subText = variant.slot
298			? [ctx.subject, ctx.broker].filter(Boolean).join(' — ')
299			: (ctx.broker || '');
300		if (subText) card.appendChild(el('p', { class: 'r5m-sub', text: subText }));
301
302		// Piège à robots, comme les formulaires en page.
303		var hp = el('input', { type: 'text', name: 'website', tabindex: '-1', autocomplete: 'off', 'aria-hidden': 'true' });
304		hp.className = 'r5-hp';
305		card.appendChild(hp);
306
307		var grid = el('div', { class: 'r5m-grid' });
308		var inputs = {
309			firstname: el('input', { type: 'text', id: 'r5m-prenom', autocomplete: 'given-name' }),
310			lastname: el('input', { type: 'text', id: 'r5m-nom', autocomplete: 'family-name' }),
311			phone: el('input', { type: 'tel', id: 'r5m-tel', autocomplete: 'tel' }),
312			email: el('input', { type: 'email', id: 'r5m-email', autocomplete: 'email' })
313		};
314		grid.appendChild(field('r5m-prenom', t('firstname', 'Prénom *'), inputs.firstname));
315		grid.appendChild(field('r5m-nom', t('lastname', 'Nom *'), inputs.lastname));
316		grid.appendChild(field('r5m-tel', t('phone', 'Téléphone *'), inputs.phone));
317		grid.appendChild(field('r5m-email', t('email', 'Email *'), inputs.email));
318
319		if (variant.slot) {
320			inputs.jour = el('input', { type: 'date', id: 'r5m-date', min: new Date().toISOString().slice(0, 10) });
321			inputs.heure = el('select', { id: 'r5m-heure' });
322			['', '09:00', '10:00', '11:00', '12:00', '14:00', '15:00', '16:00', '17:00', '18:00', 'soir'].forEach(function (slot) {
323				var label = slot === '' ? t('choose', 'Choisir…') : (slot === 'soir' ? t('evening', 'En soirée (après 18h)') : slot);
324				inputs.heure.appendChild(el('option', { value: slot, text: label }));
325			});
326			grid.appendChild(field('r5m-date', t('day', 'Jour souhaité *'), inputs.jour));
327			grid.appendChild(field('r5m-heure', t('hour', 'Heure souhaitée *'), inputs.heure));
328		}
329		card.appendChild(grid);
330
331		inputs.message = el('textarea', { id: 'r5m-msg', rows: '2', placeholder: variant.placeholder });
332		card.appendChild(el('div', { class: 'r5m-msg-wrap' }, [field('r5m-msg', t('details', 'Précisions (optionnel)'), inputs.message)]));
333
334		var submit = el('button', { type: 'button', class: 'r5m-submit', text: variant.submit });
335		card.appendChild(submit);
336		card.appendChild(el('p', { class: 'r5m-note', text: variant.note(ctx.broker) }));
337
338		var status = el('p', { class: 'r5-form-status', role: 'status', 'aria-live': 'polite' });
339		card.appendChild(status);
340
341		submit.addEventListener('click', function () {
342			if (!validate(inputs, variant)) return;
343
344			var payload = {
345				form: ctx.form,
346				subject: ctx.subject,
347				property_reference: ctx.property,
348				website: hp.value,
349				firstname: inputs.firstname.value.trim(),
350				lastname: inputs.lastname.value.trim(),
351				phone: inputs.phone.value.trim(),
352				email: inputs.email.value.trim(),
353				message: inputs.message.value.trim()
354			};
355			if (variant.slot) {
356				payload.jour = inputs.jour.value;
357				payload.heure = inputs.heure.value;
358			}
359
360			submitPayload(payload, {
361				button: submit,
362				onDone: function (ok, message) {
363					if (ok) return showThanks(card, ctx, variant, inputs);
364					status.textContent = '';
365					status.classList.add('is-error');
366					status.append(message);
367				}
368			});
369		});
370
371		overlay.classList.add('open');
372		document.body.style.overflow = 'hidden';
373		inputs.firstname.focus();
374	}
375
376	function closeModal() {
377		if (!overlay) return;
378		overlay.classList.remove('open');
379		document.body.style.overflow = '';
380	}
381
382	function validate(inputs, variant) {
383		var ok = true;
384		function mark(input, bad) {
385			input.style.borderColor = bad ? 'var(--error)' : '';
386			if (bad) ok = false;
387		}
388		mark(inputs.firstname, !inputs.firstname.value.trim());
389		mark(inputs.lastname, !inputs.lastname.value.trim());
390		var digits = inputs.phone.value.replace(/[\s\-.()]/g, '');
391		mark(inputs.phone, digits.length < 9 || !/\d{7,}/.test(digits));
392		mark(inputs.email, inputs.email.value.indexOf('@') === -1);
393		if (variant.slot) {
394			mark(inputs.jour, !inputs.jour.value);
395			mark(inputs.heure, !inputs.heure.value);
396		}
397		return ok;
398	}
399
400	/**
401	 * Écran de remerciement — repris du modèle d'origine, affiché
402	 * SEULEMENT une fois l'envoi réellement accepté par le serveur.
403	 */
404	function showThanks(card, ctx, variant, inputs) {
405		var firstname = inputs.firstname.value.trim();
406		var wrap = el('div', { class: 'r5m-thanks' });
407
408		wrap.appendChild(el('div', { class: 'r5m-thanks-title', text: t('thanksTitle', 'Merci pour votre demande') }));
409
410		var p1 = el('p', { class: 'r5m-thanks-main' });
411		if (variant.slot && inputs.jour.value) {
412			var d = new Date(inputs.jour.value + 'T12:00:00');
413			var locale = config.locale || 'fr-CH';
414			var hLabel = inputs.heure.value === 'soir' ? t('inEvening', 'en soirée') : tpl('atHour', 'à %s', inputs.heure.value);
415			p1.append(tpl('thanksSlot1', '%s, nous vous remercions de votre confiance. Votre proposition de rendez-vous —', firstname) + ' ');
416			p1.appendChild(el('strong', { text: d.toLocaleDateString(locale, { weekday: 'long' }) + ' ' + d.toLocaleDateString(locale) + ' ' + hLabel }));
417			p1.append(' ' + tpl('thanksSlot2', '— a bien été transmise à %s.', ctx.broker || t('yourBroker', 'votre courtier Rousseau 5')));
418		} else {
419			p1.append(tpl('thanksMain', '%1$s, nous vous remercions de votre confiance. Votre demande a bien été transmise à %2$s.', firstname, ctx.broker || t('team', "l'équipe Rousseau 5")));
420		}
421		wrap.appendChild(p1);
422
423		var p2 = el('p', { class: 'r5m-thanks-sub' });
424		p2.append(t('thanksSub', 'Elle sera traitée dans les meilleurs délais : vous recevrez une réponse par téléphone ou par email.'));
425		p2.appendChild(document.createElement('br'));
426		p2.appendChild(document.createElement('br'));
427		p2.append(t('thanksBye', 'À très bientôt,'));
428		p2.appendChild(document.createElement('br'));
429		p2.appendChild(el('em', { text: t('thanksTeam', "L'équipe Rousseau 5") }));
430		wrap.appendChild(p2);
431
432		var closeBtn = el('button', { type: 'button', class: 'rappel-close', 'aria-label': t('close', 'Fermer'), text: '×' });
433		closeBtn.addEventListener('click', closeModal);
434
435		card.textContent = '';
436		card.appendChild(closeBtn);
437		card.appendChild(wrap);
438	}
439})();

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.