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.