Files
pta/assets/js/field-format.js

137 lines
5.4 KiB
JavaScript

/**
* 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 };
})();