@usefillo/react 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/index.d.ts +37 -17
- package/dist/index.js +715 -309
- package/dist/styles.css +220 -14
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,18 +1,44 @@
|
|
|
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 useMemo2, 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 } 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
44
|
const field = allFields(api.form).find((f) => f.id === fieldId);
|
|
@@ -96,17 +122,245 @@ function useCodeFormSync(form, client, onError) {
|
|
|
96
122
|
}
|
|
97
123
|
|
|
98
124
|
// src/fields.tsx
|
|
99
|
-
import { isField, pipeBlock } from "@usefillo/core";
|
|
100
|
-
import { useEffect as
|
|
125
|
+
import { isField, pipeBlock, validateResponse } from "@usefillo/core";
|
|
126
|
+
import { useEffect as useEffect6, useState as useState5 } from "react";
|
|
101
127
|
|
|
102
|
-
// src/
|
|
103
|
-
import {
|
|
128
|
+
// src/phone.tsx
|
|
129
|
+
import {
|
|
130
|
+
PHONE_COUNTRIES,
|
|
131
|
+
countryByIso,
|
|
132
|
+
digitsOnly,
|
|
133
|
+
flagEmoji,
|
|
134
|
+
formatNational,
|
|
135
|
+
parsePhone,
|
|
136
|
+
toE164
|
|
137
|
+
} from "@usefillo/core";
|
|
138
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo, useRef as useRef2, useState as useState2 } from "react";
|
|
104
139
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
140
|
+
function localeCountry() {
|
|
141
|
+
if (typeof navigator === "undefined") return void 0;
|
|
142
|
+
const tag = navigator.languages?.[0] || navigator.language || "";
|
|
143
|
+
const region = tag.split("-")[1];
|
|
144
|
+
return region && /^[A-Za-z]{2}$/.test(region) ? region.toUpperCase() : void 0;
|
|
145
|
+
}
|
|
146
|
+
function nationalFor(stored, country) {
|
|
147
|
+
const digits = digitsOnly(stored);
|
|
148
|
+
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
|
149
|
+
}
|
|
150
|
+
function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
151
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
152
|
+
const schema = field;
|
|
153
|
+
const stored = typeof value === "string" ? value : "";
|
|
154
|
+
const [country, setCountry] = useState2(
|
|
155
|
+
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? countryByIso(localeCountry()) ?? PHONE_COUNTRIES[0]
|
|
156
|
+
);
|
|
157
|
+
const national = nationalFor(stored, country);
|
|
158
|
+
const display = formatNational(country, national);
|
|
159
|
+
const inputRef = useRef2(null);
|
|
160
|
+
const caretDigits = useRef2(null);
|
|
161
|
+
useLayoutEffect(() => {
|
|
162
|
+
const el = inputRef.current;
|
|
163
|
+
if (!el || caretDigits.current == null) return;
|
|
164
|
+
const target = caretDigits.current;
|
|
165
|
+
caretDigits.current = null;
|
|
166
|
+
if (target === 0) return el.setSelectionRange(0, 0);
|
|
167
|
+
let seen = 0;
|
|
168
|
+
let pos = display.length;
|
|
169
|
+
for (let i = 0; i < display.length; i++) {
|
|
170
|
+
if (/\d/.test(display[i]) && ++seen === target) {
|
|
171
|
+
pos = i + 1;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
el.setSelectionRange(pos, pos);
|
|
176
|
+
});
|
|
177
|
+
const commit = (c, nat) => setValue(nat ? toE164(c, nat) : "");
|
|
178
|
+
function onInput(e) {
|
|
179
|
+
const el = e.target;
|
|
180
|
+
const raw = el.value;
|
|
181
|
+
const caret = el.selectionStart ?? raw.length;
|
|
182
|
+
caretDigits.current = digitsOnly(raw.slice(0, caret)).length;
|
|
183
|
+
if (raw.trimStart().startsWith("+")) {
|
|
184
|
+
const p = parsePhone(raw);
|
|
185
|
+
if (p.country) setCountry(p.country);
|
|
186
|
+
commit(p.country ?? country, p.country ? p.national : digitsOnly(raw));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
commit(country, digitsOnly(raw));
|
|
190
|
+
}
|
|
191
|
+
function pickCountry(c) {
|
|
192
|
+
setCountry(c);
|
|
193
|
+
commit(c, national);
|
|
194
|
+
inputRef.current?.focus();
|
|
195
|
+
}
|
|
196
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
197
|
+
return /* @__PURE__ */ jsxs("div", { className: `fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`, "data-field": field.id, children: [
|
|
198
|
+
/* @__PURE__ */ jsxs("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
199
|
+
field.label,
|
|
200
|
+
!field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: " (optional)" })
|
|
201
|
+
] }),
|
|
202
|
+
field.description && /* @__PURE__ */ jsx("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
203
|
+
/* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
|
|
204
|
+
/* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"input",
|
|
207
|
+
{
|
|
208
|
+
ref: inputRef,
|
|
209
|
+
id: ids.inputId,
|
|
210
|
+
className: "fillo-input fillo-phone-input",
|
|
211
|
+
type: "tel",
|
|
212
|
+
inputMode: "tel",
|
|
213
|
+
autoComplete: "tel-national",
|
|
214
|
+
value: display,
|
|
215
|
+
placeholder: formatNational(country, country.example),
|
|
216
|
+
onChange: onInput,
|
|
217
|
+
"aria-invalid": error ? true : void 0,
|
|
218
|
+
"aria-required": field.required ? true : void 0,
|
|
219
|
+
"aria-describedby": describedBy
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
] }),
|
|
223
|
+
error && /* @__PURE__ */ jsx("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
224
|
+
] });
|
|
225
|
+
}
|
|
226
|
+
function CountrySelect({
|
|
227
|
+
country,
|
|
228
|
+
onPick,
|
|
229
|
+
labelledBy
|
|
230
|
+
}) {
|
|
231
|
+
const [open, setOpen] = useState2(false);
|
|
232
|
+
const [query, setQuery] = useState2("");
|
|
233
|
+
const [active, setActive] = useState2(0);
|
|
234
|
+
const rootRef = useRef2(null);
|
|
235
|
+
const searchRef = useRef2(null);
|
|
236
|
+
const listId = `${labelledBy}-countries`;
|
|
237
|
+
const matches = useMemo(() => {
|
|
238
|
+
const q = query.trim().toLowerCase();
|
|
239
|
+
if (!q) return PHONE_COUNTRIES;
|
|
240
|
+
const qDigits = q.replace(/[^\d]/g, "");
|
|
241
|
+
return PHONE_COUNTRIES.filter(
|
|
242
|
+
(c) => c.name.toLowerCase().includes(q) || c.iso2.toLowerCase() === q || qDigits.length > 0 && c.dialCode.startsWith(qDigits)
|
|
243
|
+
);
|
|
244
|
+
}, [query]);
|
|
245
|
+
useEffect3(() => {
|
|
246
|
+
if (!open) return;
|
|
247
|
+
setQuery("");
|
|
248
|
+
setActive(Math.max(0, PHONE_COUNTRIES.indexOf(country)));
|
|
249
|
+
const id = requestAnimationFrame(() => searchRef.current?.focus());
|
|
250
|
+
return () => cancelAnimationFrame(id);
|
|
251
|
+
}, [open, country]);
|
|
109
252
|
useEffect3(() => {
|
|
253
|
+
if (!open) return;
|
|
254
|
+
const onDown = (e) => {
|
|
255
|
+
if (!rootRef.current?.contains(e.target)) setOpen(false);
|
|
256
|
+
};
|
|
257
|
+
document.addEventListener("mousedown", onDown);
|
|
258
|
+
return () => document.removeEventListener("mousedown", onDown);
|
|
259
|
+
}, [open]);
|
|
260
|
+
useEffect3(() => {
|
|
261
|
+
if (!open) return;
|
|
262
|
+
document.getElementById(`${listId}-opt-${active}`)?.scrollIntoView({ block: "nearest" });
|
|
263
|
+
}, [active, open, listId]);
|
|
264
|
+
function choose(c) {
|
|
265
|
+
if (!c) return;
|
|
266
|
+
onPick(c);
|
|
267
|
+
setOpen(false);
|
|
268
|
+
}
|
|
269
|
+
function onSearchKey(e) {
|
|
270
|
+
if (e.key === "ArrowDown") {
|
|
271
|
+
e.preventDefault();
|
|
272
|
+
setActive((i) => Math.min(i + 1, matches.length - 1));
|
|
273
|
+
} else if (e.key === "ArrowUp") {
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
setActive((i) => Math.max(i - 1, 0));
|
|
276
|
+
} else if (e.key === "Enter") {
|
|
277
|
+
e.preventDefault();
|
|
278
|
+
choose(matches[active]);
|
|
279
|
+
} else if (e.key === "Escape") {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
setOpen(false);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
|
|
285
|
+
/* @__PURE__ */ jsxs(
|
|
286
|
+
"button",
|
|
287
|
+
{
|
|
288
|
+
type: "button",
|
|
289
|
+
className: "fillo-phone-flag",
|
|
290
|
+
"aria-haspopup": "listbox",
|
|
291
|
+
"aria-expanded": open,
|
|
292
|
+
"aria-label": `Country: ${country.name} (+${country.dialCode})`,
|
|
293
|
+
onClick: () => setOpen((o) => !o),
|
|
294
|
+
children: [
|
|
295
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(country.iso2) }),
|
|
296
|
+
/* @__PURE__ */ jsxs("span", { className: "fillo-phone-dial", children: [
|
|
297
|
+
"+",
|
|
298
|
+
country.dialCode
|
|
299
|
+
] }),
|
|
300
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-caret", "aria-hidden": "true", children: "\u25BE" })
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
),
|
|
304
|
+
open && /* @__PURE__ */ jsxs("div", { className: "fillo-phone-popover", children: [
|
|
305
|
+
/* @__PURE__ */ jsx(
|
|
306
|
+
"input",
|
|
307
|
+
{
|
|
308
|
+
ref: searchRef,
|
|
309
|
+
className: "fillo-phone-search",
|
|
310
|
+
type: "text",
|
|
311
|
+
role: "combobox",
|
|
312
|
+
"aria-expanded": "true",
|
|
313
|
+
"aria-controls": listId,
|
|
314
|
+
"aria-autocomplete": "list",
|
|
315
|
+
"aria-activedescendant": matches[active] ? `${listId}-opt-${active}` : void 0,
|
|
316
|
+
placeholder: "Search country or code",
|
|
317
|
+
value: query,
|
|
318
|
+
onChange: (e) => {
|
|
319
|
+
setQuery(e.target.value);
|
|
320
|
+
setActive(0);
|
|
321
|
+
},
|
|
322
|
+
onKeyDown: onSearchKey
|
|
323
|
+
}
|
|
324
|
+
),
|
|
325
|
+
/* @__PURE__ */ jsxs("ul", { className: "fillo-phone-list", role: "listbox", id: listId, "aria-label": "Country", children: [
|
|
326
|
+
matches.map((c, i) => /* @__PURE__ */ jsxs(
|
|
327
|
+
"li",
|
|
328
|
+
{
|
|
329
|
+
id: `${listId}-opt-${i}`,
|
|
330
|
+
role: "option",
|
|
331
|
+
"aria-selected": c.iso2 === country.iso2,
|
|
332
|
+
className: `fillo-phone-option${i === active ? " fillo-phone-option--active" : ""}`,
|
|
333
|
+
onMouseEnter: () => setActive(i),
|
|
334
|
+
onMouseDown: (e) => {
|
|
335
|
+
e.preventDefault();
|
|
336
|
+
choose(c);
|
|
337
|
+
},
|
|
338
|
+
children: [
|
|
339
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-flag-emoji", "aria-hidden": "true", children: flagEmoji(c.iso2) }),
|
|
340
|
+
/* @__PURE__ */ jsx("span", { className: "fillo-phone-option-name", children: c.name }),
|
|
341
|
+
/* @__PURE__ */ jsxs("span", { className: "fillo-phone-option-dial", children: [
|
|
342
|
+
"+",
|
|
343
|
+
c.dialCode
|
|
344
|
+
] })
|
|
345
|
+
]
|
|
346
|
+
},
|
|
347
|
+
c.iso2
|
|
348
|
+
)),
|
|
349
|
+
matches.length === 0 && /* @__PURE__ */ jsx("li", { className: "fillo-phone-empty", children: "No matches" })
|
|
350
|
+
] })
|
|
351
|
+
] })
|
|
352
|
+
] });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/signature.tsx
|
|
356
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "react";
|
|
357
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
358
|
+
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
359
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
360
|
+
const canvasRef = useRef3(null);
|
|
361
|
+
const drawing = useRef3(false);
|
|
362
|
+
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
363
|
+
useEffect4(() => {
|
|
110
364
|
const canvas = canvasRef.current;
|
|
111
365
|
if (!canvas) return;
|
|
112
366
|
const scale = window.devicePixelRatio || 1;
|
|
@@ -161,19 +415,19 @@ function SignatureField({ field, value, error, setValue }) {
|
|
|
161
415
|
setHasInk(false);
|
|
162
416
|
setValue(null);
|
|
163
417
|
}
|
|
164
|
-
const describedBy = [field.description ?
|
|
165
|
-
return /* @__PURE__ */
|
|
166
|
-
/* @__PURE__ */
|
|
418
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
419
|
+
return /* @__PURE__ */ jsxs2("div", { className: `fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`, children: [
|
|
420
|
+
/* @__PURE__ */ jsxs2("label", { className: "fillo-label", htmlFor: ids.inputId, children: [
|
|
167
421
|
field.label,
|
|
168
|
-
!field.required && /* @__PURE__ */
|
|
422
|
+
!field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: " (optional)" })
|
|
169
423
|
] }),
|
|
170
|
-
field.description && /* @__PURE__ */
|
|
171
|
-
/* @__PURE__ */
|
|
172
|
-
/* @__PURE__ */
|
|
424
|
+
field.description && /* @__PURE__ */ jsx2("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
425
|
+
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
|
|
426
|
+
/* @__PURE__ */ jsx2(
|
|
173
427
|
"canvas",
|
|
174
428
|
{
|
|
175
429
|
ref: canvasRef,
|
|
176
|
-
id:
|
|
430
|
+
id: ids.inputId,
|
|
177
431
|
className: "fillo-signature-canvas",
|
|
178
432
|
"aria-invalid": error ? true : void 0,
|
|
179
433
|
"aria-describedby": describedBy,
|
|
@@ -183,31 +437,32 @@ function SignatureField({ field, value, error, setValue }) {
|
|
|
183
437
|
onPointerLeave: end
|
|
184
438
|
}
|
|
185
439
|
),
|
|
186
|
-
hasInk && /* @__PURE__ */
|
|
187
|
-
!hasInk && /* @__PURE__ */
|
|
440
|
+
hasInk && /* @__PURE__ */ jsx2("button", { type: "button", className: "fillo-signature-clear", onClick: clear, children: "Clear" }),
|
|
441
|
+
!hasInk && /* @__PURE__ */ jsx2("span", { className: "fillo-signature-hint", children: "Sign here" })
|
|
188
442
|
] }),
|
|
189
|
-
error && /* @__PURE__ */
|
|
443
|
+
error && /* @__PURE__ */ jsx2("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
190
444
|
] });
|
|
191
445
|
}
|
|
192
446
|
|
|
193
447
|
// src/upload.tsx
|
|
194
|
-
import { useEffect as
|
|
195
|
-
import { Fragment, jsx as
|
|
448
|
+
import { useEffect as useEffect5, useRef as useRef4, useState as useState4 } from "react";
|
|
449
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
196
450
|
function formatBytes(bytes) {
|
|
197
451
|
if (bytes < 1024) return `${bytes} B`;
|
|
198
452
|
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
199
453
|
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)} MB`;
|
|
200
454
|
return `${(bytes / 1024 ** 3).toFixed(2)} GB`;
|
|
201
455
|
}
|
|
202
|
-
function FileUploadField({ field, value, error, setValue, api }) {
|
|
456
|
+
function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
|
|
457
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
203
458
|
const schema = field;
|
|
204
|
-
const inputRef =
|
|
205
|
-
const [inFlight, setInFlight] =
|
|
206
|
-
const [dragOver, setDragOver] =
|
|
207
|
-
const mountedRef =
|
|
208
|
-
const controllers =
|
|
209
|
-
const inFlightCount =
|
|
210
|
-
|
|
459
|
+
const inputRef = useRef4(null);
|
|
460
|
+
const [inFlight, setInFlight] = useState4([]);
|
|
461
|
+
const [dragOver, setDragOver] = useState4(false);
|
|
462
|
+
const mountedRef = useRef4(true);
|
|
463
|
+
const controllers = useRef4(/* @__PURE__ */ new Map());
|
|
464
|
+
const inFlightCount = useRef4(0);
|
|
465
|
+
useEffect5(
|
|
211
466
|
() => () => {
|
|
212
467
|
mountedRef.current = false;
|
|
213
468
|
controllers.current.forEach((c) => c.abort());
|
|
@@ -271,14 +526,14 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
271
526
|
function removeFile(fileId) {
|
|
272
527
|
setValue(files.filter((f) => f.fileId !== fileId));
|
|
273
528
|
}
|
|
274
|
-
const describedBy = [field.description ?
|
|
275
|
-
return /* @__PURE__ */
|
|
276
|
-
/* @__PURE__ */
|
|
529
|
+
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
530
|
+
return /* @__PURE__ */ jsxs3("div", { className: `fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`, children: [
|
|
531
|
+
/* @__PURE__ */ jsxs3("label", { className: "fillo-label", htmlFor: ids.inputId, children: [
|
|
277
532
|
field.label,
|
|
278
|
-
!field.required && /* @__PURE__ */
|
|
533
|
+
!field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: " (optional)" })
|
|
279
534
|
] }),
|
|
280
|
-
field.description && /* @__PURE__ */
|
|
281
|
-
remaining > 0 && /* @__PURE__ */
|
|
535
|
+
field.description && /* @__PURE__ */ jsx3("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
536
|
+
remaining > 0 && /* @__PURE__ */ jsxs3(
|
|
282
537
|
"div",
|
|
283
538
|
{
|
|
284
539
|
className: `fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`,
|
|
@@ -299,11 +554,11 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
299
554
|
if ((e.key === "Enter" || e.key === " ") && canUpload) inputRef.current?.click();
|
|
300
555
|
},
|
|
301
556
|
children: [
|
|
302
|
-
/* @__PURE__ */
|
|
557
|
+
/* @__PURE__ */ jsx3(
|
|
303
558
|
"input",
|
|
304
559
|
{
|
|
305
560
|
ref: inputRef,
|
|
306
|
-
id:
|
|
561
|
+
id: ids.inputId,
|
|
307
562
|
type: "file",
|
|
308
563
|
hidden: true,
|
|
309
564
|
"aria-invalid": error ? true : void 0,
|
|
@@ -314,26 +569,26 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
314
569
|
onChange: (e) => handleFiles(e.target.files)
|
|
315
570
|
}
|
|
316
571
|
),
|
|
317
|
-
canUpload ? /* @__PURE__ */
|
|
318
|
-
/* @__PURE__ */
|
|
572
|
+
canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
573
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-title", children: [
|
|
319
574
|
"Drop ",
|
|
320
575
|
maxFiles > 1 ? "files" : "a file",
|
|
321
576
|
" here or click to browse"
|
|
322
577
|
] }),
|
|
323
|
-
/* @__PURE__ */
|
|
578
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-hint", children: [
|
|
324
579
|
"Up to ",
|
|
325
580
|
schema.maxFileSizeMb ?? 500,
|
|
326
581
|
" MB per file \xB7 resumable"
|
|
327
582
|
] })
|
|
328
|
-
] }) : /* @__PURE__ */
|
|
583
|
+
] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: "Uploads are disabled in preview" })
|
|
329
584
|
]
|
|
330
585
|
}
|
|
331
586
|
),
|
|
332
|
-
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */
|
|
333
|
-
files.map((f) => /* @__PURE__ */
|
|
334
|
-
/* @__PURE__ */
|
|
335
|
-
/* @__PURE__ */
|
|
336
|
-
/* @__PURE__ */
|
|
587
|
+
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
588
|
+
files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
|
|
589
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
590
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: formatBytes(f.size) }),
|
|
591
|
+
/* @__PURE__ */ jsx3(
|
|
337
592
|
"button",
|
|
338
593
|
{
|
|
339
594
|
type: "button",
|
|
@@ -344,11 +599,11 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
344
599
|
}
|
|
345
600
|
)
|
|
346
601
|
] }, f.fileId)),
|
|
347
|
-
inFlight.map((f) => /* @__PURE__ */
|
|
348
|
-
/* @__PURE__ */
|
|
349
|
-
f.error ? /* @__PURE__ */
|
|
350
|
-
/* @__PURE__ */
|
|
351
|
-
/* @__PURE__ */
|
|
602
|
+
inFlight.map((f) => /* @__PURE__ */ jsxs3("li", { className: `fillo-file${f.error ? " fillo-file--failed" : ""}`, children: [
|
|
603
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
604
|
+
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
605
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
|
|
606
|
+
/* @__PURE__ */ jsx3(
|
|
352
607
|
"button",
|
|
353
608
|
{
|
|
354
609
|
type: "button",
|
|
@@ -358,14 +613,14 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
358
613
|
children: "\xD7"
|
|
359
614
|
}
|
|
360
615
|
)
|
|
361
|
-
] }) : /* @__PURE__ */
|
|
362
|
-
/* @__PURE__ */
|
|
616
|
+
] }) : /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
617
|
+
/* @__PURE__ */ jsxs3("span", { className: "fillo-file-meta", children: [
|
|
363
618
|
Math.round(f.fraction * 100),
|
|
364
619
|
"% of ",
|
|
365
620
|
formatBytes(f.size)
|
|
366
621
|
] }),
|
|
367
|
-
/* @__PURE__ */
|
|
368
|
-
/* @__PURE__ */
|
|
622
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-progress", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "fillo-progress-bar", style: { width: `${f.fraction * 100}%` } }) }),
|
|
623
|
+
/* @__PURE__ */ jsx3(
|
|
369
624
|
"button",
|
|
370
625
|
{
|
|
371
626
|
type: "button",
|
|
@@ -378,16 +633,16 @@ function FileUploadField({ field, value, error, setValue, api }) {
|
|
|
378
633
|
] })
|
|
379
634
|
] }, f.key))
|
|
380
635
|
] }),
|
|
381
|
-
error && /* @__PURE__ */
|
|
636
|
+
error && /* @__PURE__ */ jsx3("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
382
637
|
] });
|
|
383
638
|
}
|
|
384
639
|
|
|
385
640
|
// src/fields.tsx
|
|
386
|
-
import { jsx as
|
|
387
|
-
function fieldAria(field, error) {
|
|
641
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
642
|
+
function fieldAria(field, error, ids) {
|
|
388
643
|
const describedBy = [
|
|
389
|
-
field.description ?
|
|
390
|
-
error ?
|
|
644
|
+
field.description ? ids.descriptionId : null,
|
|
645
|
+
error ? ids.errorId : null
|
|
391
646
|
].filter(Boolean);
|
|
392
647
|
return {
|
|
393
648
|
"aria-invalid": error ? true : void 0,
|
|
@@ -398,58 +653,61 @@ function fieldAria(field, error) {
|
|
|
398
653
|
function FieldShell({
|
|
399
654
|
field,
|
|
400
655
|
error,
|
|
656
|
+
ids,
|
|
401
657
|
children
|
|
402
658
|
}) {
|
|
403
|
-
return /* @__PURE__ */
|
|
659
|
+
return /* @__PURE__ */ jsxs4(
|
|
404
660
|
"div",
|
|
405
661
|
{
|
|
406
662
|
className: `fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
|
|
407
663
|
"data-field": field.id,
|
|
408
664
|
children: [
|
|
409
|
-
/* @__PURE__ */
|
|
665
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
410
666
|
field.label,
|
|
411
|
-
!field.required && /* @__PURE__ */
|
|
667
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
|
|
412
668
|
] }),
|
|
413
|
-
field.description && /* @__PURE__ */
|
|
669
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
414
670
|
children,
|
|
415
|
-
error && /* @__PURE__ */
|
|
671
|
+
error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
416
672
|
]
|
|
417
673
|
}
|
|
418
674
|
);
|
|
419
675
|
}
|
|
420
|
-
function TextInput({ field, value, error, setValue }) {
|
|
421
|
-
const
|
|
422
|
-
|
|
676
|
+
function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
677
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
678
|
+
const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
|
|
679
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
423
680
|
"input",
|
|
424
681
|
{
|
|
425
|
-
id:
|
|
682
|
+
id: ids.inputId,
|
|
426
683
|
className: "fillo-input",
|
|
427
684
|
type,
|
|
428
685
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
429
686
|
placeholder: field.placeholder,
|
|
430
687
|
onChange: (e) => setValue(e.target.value),
|
|
431
|
-
...fieldAria(field, error)
|
|
688
|
+
...fieldAria(field, error, ids)
|
|
432
689
|
}
|
|
433
690
|
) });
|
|
434
691
|
}
|
|
435
|
-
function LongText({ field, value, error, setValue }) {
|
|
436
|
-
|
|
692
|
+
function LongText({ field, value, error, setValue, ids: providedIds }) {
|
|
693
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
694
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
437
695
|
"textarea",
|
|
438
696
|
{
|
|
439
|
-
id:
|
|
697
|
+
id: ids.inputId,
|
|
440
698
|
className: "fillo-input fillo-textarea",
|
|
441
699
|
rows: 4,
|
|
442
700
|
value: typeof value === "string" ? value : "",
|
|
443
701
|
placeholder: field.placeholder,
|
|
444
702
|
onChange: (e) => setValue(e.target.value),
|
|
445
|
-
...fieldAria(field, error)
|
|
703
|
+
...fieldAria(field, error, ids)
|
|
446
704
|
}
|
|
447
705
|
) });
|
|
448
706
|
}
|
|
449
707
|
function useDisplayOptions(choice) {
|
|
450
708
|
const idsKey = choice.options.map((o) => o.id).join("\0");
|
|
451
|
-
const [order, setOrder] =
|
|
452
|
-
|
|
709
|
+
const [order, setOrder] = useState5(null);
|
|
710
|
+
useEffect6(() => {
|
|
453
711
|
if (!choice.shuffleOptions) return;
|
|
454
712
|
const ids = idsKey === "" ? [] : idsKey.split("\0");
|
|
455
713
|
for (let i = ids.length - 1; i > 0; i--) {
|
|
@@ -463,32 +721,54 @@ function useDisplayOptions(choice) {
|
|
|
463
721
|
const shuffled = order.flatMap((id) => byId.get(id) ?? []);
|
|
464
722
|
return shuffled.length === choice.options.length ? shuffled : choice.options;
|
|
465
723
|
}
|
|
466
|
-
|
|
724
|
+
var OPTION_ICON_PATHS = {
|
|
725
|
+
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",
|
|
726
|
+
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"
|
|
727
|
+
};
|
|
728
|
+
function ChoiceIcon({ icon }) {
|
|
729
|
+
if (!icon) return null;
|
|
730
|
+
return /* @__PURE__ */ jsx4(
|
|
731
|
+
"svg",
|
|
732
|
+
{
|
|
733
|
+
className: "fillo-option-icon",
|
|
734
|
+
viewBox: "0 0 24 24",
|
|
735
|
+
fill: "none",
|
|
736
|
+
stroke: "currentColor",
|
|
737
|
+
strokeWidth: "1.8",
|
|
738
|
+
strokeLinecap: "round",
|
|
739
|
+
strokeLinejoin: "round",
|
|
740
|
+
"aria-hidden": "true",
|
|
741
|
+
children: /* @__PURE__ */ jsx4("path", { d: OPTION_ICON_PATHS[icon] })
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
746
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
467
747
|
const choice = field;
|
|
468
748
|
const options = useDisplayOptions(choice);
|
|
469
749
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
470
|
-
const [otherOn, setOtherOn] =
|
|
471
|
-
const otherActive =
|
|
472
|
-
return /* @__PURE__ */
|
|
750
|
+
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
751
|
+
const otherActive = isOtherValue || value === "" && otherOn;
|
|
752
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
|
|
473
753
|
"div",
|
|
474
754
|
{
|
|
475
755
|
className: "fillo-options",
|
|
476
756
|
role: "radiogroup",
|
|
477
|
-
"aria-labelledby":
|
|
478
|
-
...fieldAria(field, error),
|
|
757
|
+
"aria-labelledby": ids.labelId,
|
|
758
|
+
...fieldAria(field, error, ids),
|
|
479
759
|
children: [
|
|
480
|
-
options.map((opt) => /* @__PURE__ */
|
|
760
|
+
options.map((opt) => /* @__PURE__ */ jsxs4(
|
|
481
761
|
"label",
|
|
482
762
|
{
|
|
483
763
|
"data-option": opt.id,
|
|
484
|
-
className: `fillo-option${value === opt.id ? " fillo-option--selected" : ""}`,
|
|
764
|
+
className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
|
|
485
765
|
children: [
|
|
486
|
-
/* @__PURE__ */
|
|
766
|
+
/* @__PURE__ */ jsx4(
|
|
487
767
|
"input",
|
|
488
768
|
{
|
|
489
769
|
type: "radio",
|
|
490
770
|
className: "fillo-option-input",
|
|
491
|
-
name:
|
|
771
|
+
name: ids.name,
|
|
492
772
|
checked: value === opt.id,
|
|
493
773
|
onChange: () => {
|
|
494
774
|
setOtherOn(false);
|
|
@@ -496,66 +776,76 @@ function SingleChoice({ field, value, error, setValue }) {
|
|
|
496
776
|
}
|
|
497
777
|
}
|
|
498
778
|
),
|
|
499
|
-
/* @__PURE__ */
|
|
779
|
+
/* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
|
|
780
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
|
|
500
781
|
]
|
|
501
782
|
},
|
|
502
783
|
opt.id
|
|
503
784
|
)),
|
|
504
|
-
choice.allowOther && /* @__PURE__ */
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
{
|
|
508
|
-
|
|
509
|
-
className: "fillo-option-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
785
|
+
choice.allowOther && /* @__PURE__ */ jsxs4(
|
|
786
|
+
"label",
|
|
787
|
+
{
|
|
788
|
+
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
789
|
+
children: [
|
|
790
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-option-main", children: [
|
|
791
|
+
/* @__PURE__ */ jsx4(
|
|
792
|
+
"input",
|
|
793
|
+
{
|
|
794
|
+
type: "radio",
|
|
795
|
+
className: "fillo-option-input",
|
|
796
|
+
name: ids.name,
|
|
797
|
+
checked: otherActive,
|
|
798
|
+
onChange: () => {
|
|
799
|
+
setOtherOn(true);
|
|
800
|
+
setValue("");
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
),
|
|
804
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
|
|
805
|
+
] }),
|
|
806
|
+
otherActive && /* @__PURE__ */ jsx4(
|
|
807
|
+
"input",
|
|
808
|
+
{
|
|
809
|
+
type: "text",
|
|
810
|
+
className: "fillo-input fillo-other-input",
|
|
811
|
+
placeholder: "Your answer",
|
|
812
|
+
autoFocus: !isOtherValue,
|
|
813
|
+
value: isOtherValue ? String(value) : "",
|
|
814
|
+
onChange: (e) => setValue(e.target.value)
|
|
815
|
+
}
|
|
816
|
+
)
|
|
817
|
+
]
|
|
818
|
+
}
|
|
819
|
+
)
|
|
531
820
|
]
|
|
532
821
|
}
|
|
533
822
|
) });
|
|
534
823
|
}
|
|
535
|
-
function MultiChoice({ field, value, error, setValue }) {
|
|
824
|
+
function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
825
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
536
826
|
const choice = field;
|
|
537
827
|
const options = useDisplayOptions(choice);
|
|
538
|
-
const
|
|
828
|
+
const optionIds = new Set(choice.options.map((o) => o.id));
|
|
539
829
|
const selected = Array.isArray(value) ? value : [];
|
|
540
|
-
const otherText = selected.find((v) => !
|
|
541
|
-
const [otherOn, setOtherOn] =
|
|
542
|
-
const otherActive =
|
|
830
|
+
const otherText = selected.find((v) => !optionIds.has(v));
|
|
831
|
+
const [otherOn, setOtherOn] = useState5(otherText !== void 0);
|
|
832
|
+
const otherActive = otherText !== void 0 || otherOn;
|
|
543
833
|
const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
|
|
544
|
-
return /* @__PURE__ */
|
|
834
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
|
|
545
835
|
"div",
|
|
546
836
|
{
|
|
547
837
|
className: "fillo-options",
|
|
548
838
|
role: "group",
|
|
549
|
-
"aria-labelledby":
|
|
550
|
-
...fieldAria(field, error),
|
|
839
|
+
"aria-labelledby": ids.labelId,
|
|
840
|
+
...fieldAria(field, error, ids),
|
|
551
841
|
children: [
|
|
552
|
-
options.map((opt) => /* @__PURE__ */
|
|
842
|
+
options.map((opt) => /* @__PURE__ */ jsxs4(
|
|
553
843
|
"label",
|
|
554
844
|
{
|
|
555
845
|
"data-option": opt.id,
|
|
556
|
-
className: `fillo-option${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
|
|
846
|
+
className: `fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
|
|
557
847
|
children: [
|
|
558
|
-
/* @__PURE__ */
|
|
848
|
+
/* @__PURE__ */ jsx4(
|
|
559
849
|
"input",
|
|
560
850
|
{
|
|
561
851
|
type: "checkbox",
|
|
@@ -564,80 +854,93 @@ function MultiChoice({ field, value, error, setValue }) {
|
|
|
564
854
|
onChange: () => toggle(opt.id)
|
|
565
855
|
}
|
|
566
856
|
),
|
|
567
|
-
/* @__PURE__ */
|
|
857
|
+
/* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
|
|
858
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
|
|
568
859
|
]
|
|
569
860
|
},
|
|
570
861
|
opt.id
|
|
571
862
|
)),
|
|
572
|
-
choice.allowOther && /* @__PURE__ */
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
{
|
|
576
|
-
|
|
577
|
-
className: "fillo-option-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
863
|
+
choice.allowOther && /* @__PURE__ */ jsxs4(
|
|
864
|
+
"label",
|
|
865
|
+
{
|
|
866
|
+
className: `fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
867
|
+
children: [
|
|
868
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-option-main", children: [
|
|
869
|
+
/* @__PURE__ */ jsx4(
|
|
870
|
+
"input",
|
|
871
|
+
{
|
|
872
|
+
type: "checkbox",
|
|
873
|
+
className: "fillo-option-input",
|
|
874
|
+
checked: otherActive,
|
|
875
|
+
onChange: () => {
|
|
876
|
+
if (otherActive) {
|
|
877
|
+
setOtherOn(false);
|
|
878
|
+
setValue(selected.filter((v) => optionIds.has(v)));
|
|
879
|
+
} else {
|
|
880
|
+
setOtherOn(true);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
),
|
|
885
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: "Other" })
|
|
886
|
+
] }),
|
|
887
|
+
otherActive && /* @__PURE__ */ jsx4(
|
|
888
|
+
"input",
|
|
889
|
+
{
|
|
890
|
+
type: "text",
|
|
891
|
+
className: "fillo-input fillo-other-input",
|
|
892
|
+
placeholder: "Your answer",
|
|
893
|
+
autoFocus: otherText === void 0,
|
|
894
|
+
value: otherText ?? "",
|
|
895
|
+
onChange: (e) => {
|
|
896
|
+
const rest = selected.filter((v) => optionIds.has(v));
|
|
897
|
+
setValue(e.target.value ? [...rest, e.target.value] : rest);
|
|
898
|
+
}
|
|
585
899
|
}
|
|
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
|
-
] })
|
|
900
|
+
)
|
|
901
|
+
]
|
|
902
|
+
}
|
|
903
|
+
)
|
|
605
904
|
]
|
|
606
905
|
}
|
|
607
906
|
) });
|
|
608
907
|
}
|
|
609
908
|
var OTHER_SENTINEL = "__fw_other__";
|
|
610
|
-
function Dropdown({ field, value, error, setValue }) {
|
|
909
|
+
function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
910
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
611
911
|
const choice = field;
|
|
612
912
|
const options = useDisplayOptions(choice);
|
|
613
913
|
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
|
-
|
|
914
|
+
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
915
|
+
const otherActive = isOtherValue || value === "" && otherOn;
|
|
916
|
+
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
917
|
+
/* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
|
|
918
|
+
/* @__PURE__ */ jsxs4(
|
|
919
|
+
"select",
|
|
920
|
+
{
|
|
921
|
+
id: ids.inputId,
|
|
922
|
+
className: "fillo-input fillo-select",
|
|
923
|
+
...fieldAria(field, error, ids),
|
|
924
|
+
value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
|
|
925
|
+
onChange: (e) => {
|
|
926
|
+
if (e.target.value === OTHER_SENTINEL) {
|
|
927
|
+
setOtherOn(true);
|
|
928
|
+
setValue("");
|
|
929
|
+
} else {
|
|
930
|
+
setOtherOn(false);
|
|
931
|
+
setValue(e.target.value || null);
|
|
932
|
+
}
|
|
933
|
+
},
|
|
934
|
+
children: [
|
|
935
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? "Choose\u2026" }),
|
|
936
|
+
options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
|
|
937
|
+
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: "Other\u2026" })
|
|
938
|
+
]
|
|
939
|
+
}
|
|
940
|
+
),
|
|
941
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-select-icon", "aria-hidden": "true" })
|
|
942
|
+
] }),
|
|
943
|
+
otherActive && /* @__PURE__ */ jsx4(
|
|
641
944
|
"input",
|
|
642
945
|
{
|
|
643
946
|
type: "text",
|
|
@@ -650,40 +953,76 @@ function Dropdown({ field, value, error, setValue }) {
|
|
|
650
953
|
)
|
|
651
954
|
] });
|
|
652
955
|
}
|
|
653
|
-
function Checkbox({ field, value, error, setValue }) {
|
|
654
|
-
|
|
956
|
+
function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
957
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
958
|
+
const checkbox = field;
|
|
959
|
+
if (checkbox.appearance === "toggle") {
|
|
960
|
+
return /* @__PURE__ */ jsxs4(
|
|
961
|
+
"div",
|
|
962
|
+
{
|
|
963
|
+
className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
|
|
964
|
+
"data-field": field.id,
|
|
965
|
+
children: [
|
|
966
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", children: [
|
|
967
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
|
|
968
|
+
field.label,
|
|
969
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
|
|
970
|
+
] }) }),
|
|
971
|
+
/* @__PURE__ */ jsx4(
|
|
972
|
+
"input",
|
|
973
|
+
{
|
|
974
|
+
id: ids.inputId,
|
|
975
|
+
type: "checkbox",
|
|
976
|
+
role: "switch",
|
|
977
|
+
className: "fillo-toggle-input",
|
|
978
|
+
checked: value === true,
|
|
979
|
+
onChange: (e) => setValue(e.target.checked),
|
|
980
|
+
"aria-checked": value === true,
|
|
981
|
+
...fieldAria(field, error, ids)
|
|
982
|
+
}
|
|
983
|
+
),
|
|
984
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
|
|
985
|
+
] }),
|
|
986
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
987
|
+
error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
988
|
+
]
|
|
989
|
+
}
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
return /* @__PURE__ */ jsxs4(
|
|
655
993
|
"div",
|
|
656
994
|
{
|
|
657
995
|
className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
|
|
658
996
|
"data-field": field.id,
|
|
659
997
|
children: [
|
|
660
|
-
/* @__PURE__ */
|
|
661
|
-
/* @__PURE__ */
|
|
998
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", children: [
|
|
999
|
+
/* @__PURE__ */ jsx4(
|
|
662
1000
|
"input",
|
|
663
1001
|
{
|
|
664
|
-
id:
|
|
1002
|
+
id: ids.inputId,
|
|
665
1003
|
type: "checkbox",
|
|
666
1004
|
className: "fillo-option-input",
|
|
667
1005
|
checked: value === true,
|
|
668
1006
|
onChange: (e) => setValue(e.target.checked),
|
|
669
|
-
...fieldAria(field, error)
|
|
1007
|
+
...fieldAria(field, error, ids)
|
|
670
1008
|
}
|
|
671
1009
|
),
|
|
672
|
-
/* @__PURE__ */
|
|
1010
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
|
|
673
1011
|
field.label,
|
|
674
|
-
!field.required && /* @__PURE__ */
|
|
1012
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: " (optional)" })
|
|
675
1013
|
] })
|
|
676
1014
|
] }),
|
|
677
|
-
field.description && /* @__PURE__ */
|
|
678
|
-
error && /* @__PURE__ */
|
|
1015
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
1016
|
+
error && /* @__PURE__ */ jsx4("p", { className: "fillo-error", id: ids.errorId, role: "alert", children: error })
|
|
679
1017
|
]
|
|
680
1018
|
}
|
|
681
1019
|
);
|
|
682
1020
|
}
|
|
683
|
-
function Rating({ field, value, error, setValue }) {
|
|
1021
|
+
function Rating({ field, value, error, setValue, ids: providedIds }) {
|
|
1022
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
684
1023
|
const max = field.max ?? 5;
|
|
685
1024
|
const current = typeof value === "number" ? value : 0;
|
|
686
|
-
return /* @__PURE__ */
|
|
1025
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("div", { className: "fillo-rating", role: "group", "aria-label": field.label, ...fieldAria(field, error, ids), children: Array.from({ length: max }, (_, i) => i + 1).map((n) => /* @__PURE__ */ jsx4(
|
|
687
1026
|
"button",
|
|
688
1027
|
{
|
|
689
1028
|
type: "button",
|
|
@@ -696,13 +1035,14 @@ function Rating({ field, value, error, setValue }) {
|
|
|
696
1035
|
n
|
|
697
1036
|
)) }) });
|
|
698
1037
|
}
|
|
699
|
-
function LinearScale({ field, value, error, setValue }) {
|
|
1038
|
+
function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
1039
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
700
1040
|
const scale = field;
|
|
701
1041
|
const min = scale.min ?? 1;
|
|
702
1042
|
const max = scale.max ?? 10;
|
|
703
1043
|
const steps = Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
704
|
-
return /* @__PURE__ */
|
|
705
|
-
/* @__PURE__ */
|
|
1044
|
+
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1045
|
+
/* @__PURE__ */ jsx4("div", { className: "fillo-scale", role: "group", "aria-label": field.label, ...fieldAria(field, error, ids), children: steps.map((n) => /* @__PURE__ */ jsx4(
|
|
706
1046
|
"button",
|
|
707
1047
|
{
|
|
708
1048
|
type: "button",
|
|
@@ -713,13 +1053,14 @@ function LinearScale({ field, value, error, setValue }) {
|
|
|
713
1053
|
},
|
|
714
1054
|
n
|
|
715
1055
|
)) }),
|
|
716
|
-
(scale.minLabel || scale.maxLabel) && /* @__PURE__ */
|
|
717
|
-
/* @__PURE__ */
|
|
718
|
-
/* @__PURE__ */
|
|
1056
|
+
(scale.minLabel || scale.maxLabel) && /* @__PURE__ */ jsxs4("div", { className: "fillo-scale-labels", children: [
|
|
1057
|
+
/* @__PURE__ */ jsx4("span", { children: scale.minLabel }),
|
|
1058
|
+
/* @__PURE__ */ jsx4("span", { children: scale.maxLabel })
|
|
719
1059
|
] })
|
|
720
1060
|
] });
|
|
721
1061
|
}
|
|
722
|
-
function Ranking({ field, value, error, setValue }) {
|
|
1062
|
+
function Ranking({ field, value, error, setValue, ids: providedIds }) {
|
|
1063
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
723
1064
|
const ranking = field;
|
|
724
1065
|
const answered = Array.isArray(value) ? value : [];
|
|
725
1066
|
const order = [
|
|
@@ -734,14 +1075,14 @@ function Ranking({ field, value, error, setValue }) {
|
|
|
734
1075
|
[next[index], next[target]] = [next[target], next[index]];
|
|
735
1076
|
setValue(next);
|
|
736
1077
|
}
|
|
737
|
-
return /* @__PURE__ */
|
|
1078
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
|
|
738
1079
|
const option = ranking.options.find((o) => o.id === id);
|
|
739
1080
|
if (!option) return null;
|
|
740
|
-
return /* @__PURE__ */
|
|
741
|
-
/* @__PURE__ */
|
|
742
|
-
/* @__PURE__ */
|
|
743
|
-
/* @__PURE__ */
|
|
744
|
-
/* @__PURE__ */
|
|
1081
|
+
return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
|
|
1082
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
|
|
1083
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
|
|
1084
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
|
|
1085
|
+
/* @__PURE__ */ jsx4(
|
|
745
1086
|
"button",
|
|
746
1087
|
{
|
|
747
1088
|
type: "button",
|
|
@@ -752,7 +1093,7 @@ function Ranking({ field, value, error, setValue }) {
|
|
|
752
1093
|
children: "\u2191"
|
|
753
1094
|
}
|
|
754
1095
|
),
|
|
755
|
-
/* @__PURE__ */
|
|
1096
|
+
/* @__PURE__ */ jsx4(
|
|
756
1097
|
"button",
|
|
757
1098
|
{
|
|
758
1099
|
type: "button",
|
|
@@ -767,22 +1108,23 @@ function Ranking({ field, value, error, setValue }) {
|
|
|
767
1108
|
] }, id);
|
|
768
1109
|
}) }) });
|
|
769
1110
|
}
|
|
770
|
-
function Matrix({ field, value, error, setValue }) {
|
|
1111
|
+
function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
1112
|
+
const ids = useFilloFieldIds(field.id, providedIds);
|
|
771
1113
|
const matrix = field;
|
|
772
1114
|
const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
773
|
-
return /* @__PURE__ */
|
|
774
|
-
/* @__PURE__ */
|
|
775
|
-
/* @__PURE__ */
|
|
776
|
-
matrix.columns.map((col) => /* @__PURE__ */
|
|
1115
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4("div", { className: "fillo-matrix-wrap", children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
|
|
1116
|
+
/* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1117
|
+
/* @__PURE__ */ jsx4("th", {}),
|
|
1118
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
|
|
777
1119
|
] }) }),
|
|
778
|
-
/* @__PURE__ */
|
|
779
|
-
/* @__PURE__ */
|
|
780
|
-
matrix.columns.map((col) => /* @__PURE__ */
|
|
1120
|
+
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1121
|
+
/* @__PURE__ */ jsx4("th", { scope: "row", children: row.label }),
|
|
1122
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
|
|
781
1123
|
"input",
|
|
782
1124
|
{
|
|
783
1125
|
type: "radio",
|
|
784
1126
|
className: "fillo-option-input",
|
|
785
|
-
name:
|
|
1127
|
+
name: `${ids.name}-${row.id}`,
|
|
786
1128
|
"aria-label": `${row.label}: ${col.label}`,
|
|
787
1129
|
checked: answers[row.id] === col.id,
|
|
788
1130
|
onChange: () => setValue({ ...answers, [row.id]: col.id })
|
|
@@ -798,7 +1140,7 @@ var DEFAULT_COMPONENTS = {
|
|
|
798
1140
|
short_text: TextInput,
|
|
799
1141
|
email: TextInput,
|
|
800
1142
|
url: TextInput,
|
|
801
|
-
phone:
|
|
1143
|
+
phone: PhoneField,
|
|
802
1144
|
number: TextInput,
|
|
803
1145
|
date: TextInput,
|
|
804
1146
|
long_text: LongText,
|
|
@@ -817,13 +1159,32 @@ var DEFAULT_COMPONENTS = {
|
|
|
817
1159
|
function ContentRenderer({ block }) {
|
|
818
1160
|
switch (block.kind) {
|
|
819
1161
|
case "heading":
|
|
820
|
-
return /* @__PURE__ */
|
|
1162
|
+
return /* @__PURE__ */ jsx4("h3", { className: "fillo-heading", children: block.text });
|
|
821
1163
|
case "paragraph":
|
|
822
|
-
return /* @__PURE__ */
|
|
1164
|
+
return /* @__PURE__ */ jsx4("p", { className: "fillo-paragraph", children: block.text });
|
|
823
1165
|
case "divider":
|
|
824
|
-
return /* @__PURE__ */
|
|
1166
|
+
return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
|
|
825
1167
|
}
|
|
826
1168
|
}
|
|
1169
|
+
function shouldAutoSubmit(field, value, api) {
|
|
1170
|
+
if (api.form.settings.submitMode !== "auto") return false;
|
|
1171
|
+
if (!api.isLastPage || api.status !== "idle" || api.uploading) return false;
|
|
1172
|
+
const hasDiscreteAnswer = (() => {
|
|
1173
|
+
switch (field.kind) {
|
|
1174
|
+
case "select":
|
|
1175
|
+
case "dropdown":
|
|
1176
|
+
return typeof value === "string" && field.options.some((option) => option.id === value);
|
|
1177
|
+
case "checkbox":
|
|
1178
|
+
return value === true;
|
|
1179
|
+
case "rating":
|
|
1180
|
+
case "linear_scale":
|
|
1181
|
+
return typeof value === "number";
|
|
1182
|
+
default:
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
})();
|
|
1186
|
+
return hasDiscreteAnswer && validateResponse(api.form, { ...api.data, [field.id]: value }).ok;
|
|
1187
|
+
}
|
|
827
1188
|
function BlockRenderer({
|
|
828
1189
|
block,
|
|
829
1190
|
api,
|
|
@@ -831,7 +1192,8 @@ function BlockRenderer({
|
|
|
831
1192
|
customComponents
|
|
832
1193
|
}) {
|
|
833
1194
|
block = pipeBlock(block, api.data, api.form);
|
|
834
|
-
|
|
1195
|
+
const ids = useFilloFieldIds(block.id);
|
|
1196
|
+
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
835
1197
|
let Component;
|
|
836
1198
|
if (block.kind === "custom") {
|
|
837
1199
|
Component = customComponents?.[block.component] ?? components?.custom;
|
|
@@ -847,14 +1209,18 @@ function BlockRenderer({
|
|
|
847
1209
|
} else {
|
|
848
1210
|
Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
|
|
849
1211
|
}
|
|
850
|
-
return /* @__PURE__ */
|
|
1212
|
+
return /* @__PURE__ */ jsx4(
|
|
851
1213
|
Component,
|
|
852
1214
|
{
|
|
853
1215
|
field: block,
|
|
854
1216
|
value: api.data[block.id],
|
|
855
1217
|
error: api.errors[block.id],
|
|
856
|
-
setValue: (v) =>
|
|
857
|
-
|
|
1218
|
+
setValue: (v) => {
|
|
1219
|
+
api.setValue(block.id, v);
|
|
1220
|
+
if (shouldAutoSubmit(block, v, api)) void api.submit();
|
|
1221
|
+
},
|
|
1222
|
+
api,
|
|
1223
|
+
ids
|
|
858
1224
|
}
|
|
859
1225
|
);
|
|
860
1226
|
}
|
|
@@ -866,7 +1232,7 @@ function FormField({
|
|
|
866
1232
|
const api = useFillo();
|
|
867
1233
|
const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
|
|
868
1234
|
if (!block) return null;
|
|
869
|
-
return /* @__PURE__ */
|
|
1235
|
+
return /* @__PURE__ */ jsx4(
|
|
870
1236
|
BlockRenderer,
|
|
871
1237
|
{
|
|
872
1238
|
block,
|
|
@@ -878,34 +1244,61 @@ function FormField({
|
|
|
878
1244
|
}
|
|
879
1245
|
|
|
880
1246
|
// src/FilloForm.tsx
|
|
881
|
-
import { Fragment as Fragment2, jsx as
|
|
882
|
-
function
|
|
883
|
-
|
|
1247
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1248
|
+
function themeProps(theme) {
|
|
1249
|
+
const safeTheme = normalizeFormTheme(theme);
|
|
1250
|
+
if (!safeTheme) return {};
|
|
884
1251
|
const style = {};
|
|
885
|
-
if (
|
|
886
|
-
if (
|
|
887
|
-
if (
|
|
888
|
-
if (
|
|
889
|
-
if (
|
|
890
|
-
|
|
1252
|
+
if (safeTheme.primary) style["--fillo-primary"] = safeTheme.primary;
|
|
1253
|
+
if (safeTheme.background) style["--fillo-bg"] = safeTheme.background;
|
|
1254
|
+
if (safeTheme.text) style["--fillo-text"] = safeTheme.text;
|
|
1255
|
+
if (safeTheme.radius) style["--fillo-radius"] = safeTheme.radius;
|
|
1256
|
+
if (safeTheme.fontFamily) style["--fillo-font"] = safeTheme.fontFamily;
|
|
1257
|
+
const hasColorTheme = Boolean(safeTheme.primary || safeTheme.background || safeTheme.text);
|
|
1258
|
+
return {
|
|
1259
|
+
...Object.keys(style).length ? { style } : {},
|
|
1260
|
+
...safeTheme.colorScheme ? { "data-fillo-color-scheme": safeTheme.colorScheme } : {},
|
|
1261
|
+
...hasColorTheme ? { "data-fillo-has-color-theme": "" } : {}
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
function safeHttpUrl(href) {
|
|
1265
|
+
try {
|
|
1266
|
+
if (!href) return null;
|
|
1267
|
+
const url = new URL(href);
|
|
1268
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
1269
|
+
return url.toString();
|
|
1270
|
+
} catch {
|
|
1271
|
+
return null;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function badgeUrl(href) {
|
|
1275
|
+
const url = new URL(safeHttpUrl(href) ?? "https://fillo.so");
|
|
1276
|
+
url.searchParams.set("utm_source", "form_badge");
|
|
1277
|
+
return url.toString();
|
|
1278
|
+
}
|
|
1279
|
+
function isAutoSubmitBlock(block) {
|
|
1280
|
+
return isField2(block) && (block.kind === "select" || block.kind === "dropdown" || block.kind === "checkbox" || block.kind === "rating" || block.kind === "linear_scale");
|
|
1281
|
+
}
|
|
1282
|
+
function needsExplicitSubmit(blocks) {
|
|
1283
|
+
return blocks.some((block) => isField2(block) && block.kind !== "hidden" && !isAutoSubmitBlock(block));
|
|
891
1284
|
}
|
|
892
1285
|
function FilloForm(props) {
|
|
893
1286
|
const { form, formId } = props;
|
|
894
|
-
const client =
|
|
1287
|
+
const client = useMemo2(
|
|
895
1288
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
896
1289
|
[props.client, formId]
|
|
897
1290
|
);
|
|
898
|
-
const [fetched, setFetched] =
|
|
899
|
-
const [fetchedTheme, setFetchedTheme] =
|
|
900
|
-
const [loadError, setLoadError] =
|
|
901
|
-
const [closed, setClosed] =
|
|
902
|
-
const [
|
|
1291
|
+
const [fetched, setFetched] = useState6(null);
|
|
1292
|
+
const [fetchedTheme, setFetchedTheme] = useState6(null);
|
|
1293
|
+
const [loadError, setLoadError] = useState6(null);
|
|
1294
|
+
const [closed, setClosed] = useState6(false);
|
|
1295
|
+
const [fetchedBranding, setFetchedBranding] = useState6(null);
|
|
903
1296
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
904
1297
|
const inlineSchema = codeForm ? codeForm.schema : form;
|
|
905
1298
|
const onError = props.onError;
|
|
906
1299
|
const syncedFormId = useCodeFormSync(form, client, onError);
|
|
907
1300
|
const needsFetch = !inlineSchema && Boolean(client && formId);
|
|
908
|
-
|
|
1301
|
+
useEffect7(() => {
|
|
909
1302
|
if (!needsFetch || !client || !formId) return;
|
|
910
1303
|
let cancelled = false;
|
|
911
1304
|
setFetched(null);
|
|
@@ -915,7 +1308,7 @@ function FilloForm(props) {
|
|
|
915
1308
|
setFetched(published.schema);
|
|
916
1309
|
setFetchedTheme(published.theme);
|
|
917
1310
|
setClosed(Boolean(published.closed));
|
|
918
|
-
|
|
1311
|
+
setFetchedBranding(published.branding ?? null);
|
|
919
1312
|
}).catch((err) => {
|
|
920
1313
|
if (cancelled) return;
|
|
921
1314
|
const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
|
|
@@ -926,35 +1319,42 @@ function FilloForm(props) {
|
|
|
926
1319
|
cancelled = true;
|
|
927
1320
|
};
|
|
928
1321
|
}, [needsFetch, client, formId, onError]);
|
|
929
|
-
const
|
|
1322
|
+
const rawSchema = inlineSchema ?? fetched;
|
|
1323
|
+
const normalized = useMemo2(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1324
|
+
const schema = normalized?.ok ? normalized.schema : null;
|
|
930
1325
|
const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
|
|
1326
|
+
if (normalized && !normalized.ok) {
|
|
1327
|
+
const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1328
|
+
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
1329
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: "This form could not be rendered." });
|
|
1330
|
+
}
|
|
931
1331
|
if (loadError) {
|
|
932
1332
|
if (props.renderError) {
|
|
933
|
-
return /* @__PURE__ */
|
|
1333
|
+
return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
|
|
934
1334
|
}
|
|
935
1335
|
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__ */
|
|
1336
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
|
|
937
1337
|
}
|
|
938
1338
|
if (closed) {
|
|
939
|
-
return /* @__PURE__ */
|
|
1339
|
+
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
1340
|
}
|
|
941
1341
|
if (!schema) {
|
|
942
|
-
return /* @__PURE__ */
|
|
1342
|
+
return /* @__PURE__ */ jsxs5(
|
|
943
1343
|
"div",
|
|
944
1344
|
{
|
|
945
1345
|
className: `fillo-form fillo-form--loading ${props.className ?? ""}`,
|
|
946
1346
|
"aria-busy": "true",
|
|
947
|
-
|
|
1347
|
+
...themeProps(theme),
|
|
948
1348
|
children: [
|
|
949
|
-
/* @__PURE__ */
|
|
950
|
-
/* @__PURE__ */
|
|
951
|
-
/* @__PURE__ */
|
|
1349
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-skeleton" }),
|
|
1350
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-skeleton" }),
|
|
1351
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-skeleton fillo-skeleton--short" })
|
|
952
1352
|
]
|
|
953
1353
|
}
|
|
954
1354
|
);
|
|
955
1355
|
}
|
|
956
|
-
const poweredBy =
|
|
957
|
-
return /* @__PURE__ */
|
|
1356
|
+
const poweredBy = fetchedBranding?.poweredBy ?? true;
|
|
1357
|
+
return /* @__PURE__ */ jsx5(
|
|
958
1358
|
ResolvedForm,
|
|
959
1359
|
{
|
|
960
1360
|
...props,
|
|
@@ -962,36 +1362,30 @@ function FilloForm(props) {
|
|
|
962
1362
|
schema,
|
|
963
1363
|
theme,
|
|
964
1364
|
formId: syncedFormId ?? formId,
|
|
965
|
-
poweredBy
|
|
1365
|
+
poweredBy,
|
|
1366
|
+
brandLabel: fetchedBranding?.label,
|
|
1367
|
+
brandHref: fetchedBranding?.href
|
|
966
1368
|
}
|
|
967
1369
|
);
|
|
968
1370
|
}
|
|
969
|
-
function PoweredBy() {
|
|
970
|
-
return /* @__PURE__ */
|
|
971
|
-
"a",
|
|
972
|
-
{
|
|
973
|
-
className: "fillo-powered",
|
|
974
|
-
href: "https://fillo.so?utm_source=form_badge",
|
|
975
|
-
target: "_blank",
|
|
976
|
-
rel: "noopener",
|
|
977
|
-
children: "Powered by Fillo"
|
|
978
|
-
}
|
|
979
|
-
);
|
|
1371
|
+
function PoweredBy({ label, href }) {
|
|
1372
|
+
return /* @__PURE__ */ jsx5("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
|
|
980
1373
|
}
|
|
981
1374
|
function isDevEnv() {
|
|
982
1375
|
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
983
1376
|
}
|
|
984
1377
|
function DevNoClientNotice() {
|
|
985
|
-
return /* @__PURE__ */
|
|
986
|
-
/* @__PURE__ */
|
|
1378
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
1379
|
+
/* @__PURE__ */ jsx5("strong", { children: "No Fillo client connected." }),
|
|
987
1380
|
" This form is render-only and won\u2019t save responses. Pass a ",
|
|
988
|
-
/* @__PURE__ */
|
|
1381
|
+
/* @__PURE__ */ jsx5("code", { children: "client" }),
|
|
989
1382
|
" to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
|
|
990
1383
|
] });
|
|
991
1384
|
}
|
|
992
1385
|
function ResolvedForm(props) {
|
|
993
1386
|
const { schema, theme } = props;
|
|
994
|
-
const
|
|
1387
|
+
const instanceId = useId2().replace(/:/g, "");
|
|
1388
|
+
const hpRef = useRef5("");
|
|
995
1389
|
const api = useFilloController({
|
|
996
1390
|
form: schema,
|
|
997
1391
|
formId: props.formId,
|
|
@@ -1002,31 +1396,33 @@ function ResolvedForm(props) {
|
|
|
1002
1396
|
skipValidation: props.skipValidation,
|
|
1003
1397
|
getHoneypot: () => hpRef.current
|
|
1004
1398
|
});
|
|
1005
|
-
const
|
|
1399
|
+
const themed = useMemo2(() => themeProps(theme), [theme]);
|
|
1006
1400
|
const settings = schema.settings;
|
|
1007
1401
|
const submitted = api.status === "submitted";
|
|
1008
|
-
|
|
1402
|
+
useEffect7(() => {
|
|
1009
1403
|
if (!submitted || !settings.redirectUrl || typeof window === "undefined") return;
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
}
|
|
1404
|
+
const url = safeHttpUrl(settings.redirectUrl);
|
|
1405
|
+
if (url) window.location.assign(url);
|
|
1013
1406
|
}, [submitted, settings.redirectUrl]);
|
|
1014
1407
|
if (api.status === "submitted") {
|
|
1015
|
-
return /* @__PURE__ */
|
|
1016
|
-
props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */
|
|
1017
|
-
/* @__PURE__ */
|
|
1018
|
-
/* @__PURE__ */
|
|
1019
|
-
/* @__PURE__ */
|
|
1408
|
+
return /* @__PURE__ */ jsxs5("div", { className: `fillo-form fillo-form--success ${props.className ?? ""}`, ...themed, children: [
|
|
1409
|
+
props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5("div", { className: "fillo-success", children: [
|
|
1410
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
|
|
1411
|
+
/* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? "Thanks!" }),
|
|
1412
|
+
/* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? "Your response has been recorded." })
|
|
1020
1413
|
] }),
|
|
1021
|
-
props.poweredBy && /* @__PURE__ */
|
|
1414
|
+
props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1022
1415
|
] });
|
|
1023
1416
|
}
|
|
1024
1417
|
const multiPage = api.pageCount > 1;
|
|
1025
|
-
|
|
1418
|
+
const autoSubmit = settings.submitMode === "auto";
|
|
1419
|
+
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(api.blocks);
|
|
1420
|
+
const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
|
|
1421
|
+
return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
|
|
1026
1422
|
"form",
|
|
1027
1423
|
{
|
|
1028
1424
|
className: `fillo-form ${props.className ?? ""}`,
|
|
1029
|
-
|
|
1425
|
+
...themed,
|
|
1030
1426
|
noValidate: true,
|
|
1031
1427
|
onSubmit: (e) => {
|
|
1032
1428
|
e.preventDefault();
|
|
@@ -1034,8 +1430,8 @@ function ResolvedForm(props) {
|
|
|
1034
1430
|
else api.next();
|
|
1035
1431
|
},
|
|
1036
1432
|
children: [
|
|
1037
|
-
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */
|
|
1038
|
-
multiPage && settings.showProgress !== false && /* @__PURE__ */
|
|
1433
|
+
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
1434
|
+
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
|
|
1039
1435
|
"div",
|
|
1040
1436
|
{
|
|
1041
1437
|
className: "fillo-progress-track",
|
|
@@ -1043,7 +1439,7 @@ function ResolvedForm(props) {
|
|
|
1043
1439
|
"aria-valuemin": 0,
|
|
1044
1440
|
"aria-valuemax": api.pageCount,
|
|
1045
1441
|
"aria-valuenow": api.pageIndex + 1,
|
|
1046
|
-
children: /* @__PURE__ */
|
|
1442
|
+
children: /* @__PURE__ */ jsx5(
|
|
1047
1443
|
"div",
|
|
1048
1444
|
{
|
|
1049
1445
|
className: "fillo-progress-fill",
|
|
@@ -1052,12 +1448,12 @@ function ResolvedForm(props) {
|
|
|
1052
1448
|
)
|
|
1053
1449
|
}
|
|
1054
1450
|
),
|
|
1055
|
-
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */
|
|
1056
|
-
/* @__PURE__ */
|
|
1057
|
-
schema.description && /* @__PURE__ */
|
|
1451
|
+
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: "fillo-header", children: [
|
|
1452
|
+
/* @__PURE__ */ jsx5("h1", { className: "fillo-title", children: schema.title }),
|
|
1453
|
+
schema.description && /* @__PURE__ */ jsx5("p", { className: "fillo-form-description", children: schema.description })
|
|
1058
1454
|
] }),
|
|
1059
|
-
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */
|
|
1060
|
-
/* @__PURE__ */
|
|
1455
|
+
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: "fillo-page-title", children: api.page.title }),
|
|
1456
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-blocks", children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
|
|
1061
1457
|
BlockRenderer,
|
|
1062
1458
|
{
|
|
1063
1459
|
block,
|
|
@@ -1067,7 +1463,7 @@ function ResolvedForm(props) {
|
|
|
1067
1463
|
},
|
|
1068
1464
|
block.id
|
|
1069
1465
|
)) }),
|
|
1070
|
-
/* @__PURE__ */
|
|
1466
|
+
/* @__PURE__ */ jsx5(
|
|
1071
1467
|
"input",
|
|
1072
1468
|
{
|
|
1073
1469
|
type: "text",
|
|
@@ -1082,9 +1478,10 @@ function ResolvedForm(props) {
|
|
|
1082
1478
|
}
|
|
1083
1479
|
}
|
|
1084
1480
|
),
|
|
1085
|
-
/* @__PURE__ */
|
|
1086
|
-
|
|
1087
|
-
/* @__PURE__ */
|
|
1481
|
+
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." }),
|
|
1482
|
+
showFooter && /* @__PURE__ */ jsxs5("footer", { className: "fillo-footer", children: [
|
|
1483
|
+
multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-button fillo-button--ghost", onClick: api.back, children: "Back" }),
|
|
1484
|
+
showPrimaryButton && /* @__PURE__ */ jsx5(
|
|
1088
1485
|
"button",
|
|
1089
1486
|
{
|
|
1090
1487
|
type: "submit",
|
|
@@ -1094,17 +1491,26 @@ function ResolvedForm(props) {
|
|
|
1094
1491
|
}
|
|
1095
1492
|
)
|
|
1096
1493
|
] }),
|
|
1097
|
-
props.poweredBy && /* @__PURE__ */
|
|
1494
|
+
props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1098
1495
|
]
|
|
1099
1496
|
}
|
|
1100
|
-
) });
|
|
1497
|
+
) }) });
|
|
1101
1498
|
}
|
|
1102
1499
|
|
|
1103
1500
|
// src/provider.tsx
|
|
1104
|
-
import {
|
|
1501
|
+
import { useId as useId3, useMemo as useMemo3 } from "react";
|
|
1502
|
+
import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2 } from "@usefillo/core";
|
|
1503
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1105
1504
|
function FilloProvider({ children, form, formId, client, ...options }) {
|
|
1505
|
+
const instanceId = useId3().replace(/:/g, "");
|
|
1106
1506
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1107
|
-
const
|
|
1507
|
+
const rawSchema = codeForm ? codeForm.schema : form;
|
|
1508
|
+
const schemaKey = JSON.stringify(rawSchema);
|
|
1509
|
+
const normalized = useMemo3(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
|
|
1510
|
+
if (!normalized.ok) {
|
|
1511
|
+
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1512
|
+
}
|
|
1513
|
+
const schema = normalized.schema;
|
|
1108
1514
|
const syncedFormId = useCodeFormSync(form, client);
|
|
1109
1515
|
const api = useFilloController({
|
|
1110
1516
|
...options,
|
|
@@ -1115,7 +1521,7 @@ function FilloProvider({ children, form, formId, client, ...options }) {
|
|
|
1115
1521
|
// a paid capability the server enforces when these responses are submitted.
|
|
1116
1522
|
surface: "headless"
|
|
1117
1523
|
});
|
|
1118
|
-
return /* @__PURE__ */
|
|
1524
|
+
return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children }) });
|
|
1119
1525
|
}
|
|
1120
1526
|
|
|
1121
1527
|
// src/index.ts
|
|
@@ -1123,12 +1529,12 @@ import {
|
|
|
1123
1529
|
createClient as createClient2,
|
|
1124
1530
|
provisionWorkspace,
|
|
1125
1531
|
FilloClient,
|
|
1126
|
-
FilloError as
|
|
1532
|
+
FilloError as FilloError4
|
|
1127
1533
|
} from "@usefillo/core";
|
|
1128
1534
|
export {
|
|
1129
1535
|
BlockRenderer,
|
|
1130
1536
|
FilloClient,
|
|
1131
|
-
|
|
1537
|
+
FilloError4 as FilloError,
|
|
1132
1538
|
FileUploadField as FilloFileUpload,
|
|
1133
1539
|
FilloForm,
|
|
1134
1540
|
FilloProvider,
|