189 lines
5.8 KiB
JavaScript
189 lines
5.8 KiB
JavaScript
/**
|
|
* Gestion du formulaire de contact en modal avec envoi AJAX.
|
|
* Protections : CSRF, honeypot, limitation de débit côté session.
|
|
*/
|
|
class ContactForm {
|
|
/**
|
|
* @param {HTMLFormElement} form
|
|
* @param {HTMLElement} modal
|
|
* @param {string} url - endpoint POST
|
|
* @param {string} csrfToken
|
|
*/
|
|
constructor(form, modal, url, csrfToken) {
|
|
this.form = form;
|
|
this.modal = modal;
|
|
this.url = url;
|
|
this.csrfToken = csrfToken;
|
|
|
|
this.overlay = modal.querySelector('#contact-overlay');
|
|
this.btnClose = modal.querySelector('#contact-modal-close');
|
|
this.btnOpen = document.querySelector('#open-contact-modal');
|
|
this.submit = form.querySelector('#contact-submit');
|
|
this.success = modal.querySelector('#contact-success');
|
|
this.globalErr = modal.querySelector('#contact-global-error');
|
|
|
|
this._bindEvents();
|
|
}
|
|
|
|
// ── Initialisation ─────────────────────────────────────────────────────────
|
|
|
|
_bindEvents() {
|
|
// Ouvrir
|
|
if (this.btnOpen) {
|
|
this.btnOpen.addEventListener('click', () => {
|
|
this._setPropertyId(this.btnOpen.dataset.property);
|
|
this.open();
|
|
});
|
|
}
|
|
|
|
// Fermer
|
|
if (this.btnClose) this.btnClose.addEventListener('click', () => this.close());
|
|
if (this.overlay) this.overlay.addEventListener('click', () => this.close());
|
|
|
|
// Clavier
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && !this.modal.hidden) this.close();
|
|
});
|
|
|
|
// Soumission
|
|
this.form.addEventListener('submit', (e) => this._handleSubmit(e));
|
|
|
|
// Nettoyage des erreurs à la saisie
|
|
this.form.querySelectorAll('input, textarea').forEach((el) => {
|
|
el.addEventListener('input', () => this._clearFieldError(el.name));
|
|
});
|
|
}
|
|
|
|
// ── API publique ───────────────────────────────────────────────────────────
|
|
|
|
open() {
|
|
this._resetForm();
|
|
this.modal.hidden = false;
|
|
document.body.style.overflow = 'hidden';
|
|
this.form.querySelector('#id_name')?.focus();
|
|
}
|
|
|
|
close() {
|
|
this.modal.hidden = true;
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// ── Soumission ─────────────────────────────────────────────────────────────
|
|
|
|
async _handleSubmit(e) {
|
|
e.preventDefault();
|
|
if (!this._validateLocal()) return;
|
|
|
|
this._setLoading(true);
|
|
this._hideGlobalError();
|
|
|
|
const formData = new FormData(this.form);
|
|
|
|
try {
|
|
const response = await fetch(this.url, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-CSRFToken': this.csrfToken,
|
|
},
|
|
});
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this._showSuccess();
|
|
} else if (data.errors) {
|
|
this._showFieldErrors(data.errors);
|
|
} else if (data.error) {
|
|
this._showGlobalError(data.error);
|
|
}
|
|
} catch (err) {
|
|
this._showGlobalError(
|
|
(window.I18N?.networkError) || 'Une erreur est survenue. Veuillez réessayer.'
|
|
);
|
|
} finally {
|
|
this._setLoading(false);
|
|
}
|
|
}
|
|
|
|
// ── Validation locale légère ───────────────────────────────────────────────
|
|
|
|
_validateLocal() {
|
|
let valid = true;
|
|
const name = this.form.querySelector('#id_name');
|
|
const email = this.form.querySelector('#id_email');
|
|
const msg = this.form.querySelector('#id_message');
|
|
|
|
if (!name.value.trim()) {
|
|
this._setFieldError('name', name.required ? 'Ce champ est requis.' : '');
|
|
valid = false;
|
|
}
|
|
if (!email.value.trim() || !email.value.includes('@')) {
|
|
this._setFieldError('email', 'Adresse email invalide.');
|
|
valid = false;
|
|
}
|
|
if (!msg.value.trim() || msg.value.trim().length < 10) {
|
|
this._setFieldError('message', 'Votre message est trop court.');
|
|
valid = false;
|
|
}
|
|
return valid;
|
|
}
|
|
|
|
// ── Utilitaires ────────────────────────────────────────────────────────────
|
|
|
|
_setPropertyId(id) {
|
|
const input = this.form.querySelector('#id_property_id');
|
|
if (input) input.value = id || '';
|
|
}
|
|
|
|
_setLoading(loading) {
|
|
if (!this.submit) return;
|
|
const i18n = window.I18N || {};
|
|
this.submit.disabled = loading;
|
|
this.submit.textContent = loading
|
|
? (i18n.sending || 'Envoi en cours…')
|
|
: (i18n.send || 'Envoyer');
|
|
}
|
|
|
|
_showSuccess() {
|
|
this.form.hidden = true;
|
|
this.success.hidden = false;
|
|
}
|
|
|
|
_resetForm() {
|
|
this.form.reset();
|
|
this.form.hidden = false;
|
|
this.success.hidden = true;
|
|
this._hideGlobalError();
|
|
this.form.querySelectorAll('.field-error').forEach(el => { el.textContent = ''; });
|
|
}
|
|
|
|
_setFieldError(name, msg) {
|
|
const el = this.form.querySelector(`.field-error[data-field="${name}"]`);
|
|
if (el) el.textContent = msg;
|
|
}
|
|
|
|
_clearFieldError(name) {
|
|
this._setFieldError(name, '');
|
|
}
|
|
|
|
_showFieldErrors(errors) {
|
|
for (const [field, msgs] of Object.entries(errors)) {
|
|
const list = Array.isArray(msgs) ? msgs : [msgs];
|
|
this._setFieldError(field, list.join(' '));
|
|
}
|
|
}
|
|
|
|
_showGlobalError(msg) {
|
|
if (!this.globalErr) return;
|
|
this.globalErr.textContent = msg;
|
|
this.globalErr.hidden = false;
|
|
}
|
|
|
|
_hideGlobalError() {
|
|
if (this.globalErr) this.globalErr.hidden = true;
|
|
}
|
|
}
|