/** * Contrôle de format à la saisie (`PTA.modules.fieldFormat`), page Agents. * * Vérifie l'adresse e-mail et le numéro de téléphone pendant la frappe, dans le * formulaire de création comme dans la fiche de modification. * * COPIE ASSUMÉE DES RÈGLES SERVEUR (App\Core\ContactFormat) : le projet n'a ni * bundler ni gabarit partagé PHP/JS, la duplication est le prix du retour * immédiat. Elle ne déplace PAS la décision — le serveur revérifie et refuse en * 422 quoi qu'affiche l'interface. Toute modification de ContactFormat doit être * reportée ici ; l'inverse ne suffit jamais. * * Le contrôle passe par `setCustomValidity()` plutôt que par un attribut * `pattern` : le numéro est comparé APRÈS retrait des séparateurs, ce qu'une * expression HTML ne sait pas faire. Le champ invalide bloque donc l'envoi du * formulaire par la validation native du navigateur, sans que les modules de * formulaire aient à s'en occuper. * * Les deux champs restent FACULTATIFS : vide vaut valide. */ (() => { 'use strict'; const PTA = window.PTA; if (!PTA) throw new Error('PTA core doit être chargé avant field-format.js'); /** Miroir de ContactFormat::EMAIL_PATTERN. */ const EMAIL_PATTERN = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)*\.[A-Za-z]{2,}$/; /** Miroir de ContactFormat::PHONE_FR_PATTERN. */ const PHONE_FR_PATTERN = /^(?:\+33|0033|0)[1-9]\d{8}$/; /** Miroir de ContactFormat::PHONE_INTERNATIONAL_PATTERN. */ const PHONE_INTERNATIONAL_PATTERN = /^\+[1-9]\d{7,14}$/; /** Miroir de ContactFormat::PHONE_SEPARATORS. */ const PHONE_SEPARATORS = /[\s.\-/()\u00A0\u202F]/g; /** Champs déjà branchés, pour pouvoir les réévaluer sur demande. */ const bound = new Map(); const RULES = { email: { test: (value) => EMAIL_PATTERN.test(value), invalide: 'Adresse e-mail invalide. Format attendu : prenom.nom@exemple.fr', valide: 'Adresse e-mail au bon format.', }, phone: { test: (value) => { const compact = value.replace(PHONE_SEPARATORS, ''); return PHONE_FR_PATTERN.test(compact) || PHONE_INTERNATIONAL_PATTERN.test(compact); }, invalide: 'Numéro de téléphone invalide. Format attendu : 06 12 34 56 78 ou +33 6 12 34 56 78.', valide: 'Numéro de téléphone au bon format.', }, }; /** * Branche un champ sur sa règle. * L'état est recalculé à chaque frappe, mais n'est AFFICHÉ en erreur qu'une * fois le champ quitté ou l'envoi tenté : signaler « invalide » au deuxième * caractère tapé serait juste et insupportable. */ function attach(field) { if (bound.has(field)) return; const rule = RULES[field.dataset.format]; if (!rule) return; const status = field.parentElement?.querySelector('[data-format-status]') || field.closest('.field')?.querySelector('[data-format-status]'); let touched = false; function evaluate() { const value = field.value.trim(); // Champ facultatif : vide est valide, et rien n'est affiché. const empty = value === ''; const ok = empty || rule.test(value); field.setCustomValidity(ok ? '' : rule.invalide); field.classList.toggle('field-invalid', !ok && touched); field.classList.toggle('field-valid', ok && !empty); if (!status) return; if (empty) { status.textContent = ''; status.className = 'field-format-status'; } else if (ok) { status.textContent = rule.valide; status.className = 'field-format-status field-format-ok'; } else if (touched) { status.textContent = rule.invalide; status.className = 'field-format-status field-format-error'; } else { status.textContent = ''; status.className = 'field-format-status'; } } bound.set(field, () => { touched = false; evaluate(); }); field.addEventListener('input', evaluate); field.addEventListener('blur', () => { touched = true; evaluate(); }); // Envoi refusé par la validation native : le champ fautif s'explique. field.addEventListener('invalid', () => { touched = true; evaluate(); }); // Après un form.reset(), aucun événement de saisie n'est émis : sans // cela la note du champ resterait celle de l'agent précédent. field.form?.addEventListener('reset', () => { window.setTimeout(() => { touched = false; evaluate(); }, 0); }); evaluate(); } /** * Branche les champs porteurs de `data-format` et remet à zéro l'état de * ceux déjà branchés. * * Rappelable, et RAPPELÉ : la fiche de modification est remplie par * programme (module `administration`), ce qui n'émet aucun événement de * saisie — sans ce rappel, la note afficherait l'état de l'agent précédent. */ function init() { document.querySelectorAll('[data-format]').forEach(attach); bound.forEach((reset) => reset()); } PTA.modules.fieldFormat = { init, refresh: init }; })();