import { onScopeDispose, ref } from "vue"; //#region src/Field/useFieldValidation.ts const VALIDITY_KEYS = [ "badInput", "customError", "patternMismatch", "rangeOverflow", "rangeUnderflow", "stepMismatch", "tooLong", "tooShort", "typeMismatch", "valueMissing" ]; const DEFAULT_VALIDITY_STATE = { ...Object.fromEntries(VALIDITY_KEYS.map((key) => [key, false])), valid: null }; /** * Builds a `ValidityState` from the given failing constraints. Used for * non-native controls (e.g. `Select`, `Checkbox`), which have no * `ValidityState` of their own, and to snapshot a native element's live one. */ function createValidityState(flags = {}) { const state = Object.fromEntries(VALIDITY_KEYS.map((key) => [key, Boolean(flags[key])])); return { ...state, valid: VALIDITY_KEYS.every((key) => !state[key]) }; } function normalizeErrors(result) { if (!result) return []; return (Array.isArray(result) ? result : [result]).filter(Boolean); } function isPromiseLike(value) { return typeof value === "object" && value !== null && typeof value.then === "function"; } function getNativeErrors(element) { return element?.validationMessage ? [element.validationMessage] : []; } /** * The Field validation engine, modeled on Base UI's: native constraint * validation plus a custom sync/async `validate()`. Custom errors are * installed on the native element with `setCustomValidity`, so `:invalid` * and `validationMessage` agree with the field. Timing (when `commit`/ * `change` run) is decided by the caller based on `validationMode`. */ function useFieldValidation(options) { const validity = ref({ ...DEFAULT_VALIDITY_STATE }); /** Native messages or custom `validate` messages, only while invalid. */ const errors = ref([]); /** The value validity was last computed for. */ const value = ref(null); let debounceTimer; let commitId = 0; let installedCustomValidity; function clearDebounce() { if (debounceTimer !== void 0) { clearTimeout(debounceTimer); debounceTimer = void 0; } } function setCustomValidity(element, message) { const displaced = element.validity.customError ? element.validationMessage : ""; element.setCustomValidity(message); installedCustomValidity = [ element, message, displaced ]; } function clearCustomValidity() { const record = installedCustomValidity; installedCustomValidity = void 0; if (record && (!record[0].willValidate || record[0].validationMessage === record[1])) record[0].setCustomValidity(record[2]); } function readState(source, currentValue) { const { element, getValidity } = source; let live; if (element) live = element.willValidate ? element.validity : void 0; else if (getValidity) live = getValidity(currentValue); if (!live) return { ...createValidityState() }; const state = { ...createValidityState(live), valid: live.valid }; const onlyValueMissing = state.valueMissing && VALIDITY_KEYS.every((key) => key === "valueMissing" || !state[key]); if (onlyValueMissing && !options.markedDirty.value) { state.valueMissing = false; state.valid = true; } return state; } function publish(state, messages, committedValue) { validity.value = state; errors.value = state.valid === false ? messages : []; value.value = committedValue; } /** * Validates the value now: native constraints first, then the custom * `validate` (which also runs despite native failures when validating on * change, so its messages can replace the native ones). */ function commit(currentValue, source) { clearDebounce(); const id = ++commitId; clearCustomValidity(); let state = readState(source, currentValue); const nativeErrors = state.valid === false ? getNativeErrors(source.element) : []; const validateFn = options.validate.value; if (!validateFn || state.valid === false && !options.shouldValidateOnChange()) { publish(state, nativeErrors, currentValue); return; } let result; try { result = validateFn(currentValue, options.getFormValues()); } catch (error) { console.error(error); publish(state, nativeErrors, currentValue); return; } const finish = (resolved) => { const customErrors = normalizeErrors(resolved); if (customErrors.length > 0) { state = { ...state, valid: false, customError: true }; if (source.element?.willValidate) setCustomValidity(source.element, customErrors.join("\n")); publish(state, customErrors, currentValue); } else publish(state, getNativeErrors(source.element), currentValue); }; if (!isPromiseLike(result)) { finish(result); return; } if (state.valid === false) publish(state, nativeErrors, currentValue); else if (options.validationMode.value === "onSubmit" || !validity.value.customError) publish({ ...state, valid: null }, [], currentValue); result.then((resolved) => { if (id !== commitId) return; state = readState(source, currentValue); finish(resolved); }, (error) => { console.error(error); }); } /** * Re-checks an invalid field while its value changes but on-change * validation doesn't apply: clears a resolved `valueMissing` right away, * leaving other errors for the next blur/submit. */ function revalidate(currentValue, source) { if (validity.value.valid !== false) return; commitId++; clearCustomValidity(); const live = source.element ? source.element.willValidate ? source.element.validity : void 0 : source.getValidity?.(currentValue); if (!live || live.valueMissing) return; publish({ ...createValidityState(), valid: true }, [], currentValue); } /** Runs on a value change: validates (debounced) when on-change validation applies. */ function change(currentValue, source) { clearDebounce(); if (!options.shouldValidateOnChange()) { revalidate(currentValue, source); return; } const debounceTime = options.validationDebounceTime.value; if (debounceTime && currentValue !== "") { commitId++; debounceTimer = setTimeout(() => { debounceTimer = void 0; commit(currentValue, source); }, debounceTime); return; } commit(currentValue, source); } function reset() { clearDebounce(); commitId++; clearCustomValidity(); validity.value = { ...DEFAULT_VALIDITY_STATE }; errors.value = []; value.value = null; } onScopeDispose(() => { clearDebounce(); commitId++; }); return { validity, errors, value, commit, change, reset }; } //#endregion export { createValidityState, useFieldValidation }; //# sourceMappingURL=useFieldValidation.js.map