@usefillo/react 0.6.2 → 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 +76 -14
- package/dist/index.js +681 -239
- package/dist/styles.css +37 -0
- package/dist/styles.unlayered.css +37 -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
|
}
|
|
@@ -120,13 +126,19 @@ function useFilloController(options) {
|
|
|
120
126
|
skipValidation: options.skipValidation,
|
|
121
127
|
surface: options.surface ?? "headless",
|
|
122
128
|
// Wrapped so the engine (created once) always calls the latest resolver.
|
|
123
|
-
resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0
|
|
129
|
+
resolveFormId: options.resolveFormId ? () => optsRef.current.resolveFormId() : void 0,
|
|
130
|
+
respondent: options.respondent
|
|
124
131
|
});
|
|
125
132
|
}
|
|
126
133
|
const store = storeRef.current;
|
|
127
134
|
useEffect(() => {
|
|
128
|
-
store.setContext({
|
|
129
|
-
|
|
135
|
+
store.setContext({
|
|
136
|
+
form: options.form,
|
|
137
|
+
formId: options.formId,
|
|
138
|
+
client: options.client,
|
|
139
|
+
respondent: options.respondent
|
|
140
|
+
});
|
|
141
|
+
}, [store, options.form, options.formId, options.client, options.respondent]);
|
|
130
142
|
useEffect(() => () => store.destroy(), [store]);
|
|
131
143
|
const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
132
144
|
return useMemo2(
|
|
@@ -139,20 +151,40 @@ function useFilloController(options) {
|
|
|
139
151
|
setUploading: store.setUploading,
|
|
140
152
|
next: store.next,
|
|
141
153
|
back: store.back,
|
|
142
|
-
submit: store.submit
|
|
154
|
+
submit: store.submit,
|
|
155
|
+
flushDraft: store.flushDraft,
|
|
156
|
+
resetDraft: store.resetDraft
|
|
143
157
|
}),
|
|
144
158
|
[store, state, options.form, options.formId, options.client]
|
|
145
159
|
);
|
|
146
160
|
}
|
|
161
|
+
function useDraftFlush(api) {
|
|
162
|
+
const enabled = Boolean(api.form.settings.saveProgress);
|
|
163
|
+
const flush = api.flushDraft;
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (!enabled || typeof window === "undefined") return;
|
|
166
|
+
const onPageHide = () => flush();
|
|
167
|
+
const onVisibility = () => {
|
|
168
|
+
if (document.visibilityState === "hidden") flush();
|
|
169
|
+
};
|
|
170
|
+
window.addEventListener("pagehide", onPageHide);
|
|
171
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
172
|
+
return () => {
|
|
173
|
+
window.removeEventListener("pagehide", onPageHide);
|
|
174
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
175
|
+
};
|
|
176
|
+
}, [enabled, flush]);
|
|
177
|
+
}
|
|
147
178
|
|
|
148
179
|
// src/define.ts
|
|
149
180
|
import {
|
|
150
181
|
contentHash,
|
|
151
182
|
FilloError,
|
|
152
183
|
isCodeForm,
|
|
184
|
+
isFilloError,
|
|
153
185
|
syncCodeForm
|
|
154
186
|
} from "@usefillo/core";
|
|
155
|
-
import { useEffect as useEffect2, useState } from "react";
|
|
187
|
+
import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
|
|
156
188
|
import { defineForm, isCodeForm as isCodeForm2, syncCodeForm as syncCodeForm2 } from "@usefillo/core";
|
|
157
189
|
var noticedSync = /* @__PURE__ */ new Set();
|
|
158
190
|
function noticeSync(handle, kind, message) {
|
|
@@ -161,39 +193,156 @@ function noticeSync(handle, kind, message) {
|
|
|
161
193
|
noticedSync.add(key);
|
|
162
194
|
console.info(message);
|
|
163
195
|
}
|
|
164
|
-
function useCodeFormSync(form, client, onError) {
|
|
196
|
+
function useCodeFormSync(form, client, onError, options) {
|
|
165
197
|
const codeForm = isCodeForm(form) ? form : null;
|
|
166
|
-
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
|
+
});
|
|
167
208
|
const hasKey = Boolean(client?.key);
|
|
209
|
+
const allowUnsynced = Boolean(options?.allowUnsynced);
|
|
168
210
|
const handle = codeForm?.id;
|
|
169
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;
|
|
170
215
|
useEffect2(() => {
|
|
171
|
-
if (!codeForm || !
|
|
216
|
+
if (!codeForm || !syncKey) return;
|
|
172
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
|
+
}
|
|
173
261
|
syncCodeForm(client, codeForm).then((r) => {
|
|
174
262
|
if (cancelled) return;
|
|
175
|
-
|
|
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
|
+
});
|
|
176
285
|
const dashboardUrl = `${client.baseUrl || "https://fillo.so"}/forms/${r.formId}`;
|
|
177
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
|
+
}
|
|
178
293
|
if (r.staged) {
|
|
179
|
-
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
|
+
);
|
|
180
299
|
} else if (r.status === "draft") {
|
|
181
300
|
noticeSync(codeForm.id, "draft", `[fillo] "${codeForm.id}" is a draft \u2014 it renders here but won't accept responses until published: ${dashboardUrl}`);
|
|
182
301
|
}
|
|
183
302
|
}).catch((err) => {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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);
|
|
187
319
|
});
|
|
188
320
|
return () => {
|
|
189
321
|
cancelled = true;
|
|
190
322
|
};
|
|
191
|
-
}, [hasKey, handle, contentKey, client,
|
|
192
|
-
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
|
+
};
|
|
193
342
|
}
|
|
194
343
|
|
|
195
344
|
// src/fields.tsx
|
|
196
|
-
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";
|
|
197
346
|
import { memo, useEffect as useEffect6, useMemo as useMemo4, useState as useState5 } from "react";
|
|
198
347
|
|
|
199
348
|
// src/phone.tsx
|
|
@@ -205,9 +354,10 @@ import {
|
|
|
205
354
|
flagEmoji,
|
|
206
355
|
formatNational,
|
|
207
356
|
parsePhone,
|
|
357
|
+
positionPhonePopover,
|
|
208
358
|
toE164
|
|
209
359
|
} from "@usefillo/core";
|
|
210
|
-
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";
|
|
211
361
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
212
362
|
function localeCountry() {
|
|
213
363
|
if (typeof navigator === "undefined") return void 0;
|
|
@@ -228,31 +378,6 @@ function nationalFor(stored, country) {
|
|
|
228
378
|
const digits = digitsOnly(stored);
|
|
229
379
|
return digits.startsWith(country.dialCode) ? digits.slice(country.dialCode.length) : digits;
|
|
230
380
|
}
|
|
231
|
-
var PHONE_POPOVER_VIEWPORT_GAP = 8;
|
|
232
|
-
function positionPhonePopover(anchor, popover) {
|
|
233
|
-
if (!anchor || !popover || typeof window === "undefined") return "below";
|
|
234
|
-
const gap = PHONE_POPOVER_VIEWPORT_GAP;
|
|
235
|
-
const viewportWidth = document.documentElement.clientWidth || window.innerWidth;
|
|
236
|
-
const viewportHeight = document.documentElement.clientHeight || window.innerHeight;
|
|
237
|
-
const anchorRect = anchor.getBoundingClientRect();
|
|
238
|
-
const spaceBelow = Math.max(0, viewportHeight - anchorRect.bottom - gap);
|
|
239
|
-
const spaceAbove = Math.max(0, anchorRect.top - gap);
|
|
240
|
-
const placement = spaceBelow < popover.scrollHeight && spaceAbove > spaceBelow ? "above" : "below";
|
|
241
|
-
const availableHeight = placement === "above" ? spaceAbove : spaceBelow;
|
|
242
|
-
const maxHeight = Math.max(0, Math.min(availableHeight, viewportHeight - gap * 2));
|
|
243
|
-
const maxWidth = Math.max(0, viewportWidth - gap * 2);
|
|
244
|
-
popover.style.setProperty("--fillo-phone-popover-max-height", `${Math.floor(maxHeight)}px`);
|
|
245
|
-
popover.style.setProperty("--fillo-phone-popover-max-width", `${Math.floor(maxWidth)}px`);
|
|
246
|
-
popover.style.setProperty("--fillo-phone-popover-offset-x", "0px");
|
|
247
|
-
const width = Math.min(popover.offsetWidth, maxWidth);
|
|
248
|
-
let offsetX = 0;
|
|
249
|
-
const rightOverflow = anchorRect.left + width - (viewportWidth - gap);
|
|
250
|
-
if (rightOverflow > 0) offsetX -= rightOverflow;
|
|
251
|
-
const leftOverflow = gap - (anchorRect.left + offsetX);
|
|
252
|
-
if (leftOverflow > 0) offsetX += leftOverflow;
|
|
253
|
-
popover.style.setProperty("--fillo-phone-popover-offset-x", `${Math.round(offsetX)}px`);
|
|
254
|
-
return placement;
|
|
255
|
-
}
|
|
256
381
|
function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
257
382
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
258
383
|
const slots = useFieldSlots(field, error);
|
|
@@ -262,7 +387,12 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
262
387
|
const [country, setCountry] = useState2(
|
|
263
388
|
() => parsePhone(stored).country ?? countryByIso(schema.defaultCountry) ?? PHONE_COUNTRIES[0]
|
|
264
389
|
);
|
|
265
|
-
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]);
|
|
266
396
|
useEffect3(() => {
|
|
267
397
|
if (touched.current) return;
|
|
268
398
|
if (parsePhone(stored).country || countryByIso(schema.defaultCountry)) return;
|
|
@@ -271,8 +401,8 @@ function PhoneField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
271
401
|
}, []);
|
|
272
402
|
const national = nationalFor(stored, country);
|
|
273
403
|
const display = formatNational(country, national);
|
|
274
|
-
const inputRef =
|
|
275
|
-
const caretDigits =
|
|
404
|
+
const inputRef = useRef3(null);
|
|
405
|
+
const caretDigits = useRef3(null);
|
|
276
406
|
useLayoutEffect(() => {
|
|
277
407
|
const el = inputRef.current;
|
|
278
408
|
if (!el || caretDigits.current == null) return;
|
|
@@ -350,10 +480,10 @@ function CountrySelect({
|
|
|
350
480
|
const [query, setQuery] = useState2("");
|
|
351
481
|
const [active, setActive] = useState2(0);
|
|
352
482
|
const [placement, setPlacement] = useState2("below");
|
|
353
|
-
const rootRef =
|
|
354
|
-
const triggerRef =
|
|
355
|
-
const popoverRef =
|
|
356
|
-
const searchRef =
|
|
483
|
+
const rootRef = useRef3(null);
|
|
484
|
+
const triggerRef = useRef3(null);
|
|
485
|
+
const popoverRef = useRef3(null);
|
|
486
|
+
const searchRef = useRef3(null);
|
|
357
487
|
const listId = `${labelledBy}-countries`;
|
|
358
488
|
const matches = useMemo3(() => {
|
|
359
489
|
const q = query.trim().toLowerCase();
|
|
@@ -423,6 +553,7 @@ function CountrySelect({
|
|
|
423
553
|
className: "fillo-phone-flag",
|
|
424
554
|
"aria-haspopup": "listbox",
|
|
425
555
|
"aria-expanded": open,
|
|
556
|
+
"aria-controls": open ? listId : void 0,
|
|
426
557
|
"aria-label": `Country: ${country.name} (+${country.dialCode})`,
|
|
427
558
|
onClick: () => setOpen((o) => !o),
|
|
428
559
|
children: [
|
|
@@ -446,6 +577,7 @@ function CountrySelect({
|
|
|
446
577
|
"aria-expanded": "true",
|
|
447
578
|
"aria-controls": listId,
|
|
448
579
|
"aria-autocomplete": "list",
|
|
580
|
+
"aria-label": "Search country or code",
|
|
449
581
|
"aria-activedescendant": matches[active] ? `${listId}-opt-${active}` : void 0,
|
|
450
582
|
placeholder: "Search country or code",
|
|
451
583
|
value: query,
|
|
@@ -487,15 +619,15 @@ function CountrySelect({
|
|
|
487
619
|
}
|
|
488
620
|
|
|
489
621
|
// src/signature.tsx
|
|
490
|
-
import { useEffect as useEffect4, useRef as
|
|
622
|
+
import { useEffect as useEffect4, useRef as useRef4, useState as useState3 } from "react";
|
|
491
623
|
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
492
624
|
function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
493
625
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
494
626
|
const slots = useFieldSlots(field, error);
|
|
495
627
|
const strings = useStrings();
|
|
496
628
|
const typedInputId = `${ids.inputId}-typed`;
|
|
497
|
-
const canvasRef =
|
|
498
|
-
const drawing =
|
|
629
|
+
const canvasRef = useRef4(null);
|
|
630
|
+
const drawing = useRef4(false);
|
|
499
631
|
const [hasInk, setHasInk] = useState3(Boolean(value));
|
|
500
632
|
const [typedName, setTypedName] = useState3("");
|
|
501
633
|
useEffect4(() => {
|
|
@@ -512,12 +644,31 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
512
644
|
ctx.lineCap = "round";
|
|
513
645
|
ctx.lineJoin = "round";
|
|
514
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();
|
|
515
654
|
if (typeof value === "string" && value.startsWith("data:image/")) {
|
|
655
|
+
setHasInk(true);
|
|
516
656
|
const img = new Image();
|
|
517
|
-
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
|
+
};
|
|
518
662
|
img.src = value;
|
|
663
|
+
} else {
|
|
664
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
665
|
+
setHasInk(false);
|
|
666
|
+
setTypedName("");
|
|
519
667
|
}
|
|
520
|
-
|
|
668
|
+
return () => {
|
|
669
|
+
cancelled = true;
|
|
670
|
+
};
|
|
671
|
+
}, [value]);
|
|
521
672
|
function point(e) {
|
|
522
673
|
const rect = e.currentTarget.getBoundingClientRect();
|
|
523
674
|
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
|
|
@@ -597,6 +748,7 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
597
748
|
onPointerDown: start,
|
|
598
749
|
onPointerMove: move,
|
|
599
750
|
onPointerUp: end,
|
|
751
|
+
onPointerCancel: end,
|
|
600
752
|
onPointerLeave: end
|
|
601
753
|
}
|
|
602
754
|
),
|
|
@@ -632,21 +784,15 @@ function SignatureField({ field, value, error, setValue, ids: providedIds }) {
|
|
|
632
784
|
}
|
|
633
785
|
|
|
634
786
|
// src/upload.tsx
|
|
635
|
-
import { useEffect as useEffect5, useRef as
|
|
787
|
+
import { useEffect as useEffect5, useRef as useRef5, useState as useState4 } from "react";
|
|
636
788
|
import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
637
|
-
function uploadStatus(inFlight, doneCount) {
|
|
789
|
+
function uploadStatus(inFlight, doneCount, strings) {
|
|
638
790
|
const failed = inFlight.filter((f) => f.error).length;
|
|
639
791
|
const active = inFlight.length - failed;
|
|
640
792
|
const parts = [];
|
|
641
|
-
if (active > 0)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
if (failed > 0) {
|
|
645
|
-
parts.push(`${failed} ${failed === 1 ? "upload" : "uploads"} failed.`);
|
|
646
|
-
}
|
|
647
|
-
if (active === 0 && doneCount > 0) {
|
|
648
|
-
parts.push(`${doneCount} ${doneCount === 1 ? "file" : "files"} uploaded.`);
|
|
649
|
-
}
|
|
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));
|
|
650
796
|
return parts.join(" ");
|
|
651
797
|
}
|
|
652
798
|
function formatBytes(bytes) {
|
|
@@ -660,41 +806,49 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
660
806
|
const slots = useFieldSlots(field, error);
|
|
661
807
|
const strings = useStrings();
|
|
662
808
|
const schema = field;
|
|
663
|
-
const inputRef =
|
|
809
|
+
const inputRef = useRef5(null);
|
|
664
810
|
const [inFlight, setInFlight] = useState4([]);
|
|
665
811
|
const [dragOver, setDragOver] = useState4(false);
|
|
666
|
-
const mountedRef =
|
|
667
|
-
const controllers =
|
|
668
|
-
const inFlightCount =
|
|
669
|
-
|
|
670
|
-
|
|
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 () => {
|
|
671
820
|
mountedRef.current = false;
|
|
672
821
|
controllers.current.forEach((c) => c.abort());
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
);
|
|
822
|
+
if (inFlightCount.current > 0) setUploading(fieldId, false);
|
|
823
|
+
};
|
|
824
|
+
}, [fieldId, setUploading]);
|
|
676
825
|
const files = Array.isArray(value) ? value : [];
|
|
826
|
+
const filesRef = useRef5(files);
|
|
827
|
+
filesRef.current = files;
|
|
677
828
|
const maxFiles = schema.maxFiles ?? 1;
|
|
678
|
-
const
|
|
829
|
+
const maxMb = schema.maxFileSizeMb ?? 500;
|
|
830
|
+
const maxBytes = maxMb * 1024 * 1024;
|
|
679
831
|
const canUpload = Boolean(api.client && api.formId);
|
|
680
|
-
const
|
|
832
|
+
const activeInFlight = inFlight.filter((f) => !f.error).length;
|
|
833
|
+
const remaining = maxFiles - files.length - activeInFlight;
|
|
681
834
|
async function startUpload(file) {
|
|
835
|
+
if (maxFiles - filesRef.current.length - inFlightCount.current <= 0) return;
|
|
682
836
|
const key = `${file.name}-${file.size}-${Math.floor(performance.now())}`;
|
|
683
837
|
if (file.size > maxBytes) {
|
|
684
838
|
setInFlight((prev) => [
|
|
685
839
|
...prev,
|
|
686
|
-
{ 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) }
|
|
687
841
|
]);
|
|
688
842
|
return;
|
|
689
843
|
}
|
|
690
|
-
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 }]);
|
|
691
845
|
inFlightCount.current += 1;
|
|
692
|
-
|
|
846
|
+
setUploading(fieldId, true);
|
|
693
847
|
const controller = new AbortController();
|
|
694
848
|
controllers.current.set(key, controller);
|
|
695
849
|
try {
|
|
696
850
|
const uploaded = await api.client.uploadFile(api.formId, file, {
|
|
697
|
-
fieldId
|
|
851
|
+
fieldId,
|
|
698
852
|
signal: controller.signal,
|
|
699
853
|
onProgress: ({ fraction }) => {
|
|
700
854
|
if (mountedRef.current) {
|
|
@@ -704,11 +858,12 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
704
858
|
});
|
|
705
859
|
if (!mountedRef.current) return;
|
|
706
860
|
setInFlight((prev) => prev.filter((f) => f.key !== key));
|
|
707
|
-
const
|
|
708
|
-
|
|
861
|
+
const next = [...filesRef.current, uploaded].slice(0, maxFiles);
|
|
862
|
+
filesRef.current = next;
|
|
863
|
+
setValue(next);
|
|
709
864
|
} catch (err) {
|
|
710
865
|
if (mountedRef.current) {
|
|
711
|
-
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;
|
|
712
867
|
setInFlight(
|
|
713
868
|
(prev) => prev.map((f) => f.key === key ? { ...f, error: message } : f)
|
|
714
869
|
);
|
|
@@ -716,7 +871,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
716
871
|
} finally {
|
|
717
872
|
controllers.current.delete(key);
|
|
718
873
|
inFlightCount.current -= 1;
|
|
719
|
-
if (mountedRef.current && inFlightCount.current === 0)
|
|
874
|
+
if (mountedRef.current && inFlightCount.current === 0) setUploading(fieldId, false);
|
|
720
875
|
}
|
|
721
876
|
}
|
|
722
877
|
function cancelUpload(key) {
|
|
@@ -725,11 +880,14 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
725
880
|
}
|
|
726
881
|
function handleFiles(list) {
|
|
727
882
|
if (!list) return;
|
|
728
|
-
|
|
883
|
+
const currentRoom = Math.max(0, maxFiles - filesRef.current.length - inFlightCount.current);
|
|
884
|
+
Array.from(list).slice(0, currentRoom).forEach((file) => void startUpload(file));
|
|
729
885
|
if (inputRef.current) inputRef.current.value = "";
|
|
730
886
|
}
|
|
731
887
|
function removeFile(fileId) {
|
|
732
|
-
|
|
888
|
+
const next = filesRef.current.filter((f) => f.fileId !== fileId);
|
|
889
|
+
filesRef.current = next;
|
|
890
|
+
setValue(next);
|
|
733
891
|
}
|
|
734
892
|
const describedBy = [field.description ? ids.descriptionId : null, error ? ids.errorId : null].filter(Boolean).join(" ") || void 0;
|
|
735
893
|
return /* @__PURE__ */ jsxs3("div", { ...slots.wrapperProps(`fillo-field fillo-field--file_upload${error ? " fillo-field--error" : ""}`), children: [
|
|
@@ -757,12 +915,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
757
915
|
onClick: () => canUpload && inputRef.current?.click(),
|
|
758
916
|
role: "button",
|
|
759
917
|
tabIndex: 0,
|
|
918
|
+
"aria-disabled": !canUpload || void 0,
|
|
760
919
|
"aria-labelledby": ids.labelId,
|
|
761
920
|
"aria-required": field.required ? true : void 0,
|
|
762
921
|
"aria-invalid": error ? true : void 0,
|
|
763
922
|
"aria-describedby": describedBy,
|
|
764
923
|
onKeyDown: (e) => {
|
|
765
|
-
if (
|
|
924
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
925
|
+
e.preventDefault();
|
|
926
|
+
if (canUpload) inputRef.current?.click();
|
|
927
|
+
}
|
|
766
928
|
},
|
|
767
929
|
children: [
|
|
768
930
|
/* @__PURE__ */ jsx3(
|
|
@@ -772,23 +934,16 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
772
934
|
id: ids.inputId,
|
|
773
935
|
type: "file",
|
|
774
936
|
hidden: true,
|
|
937
|
+
disabled: !canUpload,
|
|
775
938
|
multiple: maxFiles > 1,
|
|
776
939
|
accept: schema.accept?.join(","),
|
|
777
940
|
onChange: (e) => canUpload && handleFiles(e.target.files)
|
|
778
941
|
}
|
|
779
942
|
),
|
|
780
943
|
canUpload ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
781
|
-
/* @__PURE__ */
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
" here or click to browse"
|
|
785
|
-
] }),
|
|
786
|
-
/* @__PURE__ */ jsxs3("span", { className: "fillo-dropzone-hint", children: [
|
|
787
|
-
"Up to ",
|
|
788
|
-
schema.maxFileSizeMb ?? 500,
|
|
789
|
-
" MB per file \xB7 resumable"
|
|
790
|
-
] })
|
|
791
|
-
] }) : /* @__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 })
|
|
792
947
|
]
|
|
793
948
|
}
|
|
794
949
|
),
|
|
@@ -799,7 +954,7 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
799
954
|
"aria-live": "polite",
|
|
800
955
|
className: "fillo-sr-only",
|
|
801
956
|
style: { position: "absolute", left: "-9999px", width: 1, height: 1, overflow: "hidden" },
|
|
802
|
-
children: uploadStatus(inFlight, files.length)
|
|
957
|
+
children: uploadStatus(inFlight, files.length, strings)
|
|
803
958
|
}
|
|
804
959
|
),
|
|
805
960
|
(inFlight.length > 0 || files.length > 0) && /* @__PURE__ */ jsxs3("ul", { className: "fillo-files", children: [
|
|
@@ -832,6 +987,21 @@ function FileUploadField({ field, value, error, setValue, api, ids: providedIds
|
|
|
832
987
|
/* @__PURE__ */ jsx3("span", { className: "fillo-file-name", children: f.name }),
|
|
833
988
|
f.error ? /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
834
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
|
+
),
|
|
835
1005
|
/* @__PURE__ */ jsx3(
|
|
836
1006
|
"button",
|
|
837
1007
|
{
|
|
@@ -950,6 +1120,9 @@ function TextInput({ field, value, error, setValue, ids: providedIds }) {
|
|
|
950
1120
|
type,
|
|
951
1121
|
value: typeof value === "string" || typeof value === "number" ? String(value) : "",
|
|
952
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,
|
|
953
1126
|
onChange: (e) => setValue(e.target.value),
|
|
954
1127
|
...fieldAria(field, error, ids)
|
|
955
1128
|
}
|
|
@@ -966,6 +1139,7 @@ function LongText({ field, value, error, setValue, ids: providedIds }) {
|
|
|
966
1139
|
rows: 4,
|
|
967
1140
|
value: typeof value === "string" ? value : "",
|
|
968
1141
|
placeholder: field.placeholder,
|
|
1142
|
+
maxLength: "maxLength" in field ? field.maxLength : void 0,
|
|
969
1143
|
onChange: (e) => setValue(e.target.value),
|
|
970
1144
|
...fieldAria(field, error, ids)
|
|
971
1145
|
}
|
|
@@ -1221,12 +1395,18 @@ function MultiChoice({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1221
1395
|
}
|
|
1222
1396
|
) });
|
|
1223
1397
|
}
|
|
1224
|
-
|
|
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
|
+
}
|
|
1225
1404
|
function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
1226
1405
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1227
1406
|
const strings = useStrings();
|
|
1228
1407
|
const choice = field;
|
|
1229
1408
|
const options = useDisplayOptions(choice);
|
|
1409
|
+
const otherValue = otherSentinel(choice);
|
|
1230
1410
|
const isOtherValue = typeof value === "string" && value !== "" && !choice.options.some((o) => o.id === value);
|
|
1231
1411
|
const [otherOn, setOtherOn] = useState5(isOtherValue);
|
|
1232
1412
|
const otherActive = isOtherValue || value === "" && otherOn;
|
|
@@ -1239,9 +1419,9 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1239
1419
|
id: ids.inputId,
|
|
1240
1420
|
...control,
|
|
1241
1421
|
...fieldAria(field, error, ids),
|
|
1242
|
-
value: otherActive ?
|
|
1422
|
+
value: otherActive ? otherValue : typeof value === "string" ? value : "",
|
|
1243
1423
|
onChange: (e) => {
|
|
1244
|
-
if (e.target.value ===
|
|
1424
|
+
if (e.target.value === otherValue) {
|
|
1245
1425
|
setOtherOn(true);
|
|
1246
1426
|
setValue("");
|
|
1247
1427
|
} else {
|
|
@@ -1252,7 +1432,7 @@ function Dropdown({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1252
1432
|
children: [
|
|
1253
1433
|
/* @__PURE__ */ jsx4("option", { value: "", children: field.placeholder ?? strings.choosePlaceholder }),
|
|
1254
1434
|
options.map((opt) => /* @__PURE__ */ jsx4("option", { value: opt.id, children: opt.label }, opt.id)),
|
|
1255
|
-
choice.allowOther && /* @__PURE__ */ jsx4("option", { value:
|
|
1435
|
+
choice.allowOther && /* @__PURE__ */ jsx4("option", { value: otherValue, children: `${strings.other}\u2026` })
|
|
1256
1436
|
]
|
|
1257
1437
|
}
|
|
1258
1438
|
),
|
|
@@ -1276,17 +1456,15 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1276
1456
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1277
1457
|
const strings = useStrings();
|
|
1278
1458
|
const appearance = useFilloAppearance();
|
|
1459
|
+
const slots = useFieldSlots(field, error);
|
|
1279
1460
|
const checkbox = field;
|
|
1280
1461
|
if (checkbox.appearance === "toggle") {
|
|
1281
1462
|
return /* @__PURE__ */ jsxs4(
|
|
1282
1463
|
"div",
|
|
1283
1464
|
{
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
"data-kind": field.kind,
|
|
1288
|
-
"data-invalid": error ? "" : void 0,
|
|
1289
|
-
"data-required": field.required ? "" : void 0,
|
|
1465
|
+
...slots.wrapperProps(
|
|
1466
|
+
`fillo-field fillo-field--checkbox fillo-field--toggle${error ? " fillo-field--error" : ""}`
|
|
1467
|
+
),
|
|
1290
1468
|
children: [
|
|
1291
1469
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-toggle", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1292
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: [
|
|
@@ -1308,8 +1486,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1308
1486
|
),
|
|
1309
1487
|
/* @__PURE__ */ jsx4("span", { className: "fillo-toggle-track", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "fillo-toggle-thumb" }) })
|
|
1310
1488
|
] }),
|
|
1311
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1312
|
-
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 })
|
|
1313
1491
|
]
|
|
1314
1492
|
}
|
|
1315
1493
|
);
|
|
@@ -1317,12 +1495,9 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1317
1495
|
return /* @__PURE__ */ jsxs4(
|
|
1318
1496
|
"div",
|
|
1319
1497
|
{
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
"data-kind": field.kind,
|
|
1324
|
-
"data-invalid": error ? "" : void 0,
|
|
1325
|
-
"data-required": field.required ? "" : void 0,
|
|
1498
|
+
...slots.wrapperProps(
|
|
1499
|
+
`fillo-field fillo-field--checkbox${error ? " fillo-field--error" : ""}`
|
|
1500
|
+
),
|
|
1326
1501
|
children: [
|
|
1327
1502
|
/* @__PURE__ */ jsxs4("label", { className: "fillo-option", "data-fillo": "option", "data-checked": value === true ? "" : void 0, children: [
|
|
1328
1503
|
/* @__PURE__ */ jsx4(
|
|
@@ -1341,8 +1516,8 @@ function Checkbox({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1341
1516
|
!field.required && /* @__PURE__ */ jsx4("span", { className: "fillo-optional", children: strings.optional })
|
|
1342
1517
|
] })
|
|
1343
1518
|
] }),
|
|
1344
|
-
field.description && /* @__PURE__ */ jsx4("p", { className: "fillo
|
|
1345
|
-
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 })
|
|
1346
1521
|
]
|
|
1347
1522
|
}
|
|
1348
1523
|
);
|
|
@@ -1488,73 +1663,106 @@ function Ranking({ field, value, error, setValue, ids: providedIds }) {
|
|
|
1488
1663
|
[next[index], next[target]] = [next[target], next[index]];
|
|
1489
1664
|
setValue(next);
|
|
1490
1665
|
}
|
|
1491
|
-
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
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
|
+
) });
|
|
1523
1707
|
}
|
|
1524
1708
|
function Matrix({ field, value, error, setValue, ids: providedIds }) {
|
|
1525
1709
|
const ids = useFilloFieldIds(field.id, providedIds);
|
|
1526
1710
|
const matrix = field;
|
|
1527
1711
|
const answers = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1528
|
-
return /* @__PURE__ */ jsx4(FieldShell, { field, error, ids, children: /* @__PURE__ */ jsx4(
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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
|
+
) });
|
|
1558
1766
|
}
|
|
1559
1767
|
function Hidden() {
|
|
1560
1768
|
return null;
|
|
@@ -1608,8 +1816,8 @@ function blockPropsEqual(prev, next) {
|
|
|
1608
1816
|
const id = next.block.id;
|
|
1609
1817
|
if (prev.api.data[id] !== next.api.data[id]) return false;
|
|
1610
1818
|
if (prev.api.errors[id] !== next.api.errors[id]) return false;
|
|
1611
|
-
if (next.block.kind === "custom") {
|
|
1612
|
-
return prev.api
|
|
1819
|
+
if (next.block.kind === "custom" || isField(next.block) && Boolean(next.components?.[next.block.kind])) {
|
|
1820
|
+
return prev.api === next.api;
|
|
1613
1821
|
}
|
|
1614
1822
|
if ((blockHasPiping(prev.block) || blockHasPiping(next.block)) && prev.api.data !== next.api.data) {
|
|
1615
1823
|
return false;
|
|
@@ -1624,6 +1832,7 @@ function blockPropsEqual(prev, next) {
|
|
|
1624
1832
|
function BlockRendererInner({ block, api, components, customComponents }) {
|
|
1625
1833
|
block = pipeBlock(block, api.data, api.form);
|
|
1626
1834
|
const ids = useFilloFieldIds(block.id);
|
|
1835
|
+
const strings = useStrings();
|
|
1627
1836
|
if (!isField(block)) return /* @__PURE__ */ jsx4(ContentRenderer, { block });
|
|
1628
1837
|
let Component;
|
|
1629
1838
|
if (block.kind === "custom") {
|
|
@@ -1640,12 +1849,14 @@ function BlockRendererInner({ block, api, components, customComponents }) {
|
|
|
1640
1849
|
} else {
|
|
1641
1850
|
Component = components?.[block.kind] ?? DEFAULT_COMPONENTS[block.kind];
|
|
1642
1851
|
}
|
|
1852
|
+
const rawError = api.errors[block.id];
|
|
1853
|
+
const error = rawError === REQUIRED_FIELD_MESSAGE ? strings.required : rawError;
|
|
1643
1854
|
return /* @__PURE__ */ jsx4(
|
|
1644
1855
|
Component,
|
|
1645
1856
|
{
|
|
1646
1857
|
field: block,
|
|
1647
1858
|
value: api.data[block.id],
|
|
1648
|
-
error
|
|
1859
|
+
error,
|
|
1649
1860
|
setValue: (v) => {
|
|
1650
1861
|
api.setValue(block.id, v);
|
|
1651
1862
|
const ctx = {
|
|
@@ -1724,7 +1935,9 @@ function badgeUrl(href) {
|
|
|
1724
1935
|
function focusFirstInvalid(formEl) {
|
|
1725
1936
|
if (!formEl || typeof window === "undefined") return;
|
|
1726
1937
|
const run = () => {
|
|
1727
|
-
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);
|
|
1728
1941
|
target?.focus?.();
|
|
1729
1942
|
};
|
|
1730
1943
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(run);
|
|
@@ -1752,35 +1965,54 @@ function FilloForm(props) {
|
|
|
1752
1965
|
() => props.client ?? (formId ? createClient() : void 0),
|
|
1753
1966
|
[props.client, formId]
|
|
1754
1967
|
);
|
|
1755
|
-
const [
|
|
1756
|
-
const [
|
|
1757
|
-
const [loadError, setLoadError] = useState6(null);
|
|
1758
|
-
const [closed, setClosed] = useState6(false);
|
|
1759
|
-
const [fetchedBranding, setFetchedBranding] = useState6(null);
|
|
1968
|
+
const [loaded, setLoaded] = useState6(null);
|
|
1969
|
+
const [loadFailure, setLoadFailure] = useState6(null);
|
|
1760
1970
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
1761
|
-
const inlineSchema = codeForm ? codeForm.schema : form;
|
|
1762
1971
|
const onError = props.onError;
|
|
1763
|
-
const {
|
|
1764
|
-
|
|
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);
|
|
1765
1990
|
useEffect7(() => {
|
|
1766
1991
|
onErrorRef.current = onError;
|
|
1767
1992
|
});
|
|
1768
1993
|
const needsFetch = !inlineSchema && Boolean(client && formId);
|
|
1769
1994
|
useEffect7(() => {
|
|
1770
|
-
if (!needsFetch || !client || !formId)
|
|
1995
|
+
if (!needsFetch || !client || !formId) {
|
|
1996
|
+
setLoaded(null);
|
|
1997
|
+
setLoadFailure(null);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
1771
2000
|
let cancelled = false;
|
|
1772
|
-
|
|
1773
|
-
|
|
2001
|
+
setLoaded(null);
|
|
2002
|
+
setLoadFailure(null);
|
|
1774
2003
|
client.getForm(formId).then((published) => {
|
|
1775
2004
|
if (cancelled) return;
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
2005
|
+
setLoaded({
|
|
2006
|
+
formId,
|
|
2007
|
+
schema: published.schema,
|
|
2008
|
+
theme: published.theme,
|
|
2009
|
+
closed: Boolean(published.closed),
|
|
2010
|
+
branding: published.branding ?? null
|
|
2011
|
+
});
|
|
1780
2012
|
}).catch((err) => {
|
|
1781
2013
|
if (cancelled) return;
|
|
1782
2014
|
const error = err instanceof FilloError2 ? err : new FilloError2("This form could not be loaded.", 0);
|
|
1783
|
-
|
|
2015
|
+
setLoadFailure({ formId, error });
|
|
1784
2016
|
onErrorRef.current?.(error);
|
|
1785
2017
|
});
|
|
1786
2018
|
return () => {
|
|
@@ -1788,10 +2020,13 @@ function FilloForm(props) {
|
|
|
1788
2020
|
};
|
|
1789
2021
|
}, [needsFetch, client, formId]);
|
|
1790
2022
|
const strings = resolveStrings(props.strings);
|
|
1791
|
-
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;
|
|
1792
2026
|
const normalized = useMemo5(() => rawSchema ? normalizeFormSchema(rawSchema) : null, [rawSchema]);
|
|
1793
2027
|
const schema = normalized?.ok ? normalized.schema : null;
|
|
1794
|
-
const
|
|
2028
|
+
const resolvedCodeTheme = renderResolvedSnapshot ? resolvedTheme ?? void 0 : codeForm?.theme;
|
|
2029
|
+
const theme = props.appearance?.theme ?? props.theme ?? resolvedCodeTheme ?? currentLoaded?.theme ?? void 0;
|
|
1795
2030
|
if (normalized && !normalized.ok) {
|
|
1796
2031
|
const error = new FilloError2(`This form could not be rendered: ${normalized.error}`, 422);
|
|
1797
2032
|
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
@@ -1804,16 +2039,34 @@ function FilloForm(props) {
|
|
|
1804
2039
|
const message = loadError.status === 404 ? strings.loadFailedNotFound : loadError.status === 0 ? strings.loadFailedNetwork : strings.loadFailed;
|
|
1805
2040
|
return /* @__PURE__ */ jsx5("div", { className: `fillo-form fillo-form--error ${props.className ?? ""}`, children: message });
|
|
1806
2041
|
}
|
|
1807
|
-
if (closed) {
|
|
2042
|
+
if (currentLoaded?.closed) {
|
|
1808
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 }) });
|
|
1809
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
|
+
}
|
|
1810
2062
|
const notPublished = Boolean(codeForm) && syncedStatus === "draft" && !props.skipValidation;
|
|
1811
2063
|
if (notPublished && !isDevEnv()) {
|
|
1812
2064
|
const error = new FilloError2("This form isn't published yet.", 403);
|
|
1813
2065
|
if (props.renderError) return /* @__PURE__ */ jsx5(Fragment2, { children: props.renderError(error) });
|
|
1814
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 }) });
|
|
1815
2067
|
}
|
|
1816
|
-
|
|
2068
|
+
const awaitingCanonical = Boolean(codeForm && codeFormSyncing && !props.skipValidation);
|
|
2069
|
+
if (!isDevEnv() && awaitingCanonical || !schema) {
|
|
1817
2070
|
return /* @__PURE__ */ jsxs5(
|
|
1818
2071
|
"div",
|
|
1819
2072
|
{
|
|
@@ -1828,7 +2081,8 @@ function FilloForm(props) {
|
|
|
1828
2081
|
}
|
|
1829
2082
|
);
|
|
1830
2083
|
}
|
|
1831
|
-
const
|
|
2084
|
+
const branding = codeForm ? syncedBranding : currentLoaded?.branding;
|
|
2085
|
+
const poweredBy = branding?.poweredBy ?? true;
|
|
1832
2086
|
return /* @__PURE__ */ jsx5(
|
|
1833
2087
|
ResolvedForm,
|
|
1834
2088
|
{
|
|
@@ -1838,9 +2092,11 @@ function FilloForm(props) {
|
|
|
1838
2092
|
theme,
|
|
1839
2093
|
formId: syncedFormId ?? formId,
|
|
1840
2094
|
poweredBy,
|
|
1841
|
-
brandLabel:
|
|
1842
|
-
brandHref:
|
|
1843
|
-
draftNotice: notPublished
|
|
2095
|
+
brandLabel: branding?.label,
|
|
2096
|
+
brandHref: branding?.href,
|
|
2097
|
+
draftNotice: notPublished,
|
|
2098
|
+
stagedNotice: codeChangesStaged,
|
|
2099
|
+
syncErrorNotice: syncError ?? syncNoticeError ?? void 0
|
|
1844
2100
|
}
|
|
1845
2101
|
);
|
|
1846
2102
|
}
|
|
@@ -1864,18 +2120,85 @@ function DevDraftNotice() {
|
|
|
1864
2120
|
" It renders here, but it won\u2019t accept responses until you publish it in your Fillo dashboard. (This notice only appears in development.)"
|
|
1865
2121
|
] });
|
|
1866
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
|
+
}
|
|
1867
2141
|
function ResolvedForm(props) {
|
|
1868
2142
|
const { schema, theme } = props;
|
|
1869
2143
|
const instanceId = useId2().replace(/:/g, "");
|
|
1870
|
-
const hpRef =
|
|
2144
|
+
const hpRef = useRef6(null);
|
|
1871
2145
|
const codeForm = isCodeForm2(props.form) ? props.form : null;
|
|
1872
2146
|
const client = props.client;
|
|
1873
2147
|
const resolveFormId = useMemo5(
|
|
1874
2148
|
() => codeForm && client?.key ? (
|
|
1875
2149
|
// bypassCache: a stale cached formId must not be trusted at submit time.
|
|
1876
|
-
() =>
|
|
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
|
+
}
|
|
1877
2200
|
) : void 0,
|
|
1878
|
-
[codeForm, client]
|
|
2201
|
+
[codeForm, client, props.onError, schema]
|
|
1879
2202
|
);
|
|
1880
2203
|
const api = useFilloController({
|
|
1881
2204
|
form: schema,
|
|
@@ -1885,26 +2208,29 @@ function ResolvedForm(props) {
|
|
|
1885
2208
|
onChange: props.onChange,
|
|
1886
2209
|
onSubmitted: props.onSubmitted,
|
|
1887
2210
|
skipValidation: props.skipValidation,
|
|
1888
|
-
getHoneypot: () => hpRef.current,
|
|
2211
|
+
getHoneypot: () => hpRef.current?.value ?? "",
|
|
1889
2212
|
// Framed renderer: Fillo draws the layout (and badge), so the surface is
|
|
1890
2213
|
// "default", never the bare-engine "headless".
|
|
1891
2214
|
surface: "default",
|
|
1892
|
-
resolveFormId
|
|
2215
|
+
resolveFormId,
|
|
2216
|
+
respondent: props.respondent
|
|
1893
2217
|
});
|
|
2218
|
+
useDraftFlush(api);
|
|
1894
2219
|
const themed = useMemo5(() => themeProps(theme), [theme]);
|
|
1895
2220
|
const settings = schema.settings;
|
|
1896
2221
|
const submitted = api.status === "submitted";
|
|
2222
|
+
const liveSubmit = submitted && !api.restoredSubmission;
|
|
1897
2223
|
useEffect7(() => {
|
|
1898
|
-
if (!
|
|
2224
|
+
if (!liveSubmit || !settings.redirectUrl || typeof window === "undefined") return;
|
|
1899
2225
|
const url = safeHttpUrl(settings.redirectUrl);
|
|
1900
2226
|
if (url) window.location.assign(url);
|
|
1901
|
-
}, [
|
|
1902
|
-
const successRef =
|
|
2227
|
+
}, [liveSubmit, settings.redirectUrl]);
|
|
2228
|
+
const successRef = useRef6(null);
|
|
1903
2229
|
useEffect7(() => {
|
|
1904
|
-
if (
|
|
1905
|
-
}, [
|
|
1906
|
-
const blocksRef =
|
|
1907
|
-
const prevPageIndex =
|
|
2230
|
+
if (liveSubmit) successRef.current?.focus();
|
|
2231
|
+
}, [liveSubmit]);
|
|
2232
|
+
const blocksRef = useRef6(null);
|
|
2233
|
+
const prevPageIndex = useRef6(api.pageIndex);
|
|
1908
2234
|
useEffect7(() => {
|
|
1909
2235
|
if (prevPageIndex.current === api.pageIndex) return;
|
|
1910
2236
|
prevPageIndex.current = api.pageIndex;
|
|
@@ -1937,7 +2263,7 @@ function ResolvedForm(props) {
|
|
|
1937
2263
|
children: [
|
|
1938
2264
|
/* @__PURE__ */ jsx5("div", { className: "fillo-success-mark", "aria-hidden": "true", children: "\u2713" }),
|
|
1939
2265
|
/* @__PURE__ */ jsx5("h2", { className: "fillo-success-title", children: settings.successTitle ?? strings.successTitle }),
|
|
1940
|
-
/* @__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 })
|
|
1941
2267
|
]
|
|
1942
2268
|
}
|
|
1943
2269
|
),
|
|
@@ -1977,13 +2303,37 @@ function ResolvedForm(props) {
|
|
|
1977
2303
|
children: [
|
|
1978
2304
|
!props.client && !props.skipValidation && isDevEnv() && /* @__PURE__ */ jsx5(DevNoClientNotice, {}),
|
|
1979
2305
|
props.draftNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevDraftNotice, {}),
|
|
2306
|
+
props.stagedNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevStagedNotice, {}),
|
|
2307
|
+
props.syncErrorNotice && isDevEnv() && /* @__PURE__ */ jsx5(DevSyncErrorNotice, { error: props.syncErrorNotice }),
|
|
2308
|
+
(api.resumedDraft || api.editingPrevious) && /* @__PURE__ */ jsxs5(
|
|
2309
|
+
"div",
|
|
2310
|
+
{
|
|
2311
|
+
className: slotClass3("fillo-resume", appearance, { slot: "resume" }),
|
|
2312
|
+
"data-fillo": "resume",
|
|
2313
|
+
role: "status",
|
|
2314
|
+
children: [
|
|
2315
|
+
/* @__PURE__ */ jsx5("span", { className: "fillo-resume-text", children: api.editingPrevious ? strings.editNotice : strings.resumeNotice }),
|
|
2316
|
+
/* @__PURE__ */ jsx5("button", { type: "button", className: "fillo-resume-clear", onClick: api.resetDraft, children: strings.resumeStartOver })
|
|
2317
|
+
]
|
|
2318
|
+
}
|
|
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
|
+
),
|
|
1980
2329
|
multiPage && settings.showProgress !== false && /* @__PURE__ */ jsx5(
|
|
1981
2330
|
"div",
|
|
1982
2331
|
{
|
|
1983
2332
|
className: slotClass3("fillo-progress-track", appearance, { slot: "progress" }),
|
|
1984
2333
|
"data-fillo": "progress",
|
|
1985
2334
|
role: "progressbar",
|
|
1986
|
-
"aria-
|
|
2335
|
+
"aria-label": schema.title || "Form progress",
|
|
2336
|
+
"aria-valuemin": 1,
|
|
1987
2337
|
"aria-valuemax": api.pageCount,
|
|
1988
2338
|
"aria-valuenow": api.pageIndex + 1,
|
|
1989
2339
|
children: /* @__PURE__ */ jsx5(
|
|
@@ -1997,7 +2347,7 @@ function ResolvedForm(props) {
|
|
|
1997
2347
|
}
|
|
1998
2348
|
),
|
|
1999
2349
|
api.pageIndex === 0 && props.showTitle !== false && /* @__PURE__ */ jsxs5("header", { className: slotClass3("fillo-header", appearance, { slot: "header" }), "data-fillo": "header", children: [
|
|
2000
|
-
/* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
|
|
2350
|
+
schema.title && /* @__PURE__ */ jsx5("h1", { className: slotClass3("fillo-title", appearance, { slot: "title" }), "data-fillo": "title", children: schema.title }),
|
|
2001
2351
|
schema.description && /* @__PURE__ */ jsx5(
|
|
2002
2352
|
"p",
|
|
2003
2353
|
{
|
|
@@ -2029,9 +2379,9 @@ function ResolvedForm(props) {
|
|
|
2029
2379
|
"aria-hidden": "true",
|
|
2030
2380
|
defaultValue: "",
|
|
2031
2381
|
style: HP_STYLE,
|
|
2032
|
-
ref:
|
|
2033
|
-
|
|
2034
|
-
|
|
2382
|
+
ref: (element) => {
|
|
2383
|
+
hpRef.current = element;
|
|
2384
|
+
assertHpHidden(element);
|
|
2035
2385
|
}
|
|
2036
2386
|
}
|
|
2037
2387
|
),
|
|
@@ -2065,7 +2415,7 @@ function ResolvedForm(props) {
|
|
|
2065
2415
|
}
|
|
2066
2416
|
|
|
2067
2417
|
// src/jsx.tsx
|
|
2068
|
-
import { useRef as
|
|
2418
|
+
import { useRef as useRef7 } from "react";
|
|
2069
2419
|
import {
|
|
2070
2420
|
codeFormFromJsx,
|
|
2071
2421
|
FilloJsxError,
|
|
@@ -2078,7 +2428,7 @@ function JsxForm(props) {
|
|
|
2078
2428
|
const { id, title, description, settings, theme, client, children, ...rest } = props;
|
|
2079
2429
|
const compiled = codeFormFromJsx({ id, title, description, settings, theme }, children);
|
|
2080
2430
|
const json = JSON.stringify(compiled.schema) + JSON.stringify(compiled.theme ?? null);
|
|
2081
|
-
const memo2 =
|
|
2431
|
+
const memo2 = useRef7(null);
|
|
2082
2432
|
if (memo2.current && memo2.current.json !== json && isDevEnv2()) {
|
|
2083
2433
|
console.warn(
|
|
2084
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.`
|
|
@@ -2132,29 +2482,121 @@ var Fillo = {
|
|
|
2132
2482
|
|
|
2133
2483
|
// src/provider.tsx
|
|
2134
2484
|
import { useId as useId3, useMemo as useMemo6 } from "react";
|
|
2135
|
-
import {
|
|
2136
|
-
|
|
2137
|
-
|
|
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
|
+
}) {
|
|
2138
2505
|
const instanceId = useId3().replace(/:/g, "");
|
|
2139
2506
|
const codeForm = isCodeForm2(form) ? form : null;
|
|
2140
|
-
const
|
|
2141
|
-
|
|
2142
|
-
|
|
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]);
|
|
2143
2518
|
if (!normalized.ok) {
|
|
2144
2519
|
throw new FilloError3(`This form could not be rendered: ${normalized.error}`, 422);
|
|
2145
2520
|
}
|
|
2146
2521
|
const schema = normalized.schema;
|
|
2147
|
-
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
|
+
);
|
|
2148
2575
|
const api = useFilloController({
|
|
2149
2576
|
...options,
|
|
2150
2577
|
client,
|
|
2151
2578
|
form: schema,
|
|
2152
2579
|
formId: syncedFormId ?? formId,
|
|
2580
|
+
resolveFormId,
|
|
2153
2581
|
// Headless = your markup, no Fillo-rendered layout. Free like every embed
|
|
2154
2582
|
// method; recorded per response so usage stays measurable.
|
|
2155
2583
|
surface: "headless"
|
|
2156
2584
|
});
|
|
2157
|
-
|
|
2585
|
+
useDraftFlush(api);
|
|
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";
|
|
2158
2600
|
}
|
|
2159
2601
|
|
|
2160
2602
|
// src/index.ts
|