import { create, text } from "@hesxenon/prelude/Dom.js"; import { toKebabCase } from "@hesxenon/prelude/String.js"; declare global { export type ValidationSeverity = Severity; export type ValidationMessages = { [k in keyof ValidityStateFlags]?: string; }; export type ValidationSeverities = { [k in keyof ValidityStateFlags as `${k}Severity`]: ValidationSeverity; }; export const validationMessages: ValidationMessages; } const validationMessages: ValidationMessages = { valueMissing: "Please fill out this field", rangeUnderflow: `Value must be greater than or equal to $min`, rangeOverflow: `Value must be smaller than or equal to $max`, tooShort: "Value must have at least $minlength characters", tooLong: "Value must have at most $maxlength characters", patternMismatch: "Your input must match the required format", badInput: "The field is incomplete or has invalid values", }; Object.assign(globalThis, { validationMessages, }); export type Severity = "warning" | "error" | "info"; namespace Severity { export const order = [ "info", "warning", "error", ] as const satisfies Severity[]; export const eq = (a: Severity, b: Severity) => a === b; export const gt = (a: Severity, b: Severity) => order.indexOf(a) > order.indexOf(b); export const gte = (a: Severity, b: Severity) => eq(a, b) || gt(a, b); } /** * @internal */ export const setValidity = ( validityState: ValidityStateFlags, internals: ElementInternals, elem: HTMLElement, messageAnchor?: HTMLElement, ) => { const errors: Array<[string, boolean]> = Object.entries(validityState).filter( ([, value]) => value, ); const messages = errors .map(([key]) => { const kebabKey = toKebabCase(key); return ( (kebabKey in elem ? (elem[kebabKey as keyof typeof elem] as string) : undefined) || validationMessages[key as keyof ValidationMessages] )?.replaceAll( /\$(\w+)/g, (matched, attribute) => elem.getAttribute(attribute) ?? matched, ); }) .filter((message) => message != null); const maxSeverity = errors.reduce((candidate, [key]) => { const severityKey = toKebabCase(`${key}Severity`); const severity = elem[severityKey as keyof typeof elem] != null ? (elem[severityKey as keyof typeof elem] as Severity) : "error"; return Severity.gt(severity, candidate) ? severity : candidate; }, "info"); internals.setValidity( Severity.gte(maxSeverity, "error") ? validityState : {}, messages[0], messageAnchor, ); elem .querySelectorAll("small.validation-message") .forEach((message) => message.remove()); elem.append( ...messages.map((message) => create("small", { className: "validation-message" }, [text(message)]), ), ); Severity.order.forEach((severity) => internals.states.delete(`severity-${severity}`), ); internals.states.add(`severity-${maxSeverity}`); internals.checkValidity(); };