@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/dist/index.js CHANGED
@@ -950,6 +950,65 @@ function summarizeValue(value, field2) {
950
950
  if (Array.isArray(value)) return value.join(", ");
951
951
  return String(value);
952
952
  }
953
+ var OPTION_LIMITS = {
954
+ /** Characters in a description. */
955
+ description: 500,
956
+ /** Characters in an image URL. */
957
+ image: 2048,
958
+ /** Attributes on one option. */
959
+ attributes: 20,
960
+ /** Characters in an attribute's name. */
961
+ attributeName: 64,
962
+ /** Characters in one attribute value. */
963
+ attributeValue: 200,
964
+ /** Values in one attribute's list. */
965
+ attributeItems: 50
966
+ };
967
+ function isHttpsUrl(url) {
968
+ 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).
969
+ ![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
970
+ }
971
+ function attributeName(name) {
972
+ return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
973
+ }
974
+ function readAttributes(raw) {
975
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
976
+ const out = {};
977
+ let count = 0;
978
+ for (const name of Object.keys(raw)) {
979
+ if (count >= OPTION_LIMITS.attributes) break;
980
+ if (!attributeName(name)) continue;
981
+ const v = raw[name];
982
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
983
+ if (fits(v)) out[name] = v;
984
+ else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
985
+ else continue;
986
+ count++;
987
+ }
988
+ return count ? out : void 0;
989
+ }
990
+ function readOption(raw) {
991
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
992
+ const o = raw;
993
+ if (typeof o.value !== "string") return null;
994
+ const out = { value: o.value };
995
+ if (typeof o.label === "string") out.label = o.label;
996
+ if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
997
+ out.description = o.description;
998
+ }
999
+ if (isHttpsUrl(o.image)) out.image = o.image;
1000
+ const attributes = readAttributes(o.attributes);
1001
+ if (attributes) out.attributes = attributes;
1002
+ return out;
1003
+ }
1004
+ function optionJson(option) {
1005
+ const out = { value: option.value };
1006
+ if (option.label !== void 0) out.label = option.label;
1007
+ if (option.description !== void 0) out.description = option.description;
1008
+ if (option.image !== void 0) out.image = option.image;
1009
+ if (option.attributes !== void 0) out.attributes = { ...option.attributes };
1010
+ return out;
1011
+ }
953
1012
  var OTHER_MAX_LENGTH = 200;
