@usefillo/react 0.3.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 +8 -4
- package/dist/index.d.ts +49 -21
- package/dist/index.js +1008 -386
- package/dist/styles.css +284 -14
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/FilloForm.tsx
|
|
4
|
-
import { useEffect as
|
|
5
|
-
import {
|
|
4
|
+
import { useEffect as useEffect7, useId as useId2, useMemo as useMemo4, useRef as useRef5, useState as useState6 } from "react";
|
|
5
|
+
import {
|
|
6
|
+
FilloError as FilloError2,
|
|
7
|
+
createClient,
|
|
8
|
+
isField as isField2,
|
|
9
|
+
normalizeFormSchema,
|
|
10
|
+
normalizeFormTheme
|
|
11
|
+
} from "@usefillo/core";
|
|
6
12
|
|
|
7
13
|
// src/context.ts
|
|
8
14
|
import { allFields } from "@usefillo/core";
|
|
9
|
-
import { createContext, useContext } from "react";
|
|
15
|
+
import { createContext, useContext, useId, useMemo } from "react";
|
|
10
16
|
var FilloContext = createContext(null);
|
|
17
|
+
var FilloInstanceIdContext = createContext(null);
|
|
18
|
+
function useFilloInstanceId() {
|
|
19
|
+
const instanceId = useContext(FilloInstanceIdContext);
|
|
20
|
+
const fallbackId = useId().replace(/:/g, "");
|
|
21
|
+
return instanceId ?? fallbackId;
|
|
22
|
+
}
|
|
23
|
+
function createFilloFieldIds(instanceId, fieldId) {
|
|
24
|
+
const base = `fillo-${instanceId}-${fieldId}`;
|
|
25
|
+
return {
|
|
26
|
+
inputId: base,
|
|
27
|
+
labelId: `${base}-label`,
|
|
28
|
+
descriptionId: `${base}-desc`,
|
|
29
|
+
errorId: `${base}-error`,
|
|
30
|
+
name: base
|
|
31
|
+
};
|
|
32
|
+
}
|
|
11
33
|
function useFillo() {
|
|
12
34
|
const api = useContext(FilloContext);
|
|
13
35
|
if (!api) throw new Error("useFillo must be used inside <FilloForm> or <FilloProvider>");
|
|
14
36
|
return api;
|
|
15
37
|
}
|
|
38
|
+
function useFilloFieldIds(fieldId, ids) {
|
|
39
|
+
const instanceId = useFilloInstanceId();
|
|
40
|
+
return ids ?? createFilloFieldIds(instanceId, fieldId);
|
|
41
|
+
}
|
|
16
42
|
function useField(fieldId) {
|
|
17
43
|
const api = useFillo();
|
|
18
|
-
const
|
|
44
|
+
const fieldsById = useMemo(() => {
|
|
45
|
+
const map = /* @__PURE__ */ new Map();
|
|
46
|
+
for (const f of allFields(api.form)) map.set(f.id, f);
|
|
47
|
+
return map;
|
|
48
|
+
}, [api.form]);
|
|
49
|
+
const field = fieldsById.get(fieldId);
|
|
19
50
|
return {
|
|
20
51
|
field,
|
|
21
52
|
value: api.data[fieldId],
|
|
@@ -25,7 +56,7 @@ function useField(fieldId) {
|
|
|
25
56
|
}
|
|
26
57
|
|
|
27
58
|
// src/controller.ts
|
|
28
|
-
import { useEffect, useRef, useSyncExternalStore } from "react";
|
|
59
|
+
import { useEffect, useMemo as useMemo2, useRef, useSyncExternalStore } from "react";
|
|
29
60
|
import {
|
|
30
61
|
createFormController
|
|
31
62
|
} from "@usefillo/core";
|
|
@@ -52,17 +83,20 @@ function useFilloController(options) {
|
|
|
52
83
|
}, [store, options.form, options.formId, options.client]);
|
|
53
84
|
useEffect(() => () => store.destroy(), [store]);
|
|
54
85
|
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
return useMemo2(
|
|
87
|
+
() => ({
|
|
88
|
+
form: options.form,
|
|
89
|
+
formId: options.formId,
|
|
90
|
+
client: options.client,
|
|
91
|
+
...state,
|
|
92
|
+
setValue: store.setValue,
|
|
93
|
+
setUploading: store.setUploading,
|
|
94
|
+
next: store.next,
|
|
95
|
+
back: store.back,
|
|
96
|
+
submit: store.submit
|
|
97
|
+
}),
|
|
98
|
+
[store, state, options.form, options.formId, options.client]
|
|
99
|
+
);
|
|
66
100
|
}
|
|
67
101
|
|
|
68
102
|
// src/define.ts
|
|
@@ -96,17 +130,247 @@ function useCodeFormSync(form, client, onError) {
|
|
|
96
130
|
}
|
|
97
131
|
|
|
98
132
|
// src/fields.tsx
|
|
99
|
-
import { isField, pipeBlock } from "@usefillo/core";
|
|
100
|
-
import { useEffect as
|
|
133
|
+
import { isField, pipeBlock, validateResponse } from "@usefillo/core";
|
|
134
|
+
import { memo, useEffect as useEffect6, useState as useState5 } from "react";
|
|
101
135
|
|
|
102
|
-
// src/
|
|
103
|
-
import {
|
|
136
|
+
// src/phone.tsx
|
|
137
|
+
import {
|
|
138
|
+
PHONE_COUNTRIES,
|
|
139
|
+
countryByIso,
|
|
140
|
+
digitsOnly,
|
|
141
|
+
flagEmoji,
|
|
142
|
+
formatNational,
|
|
143
|
+
parsePhone,
|
|
144
|
+
toE164
|
|
145
|
+
} from "@usefillo/core";
|
|
146
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef2, useState as useState2 } from "react";
|
|
104
147
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
148
|
+
function localeCountry() {
|
|
149
|
+
if (typeof navigator === "undefined") return void 0;
|
|
150
|
+
const tag = navigator.languages?.[0] || navigator.language || "";
|
|
151
|
+
const region = tag.split("-")[1];
|
|
152
|
+
return region && /^[A-Za-z]{2}$/.test(region) ? region.toUpperCase() : void 0;
|
|
153
|
+
}
|
|
154
|
+
function nationalFor(stored, country) {
|
|
155
|
+
const digits = digitsOnly(stored);
|
|
156
|
+
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
|
157
|
+
}
|
|
158
|
+
function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
159
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
160
|
+
const schema = field;
|
|
161
|
+
const stored = typeof value === "string" ? value : "";
|
|
162
|
+
const [country, setCountry] = useState2(
|
|
163
|
+
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? countryByIso(localeCountry()) ?? PHONE_COUNTRIES[0]
|
|
164
|
+
);
|
|
165
|
+
const national = nationalFor(stored, country);
|
|
166
|
+
const display = formatNational(country, national);
|
|
167
|
+
const inputRef = useRef2(null);
|
|
168
|
+
const caretDigits = useRef2(null);
|
|
169
|
+
useLayoutEffect(() => {
|
|
170
|
+
const el = inputRef.current;
|
|
171
|
+
if (!el || caretDigits.current == null) return;
|
|
172
|
+
const target = caretDigits.current;
|
|
173
|
+
caretDigits.current = null;
|
|
174
|
+
if (target === 0) return el.setSelectionRange(0, 0);
|
|
175
|
+
let seen = 0;
|
|
176
|
+
let pos = display.length;
|
|
177
|
+
for (let i = 0; i < display.length; i++) {
|
|
178
|
+
if (/\d/.test(display[i]) && ++seen === target) {
|
|
179
|
+
pos = i + 1;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
el.setSelectionRange(pos, pos);
|
|
184
|
+
});
|
|
185
|
+
const commit = (c, nat) => setValue(nat ? toE164(c, nat) : "");
|
|
186
|
+
function onInput(e) {
|
|
187
|
+
const el = e.target;
|
|
188
|
+
const raw = el.value;
|
|
189
|
+
const caret = el.selectionStart ?? raw.length;
|
|
190
|
+
caretDigits.current = digitsOnly(raw.slice(0, caret)).length;
|
|
191
|
+
if (raw.trimStart().startsWith("+")) {
|
|
192
|
+
const p = parsePhone(raw);
|
|
193
|
+
if (p.country) setCountry(p.country);
|
|
194
|
+
commit(p.country ?? country, p.country ? p.national : digitsOnly(raw));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
commit(country, digitsOnly(raw));
|
|
198
|
+
}
|
|
199
|
+
function pickCountry(c) {
|
|
200
|
+
setCountry(c);
|
|
201
|
+
commit(c, national);
|
|
202
|
+
inputRef.current?.focus();
|
|
203
|
+
}
|
|
204
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
205
|
+
return /* @__PURE__ */ jsxs("div", { className: `fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`, "data-field": field.id, children: [
|
|
206
|
+
/* @__PURE__ */ jsxs("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
207
|
+
field.label,
|
|
208
|
+
!field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: " (optional)" })
|
|
209
|
+
] }),
|
|
210
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
211
|
+
/* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
|
|
212
|
+
/* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
|
|
213
|
+
/* @__PURE__ */ jsx(
|
|
214
|
+
"input",
|
|
215
|
+
{
|
|
216
|
+
ref: inputRef,
|
|
217
|
+
id: ids.inputId,
|
|
218
|
+
className: "fillo-input fillo-phone-input",
|
|
219
|
+
type: "tel",
|
|
220
|
+
inputMode: "tel",
|
|
221
|
+
autoComplete: "tel-national",
|
|
222
|
+
value: display,
|
|
223
|
+
placeholder: formatNational(country, country.example),
|
|
224
|
+
onChange: onInput,
|
|
225
|
+
"aria-invalid": error ? true : void 0,
|
|
226
|
+
"aria-required": field.required ? true : void 0,
|
|
227
|
+
"aria-describedby": describedBy
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
] }),
|
|
231
|
+
error && /* @__PURE__ */ jsx("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
232
|
+
] });
|
|
233
|
+
}
|
|
234
|
+
function CountrySelect({
|
|
235
|
+
country,
|
|
236
|
+
onPick,
|
|
237
|
+
labelledBy
|
|
238
|
+
}) {
|
|
239
|
+
const [open, setOpen] = useState2(false);
|
|
240
|
+
const [query, setQuery] = useState2("");
|
|
241
|
+
const [active, setActive] = useState2(0);
|
|
242
|
+
const rootRef = useRef2(null);
|
|
243
|
+
const searchRef = useRef2(null);
|
|
244
|
+
const listId = `${labelledBy}-countries`;
|
|
245
|
+
const matches = useMemo3(() => {
|
|
246
|
+
const q = query.trim().toLowerCase();
|
|
247
|
+
if (!q) return PHONE_COUNTRIES;
|
|
248
|
+
const qDigits = q.replace(/[^\d]/g, "");
|
|
249
|
+
return PHONE_COUNTRIES.filter(
|
|
250
|
+
(c) => c.name.toLowerCase().includes(q) || c.iso2.toLowerCase() === q || qDigits.length > 0 && c.dialCode.startsWith(qDigits)
|
|
251
|
+
);
|
|
252
|
+
}, [query]);
|
|
253
|
+
useEffect3(() => {
|
|
254
|
+
if (!open) return;
|
|
255
|
+
setQuery("");
|
|
256
|
+
setActive(Math.max(0, PHONE_COUNTRIES.indexOf(country)));
|
|
257
|
+
const id = requestAnimationFrame(() => searchRef.current?.focus());
|
|
258
|
+
return () => cancelAnimationFrame(id);
|
|
259
|
+
}, [open, country]);
|
|
109
260
|
useEffect3(() => {
|
|
261
|
+
if (!open) return;
|
|
262
|
+
const onDown = (e) => {
|
|
263
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
264
|
+
};
|
|
265
|
+
document.addEventListener("mousedown", onDown);
|
|
266
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
267
|
+
}, [open]);
|
|
268
|
+
useEffect3(() => {
|
|
269
|
+
if (!open) return;
|
|
270
|
+
document.getElementById(`${listId}-opt-${active}`)?.scrollIntoView({ block: "nearest" });
|
|
271
|
+
}, [active, open, listId]);
|
|
272
|
+
function choose(c) {
|
|
273
|
+
if (!c) return;
|
|
274
|
+
onPick(c);
|
|
275
|
+
setOpen(false);
|
|
276
|
+
}
|
|
277
|
+
function onSearchKey(e) {
|
|
278
|
+
if (e.key === "ArrowDown") {
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
setActive((i) => Math.min(i + 1, matches.length - 1));
|
|
281
|
+
} else if (e.key === "ArrowUp") {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
284
|
+
} else if (e.key === "Enter") {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
choose(matches[active]);
|
|
287
|
+
} else if (e.key === "Escape") {
|
|
288
|
+
e.preventDefault();
|
|
289
|
+
setOpen(false);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
|
|
293
|
+
/* @__PURE__ */ jsxs(
|
|
294
|
+
"button",
|
|
295
|
+
{
|
|
296
|
+
type: "button",
|
|
297
|
+
className: "fillo-phone-flag",
|
|
298
|
+
"aria-haspopup": "listbox",
|
|
299
|
+
"aria-expanded": open,
|
|
300
|
+
"aria-label": `Country: ${country.name} (+${country.dialCode})`,
|
|
301
|
+
onClick: () => setOpen((o) => !o),
|
|
302
|
+
children: [
|
|
303
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(country.iso2) }),
|
|
304
|
+
/* @__PURE__ */ jsxs("span", { className: "fillo-phone-dial", children: [
|
|
305
|
+
"+",
|
|
306
|
+
country.dialCode
|
|
307
|
+
] }),
|
|
308
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-caret", "aria-hidden": "true", children: "\u25BE" })
|
|
309
|
+
]
|
|
310
|
+
}
|
|
311
|
+
),
|
|
312
|
+
open && /* @__PURE__ */ jsxs("div", { className: "fillo-phone-popover", children: [
|
|
313
|
+
/* @__PURE__ */ jsx(
|
|
314
|
+
"input",
|
|
315
|
+
{
|
|
316
|
+
ref: searchRef,
|
|
317
|
+
className: "fillo-phone-search",
|
|
318
|
+
type: "text",
|
|
319
|
+
role: "combobox",
|
|
320
|
+
"aria-expanded": "true",
|
|
321
|
+
"aria-controls": listId,
|
|
322
|
+
"aria-autocomplete": "list",
|
|
323
|
+
"aria-activedescendant": matches[active] ? `${listId}-opt-${active}` : void 0,
|
|
324
|
+
placeholder: "Search country or code",
|
|
325
|
+
value: query,
|
|
326
|
+
onChange: (e) => {
|
|
327
|
+
setQuery(e.target.value);
|
|
328
|
+
setActive(0);
|
|
329
|
+
},
|
|
330
|
+
onKeyDown: onSearchKey
|
|
331
|
+
}
|
|
332
|
+
),
|
|
333
|
+
/* @__PURE__ */ jsxs("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
|
|
334
|
+
matches.map((c, i) => /* @__PURE__ */ jsxs(
|
|
335
|
+
"li",
|
|
336
|
+
{
|
|
337
|
+
id: `${listId}-opt-${i}`,
|
|
338
|
+
role: "option",
|
|
339
|
+
"aria-selected": c.iso2 === country.iso2,
|
|
340
|
+
className: `fillo-phone-option${i === active ? " fillo-phone-option--active" : ""}`,
|
|
341
|
+
onMouseEnter: () => setActive(i),
|
|
342
|
+
onMouseDown: (e) => {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
choose(c);
|
|
345
|
+
},
|
|
346
|
+
children: [
|
|
347
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(c.iso2) }),
|
|
348
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-option-name", children: c.name }),
|
|
349
|
+
/* @__PURE__ */ jsxs("span", { className: "fillo-phone-option-dial", children: [
|
|
350
|
+
"+",
|
|
351
|
+
c.dialCode
|
|
352
|
+
] })
|
|
353
|
+
]
|
|
354
|
+
},
|
|
355
|
+
c.iso2
|
|
356
|
+
)),
|
|
357
|
+
matches.length === 0 && /* @__PURE__ */ jsx("li", { className: "fillo-phone-empty", children: "No matches" })
|
|
358
|
+
] })
|
|
359
|
+
] })
|
|
360
|
+
] });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// src/signature.tsx
|
|
364
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
|
|
365
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
366
|
+
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
367
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
368
|
+
const typedInputId = `${ids.inputId}-typed`;
|
|
369
|
+
const canvasRef = useRef3(null);
|
|
370
|
+
const drawing = useRef3(false);
|
|
371
|
+
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
372
|
+
const [typedName, setTypedName] = useState3("");
|
|
373
|
+
useEffect4(() => {
|
|
110
374
|
const canvas = canvasRef.current;
|
|
111
375
|
if (!canvas) return;
|
|
112
376
|
const scale = window.devicePixelRatio || 1;
|
|
@@ -133,12 +397,31 @@ function SignatureField({ field, value, error, setValue }) {
|
|
|
133
397
|
function start(e) {
|
|
134
398
|
drawing.current = true;
|
|
135
399
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
400
|
+
if (typedName) setTypedName("");
|
|
136
401
|
const ctx = e.currentTarget.getContext("2d");
|
|
137
402
|
if (!ctx) return;
|
|
138
403
|
const { x, y } = point(e);
|
|
139
404
|
ctx.beginPath();
|
|
140
405
|
ctx.moveTo(x, y);
|
|
141
406
|
}
|
|
407
|
+
function drawTypedName(text) {
|
|
408
|
+
const canvas = canvasRef.current;
|
|
409
|
+
const ctx = canvas?.getContext("2d");
|
|
410
|
+
if (!canvas || !ctx) return;
|
|
411
|
+
const rect = canvas.getBoundingClientRect();
|
|
412
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
413
|
+
if (text.trim()) {
|
|
414
|
+
ctx.fillStyle = getComputedStyle(canvas).color;
|
|
415
|
+
ctx.font = `italic ${Math.min(40, rect.height * 0.45)}px "Segoe Script", "Brush Script MT", cursive`;
|
|
416
|
+
ctx.textBaseline = "middle";
|
|
417
|
+
ctx.fillText(text, 12, rect.height / 2);
|
|
418
|
+
setHasInk(true);
|
|
419
|
+
setValue(canvas.toDataURL("image/png"));
|
|
420
|
+
} else {
|
|
421
|
+
setHasInk(false);
|
|
422
|
+
setValue(null);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
142
425
|
function move(e) {
|
|
143
426
|
if (!drawing.current) return;
|
|
144
427
|
const ctx = e.currentTarget.getContext("2d");
|
|
@@ -159,55 +442,101 @@ function SignatureField({ field, value, error, setValue }) {
|
|
|
159
442
|
if (!canvas || !ctx) return;
|
|
160
443
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
161
444
|
setHasInk(false);
|
|
445
|
+
setTypedName("");
|
|
162
446
|
setValue(null);
|
|
163
447
|
}
|
|
164
|
-
const describedBy = [field.description ?
|
|
165
|
-
return /* @__PURE__ */
|
|
166
|
-
/* @__PURE__ */
|
|
448
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
449
|
+
return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
|
|
450
|
+
/* @__PURE__ */ jsxs2("span", { className: "fillo-label", id: ids.labelId, children: [
|
|
167
451
|
field.label,
|
|
168
|
-
!field.required && /* @__PURE__ */
|
|
169
|
-
] }),
|
|
170
|
-
field.description && /* @__PURE__ */ jsx("p", { className: "fillo-description", id: `fillo-${field.id}-desc`, children: field.description }),
|
|
171
|
-
/* @__PURE__ */ jsxs("div", { className: "fillo-signature", children: [
|
|
172
|
-
/* @__PURE__ */ jsx(
|
|
173
|
-
"canvas",
|
|
174
|
-
{
|
|
175
|
-
ref: canvasRef,
|
|
176
|
-
id: `fillo-${field.id}`,
|
|
177
|
-
className: "fillo-signature-canvas",
|
|
178
|
-
"aria-invalid": error ? true : void 0,
|
|
179
|
-
"aria-describedby": describedBy,
|
|
180
|
-
onPointerDown: start,
|
|
181
|
-
onPointerMove: move,
|
|
182
|
-
onPointerUp: end,
|
|
183
|
-
onPointerLeave: end
|
|
184
|
-
}
|
|
185
|
-
),
|
|
186
|
-
hasInk && /* @__PURE__ */ jsx("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
|
|
187
|
-
!hasInk && /* @__PURE__ */ jsx("span", { className: "fillo-signature-hint", children: "Sign here" })
|
|
452
|
+
!field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: " (optional)" })
|
|
188
453
|
] }),
|
|
189
|
-
|
|
454
|
+
field.description && /* @__PURE__ */ jsx2("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
455
|
+
/* @__PURE__ */ jsxs2(
|
|
456
|
+
"div",
|
|
457
|
+
{
|
|
458
|
+
role: "group",
|
|
459
|
+
"aria-labelledby": ids.labelId,
|
|
460
|
+
"aria-describedby": describedBy,
|
|
461
|
+
children: [
|
|
462
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
|
|
463
|
+
/* @__PURE__ */ jsx2(
|
|
464
|
+
"canvas",
|
|
465
|
+
{
|
|
466
|
+
ref: canvasRef,
|
|
467
|
+
className: "fillo-signature-canvas",
|
|
468
|
+
"aria-hidden": "true",
|
|
469
|
+
onPointerDown: start,
|
|
470
|
+
onPointerMove: move,
|
|
471
|
+
onPointerUp: end,
|
|
472
|
+
onPointerLeave: end
|
|
473
|
+
}
|
|
474
|
+
),
|
|
475
|
+
hasInk && /* @__PURE__ */ jsx2("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
|
|
476
|
+
!hasInk && /* @__PURE__ */ jsx2("span", { className: "fillo-signature-hint", "aria-hidden": "true", children: "Sign here" })
|
|
477
|
+
] }),
|
|
478
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature-type", children: [
|
|
479
|
+
/* @__PURE__ */ jsx2("label", { className: "fillo-signature-type-label", htmlFor: typedInputId, children: "Or type your full name to sign" }),
|
|
480
|
+
/* @__PURE__ */ jsx2(
|
|
481
|
+
"input",
|
|
482
|
+
{
|
|
483
|
+
id: typedInputId,
|
|
484
|
+
type: "text",
|
|
485
|
+
className: "fillo-input fillo-signature-type-input",
|
|
486
|
+
autoComplete: "name",
|
|
487
|
+
value: typedName,
|
|
488
|
+
required: field.required || void 0,
|
|
489
|
+
"aria-required": field.required || void 0,
|
|
490
|
+
"aria-invalid": error ? true : void 0,
|
|
491
|
+
"aria-describedby": describedBy,
|
|
492
|
+
onChange: (e) => {
|
|
493
|
+
setTypedName(e.target.value);
|
|
494
|
+
drawTypedName(e.target.value);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
)
|
|
498
|
+
] })
|
|
499
|
+
]
|
|
500
|
+
}
|
|
501
|
+
),
|
|
502
|
+
error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
190
503
|
] });
|
|
191
504
|
}
|
|
192
505
|
|
|
193
506
|
// src/upload.tsx
|
|
194
|
-
import { useEffect as
|
|
195
|
-
import { Fragment, jsx as
|
|
507
|
+
import { useEffect as useEffect5, useRef as useRef4, useState as useState4 } from "react";
|
|
508
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
509
|
+
function uploadStatus(inFlight, doneCount) {
|
|
510
|
+
const failed = inFlight.filter((f) => f.error).length;
|
|
511
|
+
const active = inFlight.length - failed;
|
|
512
|
+
const parts = [];
|
|
513
|
+
if (active > 0) {
|
|
514
|
+
parts.push(`Uploading ${active} ${active === 1 ? "file" : "files"}, please wait\u2026`);
|
|
515
|
+
}
|
|
516
|
+
if (failed > 0) {
|
|
517
|
+
parts.push(`${failed} ${failed === 1 ? "upload" : "uploads"} failed.`);
|
|
518
|
+
}
|
|
519
|
+
if (active === 0 && doneCount > 0) {
|
|
520
|
+
parts.push(`${doneCount} ${doneCount === 1 ? "file" : "files"} uploaded.`);
|
|
521
|
+
}
|
|
522
|
+
return parts.join(" ");
|
|
523
|
+
}
|
|
196
524
|
function formatBytes(bytes) {
|
|
197
525
|
if (bytes < 1024) return `${bytes} B`;
|
|
198
526
|
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
199
527
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
200
528
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
201
529
|
}
|
|
202
|
-
function FileUploadField({ field, value, error, setValue, api }) {
|
|
530
|
+
function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
|
|
531
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
203
532
|
const schema = field;
|
|
204
|
-
const inputRef =
|
|
205
|
-
const [inFlight, setInFlight] =
|
|
206
|
-
const [dragOver, setDragOver] =
|
|
207
|
-
const mountedRef =
|
|
208
|
-
const controllers =
|
|
209
|
-
const inFlightCount =
|
|
210
|
-
|
|
533
|
+
const inputRef = useRef4(null);
|
|
534
|
+
const [inFlight, setInFlight] = useState4([]);
|
|
535
|
+
const [dragOver, setDragOver] = useState4(false);
|
|
536
|
+
const mountedRef = useRef4(true);
|
|
537
|
+
const controllers = useRef4(/* @__PURE__ */ new Map());
|
|
538
|
+
const inFlightCount = useRef4(0);
|
|
539
|
+
useEffect5(
|
|
211
540
|
() => () => {
|
|
212
541
|
mountedRef.current = false;
|
|
213
542
|
controllers.current.forEach((c) => c.abort());
|
|
@@ -271,14 +600,14 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
271
600
|
function removeFile(fileId) {
|
|
272
601
|
setValue(files.filter((f) => f.fileId !== fileId));
|
|
273
602
|
}
|
|
274
|
-
const describedBy = [field.description ?
|
|
275
|
-
return /* @__PURE__ */
|
|
276
|
-
/* @__PURE__ */
|
|
603
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
604
|
+
return /* @__PURE__ */ jsxs3("div", { className: `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`, children: [
|
|
605
|
+
/* @__PURE__ */ jsxs3("label", { className: "fillo-label", htmlFor: ids.inputId, children: [
|
|
277
606
|
field.label,
|
|
278
|
-
!field.required && /* @__PURE__ */
|
|
607
|
+
!field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: " (optional)" })
|
|
279
608
|
] }),
|
|
280
|
-
field.description && /* @__PURE__ */
|
|
281
|
-
remaining > 0 && /* @__PURE__ */
|
|
609
|
+
field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
610
|
+
remaining > 0 && /* @__PURE__ */ jsxs3(
|
|
282
611
|
"div",
|
|
283
612
|
{
|
|
284
613
|
className: `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`,
|
|
@@ -299,11 +628,11 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
299
628
|
if ((e.key === "Enter" || e.key === " ") && canUpload) inputRef.current?.click();
|
|
300
629
|
},
|
|
301
630
|
children: [
|
|
302
|
-
/* @__PURE__ */
|
|
631
|
+
/* @__PURE__ */ jsx3(
|
|
303
632
|
"input",
|
|
304
633
|
{
|
|
305
634
|
ref: inputRef,
|
|
306
|
-
id:
|
|
635
|
+
id: ids.inputId,
|
|
307
636
|
type: "file",
|
|
308
637
|
hidden: true,
|
|
309
638
|
"aria-invalid": error ? true : void 0,
|
|
@@ -314,26 +643,27 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
314
643
|
onChange: (e) => handleFiles(e.target.files)
|
|
315
644
|
}
|
|
316
645
|
),
|
|
317
|
-
canUpload ? /* @__PURE__ */
|
|
318
|
-
/* @__PURE__ */
|
|
646
|
+
canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
647
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-title", children: [
|
|
319
648
|
"Drop ",
|
|
320
649
|
maxFiles > 1 ? "files" : "a file",
|
|
321
650
|
" here or click to browse"
|
|
322
651
|
] }),
|
|
323
|
-
/* @__PURE__ */
|
|
652
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-hint", children: [
|
|
324
653
|
"Up to ",
|
|
325
654
|
schema.maxFileSizeMb ?? 500,
|
|
326
655
|
" MB per file \xB7 resumable"
|
|
327
656
|
] })
|
|
328
|
-
] }) : /* @__PURE__ */
|
|
657
|
+
] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: "Uploads are disabled in preview" })
|
|
329
658
|
]
|
|
330
659
|
}
|
|
331
660
|
),
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
/* @__PURE__ */
|
|
336
|
-
/* @__PURE__ */
|
|
661
|
+
/* @__PURE__ */ jsx3("div", { role: "status", "aria-live": "polite", className: "fillo-sr-only", children: uploadStatus(inFlight, files.length) }),
|
|
662
|
+
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
663
|
+
files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
|
|
664
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
665
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
|
|
666
|
+
/* @__PURE__ */ jsx3(
|
|
337
667
|
"button",
|
|
338
668
|
{
|
|
339
669
|
type: "button",
|
|
@@ -344,50 +674,64 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
344
674
|
}
|
|
345
675
|
)
|
|
346
676
|
] }, f.fileId)),
|
|
347
|
-
inFlight.map((f) => /* @__PURE__ */
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
"
|
|
353
|
-
{
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
"
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
677
|
+
inFlight.map((f) => /* @__PURE__ */ jsxs3(
|
|
678
|
+
"li",
|
|
679
|
+
{
|
|
680
|
+
className: `fillo-file${f.error ? " fillo-file--failed" : ""}`,
|
|
681
|
+
...f.error ? {} : {
|
|
682
|
+
role: "progressbar",
|
|
683
|
+
"aria-label": `Uploading ${f.name}`,
|
|
684
|
+
"aria-valuemin": 0,
|
|
685
|
+
"aria-valuemax": 100,
|
|
686
|
+
"aria-valuenow": Math.round(f.fraction * 100)
|
|
687
|
+
},
|
|
688
|
+
children: [
|
|
689
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
690
|
+
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
691
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
|
|
692
|
+
/* @__PURE__ */ jsx3(
|
|
693
|
+
"button",
|
|
694
|
+
{
|
|
695
|
+
type: "button",
|
|
696
|
+
className: "fillo-file-remove",
|
|
697
|
+
"aria-label": "Dismiss",
|
|
698
|
+
onClick: () => setInFlight((prev) => prev.filter((x) => x.key !== f.key)),
|
|
699
|
+
children: "\xD7"
|
|
700
|
+
}
|
|
701
|
+
)
|
|
702
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
703
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
|
|
704
|
+
Math.round(f.fraction * 100),
|
|
705
|
+
"% of ",
|
|
706
|
+
formatBytes(f.size)
|
|
707
|
+
] }),
|
|
708
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
|
|
709
|
+
/* @__PURE__ */ jsx3(
|
|
710
|
+
"button",
|
|
711
|
+
{
|
|
712
|
+
type: "button",
|
|
713
|
+
className: "fillo-file-remove",
|
|
714
|
+
"aria-label": `Cancel ${f.name}`,
|
|
715
|
+
onClick: () => cancelUpload(f.key),
|
|
716
|
+
children: "\xD7"
|
|
717
|
+
}
|
|
718
|
+
)
|
|
719
|
+
] })
|
|
720
|
+
]
|
|
721
|
+
},
|
|
722
|
+
f.key
|
|
723
|
+
))
|
|
380
724
|
] }),
|
|
381
|
-
error && /* @__PURE__ */
|
|
725
|
+
error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
382
726
|
] });
|
|
383
727
|
}
|
|
384
728
|
|
|
385
729
|
// src/fields.tsx
|
|
386
|
-
import { jsx as
|
|
387
|
-
function fieldAria(field, error) {
|
|
730
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
731
|
+
function fieldAria(field, error, ids) {
|
|
388
732
|
const describedBy = [
|
|
389
|
-
field.description ?
|
|
390
|
-
error ?
|
|
733
|
+
field.description ? ids.descriptionId : null,
|
|
734
|
+
error ? ids.errorId : null
|
|
391
735
|
].filter(Boolean);
|
|
392
736
|
return {
|
|
393
737
|
"aria-invalid": error ? true : void 0,
|
|
@@ -398,58 +742,61 @@ function fieldAria(field, error) {
|
|
|
398
742
|
function FieldShell({
|
|
399
743
|
field,
|
|
400
744
|
error,
|
|
745
|
+
ids,
|
|
401
746
|
children
|
|
402
747
|
}) {
|
|
403
|
-
return /* @__PURE__ */
|
|
748
|
+
return /* @__PURE__ */ jsxs4(
|
|
404
749
|
"div",
|
|
405
750
|
{
|
|
406
751
|
className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
|
|
407
752
|
"data-field": field.id,
|
|
408
753
|
children: [
|
|
409
|
-
/* @__PURE__ */
|
|
754
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
410
755
|
field.label,
|
|
411
|
-
!field.required && /* @__PURE__ */
|
|
756
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
|
|
412
757
|
] }),
|
|
413
|
-
field.description && /* @__PURE__ */
|
|
758
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
414
759
|
children,
|
|
415
|
-
error && /* @__PURE__ */
|
|
760
|
+
error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
416
761
|
]
|
|
417
762
|
}
|
|
418
763
|
);
|
|
419
764
|
}
|
|
420
|
-
function TextInput({ field, value, error, setValue }) {
|
|
421
|
-
const
|
|
422
|
-
|
|
765
|
+
function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
766
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
767
|
+
const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
|
|
768
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
423
769
|
"input",
|
|
424
770
|
{
|
|
425
|
-
id:
|
|
771
|
+
id: ids.inputId,
|
|
426
772
|
className: "fillo-input",
|
|
427
773
|
type,
|
|
428
774
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
429
775
|
placeholder: field.placeholder,
|
|
430
776
|
onChange: (e) => setValue(e.target.value),
|
|
431
|
-
...fieldAria(field, error)
|
|
777
|
+
...fieldAria(field, error, ids)
|
|
432
778
|
}
|
|
433
779
|
) });
|
|
434
780
|
}
|
|
435
|
-
function LongText({ field, value, error, setValue }) {
|
|
436
|
-
|
|
781
|
+
function LongText({ field, value, error, setValue, ids: providedIds }) {
|
|
782
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
783
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
437
784
|
"textarea",
|
|
438
785
|
{
|
|
439
|
-
id:
|
|
786
|
+
id: ids.inputId,
|
|
440
787
|
className: "fillo-input fillo-textarea",
|
|
441
788
|
rows: 4,
|
|
442
789
|
value: typeof value === "string" ? value : "",
|
|
443
790
|
placeholder: field.placeholder,
|
|
444
791
|
onChange: (e) => setValue(e.target.value),
|
|
445
|
-
...fieldAria(field, error)
|
|
792
|
+
...fieldAria(field, error, ids)
|
|
446
793
|
}
|
|
447
794
|
) });
|
|
448
795
|
}
|
|
449
796
|
function useDisplayOptions(choice) {
|
|
450
797
|
const idsKey = choice.options.map((o) => o.id).join("\0");
|
|
451
|
-
const [order, setOrder] =
|
|
452
|
-
|
|
798
|
+
const [order, setOrder] = useState5(null);
|
|
799
|
+
useEffect6(() => {
|
|
453
800
|
if (!choice.shuffleOptions) return;
|
|
454
801
|
const ids = idsKey === "" ? [] : idsKey.split("\0");
|
|
455
802
|
for (let i = ids.length - 1; i > 0; i--) {
|
|
@@ -463,32 +810,54 @@ function useDisplayOptions(choice) {
|
|
|
463
810
|
const shuffled = order.flatMap((id) => byId.get(id) ?? []);
|
|
464
811
|
return shuffled.length === choice.options.length ? shuffled : choice.options;
|
|
465
812
|
}
|
|
466
|
-
|
|
813
|
+
var OPTION_ICON_PATHS = {
|
|
814
|
+
thumbs_up: "M7 10v10M7 10l4.8-6.1a2 2 0 0 1 3.5 1.7L14.6 10h4.8a2.1 2.1 0 0 1 2 2.5l-1.1 5.4A3 3 0 0 1 17.4 20H7M3 10h4v10H3z",
|
|
815
|
+
thumbs_down: "M7 14V4M7 14l4.8 6.1a2 2 0 0 0 3.5-1.7L14.6 14h4.8a2.1 2.1 0 0 0 2-2.5l-1.1-5.4A3 3 0 0 0 17.4 4H7M3 4h4v10H3z"
|
|
816
|
+
};
|
|
817
|
+
function ChoiceIcon({ icon }) {
|
|
818
|
+
if (!icon) return null;
|
|
819
|
+
return /* @__PURE__ */ jsx4(
|
|
820
|
+
"svg",
|
|
821
|
+
{
|
|
822
|
+
className: "fillo-option-icon",
|
|
823
|
+
viewBox: "0 0 24 24",
|
|
824
|
+
fill: "none",
|
|
825
|
+
stroke: "currentColor",
|
|
826
|
+
strokeWidth: "1.8",
|
|
827
|
+
strokeLinecap: "round",
|
|
828
|
+
strokeLinejoin: "round",
|
|
829
|
+
"aria-hidden": "true",
|
|
830
|
+
children: /* @__PURE__ */ jsx4("path", { d: OPTION_ICON_PATHS[icon] })
|
|
831
|
+
}
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
835
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
467
836
|
const choice = field;
|
|
468
837
|
const options = useDisplayOptions(choice);
|
|
469
838
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
470
|
-
const [otherOn, setOtherOn] =
|
|
471
|
-
const otherActive =
|
|
472
|
-
return /* @__PURE__ */
|
|
839
|
+
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
840
|
+
const otherActive = isOtherValue || value === "" && otherOn;
|
|
841
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
|
|
473
842
|
"div",
|
|
474
843
|
{
|
|
475
844
|
className: "fillo-options",
|
|
476
845
|
role: "radiogroup",
|
|
477
|
-
"aria-labelledby":
|
|
478
|
-
...fieldAria(field, error),
|
|
846
|
+
"aria-labelledby": ids.labelId,
|
|
847
|
+
...fieldAria(field, error, ids),
|
|
479
848
|
children: [
|
|
480
|
-
options.map((opt) => /* @__PURE__ */
|
|
849
|
+
options.map((opt) => /* @__PURE__ */ jsxs4(
|
|
481
850
|
"label",
|
|
482
851
|
{
|
|
483
852
|
"data-option": opt.id,
|
|
484
|
-
className: `fillo-option${value === opt.id ? " fillo-option--selected" : ""}`,
|
|
853
|
+
className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
|
|
485
854
|
children: [
|
|
486
|
-
/* @__PURE__ */
|
|
855
|
+
/* @__PURE__ */ jsx4(
|
|
487
856
|
"input",
|
|
488
857
|
{
|
|
489
858
|
type: "radio",
|
|
490
859
|
className: "fillo-option-input",
|
|
491
|
-
name:
|
|
860
|
+
name: ids.name,
|
|
492
861
|
checked: value === opt.id,
|
|
493
862
|
onChange: () => {
|
|
494
863
|
setOtherOn(false);
|
|
@@ -496,66 +865,79 @@ function SingleChoice({ field, value, error, setValue }) {
|
|
|
496
865
|
}
|
|
497
866
|
}
|
|
498
867
|
),
|
|
499
|
-
/* @__PURE__ */
|
|
868
|
+
/* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
|
|
869
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
|
|
500
870
|
]
|
|
501
871
|
},
|
|
502
872
|
opt.id
|
|
503
873
|
)),
|
|
504
|
-
choice.allowOther &&
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
874
|
+
choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
|
|
875
|
+
// toggle the radio. The radio + "Other" text keep their own <label>.
|
|
876
|
+
/* @__PURE__ */ jsxs4(
|
|
877
|
+
"div",
|
|
878
|
+
{
|
|
879
|
+
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
880
|
+
children: [
|
|
881
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
882
|
+
/* @__PURE__ */ jsx4(
|
|
883
|
+
"input",
|
|
884
|
+
{
|
|
885
|
+
type: "radio",
|
|
886
|
+
className: "fillo-option-input",
|
|
887
|
+
name: ids.name,
|
|
888
|
+
checked: otherActive,
|
|
889
|
+
onChange: () => {
|
|
890
|
+
setOtherOn(true);
|
|
891
|
+
setValue("");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
),
|
|
895
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
|
|
896
|
+
] }),
|
|
897
|
+
otherActive && /* @__PURE__ */ jsx4(
|
|
898
|
+
"input",
|
|
899
|
+
{
|
|
900
|
+
type: "text",
|
|
901
|
+
className: "fillo-input fillo-other-input",
|
|
902
|
+
"aria-label": "Other \u2014 please specify",
|
|
903
|
+
placeholder: "Your answer",
|
|
904
|
+
autoFocus: !isOtherValue,
|
|
905
|
+
value: isOtherValue ? String(value) : "",
|
|
906
|
+
onChange: (e) => setValue(e.target.value)
|
|
907
|
+
}
|
|
908
|
+
)
|
|
909
|
+
]
|
|
910
|
+
}
|
|
911
|
+
)
|
|
531
912
|
]
|
|
532
913
|
}
|
|
533
914
|
) });
|
|
534
915
|
}
|
|
535
|
-
function MultiChoice({ field, value, error, setValue }) {
|
|
916
|
+
function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
917
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
536
918
|
const choice = field;
|
|
537
919
|
const options = useDisplayOptions(choice);
|
|
538
|
-
const
|
|
920
|
+
const optionIds = new Set(choice.options.map((o) => o.id));
|
|
539
921
|
const selected = Array.isArray(value) ? value : [];
|
|
540
|
-
const otherText = selected.find((v) => !
|
|
541
|
-
const [otherOn, setOtherOn] =
|
|
542
|
-
const otherActive =
|
|
922
|
+
const otherText = selected.find((v) => !optionIds.has(v));
|
|
923
|
+
const [otherOn, setOtherOn] = useState5(otherText !== void 0);
|
|
924
|
+
const otherActive = otherText !== void 0 || otherOn;
|
|
543
925
|
const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
|
|
544
|
-
return /* @__PURE__ */
|
|
926
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
|
|
545
927
|
"div",
|
|
546
928
|
{
|
|
547
929
|
className: "fillo-options",
|
|
548
930
|
role: "group",
|
|
549
|
-
"aria-labelledby":
|
|
550
|
-
...fieldAria(field, error),
|
|
931
|
+
"aria-labelledby": ids.labelId,
|
|
932
|
+
...fieldAria(field, error, ids),
|
|
551
933
|
children: [
|
|
552
|
-
options.map((opt) => /* @__PURE__ */
|
|
934
|
+
options.map((opt) => /* @__PURE__ */ jsxs4(
|
|
553
935
|
"label",
|
|
554
936
|
{
|
|
555
937
|
"data-option": opt.id,
|
|
556
|
-
className: `fillo-option${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
|
|
938
|
+
className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
|
|
557
939
|
children: [
|
|
558
|
-
/* @__PURE__ */
|
|
940
|
+
/* @__PURE__ */ jsx4(
|
|
559
941
|
"input",
|
|
560
942
|
{
|
|
561
943
|
type: "checkbox",
|
|
@@ -564,84 +946,101 @@ function MultiChoice({ field, value, error, setValue }) {
|
|
|
564
946
|
onChange: () => toggle(opt.id)
|
|
565
947
|
}
|
|
566
948
|
),
|
|
567
|
-
/* @__PURE__ */
|
|
949
|
+
/* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
|
|
950
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
|
|
568
951
|
]
|
|
569
952
|
},
|
|
570
953
|
opt.id
|
|
571
954
|
)),
|
|
572
|
-
choice.allowOther &&
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
955
|
+
choice.allowOther && // A <div>, not a <label>, so editing the free-text input below can't
|
|
956
|
+
// toggle the checkbox. The checkbox + "Other" text keep their own <label>.
|
|
957
|
+
/* @__PURE__ */ jsxs4(
|
|
958
|
+
"div",
|
|
959
|
+
{
|
|
960
|
+
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
961
|
+
children: [
|
|
962
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
963
|
+
/* @__PURE__ */ jsx4(
|
|
964
|
+
"input",
|
|
965
|
+
{
|
|
966
|
+
type: "checkbox",
|
|
967
|
+
className: "fillo-option-input",
|
|
968
|
+
checked: otherActive,
|
|
969
|
+
onChange: () => {
|
|
970
|
+
if (otherActive) {
|
|
971
|
+
setOtherOn(false);
|
|
972
|
+
setValue(selected.filter((v) => optionIds.has(v)));
|
|
973
|
+
} else {
|
|
974
|
+
setOtherOn(true);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
),
|
|
979
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
|
|
980
|
+
] }),
|
|
981
|
+
otherActive && /* @__PURE__ */ jsx4(
|
|
982
|
+
"input",
|
|
983
|
+
{
|
|
984
|
+
type: "text",
|
|
985
|
+
className: "fillo-input fillo-other-input",
|
|
986
|
+
"aria-label": "Other \u2014 please specify",
|
|
987
|
+
placeholder: "Your answer",
|
|
988
|
+
autoFocus: otherText === void 0,
|
|
989
|
+
value: otherText ?? "",
|
|
990
|
+
onChange: (e) => {
|
|
991
|
+
const rest = selected.filter((v) => optionIds.has(v));
|
|
992
|
+
setValue(e.target.value ? [...rest, e.target.value] : rest);
|
|
993
|
+
}
|
|
585
994
|
}
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
otherActive && /* @__PURE__ */ jsx3(
|
|
591
|
-
"input",
|
|
592
|
-
{
|
|
593
|
-
type: "text",
|
|
594
|
-
className: "fillo-input fillo-other-input",
|
|
595
|
-
placeholder: "Your answer",
|
|
596
|
-
autoFocus: otherText === void 0,
|
|
597
|
-
value: otherText ?? "",
|
|
598
|
-
onChange: (e) => {
|
|
599
|
-
const rest = selected.filter((v) => ids.has(v));
|
|
600
|
-
setValue(e.target.value ? [...rest, e.target.value] : rest);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
)
|
|
604
|
-
] })
|
|
995
|
+
)
|
|
996
|
+
]
|
|
997
|
+
}
|
|
998
|
+
)
|
|
605
999
|
]
|
|
606
1000
|
}
|
|
607
1001
|
) });
|
|
608
1002
|
}
|
|
609
1003
|
var OTHER_SENTINEL = "__fw_other__";
|
|
610
|
-
function Dropdown({ field, value, error, setValue }) {
|
|
1004
|
+
function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
1005
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
611
1006
|
const choice = field;
|
|
612
1007
|
const options = useDisplayOptions(choice);
|
|
613
1008
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
614
|
-
const [otherOn, setOtherOn] =
|
|
615
|
-
const otherActive =
|
|
616
|
-
return /* @__PURE__ */
|
|
617
|
-
/* @__PURE__ */
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
1009
|
+
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
1010
|
+
const otherActive = isOtherValue || value === "" && otherOn;
|
|
1011
|
+
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1012
|
+
/* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
|
|
1013
|
+
/* @__PURE__ */ jsxs4(
|
|
1014
|
+
"select",
|
|
1015
|
+
{
|
|
1016
|
+
id: ids.inputId,
|
|
1017
|
+
className: "fillo-input fillo-select",
|
|
1018
|
+
...fieldAria(field, error, ids),
|
|
1019
|
+
value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
|
|
1020
|
+
onChange: (e) => {
|
|
1021
|
+
if (e.target.value === OTHER_SENTINEL) {
|
|
1022
|
+
setOtherOn(true);
|
|
1023
|
+
setValue("");
|
|
1024
|
+
} else {
|
|
1025
|
+
setOtherOn(false);
|
|
1026
|
+
setValue(e.target.value || null);
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
children: [
|
|
1030
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? "Choose\u2026" }),
|
|
1031
|
+
options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
|
|
1032
|
+
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: "Other\u2026" })
|
|
1033
|
+
]
|
|
1034
|
+
}
|
|
1035
|
+
),
|
|
1036
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-select-icon", "aria-hidden": "true" })
|
|
1037
|
+
] }),
|
|
1038
|
+
otherActive && /* @__PURE__ */ jsx4(
|
|
641
1039
|
"input",
|
|
642
1040
|
{
|
|
643
1041
|
type: "text",
|
|
644
1042
|
className: "fillo-input fillo-other-input fillo-other-input--block",
|
|
1043
|
+
"aria-label": "Other \u2014 please specify",
|
|
645
1044
|
placeholder: "Your answer",
|
|
646
1045
|
autoFocus: !isOtherValue,
|
|
647
1046
|
value: isOtherValue ? String(value) : "",
|
|
@@ -650,76 +1049,180 @@ function Dropdown({ field, value, error, setValue }) {
|
|
|
650
1049
|
)
|
|
651
1050
|
] });
|
|
652
1051
|
}
|
|
653
|
-
function Checkbox({ field, value, error, setValue }) {
|
|
654
|
-
|
|
1052
|
+
function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
1053
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1054
|
+
const checkbox = field;
|
|
1055
|
+
if (checkbox.appearance === "toggle") {
|
|
1056
|
+
return /* @__PURE__ */ jsxs4(
|
|
1057
|
+
"div",
|
|
1058
|
+
{
|
|
1059
|
+
className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
|
|
1060
|
+
"data-field": field.id,
|
|
1061
|
+
children: [
|
|
1062
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", children: [
|
|
1063
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
|
|
1064
|
+
field.label,
|
|
1065
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
|
|
1066
|
+
] }) }),
|
|
1067
|
+
/* @__PURE__ */ jsx4(
|
|
1068
|
+
"input",
|
|
1069
|
+
{
|
|
1070
|
+
id: ids.inputId,
|
|
1071
|
+
type: "checkbox",
|
|
1072
|
+
role: "switch",
|
|
1073
|
+
className: "fillo-toggle-input",
|
|
1074
|
+
checked: value === true,
|
|
1075
|
+
onChange: (e) => setValue(e.target.checked),
|
|
1076
|
+
"aria-checked": value === true,
|
|
1077
|
+
...fieldAria(field, error, ids)
|
|
1078
|
+
}
|
|
1079
|
+
),
|
|
1080
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
|
|
1081
|
+
] }),
|
|
1082
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
1083
|
+
error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
1084
|
+
]
|
|
1085
|
+
}
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
return /* @__PURE__ */ jsxs4(
|
|
655
1089
|
"div",
|
|
656
1090
|
{
|
|
657
1091
|
className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
|
|
658
1092
|
"data-field": field.id,
|
|
659
1093
|
children: [
|
|
660
|
-
/* @__PURE__ */
|
|
661
|
-
/* @__PURE__ */
|
|
1094
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", children: [
|
|
1095
|
+
/* @__PURE__ */ jsx4(
|
|
662
1096
|
"input",
|
|
663
1097
|
{
|
|
664
|
-
id:
|
|
1098
|
+
id: ids.inputId,
|
|
665
1099
|
type: "checkbox",
|
|
666
1100
|
className: "fillo-option-input",
|
|
667
1101
|
checked: value === true,
|
|
668
1102
|
onChange: (e) => setValue(e.target.checked),
|
|
669
|
-
...fieldAria(field, error)
|
|
1103
|
+
...fieldAria(field, error, ids)
|
|
670
1104
|
}
|
|
671
1105
|
),
|
|
672
|
-
/* @__PURE__ */
|
|
1106
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
|
|
673
1107
|
field.label,
|
|
674
|
-
!field.required && /* @__PURE__ */
|
|
1108
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
|
|
675
1109
|
] })
|
|
676
1110
|
] }),
|
|
677
|
-
field.description && /* @__PURE__ */
|
|
678
|
-
error && /* @__PURE__ */
|
|
1111
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
1112
|
+
error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
679
1113
|
]
|
|
680
1114
|
}
|
|
681
1115
|
);
|
|
682
1116
|
}
|
|
683
|
-
function
|
|
1117
|
+
function radiogroupKeyDown(e, steps, current, setValue) {
|
|
1118
|
+
const forward = e.key === "ArrowRight" || e.key === "ArrowDown";
|
|
1119
|
+
const backward = e.key === "ArrowLeft" || e.key === "ArrowUp";
|
|
1120
|
+
if (!forward && !backward) return;
|
|
1121
|
+
e.preventDefault();
|
|
1122
|
+
const index = current == null ? -1 : steps.indexOf(current);
|
|
1123
|
+
let next;
|
|
1124
|
+
if (index === -1) {
|
|
1125
|
+
next = forward ? 0 : steps.length - 1;
|
|
1126
|
+
} else {
|
|
1127
|
+
next = forward ? index + 1 : index - 1;
|
|
1128
|
+
next = (next + steps.length) % steps.length;
|
|
1129
|
+
}
|
|
1130
|
+
const value = steps[next];
|
|
1131
|
+
if (value === void 0) return;
|
|
1132
|
+
setValue(value);
|
|
1133
|
+
const group = e.currentTarget;
|
|
1134
|
+
const buttons = group.querySelectorAll('[role="radio"]');
|
|
1135
|
+
buttons[next]?.focus();
|
|
1136
|
+
}
|
|
1137
|
+
function rovingIndex(steps, current) {
|
|
1138
|
+
if (current == null) return 0;
|
|
1139
|
+
const i = steps.indexOf(current);
|
|
1140
|
+
return i === -1 ? 0 : i;
|
|
1141
|
+
}
|
|
1142
|
+
function Rating({ field, value, error, setValue, ids: providedIds }) {
|
|
1143
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
684
1144
|
const max = field.max ?? 5;
|
|
685
1145
|
const current = typeof value === "number" ? value : 0;
|
|
686
|
-
|
|
687
|
-
|
|
1146
|
+
const steps = Array.from({ length: max }, (_, i) => i + 1);
|
|
1147
|
+
const selected = current > 0 ? current : null;
|
|
1148
|
+
const tabbable = rovingIndex(steps, selected);
|
|
1149
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1150
|
+
"div",
|
|
688
1151
|
{
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
"aria-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
children:
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1152
|
+
className: "fillo-rating",
|
|
1153
|
+
role: "radiogroup",
|
|
1154
|
+
"aria-labelledby": ids.labelId,
|
|
1155
|
+
...fieldAria(field, error, ids),
|
|
1156
|
+
onKeyDown: (e) => radiogroupKeyDown(e, steps, selected, (n) => setValue(n)),
|
|
1157
|
+
children: steps.map((n, i) => /* @__PURE__ */ jsx4(
|
|
1158
|
+
"button",
|
|
1159
|
+
{
|
|
1160
|
+
type: "button",
|
|
1161
|
+
className: `fillo-star${n <= current ? " fillo-star--active" : ""}`,
|
|
1162
|
+
role: "radio",
|
|
1163
|
+
"aria-label": `${n} of ${max}`,
|
|
1164
|
+
"aria-checked": n === current,
|
|
1165
|
+
tabIndex: i === tabbable ? 0 : -1,
|
|
1166
|
+
onClick: () => setValue(n === current ? null : n),
|
|
1167
|
+
children: "\u2605"
|
|
1168
|
+
},
|
|
1169
|
+
n
|
|
1170
|
+
))
|
|
1171
|
+
}
|
|
1172
|
+
) });
|
|
698
1173
|
}
|
|
699
|
-
function LinearScale({ field, value, error, setValue }) {
|
|
1174
|
+
function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
1175
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
700
1176
|
const scale = field;
|
|
701
1177
|
const min = scale.min ?? 1;
|
|
702
1178
|
const max = scale.max ?? 10;
|
|
703
1179
|
const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
1180
|
+
const current = typeof value === "number" ? value : null;
|
|
1181
|
+
const tabbable = rovingIndex(steps, current);
|
|
1182
|
+
const hasLabels = Boolean(scale.minLabel || scale.maxLabel);
|
|
1183
|
+
const minLabelId = `${ids.inputId}-min`;
|
|
1184
|
+
const maxLabelId = `${ids.inputId}-max`;
|
|
1185
|
+
const describedBy = [
|
|
1186
|
+
scale.minLabel ? minLabelId : null,
|
|
1187
|
+
scale.maxLabel ? maxLabelId : null,
|
|
1188
|
+
field.description ? ids.descriptionId : null,
|
|
1189
|
+
error ? ids.errorId : null
|
|
1190
|
+
].filter(Boolean).join(" ") || void 0;
|
|
1191
|
+
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1192
|
+
/* @__PURE__ */ jsx4(
|
|
1193
|
+
"div",
|
|
707
1194
|
{
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
"aria-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1195
|
+
className: "fillo-scale",
|
|
1196
|
+
role: "radiogroup",
|
|
1197
|
+
"aria-labelledby": ids.labelId,
|
|
1198
|
+
"aria-invalid": error ? true : void 0,
|
|
1199
|
+
"aria-required": field.required ? true : void 0,
|
|
1200
|
+
"aria-describedby": describedBy,
|
|
1201
|
+
onKeyDown: (e) => radiogroupKeyDown(e, steps, current, (n) => setValue(n)),
|
|
1202
|
+
children: steps.map((n, i) => /* @__PURE__ */ jsx4(
|
|
1203
|
+
"button",
|
|
1204
|
+
{
|
|
1205
|
+
type: "button",
|
|
1206
|
+
className: `fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`,
|
|
1207
|
+
role: "radio",
|
|
1208
|
+
"aria-label": String(n),
|
|
1209
|
+
"aria-checked": value === n,
|
|
1210
|
+
tabIndex: i === tabbable ? 0 : -1,
|
|
1211
|
+
onClick: () => setValue(value === n ? null : n),
|
|
1212
|
+
children: n
|
|
1213
|
+
},
|
|
1214
|
+
n
|
|
1215
|
+
))
|
|
1216
|
+
}
|
|
1217
|
+
),
|
|
1218
|
+
hasLabels && /* @__PURE__ */ jsxs4("div", { className: "fillo-scale-labels", children: [
|
|
1219
|
+
/* @__PURE__ */ jsx4("span", { id: minLabelId, children: scale.minLabel }),
|
|
1220
|
+
/* @__PURE__ */ jsx4("span", { id: maxLabelId, children: scale.maxLabel })
|
|
719
1221
|
] })
|
|
720
1222
|
] });
|
|
721
1223
|
}
|
|
722
|
-
function Ranking({ field, value, error, setValue }) {
|
|
1224
|
+
function Ranking({ field, value, error, setValue, ids: providedIds }) {
|
|
1225
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
723
1226
|
const ranking = field;
|
|
724
1227
|
const answered = Array.isArray(value) ? value : [];
|
|
725
1228
|
const order = [
|
|
@@ -734,14 +1237,14 @@ function Ranking({ field, value, error, setValue }) {
|
|
|
734
1237
|
[next[index], next[target]] = [next[target], next[index]];
|
|
735
1238
|
setValue(next);
|
|
736
1239
|
}
|
|
737
|
-
return /* @__PURE__ */
|
|
1240
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
|
|
738
1241
|
const option = ranking.options.find((o) => o.id === id);
|
|
739
1242
|
if (!option) return null;
|
|
740
|
-
return /* @__PURE__ */
|
|
741
|
-
/* @__PURE__ */
|
|
742
|
-
/* @__PURE__ */
|
|
743
|
-
/* @__PURE__ */
|
|
744
|
-
/* @__PURE__ */
|
|
1243
|
+
return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
|
|
1244
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
|
|
1245
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
|
|
1246
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
|
|
1247
|
+
/* @__PURE__ */ jsx4(
|
|
745
1248
|
"button",
|
|
746
1249
|
{
|
|
747
1250
|
type: "button",
|
|
@@ -752,7 +1255,7 @@ function Ranking({ field, value, error, setValue }) {
|
|
|
752
1255
|
children: "\u2191"
|
|
753
1256
|
}
|
|
754
1257
|
),
|
|
755
|
-
/* @__PURE__ */
|
|
1258
|
+
/* @__PURE__ */ jsx4(
|
|
756
1259
|
"button",
|
|
757
1260
|
{
|
|
758
1261
|
type: "button",
|
|
@@ -767,28 +1270,39 @@ function Ranking({ field, value, error, setValue }) {
|
|
|
767
1270
|
] }, id);
|
|
768
1271
|
}) }) });
|
|
769
1272
|
}
|
|
770
|
-
function Matrix({ field, value, error, setValue }) {
|
|
1273
|
+
function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
1274
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
771
1275
|
const matrix = field;
|
|
772
1276
|
const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
773
|
-
return /* @__PURE__ */
|
|
774
|
-
/* @__PURE__ */
|
|
775
|
-
/* @__PURE__ */
|
|
776
|
-
matrix.columns.map((col) => /* @__PURE__ */
|
|
1277
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("div", { className: "fillo-matrix-wrap", children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
|
|
1278
|
+
/* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1279
|
+
/* @__PURE__ */ jsx4("th", {}),
|
|
1280
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
|
|
777
1281
|
] }) }),
|
|
778
|
-
/* @__PURE__ */
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
"
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1282
|
+
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
|
|
1283
|
+
const rowLabelId = `${ids.inputId}-${row.id}`;
|
|
1284
|
+
const answered = answers[row.id] !== void 0;
|
|
1285
|
+
const rowInvalid = Boolean(error) && field.required && !answered;
|
|
1286
|
+
return (
|
|
1287
|
+
// Each row is its own radiogroup, labelled by the row question.
|
|
1288
|
+
/* @__PURE__ */ jsxs4("tr", { role: "radiogroup", "aria-labelledby": rowLabelId, children: [
|
|
1289
|
+
/* @__PURE__ */ jsx4("th", { scope: "row", id: rowLabelId, children: row.label }),
|
|
1290
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
|
|
1291
|
+
"input",
|
|
1292
|
+
{
|
|
1293
|
+
type: "radio",
|
|
1294
|
+
className: "fillo-option-input",
|
|
1295
|
+
name: `${ids.name}-${row.id}`,
|
|
1296
|
+
"aria-label": `${row.label}: ${col.label}`,
|
|
1297
|
+
"aria-required": field.required ? true : void 0,
|
|
1298
|
+
"aria-invalid": rowInvalid ? true : void 0,
|
|
1299
|
+
checked: answers[row.id] === col.id,
|
|
1300
|
+
onChange: () => setValue({ ...answers, [row.id]: col.id })
|
|
1301
|
+
}
|
|
1302
|
+
) }, col.id))
|
|
1303
|
+
] }, row.id)
|
|
1304
|
+
);
|
|
1305
|
+
}) })
|
|
792
1306
|
] }) }) });
|
|
793
1307
|
}
|
|
794
1308
|
function Hidden() {
|
|
@@ -798,7 +1312,7 @@ var DEFAULT_COMPONENTS = {
|
|
|
798
1312
|
short_text: TextInput,
|
|
799
1313
|
email: TextInput,
|
|
800
1314
|
url: TextInput,
|
|
801
|
-
phone:
|
|
1315
|
+
phone: PhoneField,
|
|
802
1316
|
number: TextInput,
|
|
803
1317
|
date: TextInput,
|
|
804
1318
|
long_text: LongText,
|
|
@@ -817,21 +1331,66 @@ var DEFAULT_COMPONENTS = {
|
|
|
817
1331
|
function ContentRenderer({ block }) {
|
|
818
1332
|
switch (block.kind) {
|
|
819
1333
|
case "heading":
|
|
820
|
-
return /* @__PURE__ */
|
|
1334
|
+
return /* @__PURE__ */ jsx4("h3", { className: "fillo-heading", children: block.text });
|
|
821
1335
|
case "paragraph":
|
|
822
|
-
return /* @__PURE__ */
|
|
1336
|
+
return /* @__PURE__ */ jsx4("p", { className: "fillo-paragraph", children: block.text });
|
|
823
1337
|
case "divider":
|
|
824
|
-
return /* @__PURE__ */
|
|
1338
|
+
return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
|
|
825
1339
|
}
|
|
826
1340
|
}
|
|
827
|
-
function
|
|
828
|
-
|
|
829
|
-
api
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
1341
|
+
function shouldAutoSubmit(field, value, api) {
|
|
1342
|
+
if (api.form.settings.submitMode !== "auto") return false;
|
|
1343
|
+
if (!api.isLastPage || api.status !== "idle" || api.uploading) return false;
|
|
1344
|
+
const hasDiscreteAnswer = (() => {
|
|
1345
|
+
switch (field.kind) {
|
|
1346
|
+
case "select":
|
|
1347
|
+
case "dropdown":
|
|
1348
|
+
return typeof value === "string" && field.options.some((option) => option.id === value);
|
|
1349
|
+
case "checkbox":
|
|
1350
|
+
return value === true;
|
|
1351
|
+
case "rating":
|
|
1352
|
+
case "linear_scale":
|
|
1353
|
+
return typeof value === "number";
|
|
1354
|
+
default:
|
|
1355
|
+
return false;
|
|
1356
|
+
}
|
|
1357
|
+
})();
|
|
1358
|
+
if (!hasDiscreteAnswer) return false;
|
|
1359
|
+
if (value === api.data[field.id]) return false;
|
|
1360
|
+
return validateResponse(api.form, { ...api.data, [field.id]: value }).ok;
|
|
1361
|
+
}
|
|
1362
|
+
function blockHasPiping(block) {
|
|
1363
|
+
const has = (s) => typeof s === "string" && s.includes("{{");
|
|
1364
|
+
switch (block.kind) {
|
|
1365
|
+
case "heading":
|
|
1366
|
+
case "paragraph":
|
|
1367
|
+
return has(block.text);
|
|
1368
|
+
case "divider":
|
|
1369
|
+
return false;
|
|
1370
|
+
default:
|
|
1371
|
+
return has(block.label) || has(block.description);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
function blockPropsEqual(prev, next) {
|
|
1375
|
+
if (prev.block !== next.block || prev.components !== next.components || prev.customComponents !== next.customComponents) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
const id = next.block.id;
|
|
1379
|
+
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1380
|
+
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1381
|
+
const needsWiderState = next.block.kind === "custom" || blockHasPiping(prev.block) || blockHasPiping(next.block);
|
|
1382
|
+
if (needsWiderState && prev.api.data !== next.api.data) return false;
|
|
1383
|
+
if (next.api.form.settings.submitMode === "auto") {
|
|
1384
|
+
if (prev.api.data !== next.api.data || prev.api.status !== next.api.status || prev.api.uploading !== next.api.uploading || prev.api.isLastPage !== next.api.isLastPage) {
|
|
1385
|
+
return false;
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
return true;
|
|
1389
|
+
}
|
|
1390
|
+
function BlockRendererInner({ block, api, components, customComponents }) {
|
|
833
1391
|
block = pipeBlock(block, api.data, api.form);
|
|
834
|
-
|
|
1392
|
+
const ids = useFilloFieldIds(block.id);
|
|
1393
|
+
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
835
1394
|
let Component;
|
|
836
1395
|
if (block.kind === "custom") {
|
|
837
1396
|
Component = customComponents?.[block.component] ?? components?.custom;
|
|
@@ -847,17 +1406,22 @@ function BlockRenderer({
|
|
|
847
1406
|
} else {
|
|
848
1407
|
Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
|
|
849
1408
|
}
|
|
850
|
-
return /* @__PURE__ */
|
|
1409
|
+
return /* @__PURE__ */ jsx4(
|
|
851
1410
|
Component,
|
|
852
1411
|
{
|
|
853
1412
|
field: block,
|
|
854
1413
|
value: api.data[block.id],
|
|
855
1414
|
error: api.errors[block.id],
|
|
856
|
-
setValue: (v) =>
|
|
857
|
-
|
|
1415
|
+
setValue: (v) => {
|
|
1416
|
+
api.setValue(block.id, v);
|
|
1417
|
+
if (shouldAutoSubmit(block, v, api)) void api.submit();
|
|
1418
|
+
},
|
|
1419
|
+
api,
|
|
1420
|
+
ids
|
|
858
1421
|
}
|
|
859
1422
|
);
|
|
860
1423
|
}
|
|
1424
|
+
var BlockRenderer = memo(BlockRendererInner, blockPropsEqual);
|
|
861
1425
|
function FormField({
|
|
862
1426
|
id,
|
|
863
1427
|
components,
|
|
@@ -866,7 +1430,7 @@ function FormField({
|
|
|
866
1430
|
const api = useFillo();
|
|
867
1431
|
const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
|
|
868
1432
|
if (!block) return null;
|
|
869
|
-
return /* @__PURE__ */
|
|
1433
|
+
return /* @__PURE__ */ jsx4(
|
|
870
1434
|
BlockRenderer,
|
|
871
1435
|
{
|
|
872
1436
|
block,
|
|
@@ -878,34 +1442,70 @@ function FormField({
|
|
|
878
1442
|
}
|
|
879
1443
|
|
|
880
1444
|
// src/FilloForm.tsx
|
|
881
|
-
import { Fragment as Fragment2, jsx as
|
|
882
|
-
function
|
|
883
|
-
|
|
1445
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1446
|
+
function themeProps(theme) {
|
|
1447
|
+
const safeTheme = normalizeFormTheme(theme);
|
|
1448
|
+
if (!safeTheme) return {};
|
|
884
1449
|
const style = {};
|
|
885
|
-
if (
|
|
886
|
-
if (
|
|
887
|
-
if (
|
|
888
|
-
if (
|
|
889
|
-
if (
|
|
890
|
-
|
|
1450
|
+
if (safeTheme.primary) style["--fillo-primary"] = safeTheme.primary;
|
|
1451
|
+
if (safeTheme.background) style["--fillo-bg"] = safeTheme.background;
|
|
1452
|
+
if (safeTheme.text) style["--fillo-text"] = safeTheme.text;
|
|
1453
|
+
if (safeTheme.radius) style["--fillo-radius"] = safeTheme.radius;
|
|
1454
|
+
if (safeTheme.fontFamily) style["--fillo-font"] = safeTheme.fontFamily;
|
|
1455
|
+
const hasColorTheme = Boolean(safeTheme.primary || safeTheme.background || safeTheme.text);
|
|
1456
|
+
return {
|
|
1457
|
+
...Object.keys(style).length ? { style } : {},
|
|
1458
|
+
...safeTheme.colorScheme ? { "data-fillo-color-scheme": safeTheme.colorScheme } : {},
|
|
1459
|
+
...hasColorTheme ? { "data-fillo-has-color-theme": "" } : {}
|
|
1460
|
+
};
|
|
1461
|
+
}
|
|
1462
|
+
function safeHttpUrl(href) {
|
|
1463
|
+
try {
|
|
1464
|
+
if (!href) return null;
|
|
1465
|
+
const url = new URL(href);
|
|
1466
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
1467
|
+
return url.toString();
|
|
1468
|
+
} catch {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
function badgeUrl(href) {
|
|
1473
|
+
const url = new URL(safeHttpUrl(href) ?? "https://fillo.so");
|
|
1474
|
+
url.searchParams.set("utm_source", "form_badge");
|
|
1475
|
+
return url.toString();
|
|
1476
|
+
}
|
|
1477
|
+
function focusFirstInvalid(formEl) {
|
|
1478
|
+
if (!formEl || typeof window === "undefined") return;
|
|
1479
|
+
const run = () => {
|
|
1480
|
+
const target = formEl.querySelector('[aria-invalid="true"]');
|
|
1481
|
+
target?.focus?.();
|
|
1482
|
+
};
|
|
1483
|
+
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
1484
|
+
else setTimeout(run, 0);
|
|
1485
|
+
}
|
|
1486
|
+
function isAutoSubmitBlock(block) {
|
|
1487
|
+
return isField2(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
1488
|
+
}
|
|
1489
|
+
function needsExplicitSubmit(blocks) {
|
|
1490
|
+
return blocks.some((block) => isField2(block) && block.kind !== "hidden" && !isAutoSubmitBlock(block));
|
|
891
1491
|
}
|
|
892
1492
|
function FilloForm(props) {
|
|
893
1493
|
const { form, formId } = props;
|
|
894
|
-
const client =
|
|
1494
|
+
const client = useMemo4(
|
|
895
1495
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
896
1496
|
[props.client, formId]
|
|
897
1497
|
);
|
|
898
|
-
const [fetched, setFetched] =
|
|
899
|
-
const [fetchedTheme, setFetchedTheme] =
|
|
900
|
-
const [loadError, setLoadError] =
|
|
901
|
-
const [closed, setClosed] =
|
|
902
|
-
const [fetchedBranding, setFetchedBranding] =
|
|
1498
|
+
const [fetched, setFetched] = useState6(null);
|
|
1499
|
+
const [fetchedTheme, setFetchedTheme] = useState6(null);
|
|
1500
|
+
const [loadError, setLoadError] = useState6(null);
|
|
1501
|
+
const [closed, setClosed] = useState6(false);
|
|
1502
|
+
const [fetchedBranding, setFetchedBranding] = useState6(null);
|
|
903
1503
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
904
1504
|
const inlineSchema = codeForm ? codeForm.schema : form;
|
|
905
1505
|
const onError = props.onError;
|
|
906
1506
|
const syncedFormId = useCodeFormSync(form, client, onError);
|
|
907
1507
|
const needsFetch = !inlineSchema && Boolean(client && formId);
|
|
908
|
-
|
|
1508
|
+
useEffect7(() => {
|
|
909
1509
|
if (!needsFetch || !client || !formId) return;
|
|
910
1510
|
let cancelled = false;
|
|
911
1511
|
setFetched(null);
|
|
@@ -926,35 +1526,42 @@ function FilloForm(props) {
|
|
|
926
1526
|
cancelled = true;
|
|
927
1527
|
};
|
|
928
1528
|
}, [needsFetch, client, formId, onError]);
|
|
929
|
-
const
|
|
1529
|
+
const rawSchema = inlineSchema ?? fetched;
|
|
1530
|
+
const normalized = useMemo4(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1531
|
+
const schema = normalized?.ok ? normalized.schema : null;
|
|
930
1532
|
const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
|
|
1533
|
+
if (normalized && !normalized.ok) {
|
|
1534
|
+
const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1535
|
+
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
1536
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: "This form could not be rendered." });
|
|
1537
|
+
}
|
|
931
1538
|
if (loadError) {
|
|
932
1539
|
if (props.renderError) {
|
|
933
|
-
return /* @__PURE__ */
|
|
1540
|
+
return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
|
|
934
1541
|
}
|
|
935
1542
|
const message = loadError.status === 404 ? "Form not found \u2014 check the form id and that it's published." : loadError.status === 0 ? "Couldn't reach the server \u2014 check your connection or CORS." : "This form could not be loaded.";
|
|
936
|
-
return /* @__PURE__ */
|
|
1543
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
|
|
937
1544
|
}
|
|
938
1545
|
if (closed) {
|
|
939
|
-
return /* @__PURE__ */
|
|
1546
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: "This form is no longer accepting responses." }) });
|
|
940
1547
|
}
|
|
941
1548
|
if (!schema) {
|
|
942
|
-
return /* @__PURE__ */
|
|
1549
|
+
return /* @__PURE__ */ jsxs5(
|
|
943
1550
|
"div",
|
|
944
1551
|
{
|
|
945
1552
|
className: `fillo-form fillo-form--loading ${props.className ?? ""}`,
|
|
946
1553
|
"aria-busy": "true",
|
|
947
|
-
|
|
1554
|
+
...themeProps(theme),
|
|
948
1555
|
children: [
|
|
949
|
-
/* @__PURE__ */
|
|
950
|
-
/* @__PURE__ */
|
|
951
|
-
/* @__PURE__ */
|
|
1556
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-skeleton" }),
|
|
1557
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-skeleton" }),
|
|
1558
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-skeleton fillo-skeleton--short" })
|
|
952
1559
|
]
|
|
953
1560
|
}
|
|
954
1561
|
);
|
|
955
1562
|
}
|
|
956
|
-
const poweredBy =
|
|
957
|
-
return /* @__PURE__ */
|
|
1563
|
+
const poweredBy = fetchedBranding?.poweredBy ?? true;
|
|
1564
|
+
return /* @__PURE__ */ jsx5(
|
|
958
1565
|
ResolvedForm,
|
|
959
1566
|
{
|
|
960
1567
|
...props,
|
|
@@ -969,24 +1576,23 @@ function FilloForm(props) {
|
|
|
969
1576
|
);
|
|
970
1577
|
}
|
|
971
1578
|
function PoweredBy({ label, href }) {
|
|
972
|
-
|
|
973
|
-
const url = `${base}${base.includes("?") ? "&" : "?"}utm_source=form_badge`;
|
|
974
|
-
return /* @__PURE__ */ jsx4("a", { className: "fillo-powered", href: url, target: "_blank", rel: "noopener", children: label ?? "Powered by Fillo" });
|
|
1579
|
+
return /* @__PURE__ */ jsx5("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
|
|
975
1580
|
}
|
|
976
1581
|
function isDevEnv() {
|
|
977
1582
|
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
978
1583
|
}
|
|
979
1584
|
function DevNoClientNotice() {
|
|
980
|
-
return /* @__PURE__ */
|
|
981
|
-
/* @__PURE__ */
|
|
1585
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
1586
|
+
/* @__PURE__ */ jsx5("strong", { children: "No Fillo client connected." }),
|
|
982
1587
|
" This form is render-only and won\u2019t save responses. Pass a ",
|
|
983
|
-
/* @__PURE__ */
|
|
1588
|
+
/* @__PURE__ */ jsx5("code", { children: "client" }),
|
|
984
1589
|
" to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
|
|
985
1590
|
] });
|
|
986
1591
|
}
|
|
987
1592
|
function ResolvedForm(props) {
|
|
988
1593
|
const { schema, theme } = props;
|
|
989
|
-
const
|
|
1594
|
+
const instanceId = useId2().replace(/:/g, "");
|
|
1595
|
+
const hpRef = useRef5("");
|
|
990
1596
|
const api = useFilloController({
|
|
991
1597
|
form: schema,
|
|
992
1598
|
formId: props.formId,
|
|
@@ -997,40 +1603,46 @@ function ResolvedForm(props) {
|
|
|
997
1603
|
skipValidation: props.skipValidation,
|
|
998
1604
|
getHoneypot: () => hpRef.current
|
|
999
1605
|
});
|
|
1000
|
-
const
|
|
1606
|
+
const themed = useMemo4(() => themeProps(theme), [theme]);
|
|
1001
1607
|
const settings = schema.settings;
|
|
1002
1608
|
const submitted = api.status === "submitted";
|
|
1003
|
-
|
|
1609
|
+
useEffect7(() => {
|
|
1004
1610
|
if (!submitted || !settings.redirectUrl || typeof window === "undefined") return;
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1611
|
+
const url = safeHttpUrl(settings.redirectUrl);
|
|
1612
|
+
if (url) window.location.assign(url);
|
|
1008
1613
|
}, [submitted, settings.redirectUrl]);
|
|
1009
1614
|
if (api.status === "submitted") {
|
|
1010
|
-
return /* @__PURE__ */
|
|
1011
|
-
props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */
|
|
1012
|
-
/* @__PURE__ */
|
|
1013
|
-
/* @__PURE__ */
|
|
1014
|
-
/* @__PURE__ */
|
|
1615
|
+
return /* @__PURE__ */ jsxs5("div", { className: `fillo-form fillo-form--success ${props.className ?? ""}`, ...themed, children: [
|
|
1616
|
+
props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5("div", { className: "fillo-success", children: [
|
|
1617
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
|
|
1618
|
+
/* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? "Thanks!" }),
|
|
1619
|
+
/* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? "Your response has been recorded." })
|
|
1015
1620
|
] }),
|
|
1016
|
-
props.poweredBy && /* @__PURE__ */
|
|
1621
|
+
props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1017
1622
|
] });
|
|
1018
1623
|
}
|
|
1019
1624
|
const multiPage = api.pageCount > 1;
|
|
1020
|
-
|
|
1625
|
+
const autoSubmit = settings.submitMode === "auto";
|
|
1626
|
+
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(api.blocks);
|
|
1627
|
+
const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
|
|
1628
|
+
return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
|
|
1021
1629
|
"form",
|
|
1022
1630
|
{
|
|
1023
1631
|
className: `fillo-form ${props.className ?? ""}`,
|
|
1024
|
-
|
|
1632
|
+
...themed,
|
|
1025
1633
|
noValidate: true,
|
|
1026
1634
|
onSubmit: (e) => {
|
|
1027
1635
|
e.preventDefault();
|
|
1028
|
-
|
|
1029
|
-
|
|
1636
|
+
const formEl = e.currentTarget;
|
|
1637
|
+
if (api.isLastPage) void api.submit().then(() => focusFirstInvalid(formEl));
|
|
1638
|
+
else {
|
|
1639
|
+
api.next();
|
|
1640
|
+
focusFirstInvalid(formEl);
|
|
1641
|
+
}
|
|
1030
1642
|
},
|
|
1031
1643
|
children: [
|
|
1032
|
-
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */
|
|
1033
|
-
multiPage && settings.showProgress !== false && /* @__PURE__ */
|
|
1644
|
+
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
1645
|
+
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
|
|
1034
1646
|
"div",
|
|
1035
1647
|
{
|
|
1036
1648
|
className: "fillo-progress-track",
|
|
@@ -1038,7 +1650,7 @@ function ResolvedForm(props) {
|
|
|
1038
1650
|
"aria-valuemin": 0,
|
|
1039
1651
|
"aria-valuemax": api.pageCount,
|
|
1040
1652
|
"aria-valuenow": api.pageIndex + 1,
|
|
1041
|
-
children: /* @__PURE__ */
|
|
1653
|
+
children: /* @__PURE__ */ jsx5(
|
|
1042
1654
|
"div",
|
|
1043
1655
|
{
|
|
1044
1656
|
className: "fillo-progress-fill",
|
|
@@ -1047,12 +1659,12 @@ function ResolvedForm(props) {
|
|
|
1047
1659
|
)
|
|
1048
1660
|
}
|
|
1049
1661
|
),
|
|
1050
|
-
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */
|
|
1051
|
-
/* @__PURE__ */
|
|
1052
|
-
schema.description && /* @__PURE__ */
|
|
1662
|
+
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: "fillo-header", children: [
|
|
1663
|
+
/* @__PURE__ */ jsx5("h1", { className: "fillo-title", children: schema.title }),
|
|
1664
|
+
schema.description && /* @__PURE__ */ jsx5("p", { className: "fillo-form-description", children: schema.description })
|
|
1053
1665
|
] }),
|
|
1054
|
-
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */
|
|
1055
|
-
/* @__PURE__ */
|
|
1666
|
+
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: "fillo-page-title", children: api.page.title }),
|
|
1667
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-blocks", children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
|
|
1056
1668
|
BlockRenderer,
|
|
1057
1669
|
{
|
|
1058
1670
|
block,
|
|
@@ -1062,7 +1674,7 @@ function ResolvedForm(props) {
|
|
|
1062
1674
|
},
|
|
1063
1675
|
block.id
|
|
1064
1676
|
)) }),
|
|
1065
|
-
/* @__PURE__ */
|
|
1677
|
+
/* @__PURE__ */ jsx5(
|
|
1066
1678
|
"input",
|
|
1067
1679
|
{
|
|
1068
1680
|
type: "text",
|
|
@@ -1077,9 +1689,10 @@ function ResolvedForm(props) {
|
|
|
1077
1689
|
}
|
|
1078
1690
|
}
|
|
1079
1691
|
),
|
|
1080
|
-
/* @__PURE__ */
|
|
1081
|
-
|
|
1082
|
-
/* @__PURE__ */
|
|
1692
|
+
api.status === "error" && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: "This form can't submit right now. Please try again in a moment." }),
|
|
1693
|
+
showFooter && /* @__PURE__ */ jsxs5("footer", { className: "fillo-footer", children: [
|
|
1694
|
+
multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-button fillo-button--ghost", onClick: api.back, children: "Back" }),
|
|
1695
|
+
showPrimaryButton && /* @__PURE__ */ jsx5(
|
|
1083
1696
|
"button",
|
|
1084
1697
|
{
|
|
1085
1698
|
type: "submit",
|
|
@@ -1089,17 +1702,26 @@ function ResolvedForm(props) {
|
|
|
1089
1702
|
}
|
|
1090
1703
|
)
|
|
1091
1704
|
] }),
|
|
1092
|
-
props.poweredBy && /* @__PURE__ */
|
|
1705
|
+
props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1093
1706
|
]
|
|
1094
1707
|
}
|
|
1095
|
-
) });
|
|
1708
|
+
) }) });
|
|
1096
1709
|
}
|
|
1097
1710
|
|
|
1098
1711
|
// src/provider.tsx
|
|
1099
|
-
import {
|
|
1712
|
+
import { useId as useId3, useMemo as useMemo5 } from "react";
|
|
1713
|
+
import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2 } from "@usefillo/core";
|
|
1714
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1100
1715
|
function FilloProvider({ children, form, formId, client, ...options }) {
|
|
1716
|
+
const instanceId = useId3().replace(/:/g, "");
|
|
1101
1717
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1102
|
-
const
|
|
1718
|
+
const rawSchema = codeForm ? codeForm.schema : form;
|
|
1719
|
+
const schemaKey = useMemo5(() => JSON.stringify(rawSchema), [rawSchema]);
|
|
1720
|
+
const normalized = useMemo5(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
|
|
1721
|
+
if (!normalized.ok) {
|
|
1722
|
+
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1723
|
+
}
|
|
1724
|
+
const schema = normalized.schema;
|
|
1103
1725
|
const syncedFormId = useCodeFormSync(form, client);
|
|
1104
1726
|
const api = useFilloController({
|
|
1105
1727
|
...options,
|
|
@@ -1110,7 +1732,7 @@ function FilloProvider({ children, form, formId, client, ...options }) {
|
|
|
1110
1732
|
// a paid capability the server enforces when these responses are submitted.
|
|
1111
1733
|
surface: "headless"
|
|
1112
1734
|
});
|
|
1113
|
-
return /* @__PURE__ */
|
|
1735
|
+
return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children }) });
|
|
1114
1736
|
}
|
|
1115
1737
|
|
|
1116
1738
|
// src/index.ts
|
|
@@ -1118,12 +1740,12 @@ import {
|
|
|
1118
1740
|
createClient as createClient2,
|
|
1119
1741
|
provisionWorkspace,
|
|
1120
1742
|
FilloClient,
|
|
1121
|
-
FilloError as
|
|
1743
|
+
FilloError as FilloError4
|
|
1122
1744
|
} from "@usefillo/core";
|
|
1123
1745
|
export {
|
|
1124
1746
|
BlockRenderer,
|
|
1125
1747
|
FilloClient,
|
|
1126
|
-
|
|
1748
|
+
FilloError4 as FilloError,
|
|
1127
1749
|
FileUploadField as FilloFileUpload,
|
|
1128
1750
|
FilloForm,
|
|
1129
1751
|
FilloProvider,
|