@usefillo/react 0.7.0 → 0.9.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 +2 -1
- package/dist/index.d.ts +49 -14
- package/dist/index.js +854 -279
- package/dist/styles.css +22 -0
- package/dist/styles.unlayered.css +22 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/FilloForm.tsx
|
|
4
|
-
import { useEffect as
|
|
4
|
+
import { useCallback, useEffect as useEffect8, useId as useId2, useMemo as useMemo5, useRef as useRef7, useState as useState6 } from "react";
|
|
5
5
|
import {
|
|
6
6
|
FilloError as FilloError2,
|
|
7
7
|
createClient,
|
|
8
|
+
formSchemasEqual,
|
|
9
|
+
isFilloError as isFilloError2,
|
|
8
10
|
needsExplicitSubmit,
|
|
9
11
|
normalizeFormSchema,
|
|
10
12
|
normalizeFormTheme,
|
|
11
|
-
|
|
13
|
+
reachableFields,
|
|
14
|
+
reachablePageSequence
|
|
12
15
|
} from "@usefillo/core";
|
|
13
16
|
import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
|
|
14
17
|
|
|
15
18
|
// src/appearance.tsx
|
|
16
19
|
import { createContext, useContext } from "react";
|
|
17
20
|
import {
|
|
21
|
+
DEFAULT_FIELD_STRINGS,
|
|
18
22
|
DEFAULT_STRINGS,
|
|
19
23
|
slotClass
|
|
20
24
|
} from "@usefillo/core";
|
|
@@ -49,7 +53,10 @@ function useFieldSlots(field, error) {
|
|
|
49
53
|
control: (base) => slotClass(base, appearance, { slot: "control", ...state })
|
|
50
54
|
};
|
|
51
55
|
}
|
|
52
|
-
var FilloStringsContext = createContext(
|
|
56
|
+
var FilloStringsContext = createContext({
|
|
57
|
+
...DEFAULT_STRINGS,
|
|
58
|
+
...DEFAULT_FIELD_STRINGS
|
|
59
|
+
});
|
|
53
60
|
function useStrings() {
|
|
54
61
|
return useContext(FilloStringsContext);
|
|
55
62
|
}
|
|
@@ -121,7 +128,12 @@ function useFilloController(options) {
|
|
|
121
128
|
surface: options.surface ?? "headless",
|
|
122
129
|
// Wrapped so the engine (created once) always calls the latest resolver.
|
|
123
130
|
resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
|
|
124
|
-
respondent: options.respondent
|
|
131
|
+
respondent: options.respondent,
|
|
132
|
+
// Challenge wiring read through optsRef so the engine (created once) sees
|
|
133
|
+
// the latest token/reset each submit — the widget solves after mount.
|
|
134
|
+
challengeRequired: options.challengeRequired,
|
|
135
|
+
getChallengeToken: () => optsRef.current.getChallengeToken?.(),
|
|
136
|
+
onChallengeFailed: () => optsRef.current.onChallengeFailed?.()
|
|
125
137
|
});
|
|
126
138
|
}
|
|
127
139
|
const store = storeRef.current;
|
|
@@ -175,9 +187,10 @@ import {
|
|
|
175
187
|
contentHash,
|
|
176
188
|
FilloError,
|
|
177
189
|
isCodeForm,
|
|
190
|
+
isFilloError,
|
|
178
191
|
syncCodeForm
|
|
179
192
|
} from "@usefillo/core";
|
|
180
|
-
import { useEffect as useEffect2, useState } from "react";
|
|
193
|
+
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
181
194
|
import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
|
|
182
195
|
var noticedSync = /* @__PURE__ */ new Set();
|
|
183
196
|
function noticeSync(handle, kind, message) {
|
|
@@ -186,39 +199,158 @@ function noticeSync(handle, kind, message) {
|
|
|
186
199
|
noticedSync.add(key);
|
|
187
200
|
console.info(message);
|
|
188
201
|
}
|
|
189
|
-
function useCodeFormSync(form, client, onError) {
|
|
202
|
+
function useCodeFormSync(form, client, onError, options) {
|
|
190
203
|
const codeForm = isCodeForm(form) ? form : null;
|
|
191
|
-
const [synced, setSynced] = useState({
|
|
204
|
+
const [synced, setSynced] = useState({
|
|
205
|
+
key: null,
|
|
206
|
+
client: void 0,
|
|
207
|
+
settled: false,
|
|
208
|
+
formId: null,
|
|
209
|
+
status: null,
|
|
210
|
+
error: null,
|
|
211
|
+
noticeError: null,
|
|
212
|
+
staged: false
|
|
213
|
+
});
|
|
192
214
|
const hasKey = Boolean(client?.key);
|
|
215
|
+
const allowUnsynced = Boolean(options?.allowUnsynced);
|
|
193
216
|
const handle = codeForm?.id;
|
|
194
217
|
const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
|
|
218
|
+
const syncKey = codeForm ? client?.key ? `${client.baseUrl}|${client.key}|${codeForm.id}|${contentKey}` : allowUnsynced ? null : `${client?.baseUrl ?? ""}|<missing-key>|${codeForm.id}|${contentKey}` : null;
|
|
219
|
+
const onErrorRef = useRef2(onError);
|
|
220
|
+
onErrorRef.current = onError;
|
|
195
221
|
useEffect2(() => {
|
|
196
|
-
if (!codeForm || !
|
|
222
|
+
if (!codeForm || !syncKey) return;
|
|
197
223
|
let cancelled = false;
|
|
224
|
+
setSynced(
|
|
225
|
+
(previous) => previous.key === syncKey && previous.client === client ? {
|
|
226
|
+
...previous,
|
|
227
|
+
settled: false,
|
|
228
|
+
formId: null,
|
|
229
|
+
status: null,
|
|
230
|
+
error: null,
|
|
231
|
+
noticeError: null,
|
|
232
|
+
staged: false,
|
|
233
|
+
resolvedSchema: void 0,
|
|
234
|
+
resolvedTheme: void 0
|
|
235
|
+
} : {
|
|
236
|
+
key: syncKey,
|
|
237
|
+
client,
|
|
238
|
+
settled: false,
|
|
239
|
+
formId: null,
|
|
240
|
+
status: null,
|
|
241
|
+
error: null,
|
|
242
|
+
noticeError: null,
|
|
243
|
+
staged: false
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
if (!client?.key) {
|
|
247
|
+
const error = new FilloError(
|
|
248
|
+
'Code forms need createClient({ key: "pk_\u2026" }) to resolve the live form. Pass a publishable key or a published formId.',
|
|
249
|
+
401,
|
|
250
|
+
void 0,
|
|
251
|
+
"sync_key_required"
|
|
252
|
+
);
|
|
253
|
+
console.warn(`[fillo] form sync failed (${error.code}): ${error.message}`);
|
|
254
|
+
setSynced({
|
|
255
|
+
key: syncKey,
|
|
256
|
+
client,
|
|
257
|
+
settled: true,
|
|
258
|
+
formId: null,
|
|
259
|
+
status: null,
|
|
260
|
+
error,
|
|
261
|
+
noticeError: null,
|
|
262
|
+
staged: false
|
|
263
|
+
});
|
|
264
|
+
onErrorRef.current?.(error);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
198
267
|
syncCodeForm(client, codeForm).then((r) => {
|
|
199
268
|
if (cancelled) return;
|
|
200
|
-
|
|
269
|
+
if ((r.staged || r.syncError) && !r.resolvedSchema) {
|
|
270
|
+
throw new FilloError(
|
|
271
|
+
"Sync did not return the live schema needed to render safely. Publish changes before deploy, and update @usefillo/* or your API.",
|
|
272
|
+
409,
|
|
273
|
+
void 0,
|
|
274
|
+
"sync_snapshot_required"
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
const noticeError = r.syncError ? new FilloError(r.syncError.message, 403, void 0, r.syncError.code) : null;
|
|
278
|
+
setSynced({
|
|
279
|
+
key: syncKey,
|
|
280
|
+
client,
|
|
281
|
+
settled: true,
|
|
282
|
+
formId: r.formId,
|
|
283
|
+
status: r.status ?? null,
|
|
284
|
+
branding: r.branding,
|
|
285
|
+
challenge: r.challenge,
|
|
286
|
+
error: null,
|
|
287
|
+
noticeError,
|
|
288
|
+
staged: Boolean(r.staged),
|
|
289
|
+
resolvedSchema: r.resolvedSchema,
|
|
290
|
+
resolvedTheme: r.resolvedTheme
|
|
291
|
+
});
|
|
201
292
|
const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
|
|
202
293
|
if (r.warning) console.warn(`[fillo] ${r.warning}`);
|
|
294
|
+
if (noticeError) {
|
|
295
|
+
console.warn(
|
|
296
|
+
`[fillo] form sync needs attention (${noticeError.code}): ${noticeError.message}`
|
|
297
|
+
);
|
|
298
|
+
onErrorRef.current?.(noticeError);
|
|
299
|
+
}
|
|
203
300
|
if (r.staged) {
|
|
204
|
-
noticeSync(
|
|
301
|
+
noticeSync(
|
|
302
|
+
codeForm.id,
|
|
303
|
+
"staged",
|
|
304
|
+
`[fillo] "${codeForm.id}": changes staged as a draft \u2014 the live version remains rendered until you publish them at ${dashboardUrl}`
|
|
305
|
+
);
|
|
205
306
|
} else if (r.status === "draft") {
|
|
206
307
|
noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
|
|
207
308
|
}
|
|
208
309
|
}).catch((err) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
310
|
+
if (cancelled) return;
|
|
311
|
+
const error = isFilloError(err) ? err : new FilloError(String(err), 0);
|
|
312
|
+
console.warn(
|
|
313
|
+
`[fillo] form sync failed${error.code ? ` (${error.code})` : ""}: ${error.message}`
|
|
314
|
+
);
|
|
315
|
+
setSynced({
|
|
316
|
+
key: syncKey,
|
|
317
|
+
client,
|
|
318
|
+
settled: true,
|
|
319
|
+
formId: null,
|
|
320
|
+
status: null,
|
|
321
|
+
error,
|
|
322
|
+
noticeError: null,
|
|
323
|
+
staged: false
|
|
324
|
+
});
|
|
325
|
+
onErrorRef.current?.(error);
|
|
212
326
|
});
|
|
213
327
|
return () => {
|
|
214
328
|
cancelled = true;
|
|
215
329
|
};
|
|
216
|
-
}, [hasKey, handle, contentKey, client,
|
|
217
|
-
return synced
|
|
330
|
+
}, [hasKey, handle, contentKey, client, syncKey, allowUnsynced]);
|
|
331
|
+
return synced.key === syncKey && synced.client === client ? {
|
|
332
|
+
formId: synced.formId,
|
|
333
|
+
status: synced.status,
|
|
334
|
+
branding: synced.branding,
|
|
335
|
+
challenge: synced.challenge,
|
|
336
|
+
error: synced.error,
|
|
337
|
+
noticeError: synced.noticeError,
|
|
338
|
+
syncing: Boolean(syncKey) && !synced.settled,
|
|
339
|
+
staged: synced.staged,
|
|
340
|
+
resolvedSchema: synced.resolvedSchema,
|
|
341
|
+
resolvedTheme: synced.resolvedTheme
|
|
342
|
+
} : {
|
|
343
|
+
formId: null,
|
|
344
|
+
status: null,
|
|
345
|
+
error: null,
|
|
346
|
+
noticeError: null,
|
|
347
|
+
syncing: Boolean(syncKey),
|
|
348
|
+
staged: false
|
|
349
|
+
};
|
|
218
350
|
}
|
|
219
351
|
|
|
220
352
|
// src/fields.tsx
|
|
221
|
-
import { isField, pipeBlock, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
|
|
353
|
+
import { isField, pipeBlock, REQUIRED_FIELD_MESSAGE, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
|
|
222
354
|
import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
223
355
|
|
|
224
356
|
// src/phone.tsx
|
|
@@ -230,9 +362,10 @@ import {
|
|
|
230
362
|
flagEmoji,
|
|
231
363
|
formatNational,
|
|
232
364
|
parsePhone,
|
|
365
|
+
positionPhonePopover,
|
|
233
366
|
toE164
|
|
234
367
|
} from "@usefillo/core";
|
|
235
|
-
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as
|
|
368
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
|
|
236
369
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
237
370
|
function localeCountry() {
|
|
238
371
|
if (typeof navigator === "undefined") return void 0;
|
|
@@ -253,31 +386,6 @@ function nationalFor(stored, country) {
|
|
|
253
386
|
const digits = digitsOnly(stored);
|
|
254
387
|
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
|
255
388
|
}
|
|
256
|
-
var PHONE_POPOVER_VIEWPORT_GAP = 8;
|
|
257
|
-
function positionPhonePopover(anchor, popover) {
|
|
258
|
-
if (!anchor || !popover || typeof window === "undefined") return "below";
|
|
259
|
-
const gap = PHONE_POPOVER_VIEWPORT_GAP;
|
|
260
|
-
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
261
|
-
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
262
|
-
const anchorRect = anchor.getBoundingClientRect();
|
|
263
|
-
const spaceBelow = Math.max(0, viewportHeight - anchorRect.bottom - gap);
|
|
264
|
-
const spaceAbove = Math.max(0, anchorRect.top - gap);
|
|
265
|
-
const placement = spaceBelow < popover.scrollHeight && spaceAbove > spaceBelow ? "above" : "below";
|
|
266
|
-
const availableHeight = placement === "above" ? spaceAbove : spaceBelow;
|
|
267
|
-
const maxHeight = Math.max(0, Math.min(availableHeight, viewportHeight - gap * 2));
|
|
268
|
-
const maxWidth = Math.max(0, viewportWidth - gap * 2);
|
|
269
|
-
popover.style.setProperty("--fillo-phone-popover-max-height", `${Math.floor(maxHeight)}px`);
|
|
270
|
-
popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
|
|
271
|
-
popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
|
|
272
|
-
const width = Math.min(popover.offsetWidth, maxWidth);
|
|
273
|
-
let offsetX = 0;
|
|
274
|
-
const rightOverflow = anchorRect.left + width - (viewportWidth - gap);
|
|
275
|
-
if (rightOverflow > 0) offsetX -= rightOverflow;
|
|
276
|
-
const leftOverflow = gap - (anchorRect.left + offsetX);
|
|
277
|
-
if (leftOverflow > 0) offsetX += leftOverflow;
|
|
278
|
-
popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
|
|
279
|
-
return placement;
|
|
280
|
-
}
|
|
281
389
|
function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
282
390
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
283
391
|
const slots = useFieldSlots(field, error);
|
|
@@ -287,7 +395,12 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
287
395
|
const [country, setCountry] = useState2(
|
|
288
396
|
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? PHONE_COUNTRIES[0]
|
|
289
397
|
);
|
|
290
|
-
const touched =
|
|
398
|
+
const touched = useRef3(false);
|
|
399
|
+
useEffect3(() => {
|
|
400
|
+
if (touched.current || !stored) return;
|
|
401
|
+
const restoredCountry = parsePhone(stored).country;
|
|
402
|
+
if (restoredCountry) setCountry(restoredCountry);
|
|
403
|
+
}, [stored]);
|
|
291
404
|
useEffect3(() => {
|
|
292
405
|
if (touched.current) return;
|
|
293
406
|
if (parsePhone(stored).country || countryByIso(schema.defaultCountry)) return;
|
|
@@ -296,8 +409,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
296
409
|
}, []);
|
|
297
410
|
const national = nationalFor(stored, country);
|
|
298
411
|
const display = formatNational(country, national);
|
|
299
|
-
const inputRef =
|
|
300
|
-
const caretDigits =
|
|
412
|
+
const inputRef = useRef3(null);
|
|
413
|
+
const caretDigits = useRef3(null);
|
|
301
414
|
useLayoutEffect(() => {
|
|
302
415
|
const el = inputRef.current;
|
|
303
416
|
if (!el || caretDigits.current == null) return;
|
|
@@ -375,10 +488,10 @@ function CountrySelect({
|
|
|
375
488
|
const [query, setQuery] = useState2("");
|
|
376
489
|
const [active, setActive] = useState2(0);
|
|
377
490
|
const [placement, setPlacement] = useState2("below");
|
|
378
|
-
const rootRef =
|
|
379
|
-
const triggerRef =
|
|
380
|
-
const popoverRef =
|
|
381
|
-
const searchRef =
|
|
491
|
+
const rootRef = useRef3(null);
|
|
492
|
+
const triggerRef = useRef3(null);
|
|
493
|
+
const popoverRef = useRef3(null);
|
|
494
|
+
const searchRef = useRef3(null);
|
|
382
495
|
const listId = `${labelledBy}-countries`;
|
|
383
496
|
const matches = useMemo3(() => {
|
|
384
497
|
const q = query.trim().toLowerCase();
|
|
@@ -448,6 +561,7 @@ function CountrySelect({
|
|
|
448
561
|
className: "fillo-phone-flag",
|
|
449
562
|
"aria-haspopup": "listbox",
|
|
450
563
|
"aria-expanded": open,
|
|
564
|
+
"aria-controls": open ? listId : void 0,
|
|
451
565
|
"aria-label": `Country: ${country.name} (+${country.dialCode})`,
|
|
452
566
|
onClick: () => setOpen((o) => !o),
|
|
453
567
|
children: [
|
|
@@ -471,6 +585,7 @@ function CountrySelect({
|
|
|
471
585
|
"aria-expanded": "true",
|
|
472
586
|
"aria-controls": listId,
|
|
473
587
|
"aria-autocomplete": "list",
|
|
588
|
+
"aria-label": "Search country or code",
|
|
474
589
|
"aria-activedescendant": matches[active] ? `${listId}-opt-${active}` : void 0,
|
|
475
590
|
placeholder: "Search country or code",
|
|
476
591
|
value: query,
|
|
@@ -512,15 +627,15 @@ function CountrySelect({
|
|
|
512
627
|
}
|
|
513
628
|
|
|
514
629
|
// src/signature.tsx
|
|
515
|
-
import { useEffect as useEffect4, useRef as
|
|
630
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
|
|
516
631
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
517
632
|
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
518
633
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
519
634
|
const slots = useFieldSlots(field, error);
|
|
520
635
|
const strings = useStrings();
|
|
521
636
|
const typedInputId = `${ids.inputId}-typed`;
|
|
522
|
-
const canvasRef =
|
|
523
|
-
const drawing =
|
|
637
|
+
const canvasRef = useRef4(null);
|
|
638
|
+
const drawing = useRef4(false);
|
|
524
639
|
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
525
640
|
const [typedName, setTypedName] = useState3("");
|
|
526
641
|
useEffect4(() => {
|
|
@@ -537,12 +652,31 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
537
652
|
ctx.lineCap = "round";
|
|
538
653
|
ctx.lineJoin = "round";
|
|
539
654
|
ctx.strokeStyle = getComputedStyle(canvas).color;
|
|
655
|
+
}, []);
|
|
656
|
+
useEffect4(() => {
|
|
657
|
+
const canvas = canvasRef.current;
|
|
658
|
+
const ctx = canvas?.getContext("2d");
|
|
659
|
+
if (!canvas || !ctx) return;
|
|
660
|
+
let cancelled = false;
|
|
661
|
+
const rect = canvas.getBoundingClientRect();
|
|
540
662
|
if (typeof value === "string" && value.startsWith("data:image/")) {
|
|
663
|
+
setHasInk(true);
|
|
541
664
|
const img = new Image();
|
|
542
|
-
img.onload = () =>
|
|
665
|
+
img.onload = () => {
|
|
666
|
+
if (cancelled) return;
|
|
667
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
668
|
+
ctx.drawImage(img, 0, 0, rect.width, rect.height);
|
|
669
|
+
};
|
|
543
670
|
img.src = value;
|
|
671
|
+
} else {
|
|
672
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
673
|
+
setHasInk(false);
|
|
674
|
+
setTypedName("");
|
|
544
675
|
}
|
|
545
|
-
|
|
676
|
+
return () => {
|
|
677
|
+
cancelled = true;
|
|
678
|
+
};
|
|
679
|
+
}, [value]);
|
|
546
680
|
function point(e) {
|
|
547
681
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
548
682
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
@@ -622,6 +756,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
622
756
|
onPointerDown: start,
|
|
623
757
|
onPointerMove: move,
|
|
624
758
|
onPointerUp: end,
|
|
759
|
+
onPointerCancel: end,
|
|
625
760
|
onPointerLeave: end
|
|
626
761
|
}
|
|
627
762
|
),
|
|
@@ -657,21 +792,15 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
657
792
|
}
|
|
658
793
|
|
|
659
794
|
// src/upload.tsx
|
|
660
|
-
import { useEffect as useEffect5, useRef as
|
|
795
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
|
|
661
796
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
662
|
-
function uploadStatus(inFlight, doneCount) {
|
|
797
|
+
function uploadStatus(inFlight, doneCount, strings) {
|
|
663
798
|
const failed = inFlight.filter((f) => f.error).length;
|
|
664
799
|
const active = inFlight.length - failed;
|
|
665
800
|
const parts = [];
|
|
666
|
-
if (active > 0)
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
if (failed > 0) {
|
|
670
|
-
parts.push(`${failed} ${failed === 1 ? "upload" : "uploads"} failed.`);
|
|
671
|
-
}
|
|
672
|
-
if (active === 0 && doneCount > 0) {
|
|
673
|
-
parts.push(`${doneCount} ${doneCount === 1 ? "file" : "files"} uploaded.`);
|
|
674
|
-
}
|
|
801
|
+
if (active > 0) parts.push(strings.filesUploading(active));
|
|
802
|
+
if (failed > 0) parts.push(strings.uploadsFailed(failed));
|
|
803
|
+
if (active === 0 && doneCount > 0) parts.push(strings.filesUploaded(doneCount));
|
|
675
804
|
return parts.join(" ");
|
|
676
805
|
}
|
|
677
806
|
function formatBytes(bytes) {
|
|
@@ -685,41 +814,49 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
685
814
|
const slots = useFieldSlots(field, error);
|
|
686
815
|
const strings = useStrings();
|
|
687
816
|
const schema = field;
|
|
688
|
-
const inputRef =
|
|
817
|
+
const inputRef = useRef5(null);
|
|
689
818
|
const [inFlight, setInFlight] = useState4([]);
|
|
690
819
|
const [dragOver, setDragOver] = useState4(false);
|
|
691
|
-
const mountedRef =
|
|
692
|
-
const controllers =
|
|
693
|
-
const inFlightCount =
|
|
694
|
-
|
|
695
|
-
|
|
820
|
+
const mountedRef = useRef5(true);
|
|
821
|
+
const controllers = useRef5(/* @__PURE__ */ new Map());
|
|
822
|
+
const inFlightCount = useRef5(0);
|
|
823
|
+
const setUploading = api.setUploading;
|
|
824
|
+
const fieldId = field.id;
|
|
825
|
+
useEffect5(() => {
|
|
826
|
+
mountedRef.current = true;
|
|
827
|
+
return () => {
|
|
696
828
|
mountedRef.current = false;
|
|
697
829
|
controllers.current.forEach((c) => c.abort());
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
);
|
|
830
|
+
if (inFlightCount.current > 0) setUploading(fieldId, false);
|
|
831
|
+
};
|
|
832
|
+
}, [fieldId, setUploading]);
|
|
701
833
|
const files = Array.isArray(value) ? value : [];
|
|
834
|
+
const filesRef = useRef5(files);
|
|
835
|
+
filesRef.current = files;
|
|
702
836
|
const maxFiles = schema.maxFiles ?? 1;
|
|
703
|
-
const
|
|
837
|
+
const maxMb = schema.maxFileSizeMb ?? 500;
|
|
838
|
+
const maxBytes = maxMb * 1024 * 1024;
|
|
704
839
|
const canUpload = Boolean(api.client && api.formId);
|
|
705
|
-
const
|
|
840
|
+
const activeInFlight = inFlight.filter((f) => !f.error).length;
|
|
841
|
+
const remaining = maxFiles - files.length - activeInFlight;
|
|
706
842
|
async function startUpload(file) {
|
|
843
|
+
if (maxFiles - filesRef.current.length - inFlightCount.current <= 0) return;
|
|
707
844
|
const key = `${file.name}-${file.size}-${Math.floor(performance.now())}`;
|
|
708
845
|
if (file.size > maxBytes) {
|
|
709
846
|
setInFlight((prev) => [
|
|
710
847
|
...prev,
|
|
711
|
-
{ key, name: file.name, size: file.size, fraction: 0,
|
|
848
|
+
{ key, name: file.name, size: file.size, fraction: 0, file, tooLarge: true, error: strings.fileTooLarge(maxMb) }
|
|
712
849
|
]);
|
|
713
850
|
return;
|
|
714
851
|
}
|
|
715
|
-
setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0 }]);
|
|
852
|
+
setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0, file }]);
|
|
716
853
|
inFlightCount.current += 1;
|
|
717
|
-
|
|
854
|
+
setUploading(fieldId, true);
|
|
718
855
|
const controller = new AbortController();
|
|
719
856
|
controllers.current.set(key, controller);
|
|
720
857
|
try {
|
|
721
858
|
const uploaded = await api.client.uploadFile(api.formId, file, {
|
|
722
|
-
fieldId
|
|
859
|
+
fieldId,
|
|
723
860
|
signal: controller.signal,
|
|
724
861
|
onProgress: ({ fraction }) => {
|
|
725
862
|
if (mountedRef.current) {
|
|
@@ -729,11 +866,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
729
866
|
});
|
|
730
867
|
if (!mountedRef.current) return;
|
|
731
868
|
setInFlight((prev) => prev.filter((f) => f.key !== key));
|
|
732
|
-
const
|
|
733
|
-
|
|
869
|
+
const next = [...filesRef.current, uploaded].slice(0, maxFiles);
|
|
870
|
+
filesRef.current = next;
|
|
871
|
+
setValue(next);
|
|
734
872
|
} catch (err) {
|
|
735
873
|
if (mountedRef.current) {
|
|
736
|
-
const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message :
|
|
874
|
+
const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message : strings.uploadFailed;
|
|
737
875
|
setInFlight(
|
|
738
876
|
(prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
|
|
739
877
|
);
|
|
@@ -741,7 +879,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
741
879
|
} finally {
|
|
742
880
|
controllers.current.delete(key);
|
|
743
881
|
inFlightCount.current -= 1;
|
|
744
|
-
if (mountedRef.current && inFlightCount.current === 0)
|
|
882
|
+
if (mountedRef.current && inFlightCount.current === 0) setUploading(fieldId, false);
|
|
745
883
|
}
|
|
746
884
|
}
|
|
747
885
|
function cancelUpload(key) {
|
|
@@ -750,11 +888,14 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
750
888
|
}
|
|
751
889
|
function handleFiles(list) {
|
|
752
890
|
if (!list) return;
|
|
753
|
-
|
|
891
|
+
const currentRoom = Math.max(0, maxFiles - filesRef.current.length - inFlightCount.current);
|
|
892
|
+
Array.from(list).slice(0, currentRoom).forEach((file) => void startUpload(file));
|
|
754
893
|
if (inputRef.current) inputRef.current.value = "";
|
|
755
894
|
}
|
|
756
895
|
function removeFile(fileId) {
|
|
757
|
-
|
|
896
|
+
const next = filesRef.current.filter((f) => f.fileId !== fileId);
|
|
897
|
+
filesRef.current = next;
|
|
898
|
+
setValue(next);
|
|
758
899
|
}
|
|
759
900
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
760
901
|
return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
|
|
@@ -782,12 +923,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
782
923
|
onClick: () => canUpload && inputRef.current?.click(),
|
|
783
924
|
role: "button",
|
|
784
925
|
tabIndex: 0,
|
|
926
|
+
"aria-disabled": !canUpload || void 0,
|
|
785
927
|
"aria-labelledby": ids.labelId,
|
|
786
928
|
"aria-required": field.required ? true : void 0,
|
|
787
929
|
"aria-invalid": error ? true : void 0,
|
|
788
930
|
"aria-describedby": describedBy,
|
|
789
931
|
onKeyDown: (e) => {
|
|
790
|
-
if (
|
|
932
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
933
|
+
e.preventDefault();
|
|
934
|
+
if (canUpload) inputRef.current?.click();
|
|
935
|
+
}
|
|
791
936
|
},
|
|
792
937
|
children: [
|
|
793
938
|
/* @__PURE__ */ jsx3(
|
|
@@ -797,23 +942,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
797
942
|
id: ids.inputId,
|
|
798
943
|
type: "file",
|
|
799
944
|
hidden: true,
|
|
945
|
+
disabled: !canUpload,
|
|
800
946
|
multiple: maxFiles > 1,
|
|
801
947
|
accept: schema.accept?.join(","),
|
|
802
948
|
onChange: (e) => canUpload && handleFiles(e.target.files)
|
|
803
949
|
}
|
|
804
950
|
),
|
|
805
951
|
canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
806
|
-
/* @__PURE__ */
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
" here or click to browse"
|
|
810
|
-
] }),
|
|
811
|
-
/* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-hint", children: [
|
|
812
|
-
"Up to ",
|
|
813
|
-
schema.maxFileSizeMb ?? 500,
|
|
814
|
-
" MB per file \xB7 resumable"
|
|
815
|
-
] })
|
|
816
|
-
] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: "Uploads are disabled in preview" })
|
|
952
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
|
|
953
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
|
|
954
|
+
] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.uploadsDisabled })
|
|
817
955
|
]
|
|
818
956
|
}
|
|
819
957
|
),
|
|
@@ -824,7 +962,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
824
962
|
"aria-live": "polite",
|
|
825
963
|
className: "fillo-sr-only",
|
|
826
964
|
style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
|
|
827
|
-
children: uploadStatus(inFlight, files.length)
|
|
965
|
+
children: uploadStatus(inFlight, files.length, strings)
|
|
828
966
|
}
|
|
829
967
|
),
|
|
830
968
|
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
@@ -857,6 +995,21 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
857
995
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
858
996
|
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
859
997
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
|
|
998
|
+
f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx3(
|
|
999
|
+
"button",
|
|
1000
|
+
{
|
|
1001
|
+
type: "button",
|
|
1002
|
+
className: "fillo-file-retry",
|
|
1003
|
+
"aria-label": `${strings.uploadRetry} ${f.name}`,
|
|
1004
|
+
onClick: () => {
|
|
1005
|
+
const retryFile = f.file;
|
|
1006
|
+
if (!retryFile) return;
|
|
1007
|
+
setInFlight((prev) => prev.filter((x) => x.key !== f.key));
|
|
1008
|
+
void startUpload(retryFile);
|
|
1009
|
+
},
|
|
1010
|
+
children: strings.uploadRetry
|
|
1011
|
+
}
|
|
1012
|
+
),
|
|
860
1013
|
/* @__PURE__ */ jsx3(
|
|
861
1014
|
"button",
|
|
862
1015
|
{
|
|
@@ -975,6 +1128,9 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
|
975
1128
|
type,
|
|
976
1129
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
977
1130
|
placeholder: field.placeholder,
|
|
1131
|
+
maxLength: "maxLength" in field ? field.maxLength : void 0,
|
|
1132
|
+
min: field.kind === "number" ? field.min : void 0,
|
|
1133
|
+
max: field.kind === "number" ? field.max : void 0,
|
|
978
1134
|
onChange: (e) => setValue(e.target.value),
|
|
979
1135
|
...fieldAria(field, error, ids)
|
|
980
1136
|
}
|
|
@@ -991,6 +1147,7 @@ function LongText({ field, value, error, setValue, ids: providedIds }) {
|
|
|
991
1147
|
rows: 4,
|
|
992
1148
|
value: typeof value === "string" ? value : "",
|
|
993
1149
|
placeholder: field.placeholder,
|
|
1150
|
+
maxLength: "maxLength" in field ? field.maxLength : void 0,
|
|
994
1151
|
onChange: (e) => setValue(e.target.value),
|
|
995
1152
|
...fieldAria(field, error, ids)
|
|
996
1153
|
}
|
|
@@ -1246,12 +1403,18 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1246
1403
|
}
|
|
1247
1404
|
) });
|
|
1248
1405
|
}
|
|
1249
|
-
|
|
1406
|
+
function otherSentinel(field) {
|
|
1407
|
+
let value = "__fillo_other__";
|
|
1408
|
+
const ids = new Set(field.options.map((option) => option.id));
|
|
1409
|
+
while (ids.has(value)) value += "_";
|
|
1410
|
+
return value;
|
|
1411
|
+
}
|
|
1250
1412
|
function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
1251
1413
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1252
1414
|
const strings = useStrings();
|
|
1253
1415
|
const choice = field;
|
|
1254
1416
|
const options = useDisplayOptions(choice);
|
|
1417
|
+
const otherValue = otherSentinel(choice);
|
|
1255
1418
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
1256
1419
|
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
1257
1420
|
const otherActive = isOtherValue || value === "" && otherOn;
|
|
@@ -1264,9 +1427,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1264
1427
|
id: ids.inputId,
|
|
1265
1428
|
...control,
|
|
1266
1429
|
...fieldAria(field, error, ids),
|
|
1267
|
-
value: otherActive ?
|
|
1430
|
+
value: otherActive ? otherValue : typeof value === "string" ? value : "",
|
|
1268
1431
|
onChange: (e) => {
|
|
1269
|
-
if (e.target.value ===
|
|
1432
|
+
if (e.target.value === otherValue) {
|
|
1270
1433
|
setOtherOn(true);
|
|
1271
1434
|
setValue("");
|
|
1272
1435
|
} else {
|
|
@@ -1277,7 +1440,7 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1277
1440
|
children: [
|
|
1278
1441
|
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
|
|
1279
1442
|
options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
|
|
1280
|
-
choice.allowOther && /* @__PURE__ */ jsx4("option", { value:
|
|
1443
|
+
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: otherValue, children: `${strings.other}\u2026` })
|
|
1281
1444
|
]
|
|
1282
1445
|
}
|
|
1283
1446
|
),
|
|
@@ -1301,17 +1464,15 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1301
1464
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1302
1465
|
const strings = useStrings();
|
|
1303
1466
|
const appearance = useFilloAppearance();
|
|
1467
|
+
const slots = useFieldSlots(field, error);
|
|
1304
1468
|
const checkbox = field;
|
|
1305
1469
|
if (checkbox.appearance === "toggle") {
|
|
1306
1470
|
return /* @__PURE__ */ jsxs4(
|
|
1307
1471
|
"div",
|
|
1308
1472
|
{
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
"data-kind": field.kind,
|
|
1313
|
-
"data-invalid": error ? "" : void 0,
|
|
1314
|
-
"data-required": field.required ? "" : void 0,
|
|
1473
|
+
...slots.wrapperProps(
|
|
1474
|
+
`fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
|
|
1475
|
+
),
|
|
1315
1476
|
children: [
|
|
1316
1477
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1317
1478
|
/* @__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: [
|
|
@@ -1333,8 +1494,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1333
1494
|
),
|
|
1334
1495
|
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
|
|
1335
1496
|
] }),
|
|
1336
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1337
|
-
error && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1497
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
1498
|
+
error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
1338
1499
|
]
|
|
1339
1500
|
}
|
|
1340
1501
|
);
|
|
@@ -1342,12 +1503,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1342
1503
|
return /* @__PURE__ */ jsxs4(
|
|
1343
1504
|
"div",
|
|
1344
1505
|
{
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
"data-kind": field.kind,
|
|
1349
|
-
"data-invalid": error ? "" : void 0,
|
|
1350
|
-
"data-required": field.required ? "" : void 0,
|
|
1506
|
+
...slots.wrapperProps(
|
|
1507
|
+
`fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
|
|
1508
|
+
),
|
|
1351
1509
|
children: [
|
|
1352
1510
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1353
1511
|
/* @__PURE__ */ jsx4(
|
|
@@ -1366,8 +1524,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1366
1524
|
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
|
|
1367
1525
|
] })
|
|
1368
1526
|
] }),
|
|
1369
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1370
|
-
error && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1527
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
1528
|
+
error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
1371
1529
|
]
|
|
1372
1530
|
}
|
|
1373
1531
|
);
|
|
@@ -1513,73 +1671,106 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1513
1671
|
[next[index], next[target]] = [next[target], next[index]];
|
|
1514
1672
|
setValue(next);
|
|
1515
1673
|
}
|
|
1516
|
-
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1674
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1675
|
+
"div",
|
|
1676
|
+
{
|
|
1677
|
+
id: ids.inputId,
|
|
1678
|
+
role: "group",
|
|
1679
|
+
"aria-labelledby": ids.labelId,
|
|
1680
|
+
...fieldAria(field, error, ids),
|
|
1681
|
+
children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
|
|
1682
|
+
const option = ranking.options.find((o) => o.id === id);
|
|
1683
|
+
if (!option) return null;
|
|
1684
|
+
return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
|
|
1685
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
|
|
1686
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
|
|
1687
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
|
|
1688
|
+
/* @__PURE__ */ jsx4(
|
|
1689
|
+
"button",
|
|
1690
|
+
{
|
|
1691
|
+
type: "button",
|
|
1692
|
+
className: "fillo-ranking-move",
|
|
1693
|
+
"aria-label": `Move ${option.label} up`,
|
|
1694
|
+
disabled: index === 0,
|
|
1695
|
+
onClick: () => move(id, -1),
|
|
1696
|
+
children: "\u2191"
|
|
1697
|
+
}
|
|
1698
|
+
),
|
|
1699
|
+
/* @__PURE__ */ jsx4(
|
|
1700
|
+
"button",
|
|
1701
|
+
{
|
|
1702
|
+
type: "button",
|
|
1703
|
+
className: "fillo-ranking-move",
|
|
1704
|
+
"aria-label": `Move ${option.label} down`,
|
|
1705
|
+
disabled: index === order.length - 1,
|
|
1706
|
+
onClick: () => move(id, 1),
|
|
1707
|
+
children: "\u2193"
|
|
1708
|
+
}
|
|
1709
|
+
)
|
|
1710
|
+
] })
|
|
1711
|
+
] }, id);
|
|
1712
|
+
}) })
|
|
1713
|
+
}
|
|
1714
|
+
) });
|
|
1548
1715
|
}
|
|
1549
1716
|
function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
1550
1717
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1551
1718
|
const matrix = field;
|
|
1552
1719
|
const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1553
|
-
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1720
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1721
|
+
"div",
|
|
1722
|
+
{
|
|
1723
|
+
id: ids.inputId,
|
|
1724
|
+
className: "fillo-matrix-wrap",
|
|
1725
|
+
role: "group",
|
|
1726
|
+
"aria-labelledby": ids.labelId,
|
|
1727
|
+
...fieldAria(field, error, ids),
|
|
1728
|
+
children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
|
|
1729
|
+
/* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1730
|
+
/* @__PURE__ */ jsx4("th", {}),
|
|
1731
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
|
|
1732
|
+
] }) }),
|
|
1733
|
+
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
|
|
1734
|
+
const rowLabelId = `${ids.inputId}-${row.id}`;
|
|
1735
|
+
const answered = answers[row.id] !== void 0;
|
|
1736
|
+
const rowInvalid = Boolean(error) && field.required && !answered;
|
|
1737
|
+
const radioId = (col) => `${ids.inputId}-${row.id}-${col.id}`;
|
|
1738
|
+
return (
|
|
1739
|
+
// The <tr> keeps its native row role so the grid's row/column
|
|
1740
|
+
// header association survives for screen readers. The row's
|
|
1741
|
+
// radiogroup lives on an inner element that aria-owns the radios
|
|
1742
|
+
// scattered across the cells (they can't be DOM-nested inside a
|
|
1743
|
+
// single element within a table row).
|
|
1744
|
+
/* @__PURE__ */ jsxs4("tr", { children: [
|
|
1745
|
+
/* @__PURE__ */ jsx4("th", { scope: "row", children: /* @__PURE__ */ jsx4(
|
|
1746
|
+
"div",
|
|
1747
|
+
{
|
|
1748
|
+
role: "radiogroup",
|
|
1749
|
+
"aria-labelledby": rowLabelId,
|
|
1750
|
+
"aria-owns": matrix.columns.map(radioId).join(" "),
|
|
1751
|
+
children: /* @__PURE__ */ jsx4("span", { id: rowLabelId, children: row.label })
|
|
1752
|
+
}
|
|
1753
|
+
) }),
|
|
1754
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
|
|
1755
|
+
"input",
|
|
1756
|
+
{
|
|
1757
|
+
id: radioId(col),
|
|
1758
|
+
type: "radio",
|
|
1759
|
+
className: "fillo-option-input",
|
|
1760
|
+
name: `${ids.name}-${row.id}`,
|
|
1761
|
+
"aria-label": `${row.label}: ${col.label}`,
|
|
1762
|
+
"aria-required": field.required ? true : void 0,
|
|
1763
|
+
"aria-invalid": rowInvalid ? true : void 0,
|
|
1764
|
+
checked: answers[row.id] === col.id,
|
|
1765
|
+
onChange: () => setValue({ ...answers, [row.id]: col.id })
|
|
1766
|
+
}
|
|
1767
|
+
) }, col.id))
|
|
1768
|
+
] }, row.id)
|
|
1769
|
+
);
|
|
1770
|
+
}) })
|
|
1771
|
+
] })
|
|
1772
|
+
}
|
|
1773
|
+
) });
|
|
1583
1774
|
}
|
|
1584
1775
|
function Hidden() {
|
|
1585
1776
|
return null;
|
|
@@ -1633,8 +1824,8 @@ function blockPropsEqual(prev, next) {
|
|
|
1633
1824
|
const id = next.block.id;
|
|
1634
1825
|
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1635
1826
|
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1636
|
-
if (next.block.kind === "custom") {
|
|
1637
|
-
return prev.api
|
|
1827
|
+
if (next.block.kind === "custom" || isField(next.block) && Boolean(next.components?.[next.block.kind])) {
|
|
1828
|
+
return prev.api === next.api;
|
|
1638
1829
|
}
|
|
1639
1830
|
if ((blockHasPiping(prev.block) || blockHasPiping(next.block)) && prev.api.data !== next.api.data) {
|
|
1640
1831
|
return false;
|
|
@@ -1649,6 +1840,7 @@ function blockPropsEqual(prev, next) {
|
|
|
1649
1840
|
function BlockRendererInner({ block, api, components, customComponents }) {
|
|
1650
1841
|
block = pipeBlock(block, api.data, api.form);
|
|
1651
1842
|
const ids = useFilloFieldIds(block.id);
|
|
1843
|
+
const strings = useStrings();
|
|
1652
1844
|
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
1653
1845
|
let Component;
|
|
1654
1846
|
if (block.kind === "custom") {
|
|
@@ -1665,12 +1857,14 @@ function BlockRendererInner({ block, api, components, customComponents }) {
|
|
|
1665
1857
|
} else {
|
|
1666
1858
|
Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
|
|
1667
1859
|
}
|
|
1860
|
+
const rawError = api.errors[block.id];
|
|
1861
|
+
const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
|
|
1668
1862
|
return /* @__PURE__ */ jsx4(
|
|
1669
1863
|
Component,
|
|
1670
1864
|
{
|
|
1671
1865
|
field: block,
|
|
1672
1866
|
value: api.data[block.id],
|
|
1673
|
-
error
|
|
1867
|
+
error,
|
|
1674
1868
|
setValue: (v) => {
|
|
1675
1869
|
api.setValue(block.id, v);
|
|
1676
1870
|
const ctx = {
|
|
@@ -1713,8 +1907,127 @@ function FormField({
|
|
|
1713
1907
|
);
|
|
1714
1908
|
}
|
|
1715
1909
|
|
|
1910
|
+
// src/turnstile.tsx
|
|
1911
|
+
import { useEffect as useEffect7, useRef as useRef6 } from "react";
|
|
1912
|
+
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
1913
|
+
var SCRIPT_SRC = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
1914
|
+
var SCRIPT_ID = "fillo-turnstile-script";
|
|
1915
|
+
var scriptPromise = null;
|
|
1916
|
+
function turnstileGlobal() {
|
|
1917
|
+
return globalThis.turnstile;
|
|
1918
|
+
}
|
|
1919
|
+
function loadTurnstileScript() {
|
|
1920
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
1921
|
+
return Promise.reject(new Error("Turnstile needs a browser"));
|
|
1922
|
+
}
|
|
1923
|
+
if (turnstileGlobal()) return Promise.resolve();
|
|
1924
|
+
if (scriptPromise) return scriptPromise;
|
|
1925
|
+
scriptPromise = new Promise((resolve, reject) => {
|
|
1926
|
+
const onReady = () => {
|
|
1927
|
+
if (turnstileGlobal()) resolve();
|
|
1928
|
+
else reject(new Error("Turnstile loaded without its global"));
|
|
1929
|
+
};
|
|
1930
|
+
const existing = document.getElementById(SCRIPT_ID);
|
|
1931
|
+
if (existing) {
|
|
1932
|
+
if (turnstileGlobal()) {
|
|
1933
|
+
resolve();
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
existing.addEventListener("load", onReady, { once: true });
|
|
1937
|
+
existing.addEventListener(
|
|
1938
|
+
"error",
|
|
1939
|
+
() => reject(new Error("Turnstile script failed to load")),
|
|
1940
|
+
{ once: true }
|
|
1941
|
+
);
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
const script = document.createElement("script");
|
|
1945
|
+
script.id = SCRIPT_ID;
|
|
1946
|
+
script.src = SCRIPT_SRC;
|
|
1947
|
+
script.async = true;
|
|
1948
|
+
script.defer = true;
|
|
1949
|
+
script.addEventListener("load", onReady, { once: true });
|
|
1950
|
+
script.addEventListener(
|
|
1951
|
+
"error",
|
|
1952
|
+
() => {
|
|
1953
|
+
script.remove();
|
|
1954
|
+
scriptPromise = null;
|
|
1955
|
+
reject(new Error("Turnstile script failed to load"));
|
|
1956
|
+
},
|
|
1957
|
+
{ once: true }
|
|
1958
|
+
);
|
|
1959
|
+
document.head.appendChild(script);
|
|
1960
|
+
});
|
|
1961
|
+
return scriptPromise;
|
|
1962
|
+
}
|
|
1963
|
+
function TurnstileWidget({
|
|
1964
|
+
siteKey,
|
|
1965
|
+
onToken,
|
|
1966
|
+
onError,
|
|
1967
|
+
registerReset,
|
|
1968
|
+
className
|
|
1969
|
+
}) {
|
|
1970
|
+
const containerRef = useRef6(null);
|
|
1971
|
+
const widgetIdRef = useRef6(null);
|
|
1972
|
+
const onTokenRef = useRef6(onToken);
|
|
1973
|
+
onTokenRef.current = onToken;
|
|
1974
|
+
const onErrorRef = useRef6(onError);
|
|
1975
|
+
onErrorRef.current = onError;
|
|
1976
|
+
const registerResetRef = useRef6(registerReset);
|
|
1977
|
+
registerResetRef.current = registerReset;
|
|
1978
|
+
useEffect7(() => {
|
|
1979
|
+
let cancelled = false;
|
|
1980
|
+
loadTurnstileScript().then(() => {
|
|
1981
|
+
if (cancelled || !containerRef.current) return;
|
|
1982
|
+
const api = turnstileGlobal();
|
|
1983
|
+
if (!api) {
|
|
1984
|
+
onErrorRef.current?.();
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
const clearAndReset = () => {
|
|
1988
|
+
onTokenRef.current(void 0);
|
|
1989
|
+
if (widgetIdRef.current) api.reset(widgetIdRef.current);
|
|
1990
|
+
};
|
|
1991
|
+
widgetIdRef.current = api.render(containerRef.current, {
|
|
1992
|
+
sitekey: siteKey,
|
|
1993
|
+
theme: "auto",
|
|
1994
|
+
callback: (token) => onTokenRef.current(token),
|
|
1995
|
+
"error-callback": () => {
|
|
1996
|
+
onTokenRef.current(void 0);
|
|
1997
|
+
onErrorRef.current?.();
|
|
1998
|
+
},
|
|
1999
|
+
"expired-callback": clearAndReset,
|
|
2000
|
+
"timeout-callback": clearAndReset
|
|
2001
|
+
});
|
|
2002
|
+
registerResetRef.current?.(clearAndReset);
|
|
2003
|
+
}).catch(() => {
|
|
2004
|
+
if (!cancelled) onErrorRef.current?.();
|
|
2005
|
+
});
|
|
2006
|
+
return () => {
|
|
2007
|
+
cancelled = true;
|
|
2008
|
+
const api = turnstileGlobal();
|
|
2009
|
+
if (widgetIdRef.current && api) {
|
|
2010
|
+
try {
|
|
2011
|
+
api.remove(widgetIdRef.current);
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
widgetIdRef.current = null;
|
|
2016
|
+
onTokenRef.current(void 0);
|
|
2017
|
+
};
|
|
2018
|
+
}, [siteKey]);
|
|
2019
|
+
return /* @__PURE__ */ jsx5(
|
|
2020
|
+
"div",
|
|
2021
|
+
{
|
|
2022
|
+
ref: containerRef,
|
|
2023
|
+
className,
|
|
2024
|
+
"data-fillo": "turnstile"
|
|
2025
|
+
}
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1716
2029
|
// src/FilloForm.tsx
|
|
1717
|
-
import { Fragment as Fragment2, jsx as
|
|
2030
|
+
import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
1718
2031
|
function themeProps(theme) {
|
|
1719
2032
|
const safeTheme = normalizeFormTheme(theme);
|
|
1720
2033
|
if (!safeTheme) return {};
|
|
@@ -1749,7 +2062,9 @@ function badgeUrl(href) {
|
|
|
1749
2062
|
function focusFirstInvalid(formEl) {
|
|
1750
2063
|
if (!formEl || typeof window === "undefined") return;
|
|
1751
2064
|
const run = () => {
|
|
1752
|
-
const
|
|
2065
|
+
const invalid = formEl.querySelector('[aria-invalid="true"]');
|
|
2066
|
+
const focusable = 'input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
2067
|
+
const target = invalid?.matches(focusable) ? invalid : invalid?.querySelector(focusable);
|
|
1753
2068
|
target?.focus?.();
|
|
1754
2069
|
};
|
|
1755
2070
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
@@ -1777,35 +2092,56 @@ function FilloForm(props) {
|
|
|
1777
2092
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
1778
2093
|
[props.client, formId]
|
|
1779
2094
|
);
|
|
1780
|
-
const [
|
|
1781
|
-
const [
|
|
1782
|
-
const [loadError, setLoadError] = useState6(null);
|
|
1783
|
-
const [closed, setClosed] = useState6(false);
|
|
1784
|
-
const [fetchedBranding, setFetchedBranding] = useState6(null);
|
|
2095
|
+
const [loaded, setLoaded] = useState6(null);
|
|
2096
|
+
const [loadFailure, setLoadFailure] = useState6(null);
|
|
1785
2097
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1786
|
-
const inlineSchema = codeForm ? codeForm.schema : form;
|
|
1787
2098
|
const onError = props.onError;
|
|
1788
|
-
const {
|
|
1789
|
-
|
|
1790
|
-
|
|
2099
|
+
const {
|
|
2100
|
+
formId: syncedFormId,
|
|
2101
|
+
status: syncedStatus,
|
|
2102
|
+
branding: syncedBranding,
|
|
2103
|
+
challenge: syncedChallenge,
|
|
2104
|
+
error: syncError,
|
|
2105
|
+
noticeError: syncNoticeError,
|
|
2106
|
+
syncing: codeFormSyncing,
|
|
2107
|
+
staged: codeChangesStaged,
|
|
2108
|
+
resolvedSchema,
|
|
2109
|
+
resolvedTheme
|
|
2110
|
+
} = useCodeFormSync(form, client, onError, {
|
|
2111
|
+
// An absent client is intentional render-only use. Builder previews and an
|
|
2112
|
+
// explicit canonical id also do not need publishable-key resolution.
|
|
2113
|
+
allowUnsynced: Boolean(!client || props.skipValidation || formId)
|
|
2114
|
+
});
|
|
2115
|
+
const renderResolvedSnapshot = Boolean(resolvedSchema && !isDevEnv());
|
|
2116
|
+
const inlineSchema = codeForm ? renderResolvedSnapshot ? resolvedSchema : codeForm.schema : form;
|
|
2117
|
+
const onErrorRef = useRef7(onError);
|
|
2118
|
+
useEffect8(() => {
|
|
1791
2119
|
onErrorRef.current = onError;
|
|
1792
2120
|
});
|
|
1793
2121
|
const needsFetch = !inlineSchema && Boolean(client && formId);
|
|
1794
|
-
|
|
1795
|
-
if (!needsFetch || !client || !formId)
|
|
2122
|
+
useEffect8(() => {
|
|
2123
|
+
if (!needsFetch || !client || !formId) {
|
|
2124
|
+
setLoaded(null);
|
|
2125
|
+
setLoadFailure(null);
|
|
2126
|
+
return;
|
|
2127
|
+
}
|
|
1796
2128
|
let cancelled = false;
|
|
1797
|
-
|
|
1798
|
-
|
|
2129
|
+
setLoaded(null);
|
|
2130
|
+
setLoadFailure(null);
|
|
1799
2131
|
client.getForm(formId).then((published) => {
|
|
1800
2132
|
if (cancelled) return;
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2133
|
+
setLoaded({
|
|
2134
|
+
formId,
|
|
2135
|
+
schema: published.schema,
|
|
2136
|
+
theme: published.theme,
|
|
2137
|
+
closed: Boolean(published.closed),
|
|
2138
|
+
branding: published.branding ?? null,
|
|
2139
|
+
challenge: published.challenge ?? null
|
|
2140
|
+
});
|
|
1805
2141
|
}).catch((err) => {
|
|
1806
2142
|
if (cancelled) return;
|
|
1807
2143
|
const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
|
|
1808
|
-
|
|
2144
|
+
setLoadFailure({ formId, error });
|
|
1809
2145
|
onErrorRef.current?.(error);
|
|
1810
2146
|
});
|
|
1811
2147
|
return () => {
|
|
@@ -1813,32 +2149,53 @@ function FilloForm(props) {
|
|
|
1813
2149
|
};
|
|
1814
2150
|
}, [needsFetch, client, formId]);
|
|
1815
2151
|
const strings = resolveStrings(props.strings);
|
|
1816
|
-
const
|
|
2152
|
+
const currentLoaded = needsFetch && loaded?.formId === formId ? loaded : null;
|
|
2153
|
+
const loadError = needsFetch && loadFailure && loadFailure.formId === formId ? loadFailure.error : null;
|
|
2154
|
+
const rawSchema = inlineSchema ?? currentLoaded?.schema ?? null;
|
|
1817
2155
|
const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1818
2156
|
const schema = normalized?.ok ? normalized.schema : null;
|
|
1819
|
-
const
|
|
2157
|
+
const resolvedCodeTheme = renderResolvedSnapshot ? resolvedTheme ?? void 0 : codeForm?.theme;
|
|
2158
|
+
const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
|
|
1820
2159
|
if (normalized && !normalized.ok) {
|
|
1821
2160
|
const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1822
|
-
if (props.renderError) return /* @__PURE__ */
|
|
1823
|
-
return /* @__PURE__ */
|
|
2161
|
+
if (props.renderError) return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(error) });
|
|
2162
|
+
return /* @__PURE__ */ jsx6("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: strings.renderFailed });
|
|
1824
2163
|
}
|
|
1825
2164
|
if (loadError) {
|
|
1826
2165
|
if (props.renderError) {
|
|
1827
|
-
return /* @__PURE__ */
|
|
2166
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(loadError) });
|
|
1828
2167
|
}
|
|
1829
2168
|
const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
|
|
1830
|
-
return /* @__PURE__ */
|
|
2169
|
+
return /* @__PURE__ */ jsx6("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
|
|
1831
2170
|
}
|
|
1832
|
-
if (closed) {
|
|
1833
|
-
return /* @__PURE__ */
|
|
2171
|
+
if (currentLoaded?.closed) {
|
|
2172
|
+
return /* @__PURE__ */ jsx6("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx6("p", { className: "fillo-closed", children: strings.closed }) });
|
|
2173
|
+
}
|
|
2174
|
+
const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
|
|
2175
|
+
if (canonicalSyncError && props.renderError) {
|
|
2176
|
+
return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(canonicalSyncError) });
|
|
2177
|
+
}
|
|
2178
|
+
if (canonicalSyncError && !isDevEnv()) {
|
|
2179
|
+
return /* @__PURE__ */ jsx6(
|
|
2180
|
+
"div",
|
|
2181
|
+
{
|
|
2182
|
+
className: `fillo-form fillo-form--error ${props.className ?? ""}`,
|
|
2183
|
+
role: "alert",
|
|
2184
|
+
"data-fillo": "root",
|
|
2185
|
+
"data-state": "error",
|
|
2186
|
+
...themeProps(theme),
|
|
2187
|
+
children: strings.loadFailed
|
|
2188
|
+
}
|
|
2189
|
+
);
|
|
1834
2190
|
}
|
|
1835
2191
|
const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
|
|
1836
2192
|
if (notPublished && !isDevEnv()) {
|
|
1837
2193
|
const error = new FilloError2("This form isn't published yet.", 403);
|
|
1838
|
-
if (props.renderError) return /* @__PURE__ */
|
|
1839
|
-
return /* @__PURE__ */
|
|
2194
|
+
if (props.renderError) return /* @__PURE__ */ jsx6(Fragment2, { children: props.renderError(error) });
|
|
2195
|
+
return /* @__PURE__ */ jsx6("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx6("p", { className: "fillo-closed", children: strings.notLive }) });
|
|
1840
2196
|
}
|
|
1841
|
-
|
|
2197
|
+
const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
|
|
2198
|
+
if (!isDevEnv() && awaitingCanonical || !schema) {
|
|
1842
2199
|
return /* @__PURE__ */ jsxs5(
|
|
1843
2200
|
"div",
|
|
1844
2201
|
{
|
|
@@ -1846,15 +2203,17 @@ function FilloForm(props) {
|
|
|
1846
2203
|
"aria-busy": "true",
|
|
1847
2204
|
...themeProps(theme),
|
|
1848
2205
|
children: [
|
|
1849
|
-
/* @__PURE__ */
|
|
1850
|
-
/* @__PURE__ */
|
|
1851
|
-
/* @__PURE__ */
|
|
2206
|
+
/* @__PURE__ */ jsx6("div", { className: "fillo-skeleton" }),
|
|
2207
|
+
/* @__PURE__ */ jsx6("div", { className: "fillo-skeleton" }),
|
|
2208
|
+
/* @__PURE__ */ jsx6("div", { className: "fillo-skeleton fillo-skeleton--short" })
|
|
1852
2209
|
]
|
|
1853
2210
|
}
|
|
1854
2211
|
);
|
|
1855
2212
|
}
|
|
1856
|
-
const
|
|
1857
|
-
|
|
2213
|
+
const branding = codeForm ? syncedBranding : currentLoaded?.branding;
|
|
2214
|
+
const poweredBy = branding?.poweredBy ?? true;
|
|
2215
|
+
const challenge = props.challenge ?? (codeForm ? syncedChallenge : currentLoaded?.challenge) ?? void 0;
|
|
2216
|
+
return /* @__PURE__ */ jsx6(
|
|
1858
2217
|
ResolvedForm,
|
|
1859
2218
|
{
|
|
1860
2219
|
...props,
|
|
@@ -1863,44 +2222,128 @@ function FilloForm(props) {
|
|
|
1863
2222
|
theme,
|
|
1864
2223
|
formId: syncedFormId ?? formId,
|
|
1865
2224
|
poweredBy,
|
|
1866
|
-
brandLabel:
|
|
1867
|
-
brandHref:
|
|
1868
|
-
draftNotice: notPublished
|
|
2225
|
+
brandLabel: branding?.label,
|
|
2226
|
+
brandHref: branding?.href,
|
|
2227
|
+
draftNotice: notPublished,
|
|
2228
|
+
stagedNotice: codeChangesStaged,
|
|
2229
|
+
syncErrorNotice: syncError ?? syncNoticeError ?? void 0,
|
|
2230
|
+
challenge
|
|
1869
2231
|
}
|
|
1870
2232
|
);
|
|
1871
2233
|
}
|
|
1872
2234
|
function PoweredBy({ label, href }) {
|
|
1873
|
-
return /* @__PURE__ */
|
|
2235
|
+
return /* @__PURE__ */ jsx6("a", { className: "fillo-powered", href: badgeUrl(href), target: "_blank", rel: "noopener noreferrer", children: label ?? "Powered by Fillo" });
|
|
1874
2236
|
}
|
|
1875
2237
|
function isDevEnv() {
|
|
1876
2238
|
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
1877
2239
|
}
|
|
1878
2240
|
function DevNoClientNotice() {
|
|
1879
2241
|
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
1880
|
-
/* @__PURE__ */
|
|
2242
|
+
/* @__PURE__ */ jsx6("strong", { children: "No Fillo client connected." }),
|
|
1881
2243
|
" This form is render-only and won\u2019t save responses. Pass a ",
|
|
1882
|
-
/* @__PURE__ */
|
|
2244
|
+
/* @__PURE__ */ jsx6("code", { children: "client" }),
|
|
1883
2245
|
" to collect them in Fillo, or forward them with webhooks. (This notice only appears in development.)"
|
|
1884
2246
|
] });
|
|
1885
2247
|
}
|
|
1886
2248
|
function DevDraftNotice() {
|
|
1887
2249
|
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
1888
|
-
/* @__PURE__ */
|
|
2250
|
+
/* @__PURE__ */ jsx6("strong", { children: "Draft form." }),
|
|
1889
2251
|
" It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
|
|
1890
2252
|
] });
|
|
1891
2253
|
}
|
|
2254
|
+
function DevStagedNotice() {
|
|
2255
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
2256
|
+
/* @__PURE__ */ jsx6("strong", { children: "Code changes are staged." }),
|
|
2257
|
+
" This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
|
|
2258
|
+
] });
|
|
2259
|
+
}
|
|
2260
|
+
function DevSyncErrorNotice({ error }) {
|
|
2261
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
2262
|
+
/* @__PURE__ */ jsxs5("strong", { children: [
|
|
2263
|
+
"Form sync needs attention",
|
|
2264
|
+
error.code ? ` (${error.code})` : "",
|
|
2265
|
+
"."
|
|
2266
|
+
] }),
|
|
2267
|
+
" ",
|
|
2268
|
+
error.message,
|
|
2269
|
+
" The safe form remains available for development."
|
|
2270
|
+
] });
|
|
2271
|
+
}
|
|
1892
2272
|
function ResolvedForm(props) {
|
|
1893
2273
|
const { schema, theme } = props;
|
|
1894
2274
|
const instanceId = useId2().replace(/:/g, "");
|
|
1895
|
-
const hpRef =
|
|
2275
|
+
const hpRef = useRef7(null);
|
|
1896
2276
|
const codeForm = isCodeForm2(props.form) ? props.form : null;
|
|
1897
2277
|
const client = props.client;
|
|
2278
|
+
const challenge = props.challenge;
|
|
2279
|
+
const challengeRequired = Boolean(challenge);
|
|
2280
|
+
const challengeTokenRef = useRef7(void 0);
|
|
2281
|
+
const [challengeToken, setChallengeToken] = useState6(void 0);
|
|
2282
|
+
const [challengeError, setChallengeError] = useState6(false);
|
|
2283
|
+
const resetChallengeRef = useRef7(void 0);
|
|
2284
|
+
const handleChallengeToken = useCallback((token) => {
|
|
2285
|
+
challengeTokenRef.current = token;
|
|
2286
|
+
setChallengeToken(token);
|
|
2287
|
+
if (token) setChallengeError(false);
|
|
2288
|
+
}, []);
|
|
2289
|
+
const registerChallengeReset = useCallback((reset) => {
|
|
2290
|
+
resetChallengeRef.current = reset;
|
|
2291
|
+
}, []);
|
|
1898
2292
|
const resolveFormId = useMemo5(
|
|
1899
2293
|
() => codeForm && client?.key ? (
|
|
1900
2294
|
// bypassCache: a stale cached formId must not be trusted at submit time.
|
|
1901
|
-
() =>
|
|
2295
|
+
async () => {
|
|
2296
|
+
try {
|
|
2297
|
+
const result = await syncCodeForm2(client, codeForm, { bypassCache: true });
|
|
2298
|
+
if (result.status === "draft") {
|
|
2299
|
+
throw new FilloError2(
|
|
2300
|
+
"This form is no longer published. Submission was stopped before sending answers.",
|
|
2301
|
+
403,
|
|
2302
|
+
void 0,
|
|
2303
|
+
"form_not_published"
|
|
2304
|
+
);
|
|
2305
|
+
}
|
|
2306
|
+
const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
|
|
2307
|
+
if (!authoritative.ok) {
|
|
2308
|
+
throw new FilloError2(
|
|
2309
|
+
`The canonical form schema was invalid: ${authoritative.error}`,
|
|
2310
|
+
502,
|
|
2311
|
+
void 0,
|
|
2312
|
+
"invalid_sync_schema"
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
if (!formSchemasEqual(schema, authoritative.schema)) {
|
|
2316
|
+
throw new FilloError2(
|
|
2317
|
+
"The live form changed after this page loaded. Submission stopped before incompatible answers were sent. Reload to use the published schema.",
|
|
2318
|
+
409,
|
|
2319
|
+
void 0,
|
|
2320
|
+
"form_schema_changed"
|
|
2321
|
+
);
|
|
2322
|
+
}
|
|
2323
|
+
if (result.syncError) {
|
|
2324
|
+
const notice = new FilloError2(
|
|
2325
|
+
result.syncError.message,
|
|
2326
|
+
403,
|
|
2327
|
+
void 0,
|
|
2328
|
+
result.syncError.code
|
|
2329
|
+
);
|
|
2330
|
+
console.warn(
|
|
2331
|
+
`[fillo] form sync needs attention (${notice.code}): ${notice.message}`
|
|
2332
|
+
);
|
|
2333
|
+
props.onError?.(notice);
|
|
2334
|
+
}
|
|
2335
|
+
return result.formId;
|
|
2336
|
+
} catch (error) {
|
|
2337
|
+
const failure = isFilloError2(error) ? error : new FilloError2(error instanceof Error ? error.message : String(error), 0);
|
|
2338
|
+
console.warn(
|
|
2339
|
+
`[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
|
|
2340
|
+
);
|
|
2341
|
+
props.onError?.(failure);
|
|
2342
|
+
throw failure;
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
1902
2345
|
) : void 0,
|
|
1903
|
-
[codeForm, client]
|
|
2346
|
+
[codeForm, client, props.onError, schema]
|
|
1904
2347
|
);
|
|
1905
2348
|
const api = useFilloController({
|
|
1906
2349
|
form: schema,
|
|
@@ -1910,30 +2353,36 @@ function ResolvedForm(props) {
|
|
|
1910
2353
|
onChange: props.onChange,
|
|
1911
2354
|
onSubmitted: props.onSubmitted,
|
|
1912
2355
|
skipValidation: props.skipValidation,
|
|
1913
|
-
getHoneypot: () => hpRef.current,
|
|
2356
|
+
getHoneypot: () => hpRef.current?.value ?? "",
|
|
1914
2357
|
// Framed renderer: Fillo draws the layout (and badge), so the surface is
|
|
1915
2358
|
// "default", never the bare-engine "headless".
|
|
1916
2359
|
surface: "default",
|
|
1917
2360
|
resolveFormId,
|
|
1918
|
-
respondent: props.respondent
|
|
2361
|
+
respondent: props.respondent,
|
|
2362
|
+
challengeRequired,
|
|
2363
|
+
getChallengeToken: () => challengeTokenRef.current,
|
|
2364
|
+
onChallengeFailed: () => {
|
|
2365
|
+
handleChallengeToken(void 0);
|
|
2366
|
+
resetChallengeRef.current?.();
|
|
2367
|
+
}
|
|
1919
2368
|
});
|
|
1920
2369
|
useDraftFlush(api);
|
|
1921
2370
|
const themed = useMemo5(() => themeProps(theme), [theme]);
|
|
1922
2371
|
const settings = schema.settings;
|
|
1923
2372
|
const submitted = api.status === "submitted";
|
|
1924
2373
|
const liveSubmit = submitted && !api.restoredSubmission;
|
|
1925
|
-
|
|
2374
|
+
useEffect8(() => {
|
|
1926
2375
|
if (!liveSubmit || !settings.redirectUrl || typeof window === "undefined") return;
|
|
1927
2376
|
const url = safeHttpUrl(settings.redirectUrl);
|
|
1928
2377
|
if (url) window.location.assign(url);
|
|
1929
2378
|
}, [liveSubmit, settings.redirectUrl]);
|
|
1930
|
-
const successRef =
|
|
1931
|
-
|
|
2379
|
+
const successRef = useRef7(null);
|
|
2380
|
+
useEffect8(() => {
|
|
1932
2381
|
if (liveSubmit) successRef.current?.focus();
|
|
1933
2382
|
}, [liveSubmit]);
|
|
1934
|
-
const blocksRef =
|
|
1935
|
-
const prevPageIndex =
|
|
1936
|
-
|
|
2383
|
+
const blocksRef = useRef7(null);
|
|
2384
|
+
const prevPageIndex = useRef7(api.pageIndex);
|
|
2385
|
+
useEffect8(() => {
|
|
1937
2386
|
if (prevPageIndex.current === api.pageIndex) return;
|
|
1938
2387
|
prevPageIndex.current = api.pageIndex;
|
|
1939
2388
|
blocksRef.current?.focus();
|
|
@@ -1963,22 +2412,29 @@ function ResolvedForm(props) {
|
|
|
1963
2412
|
className: slotClass3("fillo-success", appearance, { slot: "success", status: "submitted" }),
|
|
1964
2413
|
"data-fillo": "success",
|
|
1965
2414
|
children: [
|
|
1966
|
-
/* @__PURE__ */
|
|
1967
|
-
/* @__PURE__ */
|
|
1968
|
-
/* @__PURE__ */
|
|
2415
|
+
/* @__PURE__ */ jsx6("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
|
|
2416
|
+
/* @__PURE__ */ jsx6("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
|
|
2417
|
+
/* @__PURE__ */ jsx6("p", { className: "fillo-success-message", children: api.duplicateSubmission ? strings.alreadyAnswered : settings.successMessage ?? strings.successMessage })
|
|
1969
2418
|
]
|
|
1970
2419
|
}
|
|
1971
2420
|
),
|
|
1972
|
-
props.poweredBy && /* @__PURE__ */
|
|
2421
|
+
props.poweredBy && /* @__PURE__ */ jsx6(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
1973
2422
|
]
|
|
1974
2423
|
}
|
|
1975
2424
|
);
|
|
1976
2425
|
}
|
|
1977
2426
|
const multiPage = api.pageCount > 1;
|
|
1978
2427
|
const autoSubmit = settings.submitMode === "auto";
|
|
1979
|
-
const
|
|
2428
|
+
const showChallenge = challengeRequired && api.isLastPage;
|
|
2429
|
+
const showPrimaryButton = !autoSubmit || !api.isLastPage || needsExplicitSubmit(reachableFields(api.form, api.data)) || // A challenge needs an explicit Submit even on an otherwise button-less
|
|
2430
|
+
// auto-submit form — you can't one-tap past a human check.
|
|
2431
|
+
showChallenge;
|
|
1980
2432
|
const showFooter = showPrimaryButton || multiPage && !api.isFirstPage;
|
|
1981
|
-
|
|
2433
|
+
const progressSeq = reachablePageSequence(api.form, api.data);
|
|
2434
|
+
const progressTotal = Math.max(progressSeq.length, 1);
|
|
2435
|
+
const progressPos = progressSeq.indexOf(api.page.id);
|
|
2436
|
+
const progressStep = progressPos >= 0 ? progressPos + 1 : Math.min(api.pageIndex + 1, progressTotal);
|
|
2437
|
+
return /* @__PURE__ */ jsx6(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx6(FilloStringsContext.Provider, { value: strings, children: /* @__PURE__ */ jsx6(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx6(FilloInstanceIdContext.Provider, { value: instanceId, children: /* @__PURE__ */ jsxs5(
|
|
1982
2438
|
"form",
|
|
1983
2439
|
{
|
|
1984
2440
|
className: slotClass3(`fillo-form ${props.className ?? ""}`, appearance, {
|
|
@@ -2003,8 +2459,10 @@ function ResolvedForm(props) {
|
|
|
2003
2459
|
}
|
|
2004
2460
|
},
|
|
2005
2461
|
children: [
|
|
2006
|
-
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */
|
|
2007
|
-
props.draftNotice && isDevEnv() && /* @__PURE__ */
|
|
2462
|
+
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx6(DevNoClientNotice, {}),
|
|
2463
|
+
props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx6(DevDraftNotice, {}),
|
|
2464
|
+
props.stagedNotice && isDevEnv() && /* @__PURE__ */ jsx6(DevStagedNotice, {}),
|
|
2465
|
+
props.syncErrorNotice && isDevEnv() && /* @__PURE__ */ jsx6(DevSyncErrorNotice, { error: props.syncErrorNotice }),
|
|
2008
2466
|
(api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
|
|
2009
2467
|
"div",
|
|
2010
2468
|
{
|
|
@@ -2012,33 +2470,43 @@ function ResolvedForm(props) {
|
|
|
2012
2470
|
"data-fillo": "resume",
|
|
2013
2471
|
role: "status",
|
|
2014
2472
|
children: [
|
|
2015
|
-
/* @__PURE__ */
|
|
2016
|
-
/* @__PURE__ */
|
|
2473
|
+
/* @__PURE__ */ jsx6("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
|
|
2474
|
+
/* @__PURE__ */ jsx6("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
|
|
2017
2475
|
]
|
|
2018
2476
|
}
|
|
2019
2477
|
),
|
|
2020
|
-
|
|
2478
|
+
api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx6(
|
|
2479
|
+
"div",
|
|
2480
|
+
{
|
|
2481
|
+
className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
|
|
2482
|
+
"data-fillo": "resume",
|
|
2483
|
+
role: "status",
|
|
2484
|
+
children: /* @__PURE__ */ jsx6("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
|
|
2485
|
+
}
|
|
2486
|
+
),
|
|
2487
|
+
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx6(
|
|
2021
2488
|
"div",
|
|
2022
2489
|
{
|
|
2023
2490
|
className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
|
|
2024
2491
|
"data-fillo": "progress",
|
|
2025
2492
|
role: "progressbar",
|
|
2026
|
-
"aria-
|
|
2027
|
-
"aria-
|
|
2028
|
-
"aria-
|
|
2029
|
-
|
|
2493
|
+
"aria-label": schema.title || "Form progress",
|
|
2494
|
+
"aria-valuemin": 1,
|
|
2495
|
+
"aria-valuemax": progressTotal,
|
|
2496
|
+
"aria-valuenow": progressStep,
|
|
2497
|
+
children: /* @__PURE__ */ jsx6(
|
|
2030
2498
|
"div",
|
|
2031
2499
|
{
|
|
2032
2500
|
className: slotClass3("fillo-progress-fill", appearance, { slot: "progressFill" }),
|
|
2033
2501
|
"data-fillo": "progressFill",
|
|
2034
|
-
style: { "--fillo-progress-value": `${
|
|
2502
|
+
style: { "--fillo-progress-value": `${progressStep / progressTotal * 100}%` }
|
|
2035
2503
|
}
|
|
2036
2504
|
)
|
|
2037
2505
|
}
|
|
2038
2506
|
),
|
|
2039
2507
|
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
|
|
2040
|
-
schema.title && /* @__PURE__ */
|
|
2041
|
-
schema.description && /* @__PURE__ */
|
|
2508
|
+
schema.title && /* @__PURE__ */ jsx6("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
|
|
2509
|
+
schema.description && /* @__PURE__ */ jsx6(
|
|
2042
2510
|
"p",
|
|
2043
2511
|
{
|
|
2044
2512
|
className: slotClass3("fillo-form-description", appearance, { slot: "description" }),
|
|
@@ -2047,8 +2515,8 @@ function ResolvedForm(props) {
|
|
|
2047
2515
|
}
|
|
2048
2516
|
)
|
|
2049
2517
|
] }),
|
|
2050
|
-
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */
|
|
2051
|
-
/* @__PURE__ */
|
|
2518
|
+
multiPage && api.page.title && api.pageIndex > 0 && /* @__PURE__ */ jsx6("h2", { className: slotClass3("fillo-page-title", appearance, { slot: "pageTitle" }), "data-fillo": "pageTitle", children: api.page.title }),
|
|
2519
|
+
/* @__PURE__ */ jsx6("div", { className: slotClass3("fillo-blocks", appearance, { slot: "blocks" }), "data-fillo": "blocks", ref: blocksRef, tabIndex: -1, children: api.blocks.map((block) => /* @__PURE__ */ jsx6(
|
|
2052
2520
|
BlockRenderer,
|
|
2053
2521
|
{
|
|
2054
2522
|
block,
|
|
@@ -2058,7 +2526,7 @@ function ResolvedForm(props) {
|
|
|
2058
2526
|
},
|
|
2059
2527
|
block.id
|
|
2060
2528
|
)) }),
|
|
2061
|
-
/* @__PURE__ */
|
|
2529
|
+
/* @__PURE__ */ jsx6(
|
|
2062
2530
|
"input",
|
|
2063
2531
|
{
|
|
2064
2532
|
type: "text",
|
|
@@ -2069,15 +2537,28 @@ function ResolvedForm(props) {
|
|
|
2069
2537
|
"aria-hidden": "true",
|
|
2070
2538
|
defaultValue: "",
|
|
2071
2539
|
style: HP_STYLE,
|
|
2072
|
-
ref:
|
|
2073
|
-
|
|
2074
|
-
|
|
2540
|
+
ref: (element) => {
|
|
2541
|
+
hpRef.current = element;
|
|
2542
|
+
assertHpHidden(element);
|
|
2075
2543
|
}
|
|
2076
2544
|
}
|
|
2077
2545
|
),
|
|
2078
|
-
|
|
2546
|
+
challenge && showChallenge && /* @__PURE__ */ jsxs5("div", { className: "fillo-turnstile-slot", "data-fillo": "turnstile-slot", children: [
|
|
2547
|
+
/* @__PURE__ */ jsx6(
|
|
2548
|
+
TurnstileWidget,
|
|
2549
|
+
{
|
|
2550
|
+
siteKey: challenge.siteKey,
|
|
2551
|
+
className: slotClass3("fillo-turnstile", appearance, { slot: "turnstile" }),
|
|
2552
|
+
onToken: handleChallengeToken,
|
|
2553
|
+
onError: () => setChallengeError(true),
|
|
2554
|
+
registerReset: registerChallengeReset
|
|
2555
|
+
}
|
|
2556
|
+
),
|
|
2557
|
+
challengeError && /* @__PURE__ */ jsx6("p", { className: "fillo-turnstile-error", role: "alert", children: strings.challengeUnavailable })
|
|
2558
|
+
] }),
|
|
2559
|
+
(api.status === "error" || api.submitError) && /* @__PURE__ */ jsx6("p", { className: "fillo-submit-error", role: "alert", children: api.submitError ?? strings.submitFailed }),
|
|
2079
2560
|
showFooter && /* @__PURE__ */ jsxs5("footer", { className: slotClass3("fillo-footer", appearance, { slot: "footer" }), "data-fillo": "footer", children: [
|
|
2080
|
-
multiPage && !api.isFirstPage && /* @__PURE__ */
|
|
2561
|
+
multiPage && !api.isFirstPage && /* @__PURE__ */ jsx6(
|
|
2081
2562
|
"button",
|
|
2082
2563
|
{
|
|
2083
2564
|
type: "button",
|
|
@@ -2087,38 +2568,41 @@ function ResolvedForm(props) {
|
|
|
2087
2568
|
children: strings.back
|
|
2088
2569
|
}
|
|
2089
2570
|
),
|
|
2090
|
-
showPrimaryButton && /* @__PURE__ */
|
|
2571
|
+
showPrimaryButton && /* @__PURE__ */ jsx6(
|
|
2091
2572
|
"button",
|
|
2092
2573
|
{
|
|
2093
2574
|
type: "submit",
|
|
2094
2575
|
className: slotClass3("fillo-button fillo-button--primary", appearance, { slot: "button", variant: "primary" }),
|
|
2095
2576
|
"data-fillo": "button",
|
|
2096
|
-
disabled: api.status === "submitting" || api.uploading
|
|
2577
|
+
disabled: api.status === "submitting" || api.uploading || // Wait for the human check before enabling submit (never
|
|
2578
|
+
// "Next"). The server rejects a tokenless submit regardless —
|
|
2579
|
+
// this is UX only.
|
|
2580
|
+
showChallenge && !challengeToken,
|
|
2097
2581
|
children: api.uploading ? strings.uploading : api.status === "submitting" ? strings.submitting : api.isLastPage ? settings.submitLabel ?? strings.submit : strings.next
|
|
2098
2582
|
}
|
|
2099
2583
|
)
|
|
2100
2584
|
] }),
|
|
2101
|
-
props.poweredBy && /* @__PURE__ */
|
|
2585
|
+
props.poweredBy && /* @__PURE__ */ jsx6(PoweredBy, { label: props.brandLabel, href: props.brandHref })
|
|
2102
2586
|
]
|
|
2103
2587
|
}
|
|
2104
2588
|
) }) }) }) });
|
|
2105
2589
|
}
|
|
2106
2590
|
|
|
2107
2591
|
// src/jsx.tsx
|
|
2108
|
-
import { useRef as
|
|
2592
|
+
import { useRef as useRef8 } from "react";
|
|
2109
2593
|
import {
|
|
2110
2594
|
codeFormFromJsx,
|
|
2111
2595
|
FilloJsxError,
|
|
2112
2596
|
JSX_BLOCK_COMPONENTS
|
|
2113
2597
|
} from "@usefillo/core";
|
|
2114
|
-
import { jsx as
|
|
2598
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
2115
2599
|
var B = JSX_BLOCK_COMPONENTS;
|
|
2116
2600
|
var isDevEnv2 = () => typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
2117
2601
|
function JsxForm(props) {
|
|
2118
2602
|
const { id, title, description, settings, theme, client, children, ...rest } = props;
|
|
2119
2603
|
const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
|
|
2120
2604
|
const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
|
|
2121
|
-
const memo2 =
|
|
2605
|
+
const memo2 = useRef8(null);
|
|
2122
2606
|
if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
|
|
2123
2607
|
console.warn(
|
|
2124
2608
|
`[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.`
|
|
@@ -2127,7 +2611,7 @@ function JsxForm(props) {
|
|
|
2127
2611
|
if (!memo2.current || memo2.current.json !== json) {
|
|
2128
2612
|
memo2.current = { json, form: compiled };
|
|
2129
2613
|
}
|
|
2130
|
-
return /* @__PURE__ */
|
|
2614
|
+
return /* @__PURE__ */ jsx7(FilloForm, { form: memo2.current.form, client, ...rest });
|
|
2131
2615
|
}
|
|
2132
2616
|
function defineFormFromJsx(element) {
|
|
2133
2617
|
if (!element || element.type !== JsxForm) {
|
|
@@ -2172,30 +2656,121 @@ var Fillo = {
|
|
|
2172
2656
|
|
|
2173
2657
|
// src/provider.tsx
|
|
2174
2658
|
import { useId as useId3, useMemo as useMemo6 } from "react";
|
|
2175
|
-
import {
|
|
2176
|
-
|
|
2177
|
-
|
|
2659
|
+
import {
|
|
2660
|
+
FilloError as FilloError3,
|
|
2661
|
+
formSchemasEqual as formSchemasEqual2,
|
|
2662
|
+
isFilloError as isFilloError3,
|
|
2663
|
+
normalizeFormSchema as normalizeFormSchema2,
|
|
2664
|
+
resolveStrings as resolveStrings2,
|
|
2665
|
+
syncCodeForm as syncCodeForm3
|
|
2666
|
+
} from "@usefillo/core";
|
|
2667
|
+
import { Fragment as Fragment3, jsx as jsx8 } from "react/jsx-runtime";
|
|
2668
|
+
function FilloProvider({
|
|
2669
|
+
children,
|
|
2670
|
+
form,
|
|
2671
|
+
formId,
|
|
2672
|
+
client,
|
|
2673
|
+
appearance,
|
|
2674
|
+
strings,
|
|
2675
|
+
onError,
|
|
2676
|
+
renderError,
|
|
2677
|
+
...options
|
|
2678
|
+
}) {
|
|
2178
2679
|
const instanceId = useId3().replace(/:/g, "");
|
|
2179
2680
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2681
|
+
const {
|
|
2682
|
+
formId: syncedFormId,
|
|
2683
|
+
status: syncedStatus,
|
|
2684
|
+
error: syncError,
|
|
2685
|
+
syncing: codeFormSyncing,
|
|
2686
|
+
resolvedSchema
|
|
2687
|
+
} = useCodeFormSync(form, client, onError, {
|
|
2688
|
+
allowUnsynced: Boolean(!client || formId || options.skipValidation)
|
|
2689
|
+
});
|
|
2690
|
+
const rawSchema = codeForm ? resolvedSchema && !isDevEnv3() ? resolvedSchema : codeForm.schema : form;
|
|
2691
|
+
const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema]);
|
|
2183
2692
|
if (!normalized.ok) {
|
|
2184
2693
|
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
2185
2694
|
}
|
|
2186
2695
|
const schema = normalized.schema;
|
|
2187
|
-
const
|
|
2696
|
+
const resolveFormId = useMemo6(
|
|
2697
|
+
() => codeForm && client?.key ? async () => {
|
|
2698
|
+
try {
|
|
2699
|
+
const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
|
|
2700
|
+
if (result.status === "draft") {
|
|
2701
|
+
throw new FilloError3(
|
|
2702
|
+
"This form is no longer published. Submission was stopped before sending answers.",
|
|
2703
|
+
403,
|
|
2704
|
+
void 0,
|
|
2705
|
+
"form_not_published"
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
const authoritative = normalizeFormSchema2(result.resolvedSchema ?? codeForm.schema);
|
|
2709
|
+
if (!authoritative.ok) {
|
|
2710
|
+
throw new FilloError3(
|
|
2711
|
+
`The canonical form schema was invalid: ${authoritative.error}`,
|
|
2712
|
+
502,
|
|
2713
|
+
void 0,
|
|
2714
|
+
"invalid_sync_schema"
|
|
2715
|
+
);
|
|
2716
|
+
}
|
|
2717
|
+
if (!formSchemasEqual2(schema, authoritative.schema)) {
|
|
2718
|
+
throw new FilloError3(
|
|
2719
|
+
"The live form changed after this page loaded. Submission stopped before incompatible answers were sent. Reload to use the published schema.",
|
|
2720
|
+
409,
|
|
2721
|
+
void 0,
|
|
2722
|
+
"form_schema_changed"
|
|
2723
|
+
);
|
|
2724
|
+
}
|
|
2725
|
+
if (result.syncError) {
|
|
2726
|
+
const notice = new FilloError3(
|
|
2727
|
+
result.syncError.message,
|
|
2728
|
+
403,
|
|
2729
|
+
void 0,
|
|
2730
|
+
result.syncError.code
|
|
2731
|
+
);
|
|
2732
|
+
console.warn(
|
|
2733
|
+
`[fillo] form sync needs attention (${notice.code}): ${notice.message}`
|
|
2734
|
+
);
|
|
2735
|
+
onError?.(notice);
|
|
2736
|
+
}
|
|
2737
|
+
return result.formId;
|
|
2738
|
+
} catch (error) {
|
|
2739
|
+
const failure = isFilloError3(error) ? error : new FilloError3(error instanceof Error ? error.message : String(error), 0);
|
|
2740
|
+
console.warn(
|
|
2741
|
+
`[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
|
|
2742
|
+
);
|
|
2743
|
+
onError?.(failure);
|
|
2744
|
+
throw failure;
|
|
2745
|
+
}
|
|
2746
|
+
} : void 0,
|
|
2747
|
+
[client, codeForm, onError, schema]
|
|
2748
|
+
);
|
|
2188
2749
|
const api = useFilloController({
|
|
2189
2750
|
...options,
|
|
2190
2751
|
client,
|
|
2191
2752
|
form: schema,
|
|
2192
2753
|
formId: syncedFormId ?? formId,
|
|
2754
|
+
resolveFormId,
|
|
2193
2755
|
// Headless = your markup, no Fillo-rendered layout. Free like every embed
|
|
2194
2756
|
// method; recorded per response so usage stays measurable.
|
|
2195
2757
|
surface: "headless"
|
|
2196
2758
|
});
|
|
2197
2759
|
useDraftFlush(api);
|
|
2198
|
-
|
|
2760
|
+
const resolvedStrings = resolveStrings2(strings);
|
|
2761
|
+
const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
|
|
2762
|
+
if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx8(Fragment3, { children: renderError(canonicalSyncError) });
|
|
2763
|
+
if (canonicalSyncError && !isDevEnv3()) return null;
|
|
2764
|
+
if (codeFormSyncing && !options.skipValidation && !isDevEnv3()) return null;
|
|
2765
|
+
const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !options.skipValidation;
|
|
2766
|
+
if (notPublished && !isDevEnv3()) {
|
|
2767
|
+
const error = new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published");
|
|
2768
|
+
return renderError ? /* @__PURE__ */ jsx8(Fragment3, { children: renderError(error) }) : null;
|
|
2769
|
+
}
|
|
2770
|
+
return /* @__PURE__ */ jsx8(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx8(FilloStringsContext.Provider, { value: resolvedStrings, children: /* @__PURE__ */ jsx8(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx8(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
|
|
2771
|
+
}
|
|
2772
|
+
function isDevEnv3() {
|
|
2773
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
2199
2774
|
}
|
|
2200
2775
|
|
|
2201
2776
|
// src/index.ts
|