@marktiderman/genesis-forms 0.5.0 → 0.6.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 +128 -0
- package/dist/destinations.cjs +55 -3
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +1 -1
- package/dist/destinations.d.ts +1 -1
- package/dist/destinations.js +55 -3
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-BDq0XJ21.d.cts → engine-CpfOz8FN.d.ts} +10 -3
- package/dist/{engine-Qzp6bX6O.d.ts → engine-DC4YTPdr.d.cts} +10 -3
- package/dist/index.cjs +484 -62
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +461 -63
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +541 -21
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +193 -3
- package/dist/server.d.ts +193 -3
- package/dist/server.js +522 -22
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +314 -21
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +314 -21
- package/dist/testing.js.map +1 -1
- package/dist/{types-BhYUbLJ-.d.cts → types-oAckGR_Z.d.cts} +10 -0
- package/dist/{types-BhYUbLJ-.d.ts → types-oAckGR_Z.d.ts} +10 -0
- package/dist/validate-B5WKHbst.d.cts +322 -0
- package/dist/validate-GpTXPwyl.d.ts +322 -0
- package/package.json +1 -1
- package/dist/validate-Bs1lyoOl.d.ts +0 -131
- package/dist/validate-BsZ0YTQS.d.cts +0 -131
package/dist/testing.d.cts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-
|
|
2
|
-
export { d as compactEvents } from './engine-
|
|
3
|
-
import { J as Json, F as FormDefinition, e as FormEvent } from './types-
|
|
4
|
-
import './validate-
|
|
1
|
+
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-DC4YTPdr.cjs';
|
|
2
|
+
export { d as compactEvents } from './engine-DC4YTPdr.cjs';
|
|
3
|
+
import { J as Json, F as FormDefinition, e as FormEvent } from './types-oAckGR_Z.cjs';
|
|
4
|
+
import './validate-B5WKHbst.cjs';
|
|
5
5
|
|
|
6
6
|
interface CoverageOptions {
|
|
7
7
|
/** Stop after this many explored states and report `truncated`. Default 5000. */
|
package/dist/testing.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-
|
|
2
|
-
export { d as compactEvents } from './engine-
|
|
3
|
-
import { J as Json, F as FormDefinition, e as FormEvent } from './types-
|
|
4
|
-
import './validate-
|
|
1
|
+
import { a as FormState, F as FormEngine, C as CreateFormOptions } from './engine-CpfOz8FN.js';
|
|
2
|
+
export { d as compactEvents } from './engine-CpfOz8FN.js';
|
|
3
|
+
import { J as Json, F as FormDefinition, e as FormEvent } from './types-oAckGR_Z.js';
|
|
4
|
+
import './validate-GpTXPwyl.js';
|
|
5
5
|
|
|
6
6
|
interface CoverageOptions {
|
|
7
7
|
/** Stop after this many explored states and report `truncated`. Default 5000. */
|
package/dist/testing.js
CHANGED
|
@@ -829,6 +829,65 @@ function summarizeValue(value, field) {
|
|
|
829
829
|
if (Array.isArray(value)) return value.join(", ");
|
|
830
830
|
return String(value);
|
|
831
831
|
}
|
|
832
|
+
var OPTION_LIMITS = {
|
|
833
|
+
/** Characters in a description. */
|
|
834
|
+
description: 500,
|
|
835
|
+
/** Characters in an image URL. */
|
|
836
|
+
image: 2048,
|
|
837
|
+
/** Attributes on one option. */
|
|
838
|
+
attributes: 20,
|
|
839
|
+
/** Characters in an attribute's name. */
|
|
840
|
+
attributeName: 64,
|
|
841
|
+
/** Characters in one attribute value. */
|
|
842
|
+
attributeValue: 200,
|
|
843
|
+
/** Values in one attribute's list. */
|
|
844
|
+
attributeItems: 50
|
|
845
|
+
};
|
|
846
|
+
function isHttpsUrl(url) {
|
|
847
|
+
return typeof url === "string" && url.length <= OPTION_LIMITS.image && /^https:\/\/[^\s/?#@\\]+(?:[/?#][^\s]*)?$/i.test(url) && // No control characters anywhere (a line break would end a CSV row or an HTML attribute).
|
|
848
|
+
![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
|
|
849
|
+
}
|
|
850
|
+
function attributeName(name) {
|
|
851
|
+
return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
|
|
852
|
+
}
|
|
853
|
+
function readAttributes(raw) {
|
|
854
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
855
|
+
const out = {};
|
|
856
|
+
let count = 0;
|
|
857
|
+
for (const name of Object.keys(raw)) {
|
|
858
|
+
if (count >= OPTION_LIMITS.attributes) break;
|
|
859
|
+
if (!attributeName(name)) continue;
|
|
860
|
+
const v = raw[name];
|
|
861
|
+
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
862
|
+
if (fits(v)) out[name] = v;
|
|
863
|
+
else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
|
|
864
|
+
else continue;
|
|
865
|
+
count++;
|
|
866
|
+
}
|
|
867
|
+
return count ? out : void 0;
|
|
868
|
+
}
|
|
869
|
+
function readOption(raw) {
|
|
870
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
871
|
+
const o = raw;
|
|
872
|
+
if (typeof o.value !== "string") return null;
|
|
873
|
+
const out = { value: o.value };
|
|
874
|
+
if (typeof o.label === "string") out.label = o.label;
|
|
875
|
+
if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
|
|
876
|
+
out.description = o.description;
|
|
877
|
+
}
|
|
878
|
+
if (isHttpsUrl(o.image)) out.image = o.image;
|
|
879
|
+
const attributes = readAttributes(o.attributes);
|
|
880
|
+
if (attributes) out.attributes = attributes;
|
|
881
|
+
return out;
|
|
882
|
+
}
|
|
883
|
+
function optionJson(option) {
|
|
884
|
+
const out = { value: option.value };
|
|
885
|
+
if (option.label !== void 0) out.label = option.label;
|
|
886
|
+
if (option.description !== void 0) out.description = option.description;
|
|
887
|
+
if (option.image !== void 0) out.image = option.image;
|
|
888
|
+
if (option.attributes !== void 0) out.attributes = { ...option.attributes };
|
|
889
|
+
return out;
|
|
890
|
+
}
|
|
832
891
|
var OTHER_MAX_LENGTH = 200;
|
|
833
892
|
function otherOption(field) {
|
|
834
893
|
const other = field.props?.other;
|
|
@@ -846,14 +905,21 @@ var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !=
|
|
|
846
905
|
function choiceOptions(field) {
|
|
847
906
|
const raw = field.props?.options;
|
|
848
907
|
if (!Array.isArray(raw)) return [];
|
|
849
|
-
return raw.flatMap(
|
|
850
|
-
|
|
851
|
-
|
|
908
|
+
return raw.flatMap((o) => {
|
|
909
|
+
const option = readOption(o);
|
|
910
|
+
return option ? [option] : [];
|
|
911
|
+
});
|
|
852
912
|
}
|
|
853
913
|
function optionsSource(field) {
|
|
854
914
|
const raw = field.props?.optionsFrom;
|
|
855
915
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
856
|
-
|
|
916
|
+
if (raw.source !== "host" || typeof raw.key !== "string" || !raw.key) return void 0;
|
|
917
|
+
const out = { source: "host", key: raw.key };
|
|
918
|
+
const filter = raw.filter;
|
|
919
|
+
if (filter && typeof filter === "object" && !Array.isArray(filter) && typeof filter.attribute === "string" && filter.attribute && typeof filter.field === "string" && filter.field) {
|
|
920
|
+
out.filter = { attribute: filter.attribute, field: filter.field };
|
|
921
|
+
}
|
|
922
|
+
return out;
|
|
857
923
|
}
|
|
858
924
|
function hostList(hostOptions, key) {
|
|
859
925
|
if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
|
|
@@ -862,11 +928,10 @@ function hostList(hostOptions, key) {
|
|
|
862
928
|
const seen = /* @__PURE__ */ new Set();
|
|
863
929
|
const out = [];
|
|
864
930
|
for (const o of list) {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
out.push(typeof label === "string" ? { value, label } : { value });
|
|
931
|
+
const option = readOption(o);
|
|
932
|
+
if (!option || !option.value || seen.has(option.value)) continue;
|
|
933
|
+
seen.add(option.value);
|
|
934
|
+
out.push(option);
|
|
870
935
|
}
|
|
871
936
|
return out;
|
|
872
937
|
}
|
|
@@ -881,7 +946,7 @@ function withHostOptions(definition, hostOptions) {
|
|
|
881
946
|
const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
|
|
882
947
|
if (!added.length) return block;
|
|
883
948
|
changed = true;
|
|
884
|
-
const options = [...own2, ...added].map(
|
|
949
|
+
const options = [...own2, ...added].map(optionJson);
|
|
885
950
|
return { ...block, props: { ...block.props, options } };
|
|
886
951
|
});
|
|
887
952
|
return changed ? { ...definition, blocks } : definition;
|
|
@@ -1361,6 +1426,150 @@ function fieldMap(def) {
|
|
|
1361
1426
|
return map;
|
|
1362
1427
|
}
|
|
1363
1428
|
|
|
1429
|
+
// src/options.ts
|
|
1430
|
+
var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
1431
|
+
var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
|
|
1432
|
+
function put(record, key, value) {
|
|
1433
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
1434
|
+
}
|
|
1435
|
+
function optionFilter(field) {
|
|
1436
|
+
return optionsSource(field)?.filter;
|
|
1437
|
+
}
|
|
1438
|
+
function answerValues(answer) {
|
|
1439
|
+
const one = (v) => typeof v === "string" ? [v] : typeof v === "number" || typeof v === "boolean" ? [String(v)] : [];
|
|
1440
|
+
if (answer === void 0 || answer === null) return [];
|
|
1441
|
+
return Array.isArray(answer) ? answer.flatMap(one) : one(answer);
|
|
1442
|
+
}
|
|
1443
|
+
function optionMatches(option, attribute, answer) {
|
|
1444
|
+
const have = option.attributes && Object.prototype.hasOwnProperty.call(option.attributes, attribute) ? option.attributes[attribute] : void 0;
|
|
1445
|
+
if (have === void 0) return false;
|
|
1446
|
+
const wanted = answerValues(answer);
|
|
1447
|
+
return (Array.isArray(have) ? have : [have]).some((v) => wanted.includes(v));
|
|
1448
|
+
}
|
|
1449
|
+
function filterSource(definition, key) {
|
|
1450
|
+
return definition.blocks.find((b) => b.kind === "field" && b.key === key);
|
|
1451
|
+
}
|
|
1452
|
+
function visibleOptions(definition, field, values) {
|
|
1453
|
+
const options = choiceOptions(field);
|
|
1454
|
+
const filter = optionFilter(field);
|
|
1455
|
+
if (!filter) return options;
|
|
1456
|
+
const source = filterSource(definition, filter.field);
|
|
1457
|
+
if (!source || source.id === field.id) return options;
|
|
1458
|
+
const answer = own(values, source.id);
|
|
1459
|
+
if (isEmptyValue(answer, source)) return options;
|
|
1460
|
+
return options.filter((o) => optionMatches(o, filter.attribute, answer));
|
|
1461
|
+
}
|
|
1462
|
+
function answersOnPath(state) {
|
|
1463
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1464
|
+
for (const id of state.path) out[id] = own(state.values, id);
|
|
1465
|
+
return out;
|
|
1466
|
+
}
|
|
1467
|
+
var OUTSIDE_FILTER = "Choose one of the options.";
|
|
1468
|
+
function filteredChoiceError(definition, field, value, values) {
|
|
1469
|
+
if (!optionFilter(field) || isEmptyValue(value, field)) return null;
|
|
1470
|
+
const all = new Set(choiceOptions(field).map((o) => o.value));
|
|
1471
|
+
const shown2 = new Set(visibleOptions(definition, field, values).map((o) => o.value));
|
|
1472
|
+
const picked = answerValues(value);
|
|
1473
|
+
return picked.some((v) => all.has(v) && !shown2.has(v)) ? OUTSIDE_FILTER : null;
|
|
1474
|
+
}
|
|
1475
|
+
function hasOptionFilters(definition) {
|
|
1476
|
+
return definition.blocks.some((b) => isChoice(b) && optionFilter(b) !== void 0);
|
|
1477
|
+
}
|
|
1478
|
+
function clearFilteredAnswers(definition, values, changedId) {
|
|
1479
|
+
const changed = definition.blocks.find((b) => b.kind === "field" && b.id === changedId);
|
|
1480
|
+
if (!changed) return values;
|
|
1481
|
+
const keys = /* @__PURE__ */ new Set([changed.key]);
|
|
1482
|
+
let out = values;
|
|
1483
|
+
for (const block of definition.blocks) {
|
|
1484
|
+
if (!isChoice(block)) continue;
|
|
1485
|
+
const filter = optionFilter(block);
|
|
1486
|
+
if (!filter || !keys.has(filter.field)) continue;
|
|
1487
|
+
const value = own(out, block.id);
|
|
1488
|
+
if (value === void 0 || isEmptyValue(value, block)) continue;
|
|
1489
|
+
const all = new Set(choiceOptions(block).map((o) => o.value));
|
|
1490
|
+
const shown2 = new Set(visibleOptions(definition, block, out).map((o) => o.value));
|
|
1491
|
+
const keep = (v) => typeof v !== "string" || !all.has(v) || shown2.has(v);
|
|
1492
|
+
let next = value;
|
|
1493
|
+
if (Array.isArray(value)) {
|
|
1494
|
+
const kept = value.filter(keep);
|
|
1495
|
+
if (kept.length !== value.length) next = kept.length ? kept : void 0;
|
|
1496
|
+
} else if (!keep(value)) next = void 0;
|
|
1497
|
+
if (next === value) continue;
|
|
1498
|
+
if (out === values) out = { ...values };
|
|
1499
|
+
put(out, block.id, next);
|
|
1500
|
+
keys.add(block.key);
|
|
1501
|
+
}
|
|
1502
|
+
return out;
|
|
1503
|
+
}
|
|
1504
|
+
var MAX_PREFILL_PARAM_LENGTH = 64;
|
|
1505
|
+
function isPrefillParam(param) {
|
|
1506
|
+
return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
|
|
1507
|
+
}
|
|
1508
|
+
function prefillSetting(field) {
|
|
1509
|
+
if (!CHOICE_FIELD_TYPES.has(field.type)) return void 0;
|
|
1510
|
+
const raw = field.props?.prefill;
|
|
1511
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
|
|
1512
|
+
return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
|
|
1513
|
+
}
|
|
1514
|
+
function paramValues(params, name) {
|
|
1515
|
+
let raw;
|
|
1516
|
+
if (typeof params.getAll === "function") {
|
|
1517
|
+
raw = params.getAll(name);
|
|
1518
|
+
} else if (Object.prototype.hasOwnProperty.call(params, name)) {
|
|
1519
|
+
raw = params[name];
|
|
1520
|
+
}
|
|
1521
|
+
const list = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
|
|
1522
|
+
return list.filter((v) => typeof v === "string" && v !== "" && v.length <= MAX_HIDDEN_STRING_LENGTH);
|
|
1523
|
+
}
|
|
1524
|
+
function prefillFromLink(definition, params, values = {}) {
|
|
1525
|
+
const out = {};
|
|
1526
|
+
if (!params || typeof params !== "object") return out;
|
|
1527
|
+
const seen = {};
|
|
1528
|
+
for (const id of Object.keys(values)) put(seen, id, values[id]);
|
|
1529
|
+
for (const block of definition.blocks) {
|
|
1530
|
+
if (!isChoice(block)) continue;
|
|
1531
|
+
const setting = prefillSetting(block);
|
|
1532
|
+
if (!setting || own(seen, block.id) !== void 0) continue;
|
|
1533
|
+
const raw = paramValues(params, setting.param);
|
|
1534
|
+
if (!raw.length) continue;
|
|
1535
|
+
const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
|
|
1536
|
+
let value;
|
|
1537
|
+
if (block.type === "multi-choice") {
|
|
1538
|
+
const picked = [];
|
|
1539
|
+
for (const r of raw) {
|
|
1540
|
+
for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
|
|
1541
|
+
if (shown2.has(v) && !picked.includes(v)) picked.push(v);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
if (picked.length) value = picked;
|
|
1545
|
+
} else value = raw.find((v) => shown2.has(v));
|
|
1546
|
+
if (value === void 0) continue;
|
|
1547
|
+
put(out, block.id, value);
|
|
1548
|
+
put(seen, block.id, value);
|
|
1549
|
+
}
|
|
1550
|
+
return out;
|
|
1551
|
+
}
|
|
1552
|
+
function hiddenByPrefill(definition, state) {
|
|
1553
|
+
const hideable = /* @__PURE__ */ new Set();
|
|
1554
|
+
for (const b of definition.blocks) {
|
|
1555
|
+
if (isChoice(b) && prefillSetting(b)?.hideWhenPrefilled) hideable.add(b.id);
|
|
1556
|
+
}
|
|
1557
|
+
if (!hideable.size) return [];
|
|
1558
|
+
const prefilled = /* @__PURE__ */ new Set();
|
|
1559
|
+
for (const e of state.events) {
|
|
1560
|
+
if (e.type !== "answer" || !hideable.has(e.field)) continue;
|
|
1561
|
+
if (e.source === "prefill") prefilled.add(e.field);
|
|
1562
|
+
else prefilled.delete(e.field);
|
|
1563
|
+
}
|
|
1564
|
+
if (!prefilled.size) return [];
|
|
1565
|
+
const fields = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
1566
|
+
return state.path.filter((id) => {
|
|
1567
|
+
if (!prefilled.has(id) || own(state.errors, id) !== void 0) return false;
|
|
1568
|
+
const field = fields.get(id);
|
|
1569
|
+
return !isEmptyValue(own(state.values, id), field);
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1364
1573
|
// src/recall.ts
|
|
1365
1574
|
var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
|
|
1366
1575
|
function recall(text, scope) {
|
|
@@ -1482,16 +1691,34 @@ function build(given, options, replaying = false, budget) {
|
|
|
1482
1691
|
return at === void 0 ? event : { ...event, at };
|
|
1483
1692
|
};
|
|
1484
1693
|
const isVisibleIn = (s, id) => s.path.includes(id);
|
|
1694
|
+
const filters = hasOptionFilters(definition);
|
|
1695
|
+
const clearFiltered = (values, id) => filters ? clearFilteredAnswers(definition, values, id) : values;
|
|
1485
1696
|
const errorsFor = (s, ids) => {
|
|
1486
1697
|
const errors = {};
|
|
1698
|
+
let shown2;
|
|
1487
1699
|
for (const id of ids) {
|
|
1488
1700
|
const field = fields.get(id);
|
|
1489
1701
|
if (!field || !isVisibleIn(s, id)) continue;
|
|
1490
|
-
const
|
|
1702
|
+
const value = own(s.values, id);
|
|
1703
|
+
let message = validateField(field, value, { custom: options.validators?.[id] });
|
|
1704
|
+
if (!message && filters) message = filteredChoiceError(definition, field, value, shown2 ??= answersOnPath(s));
|
|
1491
1705
|
if (message) errors[id] = message;
|
|
1492
1706
|
}
|
|
1493
1707
|
return errors;
|
|
1494
1708
|
};
|
|
1709
|
+
const skipped = (s) => new Set(hiddenByPrefill(definition, s));
|
|
1710
|
+
const shownFrom = (s, from, skip) => {
|
|
1711
|
+
for (let i = from; i < s.path.length; i++) if (!skip.has(s.path[i])) return s.path[i];
|
|
1712
|
+
return void 0;
|
|
1713
|
+
};
|
|
1714
|
+
const showCurrent = () => {
|
|
1715
|
+
if (!state.current) return;
|
|
1716
|
+
const skip = skipped(state);
|
|
1717
|
+
if (!skip.has(state.current)) return;
|
|
1718
|
+
const at = state.path.indexOf(state.current);
|
|
1719
|
+
const shown2 = at < 0 ? void 0 : shownFrom(state, at, skip);
|
|
1720
|
+
if (shown2) state = { ...state, current: shown2, furthest: state.furthest === state.current ? shown2 : state.furthest };
|
|
1721
|
+
};
|
|
1495
1722
|
let unsettled = false;
|
|
1496
1723
|
let formulasStale = false;
|
|
1497
1724
|
const settleNow = (needValues = true) => {
|
|
@@ -1520,13 +1747,13 @@ function build(given, options, replaying = false, budget) {
|
|
|
1520
1747
|
const errors = { ...state.errors };
|
|
1521
1748
|
delete errors[id];
|
|
1522
1749
|
if (batch) {
|
|
1523
|
-
state.values = { ...state.values, [id]: value };
|
|
1750
|
+
state.values = clearFiltered({ ...state.values, [id]: value }, id);
|
|
1524
1751
|
state.errors = errors;
|
|
1525
1752
|
log(event);
|
|
1526
1753
|
unsettled = true;
|
|
1527
1754
|
return;
|
|
1528
1755
|
}
|
|
1529
|
-
const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
|
|
1756
|
+
const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
|
|
1530
1757
|
emit({ ...state, values, path, errors, events: log(event) });
|
|
1531
1758
|
};
|
|
1532
1759
|
const engine = {
|
|
@@ -1557,7 +1784,7 @@ function build(given, options, replaying = false, budget) {
|
|
|
1557
1784
|
return false;
|
|
1558
1785
|
}
|
|
1559
1786
|
const at = state.path.indexOf(state.current);
|
|
1560
|
-
const target = state
|
|
1787
|
+
const target = shownFrom(state, at + 1, skipped(state));
|
|
1561
1788
|
if (!target) {
|
|
1562
1789
|
emit({ ...state, events });
|
|
1563
1790
|
return true;
|
|
@@ -1569,8 +1796,9 @@ function build(given, options, replaying = false, budget) {
|
|
|
1569
1796
|
back() {
|
|
1570
1797
|
if (state.status === "completed") return;
|
|
1571
1798
|
const history = [...state.history];
|
|
1799
|
+
const skip = skipped(state);
|
|
1572
1800
|
let previous = history.pop();
|
|
1573
|
-
while (previous && !state.path.includes(previous)) previous = history.pop();
|
|
1801
|
+
while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
|
|
1574
1802
|
if (!previous) return;
|
|
1575
1803
|
emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
|
|
1576
1804
|
},
|
|
@@ -1598,15 +1826,18 @@ function build(given, options, replaying = false, budget) {
|
|
|
1598
1826
|
return out;
|
|
1599
1827
|
}
|
|
1600
1828
|
};
|
|
1601
|
-
|
|
1829
|
+
const fromLink = options.linkParams ? prefillFromLink(definition, options.linkParams, options.initialValues) : {};
|
|
1830
|
+
const initialValues = Object.keys(fromLink).length ? { ...fromLink, ...options.initialValues } : options.initialValues;
|
|
1831
|
+
if (initialValues) {
|
|
1602
1832
|
const wasBatch = batch;
|
|
1603
1833
|
batch = true;
|
|
1604
|
-
for (const [id, value] of Object.entries(
|
|
1834
|
+
for (const [id, value] of Object.entries(initialValues)) {
|
|
1605
1835
|
if (!fields.has(id) || value === void 0) continue;
|
|
1606
1836
|
answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
|
|
1607
1837
|
}
|
|
1608
1838
|
settleNow();
|
|
1609
1839
|
batch = wasBatch;
|
|
1840
|
+
showCurrent();
|
|
1610
1841
|
}
|
|
1611
1842
|
const resumeAt = options.resumeAt;
|
|
1612
1843
|
if (resumeAt && !replaying && state.path.includes(resumeAt)) {
|
|
@@ -1625,18 +1856,22 @@ function build(given, options, replaying = false, budget) {
|
|
|
1625
1856
|
}
|
|
1626
1857
|
} else if (event.type === "next") {
|
|
1627
1858
|
settleNow(false);
|
|
1859
|
+
showCurrent();
|
|
1628
1860
|
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
1629
1861
|
engine.next();
|
|
1630
1862
|
} else if (event.type === "back") {
|
|
1631
1863
|
settleNow(false);
|
|
1864
|
+
showCurrent();
|
|
1632
1865
|
engine.back();
|
|
1633
1866
|
} else if (event.type === "submit") {
|
|
1634
1867
|
settleNow();
|
|
1868
|
+
showCurrent();
|
|
1635
1869
|
engine.submit();
|
|
1636
1870
|
}
|
|
1637
1871
|
};
|
|
1638
1872
|
const finish = () => {
|
|
1639
1873
|
settleNow();
|
|
1874
|
+
showCurrent();
|
|
1640
1875
|
batch = false;
|
|
1641
1876
|
state = { ...state, events: [...state.events] };
|
|
1642
1877
|
};
|
|
@@ -1647,7 +1882,12 @@ function replay(definition, events, options = {}) {
|
|
|
1647
1882
|
}
|
|
1648
1883
|
function replayWithin(definition, events, options, maxWork) {
|
|
1649
1884
|
const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
|
|
1650
|
-
const { engine, apply, finish } = build(
|
|
1885
|
+
const { engine, apply, finish } = build(
|
|
1886
|
+
definition,
|
|
1887
|
+
{ ...options, initialValues: void 0, linkParams: void 0, clock: () => void 0 },
|
|
1888
|
+
true,
|
|
1889
|
+
budget
|
|
1890
|
+
);
|
|
1651
1891
|
try {
|
|
1652
1892
|
for (const event of events) apply(event);
|
|
1653
1893
|
finish();
|
|
@@ -1898,6 +2138,31 @@ function parseForm(input) {
|
|
|
1898
2138
|
const optionalBoolean = (v, path) => {
|
|
1899
2139
|
if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
|
|
1900
2140
|
};
|
|
2141
|
+
const optionDetails = (o, at) => {
|
|
2142
|
+
if (o.description !== void 0 && (typeof o.description !== "string" || o.description.length > OPTION_LIMITS.description)) {
|
|
2143
|
+
issue(`${at}.description`, `Must be text of at most ${OPTION_LIMITS.description} characters.`);
|
|
2144
|
+
}
|
|
2145
|
+
if (o.image !== void 0 && !isHttpsUrl(o.image)) {
|
|
2146
|
+
issue(`${at}.image`, `Must be an https:// link of at most ${OPTION_LIMITS.image} characters.`);
|
|
2147
|
+
}
|
|
2148
|
+
if (o.attributes === void 0) return;
|
|
2149
|
+
const attributes = o.attributes;
|
|
2150
|
+
if (!isObject2(attributes) || Object.keys(attributes).length > OPTION_LIMITS.attributes) {
|
|
2151
|
+
issue(`${at}.attributes`, `Must be an object of at most ${OPTION_LIMITS.attributes} attributes.`);
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
2155
|
+
for (const [name2, v] of Object.entries(attributes)) {
|
|
2156
|
+
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
|
|
2157
|
+
issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
|
|
2158
|
+
} else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
|
|
2159
|
+
issue(
|
|
2160
|
+
`${at}.attributes.${name2}`,
|
|
2161
|
+
`Must be text, or a list of at most ${OPTION_LIMITS.attributeItems} texts, each at most ${OPTION_LIMITS.attributeValue} characters.`
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
};
|
|
1901
2166
|
const name = (v, path, what) => {
|
|
1902
2167
|
if (typeof v !== "string" || !v) {
|
|
1903
2168
|
issue(path, `${what} must be a non-empty string.`);
|
|
@@ -2070,7 +2335,31 @@ function parseForm(input) {
|
|
|
2070
2335
|
} else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2071
2336
|
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
2072
2337
|
}
|
|
2338
|
+
const filter = isObject2(from) ? from.filter : void 0;
|
|
2339
|
+
if (filter !== void 0) {
|
|
2340
|
+
const at = `${path}.props.optionsFrom.filter`;
|
|
2341
|
+
if (!isObject2(filter) || typeof filter.attribute !== "string" || !filter.attribute || filter.attribute.length > OPTION_LIMITS.attributeName || typeof filter.field !== "string" || !filter.field) {
|
|
2342
|
+
issue(at, `Must be { attribute, field }: an attribute name (at most ${OPTION_LIMITS.attributeName} characters) and an earlier question's key.`);
|
|
2343
|
+
} else if (!keys.has(filter.field)) {
|
|
2344
|
+
issue(`${at}.field`, `"${filter.field}" must be the key of an earlier question.`);
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const prefill = isObject2(block.props) ? block.props.prefill : void 0;
|
|
2349
|
+
if (prefill !== void 0) {
|
|
2350
|
+
if (!isObject2(prefill) || !isPrefillParam(prefill.param)) {
|
|
2351
|
+
issue(
|
|
2352
|
+
`${path}.props.prefill`,
|
|
2353
|
+
`Must be { param, hideWhenPrefilled? }: a link parameter of letters, digits, "_", "-" or "." (at most ${MAX_PREFILL_PARAM_LENGTH} characters).`
|
|
2354
|
+
);
|
|
2355
|
+
} else {
|
|
2356
|
+
optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
|
|
2357
|
+
if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2358
|
+
warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2073
2361
|
}
|
|
2362
|
+
if (isObject2(block.props)) optionalBoolean(block.props.searchable, `${path}.props.searchable`);
|
|
2074
2363
|
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
2075
2364
|
else if (block.props.options !== void 0) {
|
|
2076
2365
|
const options = block.props.options;
|
|
@@ -2079,7 +2368,10 @@ function parseForm(input) {
|
|
|
2079
2368
|
options.forEach((o, j) => {
|
|
2080
2369
|
const at = `${path}.props.options[${j}]`;
|
|
2081
2370
|
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
2082
|
-
else
|
|
2371
|
+
else {
|
|
2372
|
+
optionalText(o.label, `${at}.label`);
|
|
2373
|
+
optionDetails(o, at);
|
|
2374
|
+
}
|
|
2083
2375
|
});
|
|
2084
2376
|
if (from === void 0) {
|
|
2085
2377
|
choices.set(
|
|
@@ -2509,11 +2801,12 @@ function validateSubmission(published, submission, options = {}) {
|
|
|
2509
2801
|
}
|
|
2510
2802
|
} else {
|
|
2511
2803
|
const errors = {};
|
|
2512
|
-
|
|
2804
|
+
const kept = engine.answers();
|
|
2805
|
+
for (const [id, value] of Object.entries(kept)) {
|
|
2513
2806
|
const message = validateField(fields.get(id), value, {
|
|
2514
2807
|
requireAnswers: false,
|
|
2515
2808
|
custom: options.validators?.[id]
|
|
2516
|
-
});
|
|
2809
|
+
}) ?? filteredChoiceError(definition, fields.get(id), value, kept);
|
|
2517
2810
|
if (message) errors[id] = message;
|
|
2518
2811
|
}
|
|
2519
2812
|
if (Object.keys(errors).length) return { ok: false, errors };
|