954
1013
  function otherOption(field2) {
955
1014
  const other = field2.props?.other;
@@ -967,14 +1026,21 @@ var typedOther = (field2, value) => isOtherValue(field2, value) && value.trim()
967
1026
  function choiceOptions(field2) {
968
1027
  const raw = field2.props?.options;
969
1028
  if (!Array.isArray(raw)) return [];
970
- return raw.flatMap(
971
- (o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
972
- );
1029
+ return raw.flatMap((o) => {
1030
+ const option = readOption(o);
1031
+ return option ? [option] : [];
1032
+ });
973
1033
  }
974
1034
  function optionsSource(field2) {
975
1035
  const raw = field2.props?.optionsFrom;
976
1036
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
977
- return raw.source === "host" && typeof raw.key === "string" && raw.key ? { source: "host", key: raw.key } : void 0;
1037
+ if (raw.source !== "host" || typeof raw.key !== "string" || !raw.key) return void 0;
1038
+ const out = { source: "host", key: raw.key };
1039
+ const filter = raw.filter;
1040
+ if (filter && typeof filter === "object" && !Array.isArray(filter) && typeof filter.attribute === "string" && filter.attribute && typeof filter.field === "string" && filter.field) {
1041
+ out.filter = { attribute: filter.attribute, field: filter.field };
1042
+ }
1043
+ return out;
978
1044
  }
979
1045
  function hostList(hostOptions, key) {
980
1046
  if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
@@ -983,11 +1049,10 @@ function hostList(hostOptions, key) {
983
1049
  const seen = /* @__PURE__ */ new Set();
984
1050
  const out = [];
985
1051
  for (const o of list) {
986
- if (!o || typeof o !== "object" || Array.isArray(o)) continue;
987
- const { value, label } = o;
988
- if (typeof value !== "string" || !value || seen.has(value)) continue;
989
- seen.add(value);
990
- out.push(typeof label === "string" ? { value, label } : { value });
1052
+ const option = readOption(o);
1053
+ if (!option || !option.value || seen.has(option.value)) continue;
1054
+ seen.add(option.value);
1055
+ out.push(option);
991
1056
  }
992
1057
  return out;
993
1058
  }
@@ -1002,7 +1067,7 @@ function withHostOptions(definition, hostOptions) {
1002
1067
  const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
1003
1068
  if (!added.length) return block;
1004
1069
  changed = true;
1005
- const options = [...own2, ...added].map((o) => o.label === void 0 ? { value: o.value } : { value: o.value, label: o.label });
1070
+ const options = [...own2, ...added].map(optionJson);
1006
1071
  return { ...block, props: { ...block.props, options } };
1007
1072
  });
1008
1073
  return changed ? { ...definition, blocks } : definition;
@@ -1271,6 +1336,47 @@ function dateLimits(field2) {
1271
1336
  }
1272
1337
  for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
1273
1338
 
1339
+ // src/hidden.ts
1340
+ var MAX_HIDDEN_STRING_LENGTH = 1e3;
1341
+ var DECIMAL = /^-?\d+(\.\d+)?$/;
1342
+ function coerceHiddenValue(value, type) {
1343
+ if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
1344
+ if (type === "string") {
1345
+ if (typeof value === "string") return value;
1346
+ if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return String(value);
1347
+ return void 0;
1348
+ }
1349
+ if (type === "number") {
1350
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1351
+ if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
1352
+ const n = Number(value);
1353
+ return Number.isFinite(n) ? n : void 0;
1354
+ }
1355
+ if (typeof value === "boolean") return value;
1356
+ if (value === "true") return true;
1357
+ if (value === "false") return false;
1358
+ return void 0;
1359
+ }
1360
+ function coerceHidden(definition, raw, options = {}) {
1361
+ const declared = new Map((definition.hidden ?? []).map((h) => [h.name, h]));
1362
+ const values = {};
1363
+ const ignored = [];
1364
+ if (!isPlainObject(raw)) return { values, ignored };
1365
+ for (const name of Object.keys(raw)) {
1366
+ const value = raw[name];
1367
+ if (value === void 0) continue;
1368
+ const spec = declared.get(name);
1369
+ if (!spec || options.sources && !options.sources.includes(spec.source)) {
1370
+ ignored.push(name);
1371
+ continue;
1372
+ }
1373
+ const coerced = coerceHiddenValue(value, spec.type);
1374
+ if (coerced === void 0) ignored.push(name);
1375
+ else Object.defineProperty(values, name, { value: coerced, enumerable: true, writable: true, configurable: true });
1376
+ }
1377
+ return { values, ignored };
1378
+ }
1379
+
1274
1380
  // src/logic.ts
1275
1381
  function own(record, key) {
1276
1382
  return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : void 0;
@@ -1402,6 +1508,225 @@ function fieldMap(def) {
1402
1508
  return map;
1403
1509
  }
1404
1510
 
1511
+ // src/options.ts
1512
+ var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
1513
+ var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
1514
+ function put(record, key, value) {
1515
+ Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
1516
+ }
1517
+ function optionFilter(field2) {
1518
+ return optionsSource(field2)?.filter;
1519
+ }
1520
+ function answerValues(answer) {
1521
+ const one = (v) => typeof v === "string" ? [v] : typeof v === "number" || typeof v === "boolean" ? [String(v)] : [];
1522
+ if (answer === void 0 || answer === null) return [];
1523
+ return Array.isArray(answer) ? answer.flatMap(one) : one(answer);
1524
+ }
1525
+ function optionMatches(option, attribute, answer) {
1526
+ const have = option.attributes && Object.prototype.hasOwnProperty.call(option.attributes, attribute) ? option.attributes[attribute] : void 0;
1527
+ if (have === void 0) return false;
1528
+ const wanted = answerValues(answer);
1529
+ return (Array.isArray(have) ? have : [have]).some((v) => wanted.includes(v));
1530
+ }
1531
+ function filterSource(definition, key) {
1532
+ return definition.blocks.find((b) => b.kind === "field" && b.key === key);
1533
+ }
1534
+ function visibleOptions(definition, field2, values) {
1535
+ const options = choiceOptions(field2);
1536
+ const filter = optionFilter(field2);
1537
+ if (!filter) return options;
1538
+ const source = filterSource(definition, filter.field);
1539
+ if (!source || source.id === field2.id) return options;
1540
+ const answer = own(values, source.id);
1541
+ if (isEmptyValue(answer, source)) return options;
1542
+ return options.filter((o) => optionMatches(o, filter.attribute, answer));
1543
+ }
1544
+ function answersOnPath(state) {
1545
+ const out = /* @__PURE__ */ Object.create(null);
1546
+ for (const id of state.path) out[id] = own(state.values, id);
1547
+ return out;
1548
+ }
1549
+ var OUTSIDE_FILTER = "Choose one of the options.";
1550
+ function filteredChoiceError(definition, field2, value, values) {
1551
+ if (!optionFilter(field2) || isEmptyValue(value, field2)) return null;
1552
+ const all = new Set(choiceOptions(field2).map((o) => o.value));
1553
+ const shown2 = new Set(visibleOptions(definition, field2, values).map((o) => o.value));
1554
+ const picked = answerValues(value);
1555
+ return picked.some((v) => all.has(v) && !shown2.has(v)) ? OUTSIDE_FILTER : null;
1556
+ }
1557
+ function hasOptionFilters(definition) {
1558
+ return definition.blocks.some((b) => isChoice(b) && optionFilter(b) !== void 0);
1559
+ }
1560
+ function clearFilteredAnswers(definition, values, changedId) {
1561
+ const changed = definition.blocks.find((b) => b.kind === "field" && b.id === changedId);
1562
+ if (!changed) return values;
1563
+ const keys = /* @__PURE__ */ new Set([changed.key]);
1564
+ let out = values;
1565
+ for (const block of definition.blocks) {
1566
+ if (!isChoice(block)) continue;
1567
+ const filter = optionFilter(block);
1568
+ if (!filter || !keys.has(filter.field)) continue;
1569
+ const value = own(out, block.id);
1570
+ if (value === void 0 || isEmptyValue(value, block)) continue;
1571
+ const all = new Set(choiceOptions(block).map((o) => o.value));
1572
+ const shown2 = new Set(visibleOptions(definition, block, out).map((o) => o.value));
1573
+ const keep = (v) => typeof v !== "string" || !all.has(v) || shown2.has(v);
1574
+ let next = value;
1575
+ if (Array.isArray(value)) {
1576
+ const kept = value.filter(keep);
1577
+ if (kept.length !== value.length) next = kept.length ? kept : void 0;
1578
+ } else if (!keep(value)) next = void 0;
1579
+ if (next === value) continue;
1580
+ if (out === values) out = { ...values };
1581
+ put(out, block.id, next);
1582
+ keys.add(block.key);
1583
+ }
1584
+ return out;
1585
+ }
1586
+ var SEARCHABLE_OPTION_COUNT = 10;
1587
+ function isSearchable(field2, optionCount = choiceOptions(field2).length) {
1588
+ const setting = field2.props?.searchable;
1589
+ if (setting === true || setting === false) return setting;
1590
+ return optionCount > SEARCHABLE_OPTION_COUNT;
1591
+ }
1592
+ function fold(text) {
1593
+ return text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
1594
+ }
1595
+ function searchOptions(options, query) {
1596
+ const words = fold(query).split(/\s+/).filter(Boolean);
1597
+ if (!words.length) return [...options];
1598
+ return options.filter((o) => {
1599
+ const text = fold([o.label ?? "", o.value, o.description ?? ""].join(" "));
1600
+ return words.every((w) => text.includes(w));
1601
+ });
1602
+ }
1603
+ var MAX_PREFILL_PARAM_LENGTH = 64;
1604
+ function isPrefillParam(param) {
1605
+ return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
1606
+ }
1607
+ function prefillSetting(field2) {
1608
+ if (!CHOICE_FIELD_TYPES.has(field2.type)) return void 0;
1609
+ const raw = field2.props?.prefill;
1610
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
1611
+ return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
1612
+ }
1613
+ function paramValues(params, name) {
1614
+ let raw;
1615
+ if (typeof params.getAll === "function") {
1616
+ raw = params.getAll(name);
1617
+ } else if (Object.prototype.hasOwnProperty.call(params, name)) {
1618
+ raw = params[name];
1619
+ }
1620
+ const list = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
1621
+ return list.filter((v) => typeof v === "string" && v !== "" && v.length <= MAX_HIDDEN_STRING_LENGTH);
1622
+ }
1623
+ function prefillFromLink(definition, params, values = {}) {
1624
+ const out = {};
1625
+ if (!params || typeof params !== "object") return out;
1626
+ const seen = {};
1627
+ for (const id of Object.keys(values)) put(seen, id, values[id]);
1628
+ for (const block of definition.blocks) {
1629
+ if (!isChoice(block)) continue;
1630
+ const setting = prefillSetting(block);
1631
+ if (!setting || own(seen, block.id) !== void 0) continue;
1632
+ const raw = paramValues(params, setting.param);
1633
+ if (!raw.length) continue;
1634
+ const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
1635
+ let value;
1636
+ if (block.type === "multi-choice") {
1637
+ const picked = [];
1638
+ for (const r of raw) {
1639
+ for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
1640
+ if (shown2.has(v) && !picked.includes(v)) picked.push(v);
1641
+ }
1642
+ }
1643
+ if (picked.length) value = picked;
1644
+ } else value = raw.find((v) => shown2.has(v));
1645
+ if (value === void 0) continue;
1646
+ put(out, block.id, value);
1647
+ put(seen, block.id, value);
1648
+ }
1649
+ return out;
1650
+ }
1651
+ function hiddenByPrefill(definition, state) {
1652
+ const hideable = /* @__PURE__ */ new Set();
1653
+ for (const b of definition.blocks) {
1654
+ if (isChoice(b) && prefillSetting(b)?.hideWhenPrefilled) hideable.add(b.id);
1655
+ }
1656
+ if (!hideable.size) return [];
1657
+ const prefilled = /* @__PURE__ */ new Set();
1658
+ for (const e of state.events) {
1659
+ if (e.type !== "answer" || !hideable.has(e.field)) continue;
1660
+ if (e.source === "prefill") prefilled.add(e.field);
1661
+ else prefilled.delete(e.field);
1662
+ }
1663
+ if (!prefilled.size) return [];
1664
+ const fields = new Map(definition.blocks.map((b) => [b.id, b]));
1665
+ return state.path.filter((id) => {
1666
+ if (!prefilled.has(id) || own(state.errors, id) !== void 0) return false;
1667
+ const field2 = fields.get(id);
1668
+ return !isEmptyValue(own(state.values, id), field2);
1669
+ });
1670
+ }
1671
+ function respondentPath(definition, state) {
1672
+ const hidden = hiddenByPrefill(definition, state);
1673
+ if (!hidden.length) return [...state.path];
1674
+ const skip = new Set(hidden);
1675
+ return state.path.filter((id) => !skip.has(id));
1676
+ }
1677
+ function snapshotOf(byValue, value) {
1678
+ const option = byValue.get(value);
1679
+ if (option) return { value, label: option.label ?? value };
1680
+ return { value, label: value, other: true };
1681
+ }
1682
+ function answerSnapshots(definition, answers) {
1683
+ const out = {};
1684
+ for (const block of definition.blocks) {
1685
+ if (!isChoice(block) || !optionsSource(block)) continue;
1686
+ const value = own(answers, block.id);
1687
+ if (isEmptyValue(value, block)) continue;
1688
+ const byValue = new Map(choiceOptions(block).map((o) => [o.value, o]));
1689
+ let snapshot;
1690
+ if (Array.isArray(value)) {
1691
+ const items = value.filter((v) => typeof v === "string").map((v) => snapshotOf(byValue, v));
1692
+ if (items.length) snapshot = items;
1693
+ } else if (typeof value === "string") snapshot = snapshotOf(byValue, value);
1694
+ if (snapshot) put(out, block.id, snapshot);
1695
+ }
1696
+ return out;
1697
+ }
1698
+ function answersFromSnapshots(definition, snapshots) {
1699
+ const answers = {};
1700
+ const missing = {};
1701
+ for (const block of definition.blocks) {
1702
+ if (!isChoice(block)) continue;
1703
+ const raw = own(snapshots, block.id);
1704
+ const list = (Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw]).flatMap((s) => {
1705
+ const o = readOption(s);
1706
+ if (!o) return [];
1707
+ const label = o.label ?? o.value;
1708
+ return [s.other === true ? { value: o.value, label, other: true } : { value: o.value, label }];
1709
+ });
1710
+ if (!list.length) continue;
1711
+ const options = choiceOptions(block);
1712
+ const byValue = new Set(options.map((o) => o.value));
1713
+ const allowsOther = block.props?.other !== void 0 && block.props?.other !== false;
1714
+ const found = [];
1715
+ const lost = [];
1716
+ for (const s of list) {
1717
+ let value;
1718
+ if (byValue.has(s.value)) value = s.value;
1719
+ else if (s.other) value = allowsOther ? s.value : void 0;
1720
+ else value = options.find((o) => fold(o.label ?? o.value) === fold(s.label))?.value;
1721
+ if (value === void 0) lost.push(s);
1722
+ else if (!found.includes(value)) found.push(value);
1723
+ }
1724
+ if (found.length) put(answers, block.id, block.type === "multi-choice" ? found : found[0]);
1725
+ if (lost.length) put(missing, block.id, lost);
1726
+ }
1727
+ return { answers, missing };
1728
+ }
1729
+
1405
1730
  // src/recall.ts
1406
1731
  var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
1407
1732
  function recall(text, scope) {
@@ -1481,6 +1806,31 @@ function parseForm(input) {
1481
1806
  const optionalBoolean = (v, path) => {
1482
1807
  if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
1483
1808
  };
1809
+ const optionDetails = (o, at) => {
1810
+ if (o.description !== void 0 && (typeof o.description !== "string" || o.description.length > OPTION_LIMITS.description)) {
1811
+ issue(`${at}.description`, `Must be text of at most ${OPTION_LIMITS.description} characters.`);
1812
+ }
1813
+ if (o.image !== void 0 && !isHttpsUrl(o.image)) {
1814
+ issue(`${at}.image`, `Must be an https:// link of at most ${OPTION_LIMITS.image} characters.`);
1815
+ }
1816
+ if (o.attributes === void 0) return;
1817
+ const attributes = o.attributes;
1818
+ if (!isObject2(attributes) || Object.keys(attributes).length > OPTION_LIMITS.attributes) {
1819
+ issue(`${at}.attributes`, `Must be an object of at most ${OPTION_LIMITS.attributes} attributes.`);
1820
+ return;
1821
+ }
1822
+ const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
1823
+ for (const [name2, v] of Object.entries(attributes)) {
1824
+ if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
1825
+ issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
1826
+ } else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
1827
+ issue(
1828
+ `${at}.attributes.${name2}`,
1829
+ `Must be text, or a list of at most ${OPTION_LIMITS.attributeItems} texts, each at most ${OPTION_LIMITS.attributeValue} characters.`
1830
+ );
1831
+ }
1832
+ }
1833
+ };
1484
1834
  const name = (v, path, what) => {
1485
1835
  if (typeof v !== "string" || !v) {
1486
1836
  issue(path, `${what} must be a non-empty string.`);
@@ -1653,7 +2003,31 @@ function parseForm(input) {
1653
2003
  } else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
1654
2004
  warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
1655
2005
  }
2006
+ const filter = isObject2(from) ? from.filter : void 0;
2007
+ if (filter !== void 0) {
2008
+ const at = `${path}.props.optionsFrom.filter`;
2009
+ if (!isObject2(filter) || typeof filter.attribute !== "string" || !filter.attribute || filter.attribute.length > OPTION_LIMITS.attributeName || typeof filter.field !== "string" || !filter.field) {
2010
+ issue(at, `Must be { attribute, field }: an attribute name (at most ${OPTION_LIMITS.attributeName} characters) and an earlier question's key.`);
2011
+ } else if (!keys.has(filter.field)) {
2012
+ issue(`${at}.field`, `"${filter.field}" must be the key of an earlier question.`);
2013
+ }
2014
+ }
1656
2015
  }
2016
+ const prefill = isObject2(block.props) ? block.props.prefill : void 0;
2017
+ if (prefill !== void 0) {
2018
+ if (!isObject2(prefill) || !isPrefillParam(prefill.param)) {
2019
+ issue(
2020
+ `${path}.props.prefill`,
2021
+ `Must be { param, hideWhenPrefilled? }: a link parameter of letters, digits, "_", "-" or "." (at most ${MAX_PREFILL_PARAM_LENGTH} characters).`
2022
+ );
2023
+ } else {
2024
+ optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
2025
+ if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2026
+ warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
2027
+ }
2028
+ }
2029
+ }
2030
+ if (isObject2(block.props)) optionalBoolean(block.props.searchable, `${path}.props.searchable`);
1657
2031
  if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
