@marktiderman/genesis-forms 0.3.0 → 0.4.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 +75 -1
- package/dist/destinations.cjs +210 -33
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.js +210 -33
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-CPLtKVew.d.cts → engine-BkNuzlna.d.cts} +2 -1
- package/dist/{engine-DOrPgFDw.d.ts → engine-Dz9J5ej2.d.ts} +2 -1
- package/dist/index.cjs +758 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +195 -4
- package/dist/index.d.ts +195 -4
- package/dist/index.js +739 -24
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +672 -19
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +5 -1
- package/dist/server.d.ts +5 -1
- package/dist/server.js +672 -19
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +674 -19
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +2 -2
- package/dist/testing.d.ts +2 -2
- package/dist/testing.js +674 -19
- package/dist/testing.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,6 +28,441 @@ function fingerprint(value) {
|
|
|
28
28
|
return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(16).padStart(14, "0");
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// src/formula.ts
|
|
32
|
+
var FORMULA_FUNCTIONS = ["sum", "avg", "min", "max"];
|
|
33
|
+
var MAX_FORMULA_LENGTH = 2e3;
|
|
34
|
+
var MAX_DEPTH = 64;
|
|
35
|
+
var OP_ALIASES = { "\u2212": "-", "\xD7": "*", "\xF7": "/", "\u2260": "!=", "\u2264": "<=", "\u2265": ">=", "=": "==" };
|
|
36
|
+
var TWO_CHAR = /* @__PURE__ */ new Set(["==", "!=", "<=", ">="]);
|
|
37
|
+
var ONE_CHAR = /* @__PURE__ */ new Set(["+", "-", "*", "/", "<", ">", "(", ")", "?", ":", ",", ".", "=", "\u2212", "\xD7", "\xF7", "\u2260", "\u2264", "\u2265"]);
|
|
38
|
+
var COMPARISONS = /* @__PURE__ */ new Set(["==", "!=", "<", "<=", ">", ">="]);
|
|
39
|
+
var FormulaError = class extends Error {
|
|
40
|
+
constructor(message, at) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.at = at;
|
|
43
|
+
}
|
|
44
|
+
at;
|
|
45
|
+
};
|
|
46
|
+
var isIdStart = (c) => c >= "a" && c <= "z" || c >= "A" && c <= "Z" || c === "_";
|
|
47
|
+
var isIdPart = (c) => isIdStart(c) || c >= "0" && c <= "9";
|
|
48
|
+
var isDigit = (c) => c >= "0" && c <= "9";
|
|
49
|
+
function tokenize(source) {
|
|
50
|
+
const tokens = [];
|
|
51
|
+
let i = 0;
|
|
52
|
+
while (i < source.length) {
|
|
53
|
+
const c = source[i];
|
|
54
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
55
|
+
i++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const at = i;
|
|
59
|
+
if (isDigit(c) || c === "." && isDigit(source[i + 1] ?? "")) {
|
|
60
|
+
while (isDigit(source[i] ?? "")) i++;
|
|
61
|
+
if (source[i] === "." && isDigit(source[i + 1] ?? "")) {
|
|
62
|
+
i++;
|
|
63
|
+
while (isDigit(source[i] ?? "")) i++;
|
|
64
|
+
}
|
|
65
|
+
const v = Number(source.slice(at, i));
|
|
66
|
+
if (!Number.isFinite(v)) throw new FormulaError("This number is too large.", at);
|
|
67
|
+
tokens.push({ t: "num", v, at });
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (isIdStart(c)) {
|
|
71
|
+
while (isIdPart(source[i] ?? "")) i++;
|
|
72
|
+
tokens.push({ t: "id", v: source.slice(at, i), at });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (c === '"' || c === "'" || c === "\u201C") {
|
|
76
|
+
const close = c === "\u201C" ? "\u201D" : c;
|
|
77
|
+
const end = source.indexOf(close, i + 1);
|
|
78
|
+
if (end === -1) throw new FormulaError("This text has no closing quote.", at);
|
|
79
|
+
tokens.push({ t: "str", v: source.slice(i + 1, end), at });
|
|
80
|
+
i = end + 1;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const two = source.slice(i, i + 2);
|
|
84
|
+
if (TWO_CHAR.has(two)) {
|
|
85
|
+
tokens.push({ t: "op", v: two, at });
|
|
86
|
+
i += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (ONE_CHAR.has(c)) {
|
|
90
|
+
tokens.push({ t: "op", v: OP_ALIASES[c] ?? c, at });
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
throw new FormulaError(`"${c}" can't be used in a formula.`, at);
|
|
95
|
+
}
|
|
96
|
+
tokens.push({ t: "end", at: source.length });
|
|
97
|
+
return tokens;
|
|
98
|
+
}
|
|
99
|
+
function parseFormula(source) {
|
|
100
|
+
if (typeof source !== "string" || source.trim() === "") return { ok: false, message: "Write a formula.", at: 0 };
|
|
101
|
+
if (source.length > MAX_FORMULA_LENGTH) {
|
|
102
|
+
return { ok: false, message: `Keep the formula under ${MAX_FORMULA_LENGTH} characters.`, at: MAX_FORMULA_LENGTH };
|
|
103
|
+
}
|
|
104
|
+
let tokens;
|
|
105
|
+
try {
|
|
106
|
+
tokens = tokenize(source);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
const err = e;
|
|
109
|
+
return { ok: false, message: err.message, at: err.at };
|
|
110
|
+
}
|
|
111
|
+
let pos = 0;
|
|
112
|
+
const refs = [];
|
|
113
|
+
const peek = () => tokens[pos];
|
|
114
|
+
const isOp = (v) => {
|
|
115
|
+
const tok = peek();
|
|
116
|
+
return tok.t === "op" && tok.v === v;
|
|
117
|
+
};
|
|
118
|
+
const expect = (v, message) => {
|
|
119
|
+
if (!isOp(v)) throw new FormulaError(message, peek().at);
|
|
120
|
+
pos++;
|
|
121
|
+
};
|
|
122
|
+
const describe = (tok) => tok.t === "end" ? "the formula ends there" : `found "${String(tok.v)}"`;
|
|
123
|
+
const guard = (depth) => {
|
|
124
|
+
if (depth > MAX_DEPTH) throw new FormulaError("The formula is nested too deeply.", peek().at);
|
|
125
|
+
};
|
|
126
|
+
const ternary = (depth) => {
|
|
127
|
+
guard(depth);
|
|
128
|
+
const test = comparison(depth);
|
|
129
|
+
if (!isOp("?")) return test;
|
|
130
|
+
pos++;
|
|
131
|
+
const then = ternary(depth + 1);
|
|
132
|
+
expect(":", "A `?` needs a `:` and the other value: `test ? a : b`.");
|
|
133
|
+
const otherwise = ternary(depth + 1);
|
|
134
|
+
return { kind: "if", test, then, else: otherwise };
|
|
135
|
+
};
|
|
136
|
+
const comparison = (depth) => {
|
|
137
|
+
const left = additive(depth);
|
|
138
|
+
const tok = peek();
|
|
139
|
+
if (tok.t !== "op" || !COMPARISONS.has(tok.v)) return left;
|
|
140
|
+
pos++;
|
|
141
|
+
const right = additive(depth);
|
|
142
|
+
const next = peek();
|
|
143
|
+
if (next.t === "op" && COMPARISONS.has(next.v)) {
|
|
144
|
+
throw new FormulaError("Compare two values at a time; combine comparisons with `? :`.", next.at);
|
|
145
|
+
}
|
|
146
|
+
return { kind: "binary", op: tok.v, left, right };
|
|
147
|
+
};
|
|
148
|
+
const additive = (depth) => {
|
|
149
|
+
let left = multiplicative(depth);
|
|
150
|
+
for (let tok = peek(); tok.t === "op" && (tok.v === "+" || tok.v === "-"); tok = peek()) {
|
|
151
|
+
pos++;
|
|
152
|
+
left = { kind: "binary", op: tok.v, left, right: multiplicative(depth) };
|
|
153
|
+
}
|
|
154
|
+
return left;
|
|
155
|
+
};
|
|
156
|
+
const multiplicative = (depth) => {
|
|
157
|
+
let left = unary(depth);
|
|
158
|
+
for (let tok = peek(); tok.t === "op" && (tok.v === "*" || tok.v === "/"); tok = peek()) {
|
|
159
|
+
pos++;
|
|
160
|
+
left = { kind: "binary", op: tok.v, left, right: unary(depth) };
|
|
161
|
+
}
|
|
162
|
+
return left;
|
|
163
|
+
};
|
|
164
|
+
const unary = (depth) => {
|
|
165
|
+
guard(depth);
|
|
166
|
+
if (isOp("-")) {
|
|
167
|
+
pos++;
|
|
168
|
+
return { kind: "unary", op: "-", arg: unary(depth + 1) };
|
|
169
|
+
}
|
|
170
|
+
if (isOp("+")) {
|
|
171
|
+
pos++;
|
|
172
|
+
return unary(depth + 1);
|
|
173
|
+
}
|
|
174
|
+
return primary(depth);
|
|
175
|
+
};
|
|
176
|
+
const primary = (depth) => {
|
|
177
|
+
const tok = peek();
|
|
178
|
+
if (tok.t === "num") {
|
|
179
|
+
pos++;
|
|
180
|
+
return { kind: "number", value: tok.v };
|
|
181
|
+
}
|
|
182
|
+
if (tok.t === "str") {
|
|
183
|
+
pos++;
|
|
184
|
+
return { kind: "text", value: tok.v };
|
|
185
|
+
}
|
|
186
|
+
if (tok.t === "op" && tok.v === "(") {
|
|
187
|
+
pos++;
|
|
188
|
+
const inner = ternary(depth + 1);
|
|
189
|
+
expect(")", "A `(` has no matching `)`.");
|
|
190
|
+
return inner;
|
|
191
|
+
}
|
|
192
|
+
if (tok.t === "id") {
|
|
193
|
+
pos++;
|
|
194
|
+
if (isOp("(")) {
|
|
195
|
+
if (!FORMULA_FUNCTIONS.includes(tok.v)) {
|
|
196
|
+
throw new FormulaError(`There is no function "${tok.v}"; use ${FORMULA_FUNCTIONS.join(", ")}.`, tok.at);
|
|
197
|
+
}
|
|
198
|
+
pos++;
|
|
199
|
+
const args = [];
|
|
200
|
+
if (!isOp(")")) {
|
|
201
|
+
args.push(ternary(depth + 1));
|
|
202
|
+
while (isOp(",")) {
|
|
203
|
+
pos++;
|
|
204
|
+
args.push(ternary(depth + 1));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
expect(")", `"${tok.v}(" has no matching ")".`);
|
|
208
|
+
if (!args.length) throw new FormulaError(`${tok.v}() needs something to work on.`, tok.at);
|
|
209
|
+
return { kind: "call", fn: tok.v, args };
|
|
210
|
+
}
|
|
211
|
+
if (isOp(".")) {
|
|
212
|
+
pos++;
|
|
213
|
+
const column = peek();
|
|
214
|
+
if (column.t !== "id") throw new FormulaError(`Name a column after "${tok.v}.".`, column.at);
|
|
215
|
+
pos++;
|
|
216
|
+
refs.push({ key: tok.v, column: column.v });
|
|
217
|
+
return { kind: "ref", key: tok.v, column: column.v, at: tok.at };
|
|
218
|
+
}
|
|
219
|
+
refs.push({ key: tok.v });
|
|
220
|
+
return { kind: "ref", key: tok.v, at: tok.at };
|
|
221
|
+
}
|
|
222
|
+
throw new FormulaError(`Expected a number, an answer or "(", but ${describe(tok)}.`, tok.at);
|
|
223
|
+
};
|
|
224
|
+
try {
|
|
225
|
+
const ast = ternary(0);
|
|
226
|
+
const rest = peek();
|
|
227
|
+
if (rest.t !== "end") throw new FormulaError(`Expected an operator such as + or *, but ${describe(rest)}.`, rest.at);
|
|
228
|
+
return { ok: true, ast, refs };
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (e instanceof FormulaError) return { ok: false, message: e.message, at: e.at };
|
|
231
|
+
return { ok: false, message: "The formula is nested too deeply.", at: 0 };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function formulaRefs(source) {
|
|
235
|
+
const parsed = parseFormula(source);
|
|
236
|
+
if (!parsed.ok) return [];
|
|
237
|
+
const seen = /* @__PURE__ */ new Set();
|
|
238
|
+
return parsed.refs.filter((r) => {
|
|
239
|
+
const id = `${r.key}\0${r.column ?? ""}`;
|
|
240
|
+
if (seen.has(id)) return false;
|
|
241
|
+
seen.add(id);
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function renameFormulaKey(source, from, to) {
|
|
246
|
+
let tokens;
|
|
247
|
+
try {
|
|
248
|
+
tokens = tokenize(source);
|
|
249
|
+
} catch {
|
|
250
|
+
return source;
|
|
251
|
+
}
|
|
252
|
+
let out = "";
|
|
253
|
+
let last = 0;
|
|
254
|
+
tokens.forEach((tok, i) => {
|
|
255
|
+
if (tok.t !== "id" || tok.v !== from) return;
|
|
256
|
+
const prev = tokens[i - 1];
|
|
257
|
+
const next = tokens[i + 1];
|
|
258
|
+
if (prev?.t === "op" && prev.v === ".") return;
|
|
259
|
+
if (next?.t === "op" && next.v === "(") return;
|
|
260
|
+
out += source.slice(last, tok.at) + to;
|
|
261
|
+
last = tok.at + from.length;
|
|
262
|
+
});
|
|
263
|
+
return out + source.slice(last);
|
|
264
|
+
}
|
|
265
|
+
var NUMERIC = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
266
|
+
function formulaNumber(v) {
|
|
267
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
268
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
269
|
+
if (typeof v === "string") {
|
|
270
|
+
const t = v.trim().replace(/,/g, "");
|
|
271
|
+
return t.length <= 64 && NUMERIC.test(t) ? finite(Number(t)) : null;
|
|
272
|
+
}
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
var finite = (n) => Number.isFinite(n) ? n : null;
|
|
276
|
+
var scalar = (v) => Array.isArray(v) ? null : v;
|
|
277
|
+
function truthy(v) {
|
|
278
|
+
if (v === null) return false;
|
|
279
|
+
if (typeof v === "boolean") return v;
|
|
280
|
+
if (typeof v === "number") return v !== 0;
|
|
281
|
+
if (typeof v === "string") return v.trim() !== "";
|
|
282
|
+
return v.length > 0;
|
|
283
|
+
}
|
|
284
|
+
function numbersIn(values, out = []) {
|
|
285
|
+
for (const v of values) {
|
|
286
|
+
if (Array.isArray(v)) numbersIn(v, out);
|
|
287
|
+
else {
|
|
288
|
+
const n = formulaNumber(v);
|
|
289
|
+
if (n !== null) out.push(n);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
function compare(op, a, b) {
|
|
295
|
+
const blank2 = (v) => v === null || Array.isArray(v) || typeof v === "string" && v.trim() === "";
|
|
296
|
+
if (blank2(a) || blank2(b)) return op === "!=";
|
|
297
|
+
const x = formulaNumber(a);
|
|
298
|
+
const y = formulaNumber(b);
|
|
299
|
+
if (x !== null && y !== null) {
|
|
300
|
+
if (op === "==") return x === y;
|
|
301
|
+
if (op === "!=") return x !== y;
|
|
302
|
+
if (op === "<") return x < y;
|
|
303
|
+
if (op === "<=") return x <= y;
|
|
304
|
+
if (op === ">") return x > y;
|
|
305
|
+
return x >= y;
|
|
306
|
+
}
|
|
307
|
+
if (op === "==") return String(a) === String(b);
|
|
308
|
+
if (op === "!=") return String(a) !== String(b);
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
function evaluateFormula(ast, read) {
|
|
312
|
+
const run = (node) => {
|
|
313
|
+
switch (node.kind) {
|
|
314
|
+
case "number":
|
|
315
|
+
case "text":
|
|
316
|
+
return node.value;
|
|
317
|
+
case "ref":
|
|
318
|
+
return read(node.column !== void 0 ? { key: node.key, column: node.column } : { key: node.key });
|
|
319
|
+
case "unary": {
|
|
320
|
+
const n = formulaNumber(scalar(run(node.arg)));
|
|
321
|
+
return n === null ? null : -n;
|
|
322
|
+
}
|
|
323
|
+
case "if":
|
|
324
|
+
return truthy(run(node.test)) ? run(node.then) : run(node.else);
|
|
325
|
+
case "call": {
|
|
326
|
+
const nums = numbersIn(node.args.map(run));
|
|
327
|
+
const total = nums.reduce((a, b) => a + b, 0);
|
|
328
|
+
if (node.fn === "sum") return finite(total);
|
|
329
|
+
if (!nums.length) return null;
|
|
330
|
+
if (node.fn === "avg") return finite(total / nums.length);
|
|
331
|
+
return nums.reduce((a, b) => node.fn === "min" ? Math.min(a, b) : Math.max(a, b));
|
|
332
|
+
}
|
|
333
|
+
case "binary": {
|
|
334
|
+
const a = run(node.left);
|
|
335
|
+
const b = run(node.right);
|
|
336
|
+
if (node.op === "+" || node.op === "-" || node.op === "*" || node.op === "/") {
|
|
337
|
+
const x = formulaNumber(scalar(a));
|
|
338
|
+
const y = formulaNumber(scalar(b));
|
|
339
|
+
if (x === null || y === null) return null;
|
|
340
|
+
if (node.op === "+") return finite(x + y);
|
|
341
|
+
if (node.op === "-") return finite(x - y);
|
|
342
|
+
if (node.op === "*") return finite(x * y);
|
|
343
|
+
return y === 0 ? null : finite(x / y);
|
|
344
|
+
}
|
|
345
|
+
return compare(node.op, a, b);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
try {
|
|
350
|
+
return run(ast);
|
|
351
|
+
} catch {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/calculated.ts
|
|
357
|
+
var CALCULATED_TYPE = "calculated";
|
|
358
|
+
var COMPUTED_FIELD_TYPES = [CALCULATED_TYPE];
|
|
359
|
+
var MAX_CALCULATED_DECIMALS = 10;
|
|
360
|
+
function calculatedProps(field2) {
|
|
361
|
+
const p = field2.props ?? {};
|
|
362
|
+
const out = { formula: typeof p.formula === "string" ? p.formula : "" };
|
|
363
|
+
if (p.format === "number" || p.format === "percent" || p.format === "text") out.format = p.format;
|
|
364
|
+
if (typeof p.decimals === "number" && Number.isInteger(p.decimals) && p.decimals >= 0 && p.decimals <= MAX_CALCULATED_DECIMALS) {
|
|
365
|
+
out.decimals = p.decimals;
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
var isCalculated = (field2) => field2?.type === CALCULATED_TYPE;
|
|
370
|
+
var round = (n, decimals) => {
|
|
371
|
+
if (decimals === void 0) return n;
|
|
372
|
+
const r = Number(n.toFixed(decimals));
|
|
373
|
+
return Object.is(r, -0) ? 0 : r;
|
|
374
|
+
};
|
|
375
|
+
function calculatedValue(result, field2) {
|
|
376
|
+
const { format, decimals } = calculatedProps(field2);
|
|
377
|
+
if (result === null || Array.isArray(result)) return void 0;
|
|
378
|
+
if (format === "number" || format === "percent") {
|
|
379
|
+
const n = formulaNumber(result);
|
|
380
|
+
return n === null ? void 0 : round(n, decimals);
|
|
381
|
+
}
|
|
382
|
+
if (format === "text") {
|
|
383
|
+
if (typeof result === "number") return decimals === void 0 ? String(result) : result.toFixed(decimals);
|
|
384
|
+
return typeof result === "boolean" ? result ? "Yes" : "No" : result;
|
|
385
|
+
}
|
|
386
|
+
return typeof result === "number" ? round(result, decimals) : result;
|
|
387
|
+
}
|
|
388
|
+
function formatCalculated(value, field2) {
|
|
389
|
+
if (value === void 0 || value === null) return "";
|
|
390
|
+
const { format, decimals } = calculatedProps(field2);
|
|
391
|
+
const fixed = (n) => decimals === void 0 ? String(Number(n.toFixed(6))) : n.toFixed(decimals);
|
|
392
|
+
if (typeof value === "number") return format === "percent" ? `${fixed(value * 100)}%` : fixed(value);
|
|
393
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
394
|
+
return typeof value === "string" ? value : "";
|
|
395
|
+
}
|
|
396
|
+
var MAX_FORMULA_ITEMS = 1e4;
|
|
397
|
+
function formulaInput(value, column) {
|
|
398
|
+
if (value === void 0 || value === null) return null;
|
|
399
|
+
if (Array.isArray(value) && value.length > MAX_FORMULA_ITEMS) return null;
|
|
400
|
+
if (column !== void 0) {
|
|
401
|
+
if (!Array.isArray(value)) return null;
|
|
402
|
+
return value.flatMap((row) => {
|
|
403
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) return [];
|
|
404
|
+
const cell = row[column];
|
|
405
|
+
return typeof cell === "string" || typeof cell === "number" ? [cell] : [];
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (Array.isArray(value)) {
|
|
409
|
+
return value.flatMap((v) => typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? [v] : []);
|
|
410
|
+
}
|
|
411
|
+
return typeof value === "object" ? null : value;
|
|
412
|
+
}
|
|
413
|
+
var UNREAD = /* @__PURE__ */ Symbol("unread");
|
|
414
|
+
function calculator(definition) {
|
|
415
|
+
const compiled = [];
|
|
416
|
+
const idByKey = /* @__PURE__ */ new Map();
|
|
417
|
+
for (const block of definition.blocks) {
|
|
418
|
+
if (block.kind !== "field") continue;
|
|
419
|
+
idByKey.set(block.key, block.id);
|
|
420
|
+
}
|
|
421
|
+
for (const block of definition.blocks) {
|
|
422
|
+
if (block.kind !== "field" || !isCalculated(block)) continue;
|
|
423
|
+
const parsed = parseFormula(calculatedProps(block).formula);
|
|
424
|
+
const reads = parsed.ok ? parsed.refs.map((r) => idByKey.get(r.key)) : [];
|
|
425
|
+
compiled.push({ field: block, ast: parsed.ok ? parsed.ast : null, reads, last: null });
|
|
426
|
+
}
|
|
427
|
+
const withCalculated = (values, path) => {
|
|
428
|
+
if (!compiled.length) return values;
|
|
429
|
+
const visible = new Set(path);
|
|
430
|
+
let next = values;
|
|
431
|
+
const input = (id) => id === void 0 || !visible.has(id) || !Object.prototype.hasOwnProperty.call(next, id) ? UNREAD : next[id];
|
|
432
|
+
for (const entry of compiled) {
|
|
433
|
+
const { field: field2, ast } = entry;
|
|
434
|
+
const inputs = entry.reads.map(input);
|
|
435
|
+
let value;
|
|
436
|
+
if (entry.last && entry.last.inputs.length === inputs.length && entry.last.inputs.every((v, i) => v === inputs[i])) {
|
|
437
|
+
value = entry.last.value;
|
|
438
|
+
} else {
|
|
439
|
+
const read = (ref) => {
|
|
440
|
+
const raw = input(idByKey.get(ref.key));
|
|
441
|
+
return raw === UNREAD ? null : formulaInput(raw, ref.column);
|
|
442
|
+
};
|
|
443
|
+
value = ast ? calculatedValue(evaluateFormula(ast, read), field2) : void 0;
|
|
444
|
+
entry.last = { inputs, value };
|
|
445
|
+
}
|
|
446
|
+
const current = Object.prototype.hasOwnProperty.call(next, field2.id) ? next[field2.id] : void 0;
|
|
447
|
+
if (current !== value) next = { ...next, [field2.id]: value };
|
|
448
|
+
}
|
|
449
|
+
return next;
|
|
450
|
+
};
|
|
451
|
+
return { active: compiled.length > 0, withCalculated };
|
|
452
|
+
}
|
|
453
|
+
var numberOps = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
|
|
454
|
+
var calculatedFieldType = {
|
|
455
|
+
type: CALCULATED_TYPE,
|
|
456
|
+
label: "Calculated",
|
|
457
|
+
category: "advanced",
|
|
458
|
+
value: { json: true },
|
|
459
|
+
operators: numberOps,
|
|
460
|
+
a11y: { semantics: "control" },
|
|
461
|
+
// The engine works the value out; any number, text or true / false it made is valid.
|
|
462
|
+
validate: (value) => typeof value === "number" || typeof value === "string" || typeof value === "boolean" ? null : "This value is worked out from other answers.",
|
|
463
|
+
summarize: (value, field2) => formatCalculated(value, field2)
|
|
464
|
+
};
|
|
465
|
+
|
|
31
466
|
// src/events.ts
|
|
32
467
|
function isPlainObject(value) {
|
|
33
468
|
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
@@ -322,7 +757,7 @@ function intRange(field2, min, maxDefault) {
|
|
|
322
757
|
const max = typeof field2.props?.max === "number" ? field2.props.max : maxDefault;
|
|
323
758
|
return { min, max };
|
|
324
759
|
}
|
|
325
|
-
var
|
|
760
|
+
var numberOps2 = ["eq", "neq", "lt", "lte", "gt", "gte", "in", "answered", "empty"];
|
|
326
761
|
var textOps = ["eq", "neq", "contains", "answered", "empty"];
|
|
327
762
|
function maxLength(field2, fallback) {
|
|
328
763
|
return typeof field2.props?.maxLength === "number" ? field2.props.maxLength : fallback;
|
|
@@ -400,23 +835,30 @@ var builtins = [
|
|
|
400
835
|
label: "Rating",
|
|
401
836
|
category: "scale",
|
|
402
837
|
value: { json: true },
|
|
403
|
-
operators:
|
|
838
|
+
operators: numberOps2,
|
|
404
839
|
a11y: { semantics: "group" },
|
|
405
840
|
validate: (value, field2) => {
|
|
406
841
|
const { min, max } = intRange(field2, 1, 5);
|
|
407
842
|
return Number.isInteger(value) && value >= min && value <= max ? null : `Choose a rating from ${min} to ${max}.`;
|
|
408
843
|
},
|
|
409
|
-
summarize: (value, field2) =>
|
|
844
|
+
summarize: (value, field2) => {
|
|
845
|
+
const { max } = intRange(field2, 1, 5);
|
|
846
|
+
const end = scaleEndLabel(field2, value, 1, max);
|
|
847
|
+
return end ? `${value}/${max} (${end})` : `${value}/${max}`;
|
|
848
|
+
}
|
|
410
849
|
},
|
|
411
850
|
{
|
|
412
851
|
type: "nps",
|
|
413
852
|
label: "Net Promoter Score",
|
|
414
853
|
category: "scale",
|
|
415
854
|
value: { json: true },
|
|
416
|
-
operators:
|
|
855
|
+
operators: numberOps2,
|
|
417
856
|
a11y: { semantics: "group" },
|
|
418
857
|
validate: (value) => Number.isInteger(value) && value >= 0 && value <= 10 ? null : "Choose a score from 0 to 10.",
|
|
419
|
-
summarize: (value) =>
|
|
858
|
+
summarize: (value, field2) => {
|
|
859
|
+
const end = scaleEndLabel(field2, value, 0, 10);
|
|
860
|
+
return end ? `${value}/10 (${end})` : `${value}/10`;
|
|
861
|
+
}
|
|
420
862
|
},
|
|
421
863
|
{
|
|
422
864
|
type: "single-choice",
|
|
@@ -450,9 +892,125 @@ var builtins = [
|
|
|
450
892
|
a11y: { semantics: "group" },
|
|
451
893
|
validate: (value) => typeof value === "boolean" ? null : "Choose yes or no.",
|
|
452
894
|
summarize: (value) => value ? "Yes" : "No"
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
type: "dropdown",
|
|
898
|
+
label: "Dropdown",
|
|
899
|
+
category: "choice",
|
|
900
|
+
value: { json: true },
|
|
901
|
+
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
902
|
+
a11y: { semantics: "control" },
|
|
903
|
+
validate: (value, field2) => typeof value === "string" && choiceOptions(field2).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
904
|
+
summarize: (value, field2) => optionLabel(field2, String(value))
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
type: "number",
|
|
908
|
+
label: "Number",
|
|
909
|
+
category: "text",
|
|
910
|
+
value: { json: true },
|
|
911
|
+
operators: numberOps2,
|
|
912
|
+
a11y: { semantics: "control", inputMode: "decimal" },
|
|
913
|
+
validate: (value, field2) => {
|
|
914
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return "Enter a number.";
|
|
915
|
+
const { min, max, step } = numberLimits(field2);
|
|
916
|
+
if (min !== void 0 && max !== void 0 && (value < min || value > max)) {
|
|
917
|
+
return `Enter a number from ${min} to ${max}.`;
|
|
918
|
+
}
|
|
919
|
+
if (min !== void 0 && value < min) return `Enter ${min} or more.`;
|
|
920
|
+
if (max !== void 0 && value > max) return `Enter ${max} or less.`;
|
|
921
|
+
if (step !== void 0 && !onStep(value, min ?? 0, step)) {
|
|
922
|
+
return step === 1 && Number.isInteger(min ?? 0) ? "Enter a whole number." : `Enter a number in steps of ${step}.`;
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
},
|
|
926
|
+
summarize: (value) => String(value),
|
|
927
|
+
samples: (field2) => {
|
|
928
|
+
const { min, max } = numberLimits(field2);
|
|
929
|
+
return [min, max, 0, 1].filter((v) => v !== void 0);
|
|
930
|
+
}
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
type: "date",
|
|
934
|
+
label: "Date",
|
|
935
|
+
category: "date",
|
|
936
|
+
value: { json: true },
|
|
937
|
+
operators: ["eq", "neq", "answered", "empty"],
|
|
938
|
+
a11y: { semantics: "control" },
|
|
939
|
+
validate: (value, field2) => {
|
|
940
|
+
if (typeof value !== "string" || !isIsoDate(value.trim())) return "Enter a date like 2026-10-22.";
|
|
941
|
+
const date = value.trim();
|
|
942
|
+
const { min, max } = dateLimits(field2);
|
|
943
|
+
if (min && max && (date < min || date > max)) return `Choose a date from ${min} to ${max}.`;
|
|
944
|
+
if (min && date < min) return `Choose ${min} or later.`;
|
|
945
|
+
if (max && date > max) return `Choose ${max} or earlier.`;
|
|
946
|
+
return null;
|
|
947
|
+
},
|
|
948
|
+
normalize: (value) => typeof value === "string" ? value.trim() : value,
|
|
949
|
+
samples: (field2) => {
|
|
950
|
+
const { min, max } = dateLimits(field2);
|
|
951
|
+
return [min ?? max ?? "2000-01-01"];
|
|
952
|
+
}
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
type: "consent",
|
|
956
|
+
label: "Consent checkbox",
|
|
957
|
+
category: "choice",
|
|
958
|
+
value: { json: true },
|
|
959
|
+
operators: ["answered", "empty"],
|
|
960
|
+
a11y: { semantics: "group" },
|
|
961
|
+
// Unchecked is unanswered, so a required consent must be checked.
|
|
962
|
+
isEmpty: (value) => value === void 0 || value === null || value === false,
|
|
963
|
+
validate: (value) => value === true ? null : "Check the box to agree.",
|
|
964
|
+
summarize: () => "Agreed",
|
|
965
|
+
samples: () => [true]
|
|
453
966
|
}
|
|
454
967
|
];
|
|
455
|
-
|
|
968
|
+
function scaleLabels(field2) {
|
|
969
|
+
const out = {};
|
|
970
|
+
const low = field2.props?.lowLabel;
|
|
971
|
+
const high = field2.props?.highLabel;
|
|
972
|
+
if (typeof low === "string" && low.trim()) out.low = low.trim();
|
|
973
|
+
if (typeof high === "string" && high.trim()) out.high = high.trim();
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
976
|
+
function ratingDisplay(field2) {
|
|
977
|
+
return field2.props?.display === "stars" ? "stars" : "numbers";
|
|
978
|
+
}
|
|
979
|
+
function scaleEndLabel(field2, value, min, max) {
|
|
980
|
+
const { low, high } = scaleLabels(field2);
|
|
981
|
+
return value === min ? low : value === max ? high : void 0;
|
|
982
|
+
}
|
|
983
|
+
var finite2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
984
|
+
function numberLimits(field2) {
|
|
985
|
+
const out = {};
|
|
986
|
+
const min = finite2(field2.props?.min);
|
|
987
|
+
const max = finite2(field2.props?.max);
|
|
988
|
+
const step = finite2(field2.props?.step);
|
|
989
|
+
if (min !== void 0) out.min = min;
|
|
990
|
+
if (max !== void 0) out.max = max;
|
|
991
|
+
if (step !== void 0 && step > 0) out.step = step;
|
|
992
|
+
return out;
|
|
993
|
+
}
|
|
994
|
+
function onStep(value, base, step) {
|
|
995
|
+
const n = (value - base) / step;
|
|
996
|
+
return Math.abs(n - Math.round(n)) < 1e-9 * Math.max(1, Math.abs(n));
|
|
997
|
+
}
|
|
998
|
+
function isIsoDate(value) {
|
|
999
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
|
|
1000
|
+
const [y, m, d] = value.split("-").map(Number);
|
|
1001
|
+
if (y < 1 || m < 1 || m > 12 || d < 1) return false;
|
|
1002
|
+
const days = [31, y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
1003
|
+
return d <= days[m - 1];
|
|
1004
|
+
}
|
|
1005
|
+
function dateLimits(field2) {
|
|
1006
|
+
const out = {};
|
|
1007
|
+
const min = field2.props?.min;
|
|
1008
|
+
const max = field2.props?.max;
|
|
1009
|
+
if (typeof min === "string" && isIsoDate(min)) out.min = min;
|
|
1010
|
+
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
1011
|
+
return out;
|
|
1012
|
+
}
|
|
1013
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
|
|
456
1014
|
|
|
457
1015
|
// src/logic.ts
|
|
458
1016
|
function own(record, key) {
|
|
@@ -624,8 +1182,15 @@ var LITERAL_TYPE = {
|
|
|
624
1182
|
"single-choice": "string",
|
|
625
1183
|
"multi-choice": "string",
|
|
626
1184
|
"yes-no": "boolean",
|
|
627
|
-
list: "string"
|
|
1185
|
+
list: "string",
|
|
1186
|
+
dropdown: "string",
|
|
1187
|
+
number: "number",
|
|
1188
|
+
date: "string",
|
|
1189
|
+
consent: "boolean",
|
|
1190
|
+
// A calculated field holds a number unless its format says text; see `literalType`.
|
|
1191
|
+
calculated: "number"
|
|
628
1192
|
};
|
|
1193
|
+
var CHOICE_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
629
1194
|
var ORDERING = /* @__PURE__ */ new Set(["lt", "lte", "gt", "gte"]);
|
|
630
1195
|
var OPERATORS = /* @__PURE__ */ new Set([
|
|
631
1196
|
"eq",
|
|
@@ -705,7 +1270,44 @@ function parseForm(input) {
|
|
|
705
1270
|
} else if (type === "text-highlights") {
|
|
706
1271
|
optionalText(props2.text, `${path}.text`);
|
|
707
1272
|
optionalInt(props2.maxHighlights, `${path}.maxHighlights`, 1);
|
|
1273
|
+
} else if (type === CALCULATED_TYPE) {
|
|
1274
|
+
if (props2.format !== void 0 && props2.format !== "number" && props2.format !== "percent" && props2.format !== "text") {
|
|
1275
|
+
issue(`${path}.format`, "Must be number, percent or text.");
|
|
1276
|
+
}
|
|
1277
|
+
if (props2.decimals !== void 0 && !(isInt(props2.decimals) && props2.decimals >= 0 && props2.decimals <= MAX_CALCULATED_DECIMALS)) {
|
|
1278
|
+
issue(`${path}.decimals`, `Must be a whole number from 0 to ${MAX_CALCULATED_DECIMALS}.`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
1283
|
+
const fieldProps = (type, props2, path) => {
|
|
1284
|
+
if (type === "rating" || type === "nps") {
|
|
1285
|
+
optionalText(props2.lowLabel, `${path}.lowLabel`);
|
|
1286
|
+
optionalText(props2.highLabel, `${path}.highLabel`);
|
|
1287
|
+
}
|
|
1288
|
+
if (type === "rating" && props2.display !== void 0 && props2.display !== "numbers" && props2.display !== "stars") {
|
|
1289
|
+
issue(`${path}.display`, "Must be numbers or stars.");
|
|
1290
|
+
}
|
|
1291
|
+
if (type === "number") {
|
|
1292
|
+
for (const k of ["min", "max", "step"]) {
|
|
1293
|
+
if (props2[k] !== void 0 && !isFiniteNumber(props2[k])) issue(`${path}.${k}`, "Must be a number.");
|
|
1294
|
+
}
|
|
1295
|
+
if (isFiniteNumber(props2.step) && props2.step <= 0) issue(`${path}.step`, "Must be above 0.");
|
|
1296
|
+
if (isFiniteNumber(props2.min) && isFiniteNumber(props2.max) && props2.min > props2.max) {
|
|
1297
|
+
issue(`${path}.min`, "Can't be more than the largest number.");
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (type === "date") {
|
|
1301
|
+
for (const k of ["min", "max"]) {
|
|
1302
|
+
if (props2[k] !== void 0 && !(typeof props2[k] === "string" && isIsoDate(props2[k]))) {
|
|
1303
|
+
issue(`${path}.${k}`, "Must be a date like 2026-10-22.");
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (typeof props2.min === "string" && typeof props2.max === "string" && isIsoDate(props2.min) && isIsoDate(props2.max) && props2.min > props2.max) {
|
|
1307
|
+
issue(`${path}.min`, "Can't be after the latest date.");
|
|
1308
|
+
}
|
|
708
1309
|
}
|
|
1310
|
+
if (type === "consent") optionalText(props2.text, `${path}.text`);
|
|
709
1311
|
};
|
|
710
1312
|
if (!isObject2(input)) {
|
|
711
1313
|
issue("", "A form definition must be an object.");
|
|
@@ -740,7 +1342,9 @@ function parseForm(input) {
|
|
|
740
1342
|
const keys = /* @__PURE__ */ new Map();
|
|
741
1343
|
const fieldTypes = /* @__PURE__ */ new Map();
|
|
742
1344
|
const order = /* @__PURE__ */ new Map();
|
|
1345
|
+
const tableColumnKeys = /* @__PURE__ */ new Map();
|
|
743
1346
|
const choices = /* @__PURE__ */ new Map();
|
|
1347
|
+
const calculatedLiteral = /* @__PURE__ */ new Map();
|
|
744
1348
|
input.blocks.forEach((block, i) => {
|
|
745
1349
|
const path = `blocks[${i}]`;
|
|
746
1350
|
if (!isObject2(block) || typeof block.id !== "string" || !block.id) {
|
|
@@ -787,13 +1391,27 @@ function parseForm(input) {
|
|
|
787
1391
|
if (maxLength2 !== void 0 && !(isInt(maxLength2) && maxLength2 > 0)) {
|
|
788
1392
|
issue(`${path}.props.maxLength`, "Must be a whole number above 0.");
|
|
789
1393
|
}
|
|
790
|
-
|
|
1394
|
+
}
|
|
1395
|
+
if (typeof block.type === "string" && (block.props === void 0 || isObject2(block.props))) {
|
|
1396
|
+
structuredProps(block.type, block.props ?? {}, `${path}.props`);
|
|
1397
|
+
fieldProps(block.type, block.props ?? {}, `${path}.props`);
|
|
791
1398
|
}
|
|
792
1399
|
if (validKey) {
|
|
793
1400
|
if (keys.has(block.key)) issue(`${path}.key`, `Duplicate key "${String(block.key)}".`);
|
|
794
1401
|
keys.set(block.key, block.id);
|
|
795
1402
|
}
|
|
796
1403
|
fieldTypes.set(block.id, typeof block.type === "string" ? block.type : "");
|
|
1404
|
+
if (block.type === CALCULATED_TYPE) {
|
|
1405
|
+
const format = isObject2(block.props) ? block.props.format : void 0;
|
|
1406
|
+
if (format === "text") calculatedLiteral.set(block.id, "string");
|
|
1407
|
+
else if (format !== "number" && format !== "percent") calculatedLiteral.set(block.id, void 0);
|
|
1408
|
+
}
|
|
1409
|
+
if (block.type === "table" && isObject2(block.props) && Array.isArray(block.props.columns)) {
|
|
1410
|
+
tableColumnKeys.set(
|
|
1411
|
+
block.id,
|
|
1412
|
+
new Set(block.props.columns.flatMap((c) => isObject2(c) && typeof c.key === "string" ? [c.key] : []))
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
797
1415
|
} else if (block.kind === "content") {
|
|
798
1416
|
if (block.type !== "heading" && block.type !== "text") issue(`${path}.type`, "Must be heading or text.");
|
|
799
1417
|
text(block.text, `${path}.text`);
|
|
@@ -803,6 +1421,38 @@ function parseForm(input) {
|
|
|
803
1421
|
issue(`${path}.kind`, "Unknown block kind.");
|
|
804
1422
|
}
|
|
805
1423
|
});
|
|
1424
|
+
input.blocks.forEach((block, i) => {
|
|
1425
|
+
if (!isObject2(block) || block.kind !== "field" || block.type !== CALCULATED_TYPE) return;
|
|
1426
|
+
const path = `blocks[${i}].props.formula`;
|
|
1427
|
+
if (block.required === true) {
|
|
1428
|
+
warnings.push({ path: `blocks[${i}].required`, message: "A calculated field is worked out, not answered, so it is never required." });
|
|
1429
|
+
}
|
|
1430
|
+
const formula = isObject2(block.props) ? block.props.formula : void 0;
|
|
1431
|
+
if (typeof formula !== "string" || !formula.trim()) return issue(path, "A calculated field needs a formula.");
|
|
1432
|
+
const parsed = parseFormula(formula);
|
|
1433
|
+
if (!parsed.ok) return issue(path, `${parsed.message} (at character ${parsed.at + 1})`);
|
|
1434
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1435
|
+
for (const ref of parsed.refs) {
|
|
1436
|
+
const name2 = ref.column === void 0 ? ref.key : `${ref.key}.${ref.column}`;
|
|
1437
|
+
if (reported.has(name2)) continue;
|
|
1438
|
+
reported.add(name2);
|
|
1439
|
+
const target = keys.get(ref.key);
|
|
1440
|
+
const targetType = target === void 0 ? void 0 : fieldTypes.get(target);
|
|
1441
|
+
const at = target === void 0 ? void 0 : order.get(target);
|
|
1442
|
+
if (target === void 0 || targetType === void 0) issue(path, `The formula uses "${ref.key}", which is not the answer name of a question in this form.`);
|
|
1443
|
+
else if (target === block.id) issue(path, "The formula uses this field's own value.");
|
|
1444
|
+
else if (targetType === CALCULATED_TYPE && (at ?? 0) > i) {
|
|
1445
|
+
issue(path, `The formula uses "${ref.key}", a calculated field that comes after this one; move it earlier.`);
|
|
1446
|
+
} else if (targetType === "table") {
|
|
1447
|
+
const columns = tableColumnKeys.get(target);
|
|
1448
|
+
if (ref.column === void 0) issue(path, `"${ref.key}" is a table: name one of its columns, like ${ref.key}.${[...columns ?? []][0] ?? "column"}.`);
|
|
1449
|
+
else if (columns && !columns.has(ref.column)) issue(path, `The table "${ref.key}" has no column "${ref.column}".`);
|
|
1450
|
+
} else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
|
|
1451
|
+
else if (targetType === "media-notes" || targetType === "text-highlights") {
|
|
1452
|
+
warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
806
1456
|
const hiddenNames = /* @__PURE__ */ new Set();
|
|
807
1457
|
const hiddenTypes = /* @__PURE__ */ new Map();
|
|
808
1458
|
if (input.hidden !== void 0 && !Array.isArray(input.hidden)) issue("hidden", "Must be an array.");
|
|
@@ -886,8 +1536,8 @@ function parseForm(input) {
|
|
|
886
1536
|
if (typeof left.field === "string" && fieldTypes.has(left.field)) {
|
|
887
1537
|
const fieldType = fieldTypes.get(left.field);
|
|
888
1538
|
what = `"${keyOf(left.field)}"`;
|
|
889
|
-
type = LITERAL_TYPE[fieldType];
|
|
890
|
-
if (fieldType
|
|
1539
|
+
type = calculatedLiteral.has(left.field) ? calculatedLiteral.get(left.field) : LITERAL_TYPE[fieldType];
|
|
1540
|
+
if (CHOICE_TYPES.has(fieldType)) options = choices.get(left.field);
|
|
891
1541
|
} else if (typeof left.hidden === "string" && hiddenTypes.has(left.hidden)) {
|
|
892
1542
|
what = `hidden input "${left.hidden}"`;
|
|
893
1543
|
type = hiddenTypes.get(left.hidden);
|
|
@@ -1033,11 +1683,33 @@ var f = {
|
|
|
1033
1683
|
longText: (key, o) => field("long-text", key, o, o.maxLength ? { maxLength: o.maxLength } : void 0),
|
|
1034
1684
|
email: (key, o) => field("email", key, o),
|
|
1035
1685
|
phone: (key, o) => field("phone", key, o),
|
|
1036
|
-
|
|
1037
|
-
|
|
1686
|
+
/**
|
|
1687
|
+
* A 1-to-`max` scale. `lowLabel` and `highLabel` are optional words under
|
|
1688
|
+
* its ends ("Poor", "Great"); `display: "stars"` draws stars instead of numbers.
|
|
1689
|
+
*/
|
|
1690
|
+
rating: (key, o) => field(
|
|
1691
|
+
"rating",
|
|
1692
|
+
key,
|
|
1693
|
+
o,
|
|
1694
|
+
props({ max: o.max ?? 5, lowLabel: o.lowLabel || void 0, highLabel: o.highLabel || void 0, display: o.display })
|
|
1695
|
+
),
|
|
1696
|
+
/** A 0-to-10 Net Promoter Score. The end labels default to "Not likely" and "Very likely" in the renderer. */
|
|
1697
|
+
nps: (key, o) => field("nps", key, o, props({ lowLabel: o.lowLabel || void 0, highLabel: o.highLabel || void 0 })),
|
|
1038
1698
|
singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
|
|
1039
1699
|
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
|
|
1040
1700
|
yesNo: (key, o) => field("yes-no", key, o),
|
|
1701
|
+
/** One option from a compact menu: for a long list (a country, a class), where single choice would be a wall of buttons. */
|
|
1702
|
+
dropdown: (key, o) => field("dropdown", key, o, choiceProps(o.options)),
|
|
1703
|
+
/** A number, optionally between `min` and `max` and on a `step` (from `min`, else 0): `step: 1` for whole numbers. */
|
|
1704
|
+
number: (key, o) => field("number", key, o, props({ min: o.min, max: o.max, step: o.step })),
|
|
1705
|
+
/** A calendar day, stored as `YYYY-MM-DD`, optionally between `min` and `max` (also `YYYY-MM-DD`). */
|
|
1706
|
+
date: (key, o) => field("date", key, o, props({ min: o.min, max: o.max })),
|
|
1707
|
+
/**
|
|
1708
|
+
* A single checkbox to agree to something. `text` is the checkbox's own
|
|
1709
|
+
* words ("I agree to the terms"); the renderer defaults to "I agree".
|
|
1710
|
+
* Checked stores `true`; unchecked is unanswered, so `required` means it must be checked.
|
|
1711
|
+
*/
|
|
1712
|
+
consent: (key, o) => field("consent", key, o, props({ text: o.text || void 0 })),
|
|
1041
1713
|
/** Several short answers, one per line: goals, action items, three things that went well. */
|
|
1042
1714
|
list: (key, o) => field(
|
|
1043
1715
|
"list",
|
|
@@ -1045,6 +1717,12 @@ var f = {
|
|
|
1045
1717
|
o,
|
|
1046
1718
|
props({ min: o.min, max: o.max, itemMaxLength: o.itemMaxLength, placeholder: o.itemPlaceholder })
|
|
1047
1719
|
),
|
|
1720
|
+
/**
|
|
1721
|
+
* A value worked out from other answers and shown read-only, such as a
|
|
1722
|
+
* total or a score: `formula` names answers by key (`hours * rate`,
|
|
1723
|
+
* `sum(plan.cost)`). Stored with the answers; never required.
|
|
1724
|
+
*/
|
|
1725
|
+
calculated: (key, o) => field("calculated", key, o, props({ formula: o.formula, format: o.format, decimals: o.decimals })),
|
|
1048
1726
|
/**
|
|
1049
1727
|
* A small table each answer fills row by row, one answer per column: an
|
|
1050
1728
|
* action plan (What / Who / By when), pros and cons.
|
|
@@ -1104,9 +1782,9 @@ function effects(fromKey, condition) {
|
|
|
1104
1782
|
})
|
|
1105
1783
|
};
|
|
1106
1784
|
}
|
|
1107
|
-
var
|
|
1785
|
+
var compare2 = (key, op, value) => (r) => value === void 0 ? { op, left: { field: r(key) } } : { op, left: { field: r(key) }, right: { value } };
|
|
1108
1786
|
function when(key) {
|
|
1109
|
-
const on = (op, value) => effects(key,
|
|
1787
|
+
const on = (op, value) => effects(key, compare2(key, op, value));
|
|
1110
1788
|
return {
|
|
1111
1789
|
is: (value) => on("eq", value),
|
|
1112
1790
|
isNot: (value) => on("neq", value),
|
|
@@ -1234,7 +1912,7 @@ function runCustom(validator, value) {
|
|
|
1234
1912
|
}
|
|
1235
1913
|
function validateField(field2, value, options = {}) {
|
|
1236
1914
|
if (isEmptyValue(value, field2)) {
|
|
1237
|
-
return field2.required && options.requireAnswers !== false ? REQUIRED_MESSAGE : null;
|
|
1915
|
+
return field2.required && field2.type !== CALCULATED_TYPE && options.requireAnswers !== false ? REQUIRED_MESSAGE : null;
|
|
1238
1916
|
}
|
|
1239
1917
|
const manifest = getFieldType(field2.type);
|
|
1240
1918
|
if (!manifest) return `Unknown field type "${field2.type}".`;
|
|
@@ -1265,9 +1943,21 @@ function build(definition, options, replaying = false) {
|
|
|
1265
1943
|
if (!declared.has(name)) initialHidden[name] = value;
|
|
1266
1944
|
}
|
|
1267
1945
|
Object.assign(initialHidden, coerceHidden(definition, options.hidden).values);
|
|
1268
|
-
const
|
|
1946
|
+
const calc = calculator(definition);
|
|
1947
|
+
const settle = (values, hidden) => {
|
|
1948
|
+
let { path } = computePath(definition, scopeOf(values, hidden));
|
|
1949
|
+
if (!calc.active) return { values, path };
|
|
1950
|
+
for (let round2 = 0; round2 < 4; round2++) {
|
|
1951
|
+
const next = calc.withCalculated(values, path);
|
|
1952
|
+
if (next === values) break;
|
|
1953
|
+
values = next;
|
|
1954
|
+
path = computePath(definition, scopeOf(values, hidden)).path;
|
|
1955
|
+
}
|
|
1956
|
+
return { values, path };
|
|
1957
|
+
};
|
|
1958
|
+
const first = settle({}, initialHidden);
|
|
1269
1959
|
let state = {
|
|
1270
|
-
values:
|
|
1960
|
+
values: first.values,
|
|
1271
1961
|
hidden: initialHidden,
|
|
1272
1962
|
path: first.path,
|
|
1273
1963
|
current: first.path[0] ?? null,
|
|
@@ -1302,12 +1992,25 @@ function build(definition, options, replaying = false) {
|
|
|
1302
1992
|
}
|
|
1303
1993
|
return errors;
|
|
1304
1994
|
};
|
|
1995
|
+
let unsettled = false;
|
|
1996
|
+
const settleNow = () => {
|
|
1997
|
+
if (!unsettled) return;
|
|
1998
|
+
unsettled = false;
|
|
1999
|
+
const { values, path } = settle(state.values, state.hidden);
|
|
2000
|
+
state = { ...state, values, path };
|
|
2001
|
+
};
|
|
1305
2002
|
const answer = (id, value, event) => {
|
|
1306
|
-
if (state.status === "completed" || own(state.values, id) === value) return;
|
|
1307
|
-
const values = { ...state.values, [id]: value };
|
|
1308
|
-
const { path } = computePath(definition, scopeOf(values, state.hidden));
|
|
2003
|
+
if (state.status === "completed" || own(state.values, id) === value || isCalculated(fields.get(id))) return;
|
|
1309
2004
|
const errors = { ...state.errors };
|
|
1310
2005
|
delete errors[id];
|
|
2006
|
+
if (batch) {
|
|
2007
|
+
state.values = { ...state.values, [id]: value };
|
|
2008
|
+
state.errors = errors;
|
|
2009
|
+
log(event);
|
|
2010
|
+
unsettled = true;
|
|
2011
|
+
return;
|
|
2012
|
+
}
|
|
2013
|
+
const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
|
|
1311
2014
|
emit({ ...state, values, path, errors, events: log(event) });
|
|
1312
2015
|
};
|
|
1313
2016
|
const engine = {
|
|
@@ -1379,10 +2082,14 @@ function build(definition, options, replaying = false) {
|
|
|
1379
2082
|
}
|
|
1380
2083
|
};
|
|
1381
2084
|
if (options.initialValues) {
|
|
2085
|
+
const wasBatch = batch;
|
|
2086
|
+
batch = true;
|
|
1382
2087
|
for (const [id, value] of Object.entries(options.initialValues)) {
|
|
1383
2088
|
if (!fields.has(id) || value === void 0) continue;
|
|
1384
2089
|
answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
|
|
1385
2090
|
}
|
|
2091
|
+
settleNow();
|
|
2092
|
+
batch = wasBatch;
|
|
1386
2093
|
}
|
|
1387
2094
|
const apply = (event) => {
|
|
1388
2095
|
if (event.type === "answer") {
|
|
@@ -1390,11 +2097,19 @@ function build(definition, options, replaying = false) {
|
|
|
1390
2097
|
const { at: _at, ...logged } = event;
|
|
1391
2098
|
answer(event.field, event.value, logged);
|
|
1392
2099
|
}
|
|
1393
|
-
} else if (event.type === "next")
|
|
1394
|
-
|
|
1395
|
-
|
|
2100
|
+
} else if (event.type === "next") {
|
|
2101
|
+
settleNow();
|
|
2102
|
+
engine.next();
|
|
2103
|
+
} else if (event.type === "back") {
|
|
2104
|
+
settleNow();
|
|
2105
|
+
engine.back();
|
|
2106
|
+
} else if (event.type === "submit") {
|
|
2107
|
+
settleNow();
|
|
2108
|
+
engine.submit();
|
|
2109
|
+
}
|
|
1396
2110
|
};
|
|
1397
2111
|
const finish = () => {
|
|
2112
|
+
settleNow();
|
|
1398
2113
|
batch = false;
|
|
1399
2114
|
state = { ...state, events: [...state.events] };
|
|
1400
2115
|
};
|
|
@@ -1543,6 +2258,6 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
|
|
|
1543
2258
|
return `${left} ${op} ${right}`;
|
|
1544
2259
|
}
|
|
1545
2260
|
|
|
1546
|
-
export { HIGHLIGHT_NOTE_MESSAGE, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, defineForm, describeCondition, evaluate, explain, f, fingerprint, formatTimestamp, getFieldType, isEmptyValue, isFormEvent, isJson, isSensitive, listFieldTypes, listLimits, normalizeValue, parseForm, parseTimestamp, recall, registerFieldType, replay, ruleVisible, steps, summarizeValue, tableColumns, tableLimits, validateField, when };
|
|
2261
|
+
export { CALCULATED_TYPE, COMPUTED_FIELD_TYPES, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, getFieldType, isEmptyValue, isFormEvent, isIsoDate, isJson, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, parseForm, parseFormula, parseTimestamp, ratingDisplay, recall, registerFieldType, renameFormulaKey, replay, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, validateField, when };
|
|
1547
2262
|
//# sourceMappingURL=index.js.map
|
|
1548
2263
|
//# sourceMappingURL=index.js.map
|