@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/README.md +195 -0
- package/dist/destinations.cjs +147 -7
- package/dist/destinations.cjs.map +1 -1
- package/dist/destinations.d.cts +1 -1
- package/dist/destinations.d.ts +1 -1
- package/dist/destinations.js +147 -7
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-BkNuzlna.d.cts → engine-CpfOz8FN.d.ts} +33 -3
- package/dist/{engine-Dz9J5ej2.d.ts → engine-DC4YTPdr.d.cts} +33 -3
- package/dist/index.cjs +734 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +142 -75
- package/dist/index.d.ts +142 -75
- package/dist/index.js +687 -70
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +721 -26
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +201 -4
- package/dist/server.d.ts +201 -4
- package/dist/server.js +702 -27
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +494 -26
- package/dist/testing.cjs.map +1 -1
- package/dist/testing.d.cts +4 -4
- package/dist/testing.d.ts +4 -4
- package/dist/testing.js +494 -26
- package/dist/testing.js.map +1 -1
- package/dist/{types-BBKBQ8sp.d.cts → types-oAckGR_Z.d.cts} +31 -1
- package/dist/{types-BBKBQ8sp.d.ts → types-oAckGR_Z.d.ts} +31 -1
- package/dist/validate-B5WKHbst.d.cts +322 -0
- package/dist/validate-GpTXPwyl.d.ts +322 -0
- package/package.json +1 -1
- package/dist/validate-BXXqi0fq.d.cts +0 -30
- package/dist/validate-BjNwF1UE.d.ts +0 -30
package/dist/testing.cjs
CHANGED
|
@@ -721,6 +721,80 @@ var structuredFieldTypes = [
|
|
|
721
721
|
}
|
|
722
722
|
];
|
|
723
723
|
|
|
724
|
+
// src/photo.ts
|
|
725
|
+
var PHOTO_TYPE = "photo";
|
|
726
|
+
var PHOTO_FORMATS = {
|
|
727
|
+
jpeg: ["image/jpeg"],
|
|
728
|
+
png: ["image/png"],
|
|
729
|
+
heic: ["image/heic", "image/heif"],
|
|
730
|
+
webp: ["image/webp"]
|
|
731
|
+
};
|
|
732
|
+
var FORMAT_NAMES = { jpeg: "JPEG", png: "PNG", heic: "HEIC", webp: "WebP" };
|
|
733
|
+
var ALL_FORMATS = Object.keys(PHOTO_FORMATS);
|
|
734
|
+
var DEFAULT_PHOTO_FILES = 3;
|
|
735
|
+
var MAX_PHOTO_FILES = 5;
|
|
736
|
+
var DEFAULT_PHOTO_SIZE_MB = 10;
|
|
737
|
+
var MAX_PHOTO_SIZE_MB = 25;
|
|
738
|
+
var MAX_PHOTO_ID_LENGTH = 200;
|
|
739
|
+
var MAX_PHOTO_NAME_LENGTH = 255;
|
|
740
|
+
var isPhotoFormat = (v) => typeof v === "string" && Object.prototype.hasOwnProperty.call(PHOTO_FORMATS, v);
|
|
741
|
+
var wholeIn = (v, min, max) => typeof v === "number" && Number.isInteger(v) && v >= min && v <= max;
|
|
742
|
+
function photoLimits(field) {
|
|
743
|
+
const maxFiles = wholeIn(field.props?.maxFiles, 1, MAX_PHOTO_FILES) ? field.props.maxFiles : DEFAULT_PHOTO_FILES;
|
|
744
|
+
const maxSizeMb = wholeIn(field.props?.maxSizeMb, 1, MAX_PHOTO_SIZE_MB) ? field.props.maxSizeMb : DEFAULT_PHOTO_SIZE_MB;
|
|
745
|
+
const raw = field.props?.formats;
|
|
746
|
+
const picked = Array.isArray(raw) ? ALL_FORMATS.filter((f) => raw.includes(f)) : [];
|
|
747
|
+
const formats = picked.length ? picked : [...ALL_FORMATS];
|
|
748
|
+
return {
|
|
749
|
+
maxFiles,
|
|
750
|
+
maxSizeMb,
|
|
751
|
+
maxBytes: maxSizeMb * 1024 * 1024,
|
|
752
|
+
formats,
|
|
753
|
+
mimeTypes: formats.flatMap((f) => PHOTO_FORMATS[f])
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function isPhotoRef(v) {
|
|
757
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return false;
|
|
758
|
+
const r = v;
|
|
759
|
+
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;
|
|
760
|
+
}
|
|
761
|
+
function photoRefs(value) {
|
|
762
|
+
if (!Array.isArray(value)) return [];
|
|
763
|
+
const refs = [];
|
|
764
|
+
for (const r of value) if (isPhotoRef(r)) refs.push({ id: r.id, name: r.name, type: r.type, size: r.size });
|
|
765
|
+
return refs;
|
|
766
|
+
}
|
|
767
|
+
var plural2 = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
|
|
768
|
+
function photoFormatList(formats) {
|
|
769
|
+
const names = formats.map((f) => FORMAT_NAMES[f]);
|
|
770
|
+
return names.length > 1 ? `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}` : names[0] ?? "";
|
|
771
|
+
}
|
|
772
|
+
var photoTypeMessage = (field) => `Use a ${photoFormatList(photoLimits(field).formats)} photo.`;
|
|
773
|
+
var photoSizeMessage = (field) => `Each photo must be ${photoLimits(field).maxSizeMb} MB or smaller.`;
|
|
774
|
+
var photoCountMessage = (field) => `Add at most ${plural2(photoLimits(field).maxFiles, "photo")}.`;
|
|
775
|
+
var photoFieldType = {
|
|
776
|
+
type: PHOTO_TYPE,
|
|
777
|
+
label: "Photo",
|
|
778
|
+
category: "media",
|
|
779
|
+
value: { json: true },
|
|
780
|
+
operators: ["answered", "empty"],
|
|
781
|
+
a11y: { semantics: "group" },
|
|
782
|
+
isEmpty: (value) => value === void 0 || value === null || Array.isArray(value) && value.length === 0,
|
|
783
|
+
validate: (value, field) => {
|
|
784
|
+
if (!Array.isArray(value) || !value.every(isPhotoRef)) return "Add a photo.";
|
|
785
|
+
const limits = photoLimits(field);
|
|
786
|
+
if (value.length > limits.maxFiles) return photoCountMessage(field);
|
|
787
|
+
const refs = value;
|
|
788
|
+
if (new Set(refs.map((r) => r.id)).size !== refs.length) return "Each photo can be added once.";
|
|
789
|
+
if (refs.some((r) => !limits.mimeTypes.includes(r.type.toLowerCase()))) return photoTypeMessage(field);
|
|
790
|
+
if (refs.some((r) => r.size > limits.maxBytes)) return photoSizeMessage(field);
|
|
791
|
+
return null;
|
|
792
|
+
},
|
|
793
|
+
normalize: (value) => Array.isArray(value) && value.every(isPhotoRef) ? photoRefs(value) : value,
|
|
794
|
+
summarize: (value) => photoRefs(value).map((r) => r.name || "Photo").join(", "),
|
|
795
|
+
samples: () => [[{ id: "sample-photo", name: "photo.jpg", type: "image/jpeg", size: 1024 }]]
|
|
796
|
+
};
|
|
797
|
+
|
|
724
798
|
// src/fields.ts
|
|
725
799
|
var REGISTRY = /* @__PURE__ */ Symbol.for("@marktiderman/genesis-forms/field-types");
|
|
726
800
|
var globalScope = globalThis;
|
|
@@ -757,12 +831,127 @@ function summarizeValue(value, field) {
|
|
|
757
831
|
if (Array.isArray(value)) return value.join(", ");
|
|
758
832
|
return String(value);
|
|
759
833
|
}
|
|
834
|
+
var OPTION_LIMITS = {
|
|
835
|
+
/** Characters in a description. */
|
|
836
|
+
description: 500,
|
|
837
|
+
/** Characters in an image URL. */
|
|
838
|
+
image: 2048,
|
|
839
|
+
/** Attributes on one option. */
|
|
840
|
+
attributes: 20,
|
|
841
|
+
/** Characters in an attribute's name. */
|
|
842
|
+
attributeName: 64,
|
|
843
|
+
/** Characters in one attribute value. */
|
|
844
|
+
attributeValue: 200,
|
|
845
|
+
/** Values in one attribute's list. */
|
|
846
|
+
attributeItems: 50
|
|
847
|
+
};
|
|
848
|
+
function isHttpsUrl(url) {
|
|
849
|
+
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).
|
|
850
|
+
![...url].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127);
|
|
851
|
+
}
|
|
852
|
+
function attributeName(name) {
|
|
853
|
+
return name.trim() !== "" && name.length <= OPTION_LIMITS.attributeName && !(name in Object.prototype);
|
|
854
|
+
}
|
|
855
|
+
function readAttributes(raw) {
|
|
856
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
857
|
+
const out = {};
|
|
858
|
+
let count = 0;
|
|
859
|
+
for (const name of Object.keys(raw)) {
|
|
860
|
+
if (count >= OPTION_LIMITS.attributes) break;
|
|
861
|
+
if (!attributeName(name)) continue;
|
|
862
|
+
const v = raw[name];
|
|
863
|
+
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
864
|
+
if (fits(v)) out[name] = v;
|
|
865
|
+
else if (Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits)) out[name] = [...v];
|
|
866
|
+
else continue;
|
|
867
|
+
count++;
|
|
868
|
+
}
|
|
869
|
+
return count ? out : void 0;
|
|
870
|
+
}
|
|
871
|
+
function readOption(raw) {
|
|
872
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
873
|
+
const o = raw;
|
|
874
|
+
if (typeof o.value !== "string") return null;
|
|
875
|
+
const out = { value: o.value };
|
|
876
|
+
if (typeof o.label === "string") out.label = o.label;
|
|
877
|
+
if (typeof o.description === "string" && o.description.trim() && o.description.length <= OPTION_LIMITS.description) {
|
|
878
|
+
out.description = o.description;
|
|
879
|
+
}
|
|
880
|
+
if (isHttpsUrl(o.image)) out.image = o.image;
|
|
881
|
+
const attributes = readAttributes(o.attributes);
|
|
882
|
+
if (attributes) out.attributes = attributes;
|
|
883
|
+
return out;
|
|
884
|
+
}
|
|
885
|
+
function optionJson(option) {
|
|
886
|
+
const out = { value: option.value };
|
|
887
|
+
if (option.label !== void 0) out.label = option.label;
|
|
888
|
+
if (option.description !== void 0) out.description = option.description;
|
|
889
|
+
if (option.image !== void 0) out.image = option.image;
|
|
890
|
+
if (option.attributes !== void 0) out.attributes = { ...option.attributes };
|
|
891
|
+
return out;
|
|
892
|
+
}
|
|
893
|
+
var OTHER_MAX_LENGTH = 200;
|
|
894
|
+
function otherOption(field) {
|
|
895
|
+
const other = field.props?.other;
|
|
896
|
+
if (other === true) return { label: "Other" };
|
|
897
|
+
if (other && typeof other === "object" && !Array.isArray(other)) {
|
|
898
|
+
const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
|
|
899
|
+
return { label };
|
|
900
|
+
}
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
function isOtherValue(field, value) {
|
|
904
|
+
return otherOption(field) !== null && typeof value === "string" && !choiceOptions(field).some((o) => o.value === value);
|
|
905
|
+
}
|
|
906
|
+
var typedOther = (field, value) => isOtherValue(field, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
|
|
760
907
|
function choiceOptions(field) {
|
|
761
908
|
const raw = field.props?.options;
|
|
762
909
|
if (!Array.isArray(raw)) return [];
|
|
763
|
-
return raw.flatMap(
|
|
764
|
-
|
|
765
|
-
|
|
910
|
+
return raw.flatMap((o) => {
|
|
911
|
+
const option = readOption(o);
|
|
912
|
+
return option ? [option] : [];
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
function optionsSource(field) {
|
|
916
|
+
const raw = field.props?.optionsFrom;
|
|
917
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
918
|
+
if (raw.source !== "host" || typeof raw.key !== "string" || !raw.key) return void 0;
|
|
919
|
+
const out = { source: "host", key: raw.key };
|
|
920
|
+
const filter = raw.filter;
|
|
921
|
+
if (filter && typeof filter === "object" && !Array.isArray(filter) && typeof filter.attribute === "string" && filter.attribute && typeof filter.field === "string" && filter.field) {
|
|
922
|
+
out.filter = { attribute: filter.attribute, field: filter.field };
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
926
|
+
function hostList(hostOptions, key) {
|
|
927
|
+
if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
|
|
928
|
+
const list = hostOptions[key];
|
|
929
|
+
if (!Array.isArray(list)) return [];
|
|
930
|
+
const seen = /* @__PURE__ */ new Set();
|
|
931
|
+
const out = [];
|
|
932
|
+
for (const o of list) {
|
|
933
|
+
const option = readOption(o);
|
|
934
|
+
if (!option || !option.value || seen.has(option.value)) continue;
|
|
935
|
+
seen.add(option.value);
|
|
936
|
+
out.push(option);
|
|
937
|
+
}
|
|
938
|
+
return out;
|
|
939
|
+
}
|
|
940
|
+
function withHostOptions(definition, hostOptions) {
|
|
941
|
+
let changed = false;
|
|
942
|
+
const blocks = definition.blocks.map((block) => {
|
|
943
|
+
if (block.kind !== "field") return block;
|
|
944
|
+
const from = optionsSource(block);
|
|
945
|
+
if (!from) return block;
|
|
946
|
+
const own2 = choiceOptions(block);
|
|
947
|
+
const taken = new Set(own2.map((o) => o.value));
|
|
948
|
+
const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
|
|
949
|
+
if (!added.length) return block;
|
|
950
|
+
changed = true;
|
|
951
|
+
const options = [...own2, ...added].map(optionJson);
|
|
952
|
+
return { ...block, props: { ...block.props, options } };
|
|
953
|
+
});
|
|
954
|
+
return changed ? { ...definition, blocks } : definition;
|
|
766
955
|
}
|
|
767
956
|
function optionLabel(field, value) {
|
|
768
957
|
const option = choiceOptions(field).find((o) => o.value === value);
|
|
@@ -882,7 +1071,7 @@ var builtins = [
|
|
|
882
1071
|
value: { json: true },
|
|
883
1072
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
884
1073
|
a11y: { semantics: "group" },
|
|
885
|
-
validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1074
|
+
validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
|
|
886
1075
|
summarize: (value, field) => optionLabel(field, String(value))
|
|
887
1076
|
},
|
|
888
1077
|
{
|
|
@@ -894,7 +1083,8 @@ var builtins = [
|
|
|
894
1083
|
a11y: { semantics: "group" },
|
|
895
1084
|
validate: (value, field) => {
|
|
896
1085
|
const allowed = new Set(choiceOptions(field).map((o) => o.value));
|
|
897
|
-
|
|
1086
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
1087
|
+
return Array.isArray(value) && new Set(value).size === value.length && value.every((v) => typeof v === "string") && (others.length === 0 || others.length === 1 && typedOther(field, others[0])) ? null : "Choose from the options.";
|
|
898
1088
|
},
|
|
899
1089
|
summarize: (value, field) => Array.isArray(value) ? value.map((v) => optionLabel(field, String(v))).join(", ") : String(value)
|
|
900
1090
|
},
|
|
@@ -915,7 +1105,7 @@ var builtins = [
|
|
|
915
1105
|
value: { json: true },
|
|
916
1106
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
917
1107
|
a11y: { semantics: "control" },
|
|
918
|
-
validate: (value, field) => typeof value === "string" && choiceOptions(field).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1108
|
+
validate: (value, field) => typeof value === "string" && (choiceOptions(field).some((o) => o.value === value) || typedOther(field, value)) ? null : "Choose one of the options.",
|
|
919
1109
|
summarize: (value, field) => optionLabel(field, String(value))
|
|
920
1110
|
},
|
|
921
1111
|
{
|
|
@@ -1022,7 +1212,7 @@ function dateLimits(field) {
|
|
|
1022
1212
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
1023
1213
|
return out;
|
|
1024
1214
|
}
|
|
1025
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
|
|
1215
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
|
|
1026
1216
|
|
|
1027
1217
|
// src/events.ts
|
|
1028
1218
|
function isPlainObject(value) {
|
|
@@ -1238,6 +1428,150 @@ function fieldMap(def) {
|
|
|
1238
1428
|
return map;
|
|
1239
1429
|
}
|
|
1240
1430
|
|
|
1431
|
+
// src/options.ts
|
|
1432
|
+
var CHOICE_FIELD_TYPES = /* @__PURE__ */ new Set(["single-choice", "multi-choice", "dropdown"]);
|
|
1433
|
+
var isChoice = (block) => !!block && block.kind === "field" && CHOICE_FIELD_TYPES.has(block.type);
|
|
1434
|
+
function put(record, key, value) {
|
|
1435
|
+
Object.defineProperty(record, key, { value, enumerable: true, writable: true, configurable: true });
|
|
1436
|
+
}
|
|
1437
|
+
function optionFilter(field) {
|
|
1438
|
+
return optionsSource(field)?.filter;
|
|
1439
|
+
}
|
|
1440
|
+
function answerValues(answer) {
|
|
1441
|
+
const one = (v) => typeof v === "string" ? [v] : typeof v === "number" || typeof v === "boolean" ? [String(v)] : [];
|
|
1442
|
+
if (answer === void 0 || answer === null) return [];
|
|
1443
|
+
return Array.isArray(answer) ? answer.flatMap(one) : one(answer);
|
|
1444
|
+
}
|
|
1445
|
+
function optionMatches(option, attribute, answer) {
|
|
1446
|
+
const have = option.attributes && Object.prototype.hasOwnProperty.call(option.attributes, attribute) ? option.attributes[attribute] : void 0;
|
|
1447
|
+
if (have === void 0) return false;
|
|
1448
|
+
const wanted = answerValues(answer);
|
|
1449
|
+
return (Array.isArray(have) ? have : [have]).some((v) => wanted.includes(v));
|
|
1450
|
+
}
|
|
1451
|
+
function filterSource(definition, key) {
|
|
1452
|
+
return definition.blocks.find((b) => b.kind === "field" && b.key === key);
|
|
1453
|
+
}
|
|
1454
|
+
function visibleOptions(definition, field, values) {
|
|
1455
|
+
const options = choiceOptions(field);
|
|
1456
|
+
const filter = optionFilter(field);
|
|
1457
|
+
if (!filter) return options;
|
|
1458
|
+
const source = filterSource(definition, filter.field);
|
|
1459
|
+
if (!source || source.id === field.id) return options;
|
|
1460
|
+
const answer = own(values, source.id);
|
|
1461
|
+
if (isEmptyValue(answer, source)) return options;
|
|
1462
|
+
return options.filter((o) => optionMatches(o, filter.attribute, answer));
|
|
1463
|
+
}
|
|
1464
|
+
function answersOnPath(state) {
|
|
1465
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
1466
|
+
for (const id of state.path) out[id] = own(state.values, id);
|
|
1467
|
+
return out;
|
|
1468
|
+
}
|
|
1469
|
+
var OUTSIDE_FILTER = "Choose one of the options.";
|
|
1470
|
+
function filteredChoiceError(definition, field, value, values) {
|
|
1471
|
+
if (!optionFilter(field) || isEmptyValue(value, field)) return null;
|
|
1472
|
+
const all = new Set(choiceOptions(field).map((o) => o.value));
|
|
1473
|
+
const shown2 = new Set(visibleOptions(definition, field, values).map((o) => o.value));
|
|
1474
|
+
const picked = answerValues(value);
|
|
1475
|
+
return picked.some((v) => all.has(v) && !shown2.has(v)) ? OUTSIDE_FILTER : null;
|
|
1476
|
+
}
|
|
1477
|
+
function hasOptionFilters(definition) {
|
|
1478
|
+
return definition.blocks.some((b) => isChoice(b) && optionFilter(b) !== void 0);
|
|
1479
|
+
}
|
|
1480
|
+
function clearFilteredAnswers(definition, values, changedId) {
|
|
1481
|
+
const changed = definition.blocks.find((b) => b.kind === "field" && b.id === changedId);
|
|
1482
|
+
if (!changed) return values;
|
|
1483
|
+
const keys = /* @__PURE__ */ new Set([changed.key]);
|
|
1484
|
+
let out = values;
|
|
1485
|
+
for (const block of definition.blocks) {
|
|
1486
|
+
if (!isChoice(block)) continue;
|
|
1487
|
+
const filter = optionFilter(block);
|
|
1488
|
+
if (!filter || !keys.has(filter.field)) continue;
|
|
1489
|
+
const value = own(out, block.id);
|
|
1490
|
+
if (value === void 0 || isEmptyValue(value, block)) continue;
|
|
1491
|
+
const all = new Set(choiceOptions(block).map((o) => o.value));
|
|
1492
|
+
const shown2 = new Set(visibleOptions(definition, block, out).map((o) => o.value));
|
|
1493
|
+
const keep = (v) => typeof v !== "string" || !all.has(v) || shown2.has(v);
|
|
1494
|
+
let next = value;
|
|
1495
|
+
if (Array.isArray(value)) {
|
|
1496
|
+
const kept = value.filter(keep);
|
|
1497
|
+
if (kept.length !== value.length) next = kept.length ? kept : void 0;
|
|
1498
|
+
} else if (!keep(value)) next = void 0;
|
|
1499
|
+
if (next === value) continue;
|
|
1500
|
+
if (out === values) out = { ...values };
|
|
1501
|
+
put(out, block.id, next);
|
|
1502
|
+
keys.add(block.key);
|
|
1503
|
+
}
|
|
1504
|
+
return out;
|
|
1505
|
+
}
|
|
1506
|
+
var MAX_PREFILL_PARAM_LENGTH = 64;
|
|
1507
|
+
function isPrefillParam(param) {
|
|
1508
|
+
return typeof param === "string" && param.length <= MAX_PREFILL_PARAM_LENGTH && /^[A-Za-z0-9_.-]+$/.test(param);
|
|
1509
|
+
}
|
|
1510
|
+
function prefillSetting(field) {
|
|
1511
|
+
if (!CHOICE_FIELD_TYPES.has(field.type)) return void 0;
|
|
1512
|
+
const raw = field.props?.prefill;
|
|
1513
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw) || !isPrefillParam(raw.param)) return void 0;
|
|
1514
|
+
return { param: raw.param, hideWhenPrefilled: raw.hideWhenPrefilled === true };
|
|
1515
|
+
}
|
|
1516
|
+
function paramValues(params, name) {
|
|
1517
|
+
let raw;
|
|
1518
|
+
if (typeof params.getAll === "function") {
|
|
1519
|
+
raw = params.getAll(name);
|
|
1520
|
+
} else if (Object.prototype.hasOwnProperty.call(params, name)) {
|
|
1521
|
+
raw = params[name];
|
|
1522
|
+
}
|
|
1523
|
+
const list = Array.isArray(raw) ? raw : raw === void 0 ? [] : [raw];
|
|
1524
|
+
return list.filter((v) => typeof v === "string" && v !== "" && v.length <= MAX_HIDDEN_STRING_LENGTH);
|
|
1525
|
+
}
|
|
1526
|
+
function prefillFromLink(definition, params, values = {}) {
|
|
1527
|
+
const out = {};
|
|
1528
|
+
if (!params || typeof params !== "object") return out;
|
|
1529
|
+
const seen = {};
|
|
1530
|
+
for (const id of Object.keys(values)) put(seen, id, values[id]);
|
|
1531
|
+
for (const block of definition.blocks) {
|
|
1532
|
+
if (!isChoice(block)) continue;
|
|
1533
|
+
const setting = prefillSetting(block);
|
|
1534
|
+
if (!setting || own(seen, block.id) !== void 0) continue;
|
|
1535
|
+
const raw = paramValues(params, setting.param);
|
|
1536
|
+
if (!raw.length) continue;
|
|
1537
|
+
const shown2 = new Set(visibleOptions(definition, block, seen).map((o) => o.value));
|
|
1538
|
+
let value;
|
|
1539
|
+
if (block.type === "multi-choice") {
|
|
1540
|
+
const picked = [];
|
|
1541
|
+
for (const r of raw) {
|
|
1542
|
+
for (const v of shown2.has(r) ? [r] : r.split(",").map((s) => s.trim())) {
|
|
1543
|
+
if (shown2.has(v) && !picked.includes(v)) picked.push(v);
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
if (picked.length) value = picked;
|
|
1547
|
+
} else value = raw.find((v) => shown2.has(v));
|
|
1548
|
+
if (value === void 0) continue;
|
|
1549
|
+
put(out, block.id, value);
|
|
1550
|
+
put(seen, block.id, value);
|
|
1551
|
+
}
|
|
1552
|
+
return out;
|
|
1553
|
+
}
|
|
1554
|
+
function hiddenByPrefill(definition, state) {
|
|
1555
|
+
const hideable = /* @__PURE__ */ new Set();
|
|
1556
|
+
for (const b of definition.blocks) {
|
|
1557
|
+
if (isChoice(b) && prefillSetting(b)?.hideWhenPrefilled) hideable.add(b.id);
|
|
1558
|
+
}
|
|
1559
|
+
if (!hideable.size) return [];
|
|
1560
|
+
const prefilled = /* @__PURE__ */ new Set();
|
|
1561
|
+
for (const e of state.events) {
|
|
1562
|
+
if (e.type !== "answer" || !hideable.has(e.field)) continue;
|
|
1563
|
+
if (e.source === "prefill") prefilled.add(e.field);
|
|
1564
|
+
else prefilled.delete(e.field);
|
|
1565
|
+
}
|
|
1566
|
+
if (!prefilled.size) return [];
|
|
1567
|
+
const fields = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
1568
|
+
return state.path.filter((id) => {
|
|
1569
|
+
if (!prefilled.has(id) || own(state.errors, id) !== void 0) return false;
|
|
1570
|
+
const field = fields.get(id);
|
|
1571
|
+
return !isEmptyValue(own(state.values, id), field);
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1241
1575
|
// src/recall.ts
|
|
1242
1576
|
var TOKEN = /\{\{\s*(field|hidden):([A-Za-z0-9_-]+)\s*(?:\|([^}]*))?\}\}/g;
|
|
1243
1577
|
function recall(text, scope) {
|
|
@@ -1291,7 +1625,8 @@ function readsAny(node, ids) {
|
|
|
1291
1625
|
function createForm(definition, options = {}) {
|
|
1292
1626
|
return build(definition, options).engine;
|
|
1293
1627
|
}
|
|
1294
|
-
function build(
|
|
1628
|
+
function build(given, options, replaying = false, budget) {
|
|
1629
|
+
const definition = withHostOptions(given, options.hostOptions);
|
|
1295
1630
|
const fields = fieldMap(definition);
|
|
1296
1631
|
const byKey = new Map([...fields.values()].map((f) => [f.key, f]));
|
|
1297
1632
|
const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
@@ -1337,6 +1672,7 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1337
1672
|
path: first.path,
|
|
1338
1673
|
current: first.path[0] ?? null,
|
|
1339
1674
|
history: [],
|
|
1675
|
+
furthest: first.path[0] ?? null,
|
|
1340
1676
|
errors: {},
|
|
1341
1677
|
status: "in_progress",
|
|
1342
1678
|
ending: null,
|
|
@@ -1357,16 +1693,34 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1357
1693
|
return at === void 0 ? event : { ...event, at };
|
|
1358
1694
|
};
|
|
1359
1695
|
const isVisibleIn = (s, id) => s.path.includes(id);
|
|
1696
|
+
const filters = hasOptionFilters(definition);
|
|
1697
|
+
const clearFiltered = (values, id) => filters ? clearFilteredAnswers(definition, values, id) : values;
|
|
1360
1698
|
const errorsFor = (s, ids) => {
|
|
1361
1699
|
const errors = {};
|
|
1700
|
+
let shown2;
|
|
1362
1701
|
for (const id of ids) {
|
|
1363
1702
|
const field = fields.get(id);
|
|
1364
1703
|
if (!field || !isVisibleIn(s, id)) continue;
|
|
1365
|
-
const
|
|
1704
|
+
const value = own(s.values, id);
|
|
1705
|
+
let message = validateField(field, value, { custom: options.validators?.[id] });
|
|
1706
|
+
if (!message && filters) message = filteredChoiceError(definition, field, value, shown2 ??= answersOnPath(s));
|
|
1366
1707
|
if (message) errors[id] = message;
|
|
1367
1708
|
}
|
|
1368
1709
|
return errors;
|
|
1369
1710
|
};
|
|
1711
|
+
const skipped = (s) => new Set(hiddenByPrefill(definition, s));
|
|
1712
|
+
const shownFrom = (s, from, skip) => {
|
|
1713
|
+
for (let i = from; i < s.path.length; i++) if (!skip.has(s.path[i])) return s.path[i];
|
|
1714
|
+
return void 0;
|
|
1715
|
+
};
|
|
1716
|
+
const showCurrent = () => {
|
|
1717
|
+
if (!state.current) return;
|
|
1718
|
+
const skip = skipped(state);
|
|
1719
|
+
if (!skip.has(state.current)) return;
|
|
1720
|
+
const at = state.path.indexOf(state.current);
|
|
1721
|
+
const shown2 = at < 0 ? void 0 : shownFrom(state, at, skip);
|
|
1722
|
+
if (shown2) state = { ...state, current: shown2, furthest: state.furthest === state.current ? shown2 : state.furthest };
|
|
1723
|
+
};
|
|
1370
1724
|
let unsettled = false;
|
|
1371
1725
|
let formulasStale = false;
|
|
1372
1726
|
const settleNow = (needValues = true) => {
|
|
@@ -1395,13 +1749,13 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1395
1749
|
const errors = { ...state.errors };
|
|
1396
1750
|
delete errors[id];
|
|
1397
1751
|
if (batch) {
|
|
1398
|
-
state.values = { ...state.values, [id]: value };
|
|
1752
|
+
state.values = clearFiltered({ ...state.values, [id]: value }, id);
|
|
1399
1753
|
state.errors = errors;
|
|
1400
1754
|
log(event);
|
|
1401
1755
|
unsettled = true;
|
|
1402
1756
|
return;
|
|
1403
1757
|
}
|
|
1404
|
-
const { values, path } = settle({ ...state.values, [id]: value }, state.hidden);
|
|
1758
|
+
const { values, path } = settle(clearFiltered({ ...state.values, [id]: value }, id), state.hidden);
|
|
1405
1759
|
emit({ ...state, values, path, errors, events: log(event) });
|
|
1406
1760
|
};
|
|
1407
1761
|
const engine = {
|
|
@@ -1432,19 +1786,21 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1432
1786
|
return false;
|
|
1433
1787
|
}
|
|
1434
1788
|
const at = state.path.indexOf(state.current);
|
|
1435
|
-
const target = state
|
|
1789
|
+
const target = shownFrom(state, at + 1, skipped(state));
|
|
1436
1790
|
if (!target) {
|
|
1437
1791
|
emit({ ...state, events });
|
|
1438
1792
|
return true;
|
|
1439
1793
|
}
|
|
1440
|
-
|
|
1794
|
+
const furthest = state.furthest && state.path.indexOf(state.furthest) >= state.path.indexOf(target) ? state.furthest : target;
|
|
1795
|
+
emit({ ...state, current: target, history: [...state.history, state.current], furthest, events });
|
|
1441
1796
|
return true;
|
|
1442
1797
|
},
|
|
1443
1798
|
back() {
|
|
1444
1799
|
if (state.status === "completed") return;
|
|
1445
1800
|
const history = [...state.history];
|
|
1801
|
+
const skip = skipped(state);
|
|
1446
1802
|
let previous = history.pop();
|
|
1447
|
-
while (previous && !state.path.includes(previous)) previous = history.pop();
|
|
1803
|
+
while (previous && (!state.path.includes(previous) || skip.has(previous))) previous = history.pop();
|
|
1448
1804
|
if (!previous) return;
|
|
1449
1805
|
emit({ ...state, current: previous, history, events: log(stamp({ type: "back" })) });
|
|
1450
1806
|
},
|
|
@@ -1472,15 +1828,27 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1472
1828
|
return out;
|
|
1473
1829
|
}
|
|
1474
1830
|
};
|
|
1475
|
-
|
|
1831
|
+
const fromLink = options.linkParams ? prefillFromLink(definition, options.linkParams, options.initialValues) : {};
|
|
1832
|
+
const initialValues = Object.keys(fromLink).length ? { ...fromLink, ...options.initialValues } : options.initialValues;
|
|
1833
|
+
if (initialValues) {
|
|
1476
1834
|
const wasBatch = batch;
|
|
1477
1835
|
batch = true;
|
|
1478
|
-
for (const [id, value] of Object.entries(
|
|
1836
|
+
for (const [id, value] of Object.entries(initialValues)) {
|
|
1479
1837
|
if (!fields.has(id) || value === void 0) continue;
|
|
1480
1838
|
answer(id, value, stamp({ type: "answer", field: id, value, source: "prefill" }));
|
|
1481
1839
|
}
|
|
1482
1840
|
settleNow();
|
|
1483
1841
|
batch = wasBatch;
|
|
1842
|
+
showCurrent();
|
|
1843
|
+
}
|
|
1844
|
+
const resumeAt = options.resumeAt;
|
|
1845
|
+
if (resumeAt && !replaying && state.path.includes(resumeAt)) {
|
|
1846
|
+
for (let i = 0; i < state.path.length && state.current !== resumeAt; i++) {
|
|
1847
|
+
if (!engine.next()) {
|
|
1848
|
+
state = { ...state, errors: {} };
|
|
1849
|
+
break;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1484
1852
|
}
|
|
1485
1853
|
const apply = (event) => {
|
|
1486
1854
|
if (event.type === "answer") {
|
|
@@ -1490,18 +1858,22 @@ function build(definition, options, replaying = false, budget) {
|
|
|
1490
1858
|
}
|
|
1491
1859
|
} else if (event.type === "next") {
|
|
1492
1860
|
settleNow(false);
|
|
1861
|
+
showCurrent();
|
|
1493
1862
|
if (formulasStale && isCalculated(fields.get(state.current ?? ""))) settleNow(true);
|
|
1494
1863
|
engine.next();
|
|
1495
1864
|
} else if (event.type === "back") {
|
|
1496
1865
|
settleNow(false);
|
|
1866
|
+
showCurrent();
|
|
1497
1867
|
engine.back();
|
|
1498
1868
|
} else if (event.type === "submit") {
|
|
1499
1869
|
settleNow();
|
|
1870
|
+
showCurrent();
|
|
1500
1871
|
engine.submit();
|
|
1501
1872
|
}
|
|
1502
1873
|
};
|
|
1503
1874
|
const finish = () => {
|
|
1504
1875
|
settleNow();
|
|
1876
|
+
showCurrent();
|
|
1505
1877
|
batch = false;
|
|
1506
1878
|
state = { ...state, events: [...state.events] };
|
|
1507
1879
|
};
|
|
@@ -1512,7 +1884,12 @@ function replay(definition, events, options = {}) {
|
|
|
1512
1884
|
}
|
|
1513
1885
|
function replayWithin(definition, events, options, maxWork) {
|
|
1514
1886
|
const budget = maxWork === void 0 ? void 0 : { used: 0, max: maxWork };
|
|
1515
|
-
const { engine, apply, finish } = build(
|
|
1887
|
+
const { engine, apply, finish } = build(
|
|
1888
|
+
definition,
|
|
1889
|
+
{ ...options, initialValues: void 0, linkParams: void 0, clock: () => void 0 },
|
|
1890
|
+
true,
|
|
1891
|
+
budget
|
|
1892
|
+
);
|
|
1516
1893
|
try {
|
|
1517
1894
|
for (const event of events) apply(event);
|
|
1518
1895
|
finish();
|
|
@@ -1763,6 +2140,31 @@ function parseForm(input) {
|
|
|
1763
2140
|
const optionalBoolean = (v, path) => {
|
|
1764
2141
|
if (v !== void 0 && typeof v !== "boolean") issue(path, "Must be true or false.");
|
|
1765
2142
|
};
|
|
2143
|
+
const optionDetails = (o, at) => {
|
|
2144
|
+
if (o.description !== void 0 && (typeof o.description !== "string" || o.description.length > OPTION_LIMITS.description)) {
|
|
2145
|
+
issue(`${at}.description`, `Must be text of at most ${OPTION_LIMITS.description} characters.`);
|
|
2146
|
+
}
|
|
2147
|
+
if (o.image !== void 0 && !isHttpsUrl(o.image)) {
|
|
2148
|
+
issue(`${at}.image`, `Must be an https:// link of at most ${OPTION_LIMITS.image} characters.`);
|
|
2149
|
+
}
|
|
2150
|
+
if (o.attributes === void 0) return;
|
|
2151
|
+
const attributes = o.attributes;
|
|
2152
|
+
if (!isObject2(attributes) || Object.keys(attributes).length > OPTION_LIMITS.attributes) {
|
|
2153
|
+
issue(`${at}.attributes`, `Must be an object of at most ${OPTION_LIMITS.attributes} attributes.`);
|
|
2154
|
+
return;
|
|
2155
|
+
}
|
|
2156
|
+
const fits = (x) => typeof x === "string" && x.length <= OPTION_LIMITS.attributeValue;
|
|
2157
|
+
for (const [name2, v] of Object.entries(attributes)) {
|
|
2158
|
+
if (!name2.trim() || name2.length > OPTION_LIMITS.attributeName || RESERVED.has(name2)) {
|
|
2159
|
+
issue(`${at}.attributes`, `${shown(name2)} isn't a usable attribute name (at most ${OPTION_LIMITS.attributeName} characters).`);
|
|
2160
|
+
} else if (!fits(v) && !(Array.isArray(v) && v.length <= OPTION_LIMITS.attributeItems && v.every(fits))) {
|
|
2161
|
+
issue(
|
|
2162
|
+
`${at}.attributes.${name2}`,
|
|
2163
|
+
`Must be text, or a list of at most ${OPTION_LIMITS.attributeItems} texts, each at most ${OPTION_LIMITS.attributeValue} characters.`
|
|
2164
|
+
);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
1766
2168
|
const name = (v, path, what) => {
|
|
1767
2169
|
if (typeof v !== "string" || !v) {
|
|
1768
2170
|
issue(path, `${what} must be a non-empty string.`);
|
|
@@ -1851,6 +2253,22 @@ function parseForm(input) {
|
|
|
1851
2253
|
}
|
|
1852
2254
|
}
|
|
1853
2255
|
if (type === "consent") optionalText(props.text, `${path}.text`);
|
|
2256
|
+
if (type === PHOTO_TYPE) {
|
|
2257
|
+
if (props.maxFiles !== void 0 && !(isInt(props.maxFiles) && props.maxFiles >= 1 && props.maxFiles <= MAX_PHOTO_FILES)) {
|
|
2258
|
+
issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
2259
|
+
}
|
|
2260
|
+
if (props.maxSizeMb !== void 0 && !(isInt(props.maxSizeMb) && props.maxSizeMb >= 1 && props.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
2261
|
+
issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
2262
|
+
}
|
|
2263
|
+
if (props.formats !== void 0) {
|
|
2264
|
+
const formats = props.formats;
|
|
2265
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
2266
|
+
issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
2267
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
2268
|
+
issue(`${path}.formats`, "Name each format once.");
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
1854
2272
|
};
|
|
1855
2273
|
if (!isObject2(input)) {
|
|
1856
2274
|
issue("", "A form definition must be an object.");
|
|
@@ -1871,6 +2289,7 @@ function parseForm(input) {
|
|
|
1871
2289
|
else {
|
|
1872
2290
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
1873
2291
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
2292
|
+
optionalBoolean(input.settings.saveProgress, "settings.saveProgress");
|
|
1874
2293
|
}
|
|
1875
2294
|
}
|
|
1876
2295
|
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
@@ -1911,6 +2330,38 @@ function parseForm(input) {
|
|
|
1911
2330
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
1912
2331
|
}
|
|
1913
2332
|
if (block.props !== void 0) {
|
|
2333
|
+
const from = isObject2(block.props) ? block.props.optionsFrom : void 0;
|
|
2334
|
+
if (from !== void 0) {
|
|
2335
|
+
if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
|
|
2336
|
+
issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
|
|
2337
|
+
} else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2338
|
+
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
2339
|
+
}
|
|
2340
|
+
const filter = isObject2(from) ? from.filter : void 0;
|
|
2341
|
+
if (filter !== void 0) {
|
|
2342
|
+
const at = `${path}.props.optionsFrom.filter`;
|
|
2343
|
+
if (!isObject2(filter) || typeof filter.attribute !== "string" || !filter.attribute || filter.attribute.length > OPTION_LIMITS.attributeName || typeof filter.field !== "string" || !filter.field) {
|
|
2344
|
+
issue(at, `Must be { attribute, field }: an attribute name (at most ${OPTION_LIMITS.attributeName} characters) and an earlier question's key.`);
|
|
2345
|
+
} else if (!keys.has(filter.field)) {
|
|
2346
|
+
issue(`${at}.field`, `"${filter.field}" must be the key of an earlier question.`);
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const prefill = isObject2(block.props) ? block.props.prefill : void 0;
|
|
2351
|
+
if (prefill !== void 0) {
|
|
2352
|
+
if (!isObject2(prefill) || !isPrefillParam(prefill.param)) {
|
|
2353
|
+
issue(
|
|
2354
|
+
`${path}.props.prefill`,
|
|
2355
|
+
`Must be { param, hideWhenPrefilled? }: a link parameter of letters, digits, "_", "-" or "." (at most ${MAX_PREFILL_PARAM_LENGTH} characters).`
|
|
2356
|
+
);
|
|
2357
|
+
} else {
|
|
2358
|
+
optionalBoolean(prefill.hideWhenPrefilled, `${path}.props.prefill.hideWhenPrefilled`);
|
|
2359
|
+
if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2360
|
+
warnings.push({ path: `${path}.props.prefill`, message: "Only single choice, multiple choice and dropdown questions are filled in from a link." });
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (isObject2(block.props)) optionalBoolean(block.props.searchable, `${path}.props.searchable`);
|
|
1914
2365
|
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
1915
2366
|
else if (block.props.options !== void 0) {
|
|
1916
2367
|
const options = block.props.options;
|
|
@@ -1919,12 +2370,25 @@ function parseForm(input) {
|
|
|
1919
2370
|
options.forEach((o, j) => {
|
|
1920
2371
|
const at = `${path}.props.options[${j}]`;
|
|
1921
2372
|
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
1922
|
-
else
|
|
2373
|
+
else {
|
|
2374
|
+
optionalText(o.label, `${at}.label`);
|
|
2375
|
+
optionDetails(o, at);
|
|
2376
|
+
}
|
|
1923
2377
|
});
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2378
|
+
if (from === void 0) {
|
|
2379
|
+
choices.set(
|
|
2380
|
+
block.id,
|
|
2381
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
2382
|
+
);
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
const other = isObject2(block.props) ? block.props.other : void 0;
|
|
2387
|
+
if (other !== void 0) {
|
|
2388
|
+
if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
|
|
2389
|
+
issue(`${path}.props.other`, "Must be true, false or { label }.");
|
|
2390
|
+
} else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
2391
|
+
warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
|
|
1928
2392
|
}
|
|
1929
2393
|
}
|
|
1930
2394
|
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
@@ -1999,6 +2463,8 @@ function parseForm(input) {
|
|
|
1999
2463
|
} else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
|
|
2000
2464
|
else if (targetType === "media-notes" || targetType === "text-highlights") {
|
|
2001
2465
|
warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
|
|
2466
|
+
} else if (targetType === PHOTO_TYPE) {
|
|
2467
|
+
warnings.push({ path, message: `"${ref.key}" holds photos, not numbers; the formula reads it as blank.` });
|
|
2002
2468
|
}
|
|
2003
2469
|
}
|
|
2004
2470
|
});
|
|
@@ -2263,9 +2729,10 @@ function wellFormed(input) {
|
|
|
2263
2729
|
}
|
|
2264
2730
|
return { answers, events: known, hidden };
|
|
2265
2731
|
}
|
|
2266
|
-
function validateSubmission(
|
|
2267
|
-
const invalid = checkDefinition(
|
|
2732
|
+
function validateSubmission(published, submission, options = {}) {
|
|
2733
|
+
const invalid = checkDefinition(published);
|
|
2268
2734
|
if (invalid) return { ok: false, errors: { "": invalid } };
|
|
2735
|
+
const definition = withHostOptions(published, options.hostOptions);
|
|
2269
2736
|
if (!isPlainObject(submission)) return MALFORMED;
|
|
2270
2737
|
const maxEvents = options.maxEvents ?? MAX_EVENTS;
|
|
2271
2738
|
let sent = submission;
|
|
@@ -2336,11 +2803,12 @@ function validateSubmission(definition, submission, options = {}) {
|
|
|
2336
2803
|
}
|
|
2337
2804
|
} else {
|
|
2338
2805
|
const errors = {};
|
|
2339
|
-
|
|
2806
|
+
const kept = engine.answers();
|
|
2807
|
+
for (const [id, value] of Object.entries(kept)) {
|
|
2340
2808
|
const message = validateField(fields.get(id), value, {
|
|
2341
2809
|
requireAnswers: false,
|
|
2342
2810
|
custom: options.validators?.[id]
|
|
2343
|
-
});
|
|
2811
|
+
}) ?? filteredChoiceError(definition, fields.get(id), value, kept);
|
|
2344
2812
|
if (message) errors[id] = message;
|
|
2345
2813
|
}
|
|
2346
2814
|
if (Object.keys(errors).length) return { ok: false, errors };
|