@marktiderman/genesis-forms 0.4.2 → 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
@@ -835,6 +835,81 @@ var structuredFieldTypes = [
835
835
  }
836
836
  ];
837
837
 
838
+ // src/photo.ts
839
+ var PHOTO_TYPE = "photo";
840
+ var UPLOAD_FIELD_TYPES = [PHOTO_TYPE];
841
+ var PHOTO_FORMATS = {
842
+ jpeg: ["image/jpeg"],
843
+ png: ["image/png"],
844
+ heic: ["image/heic", "image/heif"],
845
+ webp: ["image/webp"]
846
+ };
847
+ var FORMAT_NAMES = { jpeg: "JPEG", png: "PNG", heic: "HEIC", webp: "WebP" };
848
+ var ALL_FORMATS = Object.keys(PHOTO_FORMATS);
849
+ var DEFAULT_PHOTO_FILES = 3;
850
+ var MAX_PHOTO_FILES = 5;
851
+ var DEFAULT_PHOTO_SIZE_MB = 10;
852
+ var MAX_PHOTO_SIZE_MB = 25;
853
+ var MAX_PHOTO_ID_LENGTH = 200;
854
+ var MAX_PHOTO_NAME_LENGTH = 255;
855
+ var isPhotoFormat = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(PHOTO_FORMATS, v);
856
+ var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
857
+ function photoLimits(field2) {
858
+ const maxFiles = wholeIn(field2.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field2.props.maxFiles : DEFAULT_PHOTO_FILES;
859
+ const maxSizeMb = wholeIn(field2.props?.maxSizeMb, 1, MAX_PHOTO_SIZE_MB) ? field2.props.maxSizeMb : DEFAULT_PHOTO_SIZE_MB;
860
+ const raw = field2.props?.formats;
861
+ const picked = Array.isArray(raw) ? ALL_FORMATS.filter((f2) => raw.includes(f2)) : [];
862
+ const formats = picked.length ? picked : [...ALL_FORMATS];
863
+ return {
864
+ maxFiles,
865
+ maxSizeMb,
866
+ maxBytes: maxSizeMb * 1024 * 1024,
867
+ formats,
868
+ mimeTypes: formats.flatMap((f2) => PHOTO_FORMATS[f2])
869
+ };
870
+ }
871
+ function isPhotoRef(v) {
872
+ if (!v || typeof v !== "object" || Array.isArray(v)) return false;
873
+ const r = v;
874
+ return typeof r.id === "string" && r.id.length > 0 && r.id.length <= MAX_PHOTO_ID_LENGTH && typeof r.name === "string" && r.name.length <= MAX_PHOTO_NAME_LENGTH && typeof r.type === "string" && typeof r.size === "number" && Number.isInteger(r.size) && r.size > 0;
875
+ }
876
+ function photoRefs(value) {
877
+ if (!Array.isArray(value)) return [];
878
+ const refs = [];
879
+ for (const r of value) if (isPhotoRef(r)) refs.push({ id: r.id, name: r.name, type: r.type, size: r.size });
880
+ return refs;
881
+ }
882
+ var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
883
+ function photoFormatList(formats) {
884
+ const names = formats.map((f2) => FORMAT_NAMES[f2]);
885
+ return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
886
+ }
887
+ var photoTypeMessage = (field2) => `Use a ${photoFormatList(photoLimits(field2).formats)} photo.`;
888
+ var photoSizeMessage = (field2) => `Each photo must be ${photoLimits(field2).maxSizeMb} MB or smaller.`;
889
+ var photoCountMessage = (field2) => `Add at most ${plural2(photoLimits(field2).maxFiles, "photo")}.`;
890
+ var photoFieldType = {
891
+ type: PHOTO_TYPE,
892
+ label: "Photo",
893
+ category: "media",
894
+ value: { json: true },
895
+ operators: ["answered", "empty"],
896
+ a11y: { semantics: "group" },
897
+ isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
898
+ validate: (value, field2) => {
899
+ if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
900
+ const limits = photoLimits(field2);
901
+ if (value.length > limits.maxFiles) return photoCountMessage(field2);
902
+ const refs = value;
903
+ if (new Set(refs.map((r) => r.id)).size !== refs.length) return "Each photo can be added once.";
904
+ if (refs.some((r) => !limits.mimeTypes.includes(r.type.toLowerCase()))) return photoTypeMessage(field2);
905
+ if (refs.some((r) => r.size > limits.maxBytes)) return photoSizeMessage(field2);
906
+ return null;
907
+ },
908
+ normalize: (value) => Array.isArray(value) && value.every(isPhotoRef) ? photoRefs(value) : value,
909
+ summarize: (value) => photoRefs(value).map((r) => r.name || "Photo").join(", "),
910
+ samples: () => [[{ id: "sample-photo", name: "photo.jpg", type: "image/jpeg", size: 1024 }]]
911
+ };
912
+
838
913
  // src/fields.ts
839
914
  var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
840
915
  var globalScope = globalThis;
@@ -877,12 +952,127 @@ function summarizeValue(value, field2) {
877
952
  if (Array.isArray(value)) return value.join(", ");
878
953
  return String(value);
879
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
+ }
1014
+ var OTHER_MAX_LENGTH = 200;
1015
+ function otherOption(field2) {
1016
+ const other = field2.props?.other;
1017
+ if (other === true) return { label: "Other" };
1018
+ if (other && typeof other === "object" && !Array.isArray(other)) {
1019
+ const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
1020
+ return { label };
1021
+ }
1022
+ return null;
1023
+ }
1024
+ function isOtherValue(field2, value) {
1025
+ return otherOption(field2) !== null && typeof value === "string" && !choiceOptions(field2).some((o) => o.value === value);
1026
+ }
1027
+ var typedOther = (field2, value) => isOtherValue(field2, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
880
1028
  function choiceOptions(field2) {
881
1029
  const raw = field2.props?.options;
882
1030
  if (!Array.isArray(raw)) return [];
883
- return raw.flatMap(
884
- (o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
885
- );
1031
+ return raw.flatMap((o) => {
1032
+ const option = readOption(o);
1033
+ return option ? [option] : [];
1034
+ });
1035
+ }
1036
+ function optionsSource(field2) {
1037
+ const raw = field2.props?.optionsFrom;
1038
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return 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;
1046
+ }
1047
+ function hostList(hostOptions, key) {
1048
+ if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
1049
+ const list = hostOptions[key];
1050
+ if (!Array.isArray(list)) return [];
1051
+ const seen = /* @__PURE__ */ new Set();
1052
+ const out = [];
1053
+ for (const o of list) {
1054
+ const option = readOption(o);
1055
+ if (!option || !option.value || seen.has(option.value)) continue;
1056
+ seen.add(option.value);
1057
+ out.push(option);
1058
+ }
1059
+ return out;
1060
+ }
1061
+ function withHostOptions(definition, hostOptions) {
1062
+ let changed = false;
1063
+ const blocks = definition.blocks.map((block) => {
1064
+ if (block.kind !== "field") return block;
1065
+ const from = optionsSource(block);
1066
+ if (!from) return block;
1067
+ const own2 = choiceOptions(block);
1068
+ const taken = new Set(own2.map((o) => o.value));
1069
+ const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
1070
+ if (!added.length) return block;
1071
+ changed = true;
1072
+ const options = [...own2, ...added].map(optionJson);
1073
+ return { ...block, props: { ...block.props, options } };
1074
+ });
1075
+ return changed ? { ...definition, blocks } : definition;
886
1076
  }
887
1077
  function optionLabel(field2, value) {
888
1078
  const option = choiceOptions(field2).find((o) => o.value === value);
@@ -1002,7 +1192,7 @@ var builtins = [
1002
1192
  value: { json: true },
1003
1193
  operators: ["eq", "neq", "in", "answered", "empty"],
1004
1194
  a11y: { semantics: "group" },
1005
- validate: (value, field2) => typeof value === "string" && choiceOptions(field2).some((o) => o.value === value) ? null : "Choose one of the options.",
1195
+ validate: (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.",
1006
1196
  summarize: (value, field2) => optionLabel(field2, String(value))
1007
1197
  },
1008
1198
  {
@@ -1014,7 +1204,8 @@ var builtins = [
1014
1204
  a11y: { semantics: "group" },
1015
1205
  validate: (value, field2) => {
1016
1206
  const allowed = new Set(choiceOptions(field2).map((o) => o.value));
1017
- return Array.isArray(value) && value.length <= allowed.size && new Set(value).size === value.length && value.every((v) => typeof v === "string" && allowed.has(v)) ? null : "Choose from the options.";
1207
+ const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
1208
+ return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field2, others[0])) ? null : "Choose from the options.";
1018
1209
  },
1019
1210
  summarize: (value, field2) => Array.isArray(value) ? value.map((v) => optionLabel(field2, String(v))).join(", ") : String(value)
1020
1211
  },
@@ -1035,7 +1226,7 @@ var builtins = [
1035
1226
  value: { json: true },
1036
1227
  operators: ["eq", "neq", "in", "answered", "empty"],
1037
1228
  a11y: { semantics: "control" },
1038
- validate: (value, field2) => typeof value === "string" && choiceOptions(field2).some((o) => o.value === value) ? null : "Choose one of the options.",
1229
+ validate: (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.",
1039
1230
  summarize: (value, field2) => optionLabel(field2, String(value))
1040
1231
  },
1041
1232
  {
@@ -1145,7 +1336,48 @@ function dateLimits(field2) {
1145
1336
  if (typeof max === "string" && isIsoDate(max)) out.max = max;
1146
1337
  return out;
1147
1338
  }
1148
- for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
1339
+ for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
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
+ }
1149
1381
 
1150
1382
  // src/logic.ts
1151
1383
  function own(record, key) {
@@ -1278,6 +1510,225 @@ function fieldMap(def) {
1278
1510
  return map;
1279
1511
  }
1280
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
+
1281
1732
  // src/recall.ts
1282
1733
  var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
1283
1734
  function recall(text, scope) {
@@ -1357,6 +1808,31 @@ function parseForm(input) {
1357
1808
  const optionalBoolean = (v, path) => {
1358
1809
  if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
1359
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
+ };
1360
1836
  const name = (v, path, what) => {
1361
1837
  if (typeof v !== "string" || !v) {
1362
1838
  issue(path, `${what} must be a non-empty string.`);
@@ -1445,6 +1921,22 @@ function parseForm(input) {
1445
1921
  }
1446
1922
  }
1447
1923
  if (type === "consent") optionalText(props2.text, `${path}.text`);
1924
+ if (type === PHOTO_TYPE) {
1925
+ if (props2.maxFiles !== void 0 && !(isInt(props2.maxFiles) && props2.maxFiles >= 1 && props2.maxFiles <= MAX_PHOTO_FILES)) {
1926
+ issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
1927
+ }
1928
+ if (props2.maxSizeMb !== void 0 && !(isInt(props2.maxSizeMb) && props2.maxSizeMb >= 1 && props2.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
1929
+ issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
1930
+ }
1931
+ if (props2.formats !== void 0) {
1932
+ const formats = props2.formats;
1933
+ if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
1934
+ issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
1935
+ } else if (new Set(formats).size !== formats.length) {
1936
+ issue(`${path}.formats`, "Name each format once.");
1937
+ }
1938
+ }
1939
+ }
1448
1940
  };
1449
1941
  if (!isObject2(input)) {
1450
1942
  issue("", "A form definition must be an object.");
@@ -1465,6 +1957,7 @@ function parseForm(input) {
1465
1957
  else {
1466
1958
  optionalBoolean(input.settings.anonymous, "settings.anonymous");
1467
1959
  optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
1960
+ optionalBoolean(input.settings.saveProgress, "settings.saveProgress");
1468
1961
  }
1469
1962
  }
1470
1963
  if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
@@ -1505,6 +1998,38 @@ function parseForm(input) {
1505
1998
  issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
1506
1999
  }
1507
2000
  if (block.props !== void 0) {
2001
+ const from = isObject2(block.props) ? block.props.optionsFrom : void 0;
2002
+ if (from !== void 0) {
2003
+ if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
2004
+ issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
2005
+ } else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2006
+ warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
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
+ }
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`);
1508
2033
  if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
1509
2034
  else if (block.props.options !== void 0) {
1510
2035
  const options = block.props.options;
@@ -1513,12 +2038,25 @@ function parseForm(input) {
1513
2038
  options.forEach((o, j) => {
1514
2039
  const at = `${path}.props.options[${j}]`;
1515
2040
  if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
1516
- else optionalText(o.label, `${at}.label`);
2041
+ else {
2042
+ optionalText(o.label, `${at}.label`);
2043
+ optionDetails(o, at);
2044
+ }
1517
2045
  });
1518
- choices.set(
1519
- block.id,
1520
- new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
1521
- );
2046
+ if (from === void 0) {
2047
+ choices.set(
2048
+ block.id,
2049
+ new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
2050
+ );
2051
+ }
2052
+ }
2053
+ }
2054
+ const other = isObject2(block.props) ? block.props.other : void 0;
2055
+ if (other !== void 0) {
2056
+ if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
2057
+ issue(`${path}.props.other`, "Must be true, false or { label }.");
2058
+ } else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
2059
+ warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
1522
2060
  }
1523
2061
  }
1524
2062
  const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
@@ -1593,6 +2131,8 @@ function parseForm(input) {
1593
2131
  } else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
1594
2132
  else if (targetType === "media-notes" || targetType === "text-highlights") {
1595
2133
  warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
2134
+ } else if (targetType === PHOTO_TYPE) {
2135
+ warnings.push({ path, message: `"${ref.key}" holds photos, not numbers; the formula reads it as blank.` });
1596
2136
  }
1597
2137
  }
1598
2138
  });
@@ -1812,9 +2352,30 @@ function field(type, key, options, props2) {
1812
2352
  return block;
1813
2353
  }
1814
2354
  var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
1815
- var choiceProps = (options) => ({
1816
- options: options.map((o) => o.label ? { value: o.value, label: o.label } : { value: o.value })
1817
- });
2355
+ var choiceProps = (o) => {
2356
+ const out = {};
2357
+ if (o.options || !o.optionsFrom) {
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
+ );
2367
+ }
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;
2376
+ if (o.other) out.other = o.other === true ? true : props({ label: o.other.label });
2377
+ return out;
2378
+ };
1818
2379
  var hiddenSpec = (type, o) => ({
1819
2380
  type,
1820
2381
  source: o.source ?? "url",
@@ -1838,11 +2399,11 @@ var f = {
1838
2399
  ),
1839
2400
  /** A 0-to-10 Net Promoter Score. The end labels default to "Not likely" and "Very likely" in the renderer. */
1840
2401
  nps: (key, o) => field("nps", key, o, props({ lowLabel: o.lowLabel || void 0, highLabel: o.highLabel || void 0 })),
1841
- singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o.options)),
1842
- multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o.options)),
2402
+ singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o)),
2403
+ multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o)),
1843
2404
  yesNo: (key, o) => field("yes-no", key, o),
1844
2405
  /** One option from a compact menu: for a long list (a country, a class), where single choice would be a wall of buttons. */
1845
- dropdown: (key, o) => field("dropdown", key, o, choiceProps(o.options)),
2406
+ dropdown: (key, o) => field("dropdown", key, o, choiceProps(o)),
1846
2407
  /** A number, optionally between `min` and `max` and on a `step` (from `min`, else 0): `step: 1` for whole numbers. */
1847
2408
  number: (key, o) => field("number", key, o, props({ min: o.min, max: o.max, step: o.step })),
1848
2409
  /** A calendar day, stored as `YYYY-MM-DD`, optionally between `min` and `max` (also `YYYY-MM-DD`). */
@@ -1894,6 +2455,17 @@ var f = {
1894
2455
  o,
1895
2456
  props({ text: o.text, maxHighlights: o.maxHighlights })
1896
2457
  ),
2458
+ /**
2459
+ * Up to a few photos (a picture of broken equipment). The answer holds the
2460
+ * references the host's upload returned, never the bytes; `formats` from
2461
+ * jpeg, png, heic and webp (default all four).
2462
+ */
2463
+ photo: (key, o) => field(
2464
+ "photo",
2465
+ key,
2466
+ o,
2467
+ props({ maxFiles: o.maxFiles, maxSizeMb: o.maxSizeMb, formats: o.formats ? [...o.formats] : void 0 })
2468
+ ),
1897
2469
  /** Any registered field type, including plugins. */
1898
2470
  custom: (type, key, o) => field(type, key, o, o.props),
1899
2471
  heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
@@ -2001,47 +2573,6 @@ ${list}`);
2001
2573
  };
2002
2574
  }
2003
2575
 
2004
- // src/hidden.ts
2005
- var MAX_HIDDEN_STRING_LENGTH = 1e3;
2006
- var DECIMAL = /^-?\d+(\.\d+)?$/;
2007
- function coerceHiddenValue(value, type) {
2008
- if (typeof value === "string" && value.length > MAX_HIDDEN_STRING_LENGTH) return void 0;
2009
- if (type === "string") {
2010
- if (typeof value === "string") return value;
2011
- if (typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) return String(value);
2012
- return void 0;
2013
- }
2014
- if (type === "number") {
2015
- if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
2016
- if (typeof value !== "string" || !DECIMAL.test(value)) return void 0;
2017
- const n = Number(value);
2018
- return Number.isFinite(n) ? n : void 0;
2019
- }
2020
- if (typeof value === "boolean") return value;
2021
- if (value === "true") return true;
2022
- if (value === "false") return false;
2023
- return void 0;
2024
- }
2025
- function coerceHidden(definition, raw, options = {}) {
2026
- const declared = new Map((definition.hidden ?? []).map((h) => [h.name, h]));
2027
- const values = {};
2028
- const ignored = [];
2029
- if (!isPlainObject(raw)) return { values, ignored };
2030
- for (const name of Object.keys(raw)) {
2031
- const value = raw[name];
2032
- if (value === void 0) continue;
2033
- const spec = declared.get(name);
2034
- if (!spec || options.sources && !options.sources.includes(spec.source)) {
2035
- ignored.push(name);
2036
- continue;
2037
- }
2038
- const coerced = coerceHiddenValue(value, spec.type);
2039
- if (coerced === void 0) ignored.push(name);
2040
- else Object.defineProperty(values, name, { value: coerced, enumerable: true, writable: true, configurable: true });
2041
- }
2042
- return { values, ignored };
2043
- }
2044
-
2045
2576
  // src/validate.ts
2046
2577
  var REQUIRED_MESSAGE = "This question needs an answer.";
2047
2578
  function runCustom(validator, value) {
@@ -2077,7 +2608,8 @@ function readsAny(node, ids) {
2077
2608
  function createForm(definition, options = {}) {
2078
2609
  return build(definition, options).engine;
2079
2610
  }
2080
- function build(definition, options, replaying = false, budget) {
2611
+ function build(given, options, replaying = false, budget) {
2612
+ const definition = withHostOptions(given, options.hostOptions);
2081
2613
  const fields = fieldMap(definition);
2082
2614
  const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
2083
2615
  const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
@@ -2123,6 +2655,7 @@ function build(definition, options, replaying = false, budget) {
2123
2655
  path: first.path,
2124
2656
  current: first.path[0] ?? null,
2125
2657
  history: [],
2658
+ furthest: first.path[0] ?? null,
2126
2659
  errors: {},
2127
2660
  status: "in_progress",
2128
2661
  ending: null,
@@ -2143,16 +2676,34 @@ function build(definition, options, replaying = false, budget) {
2143
2676
  return at === void 0 ? event : { ...event, at };
2144
2677
  };
2145
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;
2146
2681
  const errorsFor = (s, ids) => {
2147
2682
  const errors = {};
2683
+ let shown2;
2148
2684
  for (const id of ids) {
2149
2685
  const field2 = fields.get(id);
2150
2686
  if (!field2 || !isVisibleIn(s, id)) continue;
2151
- 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));
2152
2690
  if (message) errors[id] = message;
2153
2691
  }
2154
2692
  return errors;
2155
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
+ };
2156
2707
  let unsettled = false;
2157
2708
  let formulasStale = false;
2158
2709
  const settleNow = (needValues = true) => {
@@ -2181,13 +2732,13 @@ function build(definition, options, replaying = false, budget) {
2181
2732
  const errors = { ...state.errors };
2182
2733
  delete errors[id];
2183
2734
  if (batch) {
2184
- state.values = { ...state.values, [id]: value };
2735
+ state.values = clearFiltered({ ...state.values, [id]: value }, id);
2185
2736
  state.errors = errors;
2186
2737
  log(event);
2187
2738
  unsettled = true;
2188
2739
  return;
2189
2740
  }
2190
- const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
2741
+ const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
2191
2742
  emit({ ...state, values, path, errors, events: log(event) });
2192
2743
  };
2193
2744
  const engine = {
@@ -2218,19 +2769,21 @@ function build(definition, options, replaying = false, budget) {
2218
2769
  return false;
2219
2770
  }
2220
2771
  const at = state.path.indexOf(state.current);
2221
- const target = state.path[at + 1];
2772
+ const target = shownFrom(state, at + 1, skipped(state));
2222
2773
  if (!target) {
2223
2774
  emit({ ...state, events });
2224
2775
  return true;
2225
2776
  }
2226
- emit({ ...state, current: target, history: [...state.history, state.current], events });
2777
+ const furthest = state.furthest && state.path.indexOf(state.furthest) >= state.path.indexOf(target) ? state.furthest : target;
2778
+ emit({ ...state, current: target, history: [...state.history, state.current], furthest, events });
2227
2779
  return true;
2228
2780
  },
2229
2781
  back() {
2230
2782
  if (state.status === "completed") return;
2231
2783
  const history = [...state.history];
2784
+ const skip = skipped(state);
2232
2785
  let previous = history.pop();
2233
- while (previous && !state.path.includes(previous)) previous = history.pop();
2786
+ while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
2234
2787
  if (!previous) return;
2235
2788
  emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
2236
2789
  },
@@ -2258,15 +2811,27 @@ function build(definition, options, replaying = false, budget) {
2258
2811
  return out;
2259
2812
  }
2260
2813
  };
2261
- 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) {
2262
2817
  const wasBatch = batch;
2263
2818
  batch = true;
2264
- for (const [id, value] of Object.entries(options.initialValues)) {
2819
+ for (const [id, value] of Object.entries(initialValues)) {
2265
2820
  if (!fields.has(id) || value === void 0) continue;
2266
2821
  answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
2267
2822
  }
2268
2823
  settleNow();
2269
2824
  batch = wasBatch;
2825
+ showCurrent();
2826
+ }
2827
+ const resumeAt = options.resumeAt;
2828
+ if (resumeAt && !replaying && state.path.includes(resumeAt)) {
2829
+ for (let i = 0; i < state.path.length && state.current !== resumeAt; i++) {
2830
+ if (!engine.next()) {
2831
+ state = { ...state, errors: {} };
2832
+ break;
2833
+ }
2834
+ }
2270
2835
  }
2271
2836
  const apply = (event) => {
2272
2837
  if (event.type === "answer") {
@@ -2276,18 +2841,22 @@ function build(definition, options, replaying = false, budget) {
2276
2841
  }
2277
2842
  } else if (event.type === "next") {
2278
2843
  settleNow(false);
2844
+ showCurrent();
2279
2845
  if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
2280
2846
  engine.next();
2281
2847
  } else if (event.type === "back") {
2282
2848
  settleNow(false);
2849
+ showCurrent();
2283
2850
  engine.back();
2284
2851
  } else if (event.type === "submit") {
2285
2852
  settleNow();
2853
+ showCurrent();
2286
2854
  engine.submit();
2287
2855
  }
2288
2856
  };
2289
2857
  const finish = () => {
2290
2858
  settleNow();
2859
+ showCurrent();
2291
2860
  batch = false;
2292
2861
  state = { ...state, events: [...state.events] };
2293
2862
  };
@@ -2298,7 +2867,12 @@ function replay(definition, events, options = {}) {
2298
2867
  }
2299
2868
  function replayWithin(definition, events, options, maxWork) {
2300
2869
  const budget = void 0 ;
2301
- 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
+ );
2302
2876
  try {
2303
2877
  for (const event of events) apply(event);
2304
2878
  finish();
@@ -2307,6 +2881,33 @@ function replayWithin(definition, events, options, maxWork) {
2307
2881
  return engine;
2308
2882
  }
2309
2883
 
2884
+ // src/progress.ts
2885
+ function furthestReached(state, definition) {
2886
+ const at = (id) => id ? state.path.indexOf(id) : -1;
2887
+ let furthest = Math.max(at(state.current), at(state.furthest));
2888
+ for (const id of state.history) furthest = Math.max(furthest, at(id));
2889
+ const fields = fieldMap(definition);
2890
+ for (let i = state.path.length - 1; i > furthest; i--) {
2891
+ const field2 = fields.get(state.path[i]);
2892
+ if (field2 && !isEmptyValue(own(state.values, field2.id), field2)) {
2893
+ furthest = i;
2894
+ break;
2895
+ }
2896
+ }
2897
+ return furthest >= 0 ? state.path[furthest] : null;
2898
+ }
2899
+ function formProgress(engine) {
2900
+ const state = engine.getState();
2901
+ const furthest = furthestReached(state, engine.definition);
2902
+ const index = furthest ? state.path.indexOf(furthest) : -1;
2903
+ const fields = fieldMap(engine.definition);
2904
+ let fieldId = null;
2905
+ for (let i = index; i >= 0 && fieldId === null; i--) {
2906
+ if (fields.has(state.path[i])) fieldId = state.path[i];
2907
+ }
2908
+ return { step: index + 1, total: state.path.length, fieldId };
2909
+ }
2910
+
2310
2911
  // src/explain.ts
2311
2912
  function explain(source, blockId, options = {}) {
2312
2913
  const input = "getState" in source ? { definition: source.definition, values: source.getState().values, hidden: source.getState().hidden } : source;
@@ -2443,8 +3044,27 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
2443
3044
  return `${left} ${op} ${right}`;
2444
3045
  }
2445
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
+
2446
3063
  exports.CALCULATED_TYPE = CALCULATED_TYPE;
3064
+ exports.CHOICE_FIELD_TYPES = CHOICE_FIELD_TYPES;
2447
3065
  exports.COMPUTED_FIELD_TYPES = COMPUTED_FIELD_TYPES;
3066
+ exports.DEFAULT_PHOTO_FILES = DEFAULT_PHOTO_FILES;
3067
+ exports.DEFAULT_PHOTO_SIZE_MB = DEFAULT_PHOTO_SIZE_MB;
2448
3068
  exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
2449
3069
  exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
2450
3070
  exports.MAX_CALCULATED_DECIMALS = MAX_CALCULATED_DECIMALS;
@@ -2454,16 +3074,32 @@ exports.MAX_FORM_FORMULA_LENGTH = MAX_FORM_FORMULA_LENGTH;
2454
3074
  exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
2455
3075
  exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
2456
3076
  exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
3077
+ exports.MAX_PHOTO_FILES = MAX_PHOTO_FILES;
3078
+ exports.MAX_PHOTO_ID_LENGTH = MAX_PHOTO_ID_LENGTH;
3079
+ exports.MAX_PHOTO_NAME_LENGTH = MAX_PHOTO_NAME_LENGTH;
3080
+ exports.MAX_PHOTO_SIZE_MB = MAX_PHOTO_SIZE_MB;
3081
+ exports.MAX_PREFILL_PARAM_LENGTH = MAX_PREFILL_PARAM_LENGTH;
2457
3082
  exports.MAX_SETTLE_ITEMS = MAX_SETTLE_ITEMS;
2458
3083
  exports.MAX_TABLE_CELL_LENGTH = MAX_TABLE_CELL_LENGTH;
2459
3084
  exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
2460
3085
  exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;
3086
+ exports.OPTION_LIMITS = OPTION_LIMITS;
3087
+ exports.OTHER_MAX_LENGTH = OTHER_MAX_LENGTH;
3088
+ exports.PHOTO_FORMATS = PHOTO_FORMATS;
3089
+ exports.PHOTO_TYPE = PHOTO_TYPE;
2461
3090
  exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;
3091
+ exports.SEARCHABLE_OPTION_COUNT = SEARCHABLE_OPTION_COUNT;
2462
3092
  exports.STRUCTURED_FIELD_TYPES = STRUCTURED_FIELD_TYPES;
3093
+ exports.UPLOAD_FIELD_TYPES = UPLOAD_FIELD_TYPES;
3094
+ exports.answerSnapshots = answerSnapshots;
3095
+ exports.answersFromSnapshots = answersFromSnapshots;
3096
+ exports.answersOnPath = answersOnPath;
2463
3097
  exports.calculatedProps = calculatedProps;
2464
3098
  exports.calculatedValue = calculatedValue;
2465
3099
  exports.canonicalJson = canonicalJson;
3100
+ exports.carryAnswers = carryAnswers;
2466
3101
  exports.choiceOptions = choiceOptions;
3102
+ exports.clearFilteredAnswers = clearFilteredAnswers;
2467
3103
  exports.coerceHidden = coerceHidden;
2468
3104
  exports.coerceHiddenValue = coerceHiddenValue;
2469
3105
  exports.computePath = computePath;
@@ -2475,37 +3111,66 @@ exports.evaluate = evaluate;
2475
3111
  exports.evaluateFormula = evaluateFormula;
2476
3112
  exports.explain = explain;
2477
3113
  exports.f = f;
3114
+ exports.filteredChoiceError = filteredChoiceError;
2478
3115
  exports.fingerprint = fingerprint;
3116
+ exports.formProgress = formProgress;
2479
3117
  exports.formatCalculated = formatCalculated;
2480
3118
  exports.formatTimestamp = formatTimestamp;
2481
3119
  exports.formulaInput = formulaInput;
2482
3120
  exports.formulaNumber = formulaNumber;
2483
3121
  exports.formulaRefs = formulaRefs;
3122
+ exports.furthestReached = furthestReached;
2484
3123
  exports.getFieldType = getFieldType;
3124
+ exports.hasOptionFilters = hasOptionFilters;
3125
+ exports.hiddenByPrefill = hiddenByPrefill;
2485
3126
  exports.isEmptyValue = isEmptyValue;
2486
3127
  exports.isFormEvent = isFormEvent;
3128
+ exports.isHttpsUrl = isHttpsUrl;
2487
3129
  exports.isIsoDate = isIsoDate;
2488
3130
  exports.isJson = isJson;
3131
+ exports.isOtherValue = isOtherValue;
3132
+ exports.isPhotoFormat = isPhotoFormat;
3133
+ exports.isPhotoRef = isPhotoRef;
3134
+ exports.isPrefillParam = isPrefillParam;
3135
+ exports.isSearchable = isSearchable;
2489
3136
  exports.isSensitive = isSensitive;
2490
3137
  exports.listFieldTypes = listFieldTypes;
2491
3138
  exports.listLimits = listLimits;
2492
3139
  exports.normalizeValue = normalizeValue;
2493
3140
  exports.numberLimits = numberLimits;
3141
+ exports.optionFilter = optionFilter;
3142
+ exports.optionJson = optionJson;
3143
+ exports.optionMatches = optionMatches;
3144
+ exports.optionsSource = optionsSource;
3145
+ exports.otherOption = otherOption;
2494
3146
  exports.parseForm = parseForm;
2495
3147
  exports.parseFormula = parseFormula;
2496
3148
  exports.parseTimestamp = parseTimestamp;
3149
+ exports.photoCountMessage = photoCountMessage;
3150
+ exports.photoFormatList = photoFormatList;
3151
+ exports.photoLimits = photoLimits;
3152
+ exports.photoRefs = photoRefs;
3153
+ exports.photoSizeMessage = photoSizeMessage;
3154
+ exports.photoTypeMessage = photoTypeMessage;
3155
+ exports.prefillFromLink = prefillFromLink;
3156
+ exports.prefillSetting = prefillSetting;
2497
3157
  exports.ratingDisplay = ratingDisplay;
3158
+ exports.readOption = readOption;
2498
3159
  exports.recall = recall;
2499
3160
  exports.registerFieldType = registerFieldType;
2500
3161
  exports.renameFormulaKey = renameFormulaKey;
2501
3162
  exports.replay = replay;
3163
+ exports.respondentPath = respondentPath;
2502
3164
  exports.ruleVisible = ruleVisible;
2503
3165
  exports.scaleLabels = scaleLabels;
3166
+ exports.searchOptions = searchOptions;
2504
3167
  exports.steps = steps;
2505
3168
  exports.summarizeValue = summarizeValue;
2506
3169
  exports.tableColumns = tableColumns;
2507
3170
  exports.tableLimits = tableLimits;
2508
3171
  exports.validateField = validateField;
3172
+ exports.visibleOptions = visibleOptions;
2509
3173
  exports.when = when;
3174
+ exports.withHostOptions = withHostOptions;
2510
3175
  //# sourceMappingURL=index.cjs.map
2511
3176
  //# sourceMappingURL=index.cjs.map