@marktiderman/genesis-forms 0.4.2 → 0.5.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 +67 -0
- package/dist/destinations.cjs +92 -4
- 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 +92 -4
- package/dist/destinations.js.map +1 -1
- package/dist/{engine-Dz9J5ej2.d.ts → engine-BDq0XJ21.d.cts} +25 -2
- package/dist/{engine-BkNuzlna.d.cts → engine-Qzp6bX6O.d.ts} +25 -2
- package/dist/index.cjs +259 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -75
- package/dist/index.d.ts +120 -75
- package/dist/index.js +236 -17
- package/dist/index.js.map +1 -1
- package/dist/server.cjs +187 -12
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +10 -3
- package/dist/server.d.ts +10 -3
- package/dist/server.js +187 -12
- package/dist/server.js.map +1 -1
- package/dist/testing.cjs +187 -12
- 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 +187 -12
- package/dist/testing.js.map +1 -1
- package/dist/{types-BBKBQ8sp.d.cts → types-BhYUbLJ-.d.cts} +21 -1
- package/dist/{types-BBKBQ8sp.d.ts → types-BhYUbLJ-.d.ts} +21 -1
- package/dist/validate-Bs1lyoOl.d.ts +131 -0
- package/dist/validate-BsZ0YTQS.d.cts +131 -0
- package/package.json +1 -1
- package/dist/validate-BXXqi0fq.d.cts +0 -30
- package/dist/validate-BjNwF1UE.d.ts +0 -30
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as FormEvent, J as Json, F as FormDefinition, a as FieldBlock, B as Block } from './types-
|
|
2
|
-
import { F as FieldValidator } from './validate-
|
|
1
|
+
import { e as FormEvent, J as Json, F as FormDefinition, a as FieldBlock, B as Block } from './types-BhYUbLJ-.cjs';
|
|
2
|
+
import { F as FieldValidator, H as HostOptions } from './validate-BsZ0YTQS.cjs';
|
|
3
3
|
|
|
4
4
|
/** Guards and helpers for event logs and other untrusted JSON. No dependencies. */
|
|
5
5
|
|
|
@@ -29,6 +29,11 @@ interface FormState {
|
|
|
29
29
|
current: string | null;
|
|
30
30
|
/** Visited blocks, for Back. */
|
|
31
31
|
history: string[];
|
|
32
|
+
/**
|
|
33
|
+
* The furthest block reached by moving on (`next`), kept after going back;
|
|
34
|
+
* the first block until then. `formProgress` reads it.
|
|
35
|
+
*/
|
|
36
|
+
furthest: string | null;
|
|
32
37
|
errors: Record<string, string>;
|
|
33
38
|
status: "in_progress" | "completed";
|
|
34
39
|
/** The ending reached once completed. */
|
|
@@ -43,8 +48,26 @@ interface CreateFormOptions {
|
|
|
43
48
|
validators?: Record<string, FieldValidator> | undefined;
|
|
44
49
|
/** Timestamps on events. Pass `() => undefined` to record none (anonymous forms). */
|
|
45
50
|
clock?: (() => number | undefined) | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Pick up where the respondent was (with `initialValues` holding their saved
|
|
53
|
+
* answers): a block id, usually `formProgress`'s `fieldId` from an earlier
|
|
54
|
+
* visit. The form moves on from the first block with Next, as they did, until
|
|
55
|
+
* it reaches this one, so Back still walks their path; it stops early at a
|
|
56
|
+
* step that no longer passes (a question since made required), and does
|
|
57
|
+
* nothing when the block isn't on the path.
|
|
58
|
+
*/
|
|
59
|
+
resumeAt?: string | null | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* The option lists this app supplies, by key, for choice fields with
|
|
62
|
+
* `props.optionsFrom: { source: "host", key }` (see `withHostOptions`).
|
|
63
|
+
* `engine.definition` is the definition with them filled in, so renderers
|
|
64
|
+
* show them and validation accepts only them (and the field's own options).
|
|
65
|
+
* Pass the same lists to `validateSubmission` on the server.
|
|
66
|
+
*/
|
|
67
|
+
hostOptions?: HostOptions | undefined;
|
|
46
68
|
}
|
|
47
69
|
interface FormEngine {
|
|
70
|
+
/** The definition being filled in, with `hostOptions` filled in (`withHostOptions`). */
|
|
48
71
|
readonly definition: FormDefinition;
|
|
49
72
|
getState(): FormState;
|
|
50
73
|
subscribe(listener: () => void): () => void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as FormEvent, J as Json, F as FormDefinition, a as FieldBlock, B as Block } from './types-
|
|
2
|
-
import { F as FieldValidator } from './validate-
|
|
1
|
+
import { e as FormEvent, J as Json, F as FormDefinition, a as FieldBlock, B as Block } from './types-BhYUbLJ-.js';
|
|
2
|
+
import { F as FieldValidator, H as HostOptions } from './validate-Bs1lyoOl.js';
|
|
3
3
|
|
|
4
4
|
/** Guards and helpers for event logs and other untrusted JSON. No dependencies. */
|
|
5
5
|
|
|
@@ -29,6 +29,11 @@ interface FormState {
|
|
|
29
29
|
current: string | null;
|
|
30
30
|
/** Visited blocks, for Back. */
|
|
31
31
|
history: string[];
|
|
32
|
+
/**
|
|
33
|
+
* The furthest block reached by moving on (`next`), kept after going back;
|
|
34
|
+
* the first block until then. `formProgress` reads it.
|
|
35
|
+
*/
|
|
36
|
+
furthest: string | null;
|
|
32
37
|
errors: Record<string, string>;
|
|
33
38
|
status: "in_progress" | "completed";
|
|
34
39
|
/** The ending reached once completed. */
|
|
@@ -43,8 +48,26 @@ interface CreateFormOptions {
|
|
|
43
48
|
validators?: Record<string, FieldValidator> | undefined;
|
|
44
49
|
/** Timestamps on events. Pass `() => undefined` to record none (anonymous forms). */
|
|
45
50
|
clock?: (() => number | undefined) | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Pick up where the respondent was (with `initialValues` holding their saved
|
|
53
|
+
* answers): a block id, usually `formProgress`'s `fieldId` from an earlier
|
|
54
|
+
* visit. The form moves on from the first block with Next, as they did, until
|
|
55
|
+
* it reaches this one, so Back still walks their path; it stops early at a
|
|
56
|
+
* step that no longer passes (a question since made required), and does
|
|
57
|
+
* nothing when the block isn't on the path.
|
|
58
|
+
*/
|
|
59
|
+
resumeAt?: string | null | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* The option lists this app supplies, by key, for choice fields with
|
|
62
|
+
* `props.optionsFrom: { source: "host", key }` (see `withHostOptions`).
|
|
63
|
+
* `engine.definition` is the definition with them filled in, so renderers
|
|
64
|
+
* show them and validation accepts only them (and the field's own options).
|
|
65
|
+
* Pass the same lists to `validateSubmission` on the server.
|
|
66
|
+
*/
|
|
67
|
+
hostOptions?: HostOptions | undefined;
|
|
46
68
|
}
|
|
47
69
|
interface FormEngine {
|
|
70
|
+
/** The definition being filled in, with `hostOptions` filled in (`withHostOptions`). */
|
|
48
71
|
readonly definition: FormDefinition;
|
|
49
72
|
getState(): FormState;
|
|
50
73
|
subscribe(listener: () => void): () => void;
|
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,6 +952,20 @@ function summarizeValue(value, field2) {
|
|
|
877
952
|
if (Array.isArray(value)) return value.join(", ");
|
|
878
953
|
return String(value);
|
|
879
954
|
}
|
|
955
|
+
var OTHER_MAX_LENGTH = 200;
|
|
956
|
+
function otherOption(field2) {
|
|
957
|
+
const other = field2.props?.other;
|
|
958
|
+
if (other === true) return { label: "Other" };
|
|
959
|
+
if (other && typeof other === "object" && !Array.isArray(other)) {
|
|
960
|
+
const label = typeof other.label === "string" && other.label.trim() ? other.label.trim() : "Other";
|
|
961
|
+
return { label };
|
|
962
|
+
}
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
965
|
+
function isOtherValue(field2, value) {
|
|
966
|
+
return otherOption(field2) !== null && typeof value === "string" && !choiceOptions(field2).some((o) => o.value === value);
|
|
967
|
+
}
|
|
968
|
+
var typedOther = (field2, value) => isOtherValue(field2, value) && value.trim() !== "" && value.length <= OTHER_MAX_LENGTH;
|
|
880
969
|
function choiceOptions(field2) {
|
|
881
970
|
const raw = field2.props?.options;
|
|
882
971
|
if (!Array.isArray(raw)) return [];
|
|
@@ -884,6 +973,42 @@ function choiceOptions(field2) {
|
|
|
884
973
|
(o) => o && typeof o === "object" && !Array.isArray(o) && typeof o.value === "string" ? [{ value: o.value, label: typeof o.label === "string" ? o.label : void 0 }] : []
|
|
885
974
|
);
|
|
886
975
|
}
|
|
976
|
+
function optionsSource(field2) {
|
|
977
|
+
const raw = field2.props?.optionsFrom;
|
|
978
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
979
|
+
return raw.source === "host" && typeof raw.key === "string" && raw.key ? { source: "host", key: raw.key } : void 0;
|
|
980
|
+
}
|
|
981
|
+
function hostList(hostOptions, key) {
|
|
982
|
+
if (!hostOptions || !Object.prototype.hasOwnProperty.call(hostOptions, key)) return [];
|
|
983
|
+
const list = hostOptions[key];
|
|
984
|
+
if (!Array.isArray(list)) return [];
|
|
985
|
+
const seen = /* @__PURE__ */ new Set();
|
|
986
|
+
const out = [];
|
|
987
|
+
for (const o of list) {
|
|
988
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) continue;
|
|
989
|
+
const { value, label } = o;
|
|
990
|
+
if (typeof value !== "string" || !value || seen.has(value)) continue;
|
|
991
|
+
seen.add(value);
|
|
992
|
+
out.push(typeof label === "string" ? { value, label } : { value });
|
|
993
|
+
}
|
|
994
|
+
return out;
|
|
995
|
+
}
|
|
996
|
+
function withHostOptions(definition, hostOptions) {
|
|
997
|
+
let changed = false;
|
|
998
|
+
const blocks = definition.blocks.map((block) => {
|
|
999
|
+
if (block.kind !== "field") return block;
|
|
1000
|
+
const from = optionsSource(block);
|
|
1001
|
+
if (!from) return block;
|
|
1002
|
+
const own2 = choiceOptions(block);
|
|
1003
|
+
const taken = new Set(own2.map((o) => o.value));
|
|
1004
|
+
const added = hostList(hostOptions, from.key).filter((o) => !taken.has(o.value));
|
|
1005
|
+
if (!added.length) return block;
|
|
1006
|
+
changed = true;
|
|
1007
|
+
const options = [...own2, ...added].map((o) => o.label === void 0 ? { value: o.value } : { value: o.value, label: o.label });
|
|
1008
|
+
return { ...block, props: { ...block.props, options } };
|
|
1009
|
+
});
|
|
1010
|
+
return changed ? { ...definition, blocks } : definition;
|
|
1011
|
+
}
|
|
887
1012
|
function optionLabel(field2, value) {
|
|
888
1013
|
const option = choiceOptions(field2).find((o) => o.value === value);
|
|
889
1014
|
return option?.label ?? value;
|
|
@@ -1002,7 +1127,7 @@ var builtins = [
|
|
|
1002
1127
|
value: { json: true },
|
|
1003
1128
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
1004
1129
|
a11y: { semantics: "group" },
|
|
1005
|
-
validate: (value, field2) => typeof value === "string" && choiceOptions(field2).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1130
|
+
validate: (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.",
|
|
1006
1131
|
summarize: (value, field2) => optionLabel(field2, String(value))
|
|
1007
1132
|
},
|
|
1008
1133
|
{
|
|
@@ -1014,7 +1139,8 @@ var builtins = [
|
|
|
1014
1139
|
a11y: { semantics: "group" },
|
|
1015
1140
|
validate: (value, field2) => {
|
|
1016
1141
|
const allowed = new Set(choiceOptions(field2).map((o) => o.value));
|
|
1017
|
-
|
|
1142
|
+
const others = Array.isArray(value) ? value.filter((v) => typeof v === "string" && !allowed.has(v)) : [];
|
|
1143
|
+
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
1144
|
},
|
|
1019
1145
|
summarize: (value, field2) => Array.isArray(value) ? value.map((v) => optionLabel(field2, String(v))).join(", ") : String(value)
|
|
1020
1146
|
},
|
|
@@ -1035,7 +1161,7 @@ var builtins = [
|
|
|
1035
1161
|
value: { json: true },
|
|
1036
1162
|
operators: ["eq", "neq", "in", "answered", "empty"],
|
|
1037
1163
|
a11y: { semantics: "control" },
|
|
1038
|
-
validate: (value, field2) => typeof value === "string" && choiceOptions(field2).some((o) => o.value === value) ? null : "Choose one of the options.",
|
|
1164
|
+
validate: (value, field2) => typeof value === "string" && (choiceOptions(field2).some((o) => o.value === value) || typedOther(field2, value)) ? null : "Choose one of the options.",
|
|
1039
1165
|
summarize: (value, field2) => optionLabel(field2, String(value))
|
|
1040
1166
|
},
|
|
1041
1167
|
{
|
|
@@ -1145,7 +1271,7 @@ function dateLimits(field2) {
|
|
|
1145
1271
|
if (typeof max === "string" && isIsoDate(max)) out.max = max;
|
|
1146
1272
|
return out;
|
|
1147
1273
|
}
|
|
1148
|
-
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType]) registerFieldType(manifest);
|
|
1274
|
+
for (const manifest of [...builtins, ...structuredFieldTypes, calculatedFieldType, photoFieldType]) registerFieldType(manifest);
|
|
1149
1275
|
|
|
1150
1276
|
// src/logic.ts
|
|
1151
1277
|
function own(record, key) {
|
|
@@ -1445,6 +1571,22 @@ function parseForm(input) {
|
|
|
1445
1571
|
}
|
|
1446
1572
|
}
|
|
1447
1573
|
if (type === "consent") optionalText(props2.text, `${path}.text`);
|
|
1574
|
+
if (type === PHOTO_TYPE) {
|
|
1575
|
+
if (props2.maxFiles !== void 0 && !(isInt(props2.maxFiles) && props2.maxFiles >= 1 && props2.maxFiles <= MAX_PHOTO_FILES)) {
|
|
1576
|
+
issue(`${path}.maxFiles`, `Must be a whole number from 1 to ${MAX_PHOTO_FILES}.`);
|
|
1577
|
+
}
|
|
1578
|
+
if (props2.maxSizeMb !== void 0 && !(isInt(props2.maxSizeMb) && props2.maxSizeMb >= 1 && props2.maxSizeMb <= MAX_PHOTO_SIZE_MB)) {
|
|
1579
|
+
issue(`${path}.maxSizeMb`, `Must be a whole number from 1 to ${MAX_PHOTO_SIZE_MB}.`);
|
|
1580
|
+
}
|
|
1581
|
+
if (props2.formats !== void 0) {
|
|
1582
|
+
const formats = props2.formats;
|
|
1583
|
+
if (!Array.isArray(formats) || formats.length === 0 || !formats.every(isPhotoFormat)) {
|
|
1584
|
+
issue(`${path}.formats`, "Must be a list of one or more of jpeg, png, heic and webp.");
|
|
1585
|
+
} else if (new Set(formats).size !== formats.length) {
|
|
1586
|
+
issue(`${path}.formats`, "Name each format once.");
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1448
1590
|
};
|
|
1449
1591
|
if (!isObject2(input)) {
|
|
1450
1592
|
issue("", "A form definition must be an object.");
|
|
@@ -1465,6 +1607,7 @@ function parseForm(input) {
|
|
|
1465
1607
|
else {
|
|
1466
1608
|
optionalBoolean(input.settings.anonymous, "settings.anonymous");
|
|
1467
1609
|
optionalBoolean(input.settings.clearHiddenValues, "settings.clearHiddenValues");
|
|
1610
|
+
optionalBoolean(input.settings.saveProgress, "settings.saveProgress");
|
|
1468
1611
|
}
|
|
1469
1612
|
}
|
|
1470
1613
|
if (input.meta !== void 0 && (!isObject2(input.meta) || !isJson(input.meta))) {
|
|
@@ -1505,6 +1648,14 @@ function parseForm(input) {
|
|
|
1505
1648
|
issue(`${path}.type`, `Unknown field type "${String(block.type)}".`);
|
|
1506
1649
|
}
|
|
1507
1650
|
if (block.props !== void 0) {
|
|
1651
|
+
const from = isObject2(block.props) ? block.props.optionsFrom : void 0;
|
|
1652
|
+
if (from !== void 0) {
|
|
1653
|
+
if (!isObject2(from) || from.source !== "host" || typeof from.key !== "string" || !from.key) {
|
|
1654
|
+
issue(`${path}.props.optionsFrom`, 'Must be { source: "host", key } with a non-empty key.');
|
|
1655
|
+
} else if (typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
1656
|
+
warnings.push({ path: `${path}.props.optionsFrom`, message: "Only single choice, multiple choice and dropdown questions take options from the app." });
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1508
1659
|
if (!isObject2(block.props) || !isJson(block.props)) issue(`${path}.props`, "Must be a JSON object.");
|
|
1509
1660
|
else if (block.props.options !== void 0) {
|
|
1510
1661
|
const options = block.props.options;
|
|
@@ -1515,10 +1666,20 @@ function parseForm(input) {
|
|
|
1515
1666
|
if (!isObject2(o) || typeof o.value !== "string") issue(at, "Each option needs a string value.");
|
|
1516
1667
|
else optionalText(o.label, `${at}.label`);
|
|
1517
1668
|
});
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1669
|
+
if (from === void 0) {
|
|
1670
|
+
choices.set(
|
|
1671
|
+
block.id,
|
|
1672
|
+
new Set(options.flatMap((o) => isObject2(o) && typeof o.value === "string" ? [o.value] : []))
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
const other = isObject2(block.props) ? block.props.other : void 0;
|
|
1678
|
+
if (other !== void 0) {
|
|
1679
|
+
if (other !== true && other !== false && !(isObject2(other) && (other.label === void 0 || typeof other.label === "string"))) {
|
|
1680
|
+
issue(`${path}.props.other`, "Must be true, false or { label }.");
|
|
1681
|
+
} else if (other !== false && typeof block.type === "string" && !CHOICE_TYPES.has(block.type)) {
|
|
1682
|
+
warnings.push({ path: `${path}.props.other`, message: 'Only single choice, multiple choice and dropdown questions take an "Other" answer.' });
|
|
1522
1683
|
}
|
|
1523
1684
|
}
|
|
1524
1685
|
const { max, maxLength: maxLength2 } = isObject2(block.props) ? block.props : {};
|
|
@@ -1593,6 +1754,8 @@ function parseForm(input) {
|
|
|
1593
1754
|
} else if (ref.column !== void 0) issue(path, `"${ref.key}" is not a table, so it has no column "${ref.column}".`);
|
|
1594
1755
|
else if (targetType === "media-notes" || targetType === "text-highlights") {
|
|
1595
1756
|
warnings.push({ path, message: `"${ref.key}" holds notes, not numbers; the formula reads it as blank.` });
|
|
1757
|
+
} else if (targetType === PHOTO_TYPE) {
|
|
1758
|
+
warnings.push({ path, message: `"${ref.key}" holds photos, not numbers; the formula reads it as blank.` });
|
|
1596
1759
|
}
|
|
1597
1760
|
}
|
|
1598
1761
|
});
|
|
@@ -1812,9 +1975,15 @@ function field(type, key, options, props2) {
|
|
|
1812
1975
|
return block;
|
|
1813
1976
|
}
|
|
1814
1977
|
var props = (all) => Object.fromEntries(Object.entries(all).filter((e) => e[1] !== void 0));
|
|
1815
|
-
var choiceProps = (
|
|
1816
|
-
|
|
1817
|
-
|
|
1978
|
+
var choiceProps = (o) => {
|
|
1979
|
+
const out = {};
|
|
1980
|
+
if (o.options || !o.optionsFrom) {
|
|
1981
|
+
out.options = (o.options ?? []).map((x) => x.label ? { value: x.value, label: x.label } : { value: x.value });
|
|
1982
|
+
}
|
|
1983
|
+
if (o.optionsFrom) out.optionsFrom = { source: o.optionsFrom.source, key: o.optionsFrom.key };
|
|
1984
|
+
if (o.other) out.other = o.other === true ? true : props({ label: o.other.label });
|
|
1985
|
+
return out;
|
|
1986
|
+
};
|
|
1818
1987
|
var hiddenSpec = (type, o) => ({
|
|
1819
1988
|
type,
|
|
1820
1989
|
source: o.source ?? "url",
|
|
@@ -1838,11 +2007,11 @@ var f = {
|
|
|
1838
2007
|
),
|
|
1839
2008
|
/** A 0-to-10 Net Promoter Score. The end labels default to "Not likely" and "Very likely" in the renderer. */
|
|
1840
2009
|
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
|
|
1842
|
-
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o
|
|
2010
|
+
singleChoice: (key, o) => field("single-choice", key, o, choiceProps(o)),
|
|
2011
|
+
multiChoice: (key, o) => field("multi-choice", key, o, choiceProps(o)),
|
|
1843
2012
|
yesNo: (key, o) => field("yes-no", key, o),
|
|
1844
2013
|
/** 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
|
|
2014
|
+
dropdown: (key, o) => field("dropdown", key, o, choiceProps(o)),
|
|
1846
2015
|
/** A number, optionally between `min` and `max` and on a `step` (from `min`, else 0): `step: 1` for whole numbers. */
|
|
1847
2016
|
number: (key, o) => field("number", key, o, props({ min: o.min, max: o.max, step: o.step })),
|
|
1848
2017
|
/** A calendar day, stored as `YYYY-MM-DD`, optionally between `min` and `max` (also `YYYY-MM-DD`). */
|
|
@@ -1894,6 +2063,17 @@ var f = {
|
|
|
1894
2063
|
o,
|
|
1895
2064
|
props({ text: o.text, maxHighlights: o.maxHighlights })
|
|
1896
2065
|
),
|
|
2066
|
+
/**
|
|
2067
|
+
* Up to a few photos (a picture of broken equipment). The answer holds the
|
|
2068
|
+
* references the host's upload returned, never the bytes; `formats` from
|
|
2069
|
+
* jpeg, png, heic and webp (default all four).
|
|
2070
|
+
*/
|
|
2071
|
+
photo: (key, o) => field(
|
|
2072
|
+
"photo",
|
|
2073
|
+
key,
|
|
2074
|
+
o,
|
|
2075
|
+
props({ maxFiles: o.maxFiles, maxSizeMb: o.maxSizeMb, formats: o.formats ? [...o.formats] : void 0 })
|
|
2076
|
+
),
|
|
1897
2077
|
/** Any registered field type, including plugins. */
|
|
1898
2078
|
custom: (type, key, o) => field(type, key, o, o.props),
|
|
1899
2079
|
heading: (id, text) => ({ kind: "content", id, type: "heading", text }),
|
|
@@ -2077,7 +2257,8 @@ function readsAny(node, ids) {
|
|
|
2077
2257
|
function createForm(definition, options = {}) {
|
|
2078
2258
|
return build(definition, options).engine;
|
|
2079
2259
|
}
|
|
2080
|
-
function build(
|
|
2260
|
+
function build(given, options, replaying = false, budget) {
|
|
2261
|
+
const definition = withHostOptions(given, options.hostOptions);
|
|
2081
2262
|
const fields = fieldMap(definition);
|
|
2082
2263
|
const byKey = new Map([...fields.values()].map((f2) => [f2.key, f2]));
|
|
2083
2264
|
const blocks = new Map(definition.blocks.map((b) => [b.id, b]));
|
|
@@ -2123,6 +2304,7 @@ function build(definition, options, replaying = false, budget) {
|
|
|
2123
2304
|
path: first.path,
|
|
2124
2305
|
current: first.path[0] ?? null,
|
|
2125
2306
|
history: [],
|
|
2307
|
+
furthest: first.path[0] ?? null,
|
|
2126
2308
|
errors: {},
|
|
2127
2309
|
status: "in_progress",
|
|
2128
2310
|
ending: null,
|
|
@@ -2223,7 +2405,8 @@ function build(definition, options, replaying = false, budget) {
|
|
|
2223
2405
|
emit({ ...state, events });
|
|
2224
2406
|
return true;
|
|
2225
2407
|
}
|
|
2226
|
-
|
|
2408
|
+
const furthest = state.furthest && state.path.indexOf(state.furthest) >= state.path.indexOf(target) ? state.furthest : target;
|
|
2409
|
+
emit({ ...state, current: target, history: [...state.history, state.current], furthest, events });
|
|
2227
2410
|
return true;
|
|
2228
2411
|
},
|
|
2229
2412
|
back() {
|
|
@@ -2268,6 +2451,15 @@ function build(definition, options, replaying = false, budget) {
|
|
|
2268
2451
|
settleNow();
|
|
2269
2452
|
batch = wasBatch;
|
|
2270
2453
|
}
|
|
2454
|
+
const resumeAt = options.resumeAt;
|
|
2455
|
+
if (resumeAt && !replaying && state.path.includes(resumeAt)) {
|
|
2456
|
+
for (let i = 0; i < state.path.length && state.current !== resumeAt; i++) {
|
|
2457
|
+
if (!engine.next()) {
|
|
2458
|
+
state = { ...state, errors: {} };
|
|
2459
|
+
break;
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2271
2463
|
const apply = (event) => {
|
|
2272
2464
|
if (event.type === "answer") {
|
|
2273
2465
|
if (fields.has(event.field)) {
|
|
@@ -2307,6 +2499,33 @@ function replayWithin(definition, events, options, maxWork) {
|
|
|
2307
2499
|
return engine;
|
|
2308
2500
|
}
|
|
2309
2501
|
|
|
2502
|
+
// src/progress.ts
|
|
2503
|
+
function furthestReached(state, definition) {
|
|
2504
|
+
const at = (id) => id ? state.path.indexOf(id) : -1;
|
|
2505
|
+
let furthest = Math.max(at(state.current), at(state.furthest));
|
|
2506
|
+
for (const id of state.history) furthest = Math.max(furthest, at(id));
|
|
2507
|
+
const fields = fieldMap(definition);
|
|
2508
|
+
for (let i = state.path.length - 1; i > furthest; i--) {
|
|
2509
|
+
const field2 = fields.get(state.path[i]);
|
|
2510
|
+
if (field2 && !isEmptyValue(own(state.values, field2.id), field2)) {
|
|
2511
|
+
furthest = i;
|
|
2512
|
+
break;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
return furthest >= 0 ? state.path[furthest] : null;
|
|
2516
|
+
}
|
|
2517
|
+
function formProgress(engine) {
|
|
2518
|
+
const state = engine.getState();
|
|
2519
|
+
const furthest = furthestReached(state, engine.definition);
|
|
2520
|
+
const index = furthest ? state.path.indexOf(furthest) : -1;
|
|
2521
|
+
const fields = fieldMap(engine.definition);
|
|
2522
|
+
let fieldId = null;
|
|
2523
|
+
for (let i = index; i >= 0 && fieldId === null; i--) {
|
|
2524
|
+
if (fields.has(state.path[i])) fieldId = state.path[i];
|
|
2525
|
+
}
|
|
2526
|
+
return { step: index + 1, total: state.path.length, fieldId };
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2310
2529
|
// src/explain.ts
|
|
2311
2530
|
function explain(source, blockId, options = {}) {
|
|
2312
2531
|
const input = "getState" in source ? { definition: source.definition, values: source.getState().values, hidden: source.getState().hidden } : source;
|
|
@@ -2445,6 +2664,8 @@ function describeCondition(condition, def, fields = fieldMap(def)) {
|
|
|
2445
2664
|
|
|
2446
2665
|
exports.CALCULATED_TYPE = CALCULATED_TYPE;
|
|
2447
2666
|
exports.COMPUTED_FIELD_TYPES = COMPUTED_FIELD_TYPES;
|
|
2667
|
+
exports.DEFAULT_PHOTO_FILES = DEFAULT_PHOTO_FILES;
|
|
2668
|
+
exports.DEFAULT_PHOTO_SIZE_MB = DEFAULT_PHOTO_SIZE_MB;
|
|
2448
2669
|
exports.FORMULA_FUNCTIONS = FORMULA_FUNCTIONS;
|
|
2449
2670
|
exports.HIGHLIGHT_NOTE_MESSAGE = HIGHLIGHT_NOTE_MESSAGE;
|
|
2450
2671
|
exports.MAX_CALCULATED_DECIMALS = MAX_CALCULATED_DECIMALS;
|
|
@@ -2454,12 +2675,20 @@ exports.MAX_FORM_FORMULA_LENGTH = MAX_FORM_FORMULA_LENGTH;
|
|
|
2454
2675
|
exports.MAX_HIDDEN_STRING_LENGTH = MAX_HIDDEN_STRING_LENGTH;
|
|
2455
2676
|
exports.MAX_HIGHLIGHT_NOTE_LENGTH = MAX_HIGHLIGHT_NOTE_LENGTH;
|
|
2456
2677
|
exports.MAX_HIGHLIGHT_QUOTE_LENGTH = MAX_HIGHLIGHT_QUOTE_LENGTH;
|
|
2678
|
+
exports.MAX_PHOTO_FILES = MAX_PHOTO_FILES;
|
|
2679
|
+
exports.MAX_PHOTO_ID_LENGTH = MAX_PHOTO_ID_LENGTH;
|
|
2680
|
+
exports.MAX_PHOTO_NAME_LENGTH = MAX_PHOTO_NAME_LENGTH;
|
|
2681
|
+
exports.MAX_PHOTO_SIZE_MB = MAX_PHOTO_SIZE_MB;
|
|
2457
2682
|
exports.MAX_SETTLE_ITEMS = MAX_SETTLE_ITEMS;
|
|
2458
2683
|
exports.MAX_TABLE_CELL_LENGTH = MAX_TABLE_CELL_LENGTH;
|
|
2459
2684
|
exports.MAX_TABLE_COLUMNS = MAX_TABLE_COLUMNS;
|
|
2460
2685
|
exports.MAX_TIMESTAMP_SECONDS = MAX_TIMESTAMP_SECONDS;
|
|
2686
|
+
exports.OTHER_MAX_LENGTH = OTHER_MAX_LENGTH;
|
|
2687
|
+
exports.PHOTO_FORMATS = PHOTO_FORMATS;
|
|
2688
|
+
exports.PHOTO_TYPE = PHOTO_TYPE;
|
|
2461
2689
|
exports.REQUIRED_MESSAGE = REQUIRED_MESSAGE;
|
|
2462
2690
|
exports.STRUCTURED_FIELD_TYPES = STRUCTURED_FIELD_TYPES;
|
|
2691
|
+
exports.UPLOAD_FIELD_TYPES = UPLOAD_FIELD_TYPES;
|
|
2463
2692
|
exports.calculatedProps = calculatedProps;
|
|
2464
2693
|
exports.calculatedValue = calculatedValue;
|
|
2465
2694
|
exports.canonicalJson = canonicalJson;
|
|
@@ -2476,24 +2705,37 @@ exports.evaluateFormula = evaluateFormula;
|
|
|
2476
2705
|
exports.explain = explain;
|
|
2477
2706
|
exports.f = f;
|
|
2478
2707
|
exports.fingerprint = fingerprint;
|
|
2708
|
+
exports.formProgress = formProgress;
|
|
2479
2709
|
exports.formatCalculated = formatCalculated;
|
|
2480
2710
|
exports.formatTimestamp = formatTimestamp;
|
|
2481
2711
|
exports.formulaInput = formulaInput;
|
|
2482
2712
|
exports.formulaNumber = formulaNumber;
|
|
2483
2713
|
exports.formulaRefs = formulaRefs;
|
|
2714
|
+
exports.furthestReached = furthestReached;
|
|
2484
2715
|
exports.getFieldType = getFieldType;
|
|
2485
2716
|
exports.isEmptyValue = isEmptyValue;
|
|
2486
2717
|
exports.isFormEvent = isFormEvent;
|
|
2487
2718
|
exports.isIsoDate = isIsoDate;
|
|
2488
2719
|
exports.isJson = isJson;
|
|
2720
|
+
exports.isOtherValue = isOtherValue;
|
|
2721
|
+
exports.isPhotoFormat = isPhotoFormat;
|
|
2722
|
+
exports.isPhotoRef = isPhotoRef;
|
|
2489
2723
|
exports.isSensitive = isSensitive;
|
|
2490
2724
|
exports.listFieldTypes = listFieldTypes;
|
|
2491
2725
|
exports.listLimits = listLimits;
|
|
2492
2726
|
exports.normalizeValue = normalizeValue;
|
|
2493
2727
|
exports.numberLimits = numberLimits;
|
|
2728
|
+
exports.optionsSource = optionsSource;
|
|
2729
|
+
exports.otherOption = otherOption;
|
|
2494
2730
|
exports.parseForm = parseForm;
|
|
2495
2731
|
exports.parseFormula = parseFormula;
|
|
2496
2732
|
exports.parseTimestamp = parseTimestamp;
|
|
2733
|
+
exports.photoCountMessage = photoCountMessage;
|
|
2734
|
+
exports.photoFormatList = photoFormatList;
|
|
2735
|
+
exports.photoLimits = photoLimits;
|
|
2736
|
+
exports.photoRefs = photoRefs;
|
|
2737
|
+
exports.photoSizeMessage = photoSizeMessage;
|
|
2738
|
+
exports.photoTypeMessage = photoTypeMessage;
|
|
2497
2739
|
exports.ratingDisplay = ratingDisplay;
|
|
2498
2740
|
exports.recall = recall;
|
|
2499
2741
|
exports.registerFieldType = registerFieldType;
|
|
@@ -2507,5 +2749,6 @@ exports.tableColumns = tableColumns;
|
|
|
2507
2749
|
exports.tableLimits = tableLimits;
|
|
2508
2750
|
exports.validateField = validateField;
|
|
2509
2751
|
exports.when = when;
|
|
2752
|
+
exports.withHostOptions = withHostOptions;
|
|
2510
2753
|
//# sourceMappingURL=index.cjs.map
|
|
2511
2754
|
//# sourceMappingURL=index.cjs.map
|