import type { ValidationResult as LegacyValidationResult, ValidationRule as SyValidationRule, } from '@/composables/validation/useValidation' import { computed, ref, toValue, useId, watch, type Ref } from 'vue' import { devWarn } from '@/utils/devWarn' import type { ValidationRule as VuetifyValidationRule } from 'vuetify' import { useCustomValidation, type UseCustomValidationOptions } from './useCustomValidation' import { useVuetifyValidation as useVuetifyValidationComposable } from './useVuetifyValidation' import { mergeMessages } from './messageUtils' export type { VuetifyValidationRule } export type { SyValidationRule as ValidationRule } export type { LegacyValidationResult as ValidationResult } export interface FieldValidationProps { customRules?: SyValidationRule[] customSuccessRules?: SyValidationRule[] customWarningRules?: SyValidationRule[] disableErrorHandling?: boolean disabled?: boolean errorMessages?: string[] | null hasError?: boolean hasSuccess?: boolean hasWarning?: boolean isValidateOnBlur?: boolean label?: string maxErrors?: number modelValue?: unknown readonly?: boolean required?: boolean rules?: VuetifyValidationRule[] showSuccessMessages?: boolean successMessages?: string[] | null // When true (Vuetify native mode), the controller should not handle errors/successes useVuetifyValidation?: boolean warningMessages?: string[] | null } /** * Point d'entrée de la validation pour les composants de champ. * Gère à la fois la validation native Vuetify (si useVuetifyValidation vaut true) * et la validation custom Synapse (si customRules/customWarningRules/customSuccessRules sont fournis). * customRules correspond aux règles d'erreur bloquantes. * errorMessages/warningMessages/successMessages sont des messages externes injectés par le parent * et ne déclenchent aucun calcul de validation. * Expose aussi une interface unifiée pour les erreurs, avertissements, succès et la validation à la demande. * Les options permettent aux champs composites de déléguer l'enregistrement Synapse * au formulaire à leurs enfants et d'orchestrer eux-mêmes la revalidation. */ export const validationPropsDefaults = { readonly: false, disabled: false, required: false, isValidateOnBlur: true, showSuccessMessages: false, disableErrorHandling: false, customRules: () => [], customWarningRules: () => [], customSuccessRules: () => [], errorMessages: null, warningMessages: null, successMessages: null, useVuetifyValidation: false, hasError: false, hasWarning: false, hasSuccess: false, maxErrors: 1, } export function useValidation(params: { modelValue: Ref readonly: Ref disabled: Ref /** * Non lu par la validation : chaque composant transforme sa prop `required` en règle dans * `customRules`. En mode Vuetify, la règle doit être fournie dans `rules` (un avertissement * de développement le signale). */ required: Ref isValidateOnBlur: Ref showSuccessMessages: Ref disableErrorHandling: Ref label: Ref focused: Ref errorMessages?: Ref warningMessages?: Ref successMessages?: Ref hasErrorProp?: Ref hasWarningProp?: Ref hasSuccessProp?: Ref maxErrors?: Ref } & ({ useVuetifyValidation: true rules: Ref customRules?: never customWarningRules?: never customSuccessRules?: never } | { useVuetifyValidation: false customRules: Ref customWarningRules?: Ref customSuccessRules?: Ref rules?: never } | { useVuetifyValidation: Ref customRules: Ref customWarningRules?: Ref customSuccessRules?: Ref rules: Ref }), options: Pick = {}) { // En mode Vuetify, comme dans Vuetify, `required` n'ajoute que l'indication visuelle et ARIA : // sans règle dans `rules`, un champ requis vide ne bloque pas la soumission. if (import.meta.env.DEV) { watch( () => Boolean(toValue(params.useVuetifyValidation) && params.required?.value && !params.rules?.value?.length), (isRequiredUnchecked) => { if (isRequiredUnchecked) { devWarn(`[Synapse] Le champ « ${params.label?.value ?? 'sans label'} » est requis en mode Vuetify (use-vuetify-validation) sans règle Vuetify : ` + 'required n\'ajoute que l\'indication visuelle et ARIA, la soumission ne sera pas bloquée. ' + 'Ajoutez une règle, par exemple :rules="[v => !!v || \'Champ requis\']".') } }, { immediate: true }, ) } const vuetifyErrors = ref([]) const customErrors = ref([]) const innerWarnings = ref([]) const innerSuccesses = ref([]) let vuetifyValidator: ReturnType | null = null // `useVuetifyValidation` peut basculer quand il est passé en Ref : le validateur custom // est alors toujours créé, mais n'applique aucune règle et se désenregistre du SyForm // tant que le mode Vuetify est actif (sinon les customRules seraient perdues après true→false). const isCustomMode = computed(() => !toValue(params.useVuetifyValidation)) const whenCustomMode = (rules: Ref | undefined) => computed(() => isCustomMode.value ? rules?.value ?? [] : []) if (params.useVuetifyValidation !== false) { // Le validator Vuetify s'enregistre dans le VForm sous l'id `name` : il doit rester // stable, sinon après une bascule de mode (ou un changement de label) le VForm ne // reçoit plus ses mises à jour et garde un état de validité périmé. Le suffixe // unique évite la collision « Duplicate input name » quand deux champs partagent // le même label (form.update ne mettrait à jour que le premier item trouvé). const vuetifyName = toValue(params.useVuetifyValidation) && params.label?.value ? `${params.label.value}-${useId()}` : undefined const { rules } = params vuetifyValidator = useVuetifyValidationComposable( params.modelValue, // L'item VForm « fantôme » reste enregistré en mode custom : il ne doit pas // évaluer les `rules`, sinon VForm.validate() le compterait invalide et // bloquerait la soumission alors que le mode Vuetify est inactif. computed(() => isCustomMode.value ? [] : rules?.value), params.disabled, vuetifyErrors, params.hasErrorProp || ref(false), computed(() => params.errorMessages?.value || []), params.focused, params.maxErrors, computed(() => vuetifyName), params.label, params.readonly, computed(() => params.isValidateOnBlur.value ? 'blur' : 'input'), ) } const customValidator = params.useVuetifyValidation === true ? null : useCustomValidation( params.modelValue, whenCustomMode(params.customRules), whenCustomMode(params.customWarningRules), whenCustomMode(params.customSuccessRules), customErrors, innerWarnings, innerSuccesses, params.showSuccessMessages, params.label, params.focused, params.isValidateOnBlur, params.disableErrorHandling, params.readonly, params.disabled, { ...options, externalErrors: params.errorMessages, forcedError: params.hasErrorProp, enabled: isCustomMode }, ) // Bascule de mode : l'état du validateur qu'on quitte est remis à zéro, sinon une // erreur Vuetify survivrait au passage en mode custom (item VForm fantôme périmé) // ou une erreur custom resterait affichée en mode Vuetify. watch(isCustomMode, (customMode) => { if (customMode) void vuetifyValidator?.resetValidation() else customValidator?.clearValidation() }) const hasExternalErrors = computed(() => (params.errorMessages?.value?.length ?? 0) > 0) async function validate(): Promise { if (params.readonly.value || params.disabled.value || params.disableErrorHandling.value) { vuetifyErrors.value = [] customErrors.value = [] innerWarnings.value = [] innerSuccesses.value = [] // Avec disableErrorHandling, les erreurs injectées par le parent et `hasError` // restent affichés et font échouer la soumission du SyForm : validate() doit suivre. if (params.disableErrorHandling.value && !params.readonly.value && !params.disabled.value) { return !hasExternalErrors.value && !params.hasErrorProp?.value } return true } // Une erreur injectée par le parent (ex. erreur serveur) ou forcée par `hasError` est // affichée mais ne provient d'aucune règle : les validateurs ne la comptent pas. Elle // doit pourtant faire échouer la validation, comme la soumission du SyForm. if (toValue(params.useVuetifyValidation)) { const result = await vuetifyValidator!.validate() return result?.length === 0 && !hasExternalErrors.value && !params.hasErrorProp?.value } else { const result = await customValidator?.validate() return result?.state.errors.length === 0 && !hasExternalErrors.value && !params.hasErrorProp?.value } } const errors = computed(() => { const errorslist = [...params.errorMessages?.value || []] if (toValue(params.disableErrorHandling) !== true) { errorslist.push(...vuetifyErrors.value) errorslist.push(...customErrors.value) } const max = params.maxErrors?.value return mergeMessages(null, errorslist, max) }) const warnings = computed(() => { const warningsList = [...params.warningMessages?.value || []] if (toValue(params.disableErrorHandling) !== true) { warningsList.push(...innerWarnings.value) } const max = params.maxErrors?.value return mergeMessages(null, warningsList, max) }) const successes = computed(() => { const successesList = [...params.successMessages?.value || []] if (toValue(params.disableErrorHandling) !== true) { successesList.push(...innerSuccesses.value) } const max = params.maxErrors?.value return mergeMessages(null, successesList, max) }) const internalHasSuccess = computed(() => customValidator?.hasSuccess.value ?? false) const hasError = computed(() => errors.value.length > 0 || Boolean(params.hasErrorProp?.value)) const hasWarning = computed(() => warnings.value.length > 0 || Boolean(params.hasWarningProp?.value)) const state = computed(() => { if (hasError.value) return 'error' if (hasWarning.value) return 'warning' if (hasSuccess.value) return 'success' return 'default' }) // Note: vérifier si c'est la meilleure approche pour supprimer le succès en mode Vuetify const hasSuccess = computed(() => { if (toValue(params.useVuetifyValidation)) { return params.hasSuccessProp?.value ?? false } return ( (internalHasSuccess.value || (params.successMessages?.value?.length ?? 0) > 0) && !hasError.value && !hasWarning.value ) || (params.hasSuccessProp?.value ?? false) }) function clearValidation() { // `resetValidation` repasse le validator Vuetify natif à l'état vierge : // le watch de `useVuetifyValidation` en profite pour vider la ref d'erreurs // synchronisée — même canal que lorsqu'il est réinitialisé via VForm. vuetifyValidator?.resetValidation() customValidator?.clearValidation() } return { errors, warnings, successes, hasError, hasWarning, hasSuccess, state, validate, clearValidation, } }