@marktiderman/genesis-forms-web 0.3.0 → 0.4.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 +34 -0
- package/dist/index.d.ts +69 -2
- package/dist/index.js +487 -54
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { createContext, useState, useEffect, useRef, useId, useContext, useLayoutEffect } from 'react';
|
|
2
|
+
import { createContext, useState, useEffect, useRef, useId, useContext, useMemo, useLayoutEffect } from 'react';
|
|
3
3
|
import { Button } from '@marktiderman/genesis-ui/button';
|
|
4
4
|
import { Progress } from '@marktiderman/genesis-ui/progress';
|
|
5
5
|
import { cn } from '@marktiderman/genesis-ui/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { ratingDisplay, choiceOptions, otherOption, numberLimits, dateLimits, listLimits, tableColumns, tableLimits, parseTimestamp, formatTimestamp, formatCalculated, photoLimits, photoRefs, getFieldType, scaleLabels, OTHER_MAX_LENGTH, photoCountMessage, isPhotoRef, photoTypeMessage, photoSizeMessage, formProgress } from '@marktiderman/genesis-forms';
|
|
7
|
+
import { createRendererRegistry, useFormSelector, useForm, FormRuntime, useFormContext, FieldRenderer } from '@marktiderman/genesis-forms-react';
|
|
7
8
|
import { Checkbox } from '@marktiderman/genesis-ui/checkbox';
|
|
8
9
|
import { Input } from '@marktiderman/genesis-ui/input';
|
|
9
10
|
import { Label } from '@marktiderman/genesis-ui/label';
|
|
10
11
|
import { RadioGroup, RadioGroupItem } from '@marktiderman/genesis-ui/radio-group';
|
|
11
12
|
import { Textarea } from '@marktiderman/genesis-ui/textarea';
|
|
12
|
-
import {
|
|
13
|
-
import { FormDescription, FormError, FormLabel } from '@marktiderman/genesis-ui/form-field';
|
|
13
|
+
import { FormError, FormDescription, FormLabel } from '@marktiderman/genesis-ui/form-field';
|
|
14
14
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
15
15
|
import { Stack } from '@marktiderman/genesis-ui/layout';
|
|
16
16
|
import { SortableList } from '@marktiderman/genesis-ui/sortable-list';
|
|
17
|
+
import { Spinner } from '@marktiderman/genesis-ui/spinner';
|
|
17
18
|
|
|
18
19
|
// src/Form.tsx
|
|
19
20
|
var FormPresentationContext = createContext({ presentation: "embedded", layout: "classic" });
|
|
@@ -772,6 +773,237 @@ var structuredRenderers = [
|
|
|
772
773
|
{ type: "media-notes", Component: MediaNotesField },
|
|
773
774
|
{ type: "text-highlights", Component: TextHighlightsField }
|
|
774
775
|
];
|
|
776
|
+
var UploadContext = createContext(null);
|
|
777
|
+
function useUpload() {
|
|
778
|
+
return useContext(UploadContext);
|
|
779
|
+
}
|
|
780
|
+
var PendingUploadsContext = createContext(null);
|
|
781
|
+
function createPendingUploads() {
|
|
782
|
+
let n = 0;
|
|
783
|
+
return {
|
|
784
|
+
start() {
|
|
785
|
+
n++;
|
|
786
|
+
let done = false;
|
|
787
|
+
return () => {
|
|
788
|
+
if (done) return;
|
|
789
|
+
done = true;
|
|
790
|
+
n--;
|
|
791
|
+
};
|
|
792
|
+
},
|
|
793
|
+
count: () => n
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
var UPLOAD_WAIT_MESSAGE = "Wait for the photo to finish uploading.";
|
|
797
|
+
function CameraIcon() {
|
|
798
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
799
|
+
/* @__PURE__ */ jsx("path", { d: "M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z" }),
|
|
800
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "13", r: "3" })
|
|
801
|
+
] });
|
|
802
|
+
}
|
|
803
|
+
function ImageIcon({ className }) {
|
|
804
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className, children: [
|
|
805
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
806
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "2" }),
|
|
807
|
+
/* @__PURE__ */ jsx("path", { d: "m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" })
|
|
808
|
+
] });
|
|
809
|
+
}
|
|
810
|
+
function XIcon2() {
|
|
811
|
+
return /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx("path", { d: "M18 6 6 18M6 6l12 12" }) });
|
|
812
|
+
}
|
|
813
|
+
var HEIC_NAME = /\.hei[cf]$/i;
|
|
814
|
+
function photoFileType(file) {
|
|
815
|
+
const type = file.type.toLowerCase();
|
|
816
|
+
if (type) return type;
|
|
817
|
+
return HEIC_NAME.test(file.name) ? file.name.toLowerCase().endsWith(".heif") ? "image/heif" : "image/heic" : "";
|
|
818
|
+
}
|
|
819
|
+
function photoFileProblem(field, file) {
|
|
820
|
+
const limits = photoLimits(field);
|
|
821
|
+
if (!limits.mimeTypes.includes(photoFileType(file))) return photoTypeMessage(field);
|
|
822
|
+
if (file.size <= 0 || file.size > limits.maxBytes) return photoSizeMessage(field);
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
function photoAccept(field) {
|
|
826
|
+
const limits = photoLimits(field);
|
|
827
|
+
return [...limits.mimeTypes, ...limits.formats.includes("heic") ? [".heic", ".heif"] : []].join(",");
|
|
828
|
+
}
|
|
829
|
+
function Tile({ name, preview, busy }) {
|
|
830
|
+
const [failed, setFailed] = useState(false);
|
|
831
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative size-24 overflow-hidden rounded-md border bg-muted sm:size-28", children: [
|
|
832
|
+
preview && !failed ? /* @__PURE__ */ jsx("img", { src: preview, alt: name, className: cn("size-full object-cover", busy && "opacity-60"), onError: () => setFailed(true) }) : /* @__PURE__ */ jsxs("div", { className: "flex size-full flex-col items-center justify-center gap-1 p-2 text-center text-xs text-muted-foreground", children: [
|
|
833
|
+
/* @__PURE__ */ jsx(ImageIcon, { className: "size-6" }),
|
|
834
|
+
/* @__PURE__ */ jsx("span", { className: "line-clamp-2 break-all", children: name })
|
|
835
|
+
] }),
|
|
836
|
+
busy ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx(Spinner, { size: "sm", "aria-hidden": "true" }) }) : null
|
|
837
|
+
] });
|
|
838
|
+
}
|
|
839
|
+
var nextKey = 0;
|
|
840
|
+
function PhotoField(props) {
|
|
841
|
+
const { binding, inputId, engine } = props;
|
|
842
|
+
const { field } = binding;
|
|
843
|
+
const host = useUpload();
|
|
844
|
+
const pending = useContext(PendingUploadsContext);
|
|
845
|
+
const locked = useFormSelector(engine, (s) => s.status === "completed");
|
|
846
|
+
const limits = photoLimits(field);
|
|
847
|
+
const refs = photoRefs(binding.value);
|
|
848
|
+
const [uploading, setUploading] = useState([]);
|
|
849
|
+
const [problem, setProblem] = useState(null);
|
|
850
|
+
const [coarse, setCoarse] = useState(false);
|
|
851
|
+
const previews = useRef(/* @__PURE__ */ new Map());
|
|
852
|
+
const releases = useRef(/* @__PURE__ */ new Set());
|
|
853
|
+
const mounted = useRef(true);
|
|
854
|
+
const chooseRef = useRef(null);
|
|
855
|
+
const takeRef = useRef(null);
|
|
856
|
+
useEffect(() => {
|
|
857
|
+
setCoarse(typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches);
|
|
858
|
+
}, []);
|
|
859
|
+
useEffect(() => {
|
|
860
|
+
const urls = previews.current;
|
|
861
|
+
const pendingReleases = releases.current;
|
|
862
|
+
mounted.current = true;
|
|
863
|
+
return () => {
|
|
864
|
+
mounted.current = false;
|
|
865
|
+
for (const url of urls.values()) URL.revokeObjectURL(url);
|
|
866
|
+
urls.clear();
|
|
867
|
+
for (const release of pendingReleases) release();
|
|
868
|
+
pendingReleases.clear();
|
|
869
|
+
};
|
|
870
|
+
}, []);
|
|
871
|
+
const currentRefs = () => photoRefs(engine.getState().values[field.id]);
|
|
872
|
+
const full = refs.length + uploading.length >= limits.maxFiles;
|
|
873
|
+
const disabled = !host || locked;
|
|
874
|
+
const add = (files) => {
|
|
875
|
+
if (!host) return;
|
|
876
|
+
setProblem(null);
|
|
877
|
+
let room = limits.maxFiles - currentRefs().length - uploading.length;
|
|
878
|
+
for (const file of files) {
|
|
879
|
+
if (room <= 0) {
|
|
880
|
+
setProblem(photoCountMessage(field));
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
883
|
+
const why = photoFileProblem(field, file);
|
|
884
|
+
if (why) {
|
|
885
|
+
setProblem(why);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
room--;
|
|
889
|
+
const key = `u${nextKey++}`;
|
|
890
|
+
const preview = typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : null;
|
|
891
|
+
setUploading((list) => [...list, { key, name: file.name, preview }]);
|
|
892
|
+
const release = pending?.start() ?? (() => {
|
|
893
|
+
});
|
|
894
|
+
releases.current.add(release);
|
|
895
|
+
const finish = () => {
|
|
896
|
+
release();
|
|
897
|
+
releases.current.delete(release);
|
|
898
|
+
setUploading((list) => list.filter((u) => u.key !== key));
|
|
899
|
+
};
|
|
900
|
+
Promise.resolve().then(() => host.upload(file, field)).then((ref) => {
|
|
901
|
+
if (!isPhotoRef(ref)) throw new Error("The upload returned no photo reference.");
|
|
902
|
+
if (preview) {
|
|
903
|
+
const old = previews.current.get(ref.id);
|
|
904
|
+
if (old && old !== preview) URL.revokeObjectURL(old);
|
|
905
|
+
if (mounted.current) previews.current.set(ref.id, preview);
|
|
906
|
+
else URL.revokeObjectURL(preview);
|
|
907
|
+
}
|
|
908
|
+
const next = [...currentRefs().filter((r) => r.id !== ref.id), { id: ref.id, name: ref.name, type: ref.type, size: ref.size }];
|
|
909
|
+
binding.setValue(next);
|
|
910
|
+
finish();
|
|
911
|
+
}).catch(() => {
|
|
912
|
+
if (preview) URL.revokeObjectURL(preview);
|
|
913
|
+
setProblem(`\u201C${file.name}\u201D didn't upload. Try again.`);
|
|
914
|
+
finish();
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
};
|
|
918
|
+
const onPick = (e) => {
|
|
919
|
+
const files = Array.from(e.target.files ?? []);
|
|
920
|
+
e.target.value = "";
|
|
921
|
+
add(files);
|
|
922
|
+
};
|
|
923
|
+
const remove = (ref) => {
|
|
924
|
+
const url = previews.current.get(ref.id);
|
|
925
|
+
if (url) URL.revokeObjectURL(url);
|
|
926
|
+
previews.current.delete(ref.id);
|
|
927
|
+
const next = currentRefs().filter((r) => r.id !== ref.id);
|
|
928
|
+
binding.setValue(next.length ? next : void 0);
|
|
929
|
+
setProblem(null);
|
|
930
|
+
};
|
|
931
|
+
const accept = photoAccept(field);
|
|
932
|
+
const errorId = `${inputId}-upload-error`;
|
|
933
|
+
const hasAny = refs.length > 0 || uploading.length > 0;
|
|
934
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
935
|
+
hasAny ? /* @__PURE__ */ jsxs("ul", { className: "flex flex-wrap gap-3", "aria-label": "Photos", children: [
|
|
936
|
+
refs.map((ref) => /* @__PURE__ */ jsxs("li", { className: "relative", children: [
|
|
937
|
+
/* @__PURE__ */ jsx(Tile, { name: ref.name, preview: previews.current.get(ref.id) ?? null }),
|
|
938
|
+
locked ? null : /* @__PURE__ */ jsx(
|
|
939
|
+
Button,
|
|
940
|
+
{
|
|
941
|
+
type: "button",
|
|
942
|
+
variant: "secondary",
|
|
943
|
+
size: "icon",
|
|
944
|
+
"aria-label": `Remove ${ref.name}`,
|
|
945
|
+
onClick: () => remove(ref),
|
|
946
|
+
className: "absolute right-1 top-1 size-7 rounded-full shadow-sm",
|
|
947
|
+
children: /* @__PURE__ */ jsx(XIcon2, {})
|
|
948
|
+
}
|
|
949
|
+
)
|
|
950
|
+
] }, ref.id)),
|
|
951
|
+
uploading.map((u) => /* @__PURE__ */ jsx("li", { "aria-label": `Uploading ${u.name}`, children: /* @__PURE__ */ jsx(Tile, { name: u.name, preview: u.preview, busy: true }) }, u.key))
|
|
952
|
+
] }) : null,
|
|
953
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
954
|
+
/* @__PURE__ */ jsxs(
|
|
955
|
+
Button,
|
|
956
|
+
{
|
|
957
|
+
type: "button",
|
|
958
|
+
variant: "outline",
|
|
959
|
+
disabled: disabled || full,
|
|
960
|
+
"aria-invalid": binding.error ? true : void 0,
|
|
961
|
+
onClick: () => chooseRef.current?.click(),
|
|
962
|
+
children: [
|
|
963
|
+
/* @__PURE__ */ jsx(ImageIcon, {}),
|
|
964
|
+
" ",
|
|
965
|
+
refs.length ? "Add another photo" : "Choose a photo"
|
|
966
|
+
]
|
|
967
|
+
}
|
|
968
|
+
),
|
|
969
|
+
coarse ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", disabled: disabled || full, onClick: () => takeRef.current?.click(), children: [
|
|
970
|
+
/* @__PURE__ */ jsx(CameraIcon, {}),
|
|
971
|
+
" Take a photo"
|
|
972
|
+
] }) : null
|
|
973
|
+
] }),
|
|
974
|
+
/* @__PURE__ */ jsx(
|
|
975
|
+
Input,
|
|
976
|
+
{
|
|
977
|
+
ref: chooseRef,
|
|
978
|
+
type: "file",
|
|
979
|
+
accept,
|
|
980
|
+
multiple: limits.maxFiles > 1,
|
|
981
|
+
className: "hidden",
|
|
982
|
+
tabIndex: -1,
|
|
983
|
+
"aria-hidden": "true",
|
|
984
|
+
disabled,
|
|
985
|
+
onChange: onPick
|
|
986
|
+
}
|
|
987
|
+
),
|
|
988
|
+
/* @__PURE__ */ jsx(
|
|
989
|
+
Input,
|
|
990
|
+
{
|
|
991
|
+
ref: takeRef,
|
|
992
|
+
type: "file",
|
|
993
|
+
accept,
|
|
994
|
+
capture: "environment",
|
|
995
|
+
className: "hidden",
|
|
996
|
+
tabIndex: -1,
|
|
997
|
+
"aria-hidden": "true",
|
|
998
|
+
disabled,
|
|
999
|
+
onChange: onPick
|
|
1000
|
+
}
|
|
1001
|
+
),
|
|
1002
|
+
!host ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: "Photos can't be added here." }) : /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: `Up to ${limits.maxFiles} ${limits.maxFiles === 1 ? "photo" : "photos"}, ${limits.maxSizeMb} MB each.` }),
|
|
1003
|
+
problem ? /* @__PURE__ */ jsx(FormError, { id: errorId, role: "alert", children: problem }) : null
|
|
1004
|
+
] }) });
|
|
1005
|
+
}
|
|
1006
|
+
var photoRenderers = [{ type: "photo", Component: PhotoField }];
|
|
775
1007
|
function groupAria(props) {
|
|
776
1008
|
return {
|
|
777
1009
|
"aria-labelledby": `${props.inputId}-label`,
|
|
@@ -1035,6 +1267,7 @@ function useChoiceLook(inputId, count) {
|
|
|
1035
1267
|
control: pageFocus ? pageChoiceKey : void 0,
|
|
1036
1268
|
onKeyDown: pageFocus ? (e) => {
|
|
1037
1269
|
if (e.altKey || e.ctrlKey || e.metaKey || e.key.length !== 1) return;
|
|
1270
|
+
if (e.target.tagName === "INPUT") return;
|
|
1038
1271
|
const index = KEYS.indexOf(e.key.toUpperCase());
|
|
1039
1272
|
if (index < 0 || index >= count) return;
|
|
1040
1273
|
const option = document.getElementById(`${inputId}-${index}`);
|
|
@@ -1073,44 +1306,118 @@ function ChoiceContent({ look, index, label }) {
|
|
|
1073
1306
|
)
|
|
1074
1307
|
] });
|
|
1075
1308
|
}
|
|
1309
|
+
var OTHER_VALUE = "\0other";
|
|
1310
|
+
function OtherText({
|
|
1311
|
+
id,
|
|
1312
|
+
label,
|
|
1313
|
+
value,
|
|
1314
|
+
onChange,
|
|
1315
|
+
invalid: invalid2
|
|
1316
|
+
}) {
|
|
1317
|
+
const line = useLineClass();
|
|
1318
|
+
return /* @__PURE__ */ jsx(
|
|
1319
|
+
Input,
|
|
1320
|
+
{
|
|
1321
|
+
id,
|
|
1322
|
+
value,
|
|
1323
|
+
maxLength: OTHER_MAX_LENGTH,
|
|
1324
|
+
"aria-label": `${label}: your answer`,
|
|
1325
|
+
"aria-invalid": invalid2 || void 0,
|
|
1326
|
+
placeholder: "Type your answer",
|
|
1327
|
+
onChange: (e) => onChange(e.target.value),
|
|
1328
|
+
className: cn("mt-2", line)
|
|
1329
|
+
}
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
var focusSoon = (id) => requestAnimationFrame(() => document.getElementById(id)?.focus());
|
|
1333
|
+
function useOtherText(typed) {
|
|
1334
|
+
const [on, setOn] = useState(typed !== "");
|
|
1335
|
+
const [text, setText] = useState(typed);
|
|
1336
|
+
return { on, setOn, text, setText };
|
|
1337
|
+
}
|
|
1076
1338
|
function SingleChoiceField(props) {
|
|
1077
1339
|
const { binding, inputId, onAdvance } = props;
|
|
1078
1340
|
const pick = useDeliberatePick(onAdvance);
|
|
1079
1341
|
const options = choiceOptions(binding.field);
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
/* @__PURE__ */ jsx(
|
|
1092
|
-
RadioGroupItem,
|
|
1093
|
-
{
|
|
1094
|
-
id: `${inputId}-${i}`,
|
|
1095
|
-
value: o.value,
|
|
1096
|
-
onClick: pick.onItemClick,
|
|
1097
|
-
className: look.control
|
|
1098
|
-
}
|
|
1099
|
-
),
|
|
1100
|
-
/* @__PURE__ */ jsx(ChoiceContent, { look, index: i, label: o.label ?? o.value })
|
|
1101
|
-
] }, o.value))
|
|
1342
|
+
const other = otherOption(binding.field);
|
|
1343
|
+
const current = typeof binding.value === "string" ? binding.value : "";
|
|
1344
|
+
const typed = other && current && !options.some((o) => o.value === current) ? current : "";
|
|
1345
|
+
const otherText = useOtherText(typed);
|
|
1346
|
+
const look = useChoiceLook(inputId, options.length + (other ? 1 : 0));
|
|
1347
|
+
const textId = `${inputId}-other-text`;
|
|
1348
|
+
const choose = (v) => {
|
|
1349
|
+
if (v !== OTHER_VALUE) {
|
|
1350
|
+
otherText.setOn(false);
|
|
1351
|
+
binding.setValue(v);
|
|
1352
|
+
return;
|
|
1102
1353
|
}
|
|
1103
|
-
|
|
1354
|
+
otherText.setOn(true);
|
|
1355
|
+
binding.setValue(otherText.text.trim() ? otherText.text : void 0);
|
|
1356
|
+
focusSoon(textId);
|
|
1357
|
+
};
|
|
1358
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1359
|
+
/* @__PURE__ */ jsxs(
|
|
1360
|
+
RadioGroup,
|
|
1361
|
+
{
|
|
1362
|
+
...groupAria(props),
|
|
1363
|
+
...pick.rootProps,
|
|
1364
|
+
onKeyDown: look.onKeyDown,
|
|
1365
|
+
className: look.pageFocus ? "gap-2.5" : void 0,
|
|
1366
|
+
value: otherText.on ? OTHER_VALUE : current,
|
|
1367
|
+
onValueChange: choose,
|
|
1368
|
+
children: [
|
|
1369
|
+
options.map((o, i) => /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${i}`, className: look.row, children: [
|
|
1370
|
+
/* @__PURE__ */ jsx(
|
|
1371
|
+
RadioGroupItem,
|
|
1372
|
+
{
|
|
1373
|
+
id: `${inputId}-${i}`,
|
|
1374
|
+
value: o.value,
|
|
1375
|
+
onClick: pick.onItemClick,
|
|
1376
|
+
className: look.control
|
|
1377
|
+
}
|
|
1378
|
+
),
|
|
1379
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: i, label: o.label ?? o.value })
|
|
1380
|
+
] }, o.value)),
|
|
1381
|
+
other ? (
|
|
1382
|
+
// No auto-advance: the respondent still has to type.
|
|
1383
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${options.length}`, className: look.row, children: [
|
|
1384
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { id: `${inputId}-${options.length}`, value: OTHER_VALUE, className: look.control }),
|
|
1385
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: options.length, label: other.label })
|
|
1386
|
+
] })
|
|
1387
|
+
) : null
|
|
1388
|
+
]
|
|
1389
|
+
}
|
|
1390
|
+
),
|
|
1391
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1392
|
+
OtherText,
|
|
1393
|
+
{
|
|
1394
|
+
id: textId,
|
|
1395
|
+
label: other.label,
|
|
1396
|
+
value: otherText.text,
|
|
1397
|
+
invalid: !!binding.error,
|
|
1398
|
+
onChange: (text) => {
|
|
1399
|
+
otherText.setText(text);
|
|
1400
|
+
binding.setValue(text.trim() ? text : void 0);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
) : null
|
|
1404
|
+
] });
|
|
1104
1405
|
}
|
|
1105
1406
|
function MultiChoiceField(props) {
|
|
1106
1407
|
const { binding, inputId } = props;
|
|
1107
1408
|
const selected = Array.isArray(binding.value) ? binding.value : [];
|
|
1108
1409
|
const options = choiceOptions(binding.field);
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1410
|
+
const other = otherOption(binding.field);
|
|
1411
|
+
const known = new Set(options.map((o) => o.value));
|
|
1412
|
+
const chosen = selected.filter((v) => known.has(v));
|
|
1413
|
+
const otherText = useOtherText(other ? selected.find((v) => !known.has(v)) ?? "" : "");
|
|
1414
|
+
const look = useChoiceLook(inputId, options.length + (other ? 1 : 0));
|
|
1415
|
+
const textId = `${inputId}-other-text`;
|
|
1416
|
+
const commit = (picked, otherOn, text) => {
|
|
1417
|
+
const next = otherOn && text.trim() ? [...picked, text] : picked;
|
|
1112
1418
|
binding.setValue(next.length ? next : void 0);
|
|
1113
1419
|
};
|
|
1420
|
+
const toggle = (value, on) => commit(on ? [...chosen, value] : chosen.filter((v) => v !== value), otherText.on, otherText.text);
|
|
1114
1421
|
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs("div", { className: cn("grid gap-2", look.pageFocus && "gap-2.5"), onKeyDown: look.onKeyDown, children: [
|
|
1115
1422
|
look.pageFocus ? /* @__PURE__ */ jsx("p", { className: "-mt-2 mb-1 text-sm text-muted-foreground", children: "Choose as many as you like" }) : null,
|
|
1116
1423
|
options.map((o, i) => /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${i}`, className: look.row, children: [
|
|
@@ -1125,7 +1432,37 @@ function MultiChoiceField(props) {
|
|
|
1125
1432
|
}
|
|
1126
1433
|
),
|
|
1127
1434
|
/* @__PURE__ */ jsx(ChoiceContent, { look, index: i, label: o.label ?? o.value })
|
|
1128
|
-
] }, o.value))
|
|
1435
|
+
] }, o.value)),
|
|
1436
|
+
other ? /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${options.length}`, className: look.row, children: [
|
|
1437
|
+
/* @__PURE__ */ jsx(
|
|
1438
|
+
Checkbox,
|
|
1439
|
+
{
|
|
1440
|
+
id: `${inputId}-${options.length}`,
|
|
1441
|
+
checked: otherText.on,
|
|
1442
|
+
"aria-invalid": binding.error ? true : void 0,
|
|
1443
|
+
onCheckedChange: (c) => {
|
|
1444
|
+
otherText.setOn(c === true);
|
|
1445
|
+
commit(chosen, c === true, otherText.text);
|
|
1446
|
+
if (c === true) focusSoon(textId);
|
|
1447
|
+
},
|
|
1448
|
+
className: look.control
|
|
1449
|
+
}
|
|
1450
|
+
),
|
|
1451
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: options.length, label: other.label })
|
|
1452
|
+
] }) : null,
|
|
1453
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1454
|
+
OtherText,
|
|
1455
|
+
{
|
|
1456
|
+
id: textId,
|
|
1457
|
+
label: other.label,
|
|
1458
|
+
value: otherText.text,
|
|
1459
|
+
invalid: !!binding.error,
|
|
1460
|
+
onChange: (text) => {
|
|
1461
|
+
otherText.setText(text);
|
|
1462
|
+
commit(chosen, true, text);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
) : null
|
|
1129
1466
|
] }) });
|
|
1130
1467
|
}
|
|
1131
1468
|
function useLineClass() {
|
|
@@ -1147,25 +1484,53 @@ function controlAria(props) {
|
|
|
1147
1484
|
function DropdownField(props) {
|
|
1148
1485
|
const { binding } = props;
|
|
1149
1486
|
const options = choiceOptions(binding.field);
|
|
1487
|
+
const other = otherOption(binding.field);
|
|
1150
1488
|
const line = useLineClass();
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1489
|
+
const current = typeof binding.value === "string" ? binding.value : "";
|
|
1490
|
+
const typed = other && current && !options.some((o) => o.value === current) ? current : "";
|
|
1491
|
+
const otherText = useOtherText(typed);
|
|
1492
|
+
const value = otherText.on ? OTHER_VALUE : current;
|
|
1493
|
+
const textId = `${props.inputId}-other-text`;
|
|
1494
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1495
|
+
/* @__PURE__ */ jsxs(
|
|
1496
|
+
"select",
|
|
1497
|
+
{
|
|
1498
|
+
...controlAria(props),
|
|
1499
|
+
value,
|
|
1500
|
+
onChange: (e) => {
|
|
1501
|
+
const v = e.target.value;
|
|
1502
|
+
otherText.setOn(v === OTHER_VALUE);
|
|
1503
|
+
if (v === OTHER_VALUE) {
|
|
1504
|
+
binding.setValue(otherText.text.trim() ? otherText.text : void 0);
|
|
1505
|
+
focusSoon(textId);
|
|
1506
|
+
} else binding.setValue(v === "" ? void 0 : v);
|
|
1507
|
+
},
|
|
1508
|
+
className: cn(
|
|
1509
|
+
"flex h-10 w-full min-w-0 rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm aria-invalid:border-destructive",
|
|
1510
|
+
value === "" && "text-muted-foreground",
|
|
1511
|
+
line
|
|
1512
|
+
),
|
|
1513
|
+
children: [
|
|
1514
|
+
/* @__PURE__ */ jsx("option", { value: "", children: binding.field.placeholder ?? "Choose\u2026" }),
|
|
1515
|
+
options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label ?? o.value }, o.value)),
|
|
1516
|
+
other ? /* @__PURE__ */ jsx("option", { value: OTHER_VALUE, children: other.label }) : null
|
|
1517
|
+
]
|
|
1518
|
+
}
|
|
1519
|
+
),
|
|
1520
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1521
|
+
OtherText,
|
|
1522
|
+
{
|
|
1523
|
+
id: textId,
|
|
1524
|
+
label: other.label,
|
|
1525
|
+
value: otherText.text,
|
|
1526
|
+
invalid: !!binding.error,
|
|
1527
|
+
onChange: (text) => {
|
|
1528
|
+
otherText.setText(text);
|
|
1529
|
+
binding.setValue(text.trim() ? text : void 0);
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
) : null
|
|
1533
|
+
] });
|
|
1169
1534
|
}
|
|
1170
1535
|
function NumberField(props) {
|
|
1171
1536
|
const { binding } = props;
|
|
@@ -1254,7 +1619,8 @@ var builtInRenderers = [
|
|
|
1254
1619
|
{ type: "date", Component: DateField },
|
|
1255
1620
|
{ type: "consent", Component: ConsentField },
|
|
1256
1621
|
...structuredRenderers,
|
|
1257
|
-
...calculatedRenderers
|
|
1622
|
+
...calculatedRenderers,
|
|
1623
|
+
...photoRenderers
|
|
1258
1624
|
];
|
|
1259
1625
|
var defaultRegistry = createRendererRegistry(builtInRenderers);
|
|
1260
1626
|
|
|
@@ -1598,6 +1964,8 @@ function DefaultErrorSummary({ count, errors, className }) {
|
|
|
1598
1964
|
);
|
|
1599
1965
|
}
|
|
1600
1966
|
var ADVANCE_DELAY_MS = 250;
|
|
1967
|
+
var PROGRESS_DELAY_MS = 400;
|
|
1968
|
+
var CLASSIC_PROGRESS_DELAY_MS = 1500;
|
|
1601
1969
|
var engineKeys = /* @__PURE__ */ new WeakMap();
|
|
1602
1970
|
var nextEngineKey = 0;
|
|
1603
1971
|
function keyOf(engine) {
|
|
@@ -1612,11 +1980,15 @@ function Form(props) {
|
|
|
1612
1980
|
return props.engine ? /* @__PURE__ */ jsx(FormWithEngine, { ...props, engine: props.engine }) : /* @__PURE__ */ jsx(OwnedForm, { ...props });
|
|
1613
1981
|
}
|
|
1614
1982
|
function OwnedForm(props) {
|
|
1615
|
-
const engine = useForm(props.form, props.options);
|
|
1983
|
+
const engine = useForm(props.form, props.hostOptions ? { ...props.options, hostOptions: props.hostOptions } : props.options);
|
|
1616
1984
|
return /* @__PURE__ */ jsx(FormWithEngine, { ...props, engine });
|
|
1617
1985
|
}
|
|
1618
1986
|
function FormWithEngine(props) {
|
|
1619
|
-
|
|
1987
|
+
const { onUpload } = props;
|
|
1988
|
+
const outer = useContext(UploadContext);
|
|
1989
|
+
const host = useMemo(() => onUpload ? { upload: onUpload } : outer, [onUpload, outer]);
|
|
1990
|
+
const [pending] = useState(createPendingUploads);
|
|
1991
|
+
return /* @__PURE__ */ jsx(UploadContext.Provider, { value: host, children: /* @__PURE__ */ jsx(PendingUploadsContext.Provider, { value: pending, children: /* @__PURE__ */ jsx(FormRuntime, { engine: props.engine, registry: props.registry ?? defaultRegistry, children: /* @__PURE__ */ jsx(FormBody, { ...props, pendingUploads: pending }, keyOf(props.engine)) }) }) });
|
|
1620
1992
|
}
|
|
1621
1993
|
function focusWithin(root) {
|
|
1622
1994
|
if (!root) return;
|
|
@@ -1632,6 +2004,8 @@ function FormBody({
|
|
|
1632
2004
|
theme,
|
|
1633
2005
|
onSubmit,
|
|
1634
2006
|
onChange,
|
|
2007
|
+
onProgress,
|
|
2008
|
+
progressNotice = "Your answers save as you go.",
|
|
1635
2009
|
autoAdvance = true,
|
|
1636
2010
|
submitLabel = "Send",
|
|
1637
2011
|
requiredLabel = "Required",
|
|
@@ -1640,7 +2014,8 @@ function FormBody({
|
|
|
1640
2014
|
slots,
|
|
1641
2015
|
slotProps,
|
|
1642
2016
|
className,
|
|
1643
|
-
testID
|
|
2017
|
+
testID,
|
|
2018
|
+
pendingUploads
|
|
1644
2019
|
}) {
|
|
1645
2020
|
const { idPrefix } = useFormContext();
|
|
1646
2021
|
const def = engine.definition;
|
|
@@ -1675,6 +2050,52 @@ function FormBody({
|
|
|
1675
2050
|
if (!onChange) return;
|
|
1676
2051
|
return engine.subscribe(() => onChange(engine.getState(), engine));
|
|
1677
2052
|
}, [engine, onChange]);
|
|
2053
|
+
const saveProgress = def.settings?.saveProgress === true && !!onProgress;
|
|
2054
|
+
const onProgressRef = useRef(onProgress);
|
|
2055
|
+
onProgressRef.current = onProgress;
|
|
2056
|
+
useEffect(() => {
|
|
2057
|
+
if (!saveProgress) return;
|
|
2058
|
+
let timer;
|
|
2059
|
+
let last = engine.getState();
|
|
2060
|
+
const fire = () => {
|
|
2061
|
+
timer = void 0;
|
|
2062
|
+
if (engine.getState().status === "completed") return;
|
|
2063
|
+
try {
|
|
2064
|
+
void Promise.resolve(onProgressRef.current?.(engine, formProgress(engine))).catch(() => {
|
|
2065
|
+
});
|
|
2066
|
+
} catch {
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
const schedule = (delay) => {
|
|
2070
|
+
clearTimeout(timer);
|
|
2071
|
+
timer = setTimeout(fire, delay);
|
|
2072
|
+
};
|
|
2073
|
+
const unsubscribe = engine.subscribe(() => {
|
|
2074
|
+
const state = engine.getState();
|
|
2075
|
+
const previous = last;
|
|
2076
|
+
last = state;
|
|
2077
|
+
if (state.status === "completed") {
|
|
2078
|
+
clearTimeout(timer);
|
|
2079
|
+
timer = void 0;
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const event = state.events[state.events.length - 1];
|
|
2083
|
+
if (layout === "focus") {
|
|
2084
|
+
if (event?.type === "next" && state.current !== previous.current) schedule(PROGRESS_DELAY_MS);
|
|
2085
|
+
} else if (event?.type === "answer") schedule(CLASSIC_PROGRESS_DELAY_MS);
|
|
2086
|
+
});
|
|
2087
|
+
const flush = () => {
|
|
2088
|
+
if (timer === void 0) return;
|
|
2089
|
+
clearTimeout(timer);
|
|
2090
|
+
fire();
|
|
2091
|
+
};
|
|
2092
|
+
window.addEventListener("pagehide", flush);
|
|
2093
|
+
return () => {
|
|
2094
|
+
unsubscribe();
|
|
2095
|
+
window.removeEventListener("pagehide", flush);
|
|
2096
|
+
flush();
|
|
2097
|
+
};
|
|
2098
|
+
}, [engine, saveProgress, layout]);
|
|
1678
2099
|
useLayoutEffect(() => {
|
|
1679
2100
|
if (previousCurrent.current === current) return;
|
|
1680
2101
|
previousCurrent.current = current;
|
|
@@ -1703,8 +2124,14 @@ function FormBody({
|
|
|
1703
2124
|
const message = state.current ? state.errors[state.current] : void 0;
|
|
1704
2125
|
if (message) announce(message);
|
|
1705
2126
|
};
|
|
2127
|
+
const waitingForUploads = () => {
|
|
2128
|
+
if (pendingUploads.count() === 0) return false;
|
|
2129
|
+
announce(UPLOAD_WAIT_MESSAGE);
|
|
2130
|
+
return true;
|
|
2131
|
+
};
|
|
1706
2132
|
const finish = async () => {
|
|
1707
2133
|
if (sending.current) return;
|
|
2134
|
+
if (engine.getState().status !== "completed" && waitingForUploads()) return;
|
|
1708
2135
|
if (engine.getState().status !== "completed" && !engine.submit()) {
|
|
1709
2136
|
if (layout === "classic") setSummaryRequest((n) => n + 1);
|
|
1710
2137
|
else announceStepError();
|
|
@@ -1727,6 +2154,7 @@ function FormBody({
|
|
|
1727
2154
|
};
|
|
1728
2155
|
const isLastStep = (state) => state.current === state.path[state.path.length - 1];
|
|
1729
2156
|
const advance = () => {
|
|
2157
|
+
if (waitingForUploads()) return;
|
|
1730
2158
|
direction.current = "forward";
|
|
1731
2159
|
if (isLastStep(engine.getState())) void finish();
|
|
1732
2160
|
else if (!engine.next()) announceStepError();
|
|
@@ -1811,6 +2239,7 @@ function FormBody({
|
|
|
1811
2239
|
}
|
|
1812
2240
|
const pageFocus = page && layout === "focus";
|
|
1813
2241
|
const pageClassic = page && layout === "classic";
|
|
2242
|
+
const notice = saveProgress && progressNotice && historyLength === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-gf-progress-notice": "", children: progressNotice }) : null;
|
|
1814
2243
|
const renderBlock = (id, focus) => {
|
|
1815
2244
|
const block = engine.block(id);
|
|
1816
2245
|
if (!block || block.kind === "page-break") return null;
|
|
@@ -1940,6 +2369,7 @@ function FormBody({
|
|
|
1940
2369
|
/* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col justify-center px-4 pb-20 pt-4 sm:px-6 sm:pb-28", children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-xl", children: [
|
|
1941
2370
|
/* @__PURE__ */ jsx("div", { className: historyLength === 0 ? "mb-5" : "sr-only", children: header }),
|
|
1942
2371
|
fields,
|
|
2372
|
+
notice ? /* @__PURE__ */ jsx("div", { className: "mt-4", children: notice }) : null,
|
|
1943
2373
|
failure ? /* @__PURE__ */ jsx("div", { className: "mt-6", children: failure }) : null,
|
|
1944
2374
|
/* @__PURE__ */ jsx("div", { className: "mt-10", children: navigation })
|
|
1945
2375
|
] }) })
|
|
@@ -1953,6 +2383,7 @@ function FormBody({
|
|
|
1953
2383
|
children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-2xl space-y-4", children: [
|
|
1954
2384
|
brandNode ? /* @__PURE__ */ jsx("div", { className: "flex justify-center pb-2", children: brandNode }) : null,
|
|
1955
2385
|
header,
|
|
2386
|
+
notice,
|
|
1956
2387
|
errorSummary,
|
|
1957
2388
|
fields,
|
|
1958
2389
|
failure,
|
|
@@ -1962,9 +2393,11 @@ function FormBody({
|
|
|
1962
2393
|
) : /* @__PURE__ */ jsxs("form", { ...formProps, className: cn("mx-auto w-full max-w-xl space-y-8", className), children: [
|
|
1963
2394
|
brandNode,
|
|
1964
2395
|
header,
|
|
2396
|
+
layout === "classic" ? notice : null,
|
|
1965
2397
|
errorSummary,
|
|
1966
2398
|
progress,
|
|
1967
2399
|
fields,
|
|
2400
|
+
layout === "focus" ? notice : null,
|
|
1968
2401
|
failure,
|
|
1969
2402
|
navigation
|
|
1970
2403
|
] })
|
|
@@ -1972,6 +2405,6 @@ function FormBody({
|
|
|
1972
2405
|
}
|
|
1973
2406
|
var classicSection = "rounded-xl border bg-card px-5 py-6 text-card-foreground sm:px-8 has-[[aria-invalid=true]]:border-destructive";
|
|
1974
2407
|
|
|
1975
|
-
export { FieldChrome, Form, MediaTimeContext, SourceTextContext, builtInRenderers, defaultRegistry, describedBy, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText };
|
|
2408
|
+
export { CLASSIC_PROGRESS_DELAY_MS, FieldChrome, Form, MediaTimeContext, PROGRESS_DELAY_MS, SourceTextContext, UPLOAD_WAIT_MESSAGE, UploadContext, builtInRenderers, defaultRegistry, describedBy, photoAccept, photoFileProblem, photoFileType, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload };
|
|
1976
2409
|
//# sourceMappingURL=index.js.map
|
|
1977
2410
|
//# sourceMappingURL=index.js.map
|