@usefillo/core 0.9.0 → 0.11.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/README.md +18 -13
- package/dist/index.d.ts +462 -12
- package/dist/index.js +771 -139
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,117 @@ function isField(block) {
|
|
|
4
4
|
return !CONTENT_KINDS.includes(block.kind);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
// src/canonical.ts
|
|
8
|
+
function isEmpty(value) {
|
|
9
|
+
if (value === null || value === void 0) return true;
|
|
10
|
+
if (typeof value === "string") return value.trim() === "";
|
|
11
|
+
if (Array.isArray(value)) return value.length === 0;
|
|
12
|
+
if (typeof value === "boolean") return value === false;
|
|
13
|
+
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
function normalizeValue(field2, value) {
|
|
17
|
+
if (value === void 0 || value === null) return value;
|
|
18
|
+
if (field2.kind === "custom") return value;
|
|
19
|
+
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
20
|
+
const n = Number(value);
|
|
21
|
+
return Number.isNaN(n) ? value : n;
|
|
22
|
+
}
|
|
23
|
+
if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
|
|
24
|
+
const n = Number(value);
|
|
25
|
+
return Number.isNaN(n) ? value : n;
|
|
26
|
+
}
|
|
27
|
+
if (typeof value === "string") return value.trim();
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function canonicalValue(field2, value) {
|
|
31
|
+
const normalized = normalizeValue(field2, value);
|
|
32
|
+
return isEmpty(normalized) ? void 0 : normalized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// src/calc.ts
|
|
36
|
+
function clampDecimals(decimals) {
|
|
37
|
+
if (decimals === void 0 || !Number.isFinite(decimals)) return 0;
|
|
38
|
+
return Math.max(0, Math.min(6, Math.round(decimals)));
|
|
39
|
+
}
|
|
40
|
+
function roundHalfAwayFromZero(value, decimals) {
|
|
41
|
+
const factor = 10 ** decimals;
|
|
42
|
+
const rounded = Math.round(Math.abs(value) * factor) / factor;
|
|
43
|
+
return value < 0 ? -rounded : rounded;
|
|
44
|
+
}
|
|
45
|
+
function finiteOrNull(value) {
|
|
46
|
+
return Number.isFinite(value) ? value : null;
|
|
47
|
+
}
|
|
48
|
+
function evaluateCalc(expr, resolve) {
|
|
49
|
+
switch (expr.op) {
|
|
50
|
+
case "value":
|
|
51
|
+
return conditionNumber(resolve(expr.fieldId));
|
|
52
|
+
case "const":
|
|
53
|
+
return finiteOrNull(expr.value);
|
|
54
|
+
case "add":
|
|
55
|
+
case "mul":
|
|
56
|
+
case "min":
|
|
57
|
+
case "max": {
|
|
58
|
+
if (expr.args.length === 0) return null;
|
|
59
|
+
const values = [];
|
|
60
|
+
for (const arg of expr.args) {
|
|
61
|
+
const value = evaluateCalc(arg, resolve);
|
|
62
|
+
if (value === null) return null;
|
|
63
|
+
values.push(value);
|
|
64
|
+
}
|
|
65
|
+
if (expr.op === "add") return finiteOrNull(values.reduce((a, b) => a + b, 0));
|
|
66
|
+
if (expr.op === "mul") return finiteOrNull(values.reduce((a, b) => a * b, 1));
|
|
67
|
+
return expr.op === "min" ? Math.min(...values) : Math.max(...values);
|
|
68
|
+
}
|
|
69
|
+
case "sub":
|
|
70
|
+
case "div": {
|
|
71
|
+
const left = evaluateCalc(expr.left, resolve);
|
|
72
|
+
if (left === null) return null;
|
|
73
|
+
const right = evaluateCalc(expr.right, resolve);
|
|
74
|
+
if (right === null) return null;
|
|
75
|
+
if (expr.op === "sub") return finiteOrNull(left - right);
|
|
76
|
+
return right === 0 ? null : finiteOrNull(left / right);
|
|
77
|
+
}
|
|
78
|
+
case "round": {
|
|
79
|
+
const value = evaluateCalc(expr.arg, resolve);
|
|
80
|
+
return value === null ? null : finiteOrNull(roundHalfAwayFromZero(value, clampDecimals(expr.decimals)));
|
|
81
|
+
}
|
|
82
|
+
case "if":
|
|
83
|
+
return evaluateCalc(conditionsMet(expr.when, resolve) ? expr.then : expr.else, resolve);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function evaluateCalculatedField(field2, resolve) {
|
|
87
|
+
const value = evaluateCalc(field2.calc, resolve);
|
|
88
|
+
if (value === null || field2.decimals === void 0) return value;
|
|
89
|
+
return finiteOrNull(roundHalfAwayFromZero(value, clampDecimals(field2.decimals)));
|
|
90
|
+
}
|
|
91
|
+
function computeCalculated(form, data) {
|
|
92
|
+
const fields = allFields(form);
|
|
93
|
+
if (!fields.some((field2) => field2.kind === "calculated")) return data;
|
|
94
|
+
let view = data;
|
|
95
|
+
let reachable = reachableFieldIds(form, view);
|
|
96
|
+
for (let i = 0; i <= fields.length; i++) {
|
|
97
|
+
const next = {};
|
|
98
|
+
let pruned = false;
|
|
99
|
+
for (const [key, value] of Object.entries(view)) {
|
|
100
|
+
if (reachable.has(key)) next[key] = value;
|
|
101
|
+
else pruned = true;
|
|
102
|
+
}
|
|
103
|
+
if (!pruned) break;
|
|
104
|
+
view = next;
|
|
105
|
+
reachable = reachableFieldIds(form, view);
|
|
106
|
+
}
|
|
107
|
+
const state = resolveLogicState(fields, view);
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [key, value] of Object.entries(data)) {
|
|
110
|
+
if (!state.calcIds.has(key)) out[key] = value;
|
|
111
|
+
}
|
|
112
|
+
for (const [key, value] of Object.entries(state.calc)) {
|
|
113
|
+
if (reachable.has(key)) out[key] = value;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
7
118
|
// src/logic.ts
|
|
8
119
|
function isAnswered(value) {
|
|
9
120
|
if (value === null || value === void 0) return false;
|
|
@@ -62,37 +173,71 @@ function blockVisibleWith(block, resolve) {
|
|
|
62
173
|
if (!block.visibleIf || block.visibleIf.length === 0) return true;
|
|
63
174
|
return conditionsMet(block.visibleIf, resolve);
|
|
64
175
|
}
|
|
65
|
-
function makeResolver(
|
|
66
|
-
return (fieldId) =>
|
|
176
|
+
function makeResolver(canonical, raw, scoped, visible, calcIds, calc) {
|
|
177
|
+
return (fieldId) => {
|
|
178
|
+
if (scoped.has(fieldId)) {
|
|
179
|
+
if (!visible.has(fieldId)) return void 0;
|
|
180
|
+
return calcIds.has(fieldId) ? calc[fieldId] : canonical[fieldId];
|
|
181
|
+
}
|
|
182
|
+
return raw[fieldId];
|
|
183
|
+
};
|
|
67
184
|
}
|
|
68
|
-
function
|
|
185
|
+
function resolveLogicState(fields, data) {
|
|
69
186
|
const scoped = new Set(fields.map((f) => f.id));
|
|
187
|
+
const calcFields = fields.filter((f) => f.kind === "calculated");
|
|
188
|
+
const calcIds = new Set(calcFields.map((f) => f.id));
|
|
189
|
+
const canonical = {};
|
|
190
|
+
for (const f of fields) {
|
|
191
|
+
if (f.kind === "calculated") continue;
|
|
192
|
+
const value = canonicalValue(f, data[f.id]);
|
|
193
|
+
if (value !== void 0) canonical[f.id] = value;
|
|
194
|
+
}
|
|
70
195
|
let visible = scoped;
|
|
196
|
+
let calc = {};
|
|
71
197
|
for (let i = 0; i <= fields.length; i++) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
198
|
+
let nextCalc = calc;
|
|
199
|
+
if (calcFields.length > 0) {
|
|
200
|
+
nextCalc = { ...calc };
|
|
201
|
+
const resolveCalc = makeResolver(canonical, data, scoped, visible, calcIds, nextCalc);
|
|
202
|
+
for (const field2 of calcFields) {
|
|
203
|
+
const value = visible.has(field2.id) ? evaluateCalculatedField(field2, resolveCalc) : null;
|
|
204
|
+
if (value === null) delete nextCalc[field2.id];
|
|
205
|
+
else nextCalc[field2.id] = value;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const resolve = makeResolver(canonical, data, scoped, visible, calcIds, nextCalc);
|
|
209
|
+
const nextVisible = /* @__PURE__ */ new Set();
|
|
210
|
+
for (const f of fields) if (blockVisibleWith(f, resolve)) nextVisible.add(f.id);
|
|
211
|
+
const stable = nextVisible.size === visible.size && [...nextVisible].every((id) => visible.has(id)) && calcEquals(calc, nextCalc);
|
|
212
|
+
visible = nextVisible;
|
|
213
|
+
calc = nextCalc;
|
|
214
|
+
if (stable) break;
|
|
77
215
|
}
|
|
78
|
-
return
|
|
216
|
+
return {
|
|
217
|
+
visible,
|
|
218
|
+
calc,
|
|
219
|
+
calcIds,
|
|
220
|
+
resolve: makeResolver(canonical, data, scoped, visible, calcIds, calc)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function calcEquals(a, b) {
|
|
224
|
+
const aKeys = Object.keys(a);
|
|
225
|
+
return aKeys.length === Object.keys(b).length && aKeys.every((key) => a[key] === b[key]);
|
|
226
|
+
}
|
|
227
|
+
function visibleFieldIds(fields, data) {
|
|
228
|
+
return resolveLogicState(fields, data).visible;
|
|
79
229
|
}
|
|
80
230
|
function isBlockVisible(block, data) {
|
|
81
231
|
return blockVisibleWith(block, (fieldId) => data[fieldId]);
|
|
82
232
|
}
|
|
83
233
|
function visibleBlocks(page, data) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
const resolve = makeResolver(data, scoped, visibleFieldIds(fields, data));
|
|
87
|
-
return page.blocks.filter((b) => blockVisibleWith(b, resolve));
|
|
234
|
+
const state = resolveLogicState(page.blocks.filter(isField), data);
|
|
235
|
+
return page.blocks.filter((b) => blockVisibleWith(b, state.resolve));
|
|
88
236
|
}
|
|
89
237
|
function visiblePageBlocks(form, page, data) {
|
|
90
|
-
const
|
|
91
|
-
const scoped = new Set(fields.map((f) => f.id));
|
|
92
|
-
const formVisible = visibleFieldIds(fields, data);
|
|
93
|
-
const resolve = makeResolver(data, scoped, formVisible);
|
|
238
|
+
const state = resolveLogicState(allFields(form), data);
|
|
94
239
|
return page.blocks.filter(
|
|
95
|
-
(b) => isField(b) ?
|
|
240
|
+
(b) => isField(b) ? state.visible.has(b.id) : blockVisibleWith(b, state.resolve)
|
|
96
241
|
);
|
|
97
242
|
}
|
|
98
243
|
function allFields(form) {
|
|
@@ -113,9 +258,7 @@ function visibleFields(form, data) {
|
|
|
113
258
|
return fields.filter((f) => visible.has(f.id));
|
|
114
259
|
}
|
|
115
260
|
function formResolver(form, data) {
|
|
116
|
-
|
|
117
|
-
const scoped = new Set(fields.map((f) => f.id));
|
|
118
|
-
return makeResolver(data, scoped, visibleFieldIds(fields, data));
|
|
261
|
+
return resolveLogicState(allFields(form), data).resolve;
|
|
119
262
|
}
|
|
120
263
|
function resolveNextPage(form, currentPageId, data) {
|
|
121
264
|
const page = form.pages.find((p) => p.id === currentPageId);
|
|
@@ -181,79 +324,122 @@ function reachableFields(form, data) {
|
|
|
181
324
|
import * as z from "zod/mini";
|
|
182
325
|
|
|
183
326
|
// src/phone.ts
|
|
184
|
-
var
|
|
327
|
+
var regionNames;
|
|
328
|
+
function regionDisplayNames() {
|
|
329
|
+
if (regionNames !== void 0) return regionNames ?? void 0;
|
|
330
|
+
if (typeof Intl === "undefined" || typeof Intl.DisplayNames === "undefined") {
|
|
331
|
+
regionNames = null;
|
|
332
|
+
return void 0;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
regionNames = new Intl.DisplayNames(void 0, { type: "region" });
|
|
336
|
+
} catch {
|
|
337
|
+
regionNames = null;
|
|
338
|
+
}
|
|
339
|
+
return regionNames ?? void 0;
|
|
340
|
+
}
|
|
341
|
+
function regionName(iso2) {
|
|
342
|
+
try {
|
|
343
|
+
return regionDisplayNames()?.of(iso2) ?? iso2;
|
|
344
|
+
} catch {
|
|
345
|
+
return iso2;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var C = (iso2, dialCode, lengths = [], groups = [], example = "") => ({ iso2, name: regionName(iso2), dialCode, lengths, groups, example });
|
|
349
|
+
var CURATED_COUNTRIES = [
|
|
350
|
+
C("US", "1", [10], [3, 3, 4], "2015550123"),
|
|
351
|
+
C("CA", "1", [10], [3, 3, 4], "5145550123"),
|
|
352
|
+
C("GB", "44", [10], [4, 6], "7400123456"),
|
|
353
|
+
C("AU", "61", [9], [3, 3, 3], "412345678"),
|
|
354
|
+
C("DE", "49", [10, 11], [4, 7], "15123456789"),
|
|
355
|
+
C("FR", "33", [9], [1, 2, 2, 2, 2], "612345678"),
|
|
356
|
+
C("IN", "91", [10], [5, 5], "8123456789"),
|
|
357
|
+
C("DK", "45", [8], [2, 2, 2, 2], "32123456"),
|
|
358
|
+
C("SE", "46", [9], [2, 3, 4], "701234567"),
|
|
359
|
+
C("NO", "47", [8], [3, 2, 3], "40612345"),
|
|
360
|
+
C("FI", "358", [9, 10], [2, 3, 4], "412345678"),
|
|
361
|
+
C("NL", "31", [9], [1, 4, 4], "612345678"),
|
|
362
|
+
C("BE", "32", [9], [3, 2, 2, 2], "470123456"),
|
|
363
|
+
C("ES", "34", [9], [3, 3, 3], "612345678"),
|
|
364
|
+
C("IT", "39", [9, 10], [3, 3, 4], "3123456789"),
|
|
365
|
+
C("PT", "351", [9], [3, 3, 3], "912345678"),
|
|
366
|
+
C("IE", "353", [9], [2, 3, 4], "851234567"),
|
|
367
|
+
C("CH", "41", [9], [2, 3, 2, 2], "781234567"),
|
|
368
|
+
C("AT", "43", [10, 11], [3, 7], "6641234567"),
|
|
369
|
+
C("PL", "48", [9], [3, 3, 3], "512345678"),
|
|
370
|
+
C("CZ", "420", [9], [3, 3, 3], "601123456"),
|
|
371
|
+
C("GR", "30", [10], [3, 3, 4], "6912345678"),
|
|
372
|
+
C("RO", "40", [9], [3, 3, 3], "712345678"),
|
|
373
|
+
C("HU", "36", [9], [2, 3, 4], "201234567"),
|
|
374
|
+
C("UA", "380", [9], [2, 3, 2, 2], "501234567"),
|
|
375
|
+
C("RU", "7", [10], [3, 3, 2, 2], "9123456789"),
|
|
376
|
+
C("TR", "90", [10], [3, 3, 2, 2], "5012345678"),
|
|
377
|
+
C("IL", "972", [9], [2, 3, 4], "501234567"),
|
|
378
|
+
C("AE", "971", [9], [2, 3, 4], "501234567"),
|
|
379
|
+
C("SA", "966", [9], [2, 3, 4], "512345678"),
|
|
380
|
+
C("ZA", "27", [9], [2, 3, 4], "711234567"),
|
|
381
|
+
C("NG", "234", [10], [3, 3, 4], "8021234567"),
|
|
382
|
+
C("KE", "254", [9], [3, 3, 3], "712123456"),
|
|
383
|
+
C("EG", "20", [10], [3, 3, 4], "1001234567"),
|
|
384
|
+
C("BR", "55", [10, 11], [2, 5, 4], "11961234567"),
|
|
385
|
+
C("MX", "52", [10], [3, 3, 4], "2221234567"),
|
|
386
|
+
C("AR", "54", [10], [4, 6], "1123456789"),
|
|
387
|
+
C("CL", "56", [9], [1, 4, 4], "221234567"),
|
|
388
|
+
C("CO", "57", [10], [3, 3, 4], "3211234567"),
|
|
389
|
+
C("PE", "51", [9], [3, 3, 3], "912345678"),
|
|
390
|
+
C("CN", "86", [11], [3, 4, 4], "13123456789"),
|
|
391
|
+
C("JP", "81", [10], [2, 4, 4], "9012345678"),
|
|
392
|
+
C("KR", "82", [9, 10], [2, 4, 4], "1023456789"),
|
|
393
|
+
C("SG", "65", [8], [4, 4], "81234567"),
|
|
394
|
+
C("HK", "852", [8], [4, 4], "51234567"),
|
|
395
|
+
C("TW", "886", [9], [3, 3, 3], "912345678"),
|
|
396
|
+
C("MY", "60", [9, 10], [2, 3, 4], "123456789"),
|
|
397
|
+
C("ID", "62", [10, 11], [3, 4, 4], "81234567890"),
|
|
398
|
+
C("TH", "66", [9], [2, 3, 4], "812345678"),
|
|
399
|
+
C("PH", "63", [10], [3, 3, 4], "9171234567"),
|
|
400
|
+
C("VN", "84", [9], [3, 3, 3], "912345678"),
|
|
401
|
+
C("PK", "92", [10], [3, 3, 4], "3012345678"),
|
|
402
|
+
C("BD", "880", [10], [2, 4, 4], "1812345678"),
|
|
403
|
+
C("NZ", "64", [9, 10], [2, 3, 4], "211234567"),
|
|
404
|
+
C("SK", "421", [9], [3, 3, 3], "912123456"),
|
|
405
|
+
C("BG", "359", [9], [2, 3, 4], "481234567"),
|
|
406
|
+
C("HR", "385", [8, 9], [2, 3, 4], "921234567"),
|
|
407
|
+
C("RS", "381", [8, 9], [2, 3, 4], "601234567"),
|
|
408
|
+
C("SI", "386", [8], [2, 3, 3], "31234567"),
|
|
409
|
+
C("LT", "370", [8], [3, 2, 3], "61234567"),
|
|
410
|
+
C("LV", "371", [8], [2, 3, 3], "21234567"),
|
|
411
|
+
C("EE", "372", [7, 8], [3, 4], "51234567"),
|
|
412
|
+
C("IS", "354", [7], [3, 4], "6111234"),
|
|
413
|
+
C("LU", "352", [9], [3, 3, 3], "628123456"),
|
|
414
|
+
C("MA", "212", [9], [3, 3, 3], "650123456"),
|
|
415
|
+
C("CR", "506", [8], [4, 4], "83123456"),
|
|
416
|
+
C("EC", "593", [9], [2, 3, 4], "991234567"),
|
|
417
|
+
C("UY", "598", [8], [1, 3, 4], "94231234"),
|
|
418
|
+
C("QA", "974", [8], [4, 4], "33123456"),
|
|
419
|
+
C("KW", "965", [8], [4, 4], "50012345")
|
|
420
|
+
];
|
|
421
|
+
var PACKED_DIAL_CODES = "AD376 AF93 AG1 AI1 AL355 AM374 AO244 AS1 AW297 AX358 AZ994 BA387 BB1 BF226 BH973 BI257 BJ229 BL590 BM1 BN673 BO591 BQ599 BS1 BT975 BV47 BW267 BY375 BZ501 CC61 CD243 CF236 CG242 CI225 CK682 CM237 CU53 CV238 CW599 CX61 CY357 DJ253 DM1 DO1 DZ213 EH212 ER291 ET251 FJ679 FK500 FM691 FO298 GA241 GD1 GE995 GF594 GG44 GH233 GI350 GL299 GM220 GN224 GP590 GQ240 GS500 GT502 GU1 GW245 GY592 HN504 HT509 IM44 IO246 IQ964 IR98 JE44 JM1 JO962 KG996 KH855 KI686 KM269 KN1 KP850 KY1 KZ7 LA856 LB961 LC1 LI423 LK94 LR231 LS266 LY218 MC377 MD373 ME382 MF590 MG261 MH692 MK389 ML223 MM95 MN976 MO853 MP1 MQ596 MR222 MS1 MT356 MU230 MV960 MW265 MZ258 NA264 NC687 NE227 NF672 NI505 NP977 NR674 NU683 OM968 PA507 PF689 PG675 PM508 PN64 PR1 PS970 PW680 PY595 RE262 RW250 SB677 SC248 SD249 SH290 SJ47 SL232 SM378 SN221 SO252 SR597 SS211 ST239 SV503 SX1 SY963 SZ268 TC1 TD235 TF262 TG228 TJ992 TK690 TL670 TM993 TN216 TO676 TT1 TV688 TZ255 UG256 UZ998 VA379 VC1 VE58 VG1 VI1 VU678 WF681 WS685 XK383 YE967 YT262 ZM260 ZW263";
|
|
422
|
+
function parsePackedCountries(packed) {
|
|
423
|
+
const out = [];
|
|
424
|
+
for (const token of packed.trim().split(/\s+/)) {
|
|
425
|
+
const iso2 = token.slice(0, 2);
|
|
426
|
+
const dialCode = token.slice(2);
|
|
427
|
+
out.push({ iso2, name: regionName(iso2), dialCode, lengths: [], groups: [], example: "" });
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
185
431
|
var PHONE_COUNTRIES = [
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
C("GB", "United Kingdom", "44", [10], [4, 6], "7400123456"),
|
|
189
|
-
C("AU", "Australia", "61", [9], [3, 3, 3], "412345678"),
|
|
190
|
-
C("DE", "Germany", "49", [10, 11], [4, 7], "15123456789"),
|
|
191
|
-
C("FR", "France", "33", [9], [1, 2, 2, 2, 2], "612345678"),
|
|
192
|
-
C("IN", "India", "91", [10], [5, 5], "8123456789"),
|
|
193
|
-
C("DK", "Denmark", "45", [8], [2, 2, 2, 2], "32123456"),
|
|
194
|
-
C("SE", "Sweden", "46", [9], [2, 3, 4], "701234567"),
|
|
195
|
-
C("NO", "Norway", "47", [8], [3, 2, 3], "40612345"),
|
|
196
|
-
C("FI", "Finland", "358", [9, 10], [2, 3, 4], "412345678"),
|
|
197
|
-
C("NL", "Netherlands", "31", [9], [1, 4, 4], "612345678"),
|
|
198
|
-
C("BE", "Belgium", "32", [9], [3, 2, 2, 2], "470123456"),
|
|
199
|
-
C("ES", "Spain", "34", [9], [3, 3, 3], "612345678"),
|
|
200
|
-
C("IT", "Italy", "39", [9, 10], [3, 3, 4], "3123456789"),
|
|
201
|
-
C("PT", "Portugal", "351", [9], [3, 3, 3], "912345678"),
|
|
202
|
-
C("IE", "Ireland", "353", [9], [2, 3, 4], "851234567"),
|
|
203
|
-
C("CH", "Switzerland", "41", [9], [2, 3, 2, 2], "781234567"),
|
|
204
|
-
C("AT", "Austria", "43", [10, 11], [3, 7], "6641234567"),
|
|
205
|
-
C("PL", "Poland", "48", [9], [3, 3, 3], "512345678"),
|
|
206
|
-
C("CZ", "Czechia", "420", [9], [3, 3, 3], "601123456"),
|
|
207
|
-
C("GR", "Greece", "30", [10], [3, 3, 4], "6912345678"),
|
|
208
|
-
C("RO", "Romania", "40", [9], [3, 3, 3], "712345678"),
|
|
209
|
-
C("HU", "Hungary", "36", [9], [2, 3, 4], "201234567"),
|
|
210
|
-
C("UA", "Ukraine", "380", [9], [2, 3, 2, 2], "501234567"),
|
|
211
|
-
C("RU", "Russia", "7", [10], [3, 3, 2, 2], "9123456789"),
|
|
212
|
-
C("TR", "Turkey", "90", [10], [3, 3, 2, 2], "5012345678"),
|
|
213
|
-
C("IL", "Israel", "972", [9], [2, 3, 4], "501234567"),
|
|
214
|
-
C("AE", "United Arab Emirates", "971", [9], [2, 3, 4], "501234567"),
|
|
215
|
-
C("SA", "Saudi Arabia", "966", [9], [2, 3, 4], "512345678"),
|
|
216
|
-
C("ZA", "South Africa", "27", [9], [2, 3, 4], "711234567"),
|
|
217
|
-
C("NG", "Nigeria", "234", [10], [3, 3, 4], "8021234567"),
|
|
218
|
-
C("KE", "Kenya", "254", [9], [3, 3, 3], "712123456"),
|
|
219
|
-
C("EG", "Egypt", "20", [10], [3, 3, 4], "1001234567"),
|
|
220
|
-
C("BR", "Brazil", "55", [10, 11], [2, 5, 4], "11961234567"),
|
|
221
|
-
C("MX", "Mexico", "52", [10], [3, 3, 4], "2221234567"),
|
|
222
|
-
C("AR", "Argentina", "54", [10], [4, 6], "1123456789"),
|
|
223
|
-
C("CL", "Chile", "56", [9], [1, 4, 4], "221234567"),
|
|
224
|
-
C("CO", "Colombia", "57", [10], [3, 3, 4], "3211234567"),
|
|
225
|
-
C("PE", "Peru", "51", [9], [3, 3, 3], "912345678"),
|
|
226
|
-
C("CN", "China", "86", [11], [3, 4, 4], "13123456789"),
|
|
227
|
-
C("JP", "Japan", "81", [10], [2, 4, 4], "9012345678"),
|
|
228
|
-
C("KR", "South Korea", "82", [9, 10], [2, 4, 4], "1023456789"),
|
|
229
|
-
C("SG", "Singapore", "65", [8], [4, 4], "81234567"),
|
|
230
|
-
C("HK", "Hong Kong", "852", [8], [4, 4], "51234567"),
|
|
231
|
-
C("TW", "Taiwan", "886", [9], [3, 3, 3], "912345678"),
|
|
232
|
-
C("MY", "Malaysia", "60", [9, 10], [2, 3, 4], "123456789"),
|
|
233
|
-
C("ID", "Indonesia", "62", [10, 11], [3, 4, 4], "81234567890"),
|
|
234
|
-
C("TH", "Thailand", "66", [9], [2, 3, 4], "812345678"),
|
|
235
|
-
C("PH", "Philippines", "63", [10], [3, 3, 4], "9171234567"),
|
|
236
|
-
C("VN", "Vietnam", "84", [9], [3, 3, 3], "912345678"),
|
|
237
|
-
C("PK", "Pakistan", "92", [10], [3, 3, 4], "3012345678"),
|
|
238
|
-
C("BD", "Bangladesh", "880", [10], [2, 4, 4], "1812345678"),
|
|
239
|
-
C("NZ", "New Zealand", "64", [9, 10], [2, 3, 4], "211234567"),
|
|
240
|
-
C("SK", "Slovakia", "421", [9], [3, 3, 3], "912123456"),
|
|
241
|
-
C("BG", "Bulgaria", "359", [9], [2, 3, 4], "481234567"),
|
|
242
|
-
C("HR", "Croatia", "385", [8, 9], [2, 3, 4], "921234567"),
|
|
243
|
-
C("RS", "Serbia", "381", [8, 9], [2, 3, 4], "601234567"),
|
|
244
|
-
C("SI", "Slovenia", "386", [8], [2, 3, 3], "31234567"),
|
|
245
|
-
C("LT", "Lithuania", "370", [8], [3, 2, 3], "61234567"),
|
|
246
|
-
C("LV", "Latvia", "371", [8], [2, 3, 3], "21234567"),
|
|
247
|
-
C("EE", "Estonia", "372", [7, 8], [3, 4], "51234567"),
|
|
248
|
-
C("IS", "Iceland", "354", [7], [3, 4], "6111234"),
|
|
249
|
-
C("LU", "Luxembourg", "352", [9], [3, 3, 3], "628123456"),
|
|
250
|
-
C("MA", "Morocco", "212", [9], [3, 3, 3], "650123456"),
|
|
251
|
-
C("CR", "Costa Rica", "506", [8], [4, 4], "83123456"),
|
|
252
|
-
C("EC", "Ecuador", "593", [9], [2, 3, 4], "991234567"),
|
|
253
|
-
C("UY", "Uruguay", "598", [8], [1, 3, 4], "94231234"),
|
|
254
|
-
C("QA", "Qatar", "974", [8], [4, 4], "33123456"),
|
|
255
|
-
C("KW", "Kuwait", "965", [8], [4, 4], "50012345")
|
|
432
|
+
...CURATED_COUNTRIES,
|
|
433
|
+
...parsePackedCountries(PACKED_DIAL_CODES)
|
|
256
434
|
];
|
|
435
|
+
var PHONE_PICKER_COUNTRIES = sortForPicker(PHONE_COUNTRIES);
|
|
436
|
+
function sortForPicker(countries) {
|
|
437
|
+
if (typeof Intl === "undefined" || typeof Intl.Collator === "undefined") {
|
|
438
|
+
return [...countries].sort((a, b) => a.iso2 < b.iso2 ? -1 : a.iso2 > b.iso2 ? 1 : 0);
|
|
439
|
+
}
|
|
440
|
+
const collator = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
441
|
+
return [...countries].sort((a, b) => collator.compare(a.name, b.name));
|
|
442
|
+
}
|
|
257
443
|
var byIso = new Map(PHONE_COUNTRIES.map((c) => [c.iso2, c]));
|
|
258
444
|
var timeZoneIso = {
|
|
259
445
|
"Africa/Cairo": "EG",
|
|
@@ -431,19 +617,24 @@ function toE164(country, national) {
|
|
|
431
617
|
const digits = digitsOnly(national);
|
|
432
618
|
return digits ? `+${country.dialCode}${digits}` : "";
|
|
433
619
|
}
|
|
434
|
-
function parsePhone(value, fallback) {
|
|
620
|
+
function parsePhone(value, fallback, opts) {
|
|
435
621
|
const trimmed = (value ?? "").trim();
|
|
436
622
|
const hasPlus = trimmed.startsWith("+");
|
|
437
623
|
const digits = digitsOnly(trimmed);
|
|
438
|
-
if (!digits) return { country: fallback, national: "", e164: "" };
|
|
439
624
|
if (hasPlus) {
|
|
625
|
+
if (!digits) return { national: "", e164: "", pending: true, raw: trimmed };
|
|
626
|
+
const previous = opts?.previousDigits ? digitsOnly(opts.previousDigits) : "";
|
|
627
|
+
if (previous && digits === previous) {
|
|
628
|
+
return { national: digits, e164: `+${digits}`, pending: true, raw: trimmed };
|
|
629
|
+
}
|
|
440
630
|
const country = countryByDialCode(digits);
|
|
441
631
|
if (country) {
|
|
442
632
|
const national = digits.slice(country.dialCode.length);
|
|
443
633
|
return { country, national, e164: `+${digits}` };
|
|
444
634
|
}
|
|
445
|
-
return { national: digits, e164: `+${digits}
|
|
635
|
+
return { national: digits, e164: `+${digits}`, pending: true, raw: trimmed };
|
|
446
636
|
}
|
|
637
|
+
if (!digits) return { country: fallback, national: "", e164: "" };
|
|
447
638
|
if (fallback) {
|
|
448
639
|
const national = digits.startsWith(fallback.dialCode) ? digits.slice(fallback.dialCode.length) : digits;
|
|
449
640
|
return { country: fallback, national, e164: `+${fallback.dialCode}${national}` };
|
|
@@ -501,6 +692,9 @@ var DEFAULT_STRINGS = {
|
|
|
501
692
|
successMessage: "Your response has been recorded.",
|
|
502
693
|
closed: "This form is no longer accepting responses.",
|
|
503
694
|
notLive: "This form isn't live yet.",
|
|
695
|
+
notOpenTitle: "This form isn't open yet",
|
|
696
|
+
notOpenBody: "The form owner is still setting things up. Please check back soon.",
|
|
697
|
+
closedTitle: "Responses are closed",
|
|
504
698
|
submitFailed: "This form can't submit right now. Please try again in a moment.",
|
|
505
699
|
loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
|
|
506
700
|
loadFailedNetwork: "Couldn't reach the server \u2014 check your connection or CORS.",
|
|
@@ -517,13 +711,22 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
517
711
|
alreadyAnswered: "You've already answered this form.",
|
|
518
712
|
uploadRetry: "Retry",
|
|
519
713
|
uploadsDisabled: "Uploads are disabled in preview",
|
|
714
|
+
uploadsUnavailable: "Uploads are temporarily unavailable",
|
|
715
|
+
uploadUnavailable: "We couldn't upload this file right now. Try again in a moment.",
|
|
520
716
|
uploadFailed: "Upload failed \u2014 try again",
|
|
521
717
|
dropzoneTitle: (multiple) => `Drop ${multiple ? "files" : "a file"} here or click to browse`,
|
|
522
718
|
dropzoneHint: (maxMb) => `Up to ${maxMb} MB per file`,
|
|
523
719
|
fileTooLarge: (maxMb) => `Larger than ${maxMb} MB limit`,
|
|
524
720
|
filesUploading: (count) => `Uploading ${count} ${count === 1 ? "file" : "files"}, please wait\u2026`,
|
|
525
721
|
uploadsFailed: (count) => `${count} ${count === 1 ? "upload" : "uploads"} failed.`,
|
|
526
|
-
filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded
|
|
722
|
+
filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded.`,
|
|
723
|
+
submittingAnnouncement: "Submitting your response\u2026",
|
|
724
|
+
errorSummaryTitle: "Please fix the following:",
|
|
725
|
+
rankingPosition: (label, position, count) => `${label}, position ${position} of ${count}`,
|
|
726
|
+
phoneCountrySelected: (name) => `${name} selected`,
|
|
727
|
+
phoneResultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
|
|
728
|
+
signatureEmpty: "No signature yet",
|
|
729
|
+
signatureSigned: "Signature saved"
|
|
527
730
|
};
|
|
528
731
|
function resolveStrings(overrides) {
|
|
529
732
|
return overrides ? { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS, ...overrides } : { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS };
|
|
@@ -537,16 +740,9 @@ var fileValueSchema = z.object({
|
|
|
537
740
|
mime: z.string(),
|
|
538
741
|
url: z.optional(z.string())
|
|
539
742
|
});
|
|
540
|
-
function isEmpty(value) {
|
|
541
|
-
if (value === null || value === void 0) return true;
|
|
542
|
-
if (typeof value === "string") return value.trim() === "";
|
|
543
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
544
|
-
if (typeof value === "boolean") return value === false;
|
|
545
|
-
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
546
|
-
return false;
|
|
547
|
-
}
|
|
548
743
|
function validateField(field2, value) {
|
|
549
744
|
if (isEmpty(value)) {
|
|
745
|
+
if (field2.kind === "calculated") return null;
|
|
550
746
|
return field2.required ? REQUIRED_FIELD_MESSAGE : null;
|
|
551
747
|
}
|
|
552
748
|
switch (field2.kind) {
|
|
@@ -663,6 +859,9 @@ function validateField(field2, value) {
|
|
|
663
859
|
case "hidden": {
|
|
664
860
|
return typeof value === "string" ? null : "Invalid value";
|
|
665
861
|
}
|
|
862
|
+
case "calculated": {
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
666
865
|
case "custom": {
|
|
667
866
|
return null;
|
|
668
867
|
}
|
|
@@ -690,6 +889,7 @@ function validateResponse(form, data) {
|
|
|
690
889
|
const errors = {};
|
|
691
890
|
const cleaned = {};
|
|
692
891
|
for (const field2 of fields) {
|
|
892
|
+
if (field2.kind === "calculated") continue;
|
|
693
893
|
const value = normalizeValue(field2, data[field2.id]);
|
|
694
894
|
const error = validateField(field2, value);
|
|
695
895
|
if (error) {
|
|
@@ -700,20 +900,6 @@ function validateResponse(form, data) {
|
|
|
700
900
|
}
|
|
701
901
|
return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
|
|
702
902
|
}
|
|
703
|
-
function normalizeValue(field2, value) {
|
|
704
|
-
if (value === void 0 || value === null) return value;
|
|
705
|
-
if (field2.kind === "custom") return value;
|
|
706
|
-
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
707
|
-
const n = Number(value);
|
|
708
|
-
return Number.isNaN(n) ? value : n;
|
|
709
|
-
}
|
|
710
|
-
if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
|
|
711
|
-
const n = Number(value);
|
|
712
|
-
return Number.isNaN(n) ? value : n;
|
|
713
|
-
}
|
|
714
|
-
if (typeof value === "string") return value.trim();
|
|
715
|
-
return value;
|
|
716
|
-
}
|
|
717
903
|
|
|
718
904
|
// src/schema-validation.ts
|
|
719
905
|
import * as z2 from "zod/mini";
|
|
@@ -736,6 +922,7 @@ var FIELD_KINDS = [
|
|
|
736
922
|
"date",
|
|
737
923
|
"file_upload",
|
|
738
924
|
"hidden",
|
|
925
|
+
"calculated",
|
|
739
926
|
"custom"
|
|
740
927
|
];
|
|
741
928
|
var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
|
|
@@ -772,9 +959,10 @@ var schemaShape = z2.object({
|
|
|
772
959
|
});
|
|
773
960
|
var MAX_SCHEMA_VERSION = 1;
|
|
774
961
|
var FILLO_SCHEMA_VERSION = 1;
|
|
775
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
962
|
+
var FILLO_SDK_VERSION = true ? "0.11.0" : "0.0.0-dev";
|
|
776
963
|
var FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
777
964
|
var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
965
|
+
var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
778
966
|
function str(value, max, fallback = "") {
|
|
779
967
|
return typeof value === "string" ? value.trim().slice(0, max) : fallback;
|
|
780
968
|
}
|
|
@@ -782,6 +970,11 @@ function optionalStr(value, max) {
|
|
|
782
970
|
const s = str(value, max);
|
|
783
971
|
return s === "" ? void 0 : s;
|
|
784
972
|
}
|
|
973
|
+
function optionalAffix(value, max) {
|
|
974
|
+
if (typeof value !== "string") return void 0;
|
|
975
|
+
const s = value.slice(0, max);
|
|
976
|
+
return s.trim() === "" ? void 0 : s;
|
|
977
|
+
}
|
|
785
978
|
function bool(value) {
|
|
786
979
|
return typeof value === "boolean" ? value : void 0;
|
|
787
980
|
}
|
|
@@ -829,7 +1022,17 @@ function conditions(value) {
|
|
|
829
1022
|
});
|
|
830
1023
|
return normalized.length ? normalized : void 0;
|
|
831
1024
|
}
|
|
832
|
-
function
|
|
1025
|
+
function jumpDepsAllowed(fieldId, calcRefs, allowed, seen) {
|
|
1026
|
+
const refs = calcRefs.get(fieldId);
|
|
1027
|
+
if (refs === void 0) return allowed.has(fieldId);
|
|
1028
|
+
if (seen.has(fieldId)) return true;
|
|
1029
|
+
seen.add(fieldId);
|
|
1030
|
+
for (const ref of refs) {
|
|
1031
|
+
if (!jumpDepsAllowed(ref, calcRefs, allowed, seen)) return false;
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
function jumpRules(value, pageIds, sourceId, allowedFieldIds, calcRefs) {
|
|
833
1036
|
if (!Array.isArray(value)) return void 0;
|
|
834
1037
|
const rules = value.slice(0, 50).flatMap((item) => {
|
|
835
1038
|
if (!item || typeof item !== "object") return [];
|
|
@@ -841,11 +1044,187 @@ function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
|
|
|
841
1044
|
if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
|
|
842
1045
|
const when2 = conditions(rawWhen);
|
|
843
1046
|
if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
|
|
844
|
-
if (when2 && when2.some((c) => !
|
|
1047
|
+
if (when2 && when2.some((c) => !jumpDepsAllowed(c.fieldId, calcRefs, allowedFieldIds, /* @__PURE__ */ new Set())))
|
|
1048
|
+
return [];
|
|
845
1049
|
return [{ when: when2 ?? [], to }];
|
|
846
1050
|
});
|
|
847
1051
|
return rules.length ? rules : void 0;
|
|
848
1052
|
}
|
|
1053
|
+
var CALC_OPERAND_KINDS = ["number", "rating", "linear_scale", "calculated"];
|
|
1054
|
+
function normalizeCalc(value, depth = 0) {
|
|
1055
|
+
if (depth > 32) return null;
|
|
1056
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1057
|
+
const rec = value;
|
|
1058
|
+
switch (rec.op) {
|
|
1059
|
+
case "value": {
|
|
1060
|
+
const fieldId = str(rec.fieldId, 128);
|
|
1061
|
+
return fieldId ? { op: "value", fieldId } : null;
|
|
1062
|
+
}
|
|
1063
|
+
case "const": {
|
|
1064
|
+
const n = finite(rec.value);
|
|
1065
|
+
return n === void 0 ? null : { op: "const", value: n };
|
|
1066
|
+
}
|
|
1067
|
+
case "add":
|
|
1068
|
+
case "mul":
|
|
1069
|
+
case "min":
|
|
1070
|
+
case "max": {
|
|
1071
|
+
if (!Array.isArray(rec.args) || rec.args.length < 1 || rec.args.length > 50) return null;
|
|
1072
|
+
const args = [];
|
|
1073
|
+
for (const raw of rec.args) {
|
|
1074
|
+
const arg = normalizeCalc(raw, depth + 1);
|
|
1075
|
+
if (!arg) return null;
|
|
1076
|
+
args.push(arg);
|
|
1077
|
+
}
|
|
1078
|
+
return { op: rec.op, args };
|
|
1079
|
+
}
|
|
1080
|
+
case "sub":
|
|
1081
|
+
case "div": {
|
|
1082
|
+
const left = normalizeCalc(rec.left, depth + 1);
|
|
1083
|
+
const right = normalizeCalc(rec.right, depth + 1);
|
|
1084
|
+
return left && right ? { op: rec.op, left, right } : null;
|
|
1085
|
+
}
|
|
1086
|
+
case "round": {
|
|
1087
|
+
const arg = normalizeCalc(rec.arg, depth + 1);
|
|
1088
|
+
if (!arg) return null;
|
|
1089
|
+
return cleanObject({
|
|
1090
|
+
op: "round",
|
|
1091
|
+
arg,
|
|
1092
|
+
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0)
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
case "if": {
|
|
1096
|
+
if (!Array.isArray(rec.when)) return null;
|
|
1097
|
+
const when2 = conditions(rec.when) ?? [];
|
|
1098
|
+
if (when2.length !== rec.when.length) return null;
|
|
1099
|
+
const then = normalizeCalc(rec.then, depth + 1);
|
|
1100
|
+
const els = normalizeCalc(rec.else, depth + 1);
|
|
1101
|
+
return then && els ? { op: "if", when: when2, then, else: els } : null;
|
|
1102
|
+
}
|
|
1103
|
+
default:
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function collectCalcRefs(expr, valueRefs, whenRefs) {
|
|
1108
|
+
switch (expr.op) {
|
|
1109
|
+
case "value":
|
|
1110
|
+
valueRefs.add(expr.fieldId);
|
|
1111
|
+
return;
|
|
1112
|
+
case "const":
|
|
1113
|
+
return;
|
|
1114
|
+
case "add":
|
|
1115
|
+
case "mul":
|
|
1116
|
+
case "min":
|
|
1117
|
+
case "max":
|
|
1118
|
+
for (const arg of expr.args) collectCalcRefs(arg, valueRefs, whenRefs);
|
|
1119
|
+
return;
|
|
1120
|
+
case "sub":
|
|
1121
|
+
case "div":
|
|
1122
|
+
collectCalcRefs(expr.left, valueRefs, whenRefs);
|
|
1123
|
+
collectCalcRefs(expr.right, valueRefs, whenRefs);
|
|
1124
|
+
return;
|
|
1125
|
+
case "round":
|
|
1126
|
+
collectCalcRefs(expr.arg, valueRefs, whenRefs);
|
|
1127
|
+
return;
|
|
1128
|
+
case "if":
|
|
1129
|
+
for (const cond of expr.when) whenRefs.add(cond.fieldId);
|
|
1130
|
+
collectCalcRefs(expr.then, valueRefs, whenRefs);
|
|
1131
|
+
collectCalcRefs(expr.else, valueRefs, whenRefs);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
function validateCalculatedFields(pages) {
|
|
1136
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
1137
|
+
for (const page of pages) {
|
|
1138
|
+
for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
|
|
1139
|
+
}
|
|
1140
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1141
|
+
for (const page of pages) {
|
|
1142
|
+
for (const block of page.blocks) {
|
|
1143
|
+
if (block.kind !== "calculated") continue;
|
|
1144
|
+
const calc = normalizeCalc(block.calc);
|
|
1145
|
+
if (!calc) {
|
|
1146
|
+
return `Field ${block.id} has a malformed calc expression \u2014 build it from value/const/add/sub/mul/div/min/max/round/if nodes`;
|
|
1147
|
+
}
|
|
1148
|
+
block.calc = calc;
|
|
1149
|
+
const valueRefs = /* @__PURE__ */ new Set();
|
|
1150
|
+
const whenRefs = /* @__PURE__ */ new Set();
|
|
1151
|
+
collectCalcRefs(calc, valueRefs, whenRefs);
|
|
1152
|
+
for (const ref of valueRefs) {
|
|
1153
|
+
const kind = kinds.get(ref);
|
|
1154
|
+
if (kind === void 0) {
|
|
1155
|
+
return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
|
|
1156
|
+
}
|
|
1157
|
+
if (!CALC_OPERAND_KINDS.includes(kind)) {
|
|
1158
|
+
return `Calculated field ${block.id} references non-numeric field ${ref} (${kind}) \u2014 calculations can read number, rating, linear_scale, and calculated fields only`;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
deps.set(
|
|
1162
|
+
block.id,
|
|
1163
|
+
[...valueRefs, ...whenRefs].filter((ref) => kinds.get(ref) === "calculated")
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const state = /* @__PURE__ */ new Map();
|
|
1168
|
+
const visit = (id, path) => {
|
|
1169
|
+
const mark = state.get(id);
|
|
1170
|
+
if (mark === "done") return null;
|
|
1171
|
+
if (mark === "visiting") {
|
|
1172
|
+
const chain = [...path.slice(path.indexOf(id)), id].join(" \u2192 ");
|
|
1173
|
+
return `Calculated field ${id} depends on its own result (${chain}) \u2014 break the cycle so the value can be computed`;
|
|
1174
|
+
}
|
|
1175
|
+
state.set(id, "visiting");
|
|
1176
|
+
for (const dep of deps.get(id) ?? []) {
|
|
1177
|
+
const error = visit(dep, [...path, id]);
|
|
1178
|
+
if (error) return error;
|
|
1179
|
+
}
|
|
1180
|
+
state.set(id, "done");
|
|
1181
|
+
return null;
|
|
1182
|
+
};
|
|
1183
|
+
for (const id of deps.keys()) {
|
|
1184
|
+
const error = visit(id, []);
|
|
1185
|
+
if (error) return error;
|
|
1186
|
+
}
|
|
1187
|
+
return null;
|
|
1188
|
+
}
|
|
1189
|
+
var MAX_CALC_FIELDS = 100;
|
|
1190
|
+
var MAX_CALC_TOTAL_NODES = 5e3;
|
|
1191
|
+
function countCalcNodes(expr) {
|
|
1192
|
+
switch (expr.op) {
|
|
1193
|
+
case "value":
|
|
1194
|
+
case "const":
|
|
1195
|
+
return 1;
|
|
1196
|
+
case "add":
|
|
1197
|
+
case "mul":
|
|
1198
|
+
case "min":
|
|
1199
|
+
case "max":
|
|
1200
|
+
return 1 + expr.args.reduce((n, arg) => n + countCalcNodes(arg), 0);
|
|
1201
|
+
case "sub":
|
|
1202
|
+
case "div":
|
|
1203
|
+
return 1 + countCalcNodes(expr.left) + countCalcNodes(expr.right);
|
|
1204
|
+
case "round":
|
|
1205
|
+
return 1 + countCalcNodes(expr.arg);
|
|
1206
|
+
case "if":
|
|
1207
|
+
return 1 + countCalcNodes(expr.then) + countCalcNodes(expr.else);
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function calcComplexityError(pages) {
|
|
1211
|
+
let calcFields = 0;
|
|
1212
|
+
let totalNodes = 0;
|
|
1213
|
+
for (const page of pages) {
|
|
1214
|
+
for (const block of page.blocks) {
|
|
1215
|
+
if (block.kind !== "calculated") continue;
|
|
1216
|
+
calcFields += 1;
|
|
1217
|
+
totalNodes += countCalcNodes(block.calc);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
if (calcFields > MAX_CALC_FIELDS) {
|
|
1221
|
+
return `Form has ${calcFields} calculated fields \u2014 the maximum is ${MAX_CALC_FIELDS}; remove some or consolidate them into fewer expressions`;
|
|
1222
|
+
}
|
|
1223
|
+
if (totalNodes > MAX_CALC_TOTAL_NODES) {
|
|
1224
|
+
return `Calculated expressions total ${totalNodes} operations \u2014 the maximum is ${MAX_CALC_TOTAL_NODES} across the form; simplify or split up the calculations`;
|
|
1225
|
+
}
|
|
1226
|
+
return null;
|
|
1227
|
+
}
|
|
849
1228
|
function baseBlock(rec) {
|
|
850
1229
|
return {
|
|
851
1230
|
id: str(rec.id, 128),
|
|
@@ -957,7 +1336,20 @@ function normalizeBlock(input) {
|
|
|
957
1336
|
maxLength: finite(rec.maxLength) === void 0 ? void 0 : int(rec.maxLength, 1, 2e4, 2e4)
|
|
958
1337
|
});
|
|
959
1338
|
case "number":
|
|
960
|
-
return cleanObject({
|
|
1339
|
+
return cleanObject({
|
|
1340
|
+
...baseField(rec, kind),
|
|
1341
|
+
min: finite(rec.min),
|
|
1342
|
+
max: finite(rec.max),
|
|
1343
|
+
// Display-only (decision 1 in the number-formatting contract) —
|
|
1344
|
+
// mirrors the calculated field's decimals/prefix/suffix carry-through
|
|
1345
|
+
// exactly, plus notation which calculated doesn't have.
|
|
1346
|
+
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
|
|
1347
|
+
prefix: optionalAffix(rec.prefix, 100),
|
|
1348
|
+
suffix: optionalAffix(rec.suffix, 100),
|
|
1349
|
+
// The only supported value; anything else drops silently rather than
|
|
1350
|
+
// erroring (an old/garbage value is just "no grouping").
|
|
1351
|
+
notation: rec.notation === "grouped" ? "grouped" : void 0
|
|
1352
|
+
});
|
|
961
1353
|
case "select":
|
|
962
1354
|
case "multi_select":
|
|
963
1355
|
case "dropdown": {
|
|
@@ -1021,6 +1413,21 @@ function normalizeBlock(input) {
|
|
|
1021
1413
|
paramName: optionalStr(rec.paramName, 128),
|
|
1022
1414
|
defaultValue: optionalStr(rec.defaultValue, 1e3)
|
|
1023
1415
|
});
|
|
1416
|
+
case "calculated":
|
|
1417
|
+
return cleanObject({
|
|
1418
|
+
...baseField(rec, kind),
|
|
1419
|
+
// A derived value isn't answerable, so it can't be required (the
|
|
1420
|
+
// hidden-field precedent) — a required one could never be satisfied.
|
|
1421
|
+
required: false,
|
|
1422
|
+
// Kept raw here on purpose: the calc AST is normalized and structurally
|
|
1423
|
+
// validated in normalizeFormSchema once the full field universe is
|
|
1424
|
+
// known (missing refs, operand kinds, cycles are HARD errors there — a
|
|
1425
|
+
// calculated field must never be silently dropped or degraded).
|
|
1426
|
+
calc: rec.calc,
|
|
1427
|
+
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
|
|
1428
|
+
prefix: optionalAffix(rec.prefix, 100),
|
|
1429
|
+
suffix: optionalAffix(rec.suffix, 100)
|
|
1430
|
+
});
|
|
1024
1431
|
case "file_upload":
|
|
1025
1432
|
return cleanObject({
|
|
1026
1433
|
...baseField(rec, kind),
|
|
@@ -1059,6 +1466,22 @@ function normalizeFormSchema(input) {
|
|
|
1059
1466
|
return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
|
|
1060
1467
|
});
|
|
1061
1468
|
const targetIds = new Set(rebuilt.map((p) => p.id));
|
|
1469
|
+
const calcRefs = /* @__PURE__ */ new Map();
|
|
1470
|
+
for (const page of rebuilt) {
|
|
1471
|
+
for (const block of page.blocks) {
|
|
1472
|
+
if (block.kind !== "calculated") continue;
|
|
1473
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1474
|
+
const calc = normalizeCalc(block.calc);
|
|
1475
|
+
if (calc) {
|
|
1476
|
+
const valueRefs = /* @__PURE__ */ new Set();
|
|
1477
|
+
const whenRefs = /* @__PURE__ */ new Set();
|
|
1478
|
+
collectCalcRefs(calc, valueRefs, whenRefs);
|
|
1479
|
+
for (const ref of valueRefs) refs.add(ref);
|
|
1480
|
+
for (const ref of whenRefs) refs.add(ref);
|
|
1481
|
+
}
|
|
1482
|
+
calcRefs.set(block.id, refs);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1062
1485
|
const priorFieldIds = /* @__PURE__ */ new Set();
|
|
1063
1486
|
const pages = rebuilt.map((page) => {
|
|
1064
1487
|
for (const block of page.blocks) {
|
|
@@ -1068,7 +1491,7 @@ function normalizeFormSchema(input) {
|
|
|
1068
1491
|
id: page.id,
|
|
1069
1492
|
title: page.title,
|
|
1070
1493
|
blocks: page.blocks,
|
|
1071
|
-
next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
|
|
1494
|
+
next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds), calcRefs)
|
|
1072
1495
|
});
|
|
1073
1496
|
});
|
|
1074
1497
|
const pageIds = /* @__PURE__ */ new Set();
|
|
@@ -1103,6 +1526,10 @@ function normalizeFormSchema(input) {
|
|
|
1103
1526
|
}
|
|
1104
1527
|
}
|
|
1105
1528
|
}
|
|
1529
|
+
const calcError = validateCalculatedFields(pages);
|
|
1530
|
+
if (calcError) return { ok: false, error: calcError };
|
|
1531
|
+
const calcComplexity = calcComplexityError(pages);
|
|
1532
|
+
if (calcComplexity) return { ok: false, error: calcComplexity };
|
|
1106
1533
|
if (pages.every((page) => page.blocks.length === 0)) {
|
|
1107
1534
|
return { ok: false, error: "Schema must contain at least one valid block" };
|
|
1108
1535
|
}
|
|
@@ -1175,8 +1602,19 @@ function formatAnswer(field2, value) {
|
|
|
1175
1602
|
return value === true ? "Yes" : "No";
|
|
1176
1603
|
case "rating":
|
|
1177
1604
|
case "linear_scale":
|
|
1178
|
-
case "number":
|
|
1179
1605
|
return typeof value === "number" ? String(value) : String(value);
|
|
1606
|
+
case "number": {
|
|
1607
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1608
|
+
if (!Number.isFinite(n)) return String(value);
|
|
1609
|
+
const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
|
|
1610
|
+
return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
|
|
1611
|
+
}
|
|
1612
|
+
case "calculated": {
|
|
1613
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1614
|
+
if (!Number.isFinite(n)) return String(value);
|
|
1615
|
+
const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
|
|
1616
|
+
return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
|
|
1617
|
+
}
|
|
1180
1618
|
case "phone": {
|
|
1181
1619
|
const { country, national, e164 } = parsePhone(String(value));
|
|
1182
1620
|
if (!e164) return String(value);
|
|
@@ -1198,6 +1636,92 @@ function formatAnswer(field2, value) {
|
|
|
1198
1636
|
}
|
|
1199
1637
|
}
|
|
1200
1638
|
|
|
1639
|
+
// src/number-format.ts
|
|
1640
|
+
function clampDecimals2(decimals) {
|
|
1641
|
+
return Math.max(0, Math.min(6, Math.round(decimals)));
|
|
1642
|
+
}
|
|
1643
|
+
function formatGroupedNumber(value, opts) {
|
|
1644
|
+
if (!Number.isFinite(value)) return String(value);
|
|
1645
|
+
const decimals = opts?.decimals === void 0 ? void 0 : clampDecimals2(opts.decimals);
|
|
1646
|
+
if (typeof Intl === "undefined") {
|
|
1647
|
+
return decimals === void 0 ? String(value) : value.toFixed(decimals);
|
|
1648
|
+
}
|
|
1649
|
+
const format = { useGrouping: true };
|
|
1650
|
+
if (decimals === void 0) {
|
|
1651
|
+
format.maximumFractionDigits = 20;
|
|
1652
|
+
} else {
|
|
1653
|
+
format.minimumFractionDigits = decimals;
|
|
1654
|
+
format.maximumFractionDigits = decimals;
|
|
1655
|
+
}
|
|
1656
|
+
return new Intl.NumberFormat(opts?.locale, format).format(value);
|
|
1657
|
+
}
|
|
1658
|
+
function stripAll(text, separator) {
|
|
1659
|
+
return separator ? text.split(separator).join("") : text;
|
|
1660
|
+
}
|
|
1661
|
+
function toCanonicalDecimal(text, decimal) {
|
|
1662
|
+
return decimal === "." ? text : text.split(decimal).join(".");
|
|
1663
|
+
}
|
|
1664
|
+
function resolveDotGroup(body, decimal) {
|
|
1665
|
+
if (decimal !== "." && body.includes(decimal)) {
|
|
1666
|
+
const i = body.lastIndexOf(decimal);
|
|
1667
|
+
const intPart = stripAll(body.slice(0, i), ".");
|
|
1668
|
+
const fracPart = body.slice(i + decimal.length);
|
|
1669
|
+
return `${intPart}.${fracPart}`;
|
|
1670
|
+
}
|
|
1671
|
+
const segments = body.split(".");
|
|
1672
|
+
if (segments.length === 1) return body;
|
|
1673
|
+
const isPureGrouping = segments.every(
|
|
1674
|
+
(seg, i) => i === 0 ? /^\d{1,3}$/.test(seg) : /^\d{3}$/.test(seg)
|
|
1675
|
+
);
|
|
1676
|
+
if (isPureGrouping) return segments.join("");
|
|
1677
|
+
return `${segments.slice(0, -1).join("")}.${segments.at(-1) ?? ""}`;
|
|
1678
|
+
}
|
|
1679
|
+
function parseGroupedNumber(text, locale) {
|
|
1680
|
+
if (typeof Intl === "undefined") return text;
|
|
1681
|
+
if (text.trim() === "") return text;
|
|
1682
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(12345678e-1);
|
|
1683
|
+
const group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
1684
|
+
const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
1685
|
+
let body = text.trim();
|
|
1686
|
+
const negative = body.startsWith("-");
|
|
1687
|
+
if (negative) body = body.slice(1);
|
|
1688
|
+
if (/^\s$/.test(group)) {
|
|
1689
|
+
body = toCanonicalDecimal(body.replace(/\s/g, ""), decimal);
|
|
1690
|
+
} else if (group === ".") {
|
|
1691
|
+
body = resolveDotGroup(body, decimal);
|
|
1692
|
+
} else {
|
|
1693
|
+
body = toCanonicalDecimal(stripAll(body, group), decimal);
|
|
1694
|
+
}
|
|
1695
|
+
if (body === "" || body === "." || !/^\d*\.?\d*$/.test(body)) return text;
|
|
1696
|
+
return negative ? `-${body}` : body;
|
|
1697
|
+
}
|
|
1698
|
+
function isValidPartialNumberText(text, locale) {
|
|
1699
|
+
const negative = text.startsWith("-");
|
|
1700
|
+
const body = negative ? text.slice(1) : text;
|
|
1701
|
+
if (body.includes("-")) return false;
|
|
1702
|
+
if (body === "") return true;
|
|
1703
|
+
if (typeof Intl === "undefined") {
|
|
1704
|
+
return /^[\d.,]*$/.test(body);
|
|
1705
|
+
}
|
|
1706
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(12345678e-1);
|
|
1707
|
+
const group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
1708
|
+
const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
1709
|
+
const dotIsGroup = group === ".";
|
|
1710
|
+
const groupIsWhitespace = /^\s$/.test(group);
|
|
1711
|
+
const decimalMarks = dotIsGroup ? [decimal] : Array.from(/* @__PURE__ */ new Set([".", decimal]));
|
|
1712
|
+
let decimalSeen = 0;
|
|
1713
|
+
for (const ch of body) {
|
|
1714
|
+
if (/\d/.test(ch)) continue;
|
|
1715
|
+
if (decimalMarks.includes(ch)) {
|
|
1716
|
+
if (++decimalSeen > 1) return false;
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
if (groupIsWhitespace ? /\s/.test(ch) : ch === group) continue;
|
|
1720
|
+
return false;
|
|
1721
|
+
}
|
|
1722
|
+
return true;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1201
1725
|
// src/ids.ts
|
|
1202
1726
|
var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1203
1727
|
var MASK = 256 - 256 % ALPHABET.length;
|
|
@@ -1241,6 +1765,7 @@ var BLOCK_KIND_META = {
|
|
|
1241
1765
|
date: { label: "Date", hint: "Date picker" },
|
|
1242
1766
|
file_upload: { label: "File upload", hint: "Direct to connected storage" },
|
|
1243
1767
|
hidden: { label: "Hidden field", hint: "Filled from the URL" },
|
|
1768
|
+
calculated: { label: "Calculated value", hint: "Computed from other answers" },
|
|
1244
1769
|
custom: { label: "Custom", hint: "Your own component (code only)" },
|
|
1245
1770
|
heading: { label: "Heading", hint: "Section title" },
|
|
1246
1771
|
paragraph: { label: "Paragraph", hint: "Explanatory text" },
|
|
@@ -1310,6 +1835,8 @@ function createBlock(kind) {
|
|
|
1310
1835
|
return { id, kind, label: "Sign here" };
|
|
1311
1836
|
case "hidden":
|
|
1312
1837
|
return { id, kind, label: "Hidden field" };
|
|
1838
|
+
case "calculated":
|
|
1839
|
+
return { id, kind, label: "Calculated value", calc: { op: "const", value: 0 } };
|
|
1313
1840
|
case "custom":
|
|
1314
1841
|
return { id, kind, label: "Custom field", component: "" };
|
|
1315
1842
|
case "file_upload":
|
|
@@ -2340,6 +2867,9 @@ function prefillFromParams(form, params) {
|
|
|
2340
2867
|
case "custom":
|
|
2341
2868
|
break;
|
|
2342
2869
|
// not prefillable from a URL
|
|
2870
|
+
case "calculated":
|
|
2871
|
+
break;
|
|
2872
|
+
// engine-computed, never prefillable — a crafted URL can't seed it
|
|
2343
2873
|
default:
|
|
2344
2874
|
data[field2.id] = raw;
|
|
2345
2875
|
}
|
|
@@ -2471,9 +3001,12 @@ function submitFailureMessage(err) {
|
|
|
2471
3001
|
if (isFilloError(err) && err.status && err.status > 0 && err.message) return err.message;
|
|
2472
3002
|
return "Couldn't reach the server \u2014 check your connection and try again. If this keeps happening, a browser extension, firewall, or the site's Content-Security-Policy may be blocking the request.";
|
|
2473
3003
|
}
|
|
2474
|
-
function syncResolutionFailureMessage(err) {
|
|
3004
|
+
function syncResolutionFailureMessage(err, verbose) {
|
|
2475
3005
|
if (isFilloError(err)) {
|
|
2476
3006
|
const status = err.status ?? 0;
|
|
3007
|
+
if (verbose && status > 0 && err.message) {
|
|
3008
|
+
return err.code ? `${err.message} (${err.code})` : err.message;
|
|
3009
|
+
}
|
|
2477
3010
|
const definitive = status > 0 && status < 500 && status !== 408 && status !== 429;
|
|
2478
3011
|
if (definitive) return "This form is unavailable.";
|
|
2479
3012
|
}
|
|
@@ -2487,7 +3020,7 @@ function createFormController(options2) {
|
|
|
2487
3020
|
let formId = options2.formId;
|
|
2488
3021
|
let client = options2.client;
|
|
2489
3022
|
let respondent = options2.respondent;
|
|
2490
|
-
let data = { ...options2.initialData ?? {} };
|
|
3023
|
+
let data = computeCalculated(form, { ...options2.initialData ?? {} });
|
|
2491
3024
|
let errors = {};
|
|
2492
3025
|
let pageIndex = 0;
|
|
2493
3026
|
let status = "idle";
|
|
@@ -2504,7 +3037,7 @@ function createFormController(options2) {
|
|
|
2504
3037
|
}
|
|
2505
3038
|
}
|
|
2506
3039
|
if (merged && status === "idle") {
|
|
2507
|
-
data = merged;
|
|
3040
|
+
data = computeCalculated(form, merged);
|
|
2508
3041
|
notify();
|
|
2509
3042
|
}
|
|
2510
3043
|
}
|
|
@@ -2632,7 +3165,7 @@ function createFormController(options2) {
|
|
|
2632
3165
|
const target = typeof draft.page === "number" && Number.isFinite(draft.page) ? Math.max(0, Math.min(Math.floor(draft.page), form.pages.length - 1)) : 0;
|
|
2633
3166
|
const movePage = target > 0 && pageIndex === 0;
|
|
2634
3167
|
if (!merged && !movePage) return;
|
|
2635
|
-
if (merged) data = merged;
|
|
3168
|
+
if (merged) data = computeCalculated(form, merged);
|
|
2636
3169
|
if (movePage) pageIndex = target;
|
|
2637
3170
|
resumedDraft = true;
|
|
2638
3171
|
notify();
|
|
@@ -2762,7 +3295,7 @@ function createFormController(options2) {
|
|
|
2762
3295
|
sessionStarted = true;
|
|
2763
3296
|
void client.startSession(formId, form.pages.length).then((id) => sessionId = id);
|
|
2764
3297
|
}
|
|
2765
|
-
data = { ...data, [fieldId]: value };
|
|
3298
|
+
data = computeCalculated(form, { ...data, [fieldId]: value });
|
|
2766
3299
|
onChange?.(data);
|
|
2767
3300
|
submitError = void 0;
|
|
2768
3301
|
if (fieldId in errors) {
|
|
@@ -2849,7 +3382,10 @@ function createFormController(options2) {
|
|
|
2849
3382
|
formId = await options2.resolveFormId();
|
|
2850
3383
|
} catch (err) {
|
|
2851
3384
|
status = "idle";
|
|
2852
|
-
submitError = syncResolutionFailureMessage(
|
|
3385
|
+
submitError = syncResolutionFailureMessage(
|
|
3386
|
+
err,
|
|
3387
|
+
options2.verboseResolutionErrors === true
|
|
3388
|
+
);
|
|
2853
3389
|
notify();
|
|
2854
3390
|
throw err;
|
|
2855
3391
|
}
|
|
@@ -2943,6 +3479,7 @@ function createFormController(options2) {
|
|
|
2943
3479
|
const next2 = normalizeFormSchema(ctx.form);
|
|
2944
3480
|
if (!next2.ok) throw new Error(`Invalid form schema: ${next2.error}`);
|
|
2945
3481
|
form = next2.schema;
|
|
3482
|
+
data = computeCalculated(form, data);
|
|
2946
3483
|
}
|
|
2947
3484
|
if (status === "idle" && hasSubmittedOnce(form, formId, data)) {
|
|
2948
3485
|
status = "submitted";
|
|
@@ -2967,6 +3504,7 @@ function createFormController(options2) {
|
|
|
2967
3504
|
if (data[key] === void 0) data[key] = value;
|
|
2968
3505
|
}
|
|
2969
3506
|
}
|
|
3507
|
+
data = computeCalculated(form, data);
|
|
2970
3508
|
errors = {};
|
|
2971
3509
|
pageIndex = 0;
|
|
2972
3510
|
submitError = void 0;
|
|
@@ -2989,7 +3527,9 @@ function isAutoSubmitBlock(block) {
|
|
|
2989
3527
|
return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
2990
3528
|
}
|
|
2991
3529
|
function needsExplicitSubmit(visible) {
|
|
2992
|
-
const interactive = visible.filter(
|
|
3530
|
+
const interactive = visible.filter(
|
|
3531
|
+
(b) => isField(b) && b.kind !== "hidden" && b.kind !== "calculated"
|
|
3532
|
+
);
|
|
2993
3533
|
if (interactive.length !== 1) return true;
|
|
2994
3534
|
return !isAutoSubmitBlock(interactive[0]);
|
|
2995
3535
|
}
|
|
@@ -3015,11 +3555,40 @@ function shouldAutoSubmit(field2, value, ctx) {
|
|
|
3015
3555
|
const nextData = { ...ctx.data, [field2.id]: value };
|
|
3016
3556
|
const page = ctx.form.pages.find((p) => p.blocks.some((b) => b.id === field2.id));
|
|
3017
3557
|
if (!page || !isTerminalPage(ctx.form, page.id, nextData)) return false;
|
|
3018
|
-
const interactive = reachableFields(ctx.form, nextData).filter(
|
|
3558
|
+
const interactive = reachableFields(ctx.form, nextData).filter(
|
|
3559
|
+
(f) => f.kind !== "hidden" && f.kind !== "calculated"
|
|
3560
|
+
);
|
|
3019
3561
|
if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
|
|
3020
3562
|
return validateResponse(ctx.form, nextData).ok;
|
|
3021
3563
|
}
|
|
3022
3564
|
|
|
3565
|
+
// src/radio-nav.ts
|
|
3566
|
+
function radioGroupStep(key, index, length, opts) {
|
|
3567
|
+
if (length <= 0) return null;
|
|
3568
|
+
const rtl = opts?.rtl ?? false;
|
|
3569
|
+
if (key === "Home") return 0;
|
|
3570
|
+
if (key === "End") return length - 1;
|
|
3571
|
+
let delta;
|
|
3572
|
+
switch (key) {
|
|
3573
|
+
case "ArrowRight":
|
|
3574
|
+
delta = rtl ? -1 : 1;
|
|
3575
|
+
break;
|
|
3576
|
+
case "ArrowLeft":
|
|
3577
|
+
delta = rtl ? 1 : -1;
|
|
3578
|
+
break;
|
|
3579
|
+
case "ArrowDown":
|
|
3580
|
+
delta = 1;
|
|
3581
|
+
break;
|
|
3582
|
+
case "ArrowUp":
|
|
3583
|
+
delta = -1;
|
|
3584
|
+
break;
|
|
3585
|
+
default:
|
|
3586
|
+
return null;
|
|
3587
|
+
}
|
|
3588
|
+
const next = index + delta;
|
|
3589
|
+
return Math.min(length - 1, Math.max(0, next));
|
|
3590
|
+
}
|
|
3591
|
+
|
|
3023
3592
|
// src/appearance.ts
|
|
3024
3593
|
var FILLO_SLOTS = [
|
|
3025
3594
|
"root",
|
|
@@ -3042,7 +3611,8 @@ var FILLO_SLOTS = [
|
|
|
3042
3611
|
"button",
|
|
3043
3612
|
"success",
|
|
3044
3613
|
"resume",
|
|
3045
|
-
"turnstile"
|
|
3614
|
+
"turnstile",
|
|
3615
|
+
"calculated"
|
|
3046
3616
|
];
|
|
3047
3617
|
var FILLO_DATA_ATTRS = {
|
|
3048
3618
|
/** Slot name, on every slot element: `data-fillo="control"`. */
|
|
@@ -3090,6 +3660,35 @@ var FILLO_THEME_VARS = [
|
|
|
3090
3660
|
{ var: "--fillo-error" },
|
|
3091
3661
|
{ var: "--fillo-primary-contrast" }
|
|
3092
3662
|
];
|
|
3663
|
+
var DARK_LUMINANCE_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05;
|
|
3664
|
+
var HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
|
|
3665
|
+
var HEX3 = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i;
|
|
3666
|
+
function srgbToLinear(channel255) {
|
|
3667
|
+
const c = channel255 / 255;
|
|
3668
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
3669
|
+
}
|
|
3670
|
+
function relativeLuminance(token) {
|
|
3671
|
+
const trimmed = token.trim();
|
|
3672
|
+
let hex = null;
|
|
3673
|
+
const six = HEX6.exec(trimmed);
|
|
3674
|
+
if (six) {
|
|
3675
|
+
hex = `${six[1]}${six[2]}${six[3]}`;
|
|
3676
|
+
} else {
|
|
3677
|
+
const three = HEX3.exec(trimmed);
|
|
3678
|
+
if (three) hex = `${three[1]}${three[1]}${three[2]}${three[2]}${three[3]}${three[3]}`;
|
|
3679
|
+
}
|
|
3680
|
+
if (!hex) return null;
|
|
3681
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
3682
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
3683
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
3684
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
3685
|
+
}
|
|
3686
|
+
function resolveThemeAppearance(theme) {
|
|
3687
|
+
if (!theme || theme.colorScheme || !theme.background || !theme.text) return theme;
|
|
3688
|
+
const luminance = relativeLuminance(theme.background);
|
|
3689
|
+
if (luminance == null) return theme;
|
|
3690
|
+
return { ...theme, colorScheme: luminance > DARK_LUMINANCE_THRESHOLD ? "light" : "dark" };
|
|
3691
|
+
}
|
|
3093
3692
|
|
|
3094
3693
|
// src/define.ts
|
|
3095
3694
|
function defineForm(def) {
|
|
@@ -3148,16 +3747,18 @@ function formSchemasEqual(left, right) {
|
|
|
3148
3747
|
return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
|
|
3149
3748
|
}
|
|
3150
3749
|
var syncTtlMs = (status) => status === "published" ? 36e5 : 6e4;
|
|
3151
|
-
var isVolatileSync = (result) => Boolean(
|
|
3750
|
+
var isVolatileSync = (result) => Boolean(
|
|
3751
|
+
result.staged || result.resolvedSchema || result.syncError || result.accepting === false || result.uploadsAvailable === false
|
|
3752
|
+
);
|
|
3152
3753
|
function storageKey(client, handle) {
|
|
3153
|
-
return `fillo:sync:
|
|
3754
|
+
return `fillo:sync:v4:${client.baseUrl}|${client.key}|${handle}`;
|
|
3154
3755
|
}
|
|
3155
3756
|
function readStoredSync(client, handle, hash) {
|
|
3156
3757
|
try {
|
|
3157
3758
|
const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
|
|
3158
3759
|
if (!raw) return null;
|
|
3159
3760
|
const parsed = JSON.parse(raw);
|
|
3160
|
-
if (typeof parsed.formId !== "string" || typeof parsed.slug !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number" || parsed.status !== void 0 && parsed.status !== "draft" && parsed.status !== "published")
|
|
3761
|
+
if (typeof parsed.formId !== "string" || typeof parsed.slug !== "string" || parsed.hash !== hash || typeof parsed.ts !== "number" || parsed.uploadFileSizeLimitMb !== void 0 && (typeof parsed.uploadFileSizeLimitMb !== "number" || !Number.isFinite(parsed.uploadFileSizeLimitMb) || parsed.uploadFileSizeLimitMb <= 0) || parsed.status !== void 0 && parsed.status !== "draft" && parsed.status !== "published")
|
|
3161
3762
|
return null;
|
|
3162
3763
|
const ttl = syncTtlMs(parsed.status);
|
|
3163
3764
|
if (Date.now() - parsed.ts > ttl) return null;
|
|
@@ -3167,7 +3768,7 @@ function readStoredSync(client, handle, hash) {
|
|
|
3167
3768
|
}
|
|
3168
3769
|
}
|
|
3169
3770
|
function writeStoredSync(client, handle, hash, r) {
|
|
3170
|
-
if (r.staged || r.resolvedSchema || r.syncError) return;
|
|
3771
|
+
if (r.staged || r.resolvedSchema || r.syncError || r.accepting === false) return;
|
|
3171
3772
|
if (r.challenge) return;
|
|
3172
3773
|
try {
|
|
3173
3774
|
globalThis.localStorage?.setItem(
|
|
@@ -3176,6 +3777,7 @@ function writeStoredSync(client, handle, hash, r) {
|
|
|
3176
3777
|
formId: r.formId,
|
|
3177
3778
|
slug: r.slug,
|
|
3178
3779
|
status: r.status,
|
|
3780
|
+
uploadFileSizeLimitMb: r.uploadFileSizeLimitMb,
|
|
3179
3781
|
hash,
|
|
3180
3782
|
ts: Date.now()
|
|
3181
3783
|
})
|
|
@@ -3198,7 +3800,8 @@ function syncCodeForm(client, form, opts) {
|
|
|
3198
3800
|
const promise2 = Promise.resolve({
|
|
3199
3801
|
formId: stored.formId,
|
|
3200
3802
|
slug: stored.slug,
|
|
3201
|
-
status: stored.status
|
|
3803
|
+
status: stored.status,
|
|
3804
|
+
uploadFileSizeLimitMb: stored.uploadFileSizeLimitMb
|
|
3202
3805
|
});
|
|
3203
3806
|
syncCache.set(key, {
|
|
3204
3807
|
promise: promise2,
|
|
@@ -3259,7 +3862,7 @@ var JSX_BLOCK_SPECS = [
|
|
|
3259
3862
|
field("Email", "email", ["maxLength"]),
|
|
3260
3863
|
field("Url", "url", ["maxLength"]),
|
|
3261
3864
|
field("Phone", "phone", ["defaultCountry"]),
|
|
3262
|
-
field("Number", "number", ["min", "max"]),
|
|
3865
|
+
field("Number", "number", ["min", "max", "decimals", "prefix", "suffix", "notation"]),
|
|
3263
3866
|
field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
|
|
3264
3867
|
field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
|
|
3265
3868
|
field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
|
|
@@ -3272,6 +3875,7 @@ var JSX_BLOCK_SPECS = [
|
|
|
3272
3875
|
field("Date", "date", []),
|
|
3273
3876
|
field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
|
|
3274
3877
|
field("Hidden", "hidden", ["paramName", "defaultValue"]),
|
|
3878
|
+
field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
|
|
3275
3879
|
field("Custom", "custom", ["component", "config"]),
|
|
3276
3880
|
content("Heading", "heading", true),
|
|
3277
3881
|
content("Paragraph", "paragraph", true),
|
|
@@ -3548,6 +4152,23 @@ function when(fieldId) {
|
|
|
3548
4152
|
};
|
|
3549
4153
|
}
|
|
3550
4154
|
|
|
4155
|
+
// src/dev-env.ts
|
|
4156
|
+
function isLocalHostname(hostname) {
|
|
4157
|
+
const host = hostname.toLowerCase();
|
|
4158
|
+
return host === "localhost" || host.endsWith(".localhost") || // Loopback ADDRESSES only — anchored so `127.0.0.1.example.com` stays a
|
|
4159
|
+
// real (production) hostname.
|
|
4160
|
+
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(host) || host === "::1" || host === "[::1]" || host === "0.0.0.0";
|
|
4161
|
+
}
|
|
4162
|
+
function isBuildTimeDevEnv() {
|
|
4163
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
4164
|
+
}
|
|
4165
|
+
function isLikelyDevEnv() {
|
|
4166
|
+
if (isBuildTimeDevEnv()) return true;
|
|
4167
|
+
if (typeof window === "undefined") return false;
|
|
4168
|
+
const hostname = window.location?.hostname;
|
|
4169
|
+
return typeof hostname === "string" && isLocalHostname(hostname);
|
|
4170
|
+
}
|
|
4171
|
+
|
|
3551
4172
|
// src/piping.ts
|
|
3552
4173
|
var TOKEN = /\{\{\s*([\w-]+)\s*\}\}/g;
|
|
3553
4174
|
function resolveText(text, data, form) {
|
|
@@ -3581,6 +4202,7 @@ export {
|
|
|
3581
4202
|
DEFAULT_FIELD_STRINGS,
|
|
3582
4203
|
DEFAULT_STRINGS,
|
|
3583
4204
|
DRAFT_KINDS,
|
|
4205
|
+
FILLO_CALC_MIN_SDK_VERSION,
|
|
3584
4206
|
FILLO_CHALLENGE_MIN_SDK_VERSION,
|
|
3585
4207
|
FILLO_DATA_ATTRS,
|
|
3586
4208
|
FILLO_MIN_SDK_VERSION,
|
|
@@ -3594,12 +4216,14 @@ export {
|
|
|
3594
4216
|
JSX_BLOCK_COMPONENTS,
|
|
3595
4217
|
JSX_BLOCK_SPECS,
|
|
3596
4218
|
PHONE_COUNTRIES,
|
|
4219
|
+
PHONE_PICKER_COUNTRIES,
|
|
3597
4220
|
PHONE_POPOVER_VIEWPORT_GAP,
|
|
3598
4221
|
REQUIRED_FIELD_MESSAGE,
|
|
3599
4222
|
Sha1,
|
|
3600
4223
|
allFields,
|
|
3601
4224
|
assembleForm,
|
|
3602
4225
|
codeFormFromJsx,
|
|
4226
|
+
computeCalculated,
|
|
3603
4227
|
conditionsMet,
|
|
3604
4228
|
contentHash,
|
|
3605
4229
|
countryByDialCode,
|
|
@@ -3612,26 +4236,33 @@ export {
|
|
|
3612
4236
|
createId,
|
|
3613
4237
|
defineForm,
|
|
3614
4238
|
digitsOnly,
|
|
4239
|
+
evaluateCalc,
|
|
3615
4240
|
flagEmoji,
|
|
3616
4241
|
formSchemasEqual,
|
|
3617
4242
|
formatAnswer,
|
|
4243
|
+
formatGroupedNumber,
|
|
3618
4244
|
formatNational,
|
|
3619
4245
|
isAutoSubmitBlock,
|
|
3620
4246
|
isBlockVisible,
|
|
4247
|
+
isBuildTimeDevEnv,
|
|
3621
4248
|
isCodeForm,
|
|
3622
4249
|
isField,
|
|
3623
4250
|
isFilloError,
|
|
4251
|
+
isLikelyDevEnv,
|
|
3624
4252
|
isPossiblePhone,
|
|
3625
4253
|
isTerminalPage,
|
|
4254
|
+
isValidPartialNumberText,
|
|
3626
4255
|
needsExplicitSubmit,
|
|
3627
4256
|
normalizeFormSchema,
|
|
3628
4257
|
normalizeFormTheme,
|
|
3629
4258
|
normalizeSettings,
|
|
4259
|
+
parseGroupedNumber,
|
|
3630
4260
|
parsePhone,
|
|
3631
4261
|
pipeBlock,
|
|
3632
4262
|
positionPhonePopover,
|
|
3633
4263
|
prefillFromParams,
|
|
3634
4264
|
provisionWorkspace,
|
|
4265
|
+
radioGroupStep,
|
|
3635
4266
|
reachableFieldIds,
|
|
3636
4267
|
reachableFields,
|
|
3637
4268
|
reachablePageIds,
|
|
@@ -3640,6 +4271,7 @@ export {
|
|
|
3640
4271
|
resolveSlotClass,
|
|
3641
4272
|
resolveStrings,
|
|
3642
4273
|
resolveText,
|
|
4274
|
+
resolveThemeAppearance,
|
|
3643
4275
|
responseScopeValue,
|
|
3644
4276
|
schemaFromJsx,
|
|
3645
4277
|
sha1Base64,
|