1658
2032
  else if (block.props.options !== void 0) {
1659
2033
  const options = block.props.options;
@@ -1662,7 +2036,10 @@ function parseForm(input) {
1662
2036
  options.forEach((o, j) => {
1663
2037
  const at = `${path}.props.options[${j}]`;
1664
2038
  if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
1665
- else optionalText(o.label, `${at}.label`);
2039
+ else {
2040
+ optionalText(o.label, `${at}.label`);
2041
+ optionDetails(o, at);
2042
+ }
1666
2043
  });
1667
2044
  if (from === void 0) {
1668
2045
  choices.set(
@@ -1976,9 +2353,24 @@ var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !
1976
2353
  var choiceProps = (o) => {
1977
2354
  const out = {};
1978
2355
  if (o.options || !o.optionsFrom) {
1979
- out.options = (o.options ?? []).map((x) => x.label ? { value: x.value, label: x.label } : { value: x.value });
2356
+ out.options = (o.options ?? []).map(
2357
+ (x) => props({
2358
+ value: x.value,
2359
+ label: x.label || void 0,
2360
+ description: x.description,
2361
+ image: x.image,
2362
+ attributes: x.attributes
2363
+ })
2364
+ );
1980
2365
  }
1981
- if (o.optionsFrom) out.optionsFrom = { source: o.optionsFrom.source, key: o.optionsFrom.key };
2366
+ if (o.optionsFrom) {
2367
+ const { source, key, filter } = o.optionsFrom;
2368
+ out.optionsFrom = filter ? { source, key, filter: { attribute: filter.attribute, field: filter.field } } : { source, key };
2369
+ }
2370
+ if (o.prefill) {
2371
+ out.prefill = o.prefill.hideWhenPrefilled ? { param: o.prefill.param, hideWhenPrefilled: true } : { param: o.prefill.param };
2372
+ }
2373
+ if (o.searchable !== void 0) out.searchable = o.searchable;
1982
2374
  if (o.other) out.other = o.other === true ? true : props({ label: o.other.label });
1983
2375
  return out;
1984
2376
  };
@@ -2179,47 +2571,6 @@ ${list}`);
2179
2571
  };
2180
2572
  }
2181
2573
 
2182
- // src/hidden.ts
2183
- var MAX_HIDDEN_STRING_LENGTH = 1e3;
2184
- var DECIMAL = /^-?\d+(\.\d+)?$/;
2185
- function coerceHiddenValue(value, type) {
2186
- if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
2187
- if (type === "string") {
2188
- if (typeof value === "string") return value;
2189
- if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return String(value);
2190
- return void 0;
2191
- }
2192
- if (type === "number") {
2193
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
2194
- if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
2195
- const n = Number(value);
2196
- return Number.isFinite(n) ? n : void 0;
2197
- }
2198
- if (typeof value === "boolean") return value;
2199
- if (value === "true") return true;
2200
- if (value === "false") return false;
2201
- return void 0;
2202
- }
2203
- function coerceHidden(definition, raw, options = {}) {
2204
- const declared = new Map((definition.hidden ?? []).map((h) => [h.name, h]));
2205
- const values = {};
2206
- const ignored = [];
2207
- if (!isPlainObject(raw)) return { values, ignored };
2208
- for (const name of Object.keys(raw)) {
2209
- const value = raw[name];
2210
- if (value === void 0) continue;
2211
- const spec = declared.get(name);
2212
- if (!spec || options.sources && !options.sources.includes(spec.source)) {
2213
- ignored.push(name);
2214
- continue;
2215
- }
2216
- const coerced = coerceHiddenValue(value, spec.type);
2217
- if (coerced === void 0) ignored.push(name);
2218
- else Object.defineProperty(values, name, { value: coerced, enumerable: true, writable: true, configurable: true });
2219
- }
2220
- return { values, ignored };
2221
- }
2222
-
2223
2574
  // src/validate.ts
2224
2575
  var REQUIRED_MESSAGE = "This question needs an answer.";
2225
2576
  function runCustom(validator, value) {
@@ -2323,16 +2674,34 @@ function build(given, options, replaying = false, budget) {
2323
2674
  return at === void 0 ? event : { ...event, at };
2324
2675
  };
2325
2676
  const isVisibleIn = (s, id) => s.path.includes(id);
2677
+ const filters = hasOptionFilters(definition);
2678
+ const clearFiltered = (values, id) => filters ? clearFilteredAnswers(definition, values, id) : values;
2326
2679
  const errorsFor = (s, ids) => {
2327
2680
  const errors = {};
2681
+ let shown2;
2328
2682
  for (const id of ids) {
2329
2683
  const field2 = fields.get(id);
2330
2684
  if (!field2 || !isVisibleIn(s, id)) continue;
2331
- const message = validateField(field2, own(s.values, id), { custom: options.validators?.[id] });
2685
+ const value = own(s.values, id);
2686
+ let message = validateField(field2, value, { custom: options.validators?.[id] });
2687
+ if (!message && filters) message = filteredChoiceError(definition, field2, value, shown2 ??= answersOnPath(s));
2332
2688
  if (message) errors[id] = message;
2333
2689
  }
2334
2690
  return errors;
2335
2691
  };
2692
+ const skipped = (s) => new Set(hiddenByPrefill(definition, s));
2693
+ const shownFrom = (s, from, skip) => {
2694
+ for (let i = from; i < s.path.length; i++) if (!skip.has(s.path[i])) return s.path[i];
2695
+ return void 0;
2696
+ };
2697
+ const showCurrent = () => {
2698
+ if (!state.current) return;
2699
+ const skip = skipped(state);
2700
+ if (!skip.has(state.current)) return;
2701
+ const at = state.path.indexOf(state.current);
2702
+ const shown2 = at < 0 ? void 0 : shownFrom(state, at, skip);
2703
+ if (shown2) state = { ...state, current: shown2, furthest: state.furthest === state.current ? shown2 : state.furthest };
2704
+ };
2336
2705
  let unsettled = false;
2337
2706
  let formulasStale = false;
2338
2707
  const settleNow = (needValues = true) => {
@@ -2361,13 +2730,13 @@ function build(given, options, replaying = false, budget) {
2361
2730
  const errors = { ...state.errors };
2362
2731
  delete errors[id];
2363
2732
  if (batch) {
2364
- state.values = { ...state.values, [id]: value };
2733
+ state.values = clearFiltered({ ...state.values, [id]: value }, id);
2365
2734
  state.errors = errors;
2366
2735
  log(event);
2367
2736
  unsettled = true;
2368
2737
  return;
2369
2738
  }
2370
- const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
2739
+ const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
2371
2740
  emit({ ...state, values, path, errors, events: log(event) });
2372
2741
  };
2373
2742
  const engine = {
@@ -2398,7 +2767,7 @@ function build(given, options, replaying = false, budget) {
2398
2767
  return false;
2399
2768
  }
2400
2769
  const at = state.path.indexOf(state.current);
2401
- const target = state.path[at + 1];
2770
+ const target = shownFrom(state, at + 1, skipped(state));
2402
2771
  if (!target) {
2403
2772
  emit({ ...state, events });
2404
2773
  return true;
@@ -2410,8 +2779,9 @@ function build(given, options, replaying = false, budget) {
2410
2779
  back() {
2411
2780
  if (state.status === "completed") return;
2412
2781
  const history = [...state.history];
2782
+ const skip = skipped(state);
2413
2783
  let previous = history.pop();
2414
- while (previous && !state.path.includes(previous)) previous = history.pop();
2784
+ while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
2415
2785
  if (!previous) return;
2416
2786
  emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
2417
2787
  },
@@ -2439,15 +2809,18 @@ function build(given, options, replaying = false, budget) {
2439
2809
  return out;
2440
2810
  }
2441
2811
  };
2442
- if (options.initialValues) {
2812
+ const fromLink = options.linkParams ? prefillFromLink(definition, options.linkParams, options.initialValues) : {};
2813
+ const initialValues = Object.keys(fromLink).length ? { ...fromLink, ...options.initialValues } : options.initialValues;
2814
+ if (initialValues) {
2443
2815
  const wasBatch = batch;
2444
2816
  batch = true;
2445
- for (const [id, value] of Object.entries(options.initialValues)) {
2817
+ for (const [id, value] of Object.entries(initialValues)) {
2446
2818
  if (!fields.has(id) || value === void 0) continue;
2447
2819
  answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
2448
2820
  }
2449
2821
  settleNow();
2450
2822
  batch = wasBatch;
2823
+ showCurrent();
2451
2824
  }
2452
2825
  const resumeAt = options.resumeAt;
2453
2826
  if (resumeAt && !replaying && state.path.includes(resumeAt)) {
@@ -2466,18 +2839,22 @@ function build(given, options, replaying = false, budget) {
2466
2839
  }
2467
2840
  } else if (event.type === "next") {
2468
2841
  settleNow(false);
2842
+ showCurrent();
2469
2843
  if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
2470
2844
  engine.next();
2471
2845
  } else if (event.type === "back") {
2472
2846
  settleNow(false);
2847
+ showCurrent();
2473
2848
  engine.back();
2474
2849
  } else if (event.type === "submit") {
2475
2850
  settleNow();
2851
+ showCurrent();
2476
2852
  engine.submit();
2477
2853
  }
2478
2854
  };
2479
2855
  const finish = () => {
2480
2856
  settleNow();
2857
+ showCurrent();
2481
2858
  batch = false;
2482
2859
  state = { ...state, events: [...state.events] };
2483
2860
  };
@@ -2488,7 +2865,12 @@ function replay(definition, events, options = {}) {
2488
2865
  }
2489
2866
  function replayWithin(definition, events, options, maxWork) {
2490
2867
  const budget = void 0 ;
2491
- const { engine, apply, finish } = build(definition, { ...options, initialValues: void 0, clock: () => void 0 }, true, budget);
2868
+ const { engine, apply, finish } = build(
2869
+ definition,
2870
+ { ...options, initialValues: void 0, linkParams: void 0, clock: () => void 0 },
2871
+ true,
2872
+ budget
2873
+ );
2492
2874
  try {
2493
2875
  for (const event of events) apply(event);
2494
2876
  finish();
@@ -2660,6 +3042,22 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
2660
3042
  return `${left} ${op} ${right}`;
2661
3043
  }
2662
3044
 
2663
- export { CALCULATED_TYPE, COMPUTED_FIELD_TYPES, DEFAULT_PHOTO_FILES, DEFAULT_PHOTO_SIZE_MB, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_PHOTO_FILES, MAX_PHOTO_ID_LENGTH, MAX_PHOTO_NAME_LENGTH, MAX_PHOTO_SIZE_MB, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, OTHER_MAX_LENGTH, PHOTO_FORMATS, PHOTO_TYPE, REQUIRED_MESSAGE, STRUCTURED_FIELD_TYPES, UPLOAD_FIELD_TYPES, calculatedProps, calculatedValue, canonicalJson, choiceOptions, coerceHidden, coerceHiddenValue, computePath, createForm, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, fingerprint, formProgress, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, furthestReached, getFieldType, isEmptyValue, isFormEvent, isIsoDate, isJson, isOtherValue, isPhotoFormat, isPhotoRef, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, optionsSource, otherOption, parseForm, parseFormula, parseTimestamp, photoCountMessage, photoFormatList, photoLimits, photoRefs, photoSizeMessage, photoTypeMessage, ratingDisplay, recall, registerFieldType, renameFormulaKey, replay, ruleVisible, scaleLabels, steps, summarizeValue, tableColumns, tableLimits, validateField, when, withHostOptions };
3045
+ // src/carry.ts
3046
+ function carryAnswers(from, to, answers, extra = {}) {
3047
+ const byKey = /* @__PURE__ */ new Map();
3048
+ for (const block of from.blocks) {
3049
+ if (block.kind === "field" && answers[block.id] !== void 0) byKey.set(block.key, answers[block.id]);
3050
+ }
3051
+ for (const [key, value] of Object.entries(extra)) {
3052
+ if (value !== null && value !== void 0 && !byKey.has(key)) byKey.set(key, value);
3053
+ }
3054
+ const out = {};
3055
+ for (const block of to.blocks) {
3056
+ if (block.kind === "field" && byKey.has(block.key)) out[block.id] = byKey.get(block.key);
3057
+ }
3058
+ return out;
3059
+ }
3060
+
3061
+ export { CALCULATED_TYPE, CHOICE_FIELD_TYPES, COMPUTED_FIELD_TYPES, DEFAULT_PHOTO_FILES, DEFAULT_PHOTO_SIZE_MB, FORMULA_FUNCTIONS, HIGHLIGHT_NOTE_MESSAGE, MAX_CALCULATED_DECIMALS, MAX_FORMULA_ITEMS, MAX_FORMULA_LENGTH, MAX_FORM_FORMULA_LENGTH, MAX_HIDDEN_STRING_LENGTH, MAX_HIGHLIGHT_NOTE_LENGTH, MAX_HIGHLIGHT_QUOTE_LENGTH, MAX_PHOTO_FILES, MAX_PHOTO_ID_LENGTH, MAX_PHOTO_NAME_LENGTH, MAX_PHOTO_SIZE_MB, MAX_PREFILL_PARAM_LENGTH, MAX_SETTLE_ITEMS, MAX_TABLE_CELL_LENGTH, MAX_TABLE_COLUMNS, MAX_TIMESTAMP_SECONDS, OPTION_LIMITS, OTHER_MAX_LENGTH, PHOTO_FORMATS, PHOTO_TYPE, REQUIRED_MESSAGE, SEARCHABLE_OPTION_COUNT, STRUCTURED_FIELD_TYPES, UPLOAD_FIELD_TYPES, answerSnapshots, answersFromSnapshots, answersOnPath, calculatedProps, calculatedValue, canonicalJson, carryAnswers, choiceOptions, clearFilteredAnswers, coerceHidden, coerceHiddenValue, computePath, createForm, dateLimits, defineForm, describeCondition, evaluate, evaluateFormula, explain, f, filteredChoiceError, fingerprint, formProgress, formatCalculated, formatTimestamp, formulaInput, formulaNumber, formulaRefs, furthestReached, getFieldType, hasOptionFilters, hiddenByPrefill, isEmptyValue, isFormEvent, isHttpsUrl, isIsoDate, isJson, isOtherValue, isPhotoFormat, isPhotoRef, isPrefillParam, isSearchable, isSensitive, listFieldTypes, listLimits, normalizeValue, numberLimits, optionFilter, optionJson, optionMatches, optionsSource, otherOption, parseForm, parseFormula, parseTimestamp, photoCountMessage, photoFormatList, photoLimits, photoRefs, photoSizeMessage, photoTypeMessage, prefillFromLink, prefillSetting, ratingDisplay, readOption, recall, registerFieldType, renameFormulaKey, replay, respondentPath, ruleVisible, scaleLabels, searchOptions, steps, summarizeValue, tableColumns, tableLimits, validateField, visibleOptions, when, withHostOptions };
2664
3062
  //# sourceMappingURL=index.js.map
2665
3063
  //# sourceMappingURL=index.js.map