@viveksinghind/narrative-form-core 1.0.0
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/dist/index.d.mts +484 -0
- package/dist/index.d.ts +484 -0
- package/dist/index.js +641 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +625 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/state/formState.ts
|
|
4
|
+
var FormStateEngine = class {
|
|
5
|
+
/**
|
|
6
|
+
* Create a new FormStateEngine.
|
|
7
|
+
*
|
|
8
|
+
* @param fields - Ordered array of field configurations
|
|
9
|
+
* @param onChange - Optional callback invoked on every state mutation
|
|
10
|
+
*/
|
|
11
|
+
constructor(fields, onChange) {
|
|
12
|
+
this.fields = fields;
|
|
13
|
+
this.activeIndex = -1;
|
|
14
|
+
this.values = {};
|
|
15
|
+
this.statuses = {};
|
|
16
|
+
this.fieldTimings = {};
|
|
17
|
+
this.fieldStartTimes = {};
|
|
18
|
+
this.formStartTime = Date.now();
|
|
19
|
+
this.onChange = onChange;
|
|
20
|
+
for (const field of fields) {
|
|
21
|
+
this.statuses[field.key] = "idle";
|
|
22
|
+
this.fieldTimings[field.key] = 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Returns a readonly snapshot of the current form state. */
|
|
26
|
+
getSnapshot() {
|
|
27
|
+
return {
|
|
28
|
+
fields: this.fields,
|
|
29
|
+
activeIndex: this.activeIndex,
|
|
30
|
+
values: { ...this.values },
|
|
31
|
+
statuses: { ...this.statuses },
|
|
32
|
+
isComplete: this.fields.every((f) => this.statuses[f.key] === "confirmed")
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Returns a copy of all confirmed field values. */
|
|
36
|
+
getValues() {
|
|
37
|
+
return { ...this.values };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Returns analytics metadata about the form session.
|
|
41
|
+
*
|
|
42
|
+
* @param formId - Optional form identifier
|
|
43
|
+
* @param formVersion - Optional form version number
|
|
44
|
+
*/
|
|
45
|
+
getMeta(formId, formVersion) {
|
|
46
|
+
return {
|
|
47
|
+
formId,
|
|
48
|
+
formVersion,
|
|
49
|
+
totalTimeMs: Date.now() - this.formStartTime,
|
|
50
|
+
fieldTimings: { ...this.fieldTimings }
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Start typing animation for a specific field.
|
|
55
|
+
* Transitions the field from `idle` to `typing`.
|
|
56
|
+
*
|
|
57
|
+
* @param key - The field key to begin typing
|
|
58
|
+
*/
|
|
59
|
+
startTyping(key) {
|
|
60
|
+
const index = this.findFieldIndex(key);
|
|
61
|
+
if (index === -1) return;
|
|
62
|
+
this.statuses[key] = "typing";
|
|
63
|
+
this.notify();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Mark a field as active (typewriter finished, input is now visible).
|
|
67
|
+
* Starts the timing clock for this field.
|
|
68
|
+
*
|
|
69
|
+
* @param key - The field key to activate
|
|
70
|
+
*/
|
|
71
|
+
activateField(key) {
|
|
72
|
+
const index = this.findFieldIndex(key);
|
|
73
|
+
if (index === -1) return;
|
|
74
|
+
this.activeIndex = index;
|
|
75
|
+
this.statuses[key] = "active";
|
|
76
|
+
this.fieldStartTimes[key] = Date.now();
|
|
77
|
+
this.notify();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Confirm a field with a value.
|
|
81
|
+
* Records the time spent on the field and advances to the next one.
|
|
82
|
+
*
|
|
83
|
+
* @param key - The field key to confirm
|
|
84
|
+
* @param value - The confirmed value
|
|
85
|
+
*/
|
|
86
|
+
confirmField(key, value) {
|
|
87
|
+
const index = this.findFieldIndex(key);
|
|
88
|
+
if (index === -1) return;
|
|
89
|
+
this.values[key] = value;
|
|
90
|
+
this.statuses[key] = "confirmed";
|
|
91
|
+
const startTime = this.fieldStartTimes[key];
|
|
92
|
+
if (startTime !== void 0) {
|
|
93
|
+
this.fieldTimings[key] = Date.now() - startTime;
|
|
94
|
+
}
|
|
95
|
+
const nextIndex = this.findNextUnconfirmedIndex(index);
|
|
96
|
+
if (nextIndex !== -1) {
|
|
97
|
+
this.activeIndex = nextIndex;
|
|
98
|
+
}
|
|
99
|
+
this.notify();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reopen a confirmed field for editing.
|
|
103
|
+
* The field transitions to `editing` status with its current value preserved.
|
|
104
|
+
*
|
|
105
|
+
* @param key - The field key to edit
|
|
106
|
+
*/
|
|
107
|
+
editField(key) {
|
|
108
|
+
const index = this.findFieldIndex(key);
|
|
109
|
+
if (index === -1) return;
|
|
110
|
+
if (this.statuses[key] !== "confirmed") return;
|
|
111
|
+
this.statuses[key] = "editing";
|
|
112
|
+
this.activeIndex = index;
|
|
113
|
+
this.fieldStartTimes[key] = Date.now();
|
|
114
|
+
this.notify();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Re-confirm a field after editing.
|
|
118
|
+
*
|
|
119
|
+
* @param key - The field key to re-confirm
|
|
120
|
+
* @param value - The new confirmed value
|
|
121
|
+
*/
|
|
122
|
+
reconfirmField(key, value) {
|
|
123
|
+
var _a;
|
|
124
|
+
const index = this.findFieldIndex(key);
|
|
125
|
+
if (index === -1) return;
|
|
126
|
+
this.values[key] = value;
|
|
127
|
+
this.statuses[key] = "confirmed";
|
|
128
|
+
const startTime = this.fieldStartTimes[key];
|
|
129
|
+
if (startTime !== void 0) {
|
|
130
|
+
this.fieldTimings[key] = ((_a = this.fieldTimings[key]) != null ? _a : 0) + (Date.now() - startTime);
|
|
131
|
+
}
|
|
132
|
+
const nextIndex = this.findNextUnconfirmedIndex(-1);
|
|
133
|
+
if (nextIndex !== -1) {
|
|
134
|
+
this.activeIndex = nextIndex;
|
|
135
|
+
}
|
|
136
|
+
this.notify();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Move to the next field without confirming the current one.
|
|
140
|
+
* Useful for programmatic navigation via ref API.
|
|
141
|
+
*/
|
|
142
|
+
next() {
|
|
143
|
+
const nextIndex = this.findNextUnconfirmedIndex(this.activeIndex);
|
|
144
|
+
if (nextIndex !== -1) {
|
|
145
|
+
this.activeIndex = nextIndex;
|
|
146
|
+
this.notify();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Focus a specific field by key.
|
|
151
|
+
* Only works for confirmed fields (triggers edit) or the current active field.
|
|
152
|
+
*
|
|
153
|
+
* @param key - The field key to focus
|
|
154
|
+
*/
|
|
155
|
+
focusField(key) {
|
|
156
|
+
const index = this.findFieldIndex(key);
|
|
157
|
+
if (index === -1) return;
|
|
158
|
+
const status = this.statuses[key];
|
|
159
|
+
if (status === "confirmed") {
|
|
160
|
+
this.editField(key);
|
|
161
|
+
} else if (status === "active" || status === "editing") {
|
|
162
|
+
this.activeIndex = index;
|
|
163
|
+
this.notify();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Reset all form state to initial values.
|
|
168
|
+
* Clears all values, statuses, and timings.
|
|
169
|
+
*/
|
|
170
|
+
reset() {
|
|
171
|
+
this.activeIndex = -1;
|
|
172
|
+
this.values = {};
|
|
173
|
+
this.fieldTimings = {};
|
|
174
|
+
this.fieldStartTimes = {};
|
|
175
|
+
this.formStartTime = Date.now();
|
|
176
|
+
for (const field of this.fields) {
|
|
177
|
+
this.statuses[field.key] = "idle";
|
|
178
|
+
this.fieldTimings[field.key] = 0;
|
|
179
|
+
}
|
|
180
|
+
this.notify();
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Update the onChange listener.
|
|
184
|
+
* Used by framework wrappers to trigger re-renders.
|
|
185
|
+
*
|
|
186
|
+
* @param fn - Callback to invoke on state changes
|
|
187
|
+
*/
|
|
188
|
+
setOnChange(fn) {
|
|
189
|
+
this.onChange = fn;
|
|
190
|
+
}
|
|
191
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
192
|
+
findFieldIndex(key) {
|
|
193
|
+
return this.fields.findIndex((f) => f.key === key);
|
|
194
|
+
}
|
|
195
|
+
findNextUnconfirmedIndex(afterIndex) {
|
|
196
|
+
for (let i = afterIndex + 1; i < this.fields.length; i++) {
|
|
197
|
+
const field = this.fields[i];
|
|
198
|
+
if (field && this.statuses[field.key] !== "confirmed") {
|
|
199
|
+
return i;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return -1;
|
|
203
|
+
}
|
|
204
|
+
notify() {
|
|
205
|
+
var _a;
|
|
206
|
+
(_a = this.onChange) == null ? void 0 : _a.call(this);
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// src/validators/registry.ts
|
|
211
|
+
var registry = /* @__PURE__ */ new Map();
|
|
212
|
+
function registerValidator(name, fn) {
|
|
213
|
+
registry.set(name, fn);
|
|
214
|
+
}
|
|
215
|
+
function getValidator(name) {
|
|
216
|
+
return registry.get(name);
|
|
217
|
+
}
|
|
218
|
+
function hasValidator(name) {
|
|
219
|
+
return registry.has(name);
|
|
220
|
+
}
|
|
221
|
+
function unregisterValidator(name) {
|
|
222
|
+
registry.delete(name);
|
|
223
|
+
}
|
|
224
|
+
function clearValidators() {
|
|
225
|
+
registry.clear();
|
|
226
|
+
}
|
|
227
|
+
function getRegisteredValidatorNames() {
|
|
228
|
+
return Array.from(registry.keys());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// src/validation/engine.ts
|
|
232
|
+
function validateField(value, validation, allValues = {}) {
|
|
233
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
234
|
+
if (!validation) {
|
|
235
|
+
return { valid: true, errors: [] };
|
|
236
|
+
}
|
|
237
|
+
const errors = [];
|
|
238
|
+
const mode = (_a = validation.mode) != null ? _a : "bail";
|
|
239
|
+
const addError = (message) => {
|
|
240
|
+
errors.push(message);
|
|
241
|
+
return mode === "bail";
|
|
242
|
+
};
|
|
243
|
+
if (validation.required) {
|
|
244
|
+
const trimmed = value.trim();
|
|
245
|
+
if (trimmed.length === 0) {
|
|
246
|
+
if (addError((_b = validation.requiredMessage) != null ? _b : "This field is required")) {
|
|
247
|
+
return { valid: false, errors };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (value.trim().length === 0) {
|
|
252
|
+
return { valid: errors.length === 0, errors };
|
|
253
|
+
}
|
|
254
|
+
if (validation.minLength !== void 0 && value.length < validation.minLength) {
|
|
255
|
+
if (addError(
|
|
256
|
+
(_c = validation.minLengthMessage) != null ? _c : `Must be at least ${String(validation.minLength)} characters`
|
|
257
|
+
)) {
|
|
258
|
+
return { valid: false, errors };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (validation.maxLength !== void 0 && value.length > validation.maxLength) {
|
|
262
|
+
if (addError(
|
|
263
|
+
(_d = validation.maxLengthMessage) != null ? _d : `Must be at most ${String(validation.maxLength)} characters`
|
|
264
|
+
)) {
|
|
265
|
+
return { valid: false, errors };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (validation.exactLength !== void 0 && value.length !== validation.exactLength) {
|
|
269
|
+
if (addError(
|
|
270
|
+
(_e = validation.exactLengthMessage) != null ? _e : `Must be exactly ${String(validation.exactLength)} characters`
|
|
271
|
+
)) {
|
|
272
|
+
return { valid: false, errors };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (validation.min !== void 0 || validation.max !== void 0) {
|
|
276
|
+
const num = Number(value);
|
|
277
|
+
if (!isNaN(num)) {
|
|
278
|
+
if (validation.min !== void 0 && num < validation.min) {
|
|
279
|
+
if (addError(
|
|
280
|
+
(_f = validation.minMessage) != null ? _f : `Must be at least ${String(validation.min)}`
|
|
281
|
+
)) {
|
|
282
|
+
return { valid: false, errors };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (validation.max !== void 0 && num > validation.max) {
|
|
286
|
+
if (addError(
|
|
287
|
+
(_g = validation.maxMessage) != null ? _g : `Must be at most ${String(validation.max)}`
|
|
288
|
+
)) {
|
|
289
|
+
return { valid: false, errors };
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (validation.pattern !== void 0 && !validation.pattern.test(value)) {
|
|
295
|
+
if (addError((_h = validation.patternMessage) != null ? _h : "Invalid format")) {
|
|
296
|
+
return { valid: false, errors };
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (validation.isEmail) {
|
|
300
|
+
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
301
|
+
if (!emailPattern.test(value.trim())) {
|
|
302
|
+
if (addError((_i = validation.isEmailMessage) != null ? _i : "Enter a valid email address")) {
|
|
303
|
+
return { valid: false, errors };
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (validation.use !== void 0) {
|
|
308
|
+
const names = Array.isArray(validation.use) ? validation.use : [validation.use];
|
|
309
|
+
for (const name of names) {
|
|
310
|
+
const validatorFn = getValidator(name);
|
|
311
|
+
if (validatorFn) {
|
|
312
|
+
const result = validatorFn(value, allValues);
|
|
313
|
+
if (typeof result === "string") {
|
|
314
|
+
if (addError(result)) {
|
|
315
|
+
return { valid: false, errors };
|
|
316
|
+
}
|
|
317
|
+
} else if (result === false) {
|
|
318
|
+
if (addError(`Validation "${name}" failed`)) {
|
|
319
|
+
return { valid: false, errors };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (validation.custom !== void 0) {
|
|
326
|
+
const result = validation.custom(value, allValues);
|
|
327
|
+
if (typeof result === "string") {
|
|
328
|
+
if (addError(result)) {
|
|
329
|
+
return { valid: false, errors };
|
|
330
|
+
}
|
|
331
|
+
} else if (result === false) {
|
|
332
|
+
if (addError("Validation failed")) {
|
|
333
|
+
return { valid: false, errors };
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (validation.rules !== void 0) {
|
|
338
|
+
for (const rule of validation.rules) {
|
|
339
|
+
if (rule.async) continue;
|
|
340
|
+
const result = rule.validate(value, allValues);
|
|
341
|
+
if (typeof result === "string") {
|
|
342
|
+
if (addError(result)) {
|
|
343
|
+
return { valid: false, errors };
|
|
344
|
+
}
|
|
345
|
+
} else if (result === false) {
|
|
346
|
+
if (addError((_j = rule.message) != null ? _j : `Validation "${rule.name}" failed`)) {
|
|
347
|
+
return { valid: false, errors };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return { valid: errors.length === 0, errors };
|
|
353
|
+
}
|
|
354
|
+
function hasAsyncValidation(validation) {
|
|
355
|
+
var _a;
|
|
356
|
+
if (!validation) return false;
|
|
357
|
+
if ((_a = validation.rules) == null ? void 0 : _a.some((r) => r.async)) return true;
|
|
358
|
+
if (validation.serverValidate) return true;
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
function validateFieldAsync(value, validation, allValues = {}) {
|
|
362
|
+
const controller = new AbortController();
|
|
363
|
+
const promise = (async () => {
|
|
364
|
+
var _a, _b;
|
|
365
|
+
const syncResult = validateField(value, validation, allValues);
|
|
366
|
+
if (!syncResult.valid) {
|
|
367
|
+
return syncResult;
|
|
368
|
+
}
|
|
369
|
+
if (!validation) {
|
|
370
|
+
return { valid: true, errors: [] };
|
|
371
|
+
}
|
|
372
|
+
const errors = [];
|
|
373
|
+
const mode = (_a = validation.mode) != null ? _a : "bail";
|
|
374
|
+
const addError = (message) => {
|
|
375
|
+
errors.push(message);
|
|
376
|
+
return mode === "bail";
|
|
377
|
+
};
|
|
378
|
+
if (validation.custom !== void 0) {
|
|
379
|
+
const result = validation.custom(value, allValues);
|
|
380
|
+
if (result instanceof Promise) {
|
|
381
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
382
|
+
const resolved = await result;
|
|
383
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
384
|
+
if (typeof resolved === "string") {
|
|
385
|
+
if (addError(resolved)) return { valid: false, errors };
|
|
386
|
+
} else if (resolved === false) {
|
|
387
|
+
if (addError("Validation failed")) return { valid: false, errors };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (validation.use !== void 0) {
|
|
392
|
+
const names = Array.isArray(validation.use) ? validation.use : [validation.use];
|
|
393
|
+
for (const name of names) {
|
|
394
|
+
const validatorFn = getValidator(name);
|
|
395
|
+
if (validatorFn) {
|
|
396
|
+
const result = validatorFn(value, allValues);
|
|
397
|
+
if (result instanceof Promise) {
|
|
398
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
399
|
+
const resolved = await result;
|
|
400
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
401
|
+
if (typeof resolved === "string") {
|
|
402
|
+
if (addError(resolved)) return { valid: false, errors };
|
|
403
|
+
} else if (resolved === false) {
|
|
404
|
+
if (addError(`Validation "${name}" failed`))
|
|
405
|
+
return { valid: false, errors };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (validation.rules !== void 0) {
|
|
412
|
+
for (const rule of validation.rules) {
|
|
413
|
+
if (!rule.async) continue;
|
|
414
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
415
|
+
const result = await rule.validate(value, allValues);
|
|
416
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
417
|
+
if (typeof result === "string") {
|
|
418
|
+
if (addError(result)) return { valid: false, errors };
|
|
419
|
+
} else if (result === false) {
|
|
420
|
+
if (addError((_b = rule.message) != null ? _b : `Validation "${rule.name}" failed`))
|
|
421
|
+
return { valid: false, errors };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (validation.serverValidate) {
|
|
426
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
427
|
+
const serverResult = await runServerValidation(
|
|
428
|
+
value,
|
|
429
|
+
validation.serverValidate,
|
|
430
|
+
allValues,
|
|
431
|
+
controller.signal
|
|
432
|
+
);
|
|
433
|
+
if (controller.signal.aborted) return { valid: true, errors: [] };
|
|
434
|
+
if (!serverResult.valid) {
|
|
435
|
+
for (const err of serverResult.errors) {
|
|
436
|
+
if (addError(err)) return { valid: false, errors };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return { valid: errors.length === 0, errors };
|
|
441
|
+
})();
|
|
442
|
+
return {
|
|
443
|
+
promise,
|
|
444
|
+
abort: () => controller.abort()
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function runServerValidation(value, config, allValues, signal) {
|
|
448
|
+
var _a, _b;
|
|
449
|
+
const { url, method = "POST", headers = {}, timeout = 5e3 } = config;
|
|
450
|
+
try {
|
|
451
|
+
const timeoutId = setTimeout(() => {
|
|
452
|
+
}, timeout);
|
|
453
|
+
const fetchOptions = {
|
|
454
|
+
method,
|
|
455
|
+
headers: {
|
|
456
|
+
"Content-Type": "application/json",
|
|
457
|
+
...headers
|
|
458
|
+
},
|
|
459
|
+
signal
|
|
460
|
+
};
|
|
461
|
+
if (method === "POST") {
|
|
462
|
+
fetchOptions.body = JSON.stringify({ value, allValues });
|
|
463
|
+
}
|
|
464
|
+
const response = await fetch(url, fetchOptions);
|
|
465
|
+
clearTimeout(timeoutId);
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
return {
|
|
468
|
+
valid: false,
|
|
469
|
+
errors: [(_a = config.timeoutMessage) != null ? _a : "Server validation failed"]
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
const data = await response.json();
|
|
473
|
+
const isValid = getNestedValue(data, config.responsePath);
|
|
474
|
+
if (isValid === true || isValid === "true") {
|
|
475
|
+
return { valid: true, errors: [] };
|
|
476
|
+
}
|
|
477
|
+
const errorMsg = getNestedValue(data, config.errorPath);
|
|
478
|
+
const message = typeof errorMsg === "string" && errorMsg.length > 0 ? errorMsg : "Server validation failed";
|
|
479
|
+
return { valid: false, errors: [message] };
|
|
480
|
+
} catch (err) {
|
|
481
|
+
if (signal.aborted) {
|
|
482
|
+
return { valid: true, errors: [] };
|
|
483
|
+
}
|
|
484
|
+
const message = err instanceof Error && err.name === "AbortError" ? "Validation cancelled" : (_b = config.timeoutMessage) != null ? _b : "Server validation failed";
|
|
485
|
+
return { valid: false, errors: [message] };
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
function getNestedValue(obj, path) {
|
|
489
|
+
const parts = path.split(".");
|
|
490
|
+
let current = obj;
|
|
491
|
+
for (const part of parts) {
|
|
492
|
+
if (current === null || current === void 0) return void 0;
|
|
493
|
+
if (typeof current !== "object") return void 0;
|
|
494
|
+
current = current[part];
|
|
495
|
+
}
|
|
496
|
+
return current;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/validators/builtins.ts
|
|
500
|
+
var registered = false;
|
|
501
|
+
function registerBuiltinValidators() {
|
|
502
|
+
if (registered) return;
|
|
503
|
+
registered = true;
|
|
504
|
+
registerValidator("indianPhone", (value) => {
|
|
505
|
+
const cleaned = value.replace(/\s+/g, "");
|
|
506
|
+
return /^[6-9]\d{9}$/.test(cleaned) || "Enter a valid 10-digit Indian phone number";
|
|
507
|
+
});
|
|
508
|
+
registerValidator("indianPincode", (value) => {
|
|
509
|
+
return /^\d{6}$/.test(value.trim()) || "Enter a valid 6-digit pincode";
|
|
510
|
+
});
|
|
511
|
+
registerValidator("aadhaar", (value) => {
|
|
512
|
+
const cleaned = value.replace(/\s+/g, "");
|
|
513
|
+
return /^\d{12}$/.test(cleaned) || "Enter a valid 12-digit Aadhaar number";
|
|
514
|
+
});
|
|
515
|
+
registerValidator("pan", (value) => {
|
|
516
|
+
return /^[A-Z]{5}\d{4}[A-Z]$/.test(value.trim().toUpperCase()) || "Enter a valid PAN (e.g., ABCDE1234F)";
|
|
517
|
+
});
|
|
518
|
+
registerValidator("gst", (value) => {
|
|
519
|
+
return /^\d{2}[A-Z]{5}\d{4}[A-Z]\d[A-Z\d][A-Z]$/.test(value.trim().toUpperCase()) || "Enter a valid 15-character GST number";
|
|
520
|
+
});
|
|
521
|
+
registerValidator("ifsc", (value) => {
|
|
522
|
+
return /^[A-Z]{4}0[A-Z0-9]{6}$/.test(value.trim().toUpperCase()) || "Enter a valid IFSC code (e.g., SBIN0001234)";
|
|
523
|
+
});
|
|
524
|
+
registerValidator("email", (value) => {
|
|
525
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim()) || "Enter a valid email address";
|
|
526
|
+
});
|
|
527
|
+
registerValidator("url", (value) => {
|
|
528
|
+
try {
|
|
529
|
+
new URL(value.trim());
|
|
530
|
+
return true;
|
|
531
|
+
} catch {
|
|
532
|
+
return "Enter a valid URL";
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
registerValidator("strongPassword", (value) => {
|
|
536
|
+
if (value.length < 8) return "Password must be at least 8 characters";
|
|
537
|
+
if (!/[A-Z]/.test(value)) return "Password must contain an uppercase letter";
|
|
538
|
+
if (!/[a-z]/.test(value)) return "Password must contain a lowercase letter";
|
|
539
|
+
if (!/\d/.test(value)) return "Password must contain a number";
|
|
540
|
+
if (!/[^A-Za-z0-9]/.test(value)) return "Password must contain a special character";
|
|
541
|
+
return true;
|
|
542
|
+
});
|
|
543
|
+
registerValidator("alphanumeric", (value) => {
|
|
544
|
+
return /^[A-Za-z0-9]+$/.test(value) || "Only letters and numbers are allowed";
|
|
545
|
+
});
|
|
546
|
+
registerValidator("noSpaces", (value) => {
|
|
547
|
+
return !/\s/.test(value) || "Spaces are not allowed";
|
|
548
|
+
});
|
|
549
|
+
registerValidator("futureDate", (value) => {
|
|
550
|
+
const date = new Date(value);
|
|
551
|
+
if (isNaN(date.getTime())) return "Enter a valid date";
|
|
552
|
+
return date > /* @__PURE__ */ new Date() || "Date must be in the future";
|
|
553
|
+
});
|
|
554
|
+
registerValidator("pastDate", (value) => {
|
|
555
|
+
const date = new Date(value);
|
|
556
|
+
if (isNaN(date.getTime())) return "Enter a valid date";
|
|
557
|
+
return date < /* @__PURE__ */ new Date() || "Date must be in the past";
|
|
558
|
+
});
|
|
559
|
+
registerValidator("minAge", (value) => {
|
|
560
|
+
const date = new Date(value);
|
|
561
|
+
if (isNaN(date.getTime())) return "Enter a valid date";
|
|
562
|
+
const today = /* @__PURE__ */ new Date();
|
|
563
|
+
const age = today.getFullYear() - date.getFullYear();
|
|
564
|
+
const monthDiff = today.getMonth() - date.getMonth();
|
|
565
|
+
const dayDiff = today.getDate() - date.getDate();
|
|
566
|
+
const actualAge = monthDiff < 0 || monthDiff === 0 && dayDiff < 0 ? age - 1 : age;
|
|
567
|
+
return actualAge >= 18 || "You must be at least 18 years old";
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/i18n/strings.ts
|
|
572
|
+
var defaultStrings = {
|
|
573
|
+
editLabel: "Edit",
|
|
574
|
+
requiredMessage: "This field is required",
|
|
575
|
+
otpResend: "Resend code",
|
|
576
|
+
otpTimer: "Resend in {seconds}s",
|
|
577
|
+
submitLabel: "Submit",
|
|
578
|
+
loadingLabel: "Checking\u2026",
|
|
579
|
+
successLabel: "Looks good!",
|
|
580
|
+
retryLabel: "Try again",
|
|
581
|
+
fetchErrorMessage: "Something went wrong. Please try again."
|
|
582
|
+
};
|
|
583
|
+
function mergeStrings(custom) {
|
|
584
|
+
if (!custom) return { ...defaultStrings };
|
|
585
|
+
return { ...defaultStrings, ...custom };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/dynamic/fetchConfig.ts
|
|
589
|
+
var ConfigFetchError = class extends Error {
|
|
590
|
+
constructor(message) {
|
|
591
|
+
super(message);
|
|
592
|
+
this.name = "ConfigFetchError";
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
async function fetchFormConfig(url, options) {
|
|
596
|
+
try {
|
|
597
|
+
const response = await fetch(url, {
|
|
598
|
+
method: "GET",
|
|
599
|
+
headers: {
|
|
600
|
+
"Accept": "application/json",
|
|
601
|
+
...options == null ? void 0 : options.headers
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
if (!response.ok) {
|
|
605
|
+
throw new ConfigFetchError(`HTTP error! status: ${response.status}`);
|
|
606
|
+
}
|
|
607
|
+
const data = await response.json();
|
|
608
|
+
if (!data || typeof data !== "object") {
|
|
609
|
+
throw new ConfigFetchError("Invalid response: expected a JSON object.");
|
|
610
|
+
}
|
|
611
|
+
if (!Array.isArray(data.fields)) {
|
|
612
|
+
throw new ConfigFetchError("Invalid schema: 'fields' must be an array.");
|
|
613
|
+
}
|
|
614
|
+
return data;
|
|
615
|
+
} catch (error) {
|
|
616
|
+
if (error instanceof ConfigFetchError) {
|
|
617
|
+
throw error;
|
|
618
|
+
}
|
|
619
|
+
throw new ConfigFetchError(
|
|
620
|
+
error instanceof Error ? error.message : "Failed to fetch form configuration"
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
exports.ConfigFetchError = ConfigFetchError;
|
|
626
|
+
exports.FormStateEngine = FormStateEngine;
|
|
627
|
+
exports.clearValidators = clearValidators;
|
|
628
|
+
exports.defaultStrings = defaultStrings;
|
|
629
|
+
exports.fetchFormConfig = fetchFormConfig;
|
|
630
|
+
exports.getRegisteredValidatorNames = getRegisteredValidatorNames;
|
|
631
|
+
exports.getValidator = getValidator;
|
|
632
|
+
exports.hasAsyncValidation = hasAsyncValidation;
|
|
633
|
+
exports.hasValidator = hasValidator;
|
|
634
|
+
exports.mergeStrings = mergeStrings;
|
|
635
|
+
exports.registerBuiltinValidators = registerBuiltinValidators;
|
|
636
|
+
exports.registerValidator = registerValidator;
|
|
637
|
+
exports.unregisterValidator = unregisterValidator;
|
|
638
|
+
exports.validateField = validateField;
|
|
639
|
+
exports.validateFieldAsync = validateFieldAsync;
|
|
640
|
+
//# sourceMappingURL=index.js.map
|
|
641
|
+
//# sourceMappingURL=index.js.map
|