@usefillo/react 0.7.0 → 0.8.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 +36 -14
- package/dist/index.js +630 -229
- package/dist/styles.css +4 -0
- package/dist/styles.unlayered.css +4 -0
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
// src/FilloForm.tsx
|
|
4
|
-
import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as
|
|
4
|
+
import { useEffect as useEffect7, useId as useId2, useMemo as useMemo5, useRef as useRef6, 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,
|
|
@@ -15,6 +17,7 @@ import { resolveStrings, slotClass as slotClass3 } from "@usefillo/core";
|
|
|
15
17
|
// src/appearance.tsx
|
|
16
18
|
import { createContext, useContext } from "react";
|
|
17
19
|
import {
|
|
20
|
+
DEFAULT_FIELD_STRINGS,
|
|
18
21
|
DEFAULT_STRINGS,
|
|
19
22
|
slotClass
|
|
20
23
|
} from "@usefillo/core";
|
|
@@ -49,7 +52,10 @@ function useFieldSlots(field, error) {
|
|
|
49
52
|
control: (base) => slotClass(base, appearance, { slot: "control", ...state })
|
|
50
53
|
};
|
|
51
54
|
}
|
|
52
|
-
var FilloStringsContext = createContext(
|
|
55
|
+
var FilloStringsContext = createContext({
|
|
56
|
+
...DEFAULT_STRINGS,
|
|
57
|
+
...DEFAULT_FIELD_STRINGS
|
|
58
|
+
});
|
|
53
59
|
function useStrings() {
|
|
54
60
|
return useContext(FilloStringsContext);
|
|
55
61
|
}
|
|
@@ -175,9 +181,10 @@ import {
|
|
|
175
181
|
contentHash,
|
|
176
182
|
FilloError,
|
|
177
183
|
isCodeForm,
|
|
184
|
+
isFilloError,
|
|
178
185
|
syncCodeForm
|
|
179
186
|
} from "@usefillo/core";
|
|
180
|
-
import { useEffect as useEffect2, useState } from "react";
|
|
187
|
+
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
181
188
|
import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
|
|
182
189
|
var noticedSync = /* @__PURE__ */ new Set();
|
|
183
190
|
function noticeSync(handle, kind, message) {
|
|
@@ -186,39 +193,156 @@ function noticeSync(handle, kind, message) {
|
|
|
186
193
|
noticedSync.add(key);
|
|
187
194
|
console.info(message);
|
|
188
195
|
}
|
|
189
|
-
function useCodeFormSync(form, client, onError) {
|
|
196
|
+
function useCodeFormSync(form, client, onError, options) {
|
|
190
197
|
const codeForm = isCodeForm(form) ? form : null;
|
|
191
|
-
const [synced, setSynced] = useState({
|
|
198
|
+
const [synced, setSynced] = useState({
|
|
199
|
+
key: null,
|
|
200
|
+
client: void 0,
|
|
201
|
+
settled: false,
|
|
202
|
+
formId: null,
|
|
203
|
+
status: null,
|
|
204
|
+
error: null,
|
|
205
|
+
noticeError: null,
|
|
206
|
+
staged: false
|
|
207
|
+
});
|
|
192
208
|
const hasKey = Boolean(client?.key);
|
|
209
|
+
const allowUnsynced = Boolean(options?.allowUnsynced);
|
|
193
210
|
const handle = codeForm?.id;
|
|
194
211
|
const contentKey = codeForm ? contentHash(JSON.stringify(codeForm.schema) + JSON.stringify(codeForm.theme ?? null)) : null;
|
|
212
|
+
const syncKey = codeForm ? client?.key ? `${client.baseUrl}|${client.key}|${codeForm.id}|${contentKey}` : allowUnsynced ? null : `${client?.baseUrl ?? ""}|<missing-key>|${codeForm.id}|${contentKey}` : null;
|
|
213
|
+
const onErrorRef = useRef2(onError);
|
|
214
|
+
onErrorRef.current = onError;
|
|
195
215
|
useEffect2(() => {
|
|
196
|
-
if (!codeForm || !
|
|
216
|
+
if (!codeForm || !syncKey) return;
|
|
197
217
|
let cancelled = false;
|
|
218
|
+
setSynced(
|
|
219
|
+
(previous) => previous.key === syncKey && previous.client === client ? {
|
|
220
|
+
...previous,
|
|
221
|
+
settled: false,
|
|
222
|
+
formId: null,
|
|
223
|
+
status: null,
|
|
224
|
+
error: null,
|
|
225
|
+
noticeError: null,
|
|
226
|
+
staged: false,
|
|
227
|
+
resolvedSchema: void 0,
|
|
228
|
+
resolvedTheme: void 0
|
|
229
|
+
} : {
|
|
230
|
+
key: syncKey,
|
|
231
|
+
client,
|
|
232
|
+
settled: false,
|
|
233
|
+
formId: null,
|
|
234
|
+
status: null,
|
|
235
|
+
error: null,
|
|
236
|
+
noticeError: null,
|
|
237
|
+
staged: false
|
|
238
|
+
}
|
|
239
|
+
);
|
|
240
|
+
if (!client?.key) {
|
|
241
|
+
const error = new FilloError(
|
|
242
|
+
'Code forms need createClient({ key: "pk_\u2026" }) to resolve the live form. Pass a publishable key or a published formId.',
|
|
243
|
+
401,
|
|
244
|
+
void 0,
|
|
245
|
+
"sync_key_required"
|
|
246
|
+
);
|
|
247
|
+
console.warn(`[fillo] form sync failed (${error.code}): ${error.message}`);
|
|
248
|
+
setSynced({
|
|
249
|
+
key: syncKey,
|
|
250
|
+
client,
|
|
251
|
+
settled: true,
|
|
252
|
+
formId: null,
|
|
253
|
+
status: null,
|
|
254
|
+
error,
|
|
255
|
+
noticeError: null,
|
|
256
|
+
staged: false
|
|
257
|
+
});
|
|
258
|
+
onErrorRef.current?.(error);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
198
261
|
syncCodeForm(client, codeForm).then((r) => {
|
|
199
262
|
if (cancelled) return;
|
|
200
|
-
|
|
263
|
+
if ((r.staged || r.syncError) && !r.resolvedSchema) {
|
|
264
|
+
throw new FilloError(
|
|
265
|
+
"Sync did not return the live schema needed to render safely. Publish changes before deploy, and update @usefillo/* or your API.",
|
|
266
|
+
409,
|
|
267
|
+
void 0,
|
|
268
|
+
"sync_snapshot_required"
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const noticeError = r.syncError ? new FilloError(r.syncError.message, 403, void 0, r.syncError.code) : null;
|
|
272
|
+
setSynced({
|
|
273
|
+
key: syncKey,
|
|
274
|
+
client,
|
|
275
|
+
settled: true,
|
|
276
|
+
formId: r.formId,
|
|
277
|
+
status: r.status ?? null,
|
|
278
|
+
branding: r.branding,
|
|
279
|
+
error: null,
|
|
280
|
+
noticeError,
|
|
281
|
+
staged: Boolean(r.staged),
|
|
282
|
+
resolvedSchema: r.resolvedSchema,
|
|
283
|
+
resolvedTheme: r.resolvedTheme
|
|
284
|
+
});
|
|
201
285
|
const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
|
|
202
286
|
if (r.warning) console.warn(`[fillo] ${r.warning}`);
|
|
287
|
+
if (noticeError) {
|
|
288
|
+
console.warn(
|
|
289
|
+
`[fillo] form sync needs attention (${noticeError.code}): ${noticeError.message}`
|
|
290
|
+
);
|
|
291
|
+
onErrorRef.current?.(noticeError);
|
|
292
|
+
}
|
|
203
293
|
if (r.staged) {
|
|
204
|
-
noticeSync(
|
|
294
|
+
noticeSync(
|
|
295
|
+
codeForm.id,
|
|
296
|
+
"staged",
|
|
297
|
+
`[fillo] "${codeForm.id}": changes staged as a draft \u2014 the live version remains rendered until you publish them at ${dashboardUrl}`
|
|
298
|
+
);
|
|
205
299
|
} else if (r.status === "draft") {
|
|
206
300
|
noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
|
|
207
301
|
}
|
|
208
302
|
}).catch((err) => {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
303
|
+
if (cancelled) return;
|
|
304
|
+
const error = isFilloError(err) ? err : new FilloError(String(err), 0);
|
|
305
|
+
console.warn(
|
|
306
|
+
`[fillo] form sync failed${error.code ? ` (${error.code})` : ""}: ${error.message}`
|
|
307
|
+
);
|
|
308
|
+
setSynced({
|
|
309
|
+
key: syncKey,
|
|
310
|
+
client,
|
|
311
|
+
settled: true,
|
|
312
|
+
formId: null,
|
|
313
|
+
status: null,
|
|
314
|
+
error,
|
|
315
|
+
noticeError: null,
|
|
316
|
+
staged: false
|
|
317
|
+
});
|
|
318
|
+
onErrorRef.current?.(error);
|
|
212
319
|
});
|
|
213
320
|
return () => {
|
|
214
321
|
cancelled = true;
|
|
215
322
|
};
|
|
216
|
-
}, [hasKey, handle, contentKey, client,
|
|
217
|
-
return synced
|
|
323
|
+
}, [hasKey, handle, contentKey, client, syncKey, allowUnsynced]);
|
|
324
|
+
return synced.key === syncKey && synced.client === client ? {
|
|
325
|
+
formId: synced.formId,
|
|
326
|
+
status: synced.status,
|
|
327
|
+
branding: synced.branding,
|
|
328
|
+
error: synced.error,
|
|
329
|
+
noticeError: synced.noticeError,
|
|
330
|
+
syncing: Boolean(syncKey) && !synced.settled,
|
|
331
|
+
staged: synced.staged,
|
|
332
|
+
resolvedSchema: synced.resolvedSchema,
|
|
333
|
+
resolvedTheme: synced.resolvedTheme
|
|
334
|
+
} : {
|
|
335
|
+
formId: null,
|
|
336
|
+
status: null,
|
|
337
|
+
error: null,
|
|
338
|
+
noticeError: null,
|
|
339
|
+
syncing: Boolean(syncKey),
|
|
340
|
+
staged: false
|
|
341
|
+
};
|
|
218
342
|
}
|
|
219
343
|
|
|
220
344
|
// src/fields.tsx
|
|
221
|
-
import { isField, pipeBlock, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
|
|
345
|
+
import { isField, pipeBlock, REQUIRED_FIELD_MESSAGE, shouldAutoSubmit, slotClass as slotClass2, visibleFields } from "@usefillo/core";
|
|
222
346
|
import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
223
347
|
|
|
224
348
|
// src/phone.tsx
|
|
@@ -230,9 +354,10 @@ import {
|
|
|
230
354
|
flagEmoji,
|
|
231
355
|
formatNational,
|
|
232
356
|
parsePhone,
|
|
357
|
+
positionPhonePopover,
|
|
233
358
|
toE164
|
|
234
359
|
} from "@usefillo/core";
|
|
235
|
-
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as
|
|
360
|
+
import { useEffect as useEffect3, useLayoutEffect, useMemo as useMemo3, useRef as useRef3, useState as useState2 } from "react";
|
|
236
361
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
237
362
|
function localeCountry() {
|
|
238
363
|
if (typeof navigator === "undefined") return void 0;
|
|
@@ -253,31 +378,6 @@ function nationalFor(stored, country) {
|
|
|
253
378
|
const digits = digitsOnly(stored);
|
|
254
379
|
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
|
255
380
|
}
|
|
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
381
|
function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
282
382
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
283
383
|
const slots = useFieldSlots(field, error);
|
|
@@ -287,7 +387,12 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
287
387
|
const [country, setCountry] = useState2(
|
|
288
388
|
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? PHONE_COUNTRIES[0]
|
|
289
389
|
);
|
|
290
|
-
const touched =
|
|
390
|
+
const touched = useRef3(false);
|
|
391
|
+
useEffect3(() => {
|
|
392
|
+
if (touched.current || !stored) return;
|
|
393
|
+
const restoredCountry = parsePhone(stored).country;
|
|
394
|
+
if (restoredCountry) setCountry(restoredCountry);
|
|
395
|
+
}, [stored]);
|
|
291
396
|
useEffect3(() => {
|
|
292
397
|
if (touched.current) return;
|
|
293
398
|
if (parsePhone(stored).country || countryByIso(schema.defaultCountry)) return;
|
|
@@ -296,8 +401,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
296
401
|
}, []);
|
|
297
402
|
const national = nationalFor(stored, country);
|
|
298
403
|
const display = formatNational(country, national);
|
|
299
|
-
const inputRef =
|
|
300
|
-
const caretDigits =
|
|
404
|
+
const inputRef = useRef3(null);
|
|
405
|
+
const caretDigits = useRef3(null);
|
|
301
406
|
useLayoutEffect(() => {
|
|
302
407
|
const el = inputRef.current;
|
|
303
408
|
if (!el || caretDigits.current == null) return;
|
|
@@ -375,10 +480,10 @@ function CountrySelect({
|
|
|
375
480
|
const [query, setQuery] = useState2("");
|
|
376
481
|
const [active, setActive] = useState2(0);
|
|
377
482
|
const [placement, setPlacement] = useState2("below");
|
|
378
|
-
const rootRef =
|
|
379
|
-
const triggerRef =
|
|
380
|
-
const popoverRef =
|
|
381
|
-
const searchRef =
|
|
483
|
+
const rootRef = useRef3(null);
|
|
484
|
+
const triggerRef = useRef3(null);
|
|
485
|
+
const popoverRef = useRef3(null);
|
|
486
|
+
const searchRef = useRef3(null);
|
|
382
487
|
const listId = `${labelledBy}-countries`;
|
|
383
488
|
const matches = useMemo3(() => {
|
|
384
489
|
const q = query.trim().toLowerCase();
|
|
@@ -448,6 +553,7 @@ function CountrySelect({
|
|
|
448
553
|
className: "fillo-phone-flag",
|
|
449
554
|
"aria-haspopup": "listbox",
|
|
450
555
|
"aria-expanded": open,
|
|
556
|
+
"aria-controls": open ? listId : void 0,
|
|
451
557
|
"aria-label": `Country: ${country.name} (+${country.dialCode})`,
|
|
452
558
|
onClick: () => setOpen((o) => !o),
|
|
453
559
|
children: [
|
|
@@ -471,6 +577,7 @@ function CountrySelect({
|
|
|
471
577
|
"aria-expanded": "true",
|
|
472
578
|
"aria-controls": listId,
|
|
473
579
|
"aria-autocomplete": "list",
|
|
580
|
+
"aria-label": "Search country or code",
|
|
474
581
|
"aria-activedescendant": matches[active] ? `${listId}-opt-${active}` : void 0,
|
|
475
582
|
placeholder: "Search country or code",
|
|
476
583
|
value: query,
|
|
@@ -512,15 +619,15 @@ function CountrySelect({
|
|
|
512
619
|
}
|
|
513
620
|
|
|
514
621
|
// src/signature.tsx
|
|
515
|
-
import { useEffect as useEffect4, useRef as
|
|
622
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
|
|
516
623
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
517
624
|
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
518
625
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
519
626
|
const slots = useFieldSlots(field, error);
|
|
520
627
|
const strings = useStrings();
|
|
521
628
|
const typedInputId = `${ids.inputId}-typed`;
|
|
522
|
-
const canvasRef =
|
|
523
|
-
const drawing =
|
|
629
|
+
const canvasRef = useRef4(null);
|
|
630
|
+
const drawing = useRef4(false);
|
|
524
631
|
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
525
632
|
const [typedName, setTypedName] = useState3("");
|
|
526
633
|
useEffect4(() => {
|
|
@@ -537,12 +644,31 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
537
644
|
ctx.lineCap = "round";
|
|
538
645
|
ctx.lineJoin = "round";
|
|
539
646
|
ctx.strokeStyle = getComputedStyle(canvas).color;
|
|
647
|
+
}, []);
|
|
648
|
+
useEffect4(() => {
|
|
649
|
+
const canvas = canvasRef.current;
|
|
650
|
+
const ctx = canvas?.getContext("2d");
|
|
651
|
+
if (!canvas || !ctx) return;
|
|
652
|
+
let cancelled = false;
|
|
653
|
+
const rect = canvas.getBoundingClientRect();
|
|
540
654
|
if (typeof value === "string" && value.startsWith("data:image/")) {
|
|
655
|
+
setHasInk(true);
|
|
541
656
|
const img = new Image();
|
|
542
|
-
img.onload = () =>
|
|
657
|
+
img.onload = () => {
|
|
658
|
+
if (cancelled) return;
|
|
659
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
660
|
+
ctx.drawImage(img, 0, 0, rect.width, rect.height);
|
|
661
|
+
};
|
|
543
662
|
img.src = value;
|
|
663
|
+
} else {
|
|
664
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
665
|
+
setHasInk(false);
|
|
666
|
+
setTypedName("");
|
|
544
667
|
}
|
|
545
|
-
|
|
668
|
+
return () => {
|
|
669
|
+
cancelled = true;
|
|
670
|
+
};
|
|
671
|
+
}, [value]);
|
|
546
672
|
function point(e) {
|
|
547
673
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
548
674
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
@@ -622,6 +748,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
622
748
|
onPointerDown: start,
|
|
623
749
|
onPointerMove: move,
|
|
624
750
|
onPointerUp: end,
|
|
751
|
+
onPointerCancel: end,
|
|
625
752
|
onPointerLeave: end
|
|
626
753
|
}
|
|
627
754
|
),
|
|
@@ -657,21 +784,15 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
657
784
|
}
|
|
658
785
|
|
|
659
786
|
// src/upload.tsx
|
|
660
|
-
import { useEffect as useEffect5, useRef as
|
|
787
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
|
|
661
788
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
662
|
-
function uploadStatus(inFlight, doneCount) {
|
|
789
|
+
function uploadStatus(inFlight, doneCount, strings) {
|
|
663
790
|
const failed = inFlight.filter((f) => f.error).length;
|
|
664
791
|
const active = inFlight.length - failed;
|
|
665
792
|
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
|
-
}
|
|
793
|
+
if (active > 0) parts.push(strings.filesUploading(active));
|
|
794
|
+
if (failed > 0) parts.push(strings.uploadsFailed(failed));
|
|
795
|
+
if (active === 0 && doneCount > 0) parts.push(strings.filesUploaded(doneCount));
|
|
675
796
|
return parts.join(" ");
|
|
676
797
|
}
|
|
677
798
|
function formatBytes(bytes) {
|
|
@@ -685,41 +806,49 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
685
806
|
const slots = useFieldSlots(field, error);
|
|
686
807
|
const strings = useStrings();
|
|
687
808
|
const schema = field;
|
|
688
|
-
const inputRef =
|
|
809
|
+
const inputRef = useRef5(null);
|
|
689
810
|
const [inFlight, setInFlight] = useState4([]);
|
|
690
811
|
const [dragOver, setDragOver] = useState4(false);
|
|
691
|
-
const mountedRef =
|
|
692
|
-
const controllers =
|
|
693
|
-
const inFlightCount =
|
|
694
|
-
|
|
695
|
-
|
|
812
|
+
const mountedRef = useRef5(true);
|
|
813
|
+
const controllers = useRef5(/* @__PURE__ */ new Map());
|
|
814
|
+
const inFlightCount = useRef5(0);
|
|
815
|
+
const setUploading = api.setUploading;
|
|
816
|
+
const fieldId = field.id;
|
|
817
|
+
useEffect5(() => {
|
|
818
|
+
mountedRef.current = true;
|
|
819
|
+
return () => {
|
|
696
820
|
mountedRef.current = false;
|
|
697
821
|
controllers.current.forEach((c) => c.abort());
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
);
|
|
822
|
+
if (inFlightCount.current > 0) setUploading(fieldId, false);
|
|
823
|
+
};
|
|
824
|
+
}, [fieldId, setUploading]);
|
|
701
825
|
const files = Array.isArray(value) ? value : [];
|
|
826
|
+
const filesRef = useRef5(files);
|
|
827
|
+
filesRef.current = files;
|
|
702
828
|
const maxFiles = schema.maxFiles ?? 1;
|
|
703
|
-
const
|
|
829
|
+
const maxMb = schema.maxFileSizeMb ?? 500;
|
|
830
|
+
const maxBytes = maxMb * 1024 * 1024;
|
|
704
831
|
const canUpload = Boolean(api.client && api.formId);
|
|
705
|
-
const
|
|
832
|
+
const activeInFlight = inFlight.filter((f) => !f.error).length;
|
|
833
|
+
const remaining = maxFiles - files.length - activeInFlight;
|
|
706
834
|
async function startUpload(file) {
|
|
835
|
+
if (maxFiles - filesRef.current.length - inFlightCount.current <= 0) return;
|
|
707
836
|
const key = `${file.name}-${file.size}-${Math.floor(performance.now())}`;
|
|
708
837
|
if (file.size > maxBytes) {
|
|
709
838
|
setInFlight((prev) => [
|
|
710
839
|
...prev,
|
|
711
|
-
{ key, name: file.name, size: file.size, fraction: 0,
|
|
840
|
+
{ key, name: file.name, size: file.size, fraction: 0, file, tooLarge: true, error: strings.fileTooLarge(maxMb) }
|
|
712
841
|
]);
|
|
713
842
|
return;
|
|
714
843
|
}
|
|
715
|
-
setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0 }]);
|
|
844
|
+
setInFlight((prev) => [...prev, { key, name: file.name, size: file.size, fraction: 0, file }]);
|
|
716
845
|
inFlightCount.current += 1;
|
|
717
|
-
|
|
846
|
+
setUploading(fieldId, true);
|
|
718
847
|
const controller = new AbortController();
|
|
719
848
|
controllers.current.set(key, controller);
|
|
720
849
|
try {
|
|
721
850
|
const uploaded = await api.client.uploadFile(api.formId, file, {
|
|
722
|
-
fieldId
|
|
851
|
+
fieldId,
|
|
723
852
|
signal: controller.signal,
|
|
724
853
|
onProgress: ({ fraction }) => {
|
|
725
854
|
if (mountedRef.current) {
|
|
@@ -729,11 +858,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
729
858
|
});
|
|
730
859
|
if (!mountedRef.current) return;
|
|
731
860
|
setInFlight((prev) => prev.filter((f) => f.key !== key));
|
|
732
|
-
const
|
|
733
|
-
|
|
861
|
+
const next = [...filesRef.current, uploaded].slice(0, maxFiles);
|
|
862
|
+
filesRef.current = next;
|
|
863
|
+
setValue(next);
|
|
734
864
|
} catch (err) {
|
|
735
865
|
if (mountedRef.current) {
|
|
736
|
-
const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message :
|
|
866
|
+
const message = err instanceof Error && err.name === "FilloError" && err.message ? err.message : strings.uploadFailed;
|
|
737
867
|
setInFlight(
|
|
738
868
|
(prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
|
|
739
869
|
);
|
|
@@ -741,7 +871,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
741
871
|
} finally {
|
|
742
872
|
controllers.current.delete(key);
|
|
743
873
|
inFlightCount.current -= 1;
|
|
744
|
-
if (mountedRef.current && inFlightCount.current === 0)
|
|
874
|
+
if (mountedRef.current && inFlightCount.current === 0) setUploading(fieldId, false);
|
|
745
875
|
}
|
|
746
876
|
}
|
|
747
877
|
function cancelUpload(key) {
|
|
@@ -750,11 +880,14 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
750
880
|
}
|
|
751
881
|
function handleFiles(list) {
|
|
752
882
|
if (!list) return;
|
|
753
|
-
|
|
883
|
+
const currentRoom = Math.max(0, maxFiles - filesRef.current.length - inFlightCount.current);
|
|
884
|
+
Array.from(list).slice(0, currentRoom).forEach((file) => void startUpload(file));
|
|
754
885
|
if (inputRef.current) inputRef.current.value = "";
|
|
755
886
|
}
|
|
756
887
|
function removeFile(fileId) {
|
|
757
|
-
|
|
888
|
+
const next = filesRef.current.filter((f) => f.fileId !== fileId);
|
|
889
|
+
filesRef.current = next;
|
|
890
|
+
setValue(next);
|
|
758
891
|
}
|
|
759
892
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
760
893
|
return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
|
|
@@ -782,12 +915,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
782
915
|
onClick: () => canUpload && inputRef.current?.click(),
|
|
783
916
|
role: "button",
|
|
784
917
|
tabIndex: 0,
|
|
918
|
+
"aria-disabled": !canUpload || void 0,
|
|
785
919
|
"aria-labelledby": ids.labelId,
|
|
786
920
|
"aria-required": field.required ? true : void 0,
|
|
787
921
|
"aria-invalid": error ? true : void 0,
|
|
788
922
|
"aria-describedby": describedBy,
|
|
789
923
|
onKeyDown: (e) => {
|
|
790
|
-
if (
|
|
924
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
925
|
+
e.preventDefault();
|
|
926
|
+
if (canUpload) inputRef.current?.click();
|
|
927
|
+
}
|
|
791
928
|
},
|
|
792
929
|
children: [
|
|
793
930
|
/* @__PURE__ */ jsx3(
|
|
@@ -797,23 +934,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
797
934
|
id: ids.inputId,
|
|
798
935
|
type: "file",
|
|
799
936
|
hidden: true,
|
|
937
|
+
disabled: !canUpload,
|
|
800
938
|
multiple: maxFiles > 1,
|
|
801
939
|
accept: schema.accept?.join(","),
|
|
802
940
|
onChange: (e) => canUpload && handleFiles(e.target.files)
|
|
803
941
|
}
|
|
804
942
|
),
|
|
805
943
|
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" })
|
|
944
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-title", children: strings.dropzoneTitle(maxFiles > 1) }),
|
|
945
|
+
/* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.dropzoneHint(maxMb) })
|
|
946
|
+
] }) : /* @__PURE__ */ jsx3("span", { className: "fillo-dropzone-hint", children: strings.uploadsDisabled })
|
|
817
947
|
]
|
|
818
948
|
}
|
|
819
949
|
),
|
|
@@ -824,7 +954,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
824
954
|
"aria-live": "polite",
|
|
825
955
|
className: "fillo-sr-only",
|
|
826
956
|
style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
|
|
827
|
-
children: uploadStatus(inFlight, files.length)
|
|
957
|
+
children: uploadStatus(inFlight, files.length, strings)
|
|
828
958
|
}
|
|
829
959
|
),
|
|
830
960
|
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
@@ -857,6 +987,21 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
857
987
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
858
988
|
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
859
989
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-meta", children: f.error }),
|
|
990
|
+
f.file && !f.tooLarge && remaining > 0 && /* @__PURE__ */ jsx3(
|
|
991
|
+
"button",
|
|
992
|
+
{
|
|
993
|
+
type: "button",
|
|
994
|
+
className: "fillo-file-retry",
|
|
995
|
+
"aria-label": `${strings.uploadRetry} ${f.name}`,
|
|
996
|
+
onClick: () => {
|
|
997
|
+
const retryFile = f.file;
|
|
998
|
+
if (!retryFile) return;
|
|
999
|
+
setInFlight((prev) => prev.filter((x) => x.key !== f.key));
|
|
1000
|
+
void startUpload(retryFile);
|
|
1001
|
+
},
|
|
1002
|
+
children: strings.uploadRetry
|
|
1003
|
+
}
|
|
1004
|
+
),
|
|
860
1005
|
/* @__PURE__ */ jsx3(
|
|
861
1006
|
"button",
|
|
862
1007
|
{
|
|
@@ -975,6 +1120,9 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
|
975
1120
|
type,
|
|
976
1121
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
977
1122
|
placeholder: field.placeholder,
|
|
1123
|
+
maxLength: "maxLength" in field ? field.maxLength : void 0,
|
|
1124
|
+
min: field.kind === "number" ? field.min : void 0,
|
|
1125
|
+
max: field.kind === "number" ? field.max : void 0,
|
|
978
1126
|
onChange: (e) => setValue(e.target.value),
|
|
979
1127
|
...fieldAria(field, error, ids)
|
|
980
1128
|
}
|
|
@@ -991,6 +1139,7 @@ function LongText({ field, value, error, setValue, ids: providedIds }) {
|
|
|
991
1139
|
rows: 4,
|
|
992
1140
|
value: typeof value === "string" ? value : "",
|
|
993
1141
|
placeholder: field.placeholder,
|
|
1142
|
+
maxLength: "maxLength" in field ? field.maxLength : void 0,
|
|
994
1143
|
onChange: (e) => setValue(e.target.value),
|
|
995
1144
|
...fieldAria(field, error, ids)
|
|
996
1145
|
}
|
|
@@ -1246,12 +1395,18 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1246
1395
|
}
|
|
1247
1396
|
) });
|
|
1248
1397
|
}
|
|
1249
|
-
|
|
1398
|
+
function otherSentinel(field) {
|
|
1399
|
+
let value = "__fillo_other__";
|
|
1400
|
+
const ids = new Set(field.options.map((option) => option.id));
|
|
1401
|
+
while (ids.has(value)) value += "_";
|
|
1402
|
+
return value;
|
|
1403
|
+
}
|
|
1250
1404
|
function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
1251
1405
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1252
1406
|
const strings = useStrings();
|
|
1253
1407
|
const choice = field;
|
|
1254
1408
|
const options = useDisplayOptions(choice);
|
|
1409
|
+
const otherValue = otherSentinel(choice);
|
|
1255
1410
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
1256
1411
|
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
1257
1412
|
const otherActive = isOtherValue || value === "" && otherOn;
|
|
@@ -1264,9 +1419,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1264
1419
|
id: ids.inputId,
|
|
1265
1420
|
...control,
|
|
1266
1421
|
...fieldAria(field, error, ids),
|
|
1267
|
-
value: otherActive ?
|
|
1422
|
+
value: otherActive ? otherValue : typeof value === "string" ? value : "",
|
|
1268
1423
|
onChange: (e) => {
|
|
1269
|
-
if (e.target.value ===
|
|
1424
|
+
if (e.target.value === otherValue) {
|
|
1270
1425
|
setOtherOn(true);
|
|
1271
1426
|
setValue("");
|
|
1272
1427
|
} else {
|
|
@@ -1277,7 +1432,7 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1277
1432
|
children: [
|
|
1278
1433
|
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
|
|
1279
1434
|
options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
|
|
1280
|
-
choice.allowOther && /* @__PURE__ */ jsx4("option", { value:
|
|
1435
|
+
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: otherValue, children: `${strings.other}\u2026` })
|
|
1281
1436
|
]
|
|
1282
1437
|
}
|
|
1283
1438
|
),
|
|
@@ -1301,17 +1456,15 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1301
1456
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1302
1457
|
const strings = useStrings();
|
|
1303
1458
|
const appearance = useFilloAppearance();
|
|
1459
|
+
const slots = useFieldSlots(field, error);
|
|
1304
1460
|
const checkbox = field;
|
|
1305
1461
|
if (checkbox.appearance === "toggle") {
|
|
1306
1462
|
return /* @__PURE__ */ jsxs4(
|
|
1307
1463
|
"div",
|
|
1308
1464
|
{
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
"data-kind": field.kind,
|
|
1313
|
-
"data-invalid": error ? "" : void 0,
|
|
1314
|
-
"data-required": field.required ? "" : void 0,
|
|
1465
|
+
...slots.wrapperProps(
|
|
1466
|
+
`fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
|
|
1467
|
+
),
|
|
1315
1468
|
children: [
|
|
1316
1469
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1317
1470
|
/* @__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 +1486,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1333
1486
|
),
|
|
1334
1487
|
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
|
|
1335
1488
|
] }),
|
|
1336
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1337
|
-
error && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1489
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
1490
|
+
error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
1338
1491
|
]
|
|
1339
1492
|
}
|
|
1340
1493
|
);
|
|
@@ -1342,12 +1495,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1342
1495
|
return /* @__PURE__ */ jsxs4(
|
|
1343
1496
|
"div",
|
|
1344
1497
|
{
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
"data-kind": field.kind,
|
|
1349
|
-
"data-invalid": error ? "" : void 0,
|
|
1350
|
-
"data-required": field.required ? "" : void 0,
|
|
1498
|
+
...slots.wrapperProps(
|
|
1499
|
+
`fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
|
|
1500
|
+
),
|
|
1351
1501
|
children: [
|
|
1352
1502
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1353
1503
|
/* @__PURE__ */ jsx4(
|
|
@@ -1366,8 +1516,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1366
1516
|
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
|
|
1367
1517
|
] })
|
|
1368
1518
|
] }),
|
|
1369
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1370
|
-
error && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1519
|
+
field.description && /* @__PURE__ */ jsx4("p", { className: slots.description, "data-fillo": "fieldDescription", id: ids.descriptionId, children: field.description }),
|
|
1520
|
+
error && /* @__PURE__ */ jsx4("p", { className: slots.error, "data-fillo": "error", id: ids.errorId, role: "alert", children: error })
|
|
1371
1521
|
]
|
|
1372
1522
|
}
|
|
1373
1523
|
);
|
|
@@ -1513,73 +1663,106 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1513
1663
|
[next[index], next[target]] = [next[target], next[index]];
|
|
1514
1664
|
setValue(next);
|
|
1515
1665
|
}
|
|
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
|
-
|
|
1666
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1667
|
+
"div",
|
|
1668
|
+
{
|
|
1669
|
+
id: ids.inputId,
|
|
1670
|
+
role: "group",
|
|
1671
|
+
"aria-labelledby": ids.labelId,
|
|
1672
|
+
...fieldAria(field, error, ids),
|
|
1673
|
+
children: /* @__PURE__ */ jsx4("ol", { className: "fillo-ranking", children: order.map((id, index) => {
|
|
1674
|
+
const option = ranking.options.find((o) => o.id === id);
|
|
1675
|
+
if (!option) return null;
|
|
1676
|
+
return /* @__PURE__ */ jsxs4("li", { className: "fillo-ranking-item", children: [
|
|
1677
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-index", children: index + 1 }),
|
|
1678
|
+
/* @__PURE__ */ jsx4("span", { className: "fillo-ranking-label", children: option.label }),
|
|
1679
|
+
/* @__PURE__ */ jsxs4("span", { className: "fillo-ranking-controls", children: [
|
|
1680
|
+
/* @__PURE__ */ jsx4(
|
|
1681
|
+
"button",
|
|
1682
|
+
{
|
|
1683
|
+
type: "button",
|
|
1684
|
+
className: "fillo-ranking-move",
|
|
1685
|
+
"aria-label": `Move ${option.label} up`,
|
|
1686
|
+
disabled: index === 0,
|
|
1687
|
+
onClick: () => move(id, -1),
|
|
1688
|
+
children: "\u2191"
|
|
1689
|
+
}
|
|
1690
|
+
),
|
|
1691
|
+
/* @__PURE__ */ jsx4(
|
|
1692
|
+
"button",
|
|
1693
|
+
{
|
|
1694
|
+
type: "button",
|
|
1695
|
+
className: "fillo-ranking-move",
|
|
1696
|
+
"aria-label": `Move ${option.label} down`,
|
|
1697
|
+
disabled: index === order.length - 1,
|
|
1698
|
+
onClick: () => move(id, 1),
|
|
1699
|
+
children: "\u2193"
|
|
1700
|
+
}
|
|
1701
|
+
)
|
|
1702
|
+
] })
|
|
1703
|
+
] }, id);
|
|
1704
|
+
}) })
|
|
1705
|
+
}
|
|
1706
|
+
) });
|
|
1548
1707
|
}
|
|
1549
1708
|
function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
1550
1709
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1551
1710
|
const matrix = field;
|
|
1552
1711
|
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
|
-
|
|
1712
|
+
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1713
|
+
"div",
|
|
1714
|
+
{
|
|
1715
|
+
id: ids.inputId,
|
|
1716
|
+
className: "fillo-matrix-wrap",
|
|
1717
|
+
role: "group",
|
|
1718
|
+
"aria-labelledby": ids.labelId,
|
|
1719
|
+
...fieldAria(field, error, ids),
|
|
1720
|
+
children: /* @__PURE__ */ jsxs4("table", { className: "fillo-matrix", children: [
|
|
1721
|
+
/* @__PURE__ */ jsx4("thead", { children: /* @__PURE__ */ jsxs4("tr", { children: [
|
|
1722
|
+
/* @__PURE__ */ jsx4("th", {}),
|
|
1723
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("th", { scope: "col", children: col.label }, col.id))
|
|
1724
|
+
] }) }),
|
|
1725
|
+
/* @__PURE__ */ jsx4("tbody", { children: matrix.rows.map((row) => {
|
|
1726
|
+
const rowLabelId = `${ids.inputId}-${row.id}`;
|
|
1727
|
+
const answered = answers[row.id] !== void 0;
|
|
1728
|
+
const rowInvalid = Boolean(error) && field.required && !answered;
|
|
1729
|
+
const radioId = (col) => `${ids.inputId}-${row.id}-${col.id}`;
|
|
1730
|
+
return (
|
|
1731
|
+
// The <tr> keeps its native row role so the grid's row/column
|
|
1732
|
+
// header association survives for screen readers. The row's
|
|
1733
|
+
// radiogroup lives on an inner element that aria-owns the radios
|
|
1734
|
+
// scattered across the cells (they can't be DOM-nested inside a
|
|
1735
|
+
// single element within a table row).
|
|
1736
|
+
/* @__PURE__ */ jsxs4("tr", { children: [
|
|
1737
|
+
/* @__PURE__ */ jsx4("th", { scope: "row", children: /* @__PURE__ */ jsx4(
|
|
1738
|
+
"div",
|
|
1739
|
+
{
|
|
1740
|
+
role: "radiogroup",
|
|
1741
|
+
"aria-labelledby": rowLabelId,
|
|
1742
|
+
"aria-owns": matrix.columns.map(radioId).join(" "),
|
|
1743
|
+
children: /* @__PURE__ */ jsx4("span", { id: rowLabelId, children: row.label })
|
|
1744
|
+
}
|
|
1745
|
+
) }),
|
|
1746
|
+
matrix.columns.map((col) => /* @__PURE__ */ jsx4("td", { children: /* @__PURE__ */ jsx4(
|
|
1747
|
+
"input",
|
|
1748
|
+
{
|
|
1749
|
+
id: radioId(col),
|
|
1750
|
+
type: "radio",
|
|
1751
|
+
className: "fillo-option-input",
|
|
1752
|
+
name: `${ids.name}-${row.id}`,
|
|
1753
|
+
"aria-label": `${row.label}: ${col.label}`,
|
|
1754
|
+
"aria-required": field.required ? true : void 0,
|
|
1755
|
+
"aria-invalid": rowInvalid ? true : void 0,
|
|
1756
|
+
checked: answers[row.id] === col.id,
|
|
1757
|
+
onChange: () => setValue({ ...answers, [row.id]: col.id })
|
|
1758
|
+
}
|
|
1759
|
+
) }, col.id))
|
|
1760
|
+
] }, row.id)
|
|
1761
|
+
);
|
|
1762
|
+
}) })
|
|
1763
|
+
] })
|
|
1764
|
+
}
|
|
1765
|
+
) });
|
|
1583
1766
|
}
|
|
1584
1767
|
function Hidden() {
|
|
1585
1768
|
return null;
|
|
@@ -1633,8 +1816,8 @@ function blockPropsEqual(prev, next) {
|
|
|
1633
1816
|
const id = next.block.id;
|
|
1634
1817
|
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1635
1818
|
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1636
|
-
if (next.block.kind === "custom") {
|
|
1637
|
-
return prev.api
|
|
1819
|
+
if (next.block.kind === "custom" || isField(next.block) && Boolean(next.components?.[next.block.kind])) {
|
|
1820
|
+
return prev.api === next.api;
|
|
1638
1821
|
}
|
|
1639
1822
|
if ((blockHasPiping(prev.block) || blockHasPiping(next.block)) && prev.api.data !== next.api.data) {
|
|
1640
1823
|
return false;
|
|
@@ -1649,6 +1832,7 @@ function blockPropsEqual(prev, next) {
|
|
|
1649
1832
|
function BlockRendererInner({ block, api, components, customComponents }) {
|
|
1650
1833
|
block = pipeBlock(block, api.data, api.form);
|
|
1651
1834
|
const ids = useFilloFieldIds(block.id);
|
|
1835
|
+
const strings = useStrings();
|
|
1652
1836
|
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
1653
1837
|
let Component;
|
|
1654
1838
|
if (block.kind === "custom") {
|
|
@@ -1665,12 +1849,14 @@ function BlockRendererInner({ block, api, components, customComponents }) {
|
|
|
1665
1849
|
} else {
|
|
1666
1850
|
Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
|
|
1667
1851
|
}
|
|
1852
|
+
const rawError = api.errors[block.id];
|
|
1853
|
+
const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
|
|
1668
1854
|
return /* @__PURE__ */ jsx4(
|
|
1669
1855
|
Component,
|
|
1670
1856
|
{
|
|
1671
1857
|
field: block,
|
|
1672
1858
|
value: api.data[block.id],
|
|
1673
|
-
error
|
|
1859
|
+
error,
|
|
1674
1860
|
setValue: (v) => {
|
|
1675
1861
|
api.setValue(block.id, v);
|
|
1676
1862
|
const ctx = {
|
|
@@ -1749,7 +1935,9 @@ function badgeUrl(href) {
|
|
|
1749
1935
|
function focusFirstInvalid(formEl) {
|
|
1750
1936
|
if (!formEl || typeof window === "undefined") return;
|
|
1751
1937
|
const run = () => {
|
|
1752
|
-
const
|
|
1938
|
+
const invalid = formEl.querySelector('[aria-invalid="true"]');
|
|
1939
|
+
const focusable = 'input:not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
1940
|
+
const target = invalid?.matches(focusable) ? invalid : invalid?.querySelector(focusable);
|
|
1753
1941
|
target?.focus?.();
|
|
1754
1942
|
};
|
|
1755
1943
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
@@ -1777,35 +1965,54 @@ function FilloForm(props) {
|
|
|
1777
1965
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
1778
1966
|
[props.client, formId]
|
|
1779
1967
|
);
|
|
1780
|
-
const [
|
|
1781
|
-
const [
|
|
1782
|
-
const [loadError, setLoadError] = useState6(null);
|
|
1783
|
-
const [closed, setClosed] = useState6(false);
|
|
1784
|
-
const [fetchedBranding, setFetchedBranding] = useState6(null);
|
|
1968
|
+
const [loaded, setLoaded] = useState6(null);
|
|
1969
|
+
const [loadFailure, setLoadFailure] = useState6(null);
|
|
1785
1970
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1786
|
-
const inlineSchema = codeForm ? codeForm.schema : form;
|
|
1787
1971
|
const onError = props.onError;
|
|
1788
|
-
const {
|
|
1789
|
-
|
|
1972
|
+
const {
|
|
1973
|
+
formId: syncedFormId,
|
|
1974
|
+
status: syncedStatus,
|
|
1975
|
+
branding: syncedBranding,
|
|
1976
|
+
error: syncError,
|
|
1977
|
+
noticeError: syncNoticeError,
|
|
1978
|
+
syncing: codeFormSyncing,
|
|
1979
|
+
staged: codeChangesStaged,
|
|
1980
|
+
resolvedSchema,
|
|
1981
|
+
resolvedTheme
|
|
1982
|
+
} = useCodeFormSync(form, client, onError, {
|
|
1983
|
+
// An absent client is intentional render-only use. Builder previews and an
|
|
1984
|
+
// explicit canonical id also do not need publishable-key resolution.
|
|
1985
|
+
allowUnsynced: Boolean(!client || props.skipValidation || formId)
|
|
1986
|
+
});
|
|
1987
|
+
const renderResolvedSnapshot = Boolean(resolvedSchema && !isDevEnv());
|
|
1988
|
+
const inlineSchema = codeForm ? renderResolvedSnapshot ? resolvedSchema : codeForm.schema : form;
|
|
1989
|
+
const onErrorRef = useRef6(onError);
|
|
1790
1990
|
useEffect7(() => {
|
|
1791
1991
|
onErrorRef.current = onError;
|
|
1792
1992
|
});
|
|
1793
1993
|
const needsFetch = !inlineSchema && Boolean(client && formId);
|
|
1794
1994
|
useEffect7(() => {
|
|
1795
|
-
if (!needsFetch || !client || !formId)
|
|
1995
|
+
if (!needsFetch || !client || !formId) {
|
|
1996
|
+
setLoaded(null);
|
|
1997
|
+
setLoadFailure(null);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1796
2000
|
let cancelled = false;
|
|
1797
|
-
|
|
1798
|
-
|
|
2001
|
+
setLoaded(null);
|
|
2002
|
+
setLoadFailure(null);
|
|
1799
2003
|
client.getForm(formId).then((published) => {
|
|
1800
2004
|
if (cancelled) return;
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2005
|
+
setLoaded({
|
|
2006
|
+
formId,
|
|
2007
|
+
schema: published.schema,
|
|
2008
|
+
theme: published.theme,
|
|
2009
|
+
closed: Boolean(published.closed),
|
|
2010
|
+
branding: published.branding ?? null
|
|
2011
|
+
});
|
|
1805
2012
|
}).catch((err) => {
|
|
1806
2013
|
if (cancelled) return;
|
|
1807
2014
|
const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
|
|
1808
|
-
|
|
2015
|
+
setLoadFailure({ formId, error });
|
|
1809
2016
|
onErrorRef.current?.(error);
|
|
1810
2017
|
});
|
|
1811
2018
|
return () => {
|
|
@@ -1813,10 +2020,13 @@ function FilloForm(props) {
|
|
|
1813
2020
|
};
|
|
1814
2021
|
}, [needsFetch, client, formId]);
|
|
1815
2022
|
const strings = resolveStrings(props.strings);
|
|
1816
|
-
const
|
|
2023
|
+
const currentLoaded = needsFetch && loaded?.formId === formId ? loaded : null;
|
|
2024
|
+
const loadError = needsFetch && loadFailure && loadFailure.formId === formId ? loadFailure.error : null;
|
|
2025
|
+
const rawSchema = inlineSchema ?? currentLoaded?.schema ?? null;
|
|
1817
2026
|
const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1818
2027
|
const schema = normalized?.ok ? normalized.schema : null;
|
|
1819
|
-
const
|
|
2028
|
+
const resolvedCodeTheme = renderResolvedSnapshot ? resolvedTheme ?? void 0 : codeForm?.theme;
|
|
2029
|
+
const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
|
|
1820
2030
|
if (normalized && !normalized.ok) {
|
|
1821
2031
|
const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1822
2032
|
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
@@ -1829,16 +2039,34 @@ function FilloForm(props) {
|
|
|
1829
2039
|
const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
|
|
1830
2040
|
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
|
|
1831
2041
|
}
|
|
1832
|
-
if (closed) {
|
|
2042
|
+
if (currentLoaded?.closed) {
|
|
1833
2043
|
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.closed }) });
|
|
1834
2044
|
}
|
|
2045
|
+
const canonicalSyncError = codeForm && syncError && !props.skipValidation ? syncError : null;
|
|
2046
|
+
if (canonicalSyncError && props.renderError) {
|
|
2047
|
+
return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(canonicalSyncError) });
|
|
2048
|
+
}
|
|
2049
|
+
if (canonicalSyncError && !isDevEnv()) {
|
|
2050
|
+
return /* @__PURE__ */ jsx5(
|
|
2051
|
+
"div",
|
|
2052
|
+
{
|
|
2053
|
+
className: `fillo-form fillo-form--error ${props.className ?? ""}`,
|
|
2054
|
+
role: "alert",
|
|
2055
|
+
"data-fillo": "root",
|
|
2056
|
+
"data-state": "error",
|
|
2057
|
+
...themeProps(theme),
|
|
2058
|
+
children: strings.loadFailed
|
|
2059
|
+
}
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
1835
2062
|
const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
|
|
1836
2063
|
if (notPublished && !isDevEnv()) {
|
|
1837
2064
|
const error = new FilloError2("This form isn't published yet.", 403);
|
|
1838
2065
|
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
1839
2066
|
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--closed ${props.className ?? ""}`, ...themeProps(theme), children: /* @__PURE__ */ jsx5("p", { className: "fillo-closed", children: strings.notLive }) });
|
|
1840
2067
|
}
|
|
1841
|
-
|
|
2068
|
+
const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
|
|
2069
|
+
if (!isDevEnv() && awaitingCanonical || !schema) {
|
|
1842
2070
|
return /* @__PURE__ */ jsxs5(
|
|
1843
2071
|
"div",
|
|
1844
2072
|
{
|
|
@@ -1853,7 +2081,8 @@ function FilloForm(props) {
|
|
|
1853
2081
|
}
|
|
1854
2082
|
);
|
|
1855
2083
|
}
|
|
1856
|
-
const
|
|
2084
|
+
const branding = codeForm ? syncedBranding : currentLoaded?.branding;
|
|
2085
|
+
const poweredBy = branding?.poweredBy ?? true;
|
|
1857
2086
|
return /* @__PURE__ */ jsx5(
|
|
1858
2087
|
ResolvedForm,
|
|
1859
2088
|
{
|
|
@@ -1863,9 +2092,11 @@ function FilloForm(props) {
|
|
|
1863
2092
|
theme,
|
|
1864
2093
|
formId: syncedFormId ?? formId,
|
|
1865
2094
|
poweredBy,
|
|
1866
|
-
brandLabel:
|
|
1867
|
-
brandHref:
|
|
1868
|
-
draftNotice: notPublished
|
|
2095
|
+
brandLabel: branding?.label,
|
|
2096
|
+
brandHref: branding?.href,
|
|
2097
|
+
draftNotice: notPublished,
|
|
2098
|
+
stagedNotice: codeChangesStaged,
|
|
2099
|
+
syncErrorNotice: syncError ?? syncNoticeError ?? void 0
|
|
1869
2100
|
}
|
|
1870
2101
|
);
|
|
1871
2102
|
}
|
|
@@ -1889,18 +2120,85 @@ function DevDraftNotice() {
|
|
|
1889
2120
|
" It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
|
|
1890
2121
|
] });
|
|
1891
2122
|
}
|
|
2123
|
+
function DevStagedNotice() {
|
|
2124
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
2125
|
+
/* @__PURE__ */ jsx5("strong", { children: "Code changes are staged." }),
|
|
2126
|
+
" This local preview is not the live form yet. Publish the changes in Fillo before deploying them for respondents."
|
|
2127
|
+
] });
|
|
2128
|
+
}
|
|
2129
|
+
function DevSyncErrorNotice({ error }) {
|
|
2130
|
+
return /* @__PURE__ */ jsxs5("div", { className: "fillo-devwarning", role: "alert", children: [
|
|
2131
|
+
/* @__PURE__ */ jsxs5("strong", { children: [
|
|
2132
|
+
"Form sync needs attention",
|
|
2133
|
+
error.code ? ` (${error.code})` : "",
|
|
2134
|
+
"."
|
|
2135
|
+
] }),
|
|
2136
|
+
" ",
|
|
2137
|
+
error.message,
|
|
2138
|
+
" The safe form remains available for development."
|
|
2139
|
+
] });
|
|
2140
|
+
}
|
|
1892
2141
|
function ResolvedForm(props) {
|
|
1893
2142
|
const { schema, theme } = props;
|
|
1894
2143
|
const instanceId = useId2().replace(/:/g, "");
|
|
1895
|
-
const hpRef =
|
|
2144
|
+
const hpRef = useRef6(null);
|
|
1896
2145
|
const codeForm = isCodeForm2(props.form) ? props.form : null;
|
|
1897
2146
|
const client = props.client;
|
|
1898
2147
|
const resolveFormId = useMemo5(
|
|
1899
2148
|
() => codeForm && client?.key ? (
|
|
1900
2149
|
// bypassCache: a stale cached formId must not be trusted at submit time.
|
|
1901
|
-
() =>
|
|
2150
|
+
async () => {
|
|
2151
|
+
try {
|
|
2152
|
+
const result = await syncCodeForm2(client, codeForm, { bypassCache: true });
|
|
2153
|
+
if (result.status === "draft") {
|
|
2154
|
+
throw new FilloError2(
|
|
2155
|
+
"This form is no longer published. Submission was stopped before sending answers.",
|
|
2156
|
+
403,
|
|
2157
|
+
void 0,
|
|
2158
|
+
"form_not_published"
|
|
2159
|
+
);
|
|
2160
|
+
}
|
|
2161
|
+
const authoritative = normalizeFormSchema(result.resolvedSchema ?? codeForm.schema);
|
|
2162
|
+
if (!authoritative.ok) {
|
|
2163
|
+
throw new FilloError2(
|
|
2164
|
+
`The canonical form schema was invalid: ${authoritative.error}`,
|
|
2165
|
+
502,
|
|
2166
|
+
void 0,
|
|
2167
|
+
"invalid_sync_schema"
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
if (!formSchemasEqual(schema, authoritative.schema)) {
|
|
2171
|
+
throw new FilloError2(
|
|
2172
|
+
"The live form changed after this page loaded. Submission stopped before incompatible answers were sent. Reload to use the published schema.",
|
|
2173
|
+
409,
|
|
2174
|
+
void 0,
|
|
2175
|
+
"form_schema_changed"
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
if (result.syncError) {
|
|
2179
|
+
const notice = new FilloError2(
|
|
2180
|
+
result.syncError.message,
|
|
2181
|
+
403,
|
|
2182
|
+
void 0,
|
|
2183
|
+
result.syncError.code
|
|
2184
|
+
);
|
|
2185
|
+
console.warn(
|
|
2186
|
+
`[fillo] form sync needs attention (${notice.code}): ${notice.message}`
|
|
2187
|
+
);
|
|
2188
|
+
props.onError?.(notice);
|
|
2189
|
+
}
|
|
2190
|
+
return result.formId;
|
|
2191
|
+
} catch (error) {
|
|
2192
|
+
const failure = isFilloError2(error) ? error : new FilloError2(error instanceof Error ? error.message : String(error), 0);
|
|
2193
|
+
console.warn(
|
|
2194
|
+
`[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
|
|
2195
|
+
);
|
|
2196
|
+
props.onError?.(failure);
|
|
2197
|
+
throw failure;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
1902
2200
|
) : void 0,
|
|
1903
|
-
[codeForm, client]
|
|
2201
|
+
[codeForm, client, props.onError, schema]
|
|
1904
2202
|
);
|
|
1905
2203
|
const api = useFilloController({
|
|
1906
2204
|
form: schema,
|
|
@@ -1910,7 +2208,7 @@ function ResolvedForm(props) {
|
|
|
1910
2208
|
onChange: props.onChange,
|
|
1911
2209
|
onSubmitted: props.onSubmitted,
|
|
1912
2210
|
skipValidation: props.skipValidation,
|
|
1913
|
-
getHoneypot: () => hpRef.current,
|
|
2211
|
+
getHoneypot: () => hpRef.current?.value ?? "",
|
|
1914
2212
|
// Framed renderer: Fillo draws the layout (and badge), so the surface is
|
|
1915
2213
|
// "default", never the bare-engine "headless".
|
|
1916
2214
|
surface: "default",
|
|
@@ -1927,12 +2225,12 @@ function ResolvedForm(props) {
|
|
|
1927
2225
|
const url = safeHttpUrl(settings.redirectUrl);
|
|
1928
2226
|
if (url) window.location.assign(url);
|
|
1929
2227
|
}, [liveSubmit, settings.redirectUrl]);
|
|
1930
|
-
const successRef =
|
|
2228
|
+
const successRef = useRef6(null);
|
|
1931
2229
|
useEffect7(() => {
|
|
1932
2230
|
if (liveSubmit) successRef.current?.focus();
|
|
1933
2231
|
}, [liveSubmit]);
|
|
1934
|
-
const blocksRef =
|
|
1935
|
-
const prevPageIndex =
|
|
2232
|
+
const blocksRef = useRef6(null);
|
|
2233
|
+
const prevPageIndex = useRef6(api.pageIndex);
|
|
1936
2234
|
useEffect7(() => {
|
|
1937
2235
|
if (prevPageIndex.current === api.pageIndex) return;
|
|
1938
2236
|
prevPageIndex.current = api.pageIndex;
|
|
@@ -1965,7 +2263,7 @@ function ResolvedForm(props) {
|
|
|
1965
2263
|
children: [
|
|
1966
2264
|
/* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
|
|
1967
2265
|
/* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
|
|
1968
|
-
/* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: settings.successMessage ?? strings.successMessage })
|
|
2266
|
+
/* @__PURE__ */ jsx5("p", { className: "fillo-success-message", children: api.duplicateSubmission ? strings.alreadyAnswered : settings.successMessage ?? strings.successMessage })
|
|
1969
2267
|
]
|
|
1970
2268
|
}
|
|
1971
2269
|
),
|
|
@@ -2005,6 +2303,8 @@ function ResolvedForm(props) {
|
|
|
2005
2303
|
children: [
|
|
2006
2304
|
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
2007
2305
|
props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
|
|
2306
|
+
props.stagedNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevStagedNotice, {}),
|
|
2307
|
+
props.syncErrorNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevSyncErrorNotice, { error: props.syncErrorNotice }),
|
|
2008
2308
|
(api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
|
|
2009
2309
|
"div",
|
|
2010
2310
|
{
|
|
@@ -2017,13 +2317,23 @@ function ResolvedForm(props) {
|
|
|
2017
2317
|
]
|
|
2018
2318
|
}
|
|
2019
2319
|
),
|
|
2320
|
+
api.resumeLinkFailed && !api.resumedDraft && !api.editingPrevious && /* @__PURE__ */ jsx5(
|
|
2321
|
+
"div",
|
|
2322
|
+
{
|
|
2323
|
+
className: slotClass3("fillo-resume fillo-resume--expired", appearance, { slot: "resume" }),
|
|
2324
|
+
"data-fillo": "resume",
|
|
2325
|
+
role: "status",
|
|
2326
|
+
children: /* @__PURE__ */ jsx5("span", { className: "fillo-resume-text", children: strings.resumeLinkExpired })
|
|
2327
|
+
}
|
|
2328
|
+
),
|
|
2020
2329
|
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
|
|
2021
2330
|
"div",
|
|
2022
2331
|
{
|
|
2023
2332
|
className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
|
|
2024
2333
|
"data-fillo": "progress",
|
|
2025
2334
|
role: "progressbar",
|
|
2026
|
-
"aria-
|
|
2335
|
+
"aria-label": schema.title || "Form progress",
|
|
2336
|
+
"aria-valuemin": 1,
|
|
2027
2337
|
"aria-valuemax": api.pageCount,
|
|
2028
2338
|
"aria-valuenow": api.pageIndex + 1,
|
|
2029
2339
|
children: /* @__PURE__ */ jsx5(
|
|
@@ -2069,9 +2379,9 @@ function ResolvedForm(props) {
|
|
|
2069
2379
|
"aria-hidden": "true",
|
|
2070
2380
|
defaultValue: "",
|
|
2071
2381
|
style: HP_STYLE,
|
|
2072
|
-
ref:
|
|
2073
|
-
|
|
2074
|
-
|
|
2382
|
+
ref: (element) => {
|
|
2383
|
+
hpRef.current = element;
|
|
2384
|
+
assertHpHidden(element);
|
|
2075
2385
|
}
|
|
2076
2386
|
}
|
|
2077
2387
|
),
|
|
@@ -2105,7 +2415,7 @@ function ResolvedForm(props) {
|
|
|
2105
2415
|
}
|
|
2106
2416
|
|
|
2107
2417
|
// src/jsx.tsx
|
|
2108
|
-
import { useRef as
|
|
2418
|
+
import { useRef as useRef7 } from "react";
|
|
2109
2419
|
import {
|
|
2110
2420
|
codeFormFromJsx,
|
|
2111
2421
|
FilloJsxError,
|
|
@@ -2118,7 +2428,7 @@ function JsxForm(props) {
|
|
|
2118
2428
|
const { id, title, description, settings, theme, client, children, ...rest } = props;
|
|
2119
2429
|
const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
|
|
2120
2430
|
const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
|
|
2121
|
-
const memo2 =
|
|
2431
|
+
const memo2 = useRef7(null);
|
|
2122
2432
|
if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
|
|
2123
2433
|
console.warn(
|
|
2124
2434
|
`[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.`
|
|
@@ -2172,30 +2482,121 @@ var Fillo = {
|
|
|
2172
2482
|
|
|
2173
2483
|
// src/provider.tsx
|
|
2174
2484
|
import { useId as useId3, useMemo as useMemo6 } from "react";
|
|
2175
|
-
import {
|
|
2176
|
-
|
|
2177
|
-
|
|
2485
|
+
import {
|
|
2486
|
+
FilloError as FilloError3,
|
|
2487
|
+
formSchemasEqual as formSchemasEqual2,
|
|
2488
|
+
isFilloError as isFilloError3,
|
|
2489
|
+
normalizeFormSchema as normalizeFormSchema2,
|
|
2490
|
+
resolveStrings as resolveStrings2,
|
|
2491
|
+
syncCodeForm as syncCodeForm3
|
|
2492
|
+
} from "@usefillo/core";
|
|
2493
|
+
import { Fragment as Fragment3, jsx as jsx7 } from "react/jsx-runtime";
|
|
2494
|
+
function FilloProvider({
|
|
2495
|
+
children,
|
|
2496
|
+
form,
|
|
2497
|
+
formId,
|
|
2498
|
+
client,
|
|
2499
|
+
appearance,
|
|
2500
|
+
strings,
|
|
2501
|
+
onError,
|
|
2502
|
+
renderError,
|
|
2503
|
+
...options
|
|
2504
|
+
}) {
|
|
2178
2505
|
const instanceId = useId3().replace(/:/g, "");
|
|
2179
2506
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2507
|
+
const {
|
|
2508
|
+
formId: syncedFormId,
|
|
2509
|
+
status: syncedStatus,
|
|
2510
|
+
error: syncError,
|
|
2511
|
+
syncing: codeFormSyncing,
|
|
2512
|
+
resolvedSchema
|
|
2513
|
+
} = useCodeFormSync(form, client, onError, {
|
|
2514
|
+
allowUnsynced: Boolean(!client || formId || options.skipValidation)
|
|
2515
|
+
});
|
|
2516
|
+
const rawSchema = codeForm ? resolvedSchema && !isDevEnv3() ? resolvedSchema : codeForm.schema : form;
|
|
2517
|
+
const normalized = useMemo6(() => normalizeFormSchema2(rawSchema), [rawSchema]);
|
|
2183
2518
|
if (!normalized.ok) {
|
|
2184
2519
|
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
2185
2520
|
}
|
|
2186
2521
|
const schema = normalized.schema;
|
|
2187
|
-
const
|
|
2522
|
+
const resolveFormId = useMemo6(
|
|
2523
|
+
() => codeForm && client?.key ? async () => {
|
|
2524
|
+
try {
|
|
2525
|
+
const result = await syncCodeForm3(client, codeForm, { bypassCache: true });
|
|
2526
|
+
if (result.status === "draft") {
|
|
2527
|
+
throw new FilloError3(
|
|
2528
|
+
"This form is no longer published. Submission was stopped before sending answers.",
|
|
2529
|
+
403,
|
|
2530
|
+
void 0,
|
|
2531
|
+
"form_not_published"
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2534
|
+
const authoritative = normalizeFormSchema2(result.resolvedSchema ?? codeForm.schema);
|
|
2535
|
+
if (!authoritative.ok) {
|
|
2536
|
+
throw new FilloError3(
|
|
2537
|
+
`The canonical form schema was invalid: ${authoritative.error}`,
|
|
2538
|
+
502,
|
|
2539
|
+
void 0,
|
|
2540
|
+
"invalid_sync_schema"
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
if (!formSchemasEqual2(schema, authoritative.schema)) {
|
|
2544
|
+
throw new FilloError3(
|
|
2545
|
+
"The live form changed after this page loaded. Submission stopped before incompatible answers were sent. Reload to use the published schema.",
|
|
2546
|
+
409,
|
|
2547
|
+
void 0,
|
|
2548
|
+
"form_schema_changed"
|
|
2549
|
+
);
|
|
2550
|
+
}
|
|
2551
|
+
if (result.syncError) {
|
|
2552
|
+
const notice = new FilloError3(
|
|
2553
|
+
result.syncError.message,
|
|
2554
|
+
403,
|
|
2555
|
+
void 0,
|
|
2556
|
+
result.syncError.code
|
|
2557
|
+
);
|
|
2558
|
+
console.warn(
|
|
2559
|
+
`[fillo] form sync needs attention (${notice.code}): ${notice.message}`
|
|
2560
|
+
);
|
|
2561
|
+
onError?.(notice);
|
|
2562
|
+
}
|
|
2563
|
+
return result.formId;
|
|
2564
|
+
} catch (error) {
|
|
2565
|
+
const failure = isFilloError3(error) ? error : new FilloError3(error instanceof Error ? error.message : String(error), 0);
|
|
2566
|
+
console.warn(
|
|
2567
|
+
`[fillo] form sync failed${failure.code ? ` (${failure.code})` : ""}: ${failure.message}`
|
|
2568
|
+
);
|
|
2569
|
+
onError?.(failure);
|
|
2570
|
+
throw failure;
|
|
2571
|
+
}
|
|
2572
|
+
} : void 0,
|
|
2573
|
+
[client, codeForm, onError, schema]
|
|
2574
|
+
);
|
|
2188
2575
|
const api = useFilloController({
|
|
2189
2576
|
...options,
|
|
2190
2577
|
client,
|
|
2191
2578
|
form: schema,
|
|
2192
2579
|
formId: syncedFormId ?? formId,
|
|
2580
|
+
resolveFormId,
|
|
2193
2581
|
// Headless = your markup, no Fillo-rendered layout. Free like every embed
|
|
2194
2582
|
// method; recorded per response so usage stays measurable.
|
|
2195
2583
|
surface: "headless"
|
|
2196
2584
|
});
|
|
2197
2585
|
useDraftFlush(api);
|
|
2198
|
-
|
|
2586
|
+
const resolvedStrings = resolveStrings2(strings);
|
|
2587
|
+
const canonicalSyncError = syncError && !options.skipValidation ? syncError : null;
|
|
2588
|
+
if (canonicalSyncError && renderError) return /* @__PURE__ */ jsx7(Fragment3, { children: renderError(canonicalSyncError) });
|
|
2589
|
+
if (canonicalSyncError && !isDevEnv3()) return null;
|
|
2590
|
+
if (codeFormSyncing && !options.skipValidation && !isDevEnv3()) return null;
|
|
2591
|
+
const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !options.skipValidation;
|
|
2592
|
+
if (notPublished && !isDevEnv3()) {
|
|
2593
|
+
const error = new FilloError3("This form isn't published yet.", 403, void 0, "form_not_published");
|
|
2594
|
+
return renderError ? /* @__PURE__ */ jsx7(Fragment3, { children: renderError(error) }) : null;
|
|
2595
|
+
}
|
|
2596
|
+
return /* @__PURE__ */ jsx7(FilloContext.Provider, { value: api, children: /* @__PURE__ */ jsx7(FilloStringsContext.Provider, { value: resolvedStrings, children: /* @__PURE__ */ jsx7(FilloAppearanceContext.Provider, { value: appearance, children: /* @__PURE__ */ jsx7(FilloInstanceIdContext.Provider, { value: instanceId, children }) }) }) });
|
|
2597
|
+
}
|
|
2598
|
+
function isDevEnv3() {
|
|
2599
|
+
return typeof process !== "undefined" && process.env?.NODE_ENV !== "production";
|
|
2199
2600
|
}
|
|
2200
2601
|
|
|
2201
2602
|
// src/index.ts
|