@10x-media/form-builder 0.1.0-beta.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.
@@ -0,0 +1,665 @@
1
+ import { t as keys } from "./keys-N5xGiUsh.js";
2
+ import { s as labelFor } from "./registry-CoCyhtvB.js";
3
+ import { transformWhereQuery } from "payload/shared";
4
+ //#region src/recall/interpolate.ts
5
+ const TOKEN = /\{\{\s*([^{}|]+?)\s*(?:\|([^{}]*?))?\}\}/g;
6
+ /**
7
+ * Replace `{{ name }}` and `{{ name|fallback }}` tokens. `resolve` returns the recalled value (''
8
+ * when absent); the fallback (or '') is substituted when `resolve` yields ''. Token-free input is
9
+ * returned unchanged, so this is a no-op for ordinary text.
10
+ */
11
+ const interpolate = (template, resolve) => template.replace(TOKEN, (_match, rawName, rawFallback) => {
12
+ const value = resolve(rawName.trim());
13
+ if (value !== "") return value;
14
+ return rawFallback === void 0 ? "" : rawFallback.trim();
15
+ });
16
+ //#endregion
17
+ //#region src/events/noopSink.ts
18
+ const noopEventSink = { emit: () => Promise.resolve() };
19
+ //#endregion
20
+ //#region src/spam/constants.ts
21
+ /** Default honeypot decoy field name. Plausible to bots, unlikely to collide with a real field name. */
22
+ const DEFAULT_HONEYPOT_FIELD = "confirm_email";
23
+ /** Reserved `values` entry carrying a captcha token from the client. */
24
+ const CAPTCHA_TOKEN_KEY = "__fb_captcha";
25
+ /** `req.context` key under which the spam guard stashes the resolved identity for upload-ownership verification. */
26
+ const IDENTITY_CONTEXT_KEY = "formBuilderSpamIdentity";
27
+ //#endregion
28
+ //#region src/presentations/defaults.ts
29
+ const DEFAULT_PRESENTATION_NAME = "page";
30
+ const defaultPresentationDescriptors = {
31
+ page: {
32
+ name: "page",
33
+ label: keys.presentationPage,
34
+ surface: "page",
35
+ density: "comfortable"
36
+ },
37
+ inline: {
38
+ name: "inline",
39
+ label: keys.presentationInline,
40
+ surface: "inline",
41
+ density: "comfortable"
42
+ },
43
+ modal: {
44
+ name: "modal",
45
+ label: keys.presentationModal,
46
+ surface: "overlay",
47
+ density: "comfortable",
48
+ dismissOnSuccess: true
49
+ },
50
+ drawer: {
51
+ name: "drawer",
52
+ label: keys.presentationDrawer,
53
+ surface: "overlay",
54
+ density: "comfortable",
55
+ dismissOnSuccess: true
56
+ }
57
+ };
58
+ //#endregion
59
+ //#region src/calc/evaluate.ts
60
+ const MAX_DEPTH = 64;
61
+ const toNumber$1 = (value) => {
62
+ if (typeof value === "number") return Number.isFinite(value) ? value : 0;
63
+ if (typeof value === "string" && value.trim() !== "") {
64
+ const n = Number(value);
65
+ return Number.isFinite(n) ? n : 0;
66
+ }
67
+ return 0;
68
+ };
69
+ const finite = (n) => Number.isFinite(n) ? n : 0;
70
+ const FUNCTIONS = {
71
+ min: (a) => a.length ? Math.min(...a) : 0,
72
+ max: (a) => a.length ? Math.max(...a) : 0,
73
+ round: (a) => Math.round(a[0] ?? 0),
74
+ abs: (a) => Math.abs(a[0] ?? 0),
75
+ ceil: (a) => Math.ceil(a[0] ?? 0),
76
+ floor: (a) => Math.floor(a[0] ?? 0)
77
+ };
78
+ const evalNode = (expr, answers, depth) => {
79
+ if (depth > MAX_DEPTH || expr == null || typeof expr !== "object") return 0;
80
+ switch (expr.type) {
81
+ case "lit": return finite(toNumber$1(expr.value));
82
+ case "ref": return toNumber$1(answers[expr.field]);
83
+ case "neg": return finite(-evalNode(expr.operand, answers, depth + 1));
84
+ case "op": {
85
+ const l = evalNode(expr.left, answers, depth + 1);
86
+ const r = evalNode(expr.right, answers, depth + 1);
87
+ switch (expr.op) {
88
+ case "+": return finite(l + r);
89
+ case "-": return finite(l - r);
90
+ case "*": return finite(l * r);
91
+ case "/": return r === 0 ? 0 : finite(l / r);
92
+ case "%": return r === 0 ? 0 : finite(l % r);
93
+ default: return 0;
94
+ }
95
+ }
96
+ case "fn": {
97
+ const fn = FUNCTIONS[expr.fn];
98
+ if (!fn) return 0;
99
+ return finite(fn(Array.isArray(expr.args) ? expr.args.map((a) => evalNode(a, answers, depth + 1)) : []));
100
+ }
101
+ case "weight": {
102
+ const weights = expr.weights ?? {};
103
+ const value = answers[expr.field];
104
+ return finite((Array.isArray(value) ? value : value == null || value === "" ? [] : [value]).reduce((sum, v) => sum + toNumber$1(weights[String(v)]), 0));
105
+ }
106
+ default: return 0;
107
+ }
108
+ };
109
+ /** Evaluate a calc expression against form answers. Total + safe: no `eval`, always finite, div/mod by zero -> 0, missing ref -> 0, depth-guarded. Isomorphic (client + server). */
110
+ const evaluateCalc = (expr, answers) => {
111
+ if (!expr) return 0;
112
+ return evalNode(expr, answers, 0);
113
+ };
114
+ //#endregion
115
+ //#region src/calc/computeCalcFields.ts
116
+ /** Returns the calc expression if the field carries one (non-null object), otherwise undefined. */
117
+ const calcExpressionOf = (field) => {
118
+ const expr = field.expression;
119
+ return expr !== null && typeof expr === "object" && !Array.isArray(expr) ? expr : void 0;
120
+ };
121
+ /**
122
+ * Returns answers with every calc field's value derived from its expression,
123
+ * folded in declaration order so a calc may reference an earlier calc's result.
124
+ * Identity when no field carries an expression.
125
+ */
126
+ const computeCalcFields = (fields, answers) => {
127
+ let next = answers;
128
+ for (const field of fields) {
129
+ const expr = calcExpressionOf(field);
130
+ if (expr) next = {
131
+ ...next,
132
+ [field.name]: evaluateCalc(expr, next)
133
+ };
134
+ }
135
+ return next;
136
+ };
137
+ //#endregion
138
+ //#region src/conditions/evaluate.ts
139
+ const isNil = (value) => value === null || value === void 0;
140
+ const toNumber = (value) => {
141
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
142
+ if (typeof value === "string" && value.trim() !== "") {
143
+ const next = Number(value);
144
+ return Number.isNaN(next) ? void 0 : next;
145
+ }
146
+ };
147
+ const toTime = (value) => {
148
+ if (value instanceof Date) return value.getTime();
149
+ if (typeof value === "string" && value.trim() !== "") {
150
+ const time = Date.parse(value);
151
+ return Number.isNaN(time) ? void 0 : time;
152
+ }
153
+ };
154
+ /** Strict, then numeric, then string equality, mirroring Payload's coerce-by-type-then-compare. */
155
+ const valuesEqual = (answer, value) => {
156
+ if (answer === value) return true;
157
+ if (isNil(answer) || isNil(value)) return false;
158
+ const a = toNumber(answer);
159
+ const b = toNumber(value);
160
+ if (a !== void 0 && b !== void 0) return a === b;
161
+ return String(answer) === String(value);
162
+ };
163
+ const toList = (value) => {
164
+ if (Array.isArray(value)) return value;
165
+ if (typeof value === "string") return value.split(",").map((entry) => entry.trim());
166
+ return [value];
167
+ };
168
+ const ordered = (answer, value, compare) => {
169
+ const a = toNumber(answer) ?? toTime(answer);
170
+ const b = toNumber(value) ?? toTime(value);
171
+ if (a === void 0 || b === void 0) return false;
172
+ return compare(a, b);
173
+ };
174
+ /** Case-insensitive: every space-separated word in `value` is a substring of `answer` (Payload `like`). */
175
+ const everyWord = (answer, value) => {
176
+ const haystack = String(answer).toLowerCase();
177
+ return String(value).toLowerCase().split(" ").filter((word) => word.length > 0).every((word) => haystack.includes(word));
178
+ };
179
+ const evaluateOperator = (operator, answer, value) => {
180
+ switch (operator) {
181
+ case "equals": return isNil(value) ? isNil(answer) : valuesEqual(answer, value);
182
+ case "not_equals": return isNil(value) ? !isNil(answer) : isNil(answer) || !valuesEqual(answer, value);
183
+ case "in": return toList(value).some((entry) => valuesEqual(answer, entry));
184
+ case "not_in": return isNil(answer) || !toList(value).some((entry) => valuesEqual(answer, entry));
185
+ case "exists": {
186
+ const present = !isNil(answer) && answer !== "";
187
+ return value === true || value === "true" ? present : !present;
188
+ }
189
+ case "greater_than": return ordered(answer, value, (a, b) => a > b);
190
+ case "greater_than_equal": return ordered(answer, value, (a, b) => a >= b);
191
+ case "less_than": return ordered(answer, value, (a, b) => a < b);
192
+ case "less_than_equal": return ordered(answer, value, (a, b) => a <= b);
193
+ case "like": return isNil(answer) ? false : everyWord(answer, value);
194
+ case "not_like": return isNil(answer) ? true : !everyWord(answer, value);
195
+ case "contains": return isNil(answer) ? false : String(answer).toLowerCase().includes(String(value).toLowerCase());
196
+ default: return false;
197
+ }
198
+ };
199
+ /**
200
+ * Evaluate one `Where` node: every top-level key is ANDed. `or` is a disjunction of sub-conditions
201
+ * (an empty `or` matches), `and` is a conjunction, and any other key is a field path whose
202
+ * `{ operator: value }` constraints are ANDed. Recurses, so nested `or`/`and` groups are supported.
203
+ */
204
+ const evaluateWhere = (where, answers) => Object.entries(where).every(([key, constraint]) => {
205
+ if (key === "or") {
206
+ const groups = Array.isArray(constraint) ? constraint : [];
207
+ return groups.length === 0 || groups.some((group) => evaluateWhere(group, answers));
208
+ }
209
+ if (key === "and") return (Array.isArray(constraint) ? constraint : []).every((group) => evaluateWhere(group, answers));
210
+ if (constraint == null || typeof constraint !== "object" || Array.isArray(constraint)) return false;
211
+ return Object.entries(constraint).every(([operator, value]) => evaluateOperator(operator, answers[key], value));
212
+ });
213
+ /**
214
+ * Evaluate a serializable `Where`-shaped condition against a flat map of (already coerced) form answers.
215
+ * An absent or empty condition matches (returns true). Operator semantics mirror Payload's query
216
+ * adapters: coerce then compare; `not_equals`/`not_in` are null-inclusive; `exists` treats `''`/absent
217
+ * as not-existing; `like` is case-insensitive and space-splits (every word must be a substring), and
218
+ * `not_like` is its logical negation (at least one word absent); `contains` is a single case-insensitive
219
+ * substring; comma-delimited `in`/`not_in` string values are split and trimmed. Geo (`near`/`within`/
220
+ * `intersects`) and `all` are out of scope and evaluate to false. Isomorphic: no `req`/DB access, so the
221
+ * renderer reuses it client-side.
222
+ */
223
+ const evaluateCondition = (where, answers) => {
224
+ if (!where || Object.keys(where).length === 0) return true;
225
+ return evaluateWhere(transformWhereQuery(where), answers);
226
+ };
227
+ //#endregion
228
+ //#region src/validation/message.ts
229
+ /** Replace `{key}` placeholders with `String(vars[key])`; unknown placeholders are left as-is. */
230
+ const resolveMessage = (template, vars) => template.replace(/\{(\w+)\}/g, (match, key) => Object.hasOwn(vars, key) ? String(vars[key]) : match);
231
+ /**
232
+ * Race a (possibly async) rule against a deadline so a hung server rule cannot stall the submit path.
233
+ * On timeout the `fallback` is resolved (the engine treats a timed-out rule as a non-blocking skip).
234
+ */
235
+ const withTimeout = (promise, ms, fallback) => Promise.race([promise, new Promise((resolve) => {
236
+ setTimeout(() => resolve(fallback), ms);
237
+ })]);
238
+ //#endregion
239
+ //#region src/validation/runValidation.ts
240
+ const RULE_TIMEOUT_MS = 5e3;
241
+ const isEmpty = (value) => value == null || value === "" || Array.isArray(value) && value.length === 0;
242
+ /** Build the per-instance message resolver: custom override (a literal template) or the localized default. */
243
+ const makeMessage = (instance, defaultMessageKey, t) => {
244
+ const { blockType: _blockType, message: _message, severity: _severity, ...params } = instance;
245
+ const template = typeof instance.message === "string" && instance.message.length > 0 ? instance.message : t(defaultMessageKey);
246
+ return (vars = {}) => resolveMessage(template, {
247
+ ...params,
248
+ ...vars
249
+ });
250
+ };
251
+ const toIssue = (result, fallbackSeverity) => {
252
+ if (result === true) return null;
253
+ if (typeof result === "string") return {
254
+ message: result,
255
+ severity: fallbackSeverity
256
+ };
257
+ return {
258
+ message: result.message,
259
+ severity: result.severity ?? fallbackSeverity
260
+ };
261
+ };
262
+ /**
263
+ * The one validation engine, run per field on client and server. Order: required, the field type's
264
+ * intrinsic validator, then each declarative rule instance. Server-only rules are skipped in client
265
+ * mode; every rule is wrapped in try/catch + a timeout so a bad rule cannot crash or stall the submit
266
+ * path (a thrown or timed-out rule fails open, logged on the server). Only `error`-severity issues
267
+ * block submission; the caller filters severity.
268
+ */
269
+ const runValidation = async (input) => {
270
+ const { field, fieldDefinition, value, fieldType, ruleRegistry, answers, locale, t, operation, event, mode, req, payload, formId, timeoutMs } = input;
271
+ const timeout = timeoutMs ?? RULE_TIMEOUT_MS;
272
+ const errors = [];
273
+ if (field.required && isEmpty(value)) return { errors: [{
274
+ message: t(keys.validationRequired),
275
+ severity: "error"
276
+ }] };
277
+ if (isEmpty(value)) return { errors: [] };
278
+ if (fieldDefinition?.validate) try {
279
+ const ran = fieldDefinition.validate({
280
+ value,
281
+ config: field,
282
+ siblingData: answers,
283
+ data: answers,
284
+ locale,
285
+ t
286
+ });
287
+ const result = ran instanceof Promise ? await withTimeout(ran, timeout, true) : ran;
288
+ if (result !== true) errors.push({
289
+ message: result,
290
+ severity: "error"
291
+ });
292
+ } catch (error) {
293
+ req?.payload?.logger?.error?.({
294
+ err: error,
295
+ field: field.name
296
+ }, "form-builder intrinsic field validator threw");
297
+ }
298
+ if (fieldDefinition?.schema) try {
299
+ const outcome = await fieldDefinition.schema["~standard"].validate(value);
300
+ if (outcome.issues) for (const issue of outcome.issues) errors.push({
301
+ message: issue.message,
302
+ severity: "error"
303
+ });
304
+ } catch (error) {
305
+ req?.payload?.logger?.error?.({
306
+ err: error,
307
+ field: field.name
308
+ }, "form-builder field schema threw");
309
+ }
310
+ const instances = Array.isArray(field.validations) ? field.validations : [];
311
+ for (const instance of instances) {
312
+ const rule = ruleRegistry.get(instance.blockType);
313
+ if (!rule) continue;
314
+ if (mode === "client" && rule.client === false) continue;
315
+ const message = makeMessage(instance, rule.defaultMessage, t);
316
+ const fallbackSeverity = instance.severity ?? rule.severity ?? "error";
317
+ try {
318
+ const ran = rule.validate({
319
+ value,
320
+ params: instance,
321
+ siblingData: answers,
322
+ data: answers,
323
+ field,
324
+ fieldType,
325
+ operation,
326
+ event,
327
+ locale,
328
+ message,
329
+ req: mode === "server" ? req : void 0,
330
+ payload: mode === "server" ? payload : void 0,
331
+ formId: mode === "server" ? formId : void 0
332
+ });
333
+ const issue = toIssue(ran instanceof Promise ? await withTimeout(ran, timeout, true) : ran, fallbackSeverity);
334
+ if (issue) errors.push(issue);
335
+ } catch (error) {
336
+ req?.payload?.logger?.error?.({
337
+ err: error,
338
+ rule: rule.type
339
+ }, "form-builder validation rule threw");
340
+ }
341
+ }
342
+ return { errors };
343
+ };
344
+ //#endregion
345
+ //#region src/validation/defineValidationRule.ts
346
+ /**
347
+ * Define a validation rule type once: its `params` become a Payload `Field[]` in the per-field
348
+ * constraint list, its typed `validate` runs in the one engine on client and server. Built-in rules
349
+ * use this same primitive, so custom rules are never second-class.
350
+ */
351
+ const defineValidationRule = (rule) => rule;
352
+ //#endregion
353
+ //#region src/validation/builtin/email.ts
354
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
355
+ const emailRule = defineValidationRule({
356
+ type: "email",
357
+ label: keys.ruleEmail,
358
+ appliesTo: [
359
+ "text",
360
+ "textarea",
361
+ "email"
362
+ ],
363
+ defaultMessage: keys.ruleEmailMessage,
364
+ validate: ({ value, message }) => value == null || value === "" || EMAIL_PATTERN.test(value) ? true : message()
365
+ });
366
+ //#endregion
367
+ //#region src/validation/builtin/matchesField.ts
368
+ const matchesFieldRule = defineValidationRule({
369
+ type: "matchesField",
370
+ label: keys.ruleMatchesField,
371
+ params: [{
372
+ name: "field",
373
+ type: "text",
374
+ required: true,
375
+ label: labelFor(keys.ruleParamField)
376
+ }],
377
+ defaultMessage: keys.ruleMatchesFieldMessage,
378
+ validate: ({ value, params, siblingData, message }) => value === siblingData[params.field] ? true : message()
379
+ });
380
+ //#endregion
381
+ //#region src/validation/builtin/max.ts
382
+ const maxRule = defineValidationRule({
383
+ type: "max",
384
+ label: keys.ruleMax,
385
+ appliesTo: ["number"],
386
+ params: [{
387
+ name: "max",
388
+ type: "number",
389
+ required: true,
390
+ label: labelFor(keys.ruleParamMax)
391
+ }],
392
+ defaultMessage: keys.ruleMaxMessage,
393
+ validate: ({ value, params, message }) => value == null || value <= params.max ? true : message({ max: params.max })
394
+ });
395
+ //#endregion
396
+ //#region src/validation/builtin/maxLength.ts
397
+ const maxLengthRule = defineValidationRule({
398
+ type: "maxLength",
399
+ label: keys.ruleMaxLength,
400
+ appliesTo: [
401
+ "text",
402
+ "textarea",
403
+ "email"
404
+ ],
405
+ params: [{
406
+ name: "max",
407
+ type: "number",
408
+ required: true,
409
+ min: 0,
410
+ label: labelFor(keys.ruleParamMax)
411
+ }],
412
+ defaultMessage: keys.ruleMaxLengthMessage,
413
+ validate: ({ value, params, message }) => value == null || value === "" || value.length <= params.max ? true : message({ max: params.max })
414
+ });
415
+ //#endregion
416
+ //#region src/validation/builtin/min.ts
417
+ const minRule = defineValidationRule({
418
+ type: "min",
419
+ label: keys.ruleMin,
420
+ appliesTo: ["number"],
421
+ params: [{
422
+ name: "min",
423
+ type: "number",
424
+ required: true,
425
+ label: labelFor(keys.ruleParamMin)
426
+ }],
427
+ defaultMessage: keys.ruleMinMessage,
428
+ validate: ({ value, params, message }) => value == null || value >= params.min ? true : message({ min: params.min })
429
+ });
430
+ //#endregion
431
+ //#region src/validation/builtin/minLength.ts
432
+ const minLengthRule = defineValidationRule({
433
+ type: "minLength",
434
+ label: keys.ruleMinLength,
435
+ appliesTo: [
436
+ "text",
437
+ "textarea",
438
+ "email"
439
+ ],
440
+ params: [{
441
+ name: "min",
442
+ type: "number",
443
+ required: true,
444
+ min: 0,
445
+ label: labelFor(keys.ruleParamMin)
446
+ }],
447
+ defaultMessage: keys.ruleMinLengthMessage,
448
+ validate: ({ value, params, message }) => value == null || value === "" || value.length >= params.min ? true : message({ min: params.min })
449
+ });
450
+ //#endregion
451
+ //#region src/validation/builtin/notAlreadySubmitted.ts
452
+ const FORM_SUBMISSIONS_SLUG = "form-submissions";
453
+ const SCAN_LIMIT = 500;
454
+ /**
455
+ * Server-only async exemplar: rejects a value already present for the same form. Scans up to
456
+ * `SCAN_LIMIT` recent submissions in JS (the values live in a json column, so a portable DB query is
457
+ * not available); a future phase can add an indexed dedup column for scale.
458
+ */
459
+ const notAlreadySubmittedRule = defineValidationRule({
460
+ type: "notAlreadySubmitted",
461
+ label: keys.ruleNotAlreadySubmitted,
462
+ client: false,
463
+ defaultMessage: keys.ruleNotAlreadySubmittedMessage,
464
+ validate: async ({ value, field, payload, req, formId, message }) => {
465
+ if (value == null || value === "" || !payload || formId == null) return true;
466
+ return (await payload.find({
467
+ collection: FORM_SUBMISSIONS_SLUG,
468
+ where: { form: { equals: formId } },
469
+ limit: SCAN_LIMIT,
470
+ depth: 0,
471
+ req
472
+ })).docs.some((doc) => {
473
+ return (doc.values ?? []).some((entry) => entry.field === field.name && entry.value === value);
474
+ }) ? message() : true;
475
+ }
476
+ });
477
+ //#endregion
478
+ //#region src/validation/builtin/oneOf.ts
479
+ const oneOfRule = defineValidationRule({
480
+ type: "oneOf",
481
+ label: keys.ruleOneOf,
482
+ appliesTo: [
483
+ "text",
484
+ "textarea",
485
+ "select"
486
+ ],
487
+ params: [{
488
+ name: "values",
489
+ type: "array",
490
+ label: labelFor(keys.ruleParamValues),
491
+ fields: [{
492
+ name: "value",
493
+ type: "text",
494
+ required: true
495
+ }]
496
+ }],
497
+ defaultMessage: keys.ruleOneOfMessage,
498
+ validate: ({ value, params, message }) => {
499
+ if (value == null || value === "") return true;
500
+ const allowed = (params.values ?? []).map((entry) => entry.value);
501
+ if (allowed.length === 0) return true;
502
+ return allowed.includes(value) ? true : message();
503
+ }
504
+ });
505
+ //#endregion
506
+ //#region src/validation/builtin/pattern.ts
507
+ const tryRegExp = (pattern, flags) => {
508
+ try {
509
+ return new RegExp(pattern, flags);
510
+ } catch {
511
+ return;
512
+ }
513
+ };
514
+ const patternRule = defineValidationRule({
515
+ type: "pattern",
516
+ label: keys.rulePattern,
517
+ appliesTo: [
518
+ "text",
519
+ "textarea",
520
+ "email"
521
+ ],
522
+ params: [{
523
+ name: "pattern",
524
+ type: "text",
525
+ required: true,
526
+ label: labelFor(keys.ruleParamPattern)
527
+ }, {
528
+ name: "flags",
529
+ type: "text",
530
+ label: labelFor(keys.ruleParamFlags)
531
+ }],
532
+ defaultMessage: keys.rulePatternMessage,
533
+ validate: ({ value, params, message }) => {
534
+ if (value == null || value === "") return true;
535
+ const regex = tryRegExp(params.pattern, params.flags);
536
+ if (!regex) return true;
537
+ return regex.test(value) ? true : message();
538
+ }
539
+ });
540
+ //#endregion
541
+ //#region src/validation/builtin/url.ts
542
+ const isUrl = (value) => {
543
+ try {
544
+ const url = new URL(value);
545
+ return url.protocol === "http:" || url.protocol === "https:";
546
+ } catch {
547
+ return false;
548
+ }
549
+ };
550
+ //#endregion
551
+ //#region src/validation/builtin/index.ts
552
+ const defaultValidationRules = [
553
+ minLengthRule,
554
+ maxLengthRule,
555
+ minRule,
556
+ maxRule,
557
+ patternRule,
558
+ emailRule,
559
+ defineValidationRule({
560
+ type: "url",
561
+ label: keys.ruleUrl,
562
+ appliesTo: ["text", "textarea"],
563
+ defaultMessage: keys.ruleUrlMessage,
564
+ validate: ({ value, message }) => value == null || value === "" || isUrl(value) ? true : message()
565
+ }),
566
+ oneOfRule,
567
+ matchesFieldRule,
568
+ notAlreadySubmittedRule
569
+ ];
570
+ //#endregion
571
+ //#region src/validation/registry.ts
572
+ const buildRuleRegistry = (rules) => {
573
+ const registry = /* @__PURE__ */ new Map();
574
+ for (const rule of rules) registry.set(rule.type, rule);
575
+ return registry;
576
+ };
577
+ /** Resolve the active rule registry from the built-in defaults and the plugin `rules` option. */
578
+ const resolveValidationRules = (defaults, config = {}) => {
579
+ const registry = buildRuleRegistry(defaults);
580
+ for (const [type, option] of Object.entries(config)) if (option === false) registry.delete(type);
581
+ else if (option === true) {} else registry.set(type, {
582
+ ...option,
583
+ type
584
+ });
585
+ return registry;
586
+ };
587
+ //#endregion
588
+ //#region src/prefill/valuesFromSearchParams.ts
589
+ const TRUTHY = new Set([
590
+ "true",
591
+ "1",
592
+ "on",
593
+ "yes"
594
+ ]);
595
+ const coerce = (kind, raw, all) => {
596
+ switch (kind) {
597
+ case "number": {
598
+ const n = Number(raw);
599
+ return Number.isNaN(n) ? void 0 : n;
600
+ }
601
+ case "boolean": return TRUTHY.has(raw.toLowerCase());
602
+ case "text[]": return all;
603
+ default: return raw;
604
+ }
605
+ };
606
+ /**
607
+ * Map URL/query params to typed initial values for KNOWN fields only, coerced by each field's value
608
+ * kind. Unknown, denied, or un-allowed params are ignored; invalid numbers are dropped. Prefilled
609
+ * values are never trusted -- they still validate on submit. Pass the result to `<Form initialValues>`.
610
+ */
611
+ const valuesFromSearchParams = (params, fields, registry, options = {}) => {
612
+ const isSearch = params instanceof URLSearchParams;
613
+ const paramKeys = isSearch ? [...new Set(params.keys())] : Object.keys(params);
614
+ const byName = new Map(fields.map((field) => [field.name, field]));
615
+ const result = {};
616
+ for (const param of paramKeys) {
617
+ const fieldName = options.map?.[param] ?? param;
618
+ if (options.allow && !options.allow.includes(fieldName)) continue;
619
+ if (options.deny?.includes(fieldName)) continue;
620
+ const field = byName.get(fieldName);
621
+ const definition = field && registry.get(field.blockType);
622
+ if (!field || !definition) continue;
623
+ const all = isSearch ? params.getAll(param) : [params[param]];
624
+ const value = coerce(definition.value, all[0] ?? "", all);
625
+ if (value !== void 0) result[fieldName] = value;
626
+ }
627
+ return result;
628
+ };
629
+ //#endregion
630
+ //#region src/recall/resolver.ts
631
+ /** Derive a value->label map from a select-like field instance's `options`. */
632
+ const optionLabelsFor = (field) => {
633
+ const options = field.options;
634
+ if (!Array.isArray(options)) return;
635
+ const map = {};
636
+ for (const option of options) if (option && typeof option.value === "string") map[option.value] = option.label ?? option.value;
637
+ return Object.keys(map).length > 0 ? map : void 0;
638
+ };
639
+ /**
640
+ * Build a recall resolver: `{{name}}` -> the field's current value, formatted via its type's `format`
641
+ * (so a select shows its option label, a checkbox Yes/No, a date localized). Missing field or empty
642
+ * value -> ''. Pure and isomorphic; the renderer and server templates share it.
643
+ */
644
+ const buildRecallResolver = (args) => {
645
+ const byName = new Map(args.fields.map((field) => [field.name, field]));
646
+ return (name) => {
647
+ const field = byName.get(name);
648
+ if (!field) return "";
649
+ const value = args.values[name];
650
+ if (value == null || value === "" || Array.isArray(value) && value.length === 0) return "";
651
+ const definition = args.registry.get(field.blockType);
652
+ if (definition?.format) return definition.format({
653
+ value,
654
+ config: {},
655
+ optionLabels: optionLabelsFor(field),
656
+ locale: args.locale,
657
+ t: args.t
658
+ });
659
+ return Array.isArray(value) ? value.join(", ") : String(value);
660
+ };
661
+ };
662
+ //#endregion
663
+ export { IDENTITY_CONTEXT_KEY as _, resolveValidationRules as a, runValidation as c, computeCalcFields as d, evaluateCalc as f, DEFAULT_HONEYPOT_FIELD as g, CAPTCHA_TOKEN_KEY as h, buildRuleRegistry as i, evaluateCondition as l, defaultPresentationDescriptors as m, optionLabelsFor as n, defaultValidationRules as o, DEFAULT_PRESENTATION_NAME as p, valuesFromSearchParams as r, defineValidationRule as s, buildRecallResolver as t, calcExpressionOf as u, noopEventSink as v, interpolate as y };
664
+
665
+ //# sourceMappingURL=resolver-OeQyVH2N.js.map