@usefillo/react 0.5.3 → 0.6.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/dist/index.d.ts +120 -9
- package/dist/index.js +585 -155
- package/dist/styles.css +26 -13
- package/dist/styles.unlayered.css +700 -0
- package/package.json +10 -6
package/dist/index.js
CHANGED
|
@@ -1,23 +1,66 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/FilloForm.tsx
|
|
4
|
-
import { useEffect as useEffect7, useId as useId2, useMemo as
|
|
4
|
+
import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef5, useState as useState6 } from "react";
|
|
5
5
|
import {
|
|
6
6
|
FilloError as FilloError2,
|
|
7
7
|
createClient,
|
|
8
|
-
|
|
8
|
+
needsExplicitSubmit,
|
|
9
9
|
normalizeFormSchema,
|
|
10
10
|
normalizeFormTheme,
|
|
11
11
|
visibleFields as visibleFields2
|
|
12
12
|
} from "@usefillo/core";
|
|
13
|
+
import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
|
|
14
|
+
|
|
15
|
+
// src/appearance.tsx
|
|
16
|
+
import { createContext, useContext } from "react";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_STRINGS,
|
|
19
|
+
slotClass
|
|
20
|
+
} from "@usefillo/core";
|
|
21
|
+
var FilloAppearanceContext = createContext(void 0);
|
|
22
|
+
function useSlotClass(base, state) {
|
|
23
|
+
const appearance = useContext(FilloAppearanceContext);
|
|
24
|
+
return slotClass(base, appearance, state);
|
|
25
|
+
}
|
|
26
|
+
function useFilloAppearance() {
|
|
27
|
+
return useContext(FilloAppearanceContext);
|
|
28
|
+
}
|
|
29
|
+
function useFieldSlots(field, error) {
|
|
30
|
+
const appearance = useContext(FilloAppearanceContext);
|
|
31
|
+
const state = {
|
|
32
|
+
kind: field.kind,
|
|
33
|
+
fieldId: field.id,
|
|
34
|
+
invalid: Boolean(error),
|
|
35
|
+
required: Boolean(field.required)
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
wrapperProps: (base) => ({
|
|
39
|
+
className: slotClass(base, appearance, { slot: "field", ...state }),
|
|
40
|
+
"data-fillo": "field",
|
|
41
|
+
"data-field": field.id,
|
|
42
|
+
"data-kind": field.kind,
|
|
43
|
+
"data-invalid": error ? "" : void 0,
|
|
44
|
+
"data-required": field.required ? "" : void 0
|
|
45
|
+
}),
|
|
46
|
+
label: slotClass("fillo-label", appearance, { slot: "label", ...state }),
|
|
47
|
+
description: slotClass("fillo-description", appearance, { slot: "fieldDescription", ...state }),
|
|
48
|
+
error: slotClass("fillo-error", appearance, { slot: "error", ...state }),
|
|
49
|
+
control: (base) => slotClass(base, appearance, { slot: "control", ...state })
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
var FilloStringsContext = createContext(DEFAULT_STRINGS);
|
|
53
|
+
function useStrings() {
|
|
54
|
+
return useContext(FilloStringsContext);
|
|
55
|
+
}
|
|
13
56
|
|
|
14
57
|
// src/context.ts
|
|
15
58
|
import { allFields } from "@usefillo/core";
|
|
16
|
-
import { createContext, useContext, useId, useMemo } from "react";
|
|
17
|
-
var FilloContext =
|
|
18
|
-
var FilloInstanceIdContext =
|
|
59
|
+
import { createContext as createContext2, useContext as useContext2, useId, useMemo } from "react";
|
|
60
|
+
var FilloContext = createContext2(null);
|
|
61
|
+
var FilloInstanceIdContext = createContext2(null);
|
|
19
62
|
function useFilloInstanceId() {
|
|
20
|
-
const instanceId =
|
|
63
|
+
const instanceId = useContext2(FilloInstanceIdContext);
|
|
21
64
|
const fallbackId = useId().replace(/:/g, "");
|
|
22
65
|
return instanceId ?? fallbackId;
|
|
23
66
|
}
|
|
@@ -32,7 +75,7 @@ function createFilloFieldIds(instanceId, fieldId) {
|
|
|
32
75
|
};
|
|
33
76
|
}
|
|
34
77
|
function useFillo() {
|
|
35
|
-
const api =
|
|
78
|
+
const api = useContext2(FilloContext);
|
|
36
79
|
if (!api) throw new Error("useFillo must be used inside <FilloForm> or <FilloProvider>");
|
|
37
80
|
return api;
|
|
38
81
|
}
|
|
@@ -75,7 +118,9 @@ function useFilloController(options) {
|
|
|
75
118
|
onSubmitted: (id, d) => optsRef.current.onSubmitted?.(id, d),
|
|
76
119
|
getHoneypot: () => optsRef.current.getHoneypot?.() ?? "",
|
|
77
120
|
skipValidation: options.skipValidation,
|
|
78
|
-
surface: options.surface ?? "
|
|
121
|
+
surface: options.surface ?? "headless",
|
|
122
|
+
// Wrapped so the engine (created once) always calls the latest resolver.
|
|
123
|
+
resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0
|
|
79
124
|
});
|
|
80
125
|
}
|
|
81
126
|
const store = storeRef.current;
|
|
@@ -109,16 +154,33 @@ import {
|
|
|
109
154
|
} from "@usefillo/core";
|
|
110
155
|
import { useEffect as useEffect2, useState } from "react";
|
|
111
156
|
import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
|
|
157
|
+
var noticedSync = /* @__PURE__ */ new Set();
|
|
158
|
+
function noticeSync(handle, kind, message) {
|
|
159
|
+
const key = `${handle}:${kind}`;
|
|
160
|
+
if (noticedSync.has(key)) return;
|
|
161
|
+
noticedSync.add(key);
|
|
162
|
+
console.info(message);
|
|
163
|
+
}
|
|
112
164
|
function useCodeFormSync(form, client, onError) {
|
|
113
165
|
const codeForm = isCodeForm(form) ? form : null;
|
|
114
|
-
const [
|
|
166
|
+
const [synced, setSynced] = useState({ formId: null, status: null });
|
|
115
167
|
const hasKey = Boolean(client?.key);
|
|
116
168
|
const handle = codeForm?.id;
|
|
117
169
|
const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
|
|
118
170
|
useEffect2(() => {
|
|
119
171
|
if (!codeForm || !client?.key) return;
|
|
120
172
|
let cancelled = false;
|
|
121
|
-
syncCodeForm(client, codeForm).then((r) =>
|
|
173
|
+
syncCodeForm(client, codeForm).then((r) => {
|
|
174
|
+
if (cancelled) return;
|
|
175
|
+
setSynced({ formId: r.formId, status: r.status ?? null });
|
|
176
|
+
const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
|
|
177
|
+
if (r.warning) console.warn(`[fillo] ${r.warning}`);
|
|
178
|
+
if (r.staged) {
|
|
179
|
+
noticeSync(codeForm.id, "staged", `[fillo] "${codeForm.id}": changes staged as a draft \u2014 publish them at ${dashboardUrl}`);
|
|
180
|
+
} else if (r.status === "draft") {
|
|
181
|
+
noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
|
|
182
|
+
}
|
|
183
|
+
}).catch((err) => {
|
|
122
184
|
const error = err instanceof FilloError ? err : new FilloError(String(err), 0);
|
|
123
185
|
console.warn("[fillo] form sync failed:", error.message);
|
|
124
186
|
if (!cancelled) onError?.(error);
|
|
@@ -127,17 +189,18 @@ function useCodeFormSync(form, client, onError) {
|
|
|
127
189
|
cancelled = true;
|
|
128
190
|
};
|
|
129
191
|
}, [hasKey, handle, contentKey, client, onError]);
|
|
130
|
-
return
|
|
192
|
+
return synced;
|
|
131
193
|
}
|
|
132
194
|
|
|
133
195
|
// src/fields.tsx
|
|
134
|
-
import { isField, pipeBlock,
|
|
135
|
-
import { memo, useEffect as useEffect6, useState as useState5 } from "react";
|
|
196
|
+
import { isField, pipeBlock, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
|
|
197
|
+
import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
136
198
|
|
|
137
199
|
// src/phone.tsx
|
|
138
200
|
import {
|
|
139
201
|
PHONE_COUNTRIES,
|
|
140
202
|
countryByIso,
|
|
203
|
+
countryByTimeZone,
|
|
141
204
|
digitsOnly,
|
|
142
205
|
flagEmoji,
|
|
143
206
|
formatNational,
|
|
@@ -152,17 +215,60 @@ function localeCountry() {
|
|
|
152
215
|
const region = tag.split("-")[1];
|
|
153
216
|
return region && /^[A-Za-z]{2}$/.test(region) ? region.toUpperCase() : void 0;
|
|
154
217
|
}
|
|
218
|
+
function browserTimeZone() {
|
|
219
|
+
if (typeof Intl === "undefined") return void 0;
|
|
220
|
+
try {
|
|
221
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
222
|
+
return typeof timeZone === "string" && timeZone ? timeZone : void 0;
|
|
223
|
+
} catch {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
155
227
|
function nationalFor(stored, country) {
|
|
156
228
|
const digits = digitsOnly(stored);
|
|
157
229
|
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
|
158
230
|
}
|
|
231
|
+
var PHONE_POPOVER_VIEWPORT_GAP = 8;
|
|
232
|
+
function positionPhonePopover(anchor, popover) {
|
|
233
|
+
if (!anchor || !popover || typeof window === "undefined") return "below";
|
|
234
|
+
const gap = PHONE_POPOVER_VIEWPORT_GAP;
|
|
235
|
+
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
236
|
+
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
237
|
+
const anchorRect = anchor.getBoundingClientRect();
|
|
238
|
+
const spaceBelow = Math.max(0, viewportHeight - anchorRect.bottom - gap);
|
|
239
|
+
const spaceAbove = Math.max(0, anchorRect.top - gap);
|
|
240
|
+
const placement = spaceBelow < popover.scrollHeight && spaceAbove > spaceBelow ? "above" : "below";
|
|
241
|
+
const availableHeight = placement === "above" ? spaceAbove : spaceBelow;
|
|
242
|
+
const maxHeight = Math.max(0, Math.min(availableHeight, viewportHeight - gap * 2));
|
|
243
|
+
const maxWidth = Math.max(0, viewportWidth - gap * 2);
|
|
244
|
+
popover.style.setProperty("--fillo-phone-popover-max-height", `${Math.floor(maxHeight)}px`);
|
|
245
|
+
popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
|
|
246
|
+
popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
|
|
247
|
+
const width = Math.min(popover.offsetWidth, maxWidth);
|
|
248
|
+
let offsetX = 0;
|
|
249
|
+
const rightOverflow = anchorRect.left + width - (viewportWidth - gap);
|
|
250
|
+
if (rightOverflow > 0) offsetX -= rightOverflow;
|
|
251
|
+
const leftOverflow = gap - (anchorRect.left + offsetX);
|
|
252
|
+
if (leftOverflow > 0) offsetX += leftOverflow;
|
|
253
|
+
popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
|
|
254
|
+
return placement;
|
|
255
|
+
}
|
|
159
256
|
function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
160
257
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
258
|
+
const slots = useFieldSlots(field, error);
|
|
259
|
+
const strings = useStrings();
|
|
161
260
|
const schema = field;
|
|
162
261
|
const stored = typeof value === "string" ? value : "";
|
|
163
262
|
const [country, setCountry] = useState2(
|
|
164
|
-
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ??
|
|
263
|
+
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? PHONE_COUNTRIES[0]
|
|
165
264
|
);
|
|
265
|
+
const touched = useRef2(false);
|
|
266
|
+
useEffect3(() => {
|
|
267
|
+
if (touched.current) return;
|
|
268
|
+
if (parsePhone(stored).country || countryByIso(schema.defaultCountry)) return;
|
|
269
|
+
const guess = countryByTimeZone(browserTimeZone()) ?? countryByIso(localeCountry());
|
|
270
|
+
if (guess) setCountry(guess);
|
|
271
|
+
}, []);
|
|
166
272
|
const national = nationalFor(stored, country);
|
|
167
273
|
const display = formatNational(country, national);
|
|
168
274
|
const inputRef = useRef2(null);
|
|
@@ -185,6 +291,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
185
291
|
});
|
|
186
292
|
const commit = (c, nat) => setValue(nat ? toE164(c, nat) : "");
|
|
187
293
|
function onInput(e) {
|
|
294
|
+
touched.current = true;
|
|
188
295
|
const el = e.target;
|
|
189
296
|
const raw = el.value;
|
|
190
297
|
const caret = el.selectionStart ?? raw.length;
|
|
@@ -198,17 +305,18 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
198
305
|
commit(country, digitsOnly(raw));
|
|
199
306
|
}
|
|
200
307
|
function pickCountry(c) {
|
|
308
|
+
touched.current = true;
|
|
201
309
|
setCountry(c);
|
|
202
310
|
commit(c, national);
|
|
203
311
|
inputRef.current?.focus();
|
|
204
312
|
}
|
|
205
313
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
206
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
207
|
-
/* @__PURE__ */ jsxs("label", { className: "fillo
|
|
314
|
+
return /* @__PURE__ */ jsxs("div", { ...slots.wrapperProps(`fillo-field fillo-field--phone${error ? " fillo-field--error" : ""}`), children: [
|
|
315
|
+
/* @__PURE__ */ jsxs("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
208
316
|
field.label,
|
|
209
|
-
!field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children:
|
|
317
|
+
!field.required && /* @__PURE__ */ jsx("span", { className: "fillo-optional", children: strings.optional })
|
|
210
318
|
] }),
|
|
211
|
-
field.description && /* @__PURE__ */ jsx("p", { className: "fillo
|
|
319
|
+
field.description && /* @__PURE__ */ jsx("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
212
320
|
/* @__PURE__ */ jsxs("div", { className: "fillo-phone", children: [
|
|
213
321
|
/* @__PURE__ */ jsx(CountrySelect, { country, onPick: pickCountry, labelledBy: ids.labelId }),
|
|
214
322
|
/* @__PURE__ */ jsx(
|
|
@@ -216,7 +324,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
216
324
|
{
|
|
217
325
|
ref: inputRef,
|
|
218
326
|
id: ids.inputId,
|
|
219
|
-
className: "fillo-input fillo-phone-input",
|
|
327
|
+
className: slots.control("fillo-input fillo-phone-input"),
|
|
328
|
+
"data-fillo": "control",
|
|
220
329
|
type: "tel",
|
|
221
330
|
inputMode: "tel",
|
|
222
331
|
autoComplete: "tel-national",
|
|
@@ -229,7 +338,7 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
229
338
|
}
|
|
230
339
|
)
|
|
231
340
|
] }),
|
|
232
|
-
error && /* @__PURE__ */ jsx("p", { className: "fillo
|
|
341
|
+
error && /* @__PURE__ */ jsx("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
233
342
|
] });
|
|
234
343
|
}
|
|
235
344
|
function CountrySelect({
|
|
@@ -240,7 +349,10 @@ function CountrySelect({
|
|
|
240
349
|
const [open, setOpen] = useState2(false);
|
|
241
350
|
const [query, setQuery] = useState2("");
|
|
242
351
|
const [active, setActive] = useState2(0);
|
|
352
|
+
const [placement, setPlacement] = useState2("below");
|
|
243
353
|
const rootRef = useRef2(null);
|
|
354
|
+
const triggerRef = useRef2(null);
|
|
355
|
+
const popoverRef = useRef2(null);
|
|
244
356
|
const searchRef = useRef2(null);
|
|
245
357
|
const listId = `${labelledBy}-countries`;
|
|
246
358
|
const matches = useMemo3(() => {
|
|
@@ -266,6 +378,17 @@ function CountrySelect({
|
|
|
266
378
|
document.addEventListener("mousedown", onDown);
|
|
267
379
|
return () => document.removeEventListener("mousedown", onDown);
|
|
268
380
|
}, [open]);
|
|
381
|
+
useLayoutEffect(() => {
|
|
382
|
+
if (!open) return;
|
|
383
|
+
const update = () => setPlacement(positionPhonePopover(rootRef.current, popoverRef.current));
|
|
384
|
+
update();
|
|
385
|
+
window.addEventListener("resize", update);
|
|
386
|
+
window.addEventListener("scroll", update, true);
|
|
387
|
+
return () => {
|
|
388
|
+
window.removeEventListener("resize", update);
|
|
389
|
+
window.removeEventListener("scroll", update, true);
|
|
390
|
+
};
|
|
391
|
+
}, [open, matches.length]);
|
|
269
392
|
useEffect3(() => {
|
|
270
393
|
if (!open) return;
|
|
271
394
|
document.getElementById(`${listId}-opt-${active}`)?.scrollIntoView({ block: "nearest" });
|
|
@@ -288,12 +411,14 @@ function CountrySelect({
|
|
|
288
411
|
} else if (e.key === "Escape") {
|
|
289
412
|
e.preventDefault();
|
|
290
413
|
setOpen(false);
|
|
414
|
+
triggerRef.current?.focus();
|
|
291
415
|
}
|
|
292
416
|
}
|
|
293
417
|
return /* @__PURE__ */ jsxs("div", { className: "fillo-phone-country", ref: rootRef, children: [
|
|
294
418
|
/* @__PURE__ */ jsxs(
|
|
295
419
|
"button",
|
|
296
420
|
{
|
|
421
|
+
ref: triggerRef,
|
|
297
422
|
type: "button",
|
|
298
423
|
className: "fillo-phone-flag",
|
|
299
424
|
"aria-haspopup": "listbox",
|
|
@@ -310,7 +435,7 @@ function CountrySelect({
|
|
|
310
435
|
]
|
|
311
436
|
}
|
|
312
437
|
),
|
|
313
|
-
open && /* @__PURE__ */ jsxs("div", { className:
|
|
438
|
+
open && /* @__PURE__ */ jsxs("div", { ref: popoverRef, className: `fillo-phone-popover fillo-phone-popover--${placement}`, children: [
|
|
314
439
|
/* @__PURE__ */ jsx(
|
|
315
440
|
"input",
|
|
316
441
|
{
|
|
@@ -366,6 +491,8 @@ import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } fro
|
|
|
366
491
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
367
492
|
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
368
493
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
494
|
+
const slots = useFieldSlots(field, error);
|
|
495
|
+
const strings = useStrings();
|
|
369
496
|
const typedInputId = `${ids.inputId}-typed`;
|
|
370
497
|
const canvasRef = useRef3(null);
|
|
371
498
|
const drawing = useRef3(false);
|
|
@@ -447,12 +574,12 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
447
574
|
setValue(null);
|
|
448
575
|
}
|
|
449
576
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
450
|
-
return /* @__PURE__ */ jsxs2("div", {
|
|
451
|
-
/* @__PURE__ */ jsxs2("span", { className: "fillo
|
|
577
|
+
return /* @__PURE__ */ jsxs2("div", { ...slots.wrapperProps(`fillo-field fillo-field--signature${error ? " fillo-field--error" : ""}`), children: [
|
|
578
|
+
/* @__PURE__ */ jsxs2("span", { className: slots.label, "data-fillo": "label", id: ids.labelId, children: [
|
|
452
579
|
field.label,
|
|
453
|
-
!field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children:
|
|
580
|
+
!field.required && /* @__PURE__ */ jsx2("span", { className: "fillo-optional", children: strings.optional })
|
|
454
581
|
] }),
|
|
455
|
-
field.description && /* @__PURE__ */ jsx2("p", { className: "fillo
|
|
582
|
+
field.description && /* @__PURE__ */ jsx2("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
456
583
|
/* @__PURE__ */ jsxs2(
|
|
457
584
|
"div",
|
|
458
585
|
{
|
|
@@ -460,7 +587,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
460
587
|
"aria-labelledby": ids.labelId,
|
|
461
588
|
"aria-describedby": describedBy,
|
|
462
589
|
children: [
|
|
463
|
-
/* @__PURE__ */ jsxs2("div", { className: "fillo-signature", children: [
|
|
590
|
+
/* @__PURE__ */ jsxs2("div", { className: slots.control("fillo-signature"), "data-fillo": "control", children: [
|
|
464
591
|
/* @__PURE__ */ jsx2(
|
|
465
592
|
"canvas",
|
|
466
593
|
{
|
|
@@ -500,7 +627,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
500
627
|
]
|
|
501
628
|
}
|
|
502
629
|
),
|
|
503
|
-
error && /* @__PURE__ */ jsx2("p", { className: "fillo
|
|
630
|
+
error && /* @__PURE__ */ jsx2("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
504
631
|
] });
|
|
505
632
|
}
|
|
506
633
|
|
|
@@ -530,6 +657,8 @@ function formatBytes(bytes) {
|
|
|
530
657
|
}
|
|
531
658
|
function FileUploadField({ field, value, error, setValue, api, ids: providedIds }) {
|
|
532
659
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
660
|
+
const slots = useFieldSlots(field, error);
|
|
661
|
+
const strings = useStrings();
|
|
533
662
|
const schema = field;
|
|
534
663
|
const inputRef = useRef4(null);
|
|
535
664
|
const [inFlight, setInFlight] = useState4([]);
|
|
@@ -602,16 +731,18 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
602
731
|
setValue(files.filter((f) => f.fileId !== fileId));
|
|
603
732
|
}
|
|
604
733
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
605
|
-
return /* @__PURE__ */ jsxs3("div", {
|
|
606
|
-
/* @__PURE__ */ jsxs3("label", { className: "fillo
|
|
734
|
+
return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
|
|
735
|
+
/* @__PURE__ */ jsxs3("label", { className: slots.label, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
607
736
|
field.label,
|
|
608
|
-
!field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children:
|
|
737
|
+
!field.required && /* @__PURE__ */ jsx3("span", { className: "fillo-optional", children: strings.optional })
|
|
609
738
|
] }),
|
|
610
|
-
field.description && /* @__PURE__ */ jsx3("p", { className: "fillo
|
|
739
|
+
field.description && /* @__PURE__ */ jsx3("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
611
740
|
remaining > 0 && /* @__PURE__ */ jsxs3(
|
|
612
741
|
"div",
|
|
613
742
|
{
|
|
614
|
-
|
|
743
|
+
"data-fillo": "control",
|
|
744
|
+
"data-drag-over": dragOver ? "" : void 0,
|
|
745
|
+
className: slots.control(`fillo-dropzone${dragOver ? " fillo-dropzone--over" : ""}${canUpload ? "" : " fillo-dropzone--disabled"}`),
|
|
615
746
|
onDragOver: (e) => {
|
|
616
747
|
e.preventDefault();
|
|
617
748
|
setDragOver(true);
|
|
@@ -625,6 +756,10 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
625
756
|
onClick: () => canUpload && inputRef.current?.click(),
|
|
626
757
|
role: "button",
|
|
627
758
|
tabIndex: 0,
|
|
759
|
+
"aria-labelledby": ids.labelId,
|
|
760
|
+
"aria-required": field.required ? true : void 0,
|
|
761
|
+
"aria-invalid": error ? true : void 0,
|
|
762
|
+
"aria-describedby": describedBy,
|
|
628
763
|
onKeyDown: (e) => {
|
|
629
764
|
if ((e.key === "Enter" || e.key === " ") && canUpload) inputRef.current?.click();
|
|
630
765
|
},
|
|
@@ -636,9 +771,6 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
636
771
|
id: ids.inputId,
|
|
637
772
|
type: "file",
|
|
638
773
|
hidden: true,
|
|
639
|
-
"aria-invalid": error ? true : void 0,
|
|
640
|
-
"aria-required": field.required ? true : void 0,
|
|
641
|
-
"aria-describedby": describedBy,
|
|
642
774
|
multiple: maxFiles > 1,
|
|
643
775
|
accept: schema.accept?.join(","),
|
|
644
776
|
onChange: (e) => handleFiles(e.target.files)
|
|
@@ -659,7 +791,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
659
791
|
]
|
|
660
792
|
}
|
|
661
793
|
),
|
|
662
|
-
/* @__PURE__ */ jsx3(
|
|
794
|
+
/* @__PURE__ */ jsx3(
|
|
795
|
+
"div",
|
|
796
|
+
{
|
|
797
|
+
role: "status",
|
|
798
|
+
"aria-live": "polite",
|
|
799
|
+
className: "fillo-sr-only",
|
|
800
|
+
style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
|
|
801
|
+
children: uploadStatus(inFlight, files.length)
|
|
802
|
+
}
|
|
803
|
+
),
|
|
663
804
|
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
664
805
|
files.map((f) => /* @__PURE__ */ jsxs3("li", { className: "fillo-file fillo-file--done", children: [
|
|
665
806
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
@@ -723,7 +864,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
723
864
|
f.key
|
|
724
865
|
))
|
|
725
866
|
] }),
|
|
726
|
-
error && /* @__PURE__ */ jsx3("p", { className: "fillo
|
|
867
|
+
error && /* @__PURE__ */ jsx3("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
727
868
|
] });
|
|
728
869
|
}
|
|
729
870
|
|
|
@@ -740,25 +881,58 @@ function fieldAria(field, error, ids) {
|
|
|
740
881
|
"aria-describedby": describedBy.length ? describedBy.join(" ") : void 0
|
|
741
882
|
};
|
|
742
883
|
}
|
|
884
|
+
function useControl(base, field, error) {
|
|
885
|
+
return {
|
|
886
|
+
className: useSlotClass(base, {
|
|
887
|
+
slot: "control",
|
|
888
|
+
kind: field.kind,
|
|
889
|
+
fieldId: field.id,
|
|
890
|
+
invalid: Boolean(error),
|
|
891
|
+
required: Boolean(field.required)
|
|
892
|
+
}),
|
|
893
|
+
"data-fillo": "control"
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function isNativeControlTarget(target) {
|
|
897
|
+
return target instanceof HTMLElement && Boolean(target.closest("input, textarea, select, button"));
|
|
898
|
+
}
|
|
743
899
|
function FieldShell({
|
|
744
900
|
field,
|
|
745
901
|
error,
|
|
746
902
|
ids,
|
|
747
903
|
children
|
|
748
904
|
}) {
|
|
905
|
+
const state = {
|
|
906
|
+
kind: field.kind,
|
|
907
|
+
fieldId: field.id,
|
|
908
|
+
invalid: Boolean(error),
|
|
909
|
+
required: Boolean(field.required)
|
|
910
|
+
};
|
|
911
|
+
const fieldCls = useSlotClass(
|
|
912
|
+
`fillo-field fillo-field--${field.kind}${error ? " fillo-field--error" : ""}`,
|
|
913
|
+
{ slot: "field", ...state }
|
|
914
|
+
);
|
|
915
|
+
const labelCls = useSlotClass("fillo-label", { slot: "label", ...state });
|
|
916
|
+
const descriptionCls = useSlotClass("fillo-description", { slot: "fieldDescription", ...state });
|
|
917
|
+
const errorCls = useSlotClass("fillo-error", { slot: "error", ...state });
|
|
918
|
+
const strings = useStrings();
|
|
749
919
|
return /* @__PURE__ */ jsxs4(
|
|
750
920
|
"div",
|
|
751
921
|
{
|
|
752
|
-
className:
|
|
922
|
+
className: fieldCls,
|
|
923
|
+
"data-fillo": "field",
|
|
753
924
|
"data-field": field.id,
|
|
925
|
+
"data-kind": field.kind,
|
|
926
|
+
"data-invalid": error ? "" : void 0,
|
|
927
|
+
"data-required": field.required ? "" : void 0,
|
|
754
928
|
children: [
|
|
755
|
-
/* @__PURE__ */ jsxs4("label", { className: "fillo
|
|
929
|
+
/* @__PURE__ */ jsxs4("label", { className: labelCls, "data-fillo": "label", id: ids.labelId, htmlFor: ids.inputId, children: [
|
|
756
930
|
field.label,
|
|
757
|
-
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children:
|
|
931
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
|
|
758
932
|
] }),
|
|
759
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
933
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: descriptionCls, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
760
934
|
children,
|
|
761
|
-
error && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
935
|
+
error && /* @__PURE__ */ jsx4("p", { className: errorCls, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
762
936
|
]
|
|
763
937
|
}
|
|
764
938
|
);
|
|
@@ -766,11 +940,12 @@ function FieldShell({
|
|
|
766
940
|
function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
767
941
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
768
942
|
const type = field.kind === "email" ? "email" : field.kind === "url" ? "url" : field.kind === "number" ? "number" : field.kind === "date" ? "date" : "text";
|
|
943
|
+
const control = useControl("fillo-input", field, error);
|
|
769
944
|
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
770
945
|
"input",
|
|
771
946
|
{
|
|
772
947
|
id: ids.inputId,
|
|
773
|
-
|
|
948
|
+
...control,
|
|
774
949
|
type,
|
|
775
950
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
776
951
|
placeholder: field.placeholder,
|
|
@@ -781,11 +956,12 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
|
781
956
|
}
|
|
782
957
|
function LongText({ field, value, error, setValue, ids: providedIds }) {
|
|
783
958
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
959
|
+
const control = useControl("fillo-input fillo-textarea", field, error);
|
|
784
960
|
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
785
961
|
"textarea",
|
|
786
962
|
{
|
|
787
963
|
id: ids.inputId,
|
|
788
|
-
|
|
964
|
+
...control,
|
|
789
965
|
rows: 4,
|
|
790
966
|
value: typeof value === "string" ? value : "",
|
|
791
967
|
placeholder: field.placeholder,
|
|
@@ -834,6 +1010,9 @@ function ChoiceIcon({ icon }) {
|
|
|
834
1010
|
}
|
|
835
1011
|
function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
836
1012
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1013
|
+
const appearance = useFilloAppearance();
|
|
1014
|
+
const strings = useStrings();
|
|
1015
|
+
const optionCls = (base, optionId, selected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected });
|
|
837
1016
|
const choice = field;
|
|
838
1017
|
const options = useDisplayOptions(choice);
|
|
839
1018
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
@@ -842,7 +1021,8 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
842
1021
|
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
|
|
843
1022
|
"div",
|
|
844
1023
|
{
|
|
845
|
-
className: "fillo-options",
|
|
1024
|
+
className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
|
|
1025
|
+
"data-fillo": "options",
|
|
846
1026
|
role: "radiogroup",
|
|
847
1027
|
"aria-labelledby": ids.labelId,
|
|
848
1028
|
...fieldAria(field, error, ids),
|
|
@@ -851,7 +1031,19 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
851
1031
|
"label",
|
|
852
1032
|
{
|
|
853
1033
|
"data-option": opt.id,
|
|
854
|
-
|
|
1034
|
+
"data-fillo": "option",
|
|
1035
|
+
"data-selected": value === opt.id ? "" : void 0,
|
|
1036
|
+
className: optionCls(
|
|
1037
|
+
`fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${value === opt.id ? " fillo-option--selected" : ""}`,
|
|
1038
|
+
opt.id,
|
|
1039
|
+
value === opt.id
|
|
1040
|
+
),
|
|
1041
|
+
onClick: (event) => {
|
|
1042
|
+
if (isNativeControlTarget(event.target)) return;
|
|
1043
|
+
event.preventDefault();
|
|
1044
|
+
setOtherOn(false);
|
|
1045
|
+
setValue(opt.id);
|
|
1046
|
+
},
|
|
855
1047
|
children: [
|
|
856
1048
|
/* @__PURE__ */ jsx4(
|
|
857
1049
|
"input",
|
|
@@ -867,7 +1059,7 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
867
1059
|
}
|
|
868
1060
|
),
|
|
869
1061
|
/* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
|
|
870
|
-
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
|
|
1062
|
+
/* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
|
|
871
1063
|
]
|
|
872
1064
|
},
|
|
873
1065
|
opt.id
|
|
@@ -877,7 +1069,13 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
877
1069
|
/* @__PURE__ */ jsxs4(
|
|
878
1070
|
"div",
|
|
879
1071
|
{
|
|
880
|
-
|
|
1072
|
+
"data-fillo": "option",
|
|
1073
|
+
"data-selected": otherActive ? "" : void 0,
|
|
1074
|
+
className: optionCls(
|
|
1075
|
+
`fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
1076
|
+
"__other",
|
|
1077
|
+
otherActive
|
|
1078
|
+
),
|
|
881
1079
|
children: [
|
|
882
1080
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
883
1081
|
/* @__PURE__ */ jsx4(
|
|
@@ -893,15 +1091,15 @@ function SingleChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
893
1091
|
}
|
|
894
1092
|
}
|
|
895
1093
|
),
|
|
896
|
-
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label",
|
|
1094
|
+
/* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
|
|
897
1095
|
] }),
|
|
898
1096
|
otherActive && /* @__PURE__ */ jsx4(
|
|
899
1097
|
"input",
|
|
900
1098
|
{
|
|
901
1099
|
type: "text",
|
|
902
1100
|
className: "fillo-input fillo-other-input",
|
|
903
|
-
"aria-label":
|
|
904
|
-
placeholder:
|
|
1101
|
+
"aria-label": strings.otherPrompt,
|
|
1102
|
+
placeholder: strings.otherPlaceholder,
|
|
905
1103
|
autoFocus: !isOtherValue,
|
|
906
1104
|
value: isOtherValue ? String(value) : "",
|
|
907
1105
|
onChange: (e) => setValue(e.target.value)
|
|
@@ -918,16 +1116,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
918
1116
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
919
1117
|
const choice = field;
|
|
920
1118
|
const options = useDisplayOptions(choice);
|
|
1119
|
+
const strings = useStrings();
|
|
921
1120
|
const optionIds = new Set(choice.options.map((o) => o.id));
|
|
922
1121
|
const selected = Array.isArray(value) ? value : [];
|
|
923
1122
|
const otherText = selected.find((v) => !optionIds.has(v));
|
|
924
1123
|
const [otherOn, setOtherOn] = useState5(otherText !== void 0);
|
|
925
1124
|
const otherActive = otherText !== void 0 || otherOn;
|
|
926
1125
|
const toggle = (id) => setValue(selected.includes(id) ? selected.filter((v) => v !== id) : [...selected, id]);
|
|
1126
|
+
const appearance = useFilloAppearance();
|
|
1127
|
+
const optionCls = (base, optionId, isSelected) => slotClass2(base, appearance, { slot: "option", kind: field.kind, fieldId: field.id, optionId, selected: isSelected });
|
|
927
1128
|
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsxs4(
|
|
928
1129
|
"div",
|
|
929
1130
|
{
|
|
930
|
-
className: "fillo-options",
|
|
1131
|
+
className: slotClass2("fillo-options", appearance, { slot: "options", kind: field.kind, fieldId: field.id }),
|
|
1132
|
+
"data-fillo": "options",
|
|
931
1133
|
role: "group",
|
|
932
1134
|
"aria-labelledby": ids.labelId,
|
|
933
1135
|
...fieldAria(field, error, ids),
|
|
@@ -936,7 +1138,18 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
936
1138
|
"label",
|
|
937
1139
|
{
|
|
938
1140
|
"data-option": opt.id,
|
|
939
|
-
|
|
1141
|
+
"data-fillo": "option",
|
|
1142
|
+
"data-selected": selected.includes(opt.id) ? "" : void 0,
|
|
1143
|
+
className: optionCls(
|
|
1144
|
+
`fillo-option${opt.icon ? " fillo-option--has-icon" : ""}${selected.includes(opt.id) ? " fillo-option--selected" : ""}`,
|
|
1145
|
+
opt.id,
|
|
1146
|
+
selected.includes(opt.id)
|
|
1147
|
+
),
|
|
1148
|
+
onClick: (event) => {
|
|
1149
|
+
if (isNativeControlTarget(event.target)) return;
|
|
1150
|
+
event.preventDefault();
|
|
1151
|
+
toggle(opt.id);
|
|
1152
|
+
},
|
|
940
1153
|
children: [
|
|
941
1154
|
/* @__PURE__ */ jsx4(
|
|
942
1155
|
"input",
|
|
@@ -948,7 +1161,7 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
948
1161
|
}
|
|
949
1162
|
),
|
|
950
1163
|
/* @__PURE__ */ jsx4(ChoiceIcon, { icon: opt.icon }),
|
|
951
|
-
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label", children: opt.label })
|
|
1164
|
+
/* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: opt.label })
|
|
952
1165
|
]
|
|
953
1166
|
},
|
|
954
1167
|
opt.id
|
|
@@ -958,7 +1171,13 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
958
1171
|
/* @__PURE__ */ jsxs4(
|
|
959
1172
|
"div",
|
|
960
1173
|
{
|
|
961
|
-
|
|
1174
|
+
"data-fillo": "option",
|
|
1175
|
+
"data-selected": otherActive ? "" : void 0,
|
|
1176
|
+
className: optionCls(
|
|
1177
|
+
`fillo-option fillo-option--other${otherActive ? " fillo-option--selected fillo-option--with-other" : ""}`,
|
|
1178
|
+
"__other",
|
|
1179
|
+
otherActive
|
|
1180
|
+
),
|
|
962
1181
|
children: [
|
|
963
1182
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-option-main", children: [
|
|
964
1183
|
/* @__PURE__ */ jsx4(
|
|
@@ -977,15 +1196,15 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
977
1196
|
}
|
|
978
1197
|
}
|
|
979
1198
|
),
|
|
980
|
-
/* @__PURE__ */ jsx4("span", { className: "fillo-option-label",
|
|
1199
|
+
/* @__PURE__ */ jsx4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: strings.other })
|
|
981
1200
|
] }),
|
|
982
1201
|
otherActive && /* @__PURE__ */ jsx4(
|
|
983
1202
|
"input",
|
|
984
1203
|
{
|
|
985
1204
|
type: "text",
|
|
986
1205
|
className: "fillo-input fillo-other-input",
|
|
987
|
-
"aria-label":
|
|
988
|
-
placeholder:
|
|
1206
|
+
"aria-label": strings.otherPrompt,
|
|
1207
|
+
placeholder: strings.otherPlaceholder,
|
|
989
1208
|
autoFocus: otherText === void 0,
|
|
990
1209
|
value: otherText ?? "",
|
|
991
1210
|
onChange: (e) => {
|
|
@@ -1004,18 +1223,20 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1004
1223
|
var OTHER_SENTINEL = "__fw_other__";
|
|
1005
1224
|
function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
1006
1225
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1226
|
+
const strings = useStrings();
|
|
1007
1227
|
const choice = field;
|
|
1008
1228
|
const options = useDisplayOptions(choice);
|
|
1009
1229
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
1010
1230
|
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
1011
1231
|
const otherActive = isOtherValue || value === "" && otherOn;
|
|
1232
|
+
const control = useControl("fillo-input fillo-select", field, error);
|
|
1012
1233
|
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1013
1234
|
/* @__PURE__ */ jsxs4("div", { className: "fillo-select-wrap", children: [
|
|
1014
1235
|
/* @__PURE__ */ jsxs4(
|
|
1015
1236
|
"select",
|
|
1016
1237
|
{
|
|
1017
1238
|
id: ids.inputId,
|
|
1018
|
-
|
|
1239
|
+
...control,
|
|
1019
1240
|
...fieldAria(field, error, ids),
|
|
1020
1241
|
value: otherActive ? OTHER_SENTINEL : typeof value === "string" ? value : "",
|
|
1021
1242
|
onChange: (e) => {
|
|
@@ -1028,9 +1249,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1028
1249
|
}
|
|
1029
1250
|
},
|
|
1030
1251
|
children: [
|
|
1031
|
-
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ??
|
|
1252
|
+
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
|
|
1032
1253
|
options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
|
|
1033
|
-
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children:
|
|
1254
|
+
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: OTHER_SENTINEL, children: `${strings.other}\u2026` })
|
|
1034
1255
|
]
|
|
1035
1256
|
}
|
|
1036
1257
|
),
|
|
@@ -1041,8 +1262,8 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1041
1262
|
{
|
|
1042
1263
|
type: "text",
|
|
1043
1264
|
className: "fillo-input fillo-other-input fillo-other-input--block",
|
|
1044
|
-
"aria-label":
|
|
1045
|
-
placeholder:
|
|
1265
|
+
"aria-label": strings.otherPrompt,
|
|
1266
|
+
placeholder: strings.otherPlaceholder,
|
|
1046
1267
|
autoFocus: !isOtherValue,
|
|
1047
1268
|
value: isOtherValue ? String(value) : "",
|
|
1048
1269
|
onChange: (e) => setValue(e.target.value)
|
|
@@ -1052,18 +1273,24 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1052
1273
|
}
|
|
1053
1274
|
function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
1054
1275
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1276
|
+
const strings = useStrings();
|
|
1277
|
+
const appearance = useFilloAppearance();
|
|
1055
1278
|
const checkbox = field;
|
|
1056
1279
|
if (checkbox.appearance === "toggle") {
|
|
1057
1280
|
return /* @__PURE__ */ jsxs4(
|
|
1058
1281
|
"div",
|
|
1059
1282
|
{
|
|
1060
1283
|
className: `fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`,
|
|
1284
|
+
"data-fillo": "field",
|
|
1061
1285
|
"data-field": field.id,
|
|
1286
|
+
"data-kind": field.kind,
|
|
1287
|
+
"data-invalid": error ? "" : void 0,
|
|
1288
|
+
"data-required": field.required ? "" : void 0,
|
|
1062
1289
|
children: [
|
|
1063
|
-
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", children: [
|
|
1064
|
-
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
|
|
1290
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1291
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-copy", children: /* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
|
|
1065
1292
|
field.label,
|
|
1066
|
-
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children:
|
|
1293
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
|
|
1067
1294
|
] }) }),
|
|
1068
1295
|
/* @__PURE__ */ jsx4(
|
|
1069
1296
|
"input",
|
|
@@ -1090,9 +1317,13 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1090
1317
|
"div",
|
|
1091
1318
|
{
|
|
1092
1319
|
className: `fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`,
|
|
1320
|
+
"data-fillo": "field",
|
|
1093
1321
|
"data-field": field.id,
|
|
1322
|
+
"data-kind": field.kind,
|
|
1323
|
+
"data-invalid": error ? "" : void 0,
|
|
1324
|
+
"data-required": field.required ? "" : void 0,
|
|
1094
1325
|
children: [
|
|
1095
|
-
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", children: [
|
|
1326
|
+
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1096
1327
|
/* @__PURE__ */ jsx4(
|
|
1097
1328
|
"input",
|
|
1098
1329
|
{
|
|
@@ -1104,9 +1335,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1104
1335
|
...fieldAria(field, error, ids)
|
|
1105
1336
|
}
|
|
1106
1337
|
),
|
|
1107
|
-
/* @__PURE__ */ jsxs4("span", { className: "fillo-option-label", children: [
|
|
1338
|
+
/* @__PURE__ */ jsxs4("span", { className: slotClass2("fillo-option-label", appearance, { slot: "optionLabel", kind: field.kind, fieldId: field.id }), "data-fillo": "optionLabel", children: [
|
|
1108
1339
|
field.label,
|
|
1109
|
-
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children:
|
|
1340
|
+
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
|
|
1110
1341
|
] })
|
|
1111
1342
|
] }),
|
|
1112
1343
|
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo-description", id: ids.descriptionId, children: field.description }),
|
|
@@ -1147,10 +1378,12 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1147
1378
|
const steps = Array.from({ length: max }, (_, i) => i + 1);
|
|
1148
1379
|
const selected = current > 0 ? current : null;
|
|
1149
1380
|
const tabbable = rovingIndex(steps, selected);
|
|
1381
|
+
const appearance = useFilloAppearance();
|
|
1382
|
+
const control = useControl("fillo-rating", field, error);
|
|
1150
1383
|
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1151
1384
|
"div",
|
|
1152
1385
|
{
|
|
1153
|
-
|
|
1386
|
+
...control,
|
|
1154
1387
|
role: "radiogroup",
|
|
1155
1388
|
"aria-labelledby": ids.labelId,
|
|
1156
1389
|
...fieldAria(field, error, ids),
|
|
@@ -1159,7 +1392,14 @@ function Rating({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1159
1392
|
"button",
|
|
1160
1393
|
{
|
|
1161
1394
|
type: "button",
|
|
1162
|
-
|
|
1395
|
+
"data-fillo": "option",
|
|
1396
|
+
"data-selected": n <= current ? "" : void 0,
|
|
1397
|
+
className: slotClass2(`fillo-star${n <= current ? " fillo-star--active" : ""}`, appearance, {
|
|
1398
|
+
slot: "option",
|
|
1399
|
+
kind: field.kind,
|
|
1400
|
+
fieldId: field.id,
|
|
1401
|
+
selected: n <= current
|
|
1402
|
+
}),
|
|
1163
1403
|
role: "radio",
|
|
1164
1404
|
"aria-label": `${n} of ${max}`,
|
|
1165
1405
|
"aria-checked": n === current,
|
|
@@ -1189,11 +1429,13 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1189
1429
|
field.description ? ids.descriptionId : null,
|
|
1190
1430
|
error ? ids.errorId : null
|
|
1191
1431
|
].filter(Boolean).join(" ") || void 0;
|
|
1432
|
+
const appearance = useFilloAppearance();
|
|
1433
|
+
const control = useControl("fillo-scale", field, error);
|
|
1192
1434
|
return /* @__PURE__ */ jsxs4(FieldShell, { field, error, ids, children: [
|
|
1193
1435
|
/* @__PURE__ */ jsx4(
|
|
1194
1436
|
"div",
|
|
1195
1437
|
{
|
|
1196
|
-
|
|
1438
|
+
...control,
|
|
1197
1439
|
role: "radiogroup",
|
|
1198
1440
|
"aria-labelledby": ids.labelId,
|
|
1199
1441
|
"aria-invalid": error ? true : void 0,
|
|
@@ -1204,7 +1446,14 @@ function LinearScale({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1204
1446
|
"button",
|
|
1205
1447
|
{
|
|
1206
1448
|
type: "button",
|
|
1207
|
-
|
|
1449
|
+
"data-fillo": "option",
|
|
1450
|
+
"data-selected": value === n ? "" : void 0,
|
|
1451
|
+
className: slotClass2(`fillo-scale-step${value === n ? " fillo-scale-step--active" : ""}`, appearance, {
|
|
1452
|
+
slot: "option",
|
|
1453
|
+
kind: field.kind,
|
|
1454
|
+
fieldId: field.id,
|
|
1455
|
+
selected: value === n
|
|
1456
|
+
}),
|
|
1208
1457
|
role: "radio",
|
|
1209
1458
|
"aria-label": String(n),
|
|
1210
1459
|
"aria-checked": value === n,
|
|
@@ -1339,30 +1588,6 @@ function ContentRenderer({ block }) {
|
|
|
1339
1588
|
return /* @__PURE__ */ jsx4("hr", { className: "fillo-divider" });
|
|
1340
1589
|
}
|
|
1341
1590
|
}
|
|
1342
|
-
function shouldAutoSubmit(field, value, api) {
|
|
1343
|
-
if (api.form.settings.submitMode !== "auto") return false;
|
|
1344
|
-
if (!api.isLastPage || api.status !== "idle" || api.uploading) return false;
|
|
1345
|
-
const hasDiscreteAnswer = (() => {
|
|
1346
|
-
switch (field.kind) {
|
|
1347
|
-
case "select":
|
|
1348
|
-
case "dropdown":
|
|
1349
|
-
return typeof value === "string" && field.options.some((option) => option.id === value);
|
|
1350
|
-
case "checkbox":
|
|
1351
|
-
return value === true;
|
|
1352
|
-
case "rating":
|
|
1353
|
-
case "linear_scale":
|
|
1354
|
-
return typeof value === "number";
|
|
1355
|
-
default:
|
|
1356
|
-
return false;
|
|
1357
|
-
}
|
|
1358
|
-
})();
|
|
1359
|
-
if (!hasDiscreteAnswer) return false;
|
|
1360
|
-
if (value === api.data[field.id]) return false;
|
|
1361
|
-
const nextData = { ...api.data, [field.id]: value };
|
|
1362
|
-
const interactive = visibleFields(api.form, nextData).filter((f) => f.kind !== "hidden");
|
|
1363
|
-
if (interactive.length !== 1 || interactive[0]?.id !== field.id) return false;
|
|
1364
|
-
return validateResponse(api.form, nextData).ok;
|
|
1365
|
-
}
|
|
1366
1591
|
function blockHasPiping(block) {
|
|
1367
1592
|
const has = (s) => typeof s === "string" && s.includes("{{");
|
|
1368
1593
|
switch (block.kind) {
|
|
@@ -1382,8 +1607,12 @@ function blockPropsEqual(prev, next) {
|
|
|
1382
1607
|
const id = next.block.id;
|
|
1383
1608
|
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1384
1609
|
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1385
|
-
|
|
1386
|
-
|
|
1610
|
+
if (next.block.kind === "custom") {
|
|
1611
|
+
return prev.api.data === next.api.data && prev.api.status === next.api.status && prev.api.uploading === next.api.uploading && prev.api.isFirstPage === next.api.isFirstPage && prev.api.isLastPage === next.api.isLastPage && prev.api.pageIndex === next.api.pageIndex;
|
|
1612
|
+
}
|
|
1613
|
+
if ((blockHasPiping(prev.block) || blockHasPiping(next.block)) && prev.api.data !== next.api.data) {
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1387
1616
|
if (next.api.form.settings.submitMode === "auto") {
|
|
1388
1617
|
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) {
|
|
1389
1618
|
return false;
|
|
@@ -1418,7 +1647,15 @@ function BlockRendererInner({ block, api, components, customComponents }) {
|
|
|
1418
1647
|
error: api.errors[block.id],
|
|
1419
1648
|
setValue: (v) => {
|
|
1420
1649
|
api.setValue(block.id, v);
|
|
1421
|
-
|
|
1650
|
+
const ctx = {
|
|
1651
|
+
form: api.form,
|
|
1652
|
+
data: api.data,
|
|
1653
|
+
status: api.status,
|
|
1654
|
+
isLastPage: api.isLastPage,
|
|
1655
|
+
uploading: api.uploading
|
|
1656
|
+
};
|
|
1657
|
+
if (shouldAutoSubmit(block, v, ctx)) void api.submit().catch(() => {
|
|
1658
|
+
});
|
|
1422
1659
|
},
|
|
1423
1660
|
api,
|
|
1424
1661
|
ids
|
|
@@ -1432,8 +1669,13 @@ function FormField({
|
|
|
1432
1669
|
customComponents
|
|
1433
1670
|
}) {
|
|
1434
1671
|
const api = useFillo();
|
|
1672
|
+
const visibleIds = useMemo4(
|
|
1673
|
+
() => new Set(visibleFields(api.form, api.data).map((f) => f.id)),
|
|
1674
|
+
[api.form, api.data]
|
|
1675
|
+
);
|
|
1435
1676
|
const block = api.form.pages.flatMap((p) => p.blocks).find((b) => b.id === id);
|
|
1436
1677
|
if (!block) return null;
|
|
1678
|
+
if (isField(block) && !visibleIds.has(block.id)) return null;
|
|
1437
1679
|
return /* @__PURE__ */ jsx4(
|
|
1438
1680
|
BlockRenderer,
|
|
1439
1681
|
{
|
|
@@ -1487,18 +1729,25 @@ function focusFirstInvalid(formEl) {
|
|
|
1487
1729
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
1488
1730
|
else setTimeout(run, 0);
|
|
1489
1731
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1732
|
+
var HP_STYLE = {
|
|
1733
|
+
position: "absolute",
|
|
1734
|
+
left: "-9999px",
|
|
1735
|
+
top: "auto",
|
|
1736
|
+
width: "1px",
|
|
1737
|
+
height: "1px",
|
|
1738
|
+
opacity: 0,
|
|
1739
|
+
overflow: "hidden",
|
|
1740
|
+
pointerEvents: "none"
|
|
1741
|
+
};
|
|
1742
|
+
function assertHpHidden(el) {
|
|
1743
|
+
if (!el) return;
|
|
1744
|
+
for (const [prop, value] of Object.entries(HP_STYLE)) {
|
|
1745
|
+
el.style[prop] = String(value);
|
|
1746
|
+
}
|
|
1498
1747
|
}
|
|
1499
1748
|
function FilloForm(props) {
|
|
1500
1749
|
const { form, formId } = props;
|
|
1501
|
-
const client =
|
|
1750
|
+
const client = useMemo5(
|
|
1502
1751
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
1503
1752
|
[props.client, formId]
|
|
1504
1753
|
);
|
|
@@ -1510,7 +1759,11 @@ function FilloForm(props) {
|
|
|
1510
1759
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1511
1760
|
const inlineSchema = codeForm ? codeForm.schema : form;
|
|
1512
1761
|
const onError = props.onError;
|
|
1513
|
-
const syncedFormId = useCodeFormSync(form, client, onError);
|
|
1762
|
+
const { formId: syncedFormId, status: syncedStatus } = useCodeFormSync(form, client, onError);
|
|
1763
|
+
const onErrorRef = useRef5(onError);
|
|
1764
|
+
useEffect7(() => {
|
|
1765
|
+
onErrorRef.current = onError;
|
|
1766
|
+
});
|
|
1514
1767
|
const needsFetch = !inlineSchema && Boolean(client && formId);
|
|
1515
1768
|
useEffect7(() => {
|
|
1516
1769
|
if (!needsFetch || !client || !formId) return;
|
|
@@ -1527,30 +1780,37 @@ function FilloForm(props) {
|
|
|
1527
1780
|
if (cancelled) return;
|
|
1528
1781
|
const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
|
|
1529
1782
|
setLoadError(error);
|
|
1530
|
-
|
|
1783
|
+
onErrorRef.current?.(error);
|
|
1531
1784
|
});
|
|
1532
1785
|
return () => {
|
|
1533
1786
|
cancelled = true;
|
|
1534
1787
|
};
|
|
1535
|
-
}, [needsFetch, client, formId
|
|
1788
|
+
}, [needsFetch, client, formId]);
|
|
1789
|
+
const strings = resolveStrings(props.strings);
|
|
1536
1790
|
const rawSchema = inlineSchema ?? fetched;
|
|
1537
|
-
const normalized =
|
|
1791
|
+
const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1538
1792
|
const schema = normalized?.ok ? normalized.schema : null;
|
|
1539
|
-
const theme = props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
|
|
1793
|
+
const theme = props.appearance?.theme ?? props.theme ?? codeForm?.theme ?? fetchedTheme ?? void 0;
|
|
1540
1794
|
if (normalized && !normalized.ok) {
|
|
1541
1795
|
const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1542
1796
|
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
1543
|
-
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children:
|
|
1797
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
|
|
1544
1798
|
}
|
|
1545
1799
|
if (loadError) {
|
|
1546
1800
|
if (props.renderError) {
|
|
1547
1801
|
return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(loadError) });
|
|
1548
1802
|
}
|
|
1549
|
-
const message = loadError.status === 404 ?
|
|
1803
|
+
const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
|
|
1550
1804
|
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
|
|
1551
1805
|
}
|
|
1552
1806
|
if (closed) {
|
|
1553
|
-
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children:
|
|
1807
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
|
|
1808
|
+
}
|
|
1809
|
+
const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
|
|
1810
|
+
if (notPublished && !isDevEnv()) {
|
|
1811
|
+
const error = new FilloError2("This form isn't published yet.", 403);
|
|
1812
|
+
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
1813
|
+
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.notLive }) });
|
|
1554
1814
|
}
|
|
1555
1815
|
if (!schema) {
|
|
1556
1816
|
return /* @__PURE__ */ jsxs5(
|
|
@@ -1578,7 +1838,8 @@ function FilloForm(props) {
|
|
|
1578
1838
|
formId: syncedFormId ?? formId,
|
|
1579
1839
|
poweredBy,
|
|
1580
1840
|
brandLabel: fetchedBranding?.label,
|
|
1581
|
-
brandHref: fetchedBranding?.href
|
|
1841
|
+
brandHref: fetchedBranding?.href,
|
|
1842
|
+
draftNotice: notPublished
|
|
1582
1843
|
}
|
|
1583
1844
|
);
|
|
1584
1845
|
}
|
|
@@ -1596,10 +1857,25 @@ function DevNoClientNotice() {
|
|
|
1596
1857
|
" to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
|
|
1597
1858
|
] });
|
|
1598
1859
|
}
|
|
1860
|
+
function DevDraftNotice() {
|
|
1861
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
1862
|
+
/* @__PURE__ */ jsx5("strong", { children: "Draft form." }),
|
|
1863
|
+
" It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
|
|
1864
|
+
] });
|
|
1865
|
+
}
|
|
1599
1866
|
function ResolvedForm(props) {
|
|
1600
1867
|
const { schema, theme } = props;
|
|
1601
1868
|
const instanceId = useId2().replace(/:/g, "");
|
|
1602
1869
|
const hpRef = useRef5("");
|
|
1870
|
+
const codeForm = isCodeForm2(props.form) ? props.form : null;
|
|
1871
|
+
const client = props.client;
|
|
1872
|
+
const resolveFormId = useMemo5(
|
|
1873
|
+
() => codeForm && client?.key ? (
|
|
1874
|
+
// bypassCache: a stale cached formId must not be trusted at submit time.
|
|
1875
|
+
() => syncCodeForm2(client, codeForm, { bypassCache: true }).then((r) => r.formId)
|
|
1876
|
+
) : void 0,
|
|
1877
|
+
[codeForm, client]
|
|
1878
|
+
);
|
|
1603
1879
|
const api = useFilloController({
|
|
1604
1880
|
form: schema,
|
|
1605
1881
|
formId: props.formId,
|
|
@@ -1608,9 +1884,13 @@ function ResolvedForm(props) {
|
|
|
1608
1884
|
onChange: props.onChange,
|
|
1609
1885
|
onSubmitted: props.onSubmitted,
|
|
1610
1886
|
skipValidation: props.skipValidation,
|
|
1611
|
-
getHoneypot: () => hpRef.current
|
|
1887
|
+
getHoneypot: () => hpRef.current,
|
|
1888
|
+
// Framed renderer: Fillo draws the layout (and badge), so the surface is
|
|
1889
|
+
// "default", never the bare-engine "headless".
|
|
1890
|
+
surface: "default",
|
|
1891
|
+
resolveFormId
|
|
1612
1892
|
});
|
|
1613
|
-
const themed =
|
|
1893
|
+
const themed = useMemo5(() => themeProps(theme), [theme]);
|
|
1614
1894
|
const settings = schema.settings;
|
|
1615
1895
|
const submitted = api.status === "submitted";
|
|
1616
1896
|
useEffect7(() => {
|
|
@@ -1618,41 +1898,89 @@ function ResolvedForm(props) {
|
|
|
1618
1898
|
const url = safeHttpUrl(settings.redirectUrl);
|
|
1619
1899
|
if (url) window.location.assign(url);
|
|
1620
1900
|
}, [submitted, settings.redirectUrl]);
|
|
1901
|
+
const successRef = useRef5(null);
|
|
1902
|
+
useEffect7(() => {
|
|
1903
|
+
if (submitted) successRef.current?.focus();
|
|
1904
|
+
}, [submitted]);
|
|
1905
|
+
const blocksRef = useRef5(null);
|
|
1906
|
+
const prevPageIndex = useRef5(api.pageIndex);
|
|
1907
|
+
useEffect7(() => {
|
|
1908
|
+
if (prevPageIndex.current === api.pageIndex) return;
|
|
1909
|
+
prevPageIndex.current = api.pageIndex;
|
|
1910
|
+
blocksRef.current?.focus();
|
|
1911
|
+
}, [api.pageIndex]);
|
|
1912
|
+
const appearance = props.appearance;
|
|
1913
|
+
const strings = resolveStrings(props.strings);
|
|
1621
1914
|
if (api.status === "submitted") {
|
|
1622
|
-
return /* @__PURE__ */ jsxs5(
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1915
|
+
return /* @__PURE__ */ jsxs5(
|
|
1916
|
+
"div",
|
|
1917
|
+
{
|
|
1918
|
+
ref: successRef,
|
|
1919
|
+
tabIndex: -1,
|
|
1920
|
+
role: "status",
|
|
1921
|
+
"aria-live": "polite",
|
|
1922
|
+
className: slotClass3(
|
|
1923
|
+
`fillo-form fillo-form--success ${props.className ?? ""}`,
|
|
1924
|
+
appearance,
|
|
1925
|
+
{ slot: "root", status: "submitted" }
|
|
1926
|
+
),
|
|
1927
|
+
"data-fillo": "root",
|
|
1928
|
+
"data-state": "submitted",
|
|
1929
|
+
...themed,
|
|
1930
|
+
children: [
|
|
1931
|
+
props.renderSuccess ? props.renderSuccess() : /* @__PURE__ */ jsxs5(
|
|
1932
|
+
"div",
|
|
1933
|
+
{
|
|
1934
|
+
className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
|
|
1935
|
+
"data-fillo": "success",
|
|
1936
|
+
children: [
|
|
1937
|
+
/* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
|
|
1938
|
+
/* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
|
|
1939
|
+
/* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? strings.successMessage })
|
|
1940
|
+
]
|
|
1941
|
+
}
|
|
1942
|
+
),
|
|
1943
|
+
props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1944
|
+
]
|
|
1945
|
+
}
|
|
1946
|
+
);
|
|
1630
1947
|
}
|
|
1631
1948
|
const multiPage = api.pageCount > 1;
|
|
1632
1949
|
const autoSubmit = settings.submitMode === "auto";
|
|
1633
1950
|
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(visibleFields2(api.form, api.data));
|
|
1634
1951
|
const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
|
|
1635
|
-
return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
|
|
1952
|
+
return /* @__PURE__ */ jsx5(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx5(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx5(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx5(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
|
|
1636
1953
|
"form",
|
|
1637
1954
|
{
|
|
1638
|
-
className: `fillo-form ${props.className ?? ""}`,
|
|
1955
|
+
className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
|
|
1956
|
+
slot: "root",
|
|
1957
|
+
status: api.status
|
|
1958
|
+
}),
|
|
1959
|
+
"data-fillo": "root",
|
|
1960
|
+
"data-state": api.status,
|
|
1961
|
+
"data-page": api.pageIndex + 1,
|
|
1962
|
+
"data-last-page": api.isLastPage ? "" : void 0,
|
|
1639
1963
|
...themed,
|
|
1640
1964
|
noValidate: true,
|
|
1641
1965
|
onSubmit: (e) => {
|
|
1642
1966
|
e.preventDefault();
|
|
1643
1967
|
const formEl = e.currentTarget;
|
|
1644
|
-
if (api.isLastPage)
|
|
1645
|
-
|
|
1968
|
+
if (api.isLastPage) {
|
|
1969
|
+
void api.submit().then(() => focusFirstInvalid(formEl)).catch(() => {
|
|
1970
|
+
});
|
|
1971
|
+
} else {
|
|
1646
1972
|
api.next();
|
|
1647
1973
|
focusFirstInvalid(formEl);
|
|
1648
1974
|
}
|
|
1649
1975
|
},
|
|
1650
1976
|
children: [
|
|
1651
1977
|
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
1978
|
+
props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
|
|
1652
1979
|
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
|
|
1653
1980
|
"div",
|
|
1654
1981
|
{
|
|
1655
|
-
className: "fillo-progress-track",
|
|
1982
|
+
className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
|
|
1983
|
+
"data-fillo": "progress",
|
|
1656
1984
|
role: "progressbar",
|
|
1657
1985
|
"aria-valuemin": 0,
|
|
1658
1986
|
"aria-valuemax": api.pageCount,
|
|
@@ -1660,18 +1988,26 @@ function ResolvedForm(props) {
|
|
|
1660
1988
|
children: /* @__PURE__ */ jsx5(
|
|
1661
1989
|
"div",
|
|
1662
1990
|
{
|
|
1663
|
-
className: "fillo-progress-fill",
|
|
1664
|
-
|
|
1991
|
+
className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
|
|
1992
|
+
"data-fillo": "progressFill",
|
|
1993
|
+
style: { "--fillo-progress-value": `${(api.pageIndex + 1) / api.pageCount * 100}%` }
|
|
1665
1994
|
}
|
|
1666
1995
|
)
|
|
1667
1996
|
}
|
|
1668
1997
|
),
|
|
1669
|
-
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: "fillo-header", children: [
|
|
1670
|
-
/* @__PURE__ */ jsx5("h1", { className: "fillo-title", children: schema.title }),
|
|
1671
|
-
schema.description && /* @__PURE__ */ jsx5(
|
|
1998
|
+
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
|
|
1999
|
+
/* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
|
|
2000
|
+
schema.description && /* @__PURE__ */ jsx5(
|
|
2001
|
+
"p",
|
|
2002
|
+
{
|
|
2003
|
+
className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
|
|
2004
|
+
"data-fillo": "description",
|
|
2005
|
+
children: schema.description
|
|
2006
|
+
}
|
|
2007
|
+
)
|
|
1672
2008
|
] }),
|
|
1673
|
-
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: "fillo-page-title", children: api.page.title }),
|
|
1674
|
-
/* @__PURE__ */ jsx5("div", { className: "fillo-blocks", children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
|
|
2009
|
+
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx5("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
|
|
2010
|
+
/* @__PURE__ */ jsx5("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx5(
|
|
1675
2011
|
BlockRenderer,
|
|
1676
2012
|
{
|
|
1677
2013
|
block,
|
|
@@ -1691,45 +2027,123 @@ function ResolvedForm(props) {
|
|
|
1691
2027
|
autoComplete: "off",
|
|
1692
2028
|
"aria-hidden": "true",
|
|
1693
2029
|
defaultValue: "",
|
|
2030
|
+
style: HP_STYLE,
|
|
2031
|
+
ref: assertHpHidden,
|
|
1694
2032
|
onChange: (e) => {
|
|
1695
2033
|
hpRef.current = e.target.value;
|
|
1696
2034
|
}
|
|
1697
2035
|
}
|
|
1698
2036
|
),
|
|
1699
|
-
api.status === "error" && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children:
|
|
1700
|
-
showFooter && /* @__PURE__ */ jsxs5("footer", { className: "fillo-footer", children: [
|
|
1701
|
-
multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5(
|
|
2037
|
+
(api.status === "error" || api.submitError) && /* @__PURE__ */ jsx5("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
|
|
2038
|
+
showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
|
|
2039
|
+
multiPage && !api.isFirstPage && /* @__PURE__ */ jsx5(
|
|
2040
|
+
"button",
|
|
2041
|
+
{
|
|
2042
|
+
type: "button",
|
|
2043
|
+
className: slotClass3("fillo-button fillo-button--ghost", appearance, { slot: "button", variant: "ghost" }),
|
|
2044
|
+
"data-fillo": "button",
|
|
2045
|
+
onClick: api.back,
|
|
2046
|
+
children: strings.back
|
|
2047
|
+
}
|
|
2048
|
+
),
|
|
1702
2049
|
showPrimaryButton && /* @__PURE__ */ jsx5(
|
|
1703
2050
|
"button",
|
|
1704
2051
|
{
|
|
1705
2052
|
type: "submit",
|
|
1706
|
-
className: "fillo-button fillo-button--primary",
|
|
2053
|
+
className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
|
|
2054
|
+
"data-fillo": "button",
|
|
1707
2055
|
disabled: api.status === "submitting" || api.uploading,
|
|
1708
|
-
children: api.uploading ?
|
|
2056
|
+
children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
|
|
1709
2057
|
}
|
|
1710
2058
|
)
|
|
1711
2059
|
] }),
|
|
1712
2060
|
props.poweredBy && /* @__PURE__ */ jsx5(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1713
2061
|
]
|
|
1714
2062
|
}
|
|
1715
|
-
) }) });
|
|
2063
|
+
) }) }) }) });
|
|
1716
2064
|
}
|
|
1717
2065
|
|
|
1718
|
-
// src/
|
|
1719
|
-
import {
|
|
1720
|
-
import {
|
|
2066
|
+
// src/jsx.tsx
|
|
2067
|
+
import { useRef as useRef6 } from "react";
|
|
2068
|
+
import {
|
|
2069
|
+
codeFormFromJsx,
|
|
2070
|
+
FilloJsxError,
|
|
2071
|
+
JSX_BLOCK_COMPONENTS
|
|
2072
|
+
} from "@usefillo/core";
|
|
1721
2073
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
1722
|
-
|
|
2074
|
+
var B = JSX_BLOCK_COMPONENTS;
|
|
2075
|
+
var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
2076
|
+
function JsxForm(props) {
|
|
2077
|
+
const { id, title, description, settings, theme, client, children, ...rest } = props;
|
|
2078
|
+
const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
|
|
2079
|
+
const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
|
|
2080
|
+
const memo2 = useRef6(null);
|
|
2081
|
+
if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
|
|
2082
|
+
console.warn(
|
|
2083
|
+
`[fillo] "${id}": form structure changed between renders. If you just edited the form, ignore this. Otherwise: conditional questions are visibleIf={when(\u2026)\u2026}, never {cond && <Fillo.\u2026/>} \u2014 a per-visitor schema churns drafts in your workspace.`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
if (!memo2.current || memo2.current.json !== json) {
|
|
2087
|
+
memo2.current = { json, form: compiled };
|
|
2088
|
+
}
|
|
2089
|
+
return /* @__PURE__ */ jsx6(FilloForm, { form: memo2.current.form, client, ...rest });
|
|
2090
|
+
}
|
|
2091
|
+
function defineFormFromJsx(element) {
|
|
2092
|
+
if (!element || element.type !== JsxForm) {
|
|
2093
|
+
throw new FilloJsxError(
|
|
2094
|
+
"NON_FILLO_CHILD",
|
|
2095
|
+
"Fillo.defineForm() takes a <Fillo.Form> element: Fillo.defineForm(<Fillo.Form id=\u2026>\u2026</Fillo.Form>)"
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
const { id, title, description, settings, theme, children } = element.props;
|
|
2099
|
+
return codeFormFromJsx({ id, title, description, settings, theme }, children);
|
|
2100
|
+
}
|
|
2101
|
+
var Fillo = {
|
|
2102
|
+
Form: JsxForm,
|
|
2103
|
+
/** Compile a <Fillo.Form> element to a CodeForm at module scope — the same
|
|
2104
|
+
* value FilloProvider (headless) and the CLI consume. */
|
|
2105
|
+
defineForm: defineFormFromJsx,
|
|
2106
|
+
Text: B.Text,
|
|
2107
|
+
LongText: B.LongText,
|
|
2108
|
+
Email: B.Email,
|
|
2109
|
+
Url: B.Url,
|
|
2110
|
+
Phone: B.Phone,
|
|
2111
|
+
Number: B.Number,
|
|
2112
|
+
Select: B.Select,
|
|
2113
|
+
MultiSelect: B.MultiSelect,
|
|
2114
|
+
Dropdown: B.Dropdown,
|
|
2115
|
+
Checkbox: B.Checkbox,
|
|
2116
|
+
Rating: B.Rating,
|
|
2117
|
+
Scale: B.Scale,
|
|
2118
|
+
Ranking: B.Ranking,
|
|
2119
|
+
Matrix: B.Matrix,
|
|
2120
|
+
Signature: B.Signature,
|
|
2121
|
+
Date: B.Date,
|
|
2122
|
+
FileUpload: B.FileUpload,
|
|
2123
|
+
Hidden: B.Hidden,
|
|
2124
|
+
Custom: B.Custom,
|
|
2125
|
+
Heading: B.Heading,
|
|
2126
|
+
Paragraph: B.Paragraph,
|
|
2127
|
+
Divider: B.Divider,
|
|
2128
|
+
Page: B.Page,
|
|
2129
|
+
Option: B.Option
|
|
2130
|
+
};
|
|
2131
|
+
|
|
2132
|
+
// src/provider.tsx
|
|
2133
|
+
import { useId as useId3, useMemo as useMemo6 } from "react";
|
|
2134
|
+
import { FilloError as FilloError3, normalizeFormSchema as normalizeFormSchema2, resolveStrings as resolveStrings2 } from "@usefillo/core";
|
|
2135
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2136
|
+
function FilloProvider({ children, form, formId, client, appearance, strings, ...options }) {
|
|
1723
2137
|
const instanceId = useId3().replace(/:/g, "");
|
|
1724
2138
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1725
2139
|
const rawSchema = codeForm ? codeForm.schema : form;
|
|
1726
|
-
const schemaKey =
|
|
1727
|
-
const normalized =
|
|
2140
|
+
const schemaKey = useMemo6(() => JSON.stringify(rawSchema), [rawSchema]);
|
|
2141
|
+
const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema, schemaKey]);
|
|
1728
2142
|
if (!normalized.ok) {
|
|
1729
2143
|
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1730
2144
|
}
|
|
1731
2145
|
const schema = normalized.schema;
|
|
1732
|
-
const syncedFormId = useCodeFormSync(form, client);
|
|
2146
|
+
const { formId: syncedFormId } = useCodeFormSync(form, client);
|
|
1733
2147
|
const api = useFilloController({
|
|
1734
2148
|
...options,
|
|
1735
2149
|
client,
|
|
@@ -1739,22 +2153,36 @@ function FilloProvider({ children, form, formId, client, ...options }) {
|
|
|
1739
2153
|
// a paid capability the server enforces when these responses are submitted.
|
|
1740
2154
|
surface: "headless"
|
|
1741
2155
|
});
|
|
1742
|
-
return /* @__PURE__ */
|
|
2156
|
+
return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolveStrings2(strings), children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
|
|
1743
2157
|
}
|
|
1744
2158
|
|
|
1745
2159
|
// src/index.ts
|
|
1746
2160
|
import {
|
|
1747
2161
|
createClient as createClient2,
|
|
1748
2162
|
provisionWorkspace,
|
|
2163
|
+
when,
|
|
2164
|
+
FilloJsxError as FilloJsxError2,
|
|
1749
2165
|
FilloClient,
|
|
1750
2166
|
FilloError as FilloError4
|
|
1751
2167
|
} from "@usefillo/core";
|
|
2168
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
2169
|
+
const key = /* @__PURE__ */ Symbol.for("usefillo.react.instance");
|
|
2170
|
+
const g = globalThis;
|
|
2171
|
+
if (g[key]) {
|
|
2172
|
+
console.error(
|
|
2173
|
+
"[fillo] Two copies of @usefillo/react are loaded \u2014 React context does not cross package copies, so forms will misbehave. Dedupe the dependency (npm dedupe / pnpm why @usefillo/react)."
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
g[key] = true;
|
|
2177
|
+
}
|
|
1752
2178
|
export {
|
|
1753
2179
|
BlockRenderer,
|
|
2180
|
+
Fillo,
|
|
1754
2181
|
FilloClient,
|
|
1755
2182
|
FilloError4 as FilloError,
|
|
1756
2183
|
FileUploadField as FilloFileUpload,
|
|
1757
2184
|
FilloForm,
|
|
2185
|
+
FilloJsxError2 as FilloJsxError,
|
|
1758
2186
|
FilloProvider,
|
|
1759
2187
|
FormField,
|
|
1760
2188
|
createClient2 as createClient,
|
|
@@ -1762,5 +2190,7 @@ export {
|
|
|
1762
2190
|
provisionWorkspace,
|
|
1763
2191
|
useField,
|
|
1764
2192
|
useFillo,
|
|
1765
|
-
|
|
2193
|
+
useFilloAppearance,
|
|
2194
|
+
useFilloController,
|
|
2195
|
+
when
|
|
1766
2196
|
};
|