@marktiderman/genesis-forms-web 0.3.0 → 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 +69 -1
- package/dist/index.d.ts +114 -2
- package/dist/index.js +783 -66
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
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, numberLimits, dateLimits, listLimits, tableColumns, tableLimits, parseTimestamp, formatTimestamp, formatCalculated, photoLimits, photoRefs, getFieldType, scaleLabels, isSearchable, otherOption, choiceOptions, searchOptions, OTHER_MAX_LENGTH, photoCountMessage, isPhotoRef, photoTypeMessage, photoSizeMessage, respondentPath, formProgress } from '@marktiderman/genesis-forms';
|
|
7
|
+
import { createRendererRegistry, useFormSelector, useChoiceOptions, 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';
|
|
18
|
+
import { Command, CommandInput, CommandList, CommandEmpty, CommandItem } from '@marktiderman/genesis-ui/command';
|
|
17
19
|
|
|
18
20
|
// src/Form.tsx
|
|
19
21
|
var FormPresentationContext = createContext({ presentation: "embedded", layout: "classic" });
|
|
@@ -772,6 +774,358 @@ var structuredRenderers = [
|
|
|
772
774
|
{ type: "media-notes", Component: MediaNotesField },
|
|
773
775
|
{ type: "text-highlights", Component: TextHighlightsField }
|
|
774
776
|
];
|
|
777
|
+
var UploadContext = createContext(null);
|
|
778
|
+
function useUpload() {
|
|
779
|
+
return useContext(UploadContext);
|
|
780
|
+
}
|
|
781
|
+
var PendingUploadsContext = createContext(null);
|
|
782
|
+
function createPendingUploads() {
|
|
783
|
+
let n = 0;
|
|
784
|
+
return {
|
|
785
|
+
start() {
|
|
786
|
+
n++;
|
|
787
|
+
let done = false;
|
|
788
|
+
return () => {
|
|
789
|
+
if (done) return;
|
|
790
|
+
done = true;
|
|
791
|
+
n--;
|
|
792
|
+
};
|
|
793
|
+
},
|
|
794
|
+
count: () => n
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
var UPLOAD_WAIT_MESSAGE = "Wait for the photo to finish uploading.";
|
|
798
|
+
function CameraIcon() {
|
|
799
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
|
|
800
|
+
/* @__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" }),
|
|
801
|
+
/* @__PURE__ */ jsx("circle", { cx: "12", cy: "13", r: "3" })
|
|
802
|
+
] });
|
|
803
|
+
}
|
|
804
|
+
function ImageIcon({ className }) {
|
|
805
|
+
return /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", className, children: [
|
|
806
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2" }),
|
|
807
|
+
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "2" }),
|
|
808
|
+
/* @__PURE__ */ jsx("path", { d: "m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21" })
|
|
809
|
+
] });
|
|
810
|
+
}
|
|
811
|
+
function XIcon2() {
|
|
812
|
+
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" }) });
|
|
813
|
+
}
|
|
814
|
+
var HEIC_NAME = /\.hei[cf]$/i;
|
|
815
|
+
function photoFileType(file) {
|
|
816
|
+
const type = file.type.toLowerCase();
|
|
817
|
+
if (type) return type;
|
|
818
|
+
return HEIC_NAME.test(file.name) ? file.name.toLowerCase().endsWith(".heif") ? "image/heif" : "image/heic" : "";
|
|
819
|
+
}
|
|
820
|
+
function photoFileProblem(field, file) {
|
|
821
|
+
const limits = photoLimits(field);
|
|
822
|
+
if (!limits.mimeTypes.includes(photoFileType(file))) return photoTypeMessage(field);
|
|
823
|
+
if (file.size <= 0 || file.size > limits.maxBytes) return photoSizeMessage(field);
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
function photoAccept(field) {
|
|
827
|
+
const limits = photoLimits(field);
|
|
828
|
+
return [...limits.mimeTypes, ...limits.formats.includes("heic") ? [".heic", ".heif"] : []].join(",");
|
|
829
|
+
}
|
|
830
|
+
function Tile({ name, preview, busy }) {
|
|
831
|
+
const [failed, setFailed] = useState(false);
|
|
832
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative size-24 overflow-hidden rounded-md border bg-muted sm:size-28", children: [
|
|
833
|
+
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: [
|
|
834
|
+
/* @__PURE__ */ jsx(ImageIcon, { className: "size-6" }),
|
|
835
|
+
/* @__PURE__ */ jsx("span", { className: "line-clamp-2 break-all", children: name })
|
|
836
|
+
] }),
|
|
837
|
+
busy ? /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ jsx(Spinner, { size: "sm", "aria-hidden": "true" }) }) : null
|
|
838
|
+
] });
|
|
839
|
+
}
|
|
840
|
+
var nextKey = 0;
|
|
841
|
+
function PhotoField(props) {
|
|
842
|
+
const { binding, inputId, engine } = props;
|
|
843
|
+
const { field } = binding;
|
|
844
|
+
const host = useUpload();
|
|
845
|
+
const pending = useContext(PendingUploadsContext);
|
|
846
|
+
const locked = useFormSelector(engine, (s) => s.status === "completed");
|
|
847
|
+
const limits = photoLimits(field);
|
|
848
|
+
const refs = photoRefs(binding.value);
|
|
849
|
+
const [uploading, setUploading] = useState([]);
|
|
850
|
+
const [problem, setProblem] = useState(null);
|
|
851
|
+
const [coarse, setCoarse] = useState(false);
|
|
852
|
+
const previews = useRef(/* @__PURE__ */ new Map());
|
|
853
|
+
const releases = useRef(/* @__PURE__ */ new Set());
|
|
854
|
+
const mounted = useRef(true);
|
|
855
|
+
const chooseRef = useRef(null);
|
|
856
|
+
const takeRef = useRef(null);
|
|
857
|
+
useEffect(() => {
|
|
858
|
+
setCoarse(typeof window !== "undefined" && !!window.matchMedia?.("(pointer: coarse)").matches);
|
|
859
|
+
}, []);
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
const urls = previews.current;
|
|
862
|
+
const pendingReleases = releases.current;
|
|
863
|
+
mounted.current = true;
|
|
864
|
+
return () => {
|
|
865
|
+
mounted.current = false;
|
|
866
|
+
for (const url of urls.values()) URL.revokeObjectURL(url);
|
|
867
|
+
urls.clear();
|
|
868
|
+
for (const release of pendingReleases) release();
|
|
869
|
+
pendingReleases.clear();
|
|
870
|
+
};
|
|
871
|
+
}, []);
|
|
872
|
+
const currentRefs = () => photoRefs(engine.getState().values[field.id]);
|
|
873
|
+
const full = refs.length + uploading.length >= limits.maxFiles;
|
|
874
|
+
const disabled = !host || locked;
|
|
875
|
+
const add = (files) => {
|
|
876
|
+
if (!host) return;
|
|
877
|
+
setProblem(null);
|
|
878
|
+
let room = limits.maxFiles - currentRefs().length - uploading.length;
|
|
879
|
+
for (const file of files) {
|
|
880
|
+
if (room <= 0) {
|
|
881
|
+
setProblem(photoCountMessage(field));
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
const why = photoFileProblem(field, file);
|
|
885
|
+
if (why) {
|
|
886
|
+
setProblem(why);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
room--;
|
|
890
|
+
const key = `u${nextKey++}`;
|
|
891
|
+
const preview = typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : null;
|
|
892
|
+
setUploading((list) => [...list, { key, name: file.name, preview }]);
|
|
893
|
+
const release = pending?.start() ?? (() => {
|
|
894
|
+
});
|
|
895
|
+
releases.current.add(release);
|
|
896
|
+
const finish = () => {
|
|
897
|
+
release();
|
|
898
|
+
releases.current.delete(release);
|
|
899
|
+
setUploading((list) => list.filter((u) => u.key !== key));
|
|
900
|
+
};
|
|
901
|
+
Promise.resolve().then(() => host.upload(file, field)).then((ref) => {
|
|
902
|
+
if (!isPhotoRef(ref)) throw new Error("The upload returned no photo reference.");
|
|
903
|
+
if (preview) {
|
|
904
|
+
const old = previews.current.get(ref.id);
|
|
905
|
+
if (old && old !== preview) URL.revokeObjectURL(old);
|
|
906
|
+
if (mounted.current) previews.current.set(ref.id, preview);
|
|
907
|
+
else URL.revokeObjectURL(preview);
|
|
908
|
+
}
|
|
909
|
+
const next = [...currentRefs().filter((r) => r.id !== ref.id), { id: ref.id, name: ref.name, type: ref.type, size: ref.size }];
|
|
910
|
+
binding.setValue(next);
|
|
911
|
+
finish();
|
|
912
|
+
}).catch(() => {
|
|
913
|
+
if (preview) URL.revokeObjectURL(preview);
|
|
914
|
+
setProblem(`\u201C${file.name}\u201D didn't upload. Try again.`);
|
|
915
|
+
finish();
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
const onPick = (e) => {
|
|
920
|
+
const files = Array.from(e.target.files ?? []);
|
|
921
|
+
e.target.value = "";
|
|
922
|
+
add(files);
|
|
923
|
+
};
|
|
924
|
+
const remove = (ref) => {
|
|
925
|
+
const url = previews.current.get(ref.id);
|
|
926
|
+
if (url) URL.revokeObjectURL(url);
|
|
927
|
+
previews.current.delete(ref.id);
|
|
928
|
+
const next = currentRefs().filter((r) => r.id !== ref.id);
|
|
929
|
+
binding.setValue(next.length ? next : void 0);
|
|
930
|
+
setProblem(null);
|
|
931
|
+
};
|
|
932
|
+
const accept = photoAccept(field);
|
|
933
|
+
const errorId = `${inputId}-upload-error`;
|
|
934
|
+
const hasAny = refs.length > 0 || uploading.length > 0;
|
|
935
|
+
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
936
|
+
hasAny ? /* @__PURE__ */ jsxs("ul", { className: "flex flex-wrap gap-3", "aria-label": "Photos", children: [
|
|
937
|
+
refs.map((ref) => /* @__PURE__ */ jsxs("li", { className: "relative", children: [
|
|
938
|
+
/* @__PURE__ */ jsx(Tile, { name: ref.name, preview: previews.current.get(ref.id) ?? null }),
|
|
939
|
+
locked ? null : /* @__PURE__ */ jsx(
|
|
940
|
+
Button,
|
|
941
|
+
{
|
|
942
|
+
type: "button",
|
|
943
|
+
variant: "secondary",
|
|
944
|
+
size: "icon",
|
|
945
|
+
"aria-label": `Remove ${ref.name}`,
|
|
946
|
+
onClick: () => remove(ref),
|
|
947
|
+
className: "absolute right-1 top-1 size-7 rounded-full shadow-sm",
|
|
948
|
+
children: /* @__PURE__ */ jsx(XIcon2, {})
|
|
949
|
+
}
|
|
950
|
+
)
|
|
951
|
+
] }, ref.id)),
|
|
952
|
+
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))
|
|
953
|
+
] }) : null,
|
|
954
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
955
|
+
/* @__PURE__ */ jsxs(
|
|
956
|
+
Button,
|
|
957
|
+
{
|
|
958
|
+
type: "button",
|
|
959
|
+
variant: "outline",
|
|
960
|
+
disabled: disabled || full,
|
|
961
|
+
"aria-invalid": binding.error ? true : void 0,
|
|
962
|
+
onClick: () => chooseRef.current?.click(),
|
|
963
|
+
children: [
|
|
964
|
+
/* @__PURE__ */ jsx(ImageIcon, {}),
|
|
965
|
+
" ",
|
|
966
|
+
refs.length ? "Add another photo" : "Choose a photo"
|
|
967
|
+
]
|
|
968
|
+
}
|
|
969
|
+
),
|
|
970
|
+
coarse ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "outline", disabled: disabled || full, onClick: () => takeRef.current?.click(), children: [
|
|
971
|
+
/* @__PURE__ */ jsx(CameraIcon, {}),
|
|
972
|
+
" Take a photo"
|
|
973
|
+
] }) : null
|
|
974
|
+
] }),
|
|
975
|
+
/* @__PURE__ */ jsx(
|
|
976
|
+
Input,
|
|
977
|
+
{
|
|
978
|
+
ref: chooseRef,
|
|
979
|
+
type: "file",
|
|
980
|
+
accept,
|
|
981
|
+
multiple: limits.maxFiles > 1,
|
|
982
|
+
className: "hidden",
|
|
983
|
+
tabIndex: -1,
|
|
984
|
+
"aria-hidden": "true",
|
|
985
|
+
disabled,
|
|
986
|
+
onChange: onPick
|
|
987
|
+
}
|
|
988
|
+
),
|
|
989
|
+
/* @__PURE__ */ jsx(
|
|
990
|
+
Input,
|
|
991
|
+
{
|
|
992
|
+
ref: takeRef,
|
|
993
|
+
type: "file",
|
|
994
|
+
accept,
|
|
995
|
+
capture: "environment",
|
|
996
|
+
className: "hidden",
|
|
997
|
+
tabIndex: -1,
|
|
998
|
+
"aria-hidden": "true",
|
|
999
|
+
disabled,
|
|
1000
|
+
onChange: onPick
|
|
1001
|
+
}
|
|
1002
|
+
),
|
|
1003
|
+
!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.` }),
|
|
1004
|
+
problem ? /* @__PURE__ */ jsx(FormError, { id: errorId, role: "alert", children: problem }) : null
|
|
1005
|
+
] }) });
|
|
1006
|
+
}
|
|
1007
|
+
var photoRenderers = [{ type: "photo", Component: PhotoField }];
|
|
1008
|
+
function OptionImage({ src, large }) {
|
|
1009
|
+
return /* @__PURE__ */ jsx(
|
|
1010
|
+
"img",
|
|
1011
|
+
{
|
|
1012
|
+
src,
|
|
1013
|
+
alt: "",
|
|
1014
|
+
loading: "lazy",
|
|
1015
|
+
"data-gf-option-image": "",
|
|
1016
|
+
className: cn("shrink-0 rounded-md bg-muted object-cover", large ? "size-12" : "size-10")
|
|
1017
|
+
}
|
|
1018
|
+
);
|
|
1019
|
+
}
|
|
1020
|
+
function OptionText({ label, description }) {
|
|
1021
|
+
return /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [
|
|
1022
|
+
/* @__PURE__ */ jsx("span", { children: label }),
|
|
1023
|
+
description ? /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", "data-gf-option-description": "", children: description }) : null
|
|
1024
|
+
] });
|
|
1025
|
+
}
|
|
1026
|
+
function CheckMark({ on }) {
|
|
1027
|
+
return /* @__PURE__ */ jsx(
|
|
1028
|
+
"svg",
|
|
1029
|
+
{
|
|
1030
|
+
viewBox: "0 0 24 24",
|
|
1031
|
+
fill: "none",
|
|
1032
|
+
stroke: "currentColor",
|
|
1033
|
+
strokeWidth: "2.5",
|
|
1034
|
+
strokeLinecap: "round",
|
|
1035
|
+
strokeLinejoin: "round",
|
|
1036
|
+
"aria-hidden": "true",
|
|
1037
|
+
className: cn("size-4 shrink-0 text-primary", on ? "opacity-100" : "opacity-0"),
|
|
1038
|
+
children: /* @__PURE__ */ jsx("path", { d: "M20 6 9 17l-5-5" })
|
|
1039
|
+
}
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
var SEARCH_OTHER_VALUE = "\0other";
|
|
1043
|
+
function SearchableOptions({
|
|
1044
|
+
props,
|
|
1045
|
+
options,
|
|
1046
|
+
selected,
|
|
1047
|
+
multiple,
|
|
1048
|
+
otherLabel,
|
|
1049
|
+
otherOn,
|
|
1050
|
+
onPick
|
|
1051
|
+
}) {
|
|
1052
|
+
const { binding, inputId } = props;
|
|
1053
|
+
const [query, setQuery] = useState("");
|
|
1054
|
+
const { presentation } = useFormPresentation();
|
|
1055
|
+
const page = presentation === "page";
|
|
1056
|
+
const shown = searchOptions(options, query);
|
|
1057
|
+
const chosen = options.filter((o) => selected.includes(o.value));
|
|
1058
|
+
const labelOf = (o) => o.label ?? o.value;
|
|
1059
|
+
return /* @__PURE__ */ jsxs(
|
|
1060
|
+
"div",
|
|
1061
|
+
{
|
|
1062
|
+
className: "space-y-2",
|
|
1063
|
+
"data-gf-searchable": "",
|
|
1064
|
+
onKeyDown: (e) => {
|
|
1065
|
+
if (e.key === "Enter" && e.target.tagName === "INPUT") e.preventDefault();
|
|
1066
|
+
},
|
|
1067
|
+
children: [
|
|
1068
|
+
/* @__PURE__ */ jsxs(Command, { shouldFilter: false, label: binding.label, className: "rounded-md border border-input bg-background", children: [
|
|
1069
|
+
/* @__PURE__ */ jsx(
|
|
1070
|
+
CommandInput,
|
|
1071
|
+
{
|
|
1072
|
+
id: inputId,
|
|
1073
|
+
"data-gf-focus": "",
|
|
1074
|
+
value: query,
|
|
1075
|
+
onValueChange: setQuery,
|
|
1076
|
+
placeholder: "Type to search",
|
|
1077
|
+
"aria-label": `Search: ${binding.label}`,
|
|
1078
|
+
"aria-invalid": binding.error ? true : void 0,
|
|
1079
|
+
"aria-required": binding.field.required || void 0,
|
|
1080
|
+
className: cn(page && "text-base")
|
|
1081
|
+
}
|
|
1082
|
+
),
|
|
1083
|
+
/* @__PURE__ */ jsxs(CommandList, { className: "max-h-72", "aria-multiselectable": multiple || void 0, children: [
|
|
1084
|
+
/* @__PURE__ */ jsx(CommandEmpty, { children: "No matches." }),
|
|
1085
|
+
shown.map((o) => {
|
|
1086
|
+
const on = selected.includes(o.value);
|
|
1087
|
+
return /* @__PURE__ */ jsxs(
|
|
1088
|
+
CommandItem,
|
|
1089
|
+
{
|
|
1090
|
+
value: o.value,
|
|
1091
|
+
keywords: [labelOf(o)],
|
|
1092
|
+
"aria-checked": on,
|
|
1093
|
+
onSelect: () => onPick(o.value),
|
|
1094
|
+
className: cn("min-h-11 gap-3 text-base", page && "min-h-12"),
|
|
1095
|
+
"data-gf-option": o.value,
|
|
1096
|
+
children: [
|
|
1097
|
+
/* @__PURE__ */ jsx(CheckMark, { on }),
|
|
1098
|
+
o.image ? /* @__PURE__ */ jsx(OptionImage, { src: o.image }) : null,
|
|
1099
|
+
/* @__PURE__ */ jsx(OptionText, { label: labelOf(o), description: o.description })
|
|
1100
|
+
]
|
|
1101
|
+
},
|
|
1102
|
+
o.value
|
|
1103
|
+
);
|
|
1104
|
+
}),
|
|
1105
|
+
otherLabel ? /* @__PURE__ */ jsxs(
|
|
1106
|
+
CommandItem,
|
|
1107
|
+
{
|
|
1108
|
+
value: SEARCH_OTHER_VALUE,
|
|
1109
|
+
"aria-checked": !!otherOn,
|
|
1110
|
+
onSelect: () => onPick(SEARCH_OTHER_VALUE),
|
|
1111
|
+
className: cn("min-h-11 gap-3 text-base", page && "min-h-12"),
|
|
1112
|
+
children: [
|
|
1113
|
+
/* @__PURE__ */ jsx(CheckMark, { on: !!otherOn }),
|
|
1114
|
+
/* @__PURE__ */ jsx(OptionText, { label: otherLabel })
|
|
1115
|
+
]
|
|
1116
|
+
}
|
|
1117
|
+
) : null
|
|
1118
|
+
] })
|
|
1119
|
+
] }),
|
|
1120
|
+
chosen.length ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground", "aria-live": "polite", "data-gf-chosen": "", children: [
|
|
1121
|
+
multiple ? "Chosen" : "Your choice",
|
|
1122
|
+
": ",
|
|
1123
|
+
chosen.map(labelOf).join(", ")
|
|
1124
|
+
] }) : null
|
|
1125
|
+
]
|
|
1126
|
+
}
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
775
1129
|
function groupAria(props) {
|
|
776
1130
|
return {
|
|
777
1131
|
"aria-labelledby": `${props.inputId}-label`,
|
|
@@ -1035,6 +1389,7 @@ function useChoiceLook(inputId, count) {
|
|
|
1035
1389
|
control: pageFocus ? pageChoiceKey : void 0,
|
|
1036
1390
|
onKeyDown: pageFocus ? (e) => {
|
|
1037
1391
|
if (e.altKey || e.ctrlKey || e.metaKey || e.key.length !== 1) return;
|
|
1392
|
+
if (e.target.tagName === "INPUT") return;
|
|
1038
1393
|
const index = KEYS.indexOf(e.key.toUpperCase());
|
|
1039
1394
|
if (index < 0 || index >= count) return;
|
|
1040
1395
|
const option = document.getElementById(`${inputId}-${index}`);
|
|
@@ -1045,8 +1400,19 @@ function useChoiceLook(inputId, count) {
|
|
|
1045
1400
|
} : void 0
|
|
1046
1401
|
};
|
|
1047
1402
|
}
|
|
1048
|
-
function ChoiceContent({
|
|
1049
|
-
|
|
1403
|
+
function ChoiceContent({
|
|
1404
|
+
look,
|
|
1405
|
+
index,
|
|
1406
|
+
label,
|
|
1407
|
+
description,
|
|
1408
|
+
image
|
|
1409
|
+
}) {
|
|
1410
|
+
if (!look.pageFocus) {
|
|
1411
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1412
|
+
image ? /* @__PURE__ */ jsx(OptionImage, { src: image }) : null,
|
|
1413
|
+
/* @__PURE__ */ jsx(OptionText, { label, description })
|
|
1414
|
+
] });
|
|
1415
|
+
}
|
|
1050
1416
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1051
1417
|
/* @__PURE__ */ jsx(
|
|
1052
1418
|
"span",
|
|
@@ -1056,7 +1422,8 @@ function ChoiceContent({ look, index, label }) {
|
|
|
1056
1422
|
children: KEYS[index] ?? ""
|
|
1057
1423
|
}
|
|
1058
1424
|
),
|
|
1059
|
-
/* @__PURE__ */ jsx(
|
|
1425
|
+
image ? /* @__PURE__ */ jsx(OptionImage, { src: image, large: true }) : null,
|
|
1426
|
+
/* @__PURE__ */ jsx(OptionText, { label, description }),
|
|
1060
1427
|
/* @__PURE__ */ jsx(
|
|
1061
1428
|
"svg",
|
|
1062
1429
|
{
|
|
@@ -1073,44 +1440,223 @@ function ChoiceContent({ look, index, label }) {
|
|
|
1073
1440
|
)
|
|
1074
1441
|
] });
|
|
1075
1442
|
}
|
|
1443
|
+
var OTHER_VALUE = "\0other";
|
|
1444
|
+
function OtherText({
|
|
1445
|
+
id,
|
|
1446
|
+
label,
|
|
1447
|
+
value,
|
|
1448
|
+
onChange,
|
|
1449
|
+
invalid: invalid2
|
|
1450
|
+
}) {
|
|
1451
|
+
const line = useLineClass();
|
|
1452
|
+
return /* @__PURE__ */ jsx(
|
|
1453
|
+
Input,
|
|
1454
|
+
{
|
|
1455
|
+
id,
|
|
1456
|
+
value,
|
|
1457
|
+
maxLength: OTHER_MAX_LENGTH,
|
|
1458
|
+
"aria-label": `${label}: your answer`,
|
|
1459
|
+
"aria-invalid": invalid2 || void 0,
|
|
1460
|
+
placeholder: "Type your answer",
|
|
1461
|
+
onChange: (e) => onChange(e.target.value),
|
|
1462
|
+
className: cn("mt-2", line)
|
|
1463
|
+
}
|
|
1464
|
+
);
|
|
1465
|
+
}
|
|
1466
|
+
var focusSoon = (id) => requestAnimationFrame(() => document.getElementById(id)?.focus());
|
|
1467
|
+
function useOtherText(typed) {
|
|
1468
|
+
const [on, setOn] = useState(typed !== "");
|
|
1469
|
+
const [text, setText] = useState(typed);
|
|
1470
|
+
return { on, setOn, text, setText };
|
|
1471
|
+
}
|
|
1472
|
+
function SearchableSingleField(props) {
|
|
1473
|
+
const { binding, onAdvance } = props;
|
|
1474
|
+
const options = useChoiceOptions(props.engine, binding.field.id);
|
|
1475
|
+
const other = otherOption(binding.field);
|
|
1476
|
+
const current = typeof binding.value === "string" ? binding.value : "";
|
|
1477
|
+
const typed = other && current && !choiceOptions(binding.field).some((o) => o.value === current) ? current : "";
|
|
1478
|
+
const otherText = useOtherText(typed);
|
|
1479
|
+
const textId = `${props.inputId}-other-text`;
|
|
1480
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1481
|
+
/* @__PURE__ */ jsx(
|
|
1482
|
+
SearchableOptions,
|
|
1483
|
+
{
|
|
1484
|
+
props,
|
|
1485
|
+
options,
|
|
1486
|
+
selected: otherText.on ? [] : [current],
|
|
1487
|
+
multiple: false,
|
|
1488
|
+
otherLabel: other?.label,
|
|
1489
|
+
otherOn: otherText.on,
|
|
1490
|
+
onPick: (v) => {
|
|
1491
|
+
if (v === SEARCH_OTHER_VALUE) {
|
|
1492
|
+
otherText.setOn(true);
|
|
1493
|
+
binding.setValue(otherText.text.trim() ? otherText.text : void 0);
|
|
1494
|
+
focusSoon(textId);
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
otherText.setOn(false);
|
|
1498
|
+
binding.setValue(v);
|
|
1499
|
+
onAdvance?.();
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
),
|
|
1503
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1504
|
+
OtherText,
|
|
1505
|
+
{
|
|
1506
|
+
id: textId,
|
|
1507
|
+
label: other.label,
|
|
1508
|
+
value: otherText.text,
|
|
1509
|
+
invalid: !!binding.error,
|
|
1510
|
+
onChange: (text) => {
|
|
1511
|
+
otherText.setText(text);
|
|
1512
|
+
binding.setValue(text.trim() ? text : void 0);
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
) : null
|
|
1516
|
+
] });
|
|
1517
|
+
}
|
|
1518
|
+
function SearchableMultiField(props) {
|
|
1519
|
+
const { binding } = props;
|
|
1520
|
+
const options = useChoiceOptions(props.engine, binding.field.id);
|
|
1521
|
+
const other = otherOption(binding.field);
|
|
1522
|
+
const selected = Array.isArray(binding.value) ? binding.value : [];
|
|
1523
|
+
const known = new Set(choiceOptions(binding.field).map((o) => o.value));
|
|
1524
|
+
const chosen = selected.filter((v) => known.has(v));
|
|
1525
|
+
const otherText = useOtherText(other ? selected.find((v) => !known.has(v)) ?? "" : "");
|
|
1526
|
+
const textId = `${props.inputId}-other-text`;
|
|
1527
|
+
const commit = (picked, otherOn, text) => {
|
|
1528
|
+
const next = otherOn && text.trim() ? [...picked, text] : picked;
|
|
1529
|
+
binding.setValue(next.length ? next : void 0);
|
|
1530
|
+
};
|
|
1531
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1532
|
+
/* @__PURE__ */ jsx(
|
|
1533
|
+
SearchableOptions,
|
|
1534
|
+
{
|
|
1535
|
+
props,
|
|
1536
|
+
options,
|
|
1537
|
+
selected: chosen,
|
|
1538
|
+
multiple: true,
|
|
1539
|
+
otherLabel: other?.label,
|
|
1540
|
+
otherOn: otherText.on,
|
|
1541
|
+
onPick: (v) => {
|
|
1542
|
+
if (v === SEARCH_OTHER_VALUE) {
|
|
1543
|
+
const on = !otherText.on;
|
|
1544
|
+
otherText.setOn(on);
|
|
1545
|
+
commit(chosen, on, otherText.text);
|
|
1546
|
+
if (on) focusSoon(textId);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
commit(chosen.includes(v) ? chosen.filter((x) => x !== v) : [...chosen, v], otherText.on, otherText.text);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
),
|
|
1553
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1554
|
+
OtherText,
|
|
1555
|
+
{
|
|
1556
|
+
id: textId,
|
|
1557
|
+
label: other.label,
|
|
1558
|
+
value: otherText.text,
|
|
1559
|
+
invalid: !!binding.error,
|
|
1560
|
+
onChange: (text) => {
|
|
1561
|
+
otherText.setText(text);
|
|
1562
|
+
commit(chosen, true, text);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
) : null
|
|
1566
|
+
] });
|
|
1567
|
+
}
|
|
1568
|
+
function useSearchable(props) {
|
|
1569
|
+
return isSearchable(props.binding.field);
|
|
1570
|
+
}
|
|
1076
1571
|
function SingleChoiceField(props) {
|
|
1572
|
+
return useSearchable(props) ? /* @__PURE__ */ jsx(SearchableSingleField, { ...props }) : /* @__PURE__ */ jsx(SingleChoiceList, { ...props });
|
|
1573
|
+
}
|
|
1574
|
+
function SingleChoiceList(props) {
|
|
1077
1575
|
const { binding, inputId, onAdvance } = props;
|
|
1078
1576
|
const pick = useDeliberatePick(onAdvance);
|
|
1079
|
-
const options =
|
|
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))
|
|
1577
|
+
const options = useChoiceOptions(props.engine, binding.field.id);
|
|
1578
|
+
const other = otherOption(binding.field);
|
|
1579
|
+
const current = typeof binding.value === "string" ? binding.value : "";
|
|
1580
|
+
const typed = other && current && !choiceOptions(binding.field).some((o) => o.value === current) ? current : "";
|
|
1581
|
+
const otherText = useOtherText(typed);
|
|
1582
|
+
const look = useChoiceLook(inputId, options.length + (other ? 1 : 0));
|
|
1583
|
+
const textId = `${inputId}-other-text`;
|
|
1584
|
+
const choose = (v) => {
|
|
1585
|
+
if (v !== OTHER_VALUE) {
|
|
1586
|
+
otherText.setOn(false);
|
|
1587
|
+
binding.setValue(v);
|
|
1588
|
+
return;
|
|
1102
1589
|
}
|
|
1103
|
-
|
|
1590
|
+
otherText.setOn(true);
|
|
1591
|
+
binding.setValue(otherText.text.trim() ? otherText.text : void 0);
|
|
1592
|
+
focusSoon(textId);
|
|
1593
|
+
};
|
|
1594
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1595
|
+
/* @__PURE__ */ jsxs(
|
|
1596
|
+
RadioGroup,
|
|
1597
|
+
{
|
|
1598
|
+
...groupAria(props),
|
|
1599
|
+
...pick.rootProps,
|
|
1600
|
+
onKeyDown: look.onKeyDown,
|
|
1601
|
+
className: look.pageFocus ? "gap-2.5" : void 0,
|
|
1602
|
+
value: otherText.on ? OTHER_VALUE : current,
|
|
1603
|
+
onValueChange: choose,
|
|
1604
|
+
children: [
|
|
1605
|
+
options.map((o, i) => /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${i}`, className: look.row, children: [
|
|
1606
|
+
/* @__PURE__ */ jsx(
|
|
1607
|
+
RadioGroupItem,
|
|
1608
|
+
{
|
|
1609
|
+
id: `${inputId}-${i}`,
|
|
1610
|
+
value: o.value,
|
|
1611
|
+
onClick: pick.onItemClick,
|
|
1612
|
+
className: look.control
|
|
1613
|
+
}
|
|
1614
|
+
),
|
|
1615
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: i, label: o.label ?? o.value, description: o.description, image: o.image })
|
|
1616
|
+
] }, o.value)),
|
|
1617
|
+
other ? (
|
|
1618
|
+
// No auto-advance: the respondent still has to type.
|
|
1619
|
+
/* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${options.length}`, className: look.row, children: [
|
|
1620
|
+
/* @__PURE__ */ jsx(RadioGroupItem, { id: `${inputId}-${options.length}`, value: OTHER_VALUE, className: look.control }),
|
|
1621
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: options.length, label: other.label })
|
|
1622
|
+
] })
|
|
1623
|
+
) : null
|
|
1624
|
+
]
|
|
1625
|
+
}
|
|
1626
|
+
),
|
|
1627
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1628
|
+
OtherText,
|
|
1629
|
+
{
|
|
1630
|
+
id: textId,
|
|
1631
|
+
label: other.label,
|
|
1632
|
+
value: otherText.text,
|
|
1633
|
+
invalid: !!binding.error,
|
|
1634
|
+
onChange: (text) => {
|
|
1635
|
+
otherText.setText(text);
|
|
1636
|
+
binding.setValue(text.trim() ? text : void 0);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
) : null
|
|
1640
|
+
] });
|
|
1104
1641
|
}
|
|
1105
1642
|
function MultiChoiceField(props) {
|
|
1643
|
+
return useSearchable(props) ? /* @__PURE__ */ jsx(SearchableMultiField, { ...props }) : /* @__PURE__ */ jsx(MultiChoiceList, { ...props });
|
|
1644
|
+
}
|
|
1645
|
+
function MultiChoiceList(props) {
|
|
1106
1646
|
const { binding, inputId } = props;
|
|
1107
1647
|
const selected = Array.isArray(binding.value) ? binding.value : [];
|
|
1108
|
-
const options =
|
|
1109
|
-
const
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1648
|
+
const options = useChoiceOptions(props.engine, binding.field.id);
|
|
1649
|
+
const other = otherOption(binding.field);
|
|
1650
|
+
const known = new Set(choiceOptions(binding.field).map((o) => o.value));
|
|
1651
|
+
const chosen = selected.filter((v) => known.has(v));
|
|
1652
|
+
const otherText = useOtherText(other ? selected.find((v) => !known.has(v)) ?? "" : "");
|
|
1653
|
+
const look = useChoiceLook(inputId, options.length + (other ? 1 : 0));
|
|
1654
|
+
const textId = `${inputId}-other-text`;
|
|
1655
|
+
const commit = (picked, otherOn, text) => {
|
|
1656
|
+
const next = otherOn && text.trim() ? [...picked, text] : picked;
|
|
1112
1657
|
binding.setValue(next.length ? next : void 0);
|
|
1113
1658
|
};
|
|
1659
|
+
const toggle = (value, on) => commit(on ? [...chosen, value] : chosen.filter((v) => v !== value), otherText.on, otherText.text);
|
|
1114
1660
|
return /* @__PURE__ */ jsx(FieldChrome, { props, children: /* @__PURE__ */ jsxs("div", { className: cn("grid gap-2", look.pageFocus && "gap-2.5"), onKeyDown: look.onKeyDown, children: [
|
|
1115
1661
|
look.pageFocus ? /* @__PURE__ */ jsx("p", { className: "-mt-2 mb-1 text-sm text-muted-foreground", children: "Choose as many as you like" }) : null,
|
|
1116
1662
|
options.map((o, i) => /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${i}`, className: look.row, children: [
|
|
@@ -1124,8 +1670,38 @@ function MultiChoiceField(props) {
|
|
|
1124
1670
|
className: look.control
|
|
1125
1671
|
}
|
|
1126
1672
|
),
|
|
1127
|
-
/* @__PURE__ */ jsx(ChoiceContent, { look, index: i, label: o.label ?? o.value })
|
|
1128
|
-
] }, o.value))
|
|
1673
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: i, label: o.label ?? o.value, description: o.description, image: o.image })
|
|
1674
|
+
] }, o.value)),
|
|
1675
|
+
other ? /* @__PURE__ */ jsxs(Label, { htmlFor: `${inputId}-${options.length}`, className: look.row, children: [
|
|
1676
|
+
/* @__PURE__ */ jsx(
|
|
1677
|
+
Checkbox,
|
|
1678
|
+
{
|
|
1679
|
+
id: `${inputId}-${options.length}`,
|
|
1680
|
+
checked: otherText.on,
|
|
1681
|
+
"aria-invalid": binding.error ? true : void 0,
|
|
1682
|
+
onCheckedChange: (c) => {
|
|
1683
|
+
otherText.setOn(c === true);
|
|
1684
|
+
commit(chosen, c === true, otherText.text);
|
|
1685
|
+
if (c === true) focusSoon(textId);
|
|
1686
|
+
},
|
|
1687
|
+
className: look.control
|
|
1688
|
+
}
|
|
1689
|
+
),
|
|
1690
|
+
/* @__PURE__ */ jsx(ChoiceContent, { look, index: options.length, label: other.label })
|
|
1691
|
+
] }) : null,
|
|
1692
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1693
|
+
OtherText,
|
|
1694
|
+
{
|
|
1695
|
+
id: textId,
|
|
1696
|
+
label: other.label,
|
|
1697
|
+
value: otherText.text,
|
|
1698
|
+
invalid: !!binding.error,
|
|
1699
|
+
onChange: (text) => {
|
|
1700
|
+
otherText.setText(text);
|
|
1701
|
+
commit(chosen, true, text);
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
) : null
|
|
1129
1705
|
] }) });
|
|
1130
1706
|
}
|
|
1131
1707
|
function useLineClass() {
|
|
@@ -1145,27 +1721,58 @@ function controlAria(props) {
|
|
|
1145
1721
|
};
|
|
1146
1722
|
}
|
|
1147
1723
|
function DropdownField(props) {
|
|
1724
|
+
return useSearchable(props) ? /* @__PURE__ */ jsx(SearchableSingleField, { ...props }) : /* @__PURE__ */ jsx(DropdownSelect, { ...props });
|
|
1725
|
+
}
|
|
1726
|
+
function DropdownSelect(props) {
|
|
1148
1727
|
const { binding } = props;
|
|
1149
|
-
const options =
|
|
1728
|
+
const options = useChoiceOptions(props.engine, binding.field.id);
|
|
1729
|
+
const other = otherOption(binding.field);
|
|
1150
1730
|
const line = useLineClass();
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1731
|
+
const current = typeof binding.value === "string" ? binding.value : "";
|
|
1732
|
+
const typed = other && current && !choiceOptions(binding.field).some((o) => o.value === current) ? current : "";
|
|
1733
|
+
const otherText = useOtherText(typed);
|
|
1734
|
+
const value = otherText.on ? OTHER_VALUE : current;
|
|
1735
|
+
const textId = `${props.inputId}-other-text`;
|
|
1736
|
+
return /* @__PURE__ */ jsxs(FieldChrome, { props, children: [
|
|
1737
|
+
/* @__PURE__ */ jsxs(
|
|
1738
|
+
"select",
|
|
1739
|
+
{
|
|
1740
|
+
...controlAria(props),
|
|
1741
|
+
value,
|
|
1742
|
+
onChange: (e) => {
|
|
1743
|
+
const v = e.target.value;
|
|
1744
|
+
otherText.setOn(v === OTHER_VALUE);
|
|
1745
|
+
if (v === OTHER_VALUE) {
|
|
1746
|
+
binding.setValue(otherText.text.trim() ? otherText.text : void 0);
|
|
1747
|
+
focusSoon(textId);
|
|
1748
|
+
} else binding.setValue(v === "" ? void 0 : v);
|
|
1749
|
+
},
|
|
1750
|
+
className: cn(
|
|
1751
|
+
"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",
|
|
1752
|
+
value === "" && "text-muted-foreground",
|
|
1753
|
+
line
|
|
1754
|
+
),
|
|
1755
|
+
children: [
|
|
1756
|
+
/* @__PURE__ */ jsx("option", { value: "", children: binding.field.placeholder ?? "Choose\u2026" }),
|
|
1757
|
+
options.map((o) => /* @__PURE__ */ jsx("option", { value: o.value, children: o.label ?? o.value }, o.value)),
|
|
1758
|
+
other ? /* @__PURE__ */ jsx("option", { value: OTHER_VALUE, children: other.label }) : null
|
|
1759
|
+
]
|
|
1760
|
+
}
|
|
1761
|
+
),
|
|
1762
|
+
other && otherText.on ? /* @__PURE__ */ jsx(
|
|
1763
|
+
OtherText,
|
|
1764
|
+
{
|
|
1765
|
+
id: textId,
|
|
1766
|
+
label: other.label,
|
|
1767
|
+
value: otherText.text,
|
|
1768
|
+
invalid: !!binding.error,
|
|
1769
|
+
onChange: (text) => {
|
|
1770
|
+
otherText.setText(text);
|
|
1771
|
+
binding.setValue(text.trim() ? text : void 0);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
) : null
|
|
1775
|
+
] });
|
|
1169
1776
|
}
|
|
1170
1777
|
function NumberField(props) {
|
|
1171
1778
|
const { binding } = props;
|
|
@@ -1254,7 +1861,8 @@ var builtInRenderers = [
|
|
|
1254
1861
|
{ type: "date", Component: DateField },
|
|
1255
1862
|
{ type: "consent", Component: ConsentField },
|
|
1256
1863
|
...structuredRenderers,
|
|
1257
|
-
...calculatedRenderers
|
|
1864
|
+
...calculatedRenderers,
|
|
1865
|
+
...photoRenderers
|
|
1258
1866
|
];
|
|
1259
1867
|
var defaultRegistry = createRendererRegistry(builtInRenderers);
|
|
1260
1868
|
|
|
@@ -1533,8 +2141,9 @@ function DefaultNavigation({
|
|
|
1533
2141
|
/* @__PURE__ */ jsx(Button, { type: "submit", size: "lg", loading: busy, className: "motion-reduce:[&_.animate-spin]:animate-none", children: label })
|
|
1534
2142
|
] });
|
|
1535
2143
|
}
|
|
1536
|
-
function DefaultEnding({ title, description, className }) {
|
|
2144
|
+
function DefaultEnding({ title, description, actions, className }) {
|
|
1537
2145
|
const { presentation } = useFormPresentation();
|
|
2146
|
+
const actionRow = actions ? /* @__PURE__ */ jsx("div", { className: "mt-4 flex flex-wrap justify-center gap-2", children: actions }) : null;
|
|
1538
2147
|
if (presentation === "page") {
|
|
1539
2148
|
return /* @__PURE__ */ jsxs("div", { className: cn("flex flex-col items-center gap-4 text-center", className), children: [
|
|
1540
2149
|
/* @__PURE__ */ jsx(
|
|
@@ -1557,12 +2166,14 @@ function DefaultEnding({ title, description, className }) {
|
|
|
1557
2166
|
children: title
|
|
1558
2167
|
}
|
|
1559
2168
|
),
|
|
1560
|
-
description ? /* @__PURE__ */ jsx("p", { className: "max-w-md text-lg text-muted-foreground text-pretty", children: description }) : null
|
|
2169
|
+
description ? /* @__PURE__ */ jsx("p", { className: "max-w-md text-lg text-muted-foreground text-pretty", children: description }) : null,
|
|
2170
|
+
actionRow
|
|
1561
2171
|
] });
|
|
1562
2172
|
}
|
|
1563
2173
|
return /* @__PURE__ */ jsxs("div", { className: cn("space-y-2 py-8 text-center", className), children: [
|
|
1564
2174
|
/* @__PURE__ */ jsx("h2", { tabIndex: -1, "data-gf-focus": "", className: cn("rounded-sm text-2xl font-semibold", focusRing), children: title }),
|
|
1565
|
-
description ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: description }) : null
|
|
2175
|
+
description ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground", children: description }) : null,
|
|
2176
|
+
actionRow
|
|
1566
2177
|
] });
|
|
1567
2178
|
}
|
|
1568
2179
|
function focusErrorTarget(event) {
|
|
@@ -1598,6 +2209,8 @@ function DefaultErrorSummary({ count, errors, className }) {
|
|
|
1598
2209
|
);
|
|
1599
2210
|
}
|
|
1600
2211
|
var ADVANCE_DELAY_MS = 250;
|
|
2212
|
+
var PROGRESS_DELAY_MS = 400;
|
|
2213
|
+
var CLASSIC_PROGRESS_DELAY_MS = 1500;
|
|
1601
2214
|
var engineKeys = /* @__PURE__ */ new WeakMap();
|
|
1602
2215
|
var nextEngineKey = 0;
|
|
1603
2216
|
function keyOf(engine) {
|
|
@@ -1612,11 +2225,20 @@ function Form(props) {
|
|
|
1612
2225
|
return props.engine ? /* @__PURE__ */ jsx(FormWithEngine, { ...props, engine: props.engine }) : /* @__PURE__ */ jsx(OwnedForm, { ...props });
|
|
1613
2226
|
}
|
|
1614
2227
|
function OwnedForm(props) {
|
|
1615
|
-
const
|
|
2228
|
+
const options = props.hostOptions || props.linkParams ? {
|
|
2229
|
+
...props.options,
|
|
2230
|
+
...props.hostOptions ? { hostOptions: props.hostOptions } : {},
|
|
2231
|
+
...props.linkParams ? { linkParams: props.linkParams } : {}
|
|
2232
|
+
} : props.options;
|
|
2233
|
+
const engine = useForm(props.form, options);
|
|
1616
2234
|
return /* @__PURE__ */ jsx(FormWithEngine, { ...props, engine });
|
|
1617
2235
|
}
|
|
1618
2236
|
function FormWithEngine(props) {
|
|
1619
|
-
|
|
2237
|
+
const { onUpload } = props;
|
|
2238
|
+
const outer = useContext(UploadContext);
|
|
2239
|
+
const host = useMemo(() => onUpload ? { upload: onUpload } : outer, [onUpload, outer]);
|
|
2240
|
+
const [pending] = useState(createPendingUploads);
|
|
2241
|
+
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
2242
|
}
|
|
1621
2243
|
function focusWithin(root) {
|
|
1622
2244
|
if (!root) return;
|
|
@@ -1632,6 +2254,8 @@ function FormBody({
|
|
|
1632
2254
|
theme,
|
|
1633
2255
|
onSubmit,
|
|
1634
2256
|
onChange,
|
|
2257
|
+
onProgress,
|
|
2258
|
+
progressNotice = "Your answers save as you go.",
|
|
1635
2259
|
autoAdvance = true,
|
|
1636
2260
|
submitLabel = "Send",
|
|
1637
2261
|
requiredLabel = "Required",
|
|
@@ -1640,14 +2264,15 @@ function FormBody({
|
|
|
1640
2264
|
slots,
|
|
1641
2265
|
slotProps,
|
|
1642
2266
|
className,
|
|
1643
|
-
testID
|
|
2267
|
+
testID,
|
|
2268
|
+
pendingUploads
|
|
1644
2269
|
}) {
|
|
1645
2270
|
const { idPrefix } = useFormContext();
|
|
1646
2271
|
const def = engine.definition;
|
|
1647
2272
|
const layout = layoutProp ?? def.layout?.mode ?? "classic";
|
|
1648
2273
|
const status = useFormSelector(engine, (s) => s.status);
|
|
1649
2274
|
const current = useFormSelector(engine, (s) => s.current);
|
|
1650
|
-
const pathKey = useFormSelector(engine, (s) => JSON.stringify(s
|
|
2275
|
+
const pathKey = useFormSelector(engine, (s) => JSON.stringify(respondentPath(def, s)));
|
|
1651
2276
|
const errorsKey = useFormSelector(engine, (s) => JSON.stringify(s.errors));
|
|
1652
2277
|
const ending = useFormSelector(engine, (s) => s.ending);
|
|
1653
2278
|
const historyLength = useFormSelector(engine, (s) => s.history.length);
|
|
@@ -1675,6 +2300,52 @@ function FormBody({
|
|
|
1675
2300
|
if (!onChange) return;
|
|
1676
2301
|
return engine.subscribe(() => onChange(engine.getState(), engine));
|
|
1677
2302
|
}, [engine, onChange]);
|
|
2303
|
+
const saveProgress = def.settings?.saveProgress === true && !!onProgress;
|
|
2304
|
+
const onProgressRef = useRef(onProgress);
|
|
2305
|
+
onProgressRef.current = onProgress;
|
|
2306
|
+
useEffect(() => {
|
|
2307
|
+
if (!saveProgress) return;
|
|
2308
|
+
let timer;
|
|
2309
|
+
let last = engine.getState();
|
|
2310
|
+
const fire = () => {
|
|
2311
|
+
timer = void 0;
|
|
2312
|
+
if (engine.getState().status === "completed") return;
|
|
2313
|
+
try {
|
|
2314
|
+
void Promise.resolve(onProgressRef.current?.(engine, formProgress(engine))).catch(() => {
|
|
2315
|
+
});
|
|
2316
|
+
} catch {
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
const schedule = (delay) => {
|
|
2320
|
+
clearTimeout(timer);
|
|
2321
|
+
timer = setTimeout(fire, delay);
|
|
2322
|
+
};
|
|
2323
|
+
const unsubscribe = engine.subscribe(() => {
|
|
2324
|
+
const state = engine.getState();
|
|
2325
|
+
const previous = last;
|
|
2326
|
+
last = state;
|
|
2327
|
+
if (state.status === "completed") {
|
|
2328
|
+
clearTimeout(timer);
|
|
2329
|
+
timer = void 0;
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
const event = state.events[state.events.length - 1];
|
|
2333
|
+
if (layout === "focus") {
|
|
2334
|
+
if (event?.type === "next" && state.current !== previous.current) schedule(PROGRESS_DELAY_MS);
|
|
2335
|
+
} else if (event?.type === "answer") schedule(CLASSIC_PROGRESS_DELAY_MS);
|
|
2336
|
+
});
|
|
2337
|
+
const flush = () => {
|
|
2338
|
+
if (timer === void 0) return;
|
|
2339
|
+
clearTimeout(timer);
|
|
2340
|
+
fire();
|
|
2341
|
+
};
|
|
2342
|
+
window.addEventListener("pagehide", flush);
|
|
2343
|
+
return () => {
|
|
2344
|
+
unsubscribe();
|
|
2345
|
+
window.removeEventListener("pagehide", flush);
|
|
2346
|
+
flush();
|
|
2347
|
+
};
|
|
2348
|
+
}, [engine, saveProgress, layout]);
|
|
1678
2349
|
useLayoutEffect(() => {
|
|
1679
2350
|
if (previousCurrent.current === current) return;
|
|
1680
2351
|
previousCurrent.current = current;
|
|
@@ -1703,8 +2374,14 @@ function FormBody({
|
|
|
1703
2374
|
const message = state.current ? state.errors[state.current] : void 0;
|
|
1704
2375
|
if (message) announce(message);
|
|
1705
2376
|
};
|
|
2377
|
+
const waitingForUploads = () => {
|
|
2378
|
+
if (pendingUploads.count() === 0) return false;
|
|
2379
|
+
announce(UPLOAD_WAIT_MESSAGE);
|
|
2380
|
+
return true;
|
|
2381
|
+
};
|
|
1706
2382
|
const finish = async () => {
|
|
1707
2383
|
if (sending.current) return;
|
|
2384
|
+
if (engine.getState().status !== "completed" && waitingForUploads()) return;
|
|
1708
2385
|
if (engine.getState().status !== "completed" && !engine.submit()) {
|
|
1709
2386
|
if (layout === "classic") setSummaryRequest((n) => n + 1);
|
|
1710
2387
|
else announceStepError();
|
|
@@ -1725,8 +2402,12 @@ function FormBody({
|
|
|
1725
2402
|
sending.current = false;
|
|
1726
2403
|
}
|
|
1727
2404
|
};
|
|
1728
|
-
const isLastStep = (state) =>
|
|
2405
|
+
const isLastStep = (state) => {
|
|
2406
|
+
const shown = respondentPath(def, state);
|
|
2407
|
+
return state.current === shown[shown.length - 1];
|
|
2408
|
+
};
|
|
1729
2409
|
const advance = () => {
|
|
2410
|
+
if (waitingForUploads()) return;
|
|
1730
2411
|
direction.current = "forward";
|
|
1731
2412
|
if (isLastStep(engine.getState())) void finish();
|
|
1732
2413
|
else if (!engine.next()) announceStepError();
|
|
@@ -1811,6 +2492,7 @@ function FormBody({
|
|
|
1811
2492
|
}
|
|
1812
2493
|
const pageFocus = page && layout === "focus";
|
|
1813
2494
|
const pageClassic = page && layout === "classic";
|
|
2495
|
+
const notice = saveProgress && progressNotice && historyLength === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", "data-gf-progress-notice": "", children: progressNotice }) : null;
|
|
1814
2496
|
const renderBlock = (id, focus) => {
|
|
1815
2497
|
const block = engine.block(id);
|
|
1816
2498
|
if (!block || block.kind === "page-break") return null;
|
|
@@ -1940,6 +2622,7 @@ function FormBody({
|
|
|
1940
2622
|
/* @__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
2623
|
/* @__PURE__ */ jsx("div", { className: historyLength === 0 ? "mb-5" : "sr-only", children: header }),
|
|
1942
2624
|
fields,
|
|
2625
|
+
notice ? /* @__PURE__ */ jsx("div", { className: "mt-4", children: notice }) : null,
|
|
1943
2626
|
failure ? /* @__PURE__ */ jsx("div", { className: "mt-6", children: failure }) : null,
|
|
1944
2627
|
/* @__PURE__ */ jsx("div", { className: "mt-10", children: navigation })
|
|
1945
2628
|
] }) })
|
|
@@ -1953,6 +2636,7 @@ function FormBody({
|
|
|
1953
2636
|
children: /* @__PURE__ */ jsxs("div", { className: "mx-auto w-full max-w-2xl space-y-4", children: [
|
|
1954
2637
|
brandNode ? /* @__PURE__ */ jsx("div", { className: "flex justify-center pb-2", children: brandNode }) : null,
|
|
1955
2638
|
header,
|
|
2639
|
+
notice,
|
|
1956
2640
|
errorSummary,
|
|
1957
2641
|
fields,
|
|
1958
2642
|
failure,
|
|
@@ -1962,9 +2646,11 @@ function FormBody({
|
|
|
1962
2646
|
) : /* @__PURE__ */ jsxs("form", { ...formProps, className: cn("mx-auto w-full max-w-xl space-y-8", className), children: [
|
|
1963
2647
|
brandNode,
|
|
1964
2648
|
header,
|
|
2649
|
+
layout === "classic" ? notice : null,
|
|
1965
2650
|
errorSummary,
|
|
1966
2651
|
progress,
|
|
1967
2652
|
fields,
|
|
2653
|
+
layout === "focus" ? notice : null,
|
|
1968
2654
|
failure,
|
|
1969
2655
|
navigation
|
|
1970
2656
|
] })
|
|
@@ -1972,6 +2658,37 @@ function FormBody({
|
|
|
1972
2658
|
}
|
|
1973
2659
|
var classicSection = "rounded-xl border bg-card px-5 py-6 text-card-foreground sm:px-8 has-[[aria-invalid=true]]:border-destructive";
|
|
1974
2660
|
|
|
1975
|
-
|
|
2661
|
+
// src/clean-url.ts
|
|
2662
|
+
var KEY_PREFIX = "form-query:";
|
|
2663
|
+
var HIDDEN_QUERY_KEEP_MS = 30 * 60 * 1e3;
|
|
2664
|
+
function hideQueryOnLoad(win, isReload, now = Date.now()) {
|
|
2665
|
+
const { pathname, search, hash } = win.location;
|
|
2666
|
+
const key = KEY_PREFIX + pathname;
|
|
2667
|
+
try {
|
|
2668
|
+
if (search && search !== "?") {
|
|
2669
|
+
win.sessionStorage.setItem(key, JSON.stringify({ search, at: now }));
|
|
2670
|
+
const replace = nativeReplaceState(win.history);
|
|
2671
|
+
replace.call(win.history, win.history.state, "", pathname + hash);
|
|
2672
|
+
return "hidden";
|
|
2673
|
+
}
|
|
2674
|
+
const saved = win.sessionStorage.getItem(key);
|
|
2675
|
+
if (!saved) return "none";
|
|
2676
|
+
const { search: kept, at } = JSON.parse(saved);
|
|
2677
|
+
if (isReload && typeof kept === "string" && typeof at === "number" && now - at < HIDDEN_QUERY_KEEP_MS) {
|
|
2678
|
+
win.location.replace(pathname + kept + hash);
|
|
2679
|
+
return "restoring";
|
|
2680
|
+
}
|
|
2681
|
+
win.sessionStorage.removeItem(key);
|
|
2682
|
+
} catch {
|
|
2683
|
+
}
|
|
2684
|
+
return "none";
|
|
2685
|
+
}
|
|
2686
|
+
function nativeReplaceState(history) {
|
|
2687
|
+
const proto = Object.getPrototypeOf(history);
|
|
2688
|
+
return typeof proto?.replaceState === "function" ? proto.replaceState : history.replaceState;
|
|
2689
|
+
}
|
|
2690
|
+
var wasReload = () => typeof performance !== "undefined" && performance.getEntriesByType?.("navigation")[0]?.type === "reload";
|
|
2691
|
+
|
|
2692
|
+
export { CLASSIC_PROGRESS_DELAY_MS, DefaultEnding, FieldChrome, Form, HIDDEN_QUERY_KEEP_MS, MediaTimeContext, PROGRESS_DELAY_MS, SourceTextContext, UPLOAD_WAIT_MESSAGE, UploadContext, builtInRenderers, defaultRegistry, describedBy, hideQueryOnLoad, photoAccept, photoFileProblem, photoFileType, themeColors, themeCss, useFormPresentation, useMediaTime, useSourceText, useUpload, wasReload };
|
|
1976
2693
|
//# sourceMappingURL=index.js.map
|
|
1977
2694
|
//# sourceMappingURL=index.js.map
|