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