@usefillo/core 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +367 -20
- package/dist/index.js +778 -143
- package/package.json +2 -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}` };
|
|
@@ -475,10 +666,12 @@ function positionPhonePopover(anchor, popover) {
|
|
|
475
666
|
popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
|
|
476
667
|
popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
|
|
477
668
|
const width = Math.min(popover.offsetWidth, maxWidth);
|
|
669
|
+
const rtl = getComputedStyle(anchor).direction === "rtl";
|
|
670
|
+
const unshiftedLeft = rtl ? anchorRect.right - width : anchorRect.left;
|
|
478
671
|
let offsetX = 0;
|
|
479
|
-
const rightOverflow =
|
|
672
|
+
const rightOverflow = unshiftedLeft + width - (viewportWidth - gap);
|
|
480
673
|
if (rightOverflow > 0) offsetX -= rightOverflow;
|
|
481
|
-
const leftOverflow = gap - (
|
|
674
|
+
const leftOverflow = gap - (unshiftedLeft + offsetX);
|
|
482
675
|
if (leftOverflow > 0) offsetX += leftOverflow;
|
|
483
676
|
popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
|
|
484
677
|
return placement;
|
|
@@ -501,8 +694,8 @@ var DEFAULT_STRINGS = {
|
|
|
501
694
|
successMessage: "Your response has been recorded.",
|
|
502
695
|
closed: "This form is no longer accepting responses.",
|
|
503
696
|
notLive: "This form isn't live yet.",
|
|
504
|
-
notOpenTitle: "
|
|
505
|
-
notOpenBody: "
|
|
697
|
+
notOpenTitle: "This form isn't open yet",
|
|
698
|
+
notOpenBody: "The form owner is still setting things up. Please check back soon.",
|
|
506
699
|
closedTitle: "Responses are closed",
|
|
507
700
|
submitFailed: "This form can't submit right now. Please try again in a moment.",
|
|
508
701
|
loadFailedNotFound: "Form not found \u2014 check the form id and that it's published.",
|
|
@@ -519,15 +712,24 @@ var DEFAULT_FIELD_STRINGS = {
|
|
|
519
712
|
resumeLinkExpired: "That saved-progress link expired or was already used \u2014 start again below.",
|
|
520
713
|
alreadyAnswered: "You've already answered this form.",
|
|
521
714
|
uploadRetry: "Retry",
|
|
522
|
-
uploadsDisabled: "
|
|
715
|
+
uploadsDisabled: "Connect this form to Fillo to enable uploads",
|
|
716
|
+
uploadsRenderOnly: "Uploads are unavailable in this render-only preview",
|
|
523
717
|
uploadsUnavailable: "Uploads are temporarily unavailable",
|
|
718
|
+
uploadUnavailable: "We couldn't upload this file right now. Try again in a moment.",
|
|
524
719
|
uploadFailed: "Upload failed \u2014 try again",
|
|
525
720
|
dropzoneTitle: (multiple) => `Drop ${multiple ? "files" : "a file"} here or click to browse`,
|
|
526
721
|
dropzoneHint: (maxMb) => `Up to ${maxMb} MB per file`,
|
|
527
722
|
fileTooLarge: (maxMb) => `Larger than ${maxMb} MB limit`,
|
|
528
723
|
filesUploading: (count) => `Uploading ${count} ${count === 1 ? "file" : "files"}, please wait\u2026`,
|
|
529
724
|
uploadsFailed: (count) => `${count} ${count === 1 ? "upload" : "uploads"} failed.`,
|
|
530
|
-
filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded
|
|
725
|
+
filesUploaded: (count) => `${count} ${count === 1 ? "file" : "files"} uploaded.`,
|
|
726
|
+
submittingAnnouncement: "Submitting your response\u2026",
|
|
727
|
+
errorSummaryTitle: "Please fix the following:",
|
|
728
|
+
rankingPosition: (label, position, count) => `${label}, position ${position} of ${count}`,
|
|
729
|
+
phoneCountrySelected: (name) => `${name} selected`,
|
|
730
|
+
phoneResultsCount: (count) => `${count} ${count === 1 ? "result" : "results"}`,
|
|
731
|
+
signatureEmpty: "No signature yet",
|
|
732
|
+
signatureSigned: "Signature saved"
|
|
531
733
|
};
|
|
532
734
|
function resolveStrings(overrides) {
|
|
533
735
|
return overrides ? { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS, ...overrides } : { ...DEFAULT_STRINGS, ...DEFAULT_FIELD_STRINGS };
|
|
@@ -541,16 +743,9 @@ var fileValueSchema = z.object({
|
|
|
541
743
|
mime: z.string(),
|
|
542
744
|
url: z.optional(z.string())
|
|
543
745
|
});
|
|
544
|
-
function isEmpty(value) {
|
|
545
|
-
if (value === null || value === void 0) return true;
|
|
546
|
-
if (typeof value === "string") return value.trim() === "";
|
|
547
|
-
if (Array.isArray(value)) return value.length === 0;
|
|
548
|
-
if (typeof value === "boolean") return value === false;
|
|
549
|
-
if (typeof value === "object") return Object.keys(value).length === 0;
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
746
|
function validateField(field2, value) {
|
|
553
747
|
if (isEmpty(value)) {
|
|
748
|
+
if (field2.kind === "calculated") return null;
|
|
554
749
|
return field2.required ? REQUIRED_FIELD_MESSAGE : null;
|
|
555
750
|
}
|
|
556
751
|
switch (field2.kind) {
|
|
@@ -667,6 +862,9 @@ function validateField(field2, value) {
|
|
|
667
862
|
case "hidden": {
|
|
668
863
|
return typeof value === "string" ? null : "Invalid value";
|
|
669
864
|
}
|
|
865
|
+
case "calculated": {
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
670
868
|
case "custom": {
|
|
671
869
|
return null;
|
|
672
870
|
}
|
|
@@ -694,6 +892,7 @@ function validateResponse(form, data) {
|
|
|
694
892
|
const errors = {};
|
|
695
893
|
const cleaned = {};
|
|
696
894
|
for (const field2 of fields) {
|
|
895
|
+
if (field2.kind === "calculated") continue;
|
|
697
896
|
const value = normalizeValue(field2, data[field2.id]);
|
|
698
897
|
const error = validateField(field2, value);
|
|
699
898
|
if (error) {
|
|
@@ -704,20 +903,6 @@ function validateResponse(form, data) {
|
|
|
704
903
|
}
|
|
705
904
|
return { ok: Object.keys(errors).length === 0, errors, data: cleaned };
|
|
706
905
|
}
|
|
707
|
-
function normalizeValue(field2, value) {
|
|
708
|
-
if (value === void 0 || value === null) return value;
|
|
709
|
-
if (field2.kind === "custom") return value;
|
|
710
|
-
if (field2.kind === "number" && typeof value === "string" && value.trim() !== "") {
|
|
711
|
-
const n = Number(value);
|
|
712
|
-
return Number.isNaN(n) ? value : n;
|
|
713
|
-
}
|
|
714
|
-
if ((field2.kind === "rating" || field2.kind === "linear_scale") && typeof value === "string") {
|
|
715
|
-
const n = Number(value);
|
|
716
|
-
return Number.isNaN(n) ? value : n;
|
|
717
|
-
}
|
|
718
|
-
if (typeof value === "string") return value.trim();
|
|
719
|
-
return value;
|
|
720
|
-
}
|
|
721
906
|
|
|
722
907
|
// src/schema-validation.ts
|
|
723
908
|
import * as z2 from "zod/mini";
|
|
@@ -740,6 +925,7 @@ var FIELD_KINDS = [
|
|
|
740
925
|
"date",
|
|
741
926
|
"file_upload",
|
|
742
927
|
"hidden",
|
|
928
|
+
"calculated",
|
|
743
929
|
"custom"
|
|
744
930
|
];
|
|
745
931
|
var CONTENT_KINDS2 = ["heading", "paragraph", "divider"];
|
|
@@ -776,9 +962,10 @@ var schemaShape = z2.object({
|
|
|
776
962
|
});
|
|
777
963
|
var MAX_SCHEMA_VERSION = 1;
|
|
778
964
|
var FILLO_SCHEMA_VERSION = 1;
|
|
779
|
-
var FILLO_SDK_VERSION = true ? "0.
|
|
965
|
+
var FILLO_SDK_VERSION = true ? "0.12.0" : "0.0.0-dev";
|
|
780
966
|
var FILLO_MIN_SDK_VERSION = "0.4.0";
|
|
781
967
|
var FILLO_CHALLENGE_MIN_SDK_VERSION = "0.9.0";
|
|
968
|
+
var FILLO_CALC_MIN_SDK_VERSION = "0.11.0";
|
|
782
969
|
function str(value, max, fallback = "") {
|
|
783
970
|
return typeof value === "string" ? value.trim().slice(0, max) : fallback;
|
|
784
971
|
}
|
|
@@ -786,6 +973,11 @@ function optionalStr(value, max) {
|
|
|
786
973
|
const s = str(value, max);
|
|
787
974
|
return s === "" ? void 0 : s;
|
|
788
975
|
}
|
|
976
|
+
function optionalAffix(value, max) {
|
|
977
|
+
if (typeof value !== "string") return void 0;
|
|
978
|
+
const s = value.slice(0, max);
|
|
979
|
+
return s.trim() === "" ? void 0 : s;
|
|
980
|
+
}
|
|
789
981
|
function bool(value) {
|
|
790
982
|
return typeof value === "boolean" ? value : void 0;
|
|
791
983
|
}
|
|
@@ -833,7 +1025,17 @@ function conditions(value) {
|
|
|
833
1025
|
});
|
|
834
1026
|
return normalized.length ? normalized : void 0;
|
|
835
1027
|
}
|
|
836
|
-
function
|
|
1028
|
+
function jumpDepsAllowed(fieldId, calcRefs, allowed, seen) {
|
|
1029
|
+
const refs = calcRefs.get(fieldId);
|
|
1030
|
+
if (refs === void 0) return allowed.has(fieldId);
|
|
1031
|
+
if (seen.has(fieldId)) return true;
|
|
1032
|
+
seen.add(fieldId);
|
|
1033
|
+
for (const ref of refs) {
|
|
1034
|
+
if (!jumpDepsAllowed(ref, calcRefs, allowed, seen)) return false;
|
|
1035
|
+
}
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
function jumpRules(value, pageIds, sourceId, allowedFieldIds, calcRefs) {
|
|
837
1039
|
if (!Array.isArray(value)) return void 0;
|
|
838
1040
|
const rules = value.slice(0, 50).flatMap((item) => {
|
|
839
1041
|
if (!item || typeof item !== "object") return [];
|
|
@@ -845,11 +1047,187 @@ function jumpRules(value, pageIds, sourceId, allowedFieldIds) {
|
|
|
845
1047
|
if (rawWhen !== void 0 && !Array.isArray(rawWhen)) return [];
|
|
846
1048
|
const when2 = conditions(rawWhen);
|
|
847
1049
|
if (Array.isArray(rawWhen) && rawWhen.length > 0 && when2 === void 0) return [];
|
|
848
|
-
if (when2 && when2.some((c) => !
|
|
1050
|
+
if (when2 && when2.some((c) => !jumpDepsAllowed(c.fieldId, calcRefs, allowedFieldIds, /* @__PURE__ */ new Set())))
|
|
1051
|
+
return [];
|
|
849
1052
|
return [{ when: when2 ?? [], to }];
|
|
850
1053
|
});
|
|
851
1054
|
return rules.length ? rules : void 0;
|
|
852
1055
|
}
|
|
1056
|
+
var CALC_OPERAND_KINDS = ["number", "rating", "linear_scale", "calculated"];
|
|
1057
|
+
function normalizeCalc(value, depth = 0) {
|
|
1058
|
+
if (depth > 32) return null;
|
|
1059
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1060
|
+
const rec = value;
|
|
1061
|
+
switch (rec.op) {
|
|
1062
|
+
case "value": {
|
|
1063
|
+
const fieldId = str(rec.fieldId, 128);
|
|
1064
|
+
return fieldId ? { op: "value", fieldId } : null;
|
|
1065
|
+
}
|
|
1066
|
+
case "const": {
|
|
1067
|
+
const n = finite(rec.value);
|
|
1068
|
+
return n === void 0 ? null : { op: "const", value: n };
|
|
1069
|
+
}
|
|
1070
|
+
case "add":
|
|
1071
|
+
case "mul":
|
|
1072
|
+
case "min":
|
|
1073
|
+
case "max": {
|
|
1074
|
+
if (!Array.isArray(rec.args) || rec.args.length < 1 || rec.args.length > 50) return null;
|
|
1075
|
+
const args = [];
|
|
1076
|
+
for (const raw of rec.args) {
|
|
1077
|
+
const arg = normalizeCalc(raw, depth + 1);
|
|
1078
|
+
if (!arg) return null;
|
|
1079
|
+
args.push(arg);
|
|
1080
|
+
}
|
|
1081
|
+
return { op: rec.op, args };
|
|
1082
|
+
}
|
|
1083
|
+
case "sub":
|
|
1084
|
+
case "div": {
|
|
1085
|
+
const left = normalizeCalc(rec.left, depth + 1);
|
|
1086
|
+
const right = normalizeCalc(rec.right, depth + 1);
|
|
1087
|
+
return left && right ? { op: rec.op, left, right } : null;
|
|
1088
|
+
}
|
|
1089
|
+
case "round": {
|
|
1090
|
+
const arg = normalizeCalc(rec.arg, depth + 1);
|
|
1091
|
+
if (!arg) return null;
|
|
1092
|
+
return cleanObject({
|
|
1093
|
+
op: "round",
|
|
1094
|
+
arg,
|
|
1095
|
+
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0)
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
case "if": {
|
|
1099
|
+
if (!Array.isArray(rec.when)) return null;
|
|
1100
|
+
const when2 = conditions(rec.when) ?? [];
|
|
1101
|
+
if (when2.length !== rec.when.length) return null;
|
|
1102
|
+
const then = normalizeCalc(rec.then, depth + 1);
|
|
1103
|
+
const els = normalizeCalc(rec.else, depth + 1);
|
|
1104
|
+
return then && els ? { op: "if", when: when2, then, else: els } : null;
|
|
1105
|
+
}
|
|
1106
|
+
default:
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function collectCalcRefs(expr, valueRefs, whenRefs) {
|
|
1111
|
+
switch (expr.op) {
|
|
1112
|
+
case "value":
|
|
1113
|
+
valueRefs.add(expr.fieldId);
|
|
1114
|
+
return;
|
|
1115
|
+
case "const":
|
|
1116
|
+
return;
|
|
1117
|
+
case "add":
|
|
1118
|
+
case "mul":
|
|
1119
|
+
case "min":
|
|
1120
|
+
case "max":
|
|
1121
|
+
for (const arg of expr.args) collectCalcRefs(arg, valueRefs, whenRefs);
|
|
1122
|
+
return;
|
|
1123
|
+
case "sub":
|
|
1124
|
+
case "div":
|
|
1125
|
+
collectCalcRefs(expr.left, valueRefs, whenRefs);
|
|
1126
|
+
collectCalcRefs(expr.right, valueRefs, whenRefs);
|
|
1127
|
+
return;
|
|
1128
|
+
case "round":
|
|
1129
|
+
collectCalcRefs(expr.arg, valueRefs, whenRefs);
|
|
1130
|
+
return;
|
|
1131
|
+
case "if":
|
|
1132
|
+
for (const cond of expr.when) whenRefs.add(cond.fieldId);
|
|
1133
|
+
collectCalcRefs(expr.then, valueRefs, whenRefs);
|
|
1134
|
+
collectCalcRefs(expr.else, valueRefs, whenRefs);
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function validateCalculatedFields(pages) {
|
|
1139
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
1140
|
+
for (const page of pages) {
|
|
1141
|
+
for (const block of page.blocks) if (isField(block)) kinds.set(block.id, block.kind);
|
|
1142
|
+
}
|
|
1143
|
+
const deps = /* @__PURE__ */ new Map();
|
|
1144
|
+
for (const page of pages) {
|
|
1145
|
+
for (const block of page.blocks) {
|
|
1146
|
+
if (block.kind !== "calculated") continue;
|
|
1147
|
+
const calc = normalizeCalc(block.calc);
|
|
1148
|
+
if (!calc) {
|
|
1149
|
+
return `Field ${block.id} has a malformed calc expression \u2014 build it from value/const/add/sub/mul/div/min/max/round/if nodes`;
|
|
1150
|
+
}
|
|
1151
|
+
block.calc = calc;
|
|
1152
|
+
const valueRefs = /* @__PURE__ */ new Set();
|
|
1153
|
+
const whenRefs = /* @__PURE__ */ new Set();
|
|
1154
|
+
collectCalcRefs(calc, valueRefs, whenRefs);
|
|
1155
|
+
for (const ref of valueRefs) {
|
|
1156
|
+
const kind = kinds.get(ref);
|
|
1157
|
+
if (kind === void 0) {
|
|
1158
|
+
return `Calculated field ${block.id} references a missing field: ${ref} \u2014 remove the reference or restore that field`;
|
|
1159
|
+
}
|
|
1160
|
+
if (!CALC_OPERAND_KINDS.includes(kind)) {
|
|
1161
|
+
return `Calculated field ${block.id} references non-numeric field ${ref} (${kind}) \u2014 calculations can read number, rating, linear_scale, and calculated fields only`;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
deps.set(
|
|
1165
|
+
block.id,
|
|
1166
|
+
[...valueRefs, ...whenRefs].filter((ref) => kinds.get(ref) === "calculated")
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
const state = /* @__PURE__ */ new Map();
|
|
1171
|
+
const visit = (id, path) => {
|
|
1172
|
+
const mark = state.get(id);
|
|
1173
|
+
if (mark === "done") return null;
|
|
1174
|
+
if (mark === "visiting") {
|
|
1175
|
+
const chain = [...path.slice(path.indexOf(id)), id].join(" \u2192 ");
|
|
1176
|
+
return `Calculated field ${id} depends on its own result (${chain}) \u2014 break the cycle so the value can be computed`;
|
|
1177
|
+
}
|
|
1178
|
+
state.set(id, "visiting");
|
|
1179
|
+
for (const dep of deps.get(id) ?? []) {
|
|
1180
|
+
const error = visit(dep, [...path, id]);
|
|
1181
|
+
if (error) return error;
|
|
1182
|
+
}
|
|
1183
|
+
state.set(id, "done");
|
|
1184
|
+
return null;
|
|
1185
|
+
};
|
|
1186
|
+
for (const id of deps.keys()) {
|
|
1187
|
+
const error = visit(id, []);
|
|
1188
|
+
if (error) return error;
|
|
1189
|
+
}
|
|
1190
|
+
return null;
|
|
1191
|
+
}
|
|
1192
|
+
var MAX_CALC_FIELDS = 100;
|
|
1193
|
+
var MAX_CALC_TOTAL_NODES = 5e3;
|
|
1194
|
+
function countCalcNodes(expr) {
|
|
1195
|
+
switch (expr.op) {
|
|
1196
|
+
case "value":
|
|
1197
|
+
case "const":
|
|
1198
|
+
return 1;
|
|
1199
|
+
case "add":
|
|
1200
|
+
case "mul":
|
|
1201
|
+
case "min":
|
|
1202
|
+
case "max":
|
|
1203
|
+
return 1 + expr.args.reduce((n, arg) => n + countCalcNodes(arg), 0);
|
|
1204
|
+
case "sub":
|
|
1205
|
+
case "div":
|
|
1206
|
+
return 1 + countCalcNodes(expr.left) + countCalcNodes(expr.right);
|
|
1207
|
+
case "round":
|
|
1208
|
+
return 1 + countCalcNodes(expr.arg);
|
|
1209
|
+
case "if":
|
|
1210
|
+
return 1 + countCalcNodes(expr.then) + countCalcNodes(expr.else);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function calcComplexityError(pages) {
|
|
1214
|
+
let calcFields = 0;
|
|
1215
|
+
let totalNodes = 0;
|
|
1216
|
+
for (const page of pages) {
|
|
1217
|
+
for (const block of page.blocks) {
|
|
1218
|
+
if (block.kind !== "calculated") continue;
|
|
1219
|
+
calcFields += 1;
|
|
1220
|
+
totalNodes += countCalcNodes(block.calc);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (calcFields > MAX_CALC_FIELDS) {
|
|
1224
|
+
return `Form has ${calcFields} calculated fields \u2014 the maximum is ${MAX_CALC_FIELDS}; remove some or consolidate them into fewer expressions`;
|
|
1225
|
+
}
|
|
1226
|
+
if (totalNodes > MAX_CALC_TOTAL_NODES) {
|
|
1227
|
+
return `Calculated expressions total ${totalNodes} operations \u2014 the maximum is ${MAX_CALC_TOTAL_NODES} across the form; simplify or split up the calculations`;
|
|
1228
|
+
}
|
|
1229
|
+
return null;
|
|
1230
|
+
}
|
|
853
1231
|
function baseBlock(rec) {
|
|
854
1232
|
return {
|
|
855
1233
|
id: str(rec.id, 128),
|
|
@@ -961,7 +1339,20 @@ function normalizeBlock(input) {
|
|
|
961
1339
|
maxLength: finite(rec.maxLength) === void 0 ? void 0 : int(rec.maxLength, 1, 2e4, 2e4)
|
|
962
1340
|
});
|
|
963
1341
|
case "number":
|
|
964
|
-
return cleanObject({
|
|
1342
|
+
return cleanObject({
|
|
1343
|
+
...baseField(rec, kind),
|
|
1344
|
+
min: finite(rec.min),
|
|
1345
|
+
max: finite(rec.max),
|
|
1346
|
+
// Display-only (decision 1 in the number-formatting contract) —
|
|
1347
|
+
// mirrors the calculated field's decimals/prefix/suffix carry-through
|
|
1348
|
+
// exactly, plus notation which calculated doesn't have.
|
|
1349
|
+
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
|
|
1350
|
+
prefix: optionalAffix(rec.prefix, 100),
|
|
1351
|
+
suffix: optionalAffix(rec.suffix, 100),
|
|
1352
|
+
// The only supported values; anything else drops silently rather than
|
|
1353
|
+
// erroring (an old/garbage value is just "no grouping").
|
|
1354
|
+
notation: rec.notation === "grouped" ? "grouped" : rec.notation === "grouped-comma" ? "grouped-comma" : rec.notation === "grouped-dot" ? "grouped-dot" : void 0
|
|
1355
|
+
});
|
|
965
1356
|
case "select":
|
|
966
1357
|
case "multi_select":
|
|
967
1358
|
case "dropdown": {
|
|
@@ -1025,6 +1416,21 @@ function normalizeBlock(input) {
|
|
|
1025
1416
|
paramName: optionalStr(rec.paramName, 128),
|
|
1026
1417
|
defaultValue: optionalStr(rec.defaultValue, 1e3)
|
|
1027
1418
|
});
|
|
1419
|
+
case "calculated":
|
|
1420
|
+
return cleanObject({
|
|
1421
|
+
...baseField(rec, kind),
|
|
1422
|
+
// A derived value isn't answerable, so it can't be required (the
|
|
1423
|
+
// hidden-field precedent) — a required one could never be satisfied.
|
|
1424
|
+
required: false,
|
|
1425
|
+
// Kept raw here on purpose: the calc AST is normalized and structurally
|
|
1426
|
+
// validated in normalizeFormSchema once the full field universe is
|
|
1427
|
+
// known (missing refs, operand kinds, cycles are HARD errors there — a
|
|
1428
|
+
// calculated field must never be silently dropped or degraded).
|
|
1429
|
+
calc: rec.calc,
|
|
1430
|
+
decimals: finite(rec.decimals) === void 0 ? void 0 : int(rec.decimals, 0, 6, 0),
|
|
1431
|
+
prefix: optionalAffix(rec.prefix, 100),
|
|
1432
|
+
suffix: optionalAffix(rec.suffix, 100)
|
|
1433
|
+
});
|
|
1028
1434
|
case "file_upload":
|
|
1029
1435
|
return cleanObject({
|
|
1030
1436
|
...baseField(rec, kind),
|
|
@@ -1063,6 +1469,22 @@ function normalizeFormSchema(input) {
|
|
|
1063
1469
|
return { id: str(page.id, 128), title: optionalStr(page.title, 500), blocks, next: page.next };
|
|
1064
1470
|
});
|
|
1065
1471
|
const targetIds = new Set(rebuilt.map((p) => p.id));
|
|
1472
|
+
const calcRefs = /* @__PURE__ */ new Map();
|
|
1473
|
+
for (const page of rebuilt) {
|
|
1474
|
+
for (const block of page.blocks) {
|
|
1475
|
+
if (block.kind !== "calculated") continue;
|
|
1476
|
+
const refs = /* @__PURE__ */ new Set();
|
|
1477
|
+
const calc = normalizeCalc(block.calc);
|
|
1478
|
+
if (calc) {
|
|
1479
|
+
const valueRefs = /* @__PURE__ */ new Set();
|
|
1480
|
+
const whenRefs = /* @__PURE__ */ new Set();
|
|
1481
|
+
collectCalcRefs(calc, valueRefs, whenRefs);
|
|
1482
|
+
for (const ref of valueRefs) refs.add(ref);
|
|
1483
|
+
for (const ref of whenRefs) refs.add(ref);
|
|
1484
|
+
}
|
|
1485
|
+
calcRefs.set(block.id, refs);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1066
1488
|
const priorFieldIds = /* @__PURE__ */ new Set();
|
|
1067
1489
|
const pages = rebuilt.map((page) => {
|
|
1068
1490
|
for (const block of page.blocks) {
|
|
@@ -1072,7 +1494,7 @@ function normalizeFormSchema(input) {
|
|
|
1072
1494
|
id: page.id,
|
|
1073
1495
|
title: page.title,
|
|
1074
1496
|
blocks: page.blocks,
|
|
1075
|
-
next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds))
|
|
1497
|
+
next: jumpRules(page.next, targetIds, page.id, new Set(priorFieldIds), calcRefs)
|
|
1076
1498
|
});
|
|
1077
1499
|
});
|
|
1078
1500
|
const pageIds = /* @__PURE__ */ new Set();
|
|
@@ -1107,6 +1529,10 @@ function normalizeFormSchema(input) {
|
|
|
1107
1529
|
}
|
|
1108
1530
|
}
|
|
1109
1531
|
}
|
|
1532
|
+
const calcError = validateCalculatedFields(pages);
|
|
1533
|
+
if (calcError) return { ok: false, error: calcError };
|
|
1534
|
+
const calcComplexity = calcComplexityError(pages);
|
|
1535
|
+
if (calcComplexity) return { ok: false, error: calcComplexity };
|
|
1110
1536
|
if (pages.every((page) => page.blocks.length === 0)) {
|
|
1111
1537
|
return { ok: false, error: "Schema must contain at least one valid block" };
|
|
1112
1538
|
}
|
|
@@ -1179,8 +1605,19 @@ function formatAnswer(field2, value) {
|
|
|
1179
1605
|
return value === true ? "Yes" : "No";
|
|
1180
1606
|
case "rating":
|
|
1181
1607
|
case "linear_scale":
|
|
1182
|
-
case "number":
|
|
1183
1608
|
return typeof value === "number" ? String(value) : String(value);
|
|
1609
|
+
case "number": {
|
|
1610
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1611
|
+
if (!Number.isFinite(n)) return String(value);
|
|
1612
|
+
const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
|
|
1613
|
+
return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
|
|
1614
|
+
}
|
|
1615
|
+
case "calculated": {
|
|
1616
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
1617
|
+
if (!Number.isFinite(n)) return String(value);
|
|
1618
|
+
const text = field2.decimals === void 0 ? String(n) : n.toFixed(Math.max(0, Math.min(6, field2.decimals)));
|
|
1619
|
+
return `${field2.prefix ?? ""}${text}${field2.suffix ?? ""}`;
|
|
1620
|
+
}
|
|
1184
1621
|
case "phone": {
|
|
1185
1622
|
const { country, national, e164 } = parsePhone(String(value));
|
|
1186
1623
|
if (!e164) return String(value);
|
|
@@ -1202,6 +1639,103 @@ function formatAnswer(field2, value) {
|
|
|
1202
1639
|
}
|
|
1203
1640
|
}
|
|
1204
1641
|
|
|
1642
|
+
// src/number-format.ts
|
|
1643
|
+
function clampDecimals2(decimals) {
|
|
1644
|
+
return Math.max(0, Math.min(6, Math.round(decimals)));
|
|
1645
|
+
}
|
|
1646
|
+
function localeForNotation(notation) {
|
|
1647
|
+
switch (notation) {
|
|
1648
|
+
case "grouped-comma":
|
|
1649
|
+
return "en-US";
|
|
1650
|
+
case "grouped-dot":
|
|
1651
|
+
return "de-DE";
|
|
1652
|
+
case "grouped":
|
|
1653
|
+
case void 0:
|
|
1654
|
+
return void 0;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
function formatGroupedNumber(value, opts) {
|
|
1658
|
+
if (!Number.isFinite(value)) return String(value);
|
|
1659
|
+
const decimals = opts?.decimals === void 0 ? void 0 : clampDecimals2(opts.decimals);
|
|
1660
|
+
if (typeof Intl === "undefined") {
|
|
1661
|
+
return decimals === void 0 ? String(value) : value.toFixed(decimals);
|
|
1662
|
+
}
|
|
1663
|
+
const format = { useGrouping: true };
|
|
1664
|
+
if (decimals === void 0) {
|
|
1665
|
+
format.maximumFractionDigits = 20;
|
|
1666
|
+
} else {
|
|
1667
|
+
format.minimumFractionDigits = decimals;
|
|
1668
|
+
format.maximumFractionDigits = decimals;
|
|
1669
|
+
}
|
|
1670
|
+
return new Intl.NumberFormat(opts?.locale, format).format(value);
|
|
1671
|
+
}
|
|
1672
|
+
function stripAll(text, separator) {
|
|
1673
|
+
return separator ? text.split(separator).join("") : text;
|
|
1674
|
+
}
|
|
1675
|
+
function toCanonicalDecimal(text, decimal) {
|
|
1676
|
+
return decimal === "." ? text : text.split(decimal).join(".");
|
|
1677
|
+
}
|
|
1678
|
+
function resolveAmbiguousGrouping(body, group, decimal) {
|
|
1679
|
+
if (decimal !== group && body.includes(decimal)) {
|
|
1680
|
+
const i = body.lastIndexOf(decimal);
|
|
1681
|
+
const intPart = stripAll(body.slice(0, i), group);
|
|
1682
|
+
const fracPart = body.slice(i + decimal.length);
|
|
1683
|
+
return `${intPart}.${fracPart}`;
|
|
1684
|
+
}
|
|
1685
|
+
const segments = body.split(group);
|
|
1686
|
+
if (segments.length === 1) return body;
|
|
1687
|
+
const isPureGrouping = segments.every(
|
|
1688
|
+
(seg, i) => i === 0 ? /^[1-9]\d{0,2}$/.test(seg) : /^\d{3}$/.test(seg)
|
|
1689
|
+
);
|
|
1690
|
+
if (isPureGrouping) return segments.join("");
|
|
1691
|
+
return `${segments.slice(0, -1).join("")}.${segments.at(-1) ?? ""}`;
|
|
1692
|
+
}
|
|
1693
|
+
function parseGroupedNumber(text, locale) {
|
|
1694
|
+
if (typeof Intl === "undefined") return text;
|
|
1695
|
+
if (text.trim() === "") return text;
|
|
1696
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(12345678e-1);
|
|
1697
|
+
const group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
1698
|
+
const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
1699
|
+
let body = text.trim();
|
|
1700
|
+
const negative = body.startsWith("-");
|
|
1701
|
+
if (negative) body = body.slice(1);
|
|
1702
|
+
if (/^\s$/.test(group)) {
|
|
1703
|
+
body = toCanonicalDecimal(body.replace(/\s/g, ""), decimal);
|
|
1704
|
+
} else if (group === "." || group === ",") {
|
|
1705
|
+
body = resolveAmbiguousGrouping(body, group, decimal);
|
|
1706
|
+
} else {
|
|
1707
|
+
body = toCanonicalDecimal(stripAll(body, group), decimal);
|
|
1708
|
+
}
|
|
1709
|
+
if (body === "" || body === "." || !/^\d*\.?\d*$/.test(body)) return text;
|
|
1710
|
+
return negative ? `-${body}` : body;
|
|
1711
|
+
}
|
|
1712
|
+
function isValidPartialNumberText(text, locale) {
|
|
1713
|
+
const negative = text.startsWith("-");
|
|
1714
|
+
const body = negative ? text.slice(1) : text;
|
|
1715
|
+
if (body.includes("-")) return false;
|
|
1716
|
+
if (body === "") return true;
|
|
1717
|
+
if (typeof Intl === "undefined") {
|
|
1718
|
+
return /^[\d.,]*$/.test(body);
|
|
1719
|
+
}
|
|
1720
|
+
const parts = new Intl.NumberFormat(locale).formatToParts(12345678e-1);
|
|
1721
|
+
const group = parts.find((p) => p.type === "group")?.value ?? ",";
|
|
1722
|
+
const decimal = parts.find((p) => p.type === "decimal")?.value ?? ".";
|
|
1723
|
+
const dotIsGroup = group === ".";
|
|
1724
|
+
const groupIsWhitespace = /^\s$/.test(group);
|
|
1725
|
+
const decimalMarks = dotIsGroup ? [decimal] : Array.from(/* @__PURE__ */ new Set([".", decimal]));
|
|
1726
|
+
let decimalSeen = 0;
|
|
1727
|
+
for (const ch of body) {
|
|
1728
|
+
if (/\d/.test(ch)) continue;
|
|
1729
|
+
if (decimalMarks.includes(ch)) {
|
|
1730
|
+
if (++decimalSeen > 1) return false;
|
|
1731
|
+
continue;
|
|
1732
|
+
}
|
|
1733
|
+
if (groupIsWhitespace ? /\s/.test(ch) : ch === group) continue;
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1205
1739
|
// src/ids.ts
|
|
1206
1740
|
var ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
1207
1741
|
var MASK = 256 - 256 % ALPHABET.length;
|
|
@@ -1245,6 +1779,7 @@ var BLOCK_KIND_META = {
|
|
|
1245
1779
|
date: { label: "Date", hint: "Date picker" },
|
|
1246
1780
|
file_upload: { label: "File upload", hint: "Direct to connected storage" },
|
|
1247
1781
|
hidden: { label: "Hidden field", hint: "Filled from the URL" },
|
|
1782
|
+
calculated: { label: "Calculated value", hint: "Computed from other answers" },
|
|
1248
1783
|
custom: { label: "Custom", hint: "Your own component (code only)" },
|
|
1249
1784
|
heading: { label: "Heading", hint: "Section title" },
|
|
1250
1785
|
paragraph: { label: "Paragraph", hint: "Explanatory text" },
|
|
@@ -1314,6 +1849,8 @@ function createBlock(kind) {
|
|
|
1314
1849
|
return { id, kind, label: "Sign here" };
|
|
1315
1850
|
case "hidden":
|
|
1316
1851
|
return { id, kind, label: "Hidden field" };
|
|
1852
|
+
case "calculated":
|
|
1853
|
+
return { id, kind, label: "Calculated value", calc: { op: "const", value: 0 } };
|
|
1317
1854
|
case "custom":
|
|
1318
1855
|
return { id, kind, label: "Custom field", component: "" };
|
|
1319
1856
|
case "file_upload":
|
|
@@ -2344,6 +2881,9 @@ function prefillFromParams(form, params) {
|
|
|
2344
2881
|
case "custom":
|
|
2345
2882
|
break;
|
|
2346
2883
|
// not prefillable from a URL
|
|
2884
|
+
case "calculated":
|
|
2885
|
+
break;
|
|
2886
|
+
// engine-computed, never prefillable — a crafted URL can't seed it
|
|
2347
2887
|
default:
|
|
2348
2888
|
data[field2.id] = raw;
|
|
2349
2889
|
}
|
|
@@ -2494,7 +3034,7 @@ function createFormController(options2) {
|
|
|
2494
3034
|
let formId = options2.formId;
|
|
2495
3035
|
let client = options2.client;
|
|
2496
3036
|
let respondent = options2.respondent;
|
|
2497
|
-
let data = { ...options2.initialData ?? {} };
|
|
3037
|
+
let data = computeCalculated(form, { ...options2.initialData ?? {} });
|
|
2498
3038
|
let errors = {};
|
|
2499
3039
|
let pageIndex = 0;
|
|
2500
3040
|
let status = "idle";
|
|
@@ -2511,7 +3051,7 @@ function createFormController(options2) {
|
|
|
2511
3051
|
}
|
|
2512
3052
|
}
|
|
2513
3053
|
if (merged && status === "idle") {
|
|
2514
|
-
data = merged;
|
|
3054
|
+
data = computeCalculated(form, merged);
|
|
2515
3055
|
notify();
|
|
2516
3056
|
}
|
|
2517
3057
|
}
|
|
@@ -2639,7 +3179,7 @@ function createFormController(options2) {
|
|
|
2639
3179
|
const target = typeof draft.page === "number" && Number.isFinite(draft.page) ? Math.max(0, Math.min(Math.floor(draft.page), form.pages.length - 1)) : 0;
|
|
2640
3180
|
const movePage = target > 0 && pageIndex === 0;
|
|
2641
3181
|
if (!merged && !movePage) return;
|
|
2642
|
-
if (merged) data = merged;
|
|
3182
|
+
if (merged) data = computeCalculated(form, merged);
|
|
2643
3183
|
if (movePage) pageIndex = target;
|
|
2644
3184
|
resumedDraft = true;
|
|
2645
3185
|
notify();
|
|
@@ -2769,7 +3309,7 @@ function createFormController(options2) {
|
|
|
2769
3309
|
sessionStarted = true;
|
|
2770
3310
|
void client.startSession(formId, form.pages.length).then((id) => sessionId = id);
|
|
2771
3311
|
}
|
|
2772
|
-
data = { ...data, [fieldId]: value };
|
|
3312
|
+
data = computeCalculated(form, { ...data, [fieldId]: value });
|
|
2773
3313
|
onChange?.(data);
|
|
2774
3314
|
submitError = void 0;
|
|
2775
3315
|
if (fieldId in errors) {
|
|
@@ -2840,9 +3380,26 @@ function createFormController(options2) {
|
|
|
2840
3380
|
if (status === "submitting" || status === "submitted") return;
|
|
2841
3381
|
const result = validateResponse(form, data);
|
|
2842
3382
|
if (!result.ok) {
|
|
2843
|
-
|
|
3383
|
+
const transportlessPreview = options2.skipValidation && !(client && formId);
|
|
3384
|
+
const previewErrors = transportlessPreview ? Object.fromEntries(
|
|
3385
|
+
Object.entries(result.errors).filter(
|
|
3386
|
+
([fieldId]) => form.pages.every(
|
|
3387
|
+
(page) => page.blocks.every(
|
|
3388
|
+
(block) => !(isField(block) && block.id === fieldId && block.kind === "file_upload")
|
|
3389
|
+
)
|
|
3390
|
+
)
|
|
3391
|
+
)
|
|
3392
|
+
) : result.errors;
|
|
3393
|
+
if (transportlessPreview && Object.keys(previewErrors).length === 0) {
|
|
3394
|
+
errors = {};
|
|
3395
|
+
status = "submitted";
|
|
3396
|
+
notify();
|
|
3397
|
+
emitSubmitted(void 0, data);
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
errors = previewErrors;
|
|
2844
3401
|
const firstBad = form.pages.findIndex(
|
|
2845
|
-
(p) => visiblePageBlocks(form, p, data).some((b) => b.id in
|
|
3402
|
+
(p) => visiblePageBlocks(form, p, data).some((b) => b.id in previewErrors)
|
|
2846
3403
|
);
|
|
2847
3404
|
if (firstBad >= 0) pageIndex = firstBad;
|
|
2848
3405
|
notify();
|
|
@@ -2953,6 +3510,7 @@ function createFormController(options2) {
|
|
|
2953
3510
|
const next2 = normalizeFormSchema(ctx.form);
|
|
2954
3511
|
if (!next2.ok) throw new Error(`Invalid form schema: ${next2.error}`);
|
|
2955
3512
|
form = next2.schema;
|
|
3513
|
+
data = computeCalculated(form, data);
|
|
2956
3514
|
}
|
|
2957
3515
|
if (status === "idle" && hasSubmittedOnce(form, formId, data)) {
|
|
2958
3516
|
status = "submitted";
|
|
@@ -2977,6 +3535,7 @@ function createFormController(options2) {
|
|
|
2977
3535
|
if (data[key] === void 0) data[key] = value;
|
|
2978
3536
|
}
|
|
2979
3537
|
}
|
|
3538
|
+
data = computeCalculated(form, data);
|
|
2980
3539
|
errors = {};
|
|
2981
3540
|
pageIndex = 0;
|
|
2982
3541
|
submitError = void 0;
|
|
@@ -2999,7 +3558,9 @@ function isAutoSubmitBlock(block) {
|
|
|
2999
3558
|
return isField(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
3000
3559
|
}
|
|
3001
3560
|
function needsExplicitSubmit(visible) {
|
|
3002
|
-
const interactive = visible.filter(
|
|
3561
|
+
const interactive = visible.filter(
|
|
3562
|
+
(b) => isField(b) && b.kind !== "hidden" && b.kind !== "calculated"
|
|
3563
|
+
);
|
|
3003
3564
|
if (interactive.length !== 1) return true;
|
|
3004
3565
|
return !isAutoSubmitBlock(interactive[0]);
|
|
3005
3566
|
}
|
|
@@ -3025,11 +3586,40 @@ function shouldAutoSubmit(field2, value, ctx) {
|
|
|
3025
3586
|
const nextData = { ...ctx.data, [field2.id]: value };
|
|
3026
3587
|
const page = ctx.form.pages.find((p) => p.blocks.some((b) => b.id === field2.id));
|
|
3027
3588
|
if (!page || !isTerminalPage(ctx.form, page.id, nextData)) return false;
|
|
3028
|
-
const interactive = reachableFields(ctx.form, nextData).filter(
|
|
3589
|
+
const interactive = reachableFields(ctx.form, nextData).filter(
|
|
3590
|
+
(f) => f.kind !== "hidden" && f.kind !== "calculated"
|
|
3591
|
+
);
|
|
3029
3592
|
if (interactive.length !== 1 || interactive[0]?.id !== field2.id) return false;
|
|
3030
3593
|
return validateResponse(ctx.form, nextData).ok;
|
|
3031
3594
|
}
|
|
3032
3595
|
|
|
3596
|
+
// src/radio-nav.ts
|
|
3597
|
+
function radioGroupStep(key, index, length, opts) {
|
|
3598
|
+
if (length <= 0) return null;
|
|
3599
|
+
const rtl = opts?.rtl ?? false;
|
|
3600
|
+
if (key === "Home") return 0;
|
|
3601
|
+
if (key === "End") return length - 1;
|
|
3602
|
+
let delta;
|
|
3603
|
+
switch (key) {
|
|
3604
|
+
case "ArrowRight":
|
|
3605
|
+
delta = rtl ? -1 : 1;
|
|
3606
|
+
break;
|
|
3607
|
+
case "ArrowLeft":
|
|
3608
|
+
delta = rtl ? 1 : -1;
|
|
3609
|
+
break;
|
|
3610
|
+
case "ArrowDown":
|
|
3611
|
+
delta = 1;
|
|
3612
|
+
break;
|
|
3613
|
+
case "ArrowUp":
|
|
3614
|
+
delta = -1;
|
|
3615
|
+
break;
|
|
3616
|
+
default:
|
|
3617
|
+
return null;
|
|
3618
|
+
}
|
|
3619
|
+
const next = index + delta;
|
|
3620
|
+
return Math.min(length - 1, Math.max(0, next));
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3033
3623
|
// src/appearance.ts
|
|
3034
3624
|
var FILLO_SLOTS = [
|
|
3035
3625
|
"root",
|
|
@@ -3052,7 +3642,8 @@ var FILLO_SLOTS = [
|
|
|
3052
3642
|
"button",
|
|
3053
3643
|
"success",
|
|
3054
3644
|
"resume",
|
|
3055
|
-
"turnstile"
|
|
3645
|
+
"turnstile",
|
|
3646
|
+
"calculated"
|
|
3056
3647
|
];
|
|
3057
3648
|
var FILLO_DATA_ATTRS = {
|
|
3058
3649
|
/** Slot name, on every slot element: `data-fillo="control"`. */
|
|
@@ -3100,6 +3691,35 @@ var FILLO_THEME_VARS = [
|
|
|
3100
3691
|
{ var: "--fillo-error" },
|
|
3101
3692
|
{ var: "--fillo-primary-contrast" }
|
|
3102
3693
|
];
|
|
3694
|
+
var DARK_LUMINANCE_THRESHOLD = Math.sqrt(1.05 * 0.05) - 0.05;
|
|
3695
|
+
var HEX6 = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i;
|
|
3696
|
+
var HEX3 = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i;
|
|
3697
|
+
function srgbToLinear(channel255) {
|
|
3698
|
+
const c = channel255 / 255;
|
|
3699
|
+
return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
|
3700
|
+
}
|
|
3701
|
+
function relativeLuminance(token) {
|
|
3702
|
+
const trimmed = token.trim();
|
|
3703
|
+
let hex = null;
|
|
3704
|
+
const six = HEX6.exec(trimmed);
|
|
3705
|
+
if (six) {
|
|
3706
|
+
hex = `${six[1]}${six[2]}${six[3]}`;
|
|
3707
|
+
} else {
|
|
3708
|
+
const three = HEX3.exec(trimmed);
|
|
3709
|
+
if (three) hex = `${three[1]}${three[1]}${three[2]}${three[2]}${three[3]}${three[3]}`;
|
|
3710
|
+
}
|
|
3711
|
+
if (!hex) return null;
|
|
3712
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
3713
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
3714
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
3715
|
+
return 0.2126 * srgbToLinear(r) + 0.7152 * srgbToLinear(g) + 0.0722 * srgbToLinear(b);
|
|
3716
|
+
}
|
|
3717
|
+
function resolveThemeAppearance(theme) {
|
|
3718
|
+
if (!theme || theme.colorScheme || !theme.background || !theme.text) return theme;
|
|
3719
|
+
const luminance = relativeLuminance(theme.background);
|
|
3720
|
+
if (luminance == null) return theme;
|
|
3721
|
+
return { ...theme, colorScheme: luminance > DARK_LUMINANCE_THRESHOLD ? "light" : "dark" };
|
|
3722
|
+
}
|
|
3103
3723
|
|
|
3104
3724
|
// src/define.ts
|
|
3105
3725
|
function defineForm(def) {
|
|
@@ -3158,16 +3778,18 @@ function formSchemasEqual(left, right) {
|
|
|
3158
3778
|
return JSON.stringify(canonicalize(left)) === JSON.stringify(canonicalize(right));
|
|
3159
3779
|
}
|
|
3160
3780
|
var syncTtlMs = (status) => status === "published" ? 36e5 : 6e4;
|
|
3161
|
-
var isVolatileSync = (result) => Boolean(
|
|
3781
|
+
var isVolatileSync = (result) => Boolean(
|
|
3782
|
+
result.staged || result.resolvedSchema || result.syncError || result.accepting === false || result.uploadsAvailable === false
|
|
3783
|
+
);
|
|
3162
3784
|
function storageKey(client, handle) {
|
|
3163
|
-
return `fillo:sync:
|
|
3785
|
+
return `fillo:sync:v4:${client.baseUrl}|${client.key}|${handle}`;
|
|
3164
3786
|
}
|
|
3165
3787
|
function readStoredSync(client, handle, hash) {
|
|
3166
3788
|
try {
|
|
3167
3789
|
const raw = globalThis.localStorage?.getItem(storageKey(client, handle));
|
|
3168
3790
|
if (!raw) return null;
|
|
3169
3791
|
const parsed = JSON.parse(raw);
|
|
3170
|
-
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")
|
|
3792
|
+
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")
|
|
3171
3793
|
return null;
|
|
3172
3794
|
const ttl = syncTtlMs(parsed.status);
|
|
3173
3795
|
if (Date.now() - parsed.ts > ttl) return null;
|
|
@@ -3186,6 +3808,7 @@ function writeStoredSync(client, handle, hash, r) {
|
|
|
3186
3808
|
formId: r.formId,
|
|
3187
3809
|
slug: r.slug,
|
|
3188
3810
|
status: r.status,
|
|
3811
|
+
uploadFileSizeLimitMb: r.uploadFileSizeLimitMb,
|
|
3189
3812
|
hash,
|
|
3190
3813
|
ts: Date.now()
|
|
3191
3814
|
})
|
|
@@ -3208,7 +3831,8 @@ function syncCodeForm(client, form, opts) {
|
|
|
3208
3831
|
const promise2 = Promise.resolve({
|
|
3209
3832
|
formId: stored.formId,
|
|
3210
3833
|
slug: stored.slug,
|
|
3211
|
-
status: stored.status
|
|
3834
|
+
status: stored.status,
|
|
3835
|
+
uploadFileSizeLimitMb: stored.uploadFileSizeLimitMb
|
|
3212
3836
|
});
|
|
3213
3837
|
syncCache.set(key, {
|
|
3214
3838
|
promise: promise2,
|
|
@@ -3269,7 +3893,7 @@ var JSX_BLOCK_SPECS = [
|
|
|
3269
3893
|
field("Email", "email", ["maxLength"]),
|
|
3270
3894
|
field("Url", "url", ["maxLength"]),
|
|
3271
3895
|
field("Phone", "phone", ["defaultCountry"]),
|
|
3272
|
-
field("Number", "number", ["min", "max"]),
|
|
3896
|
+
field("Number", "number", ["min", "max", "decimals", "prefix", "suffix", "notation"]),
|
|
3273
3897
|
field("Select", "select", CHOICE_EXTRA, { optionChildren: true }),
|
|
3274
3898
|
field("MultiSelect", "multi_select", CHOICE_EXTRA, { optionChildren: true }),
|
|
3275
3899
|
field("Dropdown", "dropdown", CHOICE_EXTRA, { optionChildren: true }),
|
|
@@ -3282,6 +3906,7 @@ var JSX_BLOCK_SPECS = [
|
|
|
3282
3906
|
field("Date", "date", []),
|
|
3283
3907
|
field("FileUpload", "file_upload", ["maxFiles", "maxFileSizeMb", "accept"]),
|
|
3284
3908
|
field("Hidden", "hidden", ["paramName", "defaultValue"]),
|
|
3909
|
+
field("Calculated", "calculated", ["calc", "decimals", "prefix", "suffix"]),
|
|
3285
3910
|
field("Custom", "custom", ["component", "config"]),
|
|
3286
3911
|
content("Heading", "heading", true),
|
|
3287
3912
|
content("Paragraph", "paragraph", true),
|
|
@@ -3608,6 +4233,7 @@ export {
|
|
|
3608
4233
|
DEFAULT_FIELD_STRINGS,
|
|
3609
4234
|
DEFAULT_STRINGS,
|
|
3610
4235
|
DRAFT_KINDS,
|
|
4236
|
+
FILLO_CALC_MIN_SDK_VERSION,
|
|
3611
4237
|
FILLO_CHALLENGE_MIN_SDK_VERSION,
|
|
3612
4238
|
FILLO_DATA_ATTRS,
|
|
3613
4239
|
FILLO_MIN_SDK_VERSION,
|
|
@@ -3621,12 +4247,14 @@ export {
|
|
|
3621
4247
|
JSX_BLOCK_COMPONENTS,
|
|
3622
4248
|
JSX_BLOCK_SPECS,
|
|
3623
4249
|
PHONE_COUNTRIES,
|
|
4250
|
+
PHONE_PICKER_COUNTRIES,
|
|
3624
4251
|
PHONE_POPOVER_VIEWPORT_GAP,
|
|
3625
4252
|
REQUIRED_FIELD_MESSAGE,
|
|
3626
4253
|
Sha1,
|
|
3627
4254
|
allFields,
|
|
3628
4255
|
assembleForm,
|
|
3629
4256
|
codeFormFromJsx,
|
|
4257
|
+
computeCalculated,
|
|
3630
4258
|
conditionsMet,
|
|
3631
4259
|
contentHash,
|
|
3632
4260
|
countryByDialCode,
|
|
@@ -3639,9 +4267,11 @@ export {
|
|
|
3639
4267
|
createId,
|
|
3640
4268
|
defineForm,
|
|
3641
4269
|
digitsOnly,
|
|
4270
|
+
evaluateCalc,
|
|
3642
4271
|
flagEmoji,
|
|
3643
4272
|
formSchemasEqual,
|
|
3644
4273
|
formatAnswer,
|
|
4274
|
+
formatGroupedNumber,
|
|
3645
4275
|
formatNational,
|
|
3646
4276
|
isAutoSubmitBlock,
|
|
3647
4277
|
isBlockVisible,
|
|
@@ -3652,15 +4282,19 @@ export {
|
|
|
3652
4282
|
isLikelyDevEnv,
|
|
3653
4283
|
isPossiblePhone,
|
|
3654
4284
|
isTerminalPage,
|
|
4285
|
+
isValidPartialNumberText,
|
|
4286
|
+
localeForNotation,
|
|
3655
4287
|
needsExplicitSubmit,
|
|
3656
4288
|
normalizeFormSchema,
|
|
3657
4289
|
normalizeFormTheme,
|
|
3658
4290
|
normalizeSettings,
|
|
4291
|
+
parseGroupedNumber,
|
|
3659
4292
|
parsePhone,
|
|
3660
4293
|
pipeBlock,
|
|
3661
4294
|
positionPhonePopover,
|
|
3662
4295
|
prefillFromParams,
|
|
3663
4296
|
provisionWorkspace,
|
|
4297
|
+
radioGroupStep,
|
|
3664
4298
|
reachableFieldIds,
|
|
3665
4299
|
reachableFields,
|
|
3666
4300
|
reachablePageIds,
|
|
@@ -3669,6 +4303,7 @@ export {
|
|
|
3669
4303
|
resolveSlotClass,
|
|
3670
4304
|
resolveStrings,
|
|
3671
4305
|
resolveText,
|
|
4306
|
+
resolveThemeAppearance,
|
|
3672
4307
|
responseScopeValue,
|
|
3673
4308
|
schemaFromJsx,
|
|
3674
4309
|
sha1Base64,
|