@orkestrel/form 0.0.12 → 0.0.13

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.
@@ -1,4 +1,5 @@
1
- import { attempt, isArray, isBoolean, isFiniteNumber, isInstance, isString, readArrayEntries } from "@orkestrel/contract";
1
+ import { isInstance } from "@orkestrel/contract";
2
+ import { formatMessage, matchesAnswer, matchesField, matchesValue } from "../core/index.js";
2
3
  import { Emitter } from "@orkestrel/emitter";
3
4
  //#region src/browser/errors.ts
4
5
  /** Reports an incompatible control, overlapping submission, or native DOM failure. */
@@ -30,185 +31,6 @@ var FormBindingError = class extends Error {
30
31
  function isFormBindingError(value) {
31
32
  return isInstance(value, FormBindingError);
32
33
  }
33
- Object.freeze([
34
- "text",
35
- "editor",
36
- "password",
37
- "number",
38
- "date",
39
- "time",
40
- "datetime",
41
- "color",
42
- "confirm",
43
- "select",
44
- "checkbox",
45
- "list",
46
- "file"
47
- ]);
48
- /** Lists the members every field declares, whatever its control. */
49
- var FIELD_BASE_KEYS = Object.freeze([
50
- "control",
51
- "name",
52
- "label",
53
- "help",
54
- "group",
55
- "hidden",
56
- "disabled",
57
- "locked",
58
- "rule",
59
- "meta"
60
- ]);
61
- Object.freeze({
62
- text: Object.freeze([
63
- ...FIELD_BASE_KEYS,
64
- "default",
65
- "placeholder"
66
- ]),
67
- editor: Object.freeze([
68
- ...FIELD_BASE_KEYS,
69
- "default",
70
- "placeholder"
71
- ]),
72
- password: Object.freeze([...FIELD_BASE_KEYS, "mask"]),
73
- number: Object.freeze([
74
- ...FIELD_BASE_KEYS,
75
- "default",
76
- "placeholder"
77
- ]),
78
- date: Object.freeze([...FIELD_BASE_KEYS, "default"]),
79
- time: Object.freeze([...FIELD_BASE_KEYS, "default"]),
80
- datetime: Object.freeze([...FIELD_BASE_KEYS, "default"]),
81
- color: Object.freeze([...FIELD_BASE_KEYS, "default"]),
82
- confirm: Object.freeze([...FIELD_BASE_KEYS, "default"]),
83
- select: Object.freeze([
84
- ...FIELD_BASE_KEYS,
85
- "choices",
86
- "default",
87
- "open"
88
- ]),
89
- checkbox: Object.freeze([
90
- ...FIELD_BASE_KEYS,
91
- "choices",
92
- "default"
93
- ]),
94
- list: Object.freeze([...FIELD_BASE_KEYS, "default"]),
95
- file: Object.freeze([
96
- ...FIELD_BASE_KEYS,
97
- "accept",
98
- "multiple"
99
- ])
100
- });
101
- Object.freeze([
102
- "editing",
103
- "settled",
104
- "abandoned"
105
- ]);
106
- /** Holds the default failure copy for every named field rule. */
107
- var RULE_MESSAGES = Object.freeze({
108
- required: "This field is required",
109
- minimum: "Must be at least {limit}",
110
- maximum: "Must be at most {limit}",
111
- step: "Must be a multiple of {limit}",
112
- pattern: "Must match the required format",
113
- email: "Must be a valid email address",
114
- url: "Must be a valid URL",
115
- integer: "Must be an integer",
116
- alphanumeric: "Must contain only letters and numbers"
117
- });
118
- Object.freeze(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
119
- Object.freeze(/^https?:\/\/[^\s]+$/);
120
- Object.freeze(/^[A-Za-z0-9]+$/);
121
- Object.freeze(/^[+-]?\d+$/);
122
- /** Matches a six-digit hexadecimal color string. */
123
- var COLOR_PATTERN = Object.freeze(/^#[0-9A-Fa-f]{6}$/);
124
- /** Matches an ISO calendar date string in `YYYY-MM-DD` form. */
125
- var DATE_PATTERN = Object.freeze(/^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/);
126
- /** Matches a 24-hour time string with optional seconds. */
127
- var TIME_PATTERN = Object.freeze(/^(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)?$/);
128
- /** Matches an ISO local date and time string with optional seconds. */
129
- var DATETIME_PATTERN = Object.freeze(/^\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)?$/);
130
- //#endregion
131
- //#region src/core/helpers.ts
132
- /**
133
- * Checks whether a value has the shape required by one field control.
134
- *
135
- * @remarks
136
- * Every write and every seeded value passes through this gate, and it reads `STRING_LIMIT` and
137
- * `LIST_LIMIT` before it consults the control, so no regular expression sees an over-long value.
138
- *
139
- * @param field - The field that owns the value.
140
- * @param value - The unknown value to inspect.
141
- * @returns True if the control can hold the value; false otherwise.
142
- */
143
- function matchesField(field, value) {
144
- if (isString(value) && value.length > 65536) return false;
145
- let entries;
146
- if (isArray(value)) {
147
- const length = attempt(() => value.length);
148
- if (!length.success || length.value > 1024) return false;
149
- const read = readArrayEntries(value);
150
- if (!read.success || !read.value.dense) return false;
151
- entries = read.value.entries;
152
- if (entries.some((entry) => !isString(entry) || entry.length > 65536)) return false;
153
- }
154
- switch (field.control) {
155
- case "text":
156
- case "editor":
157
- case "password": return isString(value);
158
- case "number": return isFiniteNumber(value);
159
- case "date": return isString(value) && DATE_PATTERN.test(value);
160
- case "time": return isString(value) && TIME_PATTERN.test(value);
161
- case "datetime": return isString(value) && DATETIME_PATTERN.test(value);
162
- case "color": return isString(value) && COLOR_PATTERN.test(value);
163
- case "confirm": return isBoolean(value);
164
- case "select": return isString(value) && !field.choices.some((choice) => choice.value === value && choice.disabled === true) && (field.open === true || field.choices.some((choice) => choice.value === value && choice.disabled !== true));
165
- case "checkbox": return entries !== void 0 && entries.every((entry) => isString(entry) && field.choices.some((choice) => choice.value === entry && choice.disabled !== true)) && new Set(entries).size === entries.length;
166
- case "list": return entries !== void 0;
167
- case "file": return entries !== void 0 && (field.multiple === true || entries.length <= 1);
168
- }
169
- }
170
- /**
171
- * Decides whether a raw binding value projects to an answered field.
172
- *
173
- * @remarks
174
- * Bind with `fill(name, matchesAnswer(raw) ? raw : undefined)`. This projection treats an absent
175
- * value and a string containing only whitespace as unanswered. Every other field value is an
176
- * answer, including an empty list, `false`, and zero. Core evaluation does not use this projection:
177
- * its `required` rule remains presence-only except that `list` requires an entry.
178
- *
179
- * @param value - The raw field value, or absence.
180
- * @returns True if the binding preserves the value as an answer; false otherwise.
181
- */
182
- function matchesAnswer(value) {
183
- return value !== void 0 && (!isString(value) || value.trim().length > 0);
184
- }
185
- /**
186
- * Compares two field values by scalar identity or ordered list content.
187
- *
188
- * @param a - The first field value.
189
- * @param b - The second field value.
190
- * @returns True if both values contain the same answer; false otherwise.
191
- */
192
- function matchesValue(a, b) {
193
- if (isArray(a) || isArray(b)) return isArray(a) && isArray(b) && a.length === b.length && a.every((entry, index) => entry === b[index]);
194
- return a === b;
195
- }
196
- /**
197
- * Resolves and interpolates one rule message.
198
- *
199
- * @remarks
200
- * A replacement in `messages` is read first and {@link RULE_MESSAGES} supplies the copy otherwise,
201
- * and `{limit}` in whichever text wins is replaced with the rule's operand.
202
- *
203
- * @param rule - The rule whose message to resolve.
204
- * @param limit - The optional operand substituted for `{limit}`.
205
- * @param messages - Optional rule-specific message replacements.
206
- * @returns The resolved failure text.
207
- */
208
- function formatMessage(rule, limit, messages) {
209
- const message = messages?.[rule] ?? RULE_MESSAGES[rule];
210
- return limit === void 0 ? message : message.replaceAll("{limit}", String(limit));
211
- }
212
34
  //#endregion
213
35
  //#region src/browser/helpers.ts
214
36
  /**