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