@orkestrel/form 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/browser/index.d.ts +726 -0
- package/dist/src/browser/index.js +916 -0
- package/dist/src/browser/index.js.map +1 -0
- package/dist/src/core/index.cjs +183 -17
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +95 -15
- package/dist/src/core/index.d.ts +95 -15
- package/dist/src/core/index.js +183 -17
- package/dist/src/core/index.js.map +1 -1
- package/package.json +31 -18
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/browser/errors.ts","../../../src/core/constants.ts","../../../src/core/helpers.ts","../../../src/browser/helpers.ts","../../../src/browser/validators.ts","../../../src/browser/FormBinding.ts","../../../src/browser/factories.ts"],"sourcesContent":["import type { FormBindingErrorCode } from './types.js'\nimport { isInstance } from '@orkestrel/contract'\n\n/** Reports an incompatible control, overlapping submission, or native DOM failure. */\nexport class FormBindingError extends Error {\n\treadonly code: FormBindingErrorCode\n\treadonly context: Readonly<Record<string, unknown>> | undefined\n\n\t/**\n\t * Creates a native binding refusal.\n\t * @param code - The refusal category.\n\t * @param message - The explanation.\n\t * @param context - The associated field or cause.\n\t */\n\tconstructor(\n\t\tcode: FormBindingErrorCode,\n\t\tmessage: string,\n\t\tcontext?: Readonly<Record<string, unknown>>,\n\t) {\n\t\tsuper(message)\n\t\tthis.name = 'FormBindingError'\n\t\tthis.code = code\n\t\tthis.context = context === undefined ? undefined : Object.freeze({ ...context })\n\t}\n}\n\n/**\n * Checks whether a value is a native binding refusal.\n * @param value - The value to inspect.\n * @returns True if the value is a native binding refusal; false otherwise.\n * @example\n * ```ts\n * isFormBindingError(new FormBindingError('CONTROL', 'Duplicate text controls')) // true\n * ```\n */\nexport function isFormBindingError(value: unknown): value is FormBindingError {\n\treturn isInstance(value, FormBindingError)\n}\n","import type { FieldControl, FieldRuleName, FormStatus } from './types.js'\n\n/** Lists every field control, in the order declared by the public contract. */\nexport const FIELD_CONTROLS: readonly FieldControl[] = Object.freeze([\n\t'text',\n\t'editor',\n\t'password',\n\t'number',\n\t'date',\n\t'time',\n\t'datetime',\n\t'color',\n\t'confirm',\n\t'select',\n\t'checkbox',\n\t'file',\n])\n\n/** Lists the members every field declares, whatever its control. */\nexport const FIELD_BASE_KEYS: readonly string[] = Object.freeze([\n\t'control',\n\t'name',\n\t'label',\n\t'help',\n\t'group',\n\t'hidden',\n\t'disabled',\n\t'locked',\n\t'rule',\n\t'meta',\n])\n\n/**\n * Lists every member one field control permits, composed from {@link FIELD_BASE_KEYS} and the\n * members the control's own interface adds.\n */\nexport const FIELD_KEYS: Readonly<Record<FieldControl, readonly string[]>> = Object.freeze({\n\ttext: Object.freeze([...FIELD_BASE_KEYS, 'default', 'placeholder']),\n\teditor: Object.freeze([...FIELD_BASE_KEYS, 'default', 'placeholder']),\n\tpassword: Object.freeze([...FIELD_BASE_KEYS, 'mask']),\n\tnumber: Object.freeze([...FIELD_BASE_KEYS, 'default', 'placeholder']),\n\tdate: Object.freeze([...FIELD_BASE_KEYS, 'default']),\n\ttime: Object.freeze([...FIELD_BASE_KEYS, 'default']),\n\tdatetime: Object.freeze([...FIELD_BASE_KEYS, 'default']),\n\tcolor: Object.freeze([...FIELD_BASE_KEYS, 'default']),\n\tconfirm: Object.freeze([...FIELD_BASE_KEYS, 'default']),\n\tselect: Object.freeze([...FIELD_BASE_KEYS, 'choices', 'default', 'open']),\n\tcheckbox: Object.freeze([...FIELD_BASE_KEYS, 'choices', 'default']),\n\tfile: Object.freeze([...FIELD_BASE_KEYS, 'accept', 'multiple']),\n})\n\n/** Lists every form lifecycle status. */\nexport const FORM_STATUSES: readonly FormStatus[] = Object.freeze([\n\t'editing',\n\t'settled',\n\t'abandoned',\n])\n\n/** Holds the default failure copy for every named field rule. */\nexport const RULE_MESSAGES: Readonly<Record<FieldRuleName, string>> = Object.freeze({\n\trequired: 'This field is required',\n\tminimum: 'Must be at least {limit}',\n\tmaximum: 'Must be at most {limit}',\n\tstep: 'Must be a multiple of {limit}',\n\tpattern: 'Must match the required format',\n\temail: 'Must be a valid email address',\n\turl: 'Must be a valid URL',\n\tinteger: 'Must be an integer',\n\talphanumeric: 'Must contain only letters and numbers',\n})\n\n/** Matches a practical whole-address email shape. */\nexport const EMAIL_PATTERN = Object.freeze(/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/)\n\n/** Matches an absolute HTTP or HTTPS URL shape. */\nexport const URL_PATTERN = Object.freeze(/^https?:\\/\\/[^\\s]+$/)\n\n/** Matches one or more ASCII letters or digits. */\nexport const ALPHANUMERIC_PATTERN = Object.freeze(/^[A-Za-z0-9]+$/)\n\n/** Matches a signed or unsigned base-ten integer string. */\nexport const INTEGER_PATTERN = Object.freeze(/^[+-]?\\d+$/)\n\n/** Matches a six-digit hexadecimal color string. */\nexport const COLOR_PATTERN = Object.freeze(/^#[0-9A-Fa-f]{6}$/)\n\n/** Matches an ISO calendar date string in `YYYY-MM-DD` form. */\nexport const DATE_PATTERN = Object.freeze(/^\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])$/)\n\n/** Matches a 24-hour time string with optional seconds. */\nexport const TIME_PATTERN = Object.freeze(/^(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d)?$/)\n\n/** Matches an ISO local date and time string with optional seconds. */\nexport const DATETIME_PATTERN = Object.freeze(\n\t/^\\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\\d|3[01])T(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d)?$/,\n)\n\n/** Caps the accepted source length for an authored regular expression, at 256. */\nexport const PATTERN_LIMIT = 256\n\n/** Caps the number of fields one schema may declare, at 512. */\nexport const FIELD_LIMIT = 512\n\n/** Caps the number of groups one schema may declare, at 64. */\nexport const GROUP_LIMIT = 64\n\n/** Caps the number of choices one `select` or `checkbox` field may offer, at 1024. */\nexport const CHOICE_LIMIT = 1024\n\n/** Caps the number of entries one list-valued answer may hold, at 1024. */\nexport const LIST_LIMIT = 1024\n\n/** Caps the length, in UTF-16 code units, of a schema, group, or field name, at 128. */\nexport const NAME_LIMIT = 128\n\n/** Caps the length, in UTF-16 code units, of any single retained string, at 65536. */\nexport const STRING_LIMIT = 65536\n\n/** Caps the total length, in UTF-16 code units, of every string one schema retains, at 1048576. */\nexport const TEXT_LIMIT = 1048576\n\n/** Caps the total number of records, arrays, and leaves one schema retains, at 16384. */\nexport const NODE_LIMIT = 16384\n","import type { JSONRecord, JSONValue } from '@orkestrel/contract'\nimport type {\n\tEvaluationOptions,\n\tFieldError,\n\tFieldControl,\n\tFieldRuleName,\n\tFieldValue,\n\tFormField,\n\tFormGroup,\n\tFormSchema,\n\tFormValues,\n} from './types.js'\nimport {\n\tattempt,\n\tcloneJSONRecord,\n\tisArray,\n\tisBoolean,\n\tisFiniteNumber,\n\tisInteger,\n\tisContractError,\n\tisRecord,\n\tisString,\n\treadArrayEntries,\n} from '@orkestrel/contract'\nimport { cloneValue } from './cloners.js'\nimport {\n\tALPHANUMERIC_PATTERN,\n\tCHOICE_LIMIT,\n\tCOLOR_PATTERN,\n\tDATE_PATTERN,\n\tDATETIME_PATTERN,\n\tEMAIL_PATTERN,\n\tFIELD_CONTROLS,\n\tFIELD_LIMIT,\n\tGROUP_LIMIT,\n\tINTEGER_PATTERN,\n\tLIST_LIMIT,\n\tNAME_LIMIT,\n\tNODE_LIMIT,\n\tPATTERN_LIMIT,\n\tRULE_MESSAGES,\n\tSTRING_LIMIT,\n\tTEXT_LIMIT,\n\tTIME_PATTERN,\n\tURL_PATTERN,\n} from './constants.js'\nimport { FormError } from './errors.js'\n\n/**\n * Writes one own enumerable data property onto a record.\n *\n * @param target - The record to write into.\n * @param name - The property name to write.\n * @param value - The value to store.\n *\n * @remarks\n * Plain assignment runs an inherited setter, so writing a `__proto__` key that way reaches\n * `Object.prototype` and leaves the record without the entry. Defining the property writes the\n * record itself, whatever the prototype chain declares. The entry stays writable and configurable.\n *\n * @example\n * ```ts\n * const values: Record<string, number> = {}\n * defineEntry(values, '__proto__', 1)\n * Object.hasOwn(values, '__proto__') // true\n * ```\n */\nexport function defineEntry<T>(target: Record<string, T>, name: string, value: T): void {\n\tObject.defineProperty(target, name, {\n\t\tvalue,\n\t\tenumerable: true,\n\t\tconfigurable: true,\n\t\twritable: true,\n\t})\n}\n\n/**\n * Writes one own enumerable data property that cannot be rewritten or removed.\n *\n * @param target - The record to write into.\n * @param name - The property name to write.\n * @param value - The value to store.\n *\n * @remarks\n * The prototype-safe write of {@link defineEntry}, frozen: the entry is neither writable nor\n * configurable, so the record a parser hands back cannot be edited through the key it filled.\n *\n * @example\n * ```ts\n * const values: Record<string, number> = {}\n * freezeEntry(values, '__proto__', 1)\n * Object.getOwnPropertyDescriptor(values, '__proto__')?.writable // false\n * ```\n */\nexport function freezeEntry<T>(target: Record<string, T>, name: string, value: T): void {\n\tObject.defineProperty(target, name, {\n\t\tvalue,\n\t\tenumerable: true,\n\t\tconfigurable: false,\n\t\twritable: false,\n\t})\n}\n\n/**\n * Checks whether a value has the shape required by one field control.\n *\n * @remarks\n * Every write and every seeded value passes through this gate, and it reads `STRING_LIMIT` and\n * `LIST_LIMIT` before it consults the control, so no regular expression sees an over-long value.\n *\n * @param field - The field that owns the value.\n * @param value - The unknown value to inspect.\n * @returns True if the control can hold the value; false otherwise.\n */\nexport function matchesField(field: FormField, value: unknown): value is FieldValue {\n\tif (isString(value) && value.length > STRING_LIMIT) return false\n\n\tlet entries: readonly unknown[] | undefined\n\tif (isArray(value)) {\n\t\tconst length = attempt(() => value.length)\n\t\tif (!length.success || length.value > LIST_LIMIT) return false\n\n\t\tconst read = readArrayEntries(value)\n\t\tif (!read.success || !read.value.dense) return false\n\n\t\tentries = read.value.entries\n\t\tif (entries.some((entry) => !isString(entry) || entry.length > STRING_LIMIT)) return false\n\t}\n\n\tswitch (field.control) {\n\t\tcase 'text':\n\t\tcase 'editor':\n\t\tcase 'password':\n\t\t\treturn isString(value)\n\t\tcase 'number':\n\t\t\treturn isFiniteNumber(value)\n\t\tcase 'date':\n\t\t\treturn isString(value) && DATE_PATTERN.test(value)\n\t\tcase 'time':\n\t\t\treturn isString(value) && TIME_PATTERN.test(value)\n\t\tcase 'datetime':\n\t\t\treturn isString(value) && DATETIME_PATTERN.test(value)\n\t\tcase 'color':\n\t\t\treturn isString(value) && COLOR_PATTERN.test(value)\n\t\tcase 'confirm':\n\t\t\treturn isBoolean(value)\n\t\tcase 'select':\n\t\t\treturn (\n\t\t\t\tisString(value) &&\n\t\t\t\t!field.choices.some((choice) => choice.value === value && choice.disabled === true) &&\n\t\t\t\t(field.open === true ||\n\t\t\t\t\tfield.choices.some((choice) => choice.value === value && choice.disabled !== true))\n\t\t\t)\n\t\tcase 'checkbox':\n\t\t\treturn (\n\t\t\t\tentries !== undefined &&\n\t\t\t\tentries.every(\n\t\t\t\t\t(entry) =>\n\t\t\t\t\t\tisString(entry) &&\n\t\t\t\t\t\tfield.choices.some((choice) => choice.value === entry && choice.disabled !== true),\n\t\t\t\t) &&\n\t\t\t\tnew Set(entries).size === entries.length\n\t\t\t)\n\t\tcase 'file':\n\t\t\treturn entries !== undefined && (field.multiple === true || entries.length <= 1)\n\t}\n}\n\n/**\n * Decides whether a raw binding value projects to an answered field.\n *\n * @remarks\n * Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent\n * value and a string containing only whitespace as unanswered. Every other field value is an\n * answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:\n * its `required` rule remains presence-only.\n *\n * @param value - The raw field value, or absence.\n * @returns True if the binding preserves the value as an answer; false otherwise.\n */\nexport function matchesAnswer(value: FieldValue | undefined): boolean {\n\treturn value !== undefined && (!isString(value) || value.trim().length > 0)\n}\n\n/**\n * Checks whether a named rule applies to one field control.\n *\n * @remarks\n * The runtime control-membership check keeps this boundary total for JavaScript callers that\n * bypass the declared {@link FieldControl} contract.\n *\n * @param control - The field control to inspect.\n * @param rule - The named rule to inspect.\n * @returns True if the control evaluates that rule; false otherwise.\n */\nexport function appliesRule(control: FieldControl, rule: FieldRuleName): boolean {\n\tif (!FIELD_CONTROLS.some((candidate) => candidate === control)) return false\n\n\tswitch (rule) {\n\t\tcase 'required':\n\t\t\treturn true\n\t\tcase 'minimum':\n\t\tcase 'maximum':\n\t\t\treturn control !== 'color' && control !== 'confirm' && control !== 'select'\n\t\tcase 'step':\n\t\t\treturn control === 'number'\n\t\tcase 'pattern':\n\t\tcase 'email':\n\t\tcase 'url':\n\t\tcase 'alphanumeric':\n\t\t\treturn (\n\t\t\t\tcontrol !== 'number' &&\n\t\t\t\tcontrol !== 'confirm' &&\n\t\t\t\tcontrol !== 'checkbox' &&\n\t\t\t\tcontrol !== 'file'\n\t\t\t)\n\t\tcase 'integer':\n\t\t\treturn control !== 'confirm' && control !== 'checkbox' && control !== 'file'\n\t}\n\n\treturn false\n}\n\n/**\n * Evaluates one field rule against its current value.\n *\n * @param field - The field and rule to evaluate.\n * @param value - The current value, or absence.\n * @param values - Every value available to a custom rule.\n * @param messages - Optional rule-specific message replacements.\n * @returns Every failure in rule order.\n * @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:\n * its own value escapes unchanged, because this helper adds no boundary around it.\n */\nexport function evaluateField(\n\tfield: FormField,\n\tvalue: FieldValue | undefined,\n\tvalues: FormValues,\n\tmessages?: Readonly<Partial<Record<FieldRuleName, string>>>,\n): readonly FieldError[] {\n\tconst errors: FieldError[] = []\n\tconst rule = field.rule\n\n\tif (value === undefined) {\n\t\tif (rule?.required === true && appliesRule(field.control, 'required')) {\n\t\t\terrors.push(createFieldError(field, 'required', undefined, messages))\n\t\t}\n\t}\n\n\tif (rule === undefined) return Object.freeze(errors)\n\n\tif (rule.minimum !== undefined && appliesRule(field.control, 'minimum')) {\n\t\tlet failed = false\n\n\t\tswitch (field.control) {\n\t\t\tcase 'text':\n\t\t\tcase 'editor':\n\t\t\tcase 'password':\n\t\t\t\tfailed = isString(value) && isFiniteNumber(rule.minimum) && value.length < rule.minimum\n\t\t\t\tbreak\n\t\t\tcase 'number':\n\t\t\t\tfailed = isFiniteNumber(value) && isFiniteNumber(rule.minimum) && value < rule.minimum\n\t\t\t\tbreak\n\t\t\tcase 'date':\n\t\t\tcase 'time':\n\t\t\tcase 'datetime':\n\t\t\t\tfailed = isString(value) && isString(rule.minimum) && value < rule.minimum\n\t\t\t\tbreak\n\t\t\tcase 'checkbox':\n\t\t\tcase 'file':\n\t\t\t\tfailed = isArray(value) && isFiniteNumber(rule.minimum) && value.length < rule.minimum\n\t\t\t\tbreak\n\t\t\tcase 'color':\n\t\t\tcase 'confirm':\n\t\t\tcase 'select':\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (failed) {\n\t\t\terrors.push(createFieldError(field, 'minimum', rule.minimum, messages))\n\t\t}\n\t}\n\n\tif (rule.maximum !== undefined && appliesRule(field.control, 'maximum')) {\n\t\tlet failed = false\n\n\t\tswitch (field.control) {\n\t\t\tcase 'text':\n\t\t\tcase 'editor':\n\t\t\tcase 'password':\n\t\t\t\tfailed = isString(value) && isFiniteNumber(rule.maximum) && value.length > rule.maximum\n\t\t\t\tbreak\n\t\t\tcase 'number':\n\t\t\t\tfailed = isFiniteNumber(value) && isFiniteNumber(rule.maximum) && value > rule.maximum\n\t\t\t\tbreak\n\t\t\tcase 'date':\n\t\t\tcase 'time':\n\t\t\tcase 'datetime':\n\t\t\t\tfailed = isString(value) && isString(rule.maximum) && value > rule.maximum\n\t\t\t\tbreak\n\t\t\tcase 'checkbox':\n\t\t\tcase 'file':\n\t\t\t\tfailed = isArray(value) && isFiniteNumber(rule.maximum) && value.length > rule.maximum\n\t\t\t\tbreak\n\t\t\tcase 'color':\n\t\t\tcase 'confirm':\n\t\t\tcase 'select':\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (failed) {\n\t\t\terrors.push(createFieldError(field, 'maximum', rule.maximum, messages))\n\t\t}\n\t}\n\n\tif (rule.step !== undefined && appliesRule(field.control, 'step') && isFiniteNumber(value)) {\n\t\tconst base = isFiniteNumber(rule.minimum) ? rule.minimum : 0\n\t\tconst multiple = (value - base) / rule.step\n\n\t\tif (\n\t\t\t!isFiniteNumber(rule.step) ||\n\t\t\trule.step === 0 ||\n\t\t\t!isFiniteNumber(multiple) ||\n\t\t\tMath.abs(multiple - Math.round(multiple)) > 1e-9\n\t\t) {\n\t\t\terrors.push(createFieldError(field, 'step', rule.step, messages))\n\t\t}\n\t}\n\n\tif (isString(value)) {\n\t\tif (rule.pattern !== undefined && appliesRule(field.control, 'pattern')) {\n\t\t\tconst pattern = rule.pattern\n\n\t\t\tif (pattern.length > PATTERN_LIMIT) {\n\t\t\t\terrors.push(createFieldError(field, 'pattern', undefined, messages))\n\t\t\t} else {\n\t\t\t\tconst outcome = attempt(() => new RegExp(pattern).test(value))\n\n\t\t\t\tif (!outcome.success || !outcome.value) {\n\t\t\t\t\terrors.push(createFieldError(field, 'pattern', undefined, messages))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (rule.email === true && appliesRule(field.control, 'email') && !EMAIL_PATTERN.test(value)) {\n\t\t\terrors.push(createFieldError(field, 'email', undefined, messages))\n\t\t}\n\n\t\tif (rule.url === true && appliesRule(field.control, 'url') && !URL_PATTERN.test(value)) {\n\t\t\terrors.push(createFieldError(field, 'url', undefined, messages))\n\t\t}\n\n\t\tif (\n\t\t\trule.alphanumeric === true &&\n\t\t\tappliesRule(field.control, 'alphanumeric') &&\n\t\t\t!ALPHANUMERIC_PATTERN.test(value)\n\t\t) {\n\t\t\terrors.push(createFieldError(field, 'alphanumeric', undefined, messages))\n\t\t}\n\n\t\tif (\n\t\t\trule.integer === true &&\n\t\t\tappliesRule(field.control, 'integer') &&\n\t\t\t!INTEGER_PATTERN.test(value)\n\t\t) {\n\t\t\terrors.push(createFieldError(field, 'integer', undefined, messages))\n\t\t}\n\t}\n\n\tif (\n\t\tvalue !== undefined &&\n\t\tfield.control === 'number' &&\n\t\trule.integer === true &&\n\t\tappliesRule(field.control, 'integer') &&\n\t\t!isInteger(value)\n\t) {\n\t\terrors.push(createFieldError(field, 'integer', undefined, messages))\n\t}\n\n\tif (rule.custom !== undefined) {\n\t\tconst result = rule.custom(value, values)\n\n\t\tif (isString(result)) errors.push(Object.freeze({ field: field.name, message: result }))\n\t}\n\n\treturn Object.freeze(errors)\n}\n\n/**\n * Evaluates every active field in schema order.\n *\n * @param schema - The form schema to evaluate.\n * @param values - The values keyed by field name.\n * @param options - Optional message replacements and the effective disabled field set.\n * @returns Every field failure in schema and rule order.\n * @throws Thrown when a {@link FieldValidator} supplied through {@link FieldRule.custom} throws:\n * its own value escapes unchanged, because this helper adds no boundary around it.\n */\nexport function evaluateForm(\n\tschema: FormSchema,\n\tvalues: FormValues,\n\toptions?: EvaluationOptions,\n): readonly FieldError[] {\n\tconst errors: FieldError[] = []\n\n\tfor (const field of schema.fields) {\n\t\tconst active =\n\t\t\toptions?.disabled === undefined ? field.disabled !== true : !options.disabled.has(field.name)\n\n\t\tif (active) {\n\t\t\tconst value = Object.hasOwn(values, field.name) ? values[field.name] : undefined\n\t\t\terrors.push(...evaluateField(field, value, values, options?.messages))\n\t\t}\n\t}\n\n\treturn Object.freeze(errors)\n}\n\n/**\n * Computes the values explicitly seeded by a schema.\n *\n * @remarks\n * `password` and `file` declare no default, so a field of either control never appears in the\n * result.\n *\n * @param schema - The schema whose defaults to collect.\n * @returns A value record containing only fields with defaults.\n */\nexport function computeDefaults(schema: FormSchema): FormValues {\n\tconst defaults: Record<string, FieldValue> = {}\n\n\tfor (const field of schema.fields) {\n\t\tswitch (field.control) {\n\t\t\tcase 'checkbox':\n\t\t\t\tif (field.default !== undefined) {\n\t\t\t\t\tdefineEntry(defaults, field.name, cloneValue(field.default))\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\tcase 'password':\n\t\t\tcase 'file':\n\t\t\t\tbreak\n\t\t\tcase 'text':\n\t\t\tcase 'editor':\n\t\t\tcase 'number':\n\t\t\tcase 'date':\n\t\t\tcase 'time':\n\t\t\tcase 'datetime':\n\t\t\tcase 'color':\n\t\t\tcase 'confirm':\n\t\t\tcase 'select':\n\t\t\t\tif (field.default !== undefined) {\n\t\t\t\t\tdefineEntry(defaults, field.name, field.default)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t}\n\t}\n\n\treturn Object.freeze(defaults)\n}\n\n/**\n * Compares two field values by scalar identity or ordered list content.\n *\n * @param a - The first field value.\n * @param b - The second field value.\n * @returns True if both values contain the same answer; false otherwise.\n */\nexport function matchesValue(a: FieldValue, b: FieldValue): boolean {\n\tif (isArray(a) || isArray(b)) {\n\t\treturn (\n\t\t\tisArray(a) &&\n\t\t\tisArray(b) &&\n\t\t\ta.length === b.length &&\n\t\t\ta.every((entry, index) => entry === b[index])\n\t\t)\n\t}\n\n\treturn a === b\n}\n\n/**\n * Extracts the names whose answers differ between two form value records.\n *\n * @remarks\n * Presence is compared in both directions before present values are compared through\n * {@link matchesValue}. The returned set is a new snapshot, exposed as readonly because later\n * changes to either input never alter its membership.\n *\n * @param current - The values the form holds.\n * @param opened - The values held when the form opened.\n * @returns A readonly snapshot of changed field names.\n */\nexport function extractChanges(current: FormValues, opened: FormValues): ReadonlySet<string> {\n\tconst names = new Set([...Object.keys(current), ...Object.keys(opened)])\n\tconst changed = new Set<string>()\n\n\tfor (const name of names) {\n\t\tconst hasCurrent = Object.hasOwn(current, name)\n\t\tconst hasOpened = Object.hasOwn(opened, name)\n\n\t\tif (hasCurrent !== hasOpened) {\n\t\t\tchanged.add(name)\n\t\t\tcontinue\n\t\t}\n\n\t\tconst now = current[name]\n\t\tconst before = opened[name]\n\t\tif (now === undefined || before === undefined || !matchesValue(now, before)) changed.add(name)\n\t}\n\n\treturn changed\n}\n\n/**\n * Compares two form value records by keys and value content.\n *\n * @param a - The first value record.\n * @param b - The second value record.\n * @returns True if both records contain the same answers; false otherwise.\n */\nexport function matchesValues(a: FormValues, b: FormValues): boolean {\n\treturn extractChanges(a, b).size === 0\n}\n\n/**\n * Resolves and interpolates one rule message.\n *\n * @remarks\n * A replacement in `messages` is read first and {@link RULE_MESSAGES} supplies the copy otherwise,\n * and `{limit}` in whichever text wins is replaced with the rule's operand.\n *\n * @param rule - The rule whose message to resolve.\n * @param limit - The optional operand substituted for `{limit}`.\n * @param messages - Optional rule-specific message replacements.\n * @returns The resolved failure text.\n */\nexport function formatMessage(\n\trule: FieldRuleName,\n\tlimit?: number | string,\n\tmessages?: Readonly<Partial<Record<FieldRuleName, string>>>,\n): string {\n\tconst message = messages?.[rule] ?? RULE_MESSAGES[rule]\n\treturn limit === undefined ? message : message.replaceAll('{limit}', String(limit))\n}\n\n/**\n * Creates one named-rule failure against a field.\n *\n * @param field - The field the rule failed on.\n * @param rule - The named rule that failed.\n * @param limit - The rule's operand, substituted for `{limit}`, or absence when it carries none.\n * @param messages - Optional rule-specific message replacements.\n * @returns A frozen {@link FieldError} carrying the field's name, the resolved text, and the rule.\n *\n * @remarks\n * A `custom` validator and the form's `invalidate` method both report a message of their own\n * under no rule name, so neither builds its failure here.\n *\n * @example\n * ```ts\n * const field: FormField = { control: 'text', name: 'nickname', rule: { minimum: 3 } }\n * const error = createFieldError(field, 'minimum', 3)\n * error.message // 'Must be at least 3'\n * ```\n */\nexport function createFieldError(\n\tfield: FormField,\n\trule: FieldRuleName,\n\tlimit: number | string | undefined,\n\tmessages?: Readonly<Partial<Record<FieldRuleName, string>>>,\n): FieldError {\n\treturn Object.freeze({ field: field.name, message: formatMessage(rule, limit, messages), rule })\n}\n\n/**\n * Projects a schema into JSON while removing custom validators and absent values.\n *\n * @param schema - The schema to project.\n * @returns A deep JSON copy of the serializable schema.\n * @throws A {@link FormError} coded `SCHEMA` when accessor-bearing metadata cannot be owned. A\n * non-contract throw while reading metadata escapes unchanged.\n */\nexport function serializeForm(schema: FormSchema): JSONRecord {\n\tconst output: Record<string, JSONValue> = {}\n\n\tif (schema.name !== undefined) output.name = schema.name\n\tif (schema.label !== undefined) output.label = schema.label\n\tif (schema.help !== undefined) output.help = schema.help\n\tif (schema.groups !== undefined) {\n\t\toutput.groups = schema.groups.map((group): JSONRecord => {\n\t\t\tconst entry: Record<string, JSONValue> = { name: group.name, label: group.label }\n\t\t\tif (group.help !== undefined) entry.help = group.help\n\t\t\treturn entry\n\t\t})\n\t}\n\n\toutput.fields = schema.fields.map((field): JSONRecord => {\n\t\tconst entry: Record<string, JSONValue> = {\n\t\t\tcontrol: field.control,\n\t\t\tname: field.name,\n\t\t}\n\n\t\tif (field.label !== undefined) entry.label = field.label\n\t\tif (field.help !== undefined) entry.help = field.help\n\t\tif (field.group !== undefined) entry.group = field.group\n\t\tif (field.hidden !== undefined) entry.hidden = field.hidden\n\t\tif (field.disabled !== undefined) entry.disabled = field.disabled\n\t\tif (field.locked !== undefined) entry.locked = field.locked\n\t\tconst meta = field.meta\n\t\tif (meta !== undefined) {\n\t\t\ttry {\n\t\t\t\tentry.meta = cloneJSONRecord(meta)\n\t\t\t} catch (error) {\n\t\t\t\tif (!isContractError(error)) throw error\n\t\t\t\tthrow new FormError('SCHEMA', `Field \"${field.name}\" has metadata that cannot be owned`, {\n\t\t\t\t\tfield: field.name,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\tswitch (field.control) {\n\t\t\tcase 'text':\n\t\t\tcase 'editor':\n\t\t\tcase 'number':\n\t\t\t\tif (field.default !== undefined) entry.default = field.default\n\t\t\t\tif (field.placeholder !== undefined) entry.placeholder = field.placeholder\n\t\t\t\tbreak\n\t\t\tcase 'password':\n\t\t\t\tif (field.mask !== undefined) entry.mask = field.mask\n\t\t\t\tbreak\n\t\t\tcase 'date':\n\t\t\tcase 'time':\n\t\t\tcase 'datetime':\n\t\t\tcase 'color':\n\t\t\tcase 'confirm':\n\t\t\t\tif (field.default !== undefined) entry.default = field.default\n\t\t\t\tbreak\n\t\t\tcase 'select':\n\t\t\t\tentry.choices = field.choices.map((choice): JSONRecord => {\n\t\t\t\t\tconst option: Record<string, JSONValue> = {\n\t\t\t\t\t\tvalue: choice.value,\n\t\t\t\t\t\tlabel: choice.label,\n\t\t\t\t\t}\n\t\t\t\t\tif (choice.help !== undefined) option.help = choice.help\n\t\t\t\t\tif (choice.disabled !== undefined) option.disabled = choice.disabled\n\t\t\t\t\treturn option\n\t\t\t\t})\n\t\t\t\tif (field.default !== undefined) entry.default = field.default\n\t\t\t\tif (field.open !== undefined) entry.open = field.open\n\t\t\t\tbreak\n\t\t\tcase 'checkbox':\n\t\t\t\tentry.choices = field.choices.map((choice): JSONRecord => {\n\t\t\t\t\tconst option: Record<string, JSONValue> = {\n\t\t\t\t\t\tvalue: choice.value,\n\t\t\t\t\t\tlabel: choice.label,\n\t\t\t\t\t}\n\t\t\t\t\tif (choice.help !== undefined) option.help = choice.help\n\t\t\t\t\tif (choice.disabled !== undefined) option.disabled = choice.disabled\n\t\t\t\t\treturn option\n\t\t\t\t})\n\t\t\t\tif (field.default !== undefined) entry.default = field.default\n\t\t\t\tbreak\n\t\t\tcase 'file':\n\t\t\t\tif (field.accept !== undefined) entry.accept = field.accept\n\t\t\t\tif (field.multiple !== undefined) entry.multiple = field.multiple\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (field.rule !== undefined) {\n\t\t\tconst rule: Record<string, JSONValue> = {}\n\n\t\t\tif (field.rule.required !== undefined) rule.required = field.rule.required\n\t\t\tif (field.rule.minimum !== undefined) rule.minimum = field.rule.minimum\n\t\t\tif (field.rule.maximum !== undefined) rule.maximum = field.rule.maximum\n\t\t\tif (field.rule.step !== undefined) rule.step = field.rule.step\n\t\t\tif (field.rule.pattern !== undefined) rule.pattern = field.rule.pattern\n\t\t\tif (field.rule.email !== undefined) rule.email = field.rule.email\n\t\t\tif (field.rule.url !== undefined) rule.url = field.rule.url\n\t\t\tif (field.rule.integer !== undefined) rule.integer = field.rule.integer\n\t\t\tif (field.rule.alphanumeric !== undefined) rule.alphanumeric = field.rule.alphanumeric\n\t\t\tif (Object.keys(rule).length > 0) entry.rule = rule\n\t\t}\n\n\t\treturn entry\n\t})\n\n\treturn cloneJSONRecord(output)\n}\n\n/**\n * Selects referenced groups in first-reference field order.\n *\n * @param schema - The schema whose group references to resolve.\n * @returns The referenced schema groups without duplicates.\n */\nexport function extractGroups(schema: FormSchema): readonly FormGroup[] {\n\tconst groups: FormGroup[] = []\n\n\tif (schema.groups === undefined) return Object.freeze(groups)\n\n\tfor (const field of schema.fields) {\n\t\tif (field.group !== undefined && !groups.some((group) => group.name === field.group)) {\n\t\t\tconst group = schema.groups.find((entry) => entry.name === field.group)\n\t\t\tif (group !== undefined) groups.push(group)\n\t\t}\n\t}\n\n\treturn Object.freeze(groups)\n}\n\n/**\n * Audits a structurally valid schema for domain invariants.\n *\n * @param schema - The form schema to audit.\n * @returns Human-readable invariant violations, or an empty list when the schema is sound.\n */\nexport function auditSchema(schema: FormSchema): readonly string[] {\n\tconst faults: string[] = []\n\tconst fields = new Set<string>()\n\tconst groups = new Set<string>()\n\tlet choiceExceeded: string | undefined\n\tlet nameExceeded = schema.name !== undefined && schema.name.length > NAME_LIMIT\n\n\tif (schema.fields.length > FIELD_LIMIT) {\n\t\tfaults.push(`Schema declares more than ${FIELD_LIMIT} fields`)\n\t}\n\tif (schema.groups !== undefined && schema.groups.length > GROUP_LIMIT) {\n\t\tfaults.push(`Schema declares more than ${GROUP_LIMIT} groups`)\n\t}\n\n\tif (schema.groups !== undefined) {\n\t\tconst count = Math.min(schema.groups.length, GROUP_LIMIT + 1)\n\t\tfor (let index = 0; index < count; index += 1) {\n\t\t\tconst group = schema.groups[index]\n\t\t\tif (group !== undefined && group.name.length > NAME_LIMIT) nameExceeded = true\n\t\t}\n\t}\n\n\tconst fieldCount = Math.min(schema.fields.length, FIELD_LIMIT + 1)\n\tfor (let index = 0; index < fieldCount; index += 1) {\n\t\tconst field = schema.fields[index]\n\t\tif (field === undefined) continue\n\n\t\tif (\n\t\t\tfield.name.length > NAME_LIMIT ||\n\t\t\t(field.group !== undefined && field.group.length > NAME_LIMIT)\n\t\t) {\n\t\t\tnameExceeded = true\n\t\t}\n\t\tif (\n\t\t\tchoiceExceeded === undefined &&\n\t\t\t(field.control === 'select' || field.control === 'checkbox') &&\n\t\t\tfield.choices.length > CHOICE_LIMIT\n\t\t) {\n\t\t\tchoiceExceeded = field.name\n\t\t}\n\t}\n\n\tif (choiceExceeded !== undefined) {\n\t\tfaults.push(`Field \"${choiceExceeded}\" offers more than ${CHOICE_LIMIT} choices`)\n\t}\n\tif (nameExceeded) faults.push(`Schema contains a name longer than ${NAME_LIMIT}`)\n\n\tconst pending: unknown[] = [schema]\n\tconst metadata: boolean[] = [false]\n\tlet position = 0\n\tlet stringExceeded = false\n\tlet textExceeded = false\n\tlet nodeExceeded = false\n\tlet text = 0\n\n\twhile (position < pending.length) {\n\t\tconst node = pending[position]\n\t\tconst inMeta = metadata[position] === true\n\t\tposition += 1\n\n\t\tif (isString(node)) {\n\t\t\tif (node.length > STRING_LIMIT) stringExceeded = true\n\t\t\ttext = Math.min(TEXT_LIMIT + 1, text + node.length)\n\t\t\tif (text > TEXT_LIMIT) textExceeded = true\n\t\t\tcontinue\n\t\t}\n\n\t\tif (isArray(node)) {\n\t\t\tconst length = attempt(() => node.length)\n\t\t\tif (!length.success || length.value > NODE_LIMIT - pending.length) {\n\t\t\t\tnodeExceeded = true\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tconst read = readArrayEntries(node)\n\t\t\tif (!read.success || !read.value.dense) continue\n\n\t\t\tfor (let index = 0; index < read.value.entries.length; index += 1) {\n\t\t\t\tconst entry = read.value.entries[index]\n\t\t\t\tif (entry !== undefined) {\n\t\t\t\t\tpending.push(entry)\n\t\t\t\t\tmetadata.push(inMeta)\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (!isRecord(node)) continue\n\n\t\tconst keys = attempt(() => Object.keys(node))\n\t\tif (!keys.success) continue\n\n\t\tfor (const key of keys.value) {\n\t\t\tif (inMeta) {\n\t\t\t\tif (key.length > STRING_LIMIT) stringExceeded = true\n\t\t\t\ttext = Math.min(TEXT_LIMIT + 1, text + key.length)\n\t\t\t\tif (text > TEXT_LIMIT) textExceeded = true\n\t\t\t}\n\n\t\t\tconst value = attempt(() => node[key])\n\t\t\tif (!value.success || value.value === undefined) continue\n\t\t\tif (pending.length >= NODE_LIMIT) {\n\t\t\t\tnodeExceeded = true\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpending.push(value.value)\n\t\t\tmetadata.push(inMeta || key === 'meta')\n\t\t}\n\t}\n\n\tif (stringExceeded) faults.push(`Schema contains a string longer than ${STRING_LIMIT}`)\n\tif (textExceeded) faults.push(`Schema retains more than ${TEXT_LIMIT} string code units`)\n\tif (nodeExceeded) faults.push(`Schema retains more than ${NODE_LIMIT} nodes`)\n\n\tif (schema.groups !== undefined) {\n\t\tconst count = Math.min(schema.groups.length, GROUP_LIMIT + 1)\n\t\tfor (let index = 0; index < count; index += 1) {\n\t\t\tconst group = schema.groups[index]\n\t\t\tif (group === undefined) continue\n\t\t\tif (groups.has(group.name)) faults.push(`Group \"${group.name}\" is declared more than once`)\n\t\t\tgroups.add(group.name)\n\t\t}\n\t}\n\n\tfor (let fieldIndex = 0; fieldIndex < fieldCount; fieldIndex += 1) {\n\t\tconst field = schema.fields[fieldIndex]\n\t\tif (field === undefined) continue\n\t\tif (field.name.length === 0) faults.push('Field \"\" has an empty name')\n\t\tif (field.name === '__proto__') faults.push('Field \"__proto__\" has a refused name')\n\t\tif (fields.has(field.name)) faults.push(`Field \"${field.name}\" is declared more than once`)\n\t\tfields.add(field.name)\n\n\t\tif (field.group !== undefined && !groups.has(field.group)) {\n\t\t\tfaults.push(`Field \"${field.name}\" references missing group \"${field.group}\"`)\n\t\t}\n\n\t\tswitch (field.control) {\n\t\t\tcase 'password':\n\t\t\tcase 'file':\n\t\t\t\tbreak\n\t\t\tcase 'text':\n\t\t\tcase 'editor':\n\t\t\tcase 'number':\n\t\t\tcase 'date':\n\t\t\tcase 'time':\n\t\t\tcase 'datetime':\n\t\t\tcase 'color':\n\t\t\tcase 'confirm':\n\t\t\tcase 'select':\n\t\t\tcase 'checkbox':\n\t\t\t\tif (field.default !== undefined && !matchesField(field, field.default)) {\n\t\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid default`)\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t}\n\n\t\tif (field.control === 'select' || field.control === 'checkbox') {\n\t\t\tconst choices = new Set<string>()\n\t\t\tconst count = Math.min(field.choices.length, CHOICE_LIMIT + 1)\n\t\t\tlet enabled = 0\n\n\t\t\tfor (let index = 0; index < count; index += 1) {\n\t\t\t\tconst choice = field.choices[index]\n\t\t\t\tif (choice === undefined) continue\n\t\t\t\tif (choice.disabled !== true) enabled += 1\n\t\t\t\tif (choices.has(choice.value)) {\n\t\t\t\t\tfaults.push(`Field \"${field.name}\" offers choice \"${choice.value}\" more than once`)\n\t\t\t\t}\n\t\t\t\tchoices.add(choice.value)\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\tfield.control === 'select' &&\n\t\t\t\tfield.rule?.required === true &&\n\t\t\t\tfield.open !== true &&\n\t\t\t\tenabled === 0\n\t\t\t) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" is required but offers no enabled choice`)\n\t\t\t}\n\n\t\t\tconst minimum = field.rule?.minimum\n\t\t\tif (\n\t\t\t\tfield.control === 'checkbox' &&\n\t\t\t\tisFiniteNumber(minimum) &&\n\t\t\t\tminimum > 0 &&\n\t\t\t\tminimum > enabled\n\t\t\t) {\n\t\t\t\tfaults.push(\n\t\t\t\t\t`Field \"${field.name}\" has minimum ${minimum} but offers only ${enabled} enabled ${enabled === 1 ? 'choice' : 'choices'}`,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\n\t\tconst rule = field.rule\n\t\tif (rule === undefined) continue\n\n\t\tconst temporal =\n\t\t\tfield.control === 'date' || field.control === 'time' || field.control === 'datetime'\n\t\tif (rule.minimum !== undefined && !appliesRule(field.control, 'minimum')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has minimum on ${field.control}`)\n\t\t} else if (isString(rule.minimum) && !temporal) {\n\t\t\tfaults.push(`Field \"${field.name}\" has a string minimum on ${field.control}`)\n\t\t}\n\t\tif (rule.maximum !== undefined && !appliesRule(field.control, 'maximum')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has maximum on ${field.control}`)\n\t\t} else if (isString(rule.maximum) && !temporal) {\n\t\t\tfaults.push(`Field \"${field.name}\" has a string maximum on ${field.control}`)\n\t\t}\n\t\tif (isFiniteNumber(rule.minimum) && temporal) {\n\t\t\tfaults.push(`Field \"${field.name}\" has a numeric minimum on ${field.control}`)\n\t\t}\n\t\tif (isFiniteNumber(rule.maximum) && temporal) {\n\t\t\tfaults.push(`Field \"${field.name}\" has a numeric maximum on ${field.control}`)\n\t\t}\n\t\tif (\n\t\t\tisFiniteNumber(rule.maximum) &&\n\t\t\trule.maximum < 0 &&\n\t\t\t(field.control === 'text' ||\n\t\t\t\tfield.control === 'editor' ||\n\t\t\t\tfield.control === 'password' ||\n\t\t\t\tfield.control === 'checkbox' ||\n\t\t\t\tfield.control === 'file')\n\t\t) {\n\t\t\tfaults.push(`Field \"${field.name}\" has a negative maximum on ${field.control}`)\n\t\t}\n\n\t\tif (rule.step !== undefined && !appliesRule(field.control, 'step')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has step on ${field.control}`)\n\t\t}\n\t\tif (rule.step !== undefined && rule.step <= 0) {\n\t\t\tfaults.push(`Field \"${field.name}\" has a non-positive step`)\n\t\t}\n\n\t\tif (rule.pattern !== undefined && !appliesRule(field.control, 'pattern')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has pattern on ${field.control}`)\n\t\t}\n\t\tif (rule.email === true && !appliesRule(field.control, 'email')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has email on ${field.control}`)\n\t\t}\n\t\tif (rule.url === true && !appliesRule(field.control, 'url')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has url on ${field.control}`)\n\t\t}\n\t\tif (rule.alphanumeric === true && !appliesRule(field.control, 'alphanumeric')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has alphanumeric on ${field.control}`)\n\t\t}\n\t\tif (rule.integer === true && !appliesRule(field.control, 'integer')) {\n\t\t\tfaults.push(`Field \"${field.name}\" has integer on ${field.control}`)\n\t\t}\n\n\t\tif (rule.pattern !== undefined) {\n\t\t\tif (rule.pattern.length > PATTERN_LIMIT) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has a pattern longer than ${PATTERN_LIMIT}`)\n\t\t\t} else {\n\t\t\t\ttry {\n\t\t\t\t\tRegExp(rule.pattern)\n\t\t\t\t} catch {\n\t\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid pattern`)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (\n\t\t\t(isFiniteNumber(rule.minimum) &&\n\t\t\t\tisFiniteNumber(rule.maximum) &&\n\t\t\t\trule.minimum > rule.maximum) ||\n\t\t\t(isString(rule.minimum) && isString(rule.maximum) && rule.minimum > rule.maximum)\n\t\t) {\n\t\t\tfaults.push(`Field \"${field.name}\" has minimum greater than maximum`)\n\t\t}\n\n\t\tif (field.control === 'date') {\n\t\t\tif (isString(rule.minimum) && !DATE_PATTERN.test(rule.minimum)) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid date minimum`)\n\t\t\t}\n\t\t\tif (isString(rule.maximum) && !DATE_PATTERN.test(rule.maximum)) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid date maximum`)\n\t\t\t}\n\t\t}\n\n\t\tif (field.control === 'time') {\n\t\t\tif (isString(rule.minimum) && !TIME_PATTERN.test(rule.minimum)) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid time minimum`)\n\t\t\t}\n\t\t\tif (isString(rule.maximum) && !TIME_PATTERN.test(rule.maximum)) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid time maximum`)\n\t\t\t}\n\t\t}\n\n\t\tif (field.control === 'datetime') {\n\t\t\tif (isString(rule.minimum) && !DATETIME_PATTERN.test(rule.minimum)) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid datetime minimum`)\n\t\t\t}\n\t\t\tif (isString(rule.maximum) && !DATETIME_PATTERN.test(rule.maximum)) {\n\t\t\t\tfaults.push(`Field \"${field.name}\" has an invalid datetime maximum`)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn Object.freeze(faults)\n}\n","import type { FieldValue, FormField } from '../core/types.js'\nimport type { FormControl } from './types.js'\nimport { matchesAnswer, matchesField } from '../core/helpers.js'\nimport { FormBindingError } from './errors.js'\n\n/**\n * Checks a native control population against a field's declared value shape.\n * @param field - The declared field.\n * @param controls - Associated controls sharing its name.\n * @returns True if the population has an unambiguous compatible shape; false otherwise.\n * @example\n * ```ts\n * matchesFormControls({ control: 'text', name: 'name' }, [document.createElement('input')]) // true\n * ```\n */\nexport function matchesFormControls(field: FormField, controls: readonly FormControl[]): boolean {\n\tif (controls.length === 0) return true\n\tif (controls.length > 1 && field.control !== 'checkbox' && field.control !== 'select')\n\t\treturn false\n\treturn controls.every((control) => {\n\t\tif (control instanceof HTMLTextAreaElement)\n\t\t\treturn field.control === 'editor' && controls.length === 1\n\t\tif (control instanceof HTMLSelectElement) {\n\t\t\treturn (\n\t\t\t\tcontrols.length === 1 &&\n\t\t\t\t(control.multiple ? field.control === 'checkbox' : field.control === 'select')\n\t\t\t)\n\t\t}\n\t\tswitch (field.control) {\n\t\t\tcase 'text':\n\t\t\t\treturn ['text', 'search', 'email', 'url', 'tel', 'hidden'].includes(control.type)\n\t\t\tcase 'password':\n\t\t\t\treturn control.type === 'password'\n\t\t\tcase 'number':\n\t\t\t\treturn control.type === 'number' || control.type === 'range'\n\t\t\tcase 'datetime':\n\t\t\t\treturn control.type === 'datetime-local'\n\t\t\tcase 'confirm':\n\t\t\t\treturn control.type === 'checkbox'\n\t\t\tcase 'checkbox':\n\t\t\t\treturn (\n\t\t\t\t\tcontrol.type === 'checkbox' &&\n\t\t\t\t\tcontrols.every((entry) => entry instanceof HTMLInputElement)\n\t\t\t\t)\n\t\t\tcase 'select':\n\t\t\t\treturn (\n\t\t\t\t\tcontrol.type === 'radio' &&\n\t\t\t\t\tcontrols.every((entry) => entry instanceof HTMLInputElement && entry.type === 'radio')\n\t\t\t\t)\n\t\t\tcase 'editor':\n\t\t\t\treturn false\n\t\t\tdefault:\n\t\t\t\treturn control.type === field.control\n\t\t}\n\t})\n}\n\n/**\n * Reads one compatible native population using the document's blank-to-absence semantics.\n * @param field - The field declaring the answer shape.\n * @param controls - Controls sharing the field's name, in native order.\n * @returns The native answer, or undefined for an absent answer or empty population.\n * @throws Thrown when the population or answer is incompatible, including numeric bad input.\n * @example\n * ```ts\n * const input = document.createElement('input')\n * input.value = 'Ada'\n * readFormControls({ control: 'text', name: 'name' }, [input]) // 'Ada'\n * ```\n */\nexport function readFormControls(\n\tfield: FormField,\n\tcontrols: readonly FormControl[],\n): FieldValue | undefined {\n\tif (!matchesFormControls(field, controls)) {\n\t\tthrow new FormBindingError('CONTROL', `Incompatible controls for \"${field.name}\"`, {\n\t\t\tfield: field.name,\n\t\t})\n\t}\n\tconst first = controls[0]\n\tif (first === undefined) return undefined\n\tlet value: FieldValue | undefined\n\tif (field.control === 'checkbox') {\n\t\tvalue =\n\t\t\tfirst instanceof HTMLSelectElement\n\t\t\t\t? Array.from(first.selectedOptions, (option) => option.value)\n\t\t\t\t: controls\n\t\t\t\t\t\t.filter((control) => control instanceof HTMLInputElement && control.checked)\n\t\t\t\t\t\t.map((control) => control.value)\n\t} else if (field.control === 'select' && first instanceof HTMLInputElement) {\n\t\tvalue = controls.find(\n\t\t\t(control) => control instanceof HTMLInputElement && control.checked,\n\t\t)?.value\n\t} else if (field.control === 'confirm' && first instanceof HTMLInputElement) {\n\t\tvalue = first.checked\n\t} else if (field.control === 'number' && first instanceof HTMLInputElement) {\n\t\tif (first.validity.badInput)\n\t\t\tthrow new FormBindingError('CONTROL', 'Enter a number', { field: field.name })\n\t\tvalue = first.value === '' ? undefined : first.valueAsNumber\n\t} else if (field.control === 'file' && first instanceof HTMLInputElement) {\n\t\tvalue = Array.from(first.files ?? [], (file) => file.name)\n\t} else {\n\t\tvalue = first.value\n\t}\n\tif (!matchesAnswer(value)) return undefined\n\tif (value !== undefined && !matchesField(field, value)) {\n\t\tthrow new FormBindingError('CONTROL', `Incompatible native answer for \"${field.name}\"`, {\n\t\t\tfield: field.name,\n\t\t})\n\t}\n\treturn value\n}\n","import type { FormControl } from './types.js'\nimport { isInstance } from '@orkestrel/contract'\n\n/**\n * Checks whether a value is a native answer control.\n * @param value - The value to inspect.\n * @returns True if the value is an input, select, or textarea; false otherwise.\n * @example\n * ```ts\n * isFormControl(document.createElement('input')) // true\n * ```\n */\nexport function isFormControl(value: unknown): value is FormControl {\n\treturn (\n\t\t(typeof HTMLInputElement !== 'undefined' && isInstance(value, HTMLInputElement)) ||\n\t\t(typeof HTMLSelectElement !== 'undefined' && isInstance(value, HTMLSelectElement)) ||\n\t\t(typeof HTMLTextAreaElement !== 'undefined' && isInstance(value, HTMLTextAreaElement))\n\t)\n}\n","import type { EmitterInterface } from '@orkestrel/emitter'\nimport type { FieldError, FieldValue, FormField, FormInterface } from '../core/types.js'\nimport type {\n\tFormBindingAttribute,\n\tFormBindingEventMap,\n\tFormBindingInterface,\n\tFormBindingOptions,\n\tFormBindingValidity,\n\tFormControl,\n} from './types.js'\nimport { Emitter } from '@orkestrel/emitter'\nimport { matchesValue } from '../core/helpers.js'\nimport { FormBindingError, isFormBindingError } from './errors.js'\nimport { matchesFormControls, readFormControls } from './helpers.js'\nimport { isFormControl } from './validators.js'\n\n/** Implements a controlled native form over a borrowed document. */\nexport class FormBinding implements FormBindingInterface {\n\treadonly #element: HTMLFormElement\n\treadonly #document: FormInterface\n\treadonly #options: FormBindingOptions | undefined\n\treadonly #emitter: Emitter<FormBindingEventMap>\n\treadonly #observer: MutationObserver\n\treadonly #listeners = new AbortController()\n\treadonly #attributes = new Map<Element, Map<string, FormBindingAttribute>>()\n\treadonly #validity = new Map<FormControl, FormBindingValidity>()\n\treadonly #feedback = new Map<string, HTMLElement>()\n\treadonly #excluded = new Set<string>()\n\treadonly #logical = new Set<string>()\n\treadonly #unreadable = new Set<string>()\n\treadonly #names = new Map<FormControl, string>()\n\treadonly #refresh = this.#onState.bind(this)\n\treadonly #fill = this.#onFill.bind(this)\n\treadonly #disable = this.#onDisable.bind(this)\n\treadonly #enable = this.#onEnable.bind(this)\n\treadonly #clear = this.#onClear.bind(this)\n\treadonly #abandon = this.destroy.bind(this)\n\t#controls: ReadonlyMap<string, readonly FormControl[]> = new Map()\n\t#summary: HTMLUListElement | undefined\n\t#summaryText = ''\n\t#destroyed = false\n\t#running = false\n\t#queued = false\n\t#reading = false\n\t#excluding: string | undefined\n\t#changed = false\n\t#notified = false\n\t#scheduled = false\n\t#submitting = false\n\n\t/**\n\t * Binds existing controls to the supplied document's answers.\n\t * @param element - The native form, including its externally associated controls.\n\t * @param document - The borrowed answer document.\n\t * @param options - Feedback destinations and listeners.\n\t * @throws Thrown when controls contradict the schema or native initialization fails.\n\t */\n\tconstructor(element: HTMLFormElement, document: FormInterface, options?: FormBindingOptions) {\n\t\tthis.#element = element\n\t\tthis.#document = document\n\t\tthis.#options = options\n\t\tthis.#emitter = new Emitter<FormBindingEventMap>(options)\n\t\tthis.#observer = new MutationObserver(() => this.#onDocument())\n\t\tfor (const name of document.disabled) this.#logical.add(name)\n\t\ttry {\n\t\t\tthis.#controls = this.#discover()\n\t\t\tconst signal = this.#listeners.signal\n\t\t\telement.addEventListener('submit', (event) => this.#onSubmit(event), { signal })\n\t\t\telement.addEventListener('reset', (event) => this.#onReset(event), { signal })\n\t\t\telement.ownerDocument.addEventListener('input', (event) => this.#onInput(event), { signal })\n\t\t\telement.ownerDocument.addEventListener('change', (event) => this.#onInput(event), { signal })\n\t\t\telement.ownerDocument.addEventListener('focusout', (event) => this.#onBlur(event), { signal })\n\t\t\telement.ownerDocument.addEventListener('click', (event) => this.#onClick(event), { signal })\n\t\t\tdocument.emitter.on('fill', this.#fill)\n\t\t\tdocument.emitter.on('disable', this.#disable)\n\t\t\tdocument.emitter.on('enable', this.#enable)\n\t\t\tdocument.emitter.on('clear', this.#clear)\n\t\t\tdocument.emitter.on('abandon', this.#abandon)\n\t\t\tfor (const event of ['touch', 'validate', 'prepare', 'accept', 'abort', 'submit'] as const) {\n\t\t\t\tdocument.emitter.on(event, this.#refresh)\n\t\t\t}\n\t\t\tthis.refresh()\n\t\t\tif (!this.#destroyed) {\n\t\t\t\tthis.#observer.observe(element.ownerDocument, {\n\t\t\t\t\tsubtree: true,\n\t\t\t\t\tchildList: true,\n\t\t\t\t\tattributes: true,\n\t\t\t\t\tattributeFilter: [\n\t\t\t\t\t\t'name',\n\t\t\t\t\t\t'form',\n\t\t\t\t\t\t'id',\n\t\t\t\t\t\t'type',\n\t\t\t\t\t\t'multiple',\n\t\t\t\t\t\t'disabled',\n\t\t\t\t\t\t'required',\n\t\t\t\t\t\t'min',\n\t\t\t\t\t\t'max',\n\t\t\t\t\t\t'step',\n\t\t\t\t\t\t'pattern',\n\t\t\t\t\t\t'minlength',\n\t\t\t\t\t\t'maxlength',\n\t\t\t\t\t\t'value',\n\t\t\t\t\t\t'hidden',\n\t\t\t\t\t\t'inert',\n\t\t\t\t\t\t'style',\n\t\t\t\t\t\t'class',\n\t\t\t\t\t],\n\t\t\t\t})\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.destroy()\n\t\t\tthrow error\n\t\t}\n\t}\n\n\t/** Holds the native form. */\n\tget element(): HTMLFormElement {\n\t\treturn this.#element\n\t}\n\t/** Holds the borrowed answer document. */\n\tget document(): FormInterface {\n\t\treturn this.#document\n\t}\n\t/** Reports whether the binding released its resources. */\n\tget destroyed(): boolean {\n\t\treturn this.#destroyed\n\t}\n\t/** Holds the binding's isolated event emitter. */\n\tget emitter(): EmitterInterface<FormBindingEventMap> {\n\t\treturn this.#emitter\n\t}\n\n\t/** Imports mounted enabled controls without acknowledging their answers. */\n\tread(): void {\n\t\tif (this.#destroyed || this.#reading || this.#document.status !== 'editing') return\n\t\tconst controls = this.#discover()\n\t\tconst answers = new Map<string, FieldValue | undefined>()\n\t\tfor (const field of this.#document.schema.fields) {\n\t\t\tconst group = controls.get(field.name)\n\t\t\tif (group === undefined || field.locked || this.#logical.has(field.name)) continue\n\t\t\tconst enabled = group.filter((control) => !this.#nativeDisabled(control))\n\t\t\tif (enabled.length === 0) continue\n\t\t\tif (enabled.some((control) => control.validity.badInput)) {\n\t\t\t\tthis.#unreadable.add(field.name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tthis.#unreadable.delete(field.name)\n\t\t\tanswers.set(field.name, readFormControls(field, enabled))\n\t\t}\n\t\tthis.#controls = controls\n\t\tthis.#reading = true\n\t\ttry {\n\t\t\tthis.#exclusions()\n\t\t\tfor (const [name, value] of answers) {\n\t\t\t\tif (this.#destroyed) break\n\t\t\t\tconst values = this.#document.values\n\t\t\t\tconst previous = Object.hasOwn(values, name) ? values[name] : undefined\n\t\t\t\tif (\n\t\t\t\t\tprevious === undefined || value === undefined\n\t\t\t\t\t\t? previous !== value\n\t\t\t\t\t\t: !matchesValue(previous, value)\n\t\t\t\t)\n\t\t\t\t\tthis.#document.fill(name, value)\n\t\t\t}\n\t\t} finally {\n\t\t\tthis.#reading = false\n\t\t\tif (!this.#destroyed) this.#drain()\n\t\t}\n\t}\n\n\t/** Rediscovers controls and projects the document into the native form. */\n\trefresh(): void {\n\t\tif (this.#destroyed) return\n\t\tthis.#unreadable.clear()\n\t\tthis.#drain()\n\t}\n\n\t/** Releases owned DOM effects and listeners without destroying the document. */\n\tdestroy(): void {\n\t\tif (this.#destroyed) return\n\t\tthis.#destroyed = true\n\t\tthis.#observer.disconnect()\n\t\tthis.#listeners.abort()\n\t\tthis.#document.emitter.off('fill', this.#fill)\n\t\tthis.#document.emitter.off('disable', this.#disable)\n\t\tthis.#document.emitter.off('enable', this.#enable)\n\t\tthis.#document.emitter.off('clear', this.#clear)\n\t\tthis.#document.emitter.off('abandon', this.#abandon)\n\t\tfor (const event of ['touch', 'validate', 'prepare', 'accept', 'abort', 'submit'] as const) {\n\t\t\tthis.#document.emitter.off(event, this.#refresh)\n\t\t}\n\t\tfor (const element of this.#attributes.keys()) this.#restore(element)\n\t\tfor (const control of this.#validity.keys()) this.#restoreValidity(control)\n\t\tfor (const node of this.#feedback.values()) node.remove()\n\t\tthis.#summary?.remove()\n\t\tif (this.#document.status === 'editing') {\n\t\t\tfor (const name of this.#excluded) {\n\t\t\t\tif (!this.#logical.has(name)) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tthis.#document.enable(name)\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tthis.#emitter.emit('error', error)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tthis.#excluded.clear()\n\t\tthis.#feedback.clear()\n\t\tthis.#names.clear()\n\t\tthis.#unreadable.clear()\n\t\tthis.#logical.clear()\n\t\tthis.#controls = new Map()\n\t\tthis.#summary = undefined\n\t\tthis.#emitter.destroy()\n\t}\n\n\t#drain(): void {\n\t\tif (this.#destroyed) return\n\t\tthis.#queued = true\n\t\tif (this.#running || this.#reading) return\n\t\tthis.#running = true\n\t\ttry {\n\t\t\tlet turns = 0\n\t\t\twhile (this.#queued && !this.#destroyed && turns < 2) {\n\t\t\t\tthis.#queued = false\n\t\t\t\tthis.#changed = false\n\t\t\t\tthis.#controls = this.#discover()\n\t\t\t\tthis.#releaseRemoved()\n\t\t\t\tthis.#exclusions()\n\t\t\t\tif (this.#destroyed) break\n\t\t\t\tthis.#paint()\n\t\t\t\tif (!this.#destroyed && (this.#changed || !this.#notified)) {\n\t\t\t\t\tthis.#notified = true\n\t\t\t\t\tthis.#emitter.emit('render')\n\t\t\t\t}\n\t\t\t\tturns += 1\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#queued = false\n\t\t\tthrow isFormBindingError(error)\n\t\t\t\t? error\n\t\t\t\t: new FormBindingError('DOM', 'Native form projection failed', { cause: error })\n\t\t} finally {\n\t\t\tthis.#running = false\n\t\t\tif (this.#queued && !this.#destroyed && !this.#scheduled) {\n\t\t\t\tthis.#scheduled = true\n\t\t\t\tqueueMicrotask(() => {\n\t\t\t\t\tthis.#scheduled = false\n\t\t\t\t\tif (!this.#destroyed) this.#onDocument()\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\t#discover(): ReadonlyMap<string, readonly FormControl[]> {\n\t\tconst groups = new Map<string, FormControl[]>()\n\t\tfor (const control of Array.from(this.#element.elements)) {\n\t\t\tif (\n\t\t\t\t!isFormControl(control) ||\n\t\t\t\tcontrol.form !== this.#element ||\n\t\t\t\t!this.#document.field(control.name)\n\t\t\t)\n\t\t\t\tcontinue\n\t\t\tif (\n\t\t\t\tcontrol instanceof HTMLInputElement &&\n\t\t\t\t['submit', 'reset', 'button', 'image'].includes(control.type)\n\t\t\t)\n\t\t\t\tcontinue\n\t\t\tconst group = groups.get(control.name) ?? []\n\t\t\tgroup.push(control)\n\t\t\tgroups.set(control.name, group)\n\t\t}\n\t\tfor (const field of this.#document.schema.fields) {\n\t\t\tconst group = groups.get(field.name)\n\t\t\tif (group !== undefined && !matchesFormControls(field, group)) {\n\t\t\t\tthrow new FormBindingError('CONTROL', `Incompatible controls for \"${field.name}\"`, {\n\t\t\t\t\tfield: field.name,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn groups\n\t}\n\n\t#nativeDisabled(control: FormControl): boolean {\n\t\tconst owned = this.#attributes.get(control)?.get('disabled')\n\t\tif (\n\t\t\towned !== undefined &&\n\t\t\tcontrol.getAttribute('disabled') === owned.value &&\n\t\t\towned.original === null\n\t\t) {\n\t\t\t// A model-owned disabled attribute cannot become a native exclusion on the next scan.\n\t\t\tlet ancestor = control.parentElement\n\t\t\twhile (ancestor !== null) {\n\t\t\t\tif (ancestor instanceof HTMLFieldSetElement && ancestor.disabled) {\n\t\t\t\t\tconst legend = Array.from(ancestor.children).find(\n\t\t\t\t\t\t(child) => child instanceof HTMLLegendElement,\n\t\t\t\t\t)\n\t\t\t\t\tif (legend === undefined || !legend.contains(control)) return true\n\t\t\t\t}\n\t\t\t\tancestor = ancestor.parentElement\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\t\treturn control.matches(':disabled')\n\t}\n\n\t#exclusions(): void {\n\t\tif (this.#document.status !== 'editing') return\n\t\tfor (const field of this.#document.schema.fields) {\n\t\t\tif (this.#destroyed) return\n\t\t\tconst group = this.#controls.get(field.name)\n\t\t\tconst excluded =\n\t\t\t\tgroup !== undefined && group.every((control) => this.#nativeDisabled(control))\n\t\t\tif (excluded && !this.#logical.has(field.name) && !this.#excluded.has(field.name)) {\n\t\t\t\tthis.#excluded.add(field.name)\n\t\t\t\tthis.#excluding = field.name\n\t\t\t\ttry {\n\t\t\t\t\tthis.#document.disable(field.name)\n\t\t\t\t} finally {\n\t\t\t\t\tthis.#excluding = undefined\n\t\t\t\t}\n\t\t\t} else if (!excluded && this.#excluded.delete(field.name) && !this.#logical.has(field.name)) {\n\t\t\t\tthis.#excluding = field.name\n\t\t\t\ttry {\n\t\t\t\t\tthis.#document.enable(field.name)\n\t\t\t\t} finally {\n\t\t\t\t\tthis.#excluding = undefined\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t#paint(): void {\n\t\tthis.#attribute(this.#element, 'novalidate', '')\n\t\tthis.#attribute(\n\t\t\tthis.#element,\n\t\t\t'aria-busy',\n\t\t\tthis.#document.pending === undefined ? 'false' : 'true',\n\t\t)\n\t\tconst errors: FieldError[] = [...this.#document.errors]\n\t\tconst values = this.#document.values\n\t\tfor (const field of this.#document.schema.fields) {\n\t\t\tif (this.#destroyed) return\n\t\t\tconst group = this.#controls.get(field.name) ?? []\n\t\t\tfor (const control of group) {\n\t\t\t\tthis.#metadata(field, control)\n\t\t\t\tif (!this.#unreadable.has(field.name))\n\t\t\t\t\tthis.#value(\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tcontrol,\n\t\t\t\t\t\tObject.hasOwn(values, field.name) ? values[field.name] : undefined,\n\t\t\t\t\t)\n\t\t\t\tthis.#restoreValidity(control)\n\t\t\t\tif (\n\t\t\t\t\t!this.#document.disabled.has(field.name) &&\n\t\t\t\t\t!control.validity.valid &&\n\t\t\t\t\tcontrol.willValidate\n\t\t\t\t) {\n\t\t\t\t\terrors.push({ field: field.name, message: control.validationMessage })\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const field of this.#document.schema.fields) {\n\t\t\tif (this.#destroyed) return\n\t\t\tconst messages = [\n\t\t\t\t...new Set(\n\t\t\t\t\terrors.filter((error) => error.field === field.name).map((error) => error.message),\n\t\t\t\t),\n\t\t\t]\n\t\t\tconst message = messages.join('\\n')\n\t\t\tconst visible =\n\t\t\t\tthis.#document.touched.has(field.name) && !this.#document.disabled.has(field.name)\n\t\t\tconst group = this.#controls.get(field.name) ?? []\n\t\t\tlet feedback = this.#feedback.get(field.name)\n\t\t\tif (\n\t\t\t\tfeedback === undefined &&\n\t\t\t\t(group.length > 0 || this.#options?.feedback?.has(field.name))\n\t\t\t) {\n\t\t\t\tfeedback = this.#element.ownerDocument.createElement('span')\n\t\t\t\tfeedback.id = `form-feedback-${crypto.randomUUID()}`\n\t\t\t\tfeedback.dataset.formFeedback = field.name\n\t\t\t\tconst destination = this.#options?.feedback?.get(field.name)\n\t\t\t\tif (destination !== undefined) destination.append(feedback)\n\t\t\t\telse (group.at(-1) ?? this.#element).insertAdjacentElement('afterend', feedback)\n\t\t\t\tthis.#feedback.set(field.name, feedback)\n\t\t\t}\n\t\t\tif (feedback !== undefined) {\n\t\t\t\tif (feedback.textContent !== message) {\n\t\t\t\t\tfeedback.textContent = message\n\t\t\t\t\tthis.#changed = true\n\t\t\t\t}\n\t\t\t\tif (feedback.hidden !== (!visible || message === '')) {\n\t\t\t\t\tfeedback.hidden = !visible || message === ''\n\t\t\t\t\tthis.#changed = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor (const control of group) {\n\t\t\t\tif (message !== '' && !control.validity.customError) {\n\t\t\t\t\tthis.#validity.set(control, { original: '', value: message })\n\t\t\t\t\tcontrol.setCustomValidity(message)\n\t\t\t\t}\n\t\t\t\tthis.#attribute(control, 'aria-invalid', visible && message !== '' ? 'true' : null)\n\t\t\t\tif (feedback !== undefined)\n\t\t\t\t\tthis.#description(control, feedback.id, visible && message !== '')\n\t\t\t}\n\t\t}\n\t\tthis.#paintSummary(\n\t\t\terrors.filter(\n\t\t\t\t(error) =>\n\t\t\t\t\tthis.#document.touched.has(error.field) && !this.#document.disabled.has(error.field),\n\t\t\t),\n\t\t)\n\t\tfor (const control of Array.from(this.#element.elements)) {\n\t\t\tif (\n\t\t\t\t(control instanceof HTMLButtonElement && control.type === 'submit') ||\n\t\t\t\t(control instanceof HTMLInputElement && ['submit', 'image'].includes(control.type))\n\t\t\t) {\n\t\t\t\tthis.#attribute(\n\t\t\t\t\tcontrol,\n\t\t\t\t\t'aria-busy',\n\t\t\t\t\tthis.#document.pending === undefined ? 'false' : 'true',\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t}\n\n\t#metadata(field: FormField, control: FormControl): void {\n\t\tif (this.#logical.has(field.name)) this.#attribute(control, 'disabled', '')\n\t\telse this.#releaseAttribute(control, 'disabled')\n\t\tif (\n\t\t\tfield.locked &&\n\t\t\t!(control instanceof HTMLSelectElement) &&\n\t\t\t!(\n\t\t\t\tcontrol instanceof HTMLInputElement &&\n\t\t\t\t['radio', 'checkbox', 'file', 'range', 'color'].includes(control.type)\n\t\t\t)\n\t\t)\n\t\t\tthis.#attribute(control, 'readonly', '')\n\t\tif (field.rule?.required && !['confirm', 'checkbox', 'file'].includes(field.control))\n\t\t\tthis.#attribute(control, 'required', '')\n\t\tif (\n\t\t\tfield.control === 'number' ||\n\t\t\tfield.control === 'date' ||\n\t\t\tfield.control === 'time' ||\n\t\t\tfield.control === 'datetime'\n\t\t) {\n\t\t\tif (field.rule?.minimum !== undefined && !control.hasAttribute('min'))\n\t\t\t\tthis.#attribute(control, 'min', String(field.rule.minimum))\n\t\t\tif (field.rule?.maximum !== undefined && !control.hasAttribute('max'))\n\t\t\t\tthis.#attribute(control, 'max', String(field.rule.maximum))\n\t\t\tif (\n\t\t\t\tfield.control === 'number' &&\n\t\t\t\tfield.rule?.step !== undefined &&\n\t\t\t\t!control.hasAttribute('step')\n\t\t\t)\n\t\t\t\tthis.#attribute(control, 'step', String(field.rule.step))\n\t\t}\n\t}\n\n\t#value(field: FormField, control: FormControl, value: FieldValue | undefined): void {\n\t\tif (control instanceof HTMLInputElement && control.type === 'file') {\n\t\t\t// The browser alone owns bytes. A differing saved name cannot create a FileList.\n\t\t\tconst names = Array.from(control.files ?? [], (file) => file.name)\n\t\t\tif (names.length > 0 && (value === undefined || !matchesValue(names, value))) {\n\t\t\t\tcontrol.value = ''\n\t\t\t\tthis.#changed = true\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\tcontrol instanceof HTMLInputElement &&\n\t\t\t(control.type === 'checkbox' || control.type === 'radio')\n\t\t) {\n\t\t\tconst checked =\n\t\t\t\tfield.control === 'confirm'\n\t\t\t\t\t? value === true\n\t\t\t\t\t: Array.isArray(value)\n\t\t\t\t\t\t? value.includes(control.value)\n\t\t\t\t\t\t: value === control.value\n\t\t\tif (control.checked !== checked) {\n\t\t\t\tcontrol.checked = checked\n\t\t\t\tthis.#changed = true\n\t\t\t}\n\t\t} else if (control instanceof HTMLSelectElement && control.multiple) {\n\t\t\tfor (const option of Array.from(control.options)) {\n\t\t\t\tconst selected = Array.isArray(value) && value.includes(option.value)\n\t\t\t\tif (option.selected !== selected) {\n\t\t\t\t\toption.selected = selected\n\t\t\t\t\tthis.#changed = true\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst text = value === undefined ? '' : String(value)\n\t\t\tif (\n\t\t\t\tcontrol.value !== text ||\n\t\t\t\t(control instanceof HTMLInputElement && control.validity.badInput)\n\t\t\t) {\n\t\t\t\tconst before = control.value\n\t\t\t\tconst invalid = control.validity.badInput\n\t\t\t\tcontrol.value = text\n\t\t\t\tif (control.value !== before || invalid) this.#changed = true\n\t\t\t}\n\t\t}\n\t}\n\n\t#attribute(element: Element, name: string, value: string | null): void {\n\t\tconst attributes = this.#attributes.get(element) ?? new Map<string, FormBindingAttribute>()\n\t\tconst previous = attributes.get(name)\n\t\tconst current = element.getAttribute(name)\n\t\tconst original =\n\t\t\tprevious === undefined || previous.value !== current ? current : previous.original\n\t\tif (current !== value) {\n\t\t\tif (value === null) element.removeAttribute(name)\n\t\t\telse element.setAttribute(name, value)\n\t\t\tthis.#changed = true\n\t\t}\n\t\tattributes.set(name, { original, value })\n\t\tthis.#attributes.set(element, attributes)\n\t}\n\n\t#description(control: FormControl, id: string, shown: boolean): void {\n\t\tconst tokens = (control.getAttribute('aria-describedby') ?? '')\n\t\t\t.split(/\\s+/)\n\t\t\t.filter((token) => token !== '' && token !== id)\n\t\tif (shown) tokens.push(id)\n\t\tthis.#attribute(control, 'aria-describedby', tokens.length === 0 ? null : tokens.join(' '))\n\t}\n\n\t#releaseAttribute(element: Element, name: string): void {\n\t\tconst attributes = this.#attributes.get(element)\n\t\tconst owned = attributes?.get(name)\n\t\tif (owned === undefined) return\n\t\tif (element.getAttribute(name) === owned.value) {\n\t\t\tif (owned.original === null) element.removeAttribute(name)\n\t\t\telse element.setAttribute(name, owned.original)\n\t\t\tthis.#changed = true\n\t\t}\n\t\tattributes?.delete(name)\n\t}\n\n\t#restore(element: Element): void {\n\t\tfor (const name of this.#attributes.get(element)?.keys() ?? [])\n\t\t\tthis.#releaseAttribute(element, name)\n\t\tconst ids = new Set([...this.#feedback.values()].map((feedback) => feedback.id))\n\t\tconst description = element.getAttribute('aria-describedby')\n\t\tif (description !== null) {\n\t\t\tconst tokens = description.split(/\\s+/).filter((token) => token !== '' && !ids.has(token))\n\t\t\tif (tokens.length === 0) element.removeAttribute('aria-describedby')\n\t\t\telse if (tokens.join(' ') !== description)\n\t\t\t\telement.setAttribute('aria-describedby', tokens.join(' '))\n\t\t}\n\t\tthis.#attributes.delete(element)\n\t}\n\n\t#restoreValidity(control: FormControl): void {\n\t\tconst owned = this.#validity.get(control)\n\t\tif (\n\t\t\towned !== undefined &&\n\t\t\tcontrol.validity.customError &&\n\t\t\tcontrol.validationMessage === owned.value\n\t\t)\n\t\t\tcontrol.setCustomValidity(owned.original)\n\t\tthis.#validity.delete(control)\n\t}\n\n\t#releaseRemoved(): void {\n\t\tconst live = new Set([...this.#controls.values()].flat())\n\t\tfor (const [control, name] of this.#names) {\n\t\t\tif (!live.has(control) || control.name !== name) {\n\t\t\t\tthis.#restoreValidity(control)\n\t\t\t\tthis.#restore(control)\n\t\t\t\tthis.#names.delete(control)\n\t\t\t}\n\t\t}\n\t\tfor (const control of live) this.#names.set(control, control.name)\n\t\tfor (const element of this.#attributes.keys()) {\n\t\t\tif (\n\t\t\t\t(isFormControl(element) || element instanceof HTMLButtonElement) &&\n\t\t\t\telement.form !== this.#element\n\t\t\t)\n\t\t\t\tthis.#restore(element)\n\t\t}\n\t\tfor (const [name, feedback] of this.#feedback) {\n\t\t\tif (!this.#controls.has(name) && !this.#options?.feedback?.has(name)) {\n\t\t\t\tfeedback.remove()\n\t\t\t\tthis.#feedback.delete(name)\n\t\t\t}\n\t\t}\n\t}\n\n\t#paintSummary(errors: readonly FieldError[]): void {\n\t\tconst destination = this.#options?.summary\n\t\tif (destination === undefined) return\n\t\tconst text = JSON.stringify(\n\t\t\terrors.map((error) => [error.field, error.message, this.#focusable(error.field)?.id]),\n\t\t)\n\t\tif (text === this.#summaryText) return\n\t\tthis.#summaryText = text\n\t\tthis.#changed = true\n\t\tthis.#summary?.remove()\n\t\tthis.#summary = this.#element.ownerDocument.createElement('ul')\n\t\tthis.#summary.dataset.formSummary = ''\n\t\tthis.#summary.hidden = errors.length === 0\n\t\tfor (const error of errors) {\n\t\t\tconst entry = this.#element.ownerDocument.createElement('li')\n\t\t\tconst target = this.#focusable(error.field)\n\t\t\tconst label = this.#document.field(error.field)?.label ?? error.field\n\t\t\tif (target === undefined) entry.textContent = `${label}: ${error.message}`\n\t\t\telse {\n\t\t\t\tif (target.id === '') this.#attribute(target, 'id', `form-control-${crypto.randomUUID()}`)\n\t\t\t\tconst link = this.#element.ownerDocument.createElement('a')\n\t\t\t\tlink.href = `#${target.id}`\n\t\t\t\tlink.textContent = `${label}: ${error.message}`\n\t\t\t\tlink.dataset.formField = error.field\n\t\t\t\tentry.append(link)\n\t\t\t}\n\t\t\tthis.#summary.append(entry)\n\t\t}\n\t\tdestination.append(this.#summary)\n\t}\n\n\t#focusable(name: string): FormControl | undefined {\n\t\treturn this.#controls\n\t\t\t.get(name)\n\t\t\t?.find(\n\t\t\t\t(control) =>\n\t\t\t\t\t!control.matches(':disabled') &&\n\t\t\t\t\tcontrol.closest('[inert]') === null &&\n\t\t\t\t\tcontrol.getClientRects().length > 0 &&\n\t\t\t\t\t!['hidden', 'collapse'].includes(getComputedStyle(control).visibility) &&\n\t\t\t\t\t!(control instanceof HTMLInputElement && control.type === 'hidden'),\n\t\t\t)\n\t}\n\n\t#onDocument(): void {\n\t\tif (this.#destroyed) return\n\t\ttry {\n\t\t\tthis.#drain()\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t#onFill(name: string): void {\n\t\tthis.#unreadable.delete(name)\n\t\tthis.#onState()\n\t}\n\n\t#onState(): void {\n\t\tthis.#notified = false\n\t\tthis.#onDocument()\n\t}\n\n\t#onDisable(name: string): void {\n\t\tif (this.#excluding !== name) this.#logical.add(name)\n\t\tthis.#onState()\n\t}\n\n\t#onEnable(name: string): void {\n\t\tif (this.#excluding !== name) {\n\t\t\tthis.#logical.delete(name)\n\t\t\tthis.#excluded.delete(name)\n\t\t}\n\t\tthis.#onState()\n\t}\n\n\t#onClear(): void {\n\t\tthis.#logical.clear()\n\t\tthis.#excluded.clear()\n\t\tthis.#unreadable.clear()\n\t\tfor (const name of this.#document.disabled) this.#logical.add(name)\n\t\tthis.#onState()\n\t}\n\n\t#onInput(event: Event): void {\n\t\tconst control = event.target\n\t\tif (\n\t\t\t!isFormControl(control) ||\n\t\t\tcontrol.form !== this.#element ||\n\t\t\t!this.#document.field(control.name) ||\n\t\t\tthis.#destroyed\n\t\t)\n\t\t\treturn\n\t\ttry {\n\t\t\tthis.read()\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t#onClick(event: MouseEvent): void {\n\t\tconst control = event.target\n\t\tif (control instanceof Element) {\n\t\t\tconst link = control.closest('a[data-form-field]')\n\t\t\tif (\n\t\t\t\tlink instanceof HTMLAnchorElement &&\n\t\t\t\tthis.#summary?.contains(link) &&\n\t\t\t\tlink.dataset.formField !== undefined\n\t\t\t) {\n\t\t\t\tevent.preventDefault()\n\t\t\t\tthis.#focusable(link.dataset.formField)?.focus()\n\t\t\t}\n\t\t}\n\t\tif (\n\t\t\tcontrol instanceof HTMLInputElement &&\n\t\t\tcontrol.form === this.#element &&\n\t\t\tthis.#document.field(control.name)?.locked &&\n\t\t\t['file', 'checkbox', 'radio', 'color'].includes(control.type)\n\t\t)\n\t\t\tevent.preventDefault()\n\t}\n\n\t#onBlur(event: FocusEvent): void {\n\t\tconst control = event.target\n\t\tif (\n\t\t\t!isFormControl(control) ||\n\t\t\tcontrol.form !== this.#element ||\n\t\t\t!this.#document.field(control.name) ||\n\t\t\tthis.#destroyed ||\n\t\t\tthis.#document.status !== 'editing'\n\t\t)\n\t\t\treturn\n\t\ttry {\n\t\t\tthis.#document.touch(control.name)\n\t\t\tthis.#drain()\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n\n\t#onSubmit(event: SubmitEvent): void {\n\t\tif (event.defaultPrevented || this.#destroyed) return\n\t\tevent.preventDefault()\n\t\tif (this.#submitting || this.#document.pending !== undefined) {\n\t\t\tthis.#emitter.emit(\n\t\t\t\t'error',\n\t\t\t\tnew FormBindingError('PENDING', 'A submission is already pending'),\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tthis.#submitting = true\n\t\ttry {\n\t\t\tthis.read()\n\t\t\tif (this.#destroyed) return\n\t\t\tfor (const field of this.#document.schema.fields) {\n\t\t\t\tif (!this.#document.disabled.has(field.name)) this.#document.touch(field.name)\n\t\t\t\tif (this.#destroyed) return\n\t\t\t}\n\t\t\tthis.#drain()\n\t\t\tconst native = [...this.#controls].filter(\n\t\t\t\t([name, controls]) =>\n\t\t\t\t\t!this.#document.disabled.has(name) &&\n\t\t\t\t\tcontrols.some((control) => control.willValidate && !control.validity.valid),\n\t\t\t)\n\t\t\tif (native.length > 0) {\n\t\t\t\tfor (const [name] of native) {\n\t\t\t\t\tconst target = this.#focusable(name)\n\t\t\t\t\tif (target !== undefined) {\n\t\t\t\t\t\ttarget.focus()\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst result = this.#document.prepare()\n\t\t\tif (this.#destroyed) return\n\t\t\tthis.#drain()\n\t\t\tif (result.success) {\n\t\t\t\tif (!result.value.signal.aborted && this.#document.pending === result.value)\n\t\t\t\t\tthis.#emitter.emit('submit', result.value, event.submitter ?? undefined)\n\t\t\t} else {\n\t\t\t\tfor (const error of result.error) {\n\t\t\t\t\tconst target = this.#focusable(error.field)\n\t\t\t\t\tif (target !== undefined) {\n\t\t\t\t\t\ttarget.focus()\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t} finally {\n\t\t\tthis.#submitting = false\n\t\t}\n\t}\n\n\t#onReset(event: Event): void {\n\t\tif (event.defaultPrevented || this.#destroyed) return\n\t\tevent.preventDefault()\n\t\ttry {\n\t\t\tthis.#document.clear()\n\t\t\tthis.refresh()\n\t\t} catch (error) {\n\t\t\tthis.#emitter.emit('error', error)\n\t\t}\n\t}\n}\n","import type { FormInterface } from '../core/types.js'\nimport type { FormBindingInterface, FormBindingOptions } from './types.js'\nimport { FormBinding } from './FormBinding.js'\n\n/**\n * Creates a native form binding exposed through its lifecycle interface.\n * @param element - The native form.\n * @param document - The borrowed answer document.\n * @param options - Feedback destinations and listeners.\n * @returns The initialized native binding.\n * @example\n * ```ts\n * const binding = createFormBinding(element, document)\n * binding.refresh()\n * ```\n */\nexport function createFormBinding(\n\telement: HTMLFormElement,\n\tdocument: FormInterface,\n\toptions?: FormBindingOptions,\n): FormBindingInterface {\n\treturn new FormBinding(element, document, options)\n}\n"],"mappings":"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"}
|
package/dist/src/core/index.cjs
CHANGED
|
@@ -1269,6 +1269,11 @@ var Form = class {
|
|
|
1269
1269
|
#touched = /* @__PURE__ */ new Set();
|
|
1270
1270
|
#disabled = /* @__PURE__ */ new Map();
|
|
1271
1271
|
#invalidations = /* @__PURE__ */ new Map();
|
|
1272
|
+
#revisions = /* @__PURE__ */ new Map();
|
|
1273
|
+
#captured = /* @__PURE__ */ new Map();
|
|
1274
|
+
#submission;
|
|
1275
|
+
#controller;
|
|
1276
|
+
#preparing;
|
|
1272
1277
|
#resolvers = Promise.withResolvers();
|
|
1273
1278
|
#errors = Object.freeze([]);
|
|
1274
1279
|
#status = "editing";
|
|
@@ -1323,10 +1328,14 @@ var Form = class {
|
|
|
1323
1328
|
for (const [name, value] of Object.entries(this.#values)) defineEntry(values, name, value);
|
|
1324
1329
|
return Object.freeze(values);
|
|
1325
1330
|
}
|
|
1326
|
-
/** Holds the answers the
|
|
1331
|
+
/** Holds the acknowledged answers, initially the schema's defaults overlaid with seeded values. */
|
|
1327
1332
|
get baseline() {
|
|
1328
1333
|
return this.#baseline;
|
|
1329
1334
|
}
|
|
1335
|
+
/** Holds the submission awaiting acknowledgement, or `undefined` when none is pending. */
|
|
1336
|
+
get pending() {
|
|
1337
|
+
return this.#submission;
|
|
1338
|
+
}
|
|
1330
1339
|
/** Holds every error the last completed evaluation produced. */
|
|
1331
1340
|
get errors() {
|
|
1332
1341
|
return this.#errors;
|
|
@@ -1349,7 +1358,7 @@ var Form = class {
|
|
|
1349
1358
|
get valid() {
|
|
1350
1359
|
return this.#errors.length === 0;
|
|
1351
1360
|
}
|
|
1352
|
-
/** Reports whether any answer
|
|
1361
|
+
/** Reports whether any answer differs from its acknowledged baseline. */
|
|
1353
1362
|
get dirty() {
|
|
1354
1363
|
return !matchesValues(this.values, this.#baseline);
|
|
1355
1364
|
}
|
|
@@ -1394,6 +1403,7 @@ var Form = class {
|
|
|
1394
1403
|
this.#batch(() => {
|
|
1395
1404
|
const moved = [];
|
|
1396
1405
|
for (const [name, answer] of entries) {
|
|
1406
|
+
this.#revisions.set(name, (this.#revisions.get(name) ?? 0) + 1);
|
|
1397
1407
|
if (this.#differs(name, answer)) moved.push(name);
|
|
1398
1408
|
if (answer === void 0) delete this.#values[name];
|
|
1399
1409
|
else defineEntry(this.#values, name, cloneValue(answer));
|
|
@@ -1416,26 +1426,34 @@ var Form = class {
|
|
|
1416
1426
|
touch(name) {
|
|
1417
1427
|
this.#gate();
|
|
1418
1428
|
this.#requireField(name);
|
|
1419
|
-
this.#
|
|
1429
|
+
this.#batch(() => {
|
|
1430
|
+
if (this.#touched.has(name)) return;
|
|
1431
|
+
this.#touched.add(name);
|
|
1432
|
+
this.#emitter.emit("touch", name);
|
|
1433
|
+
});
|
|
1420
1434
|
}
|
|
1421
1435
|
/**
|
|
1422
1436
|
* Fails a field from outside, for what the rules cannot see.
|
|
1423
1437
|
*
|
|
1424
1438
|
* @param name - The field's name.
|
|
1425
1439
|
* @param message - What to tell the person.
|
|
1440
|
+
* @param submission - The active receipt to check against the field's captured revision.
|
|
1441
|
+
* @returns True if the failure was applied; false if the receipt or field revision is stale.
|
|
1426
1442
|
* @remarks
|
|
1427
1443
|
* One field holds one external failure: a second call replaces the first. The failure lasts
|
|
1428
1444
|
* until that field is filled again or the form is cleared.
|
|
1429
1445
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended, and
|
|
1430
1446
|
* `FIELD` when the schema declares no such name.
|
|
1431
1447
|
*/
|
|
1432
|
-
invalidate(name, message) {
|
|
1433
|
-
this.#gate();
|
|
1448
|
+
invalidate(name, message, submission) {
|
|
1449
|
+
if (submission === void 0) this.#gate();
|
|
1434
1450
|
this.#requireField(name);
|
|
1451
|
+
if (submission !== void 0 && (this.#pending || submission !== this.#submission || !this.#captured.has(name) || this.#captured.get(name) !== (this.#revisions.get(name) ?? 0))) return false;
|
|
1435
1452
|
this.#batch(() => {
|
|
1436
1453
|
this.#invalidations.set(name, message);
|
|
1437
1454
|
if (this.#evaluate()) this.#emitter.emit("validate", this.#errors);
|
|
1438
1455
|
});
|
|
1456
|
+
return true;
|
|
1439
1457
|
}
|
|
1440
1458
|
/**
|
|
1441
1459
|
* Takes one or more fields out of the form.
|
|
@@ -1479,6 +1497,7 @@ var Form = class {
|
|
|
1479
1497
|
*/
|
|
1480
1498
|
submit() {
|
|
1481
1499
|
this.#gate();
|
|
1500
|
+
this.#gatePreparation();
|
|
1482
1501
|
return this.#batch(() => {
|
|
1483
1502
|
const changed = this.#evaluate();
|
|
1484
1503
|
const checked = this.#errors;
|
|
@@ -1498,12 +1517,16 @@ var Form = class {
|
|
|
1498
1517
|
const stopped = errors.length > 0;
|
|
1499
1518
|
if (stopped) {
|
|
1500
1519
|
const disabled = this.disabled;
|
|
1501
|
-
for (const field of this.#schema.fields) if (!disabled.has(field.name)
|
|
1520
|
+
for (const field of this.#schema.fields) if (!disabled.has(field.name) && !this.#touched.has(field.name)) {
|
|
1521
|
+
this.#touched.add(field.name);
|
|
1522
|
+
this.#emitter.emit("touch", field.name);
|
|
1523
|
+
}
|
|
1502
1524
|
}
|
|
1503
1525
|
if (stopped) return {
|
|
1504
1526
|
success: false,
|
|
1505
1527
|
error: errors
|
|
1506
1528
|
};
|
|
1529
|
+
this.#gatePreparation();
|
|
1507
1530
|
const answers = this.#snapshot();
|
|
1508
1531
|
this.#status = "settled";
|
|
1509
1532
|
this.#resolvers.resolve(answers);
|
|
@@ -1515,20 +1538,143 @@ var Form = class {
|
|
|
1515
1538
|
});
|
|
1516
1539
|
}
|
|
1517
1540
|
/**
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1541
|
+
* Prepares an owned enabled snapshot for acknowledgement while leaving the form editable.
|
|
1542
|
+
*
|
|
1543
|
+
* @returns The submission on success, or the errors that prevented preparation.
|
|
1544
|
+
* @remarks
|
|
1545
|
+
* A failed check refuses its checked errors even if an observer repairs them. A passing check
|
|
1546
|
+
* drains observer mutations through one further check. The receipt always carries that check's
|
|
1547
|
+
* snapshot, including when its notifications produce later edits. A throwing validator creates
|
|
1548
|
+
* no receipt. The pending identity is installed before `prepare` listeners run.
|
|
1549
|
+
* @throws A {@link FormError} coded `PENDING` while another preparation or receipt is pending,
|
|
1550
|
+
* or when clear interrupts preparation; terminal forms retain their terminal refusals.
|
|
1551
|
+
*/
|
|
1552
|
+
prepare() {
|
|
1553
|
+
this.#gate();
|
|
1554
|
+
this.#gatePreparation();
|
|
1555
|
+
const controller = new AbortController();
|
|
1556
|
+
this.#preparing = controller;
|
|
1557
|
+
return this.#batch(() => {
|
|
1558
|
+
try {
|
|
1559
|
+
let drained = false;
|
|
1560
|
+
while (true) {
|
|
1561
|
+
const values = this.values;
|
|
1562
|
+
const disabled = this.disabled;
|
|
1563
|
+
const captured = /* @__PURE__ */ new Map();
|
|
1564
|
+
const touched = [];
|
|
1565
|
+
for (const field of this.#schema.fields) {
|
|
1566
|
+
if (disabled.has(field.name)) continue;
|
|
1567
|
+
captured.set(field.name, this.#revisions.get(field.name) ?? 0);
|
|
1568
|
+
if (!this.#touched.has(field.name)) touched.push(field.name);
|
|
1569
|
+
this.#touched.add(field.name);
|
|
1570
|
+
}
|
|
1571
|
+
const before = this.#evaluation;
|
|
1572
|
+
for (const name of touched) {
|
|
1573
|
+
if (controller.signal.aborted || this.#pending) break;
|
|
1574
|
+
this.#emitter.emit("touch", name);
|
|
1575
|
+
}
|
|
1576
|
+
this.#requirePreparation(controller);
|
|
1577
|
+
const errors = this.#check(values, disabled);
|
|
1578
|
+
const mutated = this.#evaluation !== before;
|
|
1579
|
+
const changed = !mutated && this.#record(errors);
|
|
1580
|
+
const evaluation = this.#evaluation;
|
|
1581
|
+
if (changed) this.#emitter.emit("validate", this.#errors);
|
|
1582
|
+
this.#requirePreparation(controller);
|
|
1583
|
+
if (errors.length > 0) return {
|
|
1584
|
+
success: false,
|
|
1585
|
+
error: errors
|
|
1586
|
+
};
|
|
1587
|
+
if (!drained && (mutated || this.#evaluation !== evaluation)) {
|
|
1588
|
+
drained = true;
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
const submission = Object.freeze({
|
|
1592
|
+
values: this.#snapshot(values, disabled),
|
|
1593
|
+
signal: controller.signal
|
|
1594
|
+
});
|
|
1595
|
+
this.#captured = captured;
|
|
1596
|
+
this.#controller = controller;
|
|
1597
|
+
this.#submission = submission;
|
|
1598
|
+
this.#preparing = void 0;
|
|
1599
|
+
this.#emitter.emit("prepare", submission);
|
|
1600
|
+
return {
|
|
1601
|
+
success: true,
|
|
1602
|
+
value: submission
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
} finally {
|
|
1606
|
+
if (this.#preparing === controller) this.#preparing = void 0;
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
/**
|
|
1611
|
+
* Adopts the active submission's captured answers into the baseline without changing live state.
|
|
1612
|
+
*
|
|
1613
|
+
* @param submission - The receipt returned by preparation.
|
|
1614
|
+
* @returns True if this active receipt was accepted; false otherwise, including after teardown.
|
|
1615
|
+
*/
|
|
1616
|
+
accept(submission) {
|
|
1617
|
+
if (this.#pending || submission !== this.#submission) return false;
|
|
1618
|
+
return this.#batch(() => {
|
|
1619
|
+
const baseline = { ...this.#baseline };
|
|
1620
|
+
for (const name of this.#captured.keys()) {
|
|
1621
|
+
const value = Object.hasOwn(submission.values, name) ? submission.values[name] : void 0;
|
|
1622
|
+
if (value === void 0) delete baseline[name];
|
|
1623
|
+
else defineEntry(baseline, name, value);
|
|
1624
|
+
}
|
|
1625
|
+
this.#baseline = Object.freeze(baseline);
|
|
1626
|
+
this.#submission = void 0;
|
|
1627
|
+
this.#controller = void 0;
|
|
1628
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1629
|
+
this.#emitter.emit("accept", submission);
|
|
1630
|
+
return true;
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1633
|
+
/**
|
|
1634
|
+
* Consumes the active submission and aborts its signal without changing answers or baseline.
|
|
1635
|
+
*
|
|
1636
|
+
* @param submission - The receipt returned by preparation.
|
|
1637
|
+
* @returns True if this active receipt was aborted; false otherwise, including after teardown.
|
|
1638
|
+
*/
|
|
1639
|
+
abort(submission) {
|
|
1640
|
+
if (this.#pending || submission !== this.#submission) return false;
|
|
1641
|
+
this.#batch(() => {
|
|
1642
|
+
const controller = this.#controller;
|
|
1643
|
+
this.#submission = void 0;
|
|
1644
|
+
this.#controller = void 0;
|
|
1645
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1646
|
+
controller?.abort();
|
|
1647
|
+
this.#emitter.emit("abort", submission);
|
|
1648
|
+
});
|
|
1649
|
+
return true;
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Returns every answer to its acknowledged baseline and aborts pending acknowledgement.
|
|
1520
1653
|
*
|
|
1521
1654
|
* @throws A {@link FormError} coded `SETTLED` or `ABANDONED` when the form has ended.
|
|
1522
1655
|
*/
|
|
1523
1656
|
clear() {
|
|
1524
1657
|
this.#gate();
|
|
1525
1658
|
this.#batch(() => {
|
|
1659
|
+
const submission = this.#submission;
|
|
1660
|
+
const controller = this.#controller;
|
|
1661
|
+
this.#submission = void 0;
|
|
1662
|
+
this.#controller = void 0;
|
|
1663
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1664
|
+
this.#preparing?.abort();
|
|
1526
1665
|
for (const name of Object.keys(this.#values)) delete this.#values[name];
|
|
1527
1666
|
for (const [name, value] of Object.entries(this.#baseline)) defineEntry(this.#values, name, value);
|
|
1528
1667
|
this.#touched.clear();
|
|
1529
1668
|
this.#disabled.clear();
|
|
1530
1669
|
this.#invalidations.clear();
|
|
1531
|
-
|
|
1670
|
+
this.#revisions.clear();
|
|
1671
|
+
let changed = false;
|
|
1672
|
+
try {
|
|
1673
|
+
changed = this.#evaluate();
|
|
1674
|
+
} finally {
|
|
1675
|
+
controller?.abort();
|
|
1676
|
+
if (submission !== void 0) this.#emitter.emit("abort", submission);
|
|
1677
|
+
}
|
|
1532
1678
|
this.#emitter.emit("clear");
|
|
1533
1679
|
if (changed) this.#emitter.emit("validate", this.#errors);
|
|
1534
1680
|
});
|
|
@@ -1551,6 +1697,13 @@ var Form = class {
|
|
|
1551
1697
|
if (this.#status === "settled") throw new FormError("SETTLED", "The form has settled and cannot change");
|
|
1552
1698
|
if (this.#status === "abandoned" || this.#pending) throw new FormError("ABANDONED", "The form was abandoned and cannot change");
|
|
1553
1699
|
}
|
|
1700
|
+
#gatePreparation() {
|
|
1701
|
+
if (this.#preparing !== void 0 || this.#submission !== void 0) throw new FormError("PENDING", "The form has a pending preparation or submission");
|
|
1702
|
+
}
|
|
1703
|
+
#requirePreparation(controller) {
|
|
1704
|
+
this.#gate();
|
|
1705
|
+
if (controller.signal.aborted) throw new FormError("PENDING", "The preparation was interrupted by clear");
|
|
1706
|
+
}
|
|
1554
1707
|
#requireField(name) {
|
|
1555
1708
|
const field = this.field(name);
|
|
1556
1709
|
if (field === void 0) throw new FormError("FIELD", `The schema declares no field named "${name}"`, { field: name });
|
|
@@ -1575,17 +1728,22 @@ var Form = class {
|
|
|
1575
1728
|
}
|
|
1576
1729
|
#evaluate() {
|
|
1577
1730
|
const disabled = this.disabled;
|
|
1731
|
+
return this.#record(this.#check(this.values, disabled));
|
|
1732
|
+
}
|
|
1733
|
+
#check(values, disabled) {
|
|
1578
1734
|
const options = this.#messages === void 0 ? { disabled } : {
|
|
1579
1735
|
messages: this.#messages,
|
|
1580
1736
|
disabled
|
|
1581
1737
|
};
|
|
1582
|
-
const errors = [...evaluateForm(this.#schema,
|
|
1738
|
+
const errors = [...evaluateForm(this.#schema, values, options)];
|
|
1583
1739
|
for (const [field, message] of this.#invalidations) if (!disabled.has(field)) errors.push(Object.freeze({
|
|
1584
1740
|
field,
|
|
1585
1741
|
message
|
|
1586
1742
|
}));
|
|
1743
|
+
return Object.freeze(errors);
|
|
1744
|
+
}
|
|
1745
|
+
#record(next) {
|
|
1587
1746
|
const previous = this.#errors;
|
|
1588
|
-
const next = Object.freeze(errors);
|
|
1589
1747
|
this.#errors = next;
|
|
1590
1748
|
const changed = next.length !== previous.length || next.some((error, index) => {
|
|
1591
1749
|
const before = previous[index];
|
|
@@ -1607,12 +1765,11 @@ var Form = class {
|
|
|
1607
1765
|
};
|
|
1608
1766
|
if (this.#status === "abandoned") this.#gate();
|
|
1609
1767
|
}
|
|
1610
|
-
#snapshot() {
|
|
1768
|
+
#snapshot(values = this.#values, disabled = this.disabled) {
|
|
1611
1769
|
const answers = {};
|
|
1612
|
-
const disabled = this.disabled;
|
|
1613
1770
|
for (const field of this.#schema.fields) {
|
|
1614
|
-
const value = Object.hasOwn(
|
|
1615
|
-
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, value);
|
|
1771
|
+
const value = Object.hasOwn(values, field.name) ? values[field.name] : void 0;
|
|
1772
|
+
if (!disabled.has(field.name) && value !== void 0) defineEntry(answers, field.name, cloneValue(value));
|
|
1616
1773
|
}
|
|
1617
1774
|
return Object.freeze(answers);
|
|
1618
1775
|
}
|
|
@@ -1626,11 +1783,20 @@ var Form = class {
|
|
|
1626
1783
|
}
|
|
1627
1784
|
}
|
|
1628
1785
|
#teardown() {
|
|
1629
|
-
|
|
1786
|
+
const submission = this.#submission;
|
|
1787
|
+
const controller = this.#controller;
|
|
1788
|
+
this.#submission = void 0;
|
|
1789
|
+
this.#controller = void 0;
|
|
1790
|
+
this.#captured = /* @__PURE__ */ new Map();
|
|
1791
|
+
this.#preparing?.abort();
|
|
1792
|
+
const abandoned = this.#status === "editing";
|
|
1793
|
+
if (abandoned) {
|
|
1630
1794
|
this.#status = "abandoned";
|
|
1631
1795
|
this.#resolvers.reject(new FormError("ABANDONED", "The form was destroyed before it settled"));
|
|
1632
|
-
this.#emitter.emit("abandon");
|
|
1633
1796
|
}
|
|
1797
|
+
controller?.abort();
|
|
1798
|
+
if (submission !== void 0) this.#emitter.emit("abort", submission);
|
|
1799
|
+
if (abandoned) this.#emitter.emit("abandon");
|
|
1634
1800
|
this.#emitter.destroy();
|
|
1635
1801
|
}
|
|
1636
1802
|
};
|