@10x-media/form-builder 0.1.0-beta.2 → 0.1.0-beta.3
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/CHANGELOG.md +18 -0
- package/README.md +2 -2
- package/dist/actions/body/converters.d.ts +20 -0
- package/dist/actions/body/converters.js +57 -0
- package/dist/actions/body/converters.js.map +1 -0
- package/dist/actions/body/escapeHtml.d.ts +6 -0
- package/dist/actions/body/escapeHtml.js +14 -0
- package/dist/actions/body/escapeHtml.js.map +1 -0
- package/dist/actions/body/serializeBody.d.ts +35 -0
- package/dist/actions/body/serializeBody.js +76 -0
- package/dist/actions/body/serializeBody.js.map +1 -0
- package/dist/actions/body/serializeSlate.js +31 -0
- package/dist/actions/body/serializeSlate.js.map +1 -0
- package/dist/actions/body/wildcards.d.ts +10 -0
- package/dist/actions/body/wildcards.js +33 -0
- package/dist/actions/body/wildcards.js.map +1 -0
- package/dist/actions/builtin/confirmation.js +28 -12
- package/dist/actions/builtin/confirmation.js.map +1 -1
- package/dist/actions/builtin/emailTeam.js +7 -9
- package/dist/actions/builtin/emailTeam.js.map +1 -1
- package/dist/actions/builtin/signedWebhook.js +21 -3
- package/dist/actions/builtin/signedWebhook.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -1
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/dispatch.js +2 -1
- package/dist/actions/dispatch.js.map +1 -1
- package/dist/actions/runActions.js +8 -1
- package/dist/actions/runActions.js.map +1 -1
- package/dist/actions/task.js +7 -5
- package/dist/actions/task.js.map +1 -1
- package/dist/calc/normalizeCalc.js +5 -17
- package/dist/calc/normalizeCalc.js.map +1 -1
- package/dist/calc/types.d.ts +8 -2
- package/dist/calc/types.js +22 -0
- package/dist/calc/types.js.map +1 -0
- package/dist/client/ByteSizeField.d.ts +21 -0
- package/dist/client/ByteSizeField.js +56 -0
- package/dist/client/ByteSizeField.js.map +1 -0
- package/dist/client/ConditionBuilder.js +2 -1
- package/dist/client/ConditionBuilder.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +20 -0
- package/dist/client/FieldNameSelect.js +58 -0
- package/dist/client/FieldNameSelect.js.map +1 -0
- package/dist/client/FlowBuilder.d.ts +0 -1
- package/dist/client/FlowBuilder.js +4 -6
- package/dist/client/FlowBuilder.js.map +1 -1
- package/dist/client/FormConditionField.js +1 -4
- package/dist/client/FormConditionField.js.map +1 -1
- package/dist/client/admin.css +192 -0
- package/dist/client/toStaticLabel.js +13 -0
- package/dist/client/toStaticLabel.js.map +1 -0
- package/dist/collections/formSubmissions.js +5 -3
- package/dist/collections/formSubmissions.js.map +1 -1
- package/dist/collections/forms.js +74 -57
- package/dist/collections/forms.js.map +1 -1
- package/dist/exports/client.d.ts +3 -1
- package/dist/exports/client.js +3 -1
- package/dist/exports/react.d.ts +6 -1
- package/dist/exports/react.js +5 -1
- package/dist/fields/buildFieldBlocks.js +14 -18
- package/dist/fields/buildFieldBlocks.js.map +1 -1
- package/dist/fields/builtin/calculation.js +115 -2
- package/dist/fields/builtin/calculation.js.map +1 -1
- package/dist/fields/builtin/date.js +29 -0
- package/dist/fields/builtin/date.js.map +1 -0
- package/dist/fields/builtin/file.d.ts +15 -0
- package/dist/fields/builtin/file.js +79 -3
- package/dist/fields/builtin/file.js.map +1 -1
- package/dist/fields/builtin/index.d.ts +14 -0
- package/dist/fields/builtin/index.js +11 -2
- package/dist/fields/builtin/index.js.map +1 -1
- package/dist/fields/fieldNamesOfType.js +25 -0
- package/dist/fields/fieldNamesOfType.js.map +1 -0
- package/dist/fields/sharedConfig.js +30 -18
- package/dist/fields/sharedConfig.js.map +1 -1
- package/dist/index.d.ts +19 -2
- package/dist/index.js +14 -4
- package/dist/index.js.map +1 -1
- package/dist/plugin/registerCollections.js +3 -2
- package/dist/plugin/registerCollections.js.map +1 -1
- package/dist/react/Form.js +3 -3
- package/dist/react/Form.js.map +1 -1
- package/dist/react/Poll.js +1 -1
- package/dist/react/captcha/HcaptchaCaptcha.d.ts +28 -0
- package/dist/react/captcha/HcaptchaCaptcha.js +53 -0
- package/dist/react/captcha/HcaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/RecaptchaCaptcha.d.ts +41 -0
- package/dist/react/captcha/RecaptchaCaptcha.js +93 -0
- package/dist/react/captcha/RecaptchaCaptcha.js.map +1 -0
- package/dist/react/captcha/TurnstileCaptcha.d.ts +28 -0
- package/dist/react/captcha/TurnstileCaptcha.js +53 -0
- package/dist/react/captcha/TurnstileCaptcha.js.map +1 -0
- package/dist/react/captcha/types.d.ts +8 -0
- package/dist/react/captcha/useCaptchaScript.js +45 -0
- package/dist/react/captcha/useCaptchaScript.js.map +1 -0
- package/dist/react/captcha/vendors.js +8 -0
- package/dist/react/captcha/vendors.js.map +1 -0
- package/dist/react/primitives/Input.d.ts +1 -1
- package/dist/react/primitives/Input.js.map +1 -1
- package/dist/react/renderers/calculation.js +1 -1
- package/dist/react/renderers/checkbox.js +1 -1
- package/dist/react/renderers/consent.js +1 -1
- package/dist/react/renderers/date.js +37 -0
- package/dist/react/renderers/date.js.map +1 -0
- package/dist/react/renderers/email.js +1 -1
- package/dist/react/renderers/file.js +20 -7
- package/dist/react/renderers/file.js.map +1 -1
- package/dist/react/renderers/index.js +2 -0
- package/dist/react/renderers/index.js.map +1 -1
- package/dist/react/renderers/number.js +1 -1
- package/dist/react/renderers/repeater.js +1 -1
- package/dist/react/renderers/select.js +1 -1
- package/dist/react/renderers/text.js +1 -1
- package/dist/react/renderers/textarea.js +1 -1
- package/dist/spam/captcha.d.ts +3 -2
- package/dist/spam/captcha.js +3 -2
- package/dist/spam/captcha.js.map +1 -1
- package/dist/spam/clientIp.js +9 -0
- package/dist/spam/clientIp.js.map +1 -0
- package/dist/spam/identify.js +3 -6
- package/dist/spam/identify.js.map +1 -1
- package/dist/spam/providers/hcaptcha.d.ts +17 -0
- package/dist/spam/providers/hcaptcha.js +18 -0
- package/dist/spam/providers/hcaptcha.js.map +1 -0
- package/dist/spam/providers/recaptcha.d.ts +23 -0
- package/dist/spam/providers/recaptcha.js +23 -0
- package/dist/spam/providers/recaptcha.js.map +1 -0
- package/dist/spam/providers/siteverify.js +34 -0
- package/dist/spam/providers/siteverify.js.map +1 -0
- package/dist/spam/providers/turnstile.d.ts +17 -0
- package/dist/spam/providers/turnstile.js +18 -0
- package/dist/spam/providers/turnstile.js.map +1 -0
- package/dist/spam/spamGuard.js +2 -5
- package/dist/spam/spamGuard.js.map +1 -1
- package/dist/spam/types.d.ts +2 -2
- package/dist/translations/en.js +26 -4
- package/dist/translations/en.js.map +1 -1
- package/dist/translations/keys.d.ts +26 -4
- package/dist/translations/keys.js +26 -4
- package/dist/translations/keys.js.map +1 -1
- package/dist/uploads/formatBytes.d.ts +9 -0
- package/dist/uploads/formatBytes.js +25 -0
- package/dist/uploads/formatBytes.js.map +1 -0
- package/dist/uploads/resolveFileRef.js +1 -1
- package/dist/uploads/resolveFileRef.js.map +1 -1
- package/dist/validation/builtin/index.d.ts +14 -0
- package/dist/validation/builtin/index.js +12 -1
- package/dist/validation/builtin/index.js.map +1 -1
- package/dist/validation/builtin/maxDate.js +23 -0
- package/dist/validation/builtin/maxDate.js.map +1 -0
- package/dist/validation/builtin/minDate.js +23 -0
- package/dist/validation/builtin/minDate.js.map +1 -0
- package/dist/validation/builtin/validateDateBound.js +18 -0
- package/dist/validation/builtin/validateDateBound.js.map +1 -0
- package/package.json +3 -2
- package/styles/form-builder.css +0 -190
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCaptchaScript } from "./useCaptchaScript.js";
|
|
3
|
+
import { getHcaptcha } from "./vendors.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useEffect, useImperativeHandle, useRef } from "react";
|
|
6
|
+
//#region src/react/captcha/HcaptchaCaptcha.tsx
|
|
7
|
+
const HCAPTCHA_SCRIPT_URL = "https://js.hcaptcha.com/1/api.js?render=explicit";
|
|
8
|
+
/**
|
|
9
|
+
* Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,
|
|
10
|
+
* and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
|
|
11
|
+
*/
|
|
12
|
+
const HcaptchaCaptcha = ({ siteKey, onToken, options, className, scriptSrc = HCAPTCHA_SCRIPT_URL, ref }) => {
|
|
13
|
+
const containerRef = useRef(null);
|
|
14
|
+
const widgetIdRef = useRef(null);
|
|
15
|
+
const onTokenRef = useRef(onToken);
|
|
16
|
+
onTokenRef.current = onToken;
|
|
17
|
+
const optionsRef = useRef(options);
|
|
18
|
+
optionsRef.current = options;
|
|
19
|
+
const ready = useCaptchaScript(scriptSrc);
|
|
20
|
+
useImperativeHandle(ref, () => ({ reset: () => {
|
|
21
|
+
const api = getHcaptcha();
|
|
22
|
+
if (api && widgetIdRef.current !== null) api.reset(widgetIdRef.current);
|
|
23
|
+
onTokenRef.current(null);
|
|
24
|
+
} }), []);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!ready) return;
|
|
27
|
+
const container = containerRef.current;
|
|
28
|
+
const api = getHcaptcha();
|
|
29
|
+
if (!container || !api) return;
|
|
30
|
+
const widgetId = api.render(container, {
|
|
31
|
+
...optionsRef.current,
|
|
32
|
+
sitekey: siteKey,
|
|
33
|
+
callback: (token) => onTokenRef.current(token),
|
|
34
|
+
"expired-callback": () => onTokenRef.current(null),
|
|
35
|
+
"error-callback": () => onTokenRef.current(null)
|
|
36
|
+
});
|
|
37
|
+
widgetIdRef.current = widgetId;
|
|
38
|
+
return () => {
|
|
39
|
+
widgetIdRef.current = null;
|
|
40
|
+
try {
|
|
41
|
+
api.remove(widgetId);
|
|
42
|
+
} catch {}
|
|
43
|
+
};
|
|
44
|
+
}, [ready, siteKey]);
|
|
45
|
+
return /* @__PURE__ */ jsx("div", {
|
|
46
|
+
ref: containerRef,
|
|
47
|
+
className
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { HCAPTCHA_SCRIPT_URL, HcaptchaCaptcha };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=HcaptchaCaptcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"HcaptchaCaptcha.js","names":[],"sources":["../../../src/react/captcha/HcaptchaCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getHcaptcha } from './vendors'\n\nexport const HCAPTCHA_SCRIPT_URL = 'https://js.hcaptcha.com/1/api.js?render=explicit'\n\nexport type HcaptchaCaptchaProps = {\n\t/** hCaptcha site key (public). */\n\tsiteKey: string\n\t/** Receives each fresh token, and null when the token expires or errors. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params (theme, size, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL. */\n\tscriptSrc?: string\n\tref?: Ref<CaptchaWidgetHandle>\n}\n\n/**\n * Headless hCaptcha widget: loads the vendor script once, renders the widget into a bare div,\n * and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.\n */\nexport const HcaptchaCaptcha = ({\n\tsiteKey,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc = HCAPTCHA_SCRIPT_URL,\n\tref,\n}: HcaptchaCaptchaProps) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<string | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst ready = useCaptchaScript(scriptSrc)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getHcaptcha()\n\t\t\t\tif (api && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t}),\n\t\t[]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst container = containerRef.current\n\t\tconst api = getHcaptcha()\n\t\tif (!container || !api) {\n\t\t\treturn\n\t\t}\n\t\tconst widgetId = api.render(container, {\n\t\t\t...optionsRef.current,\n\t\t\tsitekey: siteKey,\n\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t})\n\t\twidgetIdRef.current = widgetId\n\t\treturn () => {\n\t\t\twidgetIdRef.current = null\n\t\t\ttry {\n\t\t\t\tapi.remove(widgetId)\n\t\t\t} catch {}\n\t\t}\n\t}, [ready, siteKey])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;AAOA,MAAa,sBAAsB;;;;;AAmBnC,MAAa,mBAAmB,EAC/B,SACA,SACA,SACA,WACA,YAAY,qBACZ,UAC2B;CAC3B,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,cAAc,OAAsB,IAAI;CAC9C,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,QAAQ,iBAAiB,SAAS;CAExC,oBACC,YACO,EACN,aAAa;EACZ,MAAM,MAAM,YAAY;EACxB,IAAI,OAAO,YAAY,YAAY,MAClC,IAAI,MAAM,YAAY,OAAO;EAE9B,WAAW,QAAQ,IAAI;CACxB,EACD,IACA,CAAC,CACF;CAEA,gBAAgB;EACf,IAAI,CAAC,OACJ;EAED,MAAM,YAAY,aAAa;EAC/B,MAAM,MAAM,YAAY;EACxB,IAAI,CAAC,aAAa,CAAC,KAClB;EAED,MAAM,WAAW,IAAI,OAAO,WAAW;GACtC,GAAG,WAAW;GACd,SAAS;GACT,WAAW,UAAkB,WAAW,QAAQ,KAAK;GACrD,0BAA0B,WAAW,QAAQ,IAAI;GACjD,wBAAwB,WAAW,QAAQ,IAAI;EAChD,CAAC;EACD,YAAY,UAAU;EACtB,aAAa;GACZ,YAAY,UAAU;GACtB,IAAI;IACH,IAAI,OAAO,QAAQ;GACpB,QAAQ,CAAC;EACV;CACD,GAAG,CAAC,OAAO,OAAO,CAAC;CAEnB,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAyB;CAAY,CAAA;AACvD"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { CaptchaWidgetHandle } from "./types.js";
|
|
2
|
+
import { Ref } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/captcha/RecaptchaCaptcha.d.ts
|
|
5
|
+
declare const RECAPTCHA_SCRIPT_URL = "https://www.google.com/recaptcha/api.js";
|
|
6
|
+
type RecaptchaCaptchaHandle = CaptchaWidgetHandle & {
|
|
7
|
+
/**
|
|
8
|
+
* v3 only: fetch a fresh token on demand and report it through `onToken`. v3 tokens expire
|
|
9
|
+
* after about two minutes, so call this right before submit for a long-lived form. Resolves
|
|
10
|
+
* null for v2 or when execution fails.
|
|
11
|
+
*/
|
|
12
|
+
execute: (action?: string) => Promise<string | null>;
|
|
13
|
+
};
|
|
14
|
+
type RecaptchaCaptchaProps = {
|
|
15
|
+
/** reCAPTCHA site key (public). */siteKey: string; /** 'v2' renders the checkbox widget; 'v3' runs invisibly and produces score-based tokens. */
|
|
16
|
+
version: 'v2' | 'v3'; /** v3 action name reported to reCAPTCHA. Default 'submit'. */
|
|
17
|
+
action?: string; /** Receives each fresh token, and null when the token expires, errors, or is reset. */
|
|
18
|
+
onToken: (token: string | null) => void; /** Extra vendor render params for v2 (theme, size, ...). */
|
|
19
|
+
options?: Record<string, unknown>;
|
|
20
|
+
className?: string; /** Override the vendor script URL (the version-specific `render` query is not appended). */
|
|
21
|
+
scriptSrc?: string;
|
|
22
|
+
ref?: Ref<RecaptchaCaptchaHandle>;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Headless Google reCAPTCHA widget for v2 (checkbox) and v3 (invisible, score-based). v2 reports
|
|
26
|
+
* tokens as the user solves the challenge; v3 fetches an initial token on mount and exposes
|
|
27
|
+
* `execute` on the ref for on-demand refresh. Pass the latest token to `<Form captchaToken>`.
|
|
28
|
+
*/
|
|
29
|
+
declare const RecaptchaCaptcha: ({
|
|
30
|
+
siteKey,
|
|
31
|
+
version,
|
|
32
|
+
action,
|
|
33
|
+
onToken,
|
|
34
|
+
options,
|
|
35
|
+
className,
|
|
36
|
+
scriptSrc,
|
|
37
|
+
ref
|
|
38
|
+
}: RecaptchaCaptchaProps) => import("react/jsx-runtime").JSX.Element;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { RECAPTCHA_SCRIPT_URL, RecaptchaCaptcha, RecaptchaCaptchaHandle, RecaptchaCaptchaProps };
|
|
41
|
+
//# sourceMappingURL=RecaptchaCaptcha.d.ts.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCaptchaScript } from "./useCaptchaScript.js";
|
|
3
|
+
import { getGrecaptcha } from "./vendors.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useCallback, useEffect, useImperativeHandle, useRef } from "react";
|
|
6
|
+
//#region src/react/captcha/RecaptchaCaptcha.tsx
|
|
7
|
+
const RECAPTCHA_SCRIPT_URL = "https://www.google.com/recaptcha/api.js";
|
|
8
|
+
/**
|
|
9
|
+
* Headless Google reCAPTCHA widget for v2 (checkbox) and v3 (invisible, score-based). v2 reports
|
|
10
|
+
* tokens as the user solves the challenge; v3 fetches an initial token on mount and exposes
|
|
11
|
+
* `execute` on the ref for on-demand refresh. Pass the latest token to `<Form captchaToken>`.
|
|
12
|
+
*/
|
|
13
|
+
const RecaptchaCaptcha = ({ siteKey, version, action, onToken, options, className, scriptSrc, ref }) => {
|
|
14
|
+
const src = scriptSrc ?? `https://www.google.com/recaptcha/api.js?render=${version === "v3" ? siteKey : "explicit"}`;
|
|
15
|
+
const containerRef = useRef(null);
|
|
16
|
+
const widgetIdRef = useRef(null);
|
|
17
|
+
const onTokenRef = useRef(onToken);
|
|
18
|
+
onTokenRef.current = onToken;
|
|
19
|
+
const optionsRef = useRef(options);
|
|
20
|
+
optionsRef.current = options;
|
|
21
|
+
const actionRef = useRef(action);
|
|
22
|
+
actionRef.current = action;
|
|
23
|
+
const ready = useCaptchaScript(src);
|
|
24
|
+
const execute = useCallback(async (overrideAction) => {
|
|
25
|
+
const api = getGrecaptcha();
|
|
26
|
+
if (!api || version !== "v3") return null;
|
|
27
|
+
try {
|
|
28
|
+
const token = await api.execute(siteKey, { action: overrideAction ?? actionRef.current ?? "submit" });
|
|
29
|
+
onTokenRef.current(token);
|
|
30
|
+
return token;
|
|
31
|
+
} catch {
|
|
32
|
+
onTokenRef.current(null);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}, [siteKey, version]);
|
|
36
|
+
useImperativeHandle(ref, () => ({
|
|
37
|
+
reset: () => {
|
|
38
|
+
const api = getGrecaptcha();
|
|
39
|
+
if (api && version === "v2" && widgetIdRef.current !== null) api.reset(widgetIdRef.current);
|
|
40
|
+
onTokenRef.current(null);
|
|
41
|
+
},
|
|
42
|
+
execute
|
|
43
|
+
}), [execute, version]);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!ready) return;
|
|
46
|
+
const api = getGrecaptcha();
|
|
47
|
+
if (!api) return;
|
|
48
|
+
let active = true;
|
|
49
|
+
if (version === "v3") {
|
|
50
|
+
api.ready(() => {
|
|
51
|
+
if (active) execute();
|
|
52
|
+
});
|
|
53
|
+
return () => {
|
|
54
|
+
active = false;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const container = containerRef.current;
|
|
58
|
+
if (!container) return;
|
|
59
|
+
api.ready(() => {
|
|
60
|
+
if (!active || widgetIdRef.current !== null) return;
|
|
61
|
+
widgetIdRef.current = api.render(container, {
|
|
62
|
+
...optionsRef.current,
|
|
63
|
+
sitekey: siteKey,
|
|
64
|
+
callback: (token) => onTokenRef.current(token),
|
|
65
|
+
"expired-callback": () => onTokenRef.current(null),
|
|
66
|
+
"error-callback": () => onTokenRef.current(null)
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
return () => {
|
|
70
|
+
active = false;
|
|
71
|
+
if (widgetIdRef.current !== null) {
|
|
72
|
+
try {
|
|
73
|
+
api.reset(widgetIdRef.current);
|
|
74
|
+
} catch {}
|
|
75
|
+
widgetIdRef.current = null;
|
|
76
|
+
}
|
|
77
|
+
container.replaceChildren();
|
|
78
|
+
};
|
|
79
|
+
}, [
|
|
80
|
+
ready,
|
|
81
|
+
siteKey,
|
|
82
|
+
version,
|
|
83
|
+
execute
|
|
84
|
+
]);
|
|
85
|
+
return /* @__PURE__ */ jsx("div", {
|
|
86
|
+
ref: containerRef,
|
|
87
|
+
className
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
//#endregion
|
|
91
|
+
export { RECAPTCHA_SCRIPT_URL, RecaptchaCaptcha };
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=RecaptchaCaptcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RecaptchaCaptcha.js","names":[],"sources":["../../../src/react/captcha/RecaptchaCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useCallback, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getGrecaptcha } from './vendors'\n\nexport const RECAPTCHA_SCRIPT_URL = 'https://www.google.com/recaptcha/api.js'\n\nexport type RecaptchaCaptchaHandle = CaptchaWidgetHandle & {\n\t/**\n\t * v3 only: fetch a fresh token on demand and report it through `onToken`. v3 tokens expire\n\t * after about two minutes, so call this right before submit for a long-lived form. Resolves\n\t * null for v2 or when execution fails.\n\t */\n\texecute: (action?: string) => Promise<string | null>\n}\n\nexport type RecaptchaCaptchaProps = {\n\t/** reCAPTCHA site key (public). */\n\tsiteKey: string\n\t/** 'v2' renders the checkbox widget; 'v3' runs invisibly and produces score-based tokens. */\n\tversion: 'v2' | 'v3'\n\t/** v3 action name reported to reCAPTCHA. Default 'submit'. */\n\taction?: string\n\t/** Receives each fresh token, and null when the token expires, errors, or is reset. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params for v2 (theme, size, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL (the version-specific `render` query is not appended). */\n\tscriptSrc?: string\n\tref?: Ref<RecaptchaCaptchaHandle>\n}\n\n/**\n * Headless Google reCAPTCHA widget for v2 (checkbox) and v3 (invisible, score-based). v2 reports\n * tokens as the user solves the challenge; v3 fetches an initial token on mount and exposes\n * `execute` on the ref for on-demand refresh. Pass the latest token to `<Form captchaToken>`.\n */\nexport const RecaptchaCaptcha = ({\n\tsiteKey,\n\tversion,\n\taction,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc,\n\tref,\n}: RecaptchaCaptchaProps) => {\n\tconst src =\n\t\tscriptSrc ?? `${RECAPTCHA_SCRIPT_URL}?render=${version === 'v3' ? siteKey : 'explicit'}`\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<number | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst actionRef = useRef(action)\n\tactionRef.current = action\n\tconst ready = useCaptchaScript(src)\n\n\tconst execute = useCallback(\n\t\tasync (overrideAction?: string): Promise<string | null> => {\n\t\t\tconst api = getGrecaptcha()\n\t\t\tif (!api || version !== 'v3') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tconst token = await api.execute(siteKey, {\n\t\t\t\t\taction: overrideAction ?? actionRef.current ?? 'submit',\n\t\t\t\t})\n\t\t\t\tonTokenRef.current(token)\n\t\t\t\treturn token\n\t\t\t} catch {\n\t\t\t\tonTokenRef.current(null)\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t\t[siteKey, version]\n\t)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getGrecaptcha()\n\t\t\t\tif (api && version === 'v2' && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t\texecute,\n\t\t}),\n\t\t[execute, version]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst api = getGrecaptcha()\n\t\tif (!api) {\n\t\t\treturn\n\t\t}\n\t\tlet active = true\n\t\tif (version === 'v3') {\n\t\t\tapi.ready(() => {\n\t\t\t\tif (active) {\n\t\t\t\t\tvoid execute()\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn () => {\n\t\t\t\tactive = false\n\t\t\t}\n\t\t}\n\t\tconst container = containerRef.current\n\t\tif (!container) {\n\t\t\treturn\n\t\t}\n\t\tapi.ready(() => {\n\t\t\tif (!active || widgetIdRef.current !== null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\twidgetIdRef.current = api.render(container, {\n\t\t\t\t...optionsRef.current,\n\t\t\t\tsitekey: siteKey,\n\t\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t\t})\n\t\t})\n\t\treturn () => {\n\t\t\tactive = false\n\t\t\tif (widgetIdRef.current !== null) {\n\t\t\t\ttry {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t} catch {}\n\t\t\t\twidgetIdRef.current = null\n\t\t\t}\n\t\t\t// grecaptcha has no remove(); clearing the container drops the detached widget DOM.\n\t\t\tcontainer.replaceChildren()\n\t\t}\n\t}, [ready, siteKey, version, execute])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;AAOA,MAAa,uBAAuB;;;;;;AAiCpC,MAAa,oBAAoB,EAChC,SACA,SACA,QACA,SACA,SACA,WACA,WACA,UAC4B;CAC5B,MAAM,MACL,aAAa,kDAAkC,YAAY,OAAO,UAAU;CAC7E,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,cAAc,OAAsB,IAAI;CAC9C,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,YAAY,OAAO,MAAM;CAC/B,UAAU,UAAU;CACpB,MAAM,QAAQ,iBAAiB,GAAG;CAElC,MAAM,UAAU,YACf,OAAO,mBAAoD;EAC1D,MAAM,MAAM,cAAc;EAC1B,IAAI,CAAC,OAAO,YAAY,MACvB,OAAO;EAER,IAAI;GACH,MAAM,QAAQ,MAAM,IAAI,QAAQ,SAAS,EACxC,QAAQ,kBAAkB,UAAU,WAAW,SAChD,CAAC;GACD,WAAW,QAAQ,KAAK;GACxB,OAAO;EACR,QAAQ;GACP,WAAW,QAAQ,IAAI;GACvB,OAAO;EACR;CACD,GACA,CAAC,SAAS,OAAO,CAClB;CAEA,oBACC,YACO;EACN,aAAa;GACZ,MAAM,MAAM,cAAc;GAC1B,IAAI,OAAO,YAAY,QAAQ,YAAY,YAAY,MACtD,IAAI,MAAM,YAAY,OAAO;GAE9B,WAAW,QAAQ,IAAI;EACxB;EACA;CACD,IACA,CAAC,SAAS,OAAO,CAClB;CAEA,gBAAgB;EACf,IAAI,CAAC,OACJ;EAED,MAAM,MAAM,cAAc;EAC1B,IAAI,CAAC,KACJ;EAED,IAAI,SAAS;EACb,IAAI,YAAY,MAAM;GACrB,IAAI,YAAY;IACf,IAAI,QACH,QAAa;GAEf,CAAC;GACD,aAAa;IACZ,SAAS;GACV;EACD;EACA,MAAM,YAAY,aAAa;EAC/B,IAAI,CAAC,WACJ;EAED,IAAI,YAAY;GACf,IAAI,CAAC,UAAU,YAAY,YAAY,MACtC;GAED,YAAY,UAAU,IAAI,OAAO,WAAW;IAC3C,GAAG,WAAW;IACd,SAAS;IACT,WAAW,UAAkB,WAAW,QAAQ,KAAK;IACrD,0BAA0B,WAAW,QAAQ,IAAI;IACjD,wBAAwB,WAAW,QAAQ,IAAI;GAChD,CAAC;EACF,CAAC;EACD,aAAa;GACZ,SAAS;GACT,IAAI,YAAY,YAAY,MAAM;IACjC,IAAI;KACH,IAAI,MAAM,YAAY,OAAO;IAC9B,QAAQ,CAAC;IACT,YAAY,UAAU;GACvB;GAEA,UAAU,gBAAgB;EAC3B;CACD,GAAG;EAAC;EAAO;EAAS;EAAS;CAAO,CAAC;CAErC,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAyB;CAAY,CAAA;AACvD"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CaptchaWidgetHandle } from "./types.js";
|
|
2
|
+
import { Ref } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/react/captcha/TurnstileCaptcha.d.ts
|
|
5
|
+
declare const TURNSTILE_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
6
|
+
type TurnstileCaptchaProps = {
|
|
7
|
+
/** Turnstile site key (public). */siteKey: string; /** Receives each fresh token, and null when the token expires or errors. */
|
|
8
|
+
onToken: (token: string | null) => void; /** Extra vendor render params (theme, size, appearance, ...). */
|
|
9
|
+
options?: Record<string, unknown>;
|
|
10
|
+
className?: string; /** Override the vendor script URL. */
|
|
11
|
+
scriptSrc?: string;
|
|
12
|
+
ref?: Ref<CaptchaWidgetHandle>;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Headless Cloudflare Turnstile widget: loads the vendor script once, renders the widget into a
|
|
16
|
+
* bare div, and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
|
|
17
|
+
*/
|
|
18
|
+
declare const TurnstileCaptcha: ({
|
|
19
|
+
siteKey,
|
|
20
|
+
onToken,
|
|
21
|
+
options,
|
|
22
|
+
className,
|
|
23
|
+
scriptSrc,
|
|
24
|
+
ref
|
|
25
|
+
}: TurnstileCaptchaProps) => import("react/jsx-runtime").JSX.Element;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { TURNSTILE_SCRIPT_URL, TurnstileCaptcha, TurnstileCaptchaProps };
|
|
28
|
+
//# sourceMappingURL=TurnstileCaptcha.d.ts.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCaptchaScript } from "./useCaptchaScript.js";
|
|
3
|
+
import { getTurnstile } from "./vendors.js";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useEffect, useImperativeHandle, useRef } from "react";
|
|
6
|
+
//#region src/react/captcha/TurnstileCaptcha.tsx
|
|
7
|
+
const TURNSTILE_SCRIPT_URL = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
|
|
8
|
+
/**
|
|
9
|
+
* Headless Cloudflare Turnstile widget: loads the vendor script once, renders the widget into a
|
|
10
|
+
* bare div, and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.
|
|
11
|
+
*/
|
|
12
|
+
const TurnstileCaptcha = ({ siteKey, onToken, options, className, scriptSrc = TURNSTILE_SCRIPT_URL, ref }) => {
|
|
13
|
+
const containerRef = useRef(null);
|
|
14
|
+
const widgetIdRef = useRef(null);
|
|
15
|
+
const onTokenRef = useRef(onToken);
|
|
16
|
+
onTokenRef.current = onToken;
|
|
17
|
+
const optionsRef = useRef(options);
|
|
18
|
+
optionsRef.current = options;
|
|
19
|
+
const ready = useCaptchaScript(scriptSrc);
|
|
20
|
+
useImperativeHandle(ref, () => ({ reset: () => {
|
|
21
|
+
const api = getTurnstile();
|
|
22
|
+
if (api && widgetIdRef.current !== null) api.reset(widgetIdRef.current);
|
|
23
|
+
onTokenRef.current(null);
|
|
24
|
+
} }), []);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!ready) return;
|
|
27
|
+
const container = containerRef.current;
|
|
28
|
+
const api = getTurnstile();
|
|
29
|
+
if (!container || !api) return;
|
|
30
|
+
const widgetId = api.render(container, {
|
|
31
|
+
...optionsRef.current,
|
|
32
|
+
sitekey: siteKey,
|
|
33
|
+
callback: (token) => onTokenRef.current(token),
|
|
34
|
+
"expired-callback": () => onTokenRef.current(null),
|
|
35
|
+
"error-callback": () => onTokenRef.current(null)
|
|
36
|
+
});
|
|
37
|
+
widgetIdRef.current = widgetId;
|
|
38
|
+
return () => {
|
|
39
|
+
widgetIdRef.current = null;
|
|
40
|
+
try {
|
|
41
|
+
api.remove(widgetId);
|
|
42
|
+
} catch {}
|
|
43
|
+
};
|
|
44
|
+
}, [ready, siteKey]);
|
|
45
|
+
return /* @__PURE__ */ jsx("div", {
|
|
46
|
+
ref: containerRef,
|
|
47
|
+
className
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { TURNSTILE_SCRIPT_URL, TurnstileCaptcha };
|
|
52
|
+
|
|
53
|
+
//# sourceMappingURL=TurnstileCaptcha.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TurnstileCaptcha.js","names":[],"sources":["../../../src/react/captcha/TurnstileCaptcha.tsx"],"sourcesContent":["'use client'\n\nimport { type Ref, useEffect, useImperativeHandle, useRef } from 'react'\nimport type { CaptchaWidgetHandle } from './types'\nimport { useCaptchaScript } from './useCaptchaScript'\nimport { getTurnstile } from './vendors'\n\nexport const TURNSTILE_SCRIPT_URL =\n\t'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'\n\nexport type TurnstileCaptchaProps = {\n\t/** Turnstile site key (public). */\n\tsiteKey: string\n\t/** Receives each fresh token, and null when the token expires or errors. */\n\tonToken: (token: string | null) => void\n\t/** Extra vendor render params (theme, size, appearance, ...). */\n\toptions?: Record<string, unknown>\n\tclassName?: string\n\t/** Override the vendor script URL. */\n\tscriptSrc?: string\n\tref?: Ref<CaptchaWidgetHandle>\n}\n\n/**\n * Headless Cloudflare Turnstile widget: loads the vendor script once, renders the widget into a\n * bare div, and reports tokens through `onToken`. Pass the latest token to `<Form captchaToken>`.\n */\nexport const TurnstileCaptcha = ({\n\tsiteKey,\n\tonToken,\n\toptions,\n\tclassName,\n\tscriptSrc = TURNSTILE_SCRIPT_URL,\n\tref,\n}: TurnstileCaptchaProps) => {\n\tconst containerRef = useRef<HTMLDivElement>(null)\n\tconst widgetIdRef = useRef<string | null>(null)\n\tconst onTokenRef = useRef(onToken)\n\tonTokenRef.current = onToken\n\tconst optionsRef = useRef(options)\n\toptionsRef.current = options\n\tconst ready = useCaptchaScript(scriptSrc)\n\n\tuseImperativeHandle(\n\t\tref,\n\t\t() => ({\n\t\t\treset: () => {\n\t\t\t\tconst api = getTurnstile()\n\t\t\t\tif (api && widgetIdRef.current !== null) {\n\t\t\t\t\tapi.reset(widgetIdRef.current)\n\t\t\t\t}\n\t\t\t\tonTokenRef.current(null)\n\t\t\t},\n\t\t}),\n\t\t[]\n\t)\n\n\tuseEffect(() => {\n\t\tif (!ready) {\n\t\t\treturn\n\t\t}\n\t\tconst container = containerRef.current\n\t\tconst api = getTurnstile()\n\t\tif (!container || !api) {\n\t\t\treturn\n\t\t}\n\t\tconst widgetId = api.render(container, {\n\t\t\t...optionsRef.current,\n\t\t\tsitekey: siteKey,\n\t\t\tcallback: (token: string) => onTokenRef.current(token),\n\t\t\t'expired-callback': () => onTokenRef.current(null),\n\t\t\t'error-callback': () => onTokenRef.current(null),\n\t\t})\n\t\twidgetIdRef.current = widgetId\n\t\treturn () => {\n\t\t\twidgetIdRef.current = null\n\t\t\ttry {\n\t\t\t\tapi.remove(widgetId)\n\t\t\t} catch {}\n\t\t}\n\t}, [ready, siteKey])\n\n\treturn <div ref={containerRef} className={className} />\n}\n"],"mappings":";;;;;;AAOA,MAAa,uBACZ;;;;;AAmBD,MAAa,oBAAoB,EAChC,SACA,SACA,SACA,WACA,YAAY,sBACZ,UAC4B;CAC5B,MAAM,eAAe,OAAuB,IAAI;CAChD,MAAM,cAAc,OAAsB,IAAI;CAC9C,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,aAAa,OAAO,OAAO;CACjC,WAAW,UAAU;CACrB,MAAM,QAAQ,iBAAiB,SAAS;CAExC,oBACC,YACO,EACN,aAAa;EACZ,MAAM,MAAM,aAAa;EACzB,IAAI,OAAO,YAAY,YAAY,MAClC,IAAI,MAAM,YAAY,OAAO;EAE9B,WAAW,QAAQ,IAAI;CACxB,EACD,IACA,CAAC,CACF;CAEA,gBAAgB;EACf,IAAI,CAAC,OACJ;EAED,MAAM,YAAY,aAAa;EAC/B,MAAM,MAAM,aAAa;EACzB,IAAI,CAAC,aAAa,CAAC,KAClB;EAED,MAAM,WAAW,IAAI,OAAO,WAAW;GACtC,GAAG,WAAW;GACd,SAAS;GACT,WAAW,UAAkB,WAAW,QAAQ,KAAK;GACrD,0BAA0B,WAAW,QAAQ,IAAI;GACjD,wBAAwB,WAAW,QAAQ,IAAI;EAChD,CAAC;EACD,YAAY,UAAU;EACtB,aAAa;GACZ,YAAY,UAAU;GACtB,IAAI;IACH,IAAI,OAAO,QAAQ;GACpB,QAAQ,CAAC;EACV;CACD,GAAG,CAAC,OAAO,OAAO,CAAC;CAEnB,OAAO,oBAAC,OAAD;EAAK,KAAK;EAAyB;CAAY,CAAA;AACvD"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/react/captcha/types.d.ts
|
|
2
|
+
/** Imperative handle exposed by the captcha widget components via their `ref` prop. */
|
|
3
|
+
type CaptchaWidgetHandle = {
|
|
4
|
+
/** Reset the widget, invalidating the current token. */reset: () => void;
|
|
5
|
+
};
|
|
6
|
+
//#endregion
|
|
7
|
+
export { CaptchaWidgetHandle };
|
|
8
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
//#region src/react/captcha/useCaptchaScript.ts
|
|
4
|
+
const pending = /* @__PURE__ */ new Map();
|
|
5
|
+
const loadScript = (src) => {
|
|
6
|
+
const cached = pending.get(src);
|
|
7
|
+
if (cached) return cached;
|
|
8
|
+
const promise = new Promise((resolve, reject) => {
|
|
9
|
+
const script = document.createElement("script");
|
|
10
|
+
script.src = src;
|
|
11
|
+
script.async = true;
|
|
12
|
+
script.addEventListener("load", () => resolve());
|
|
13
|
+
script.addEventListener("error", () => {
|
|
14
|
+
pending.delete(src);
|
|
15
|
+
script.remove();
|
|
16
|
+
reject(/* @__PURE__ */ new Error(`Failed to load ${src}`));
|
|
17
|
+
});
|
|
18
|
+
document.head.appendChild(script);
|
|
19
|
+
});
|
|
20
|
+
pending.set(src, promise);
|
|
21
|
+
return promise;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Load a vendor captcha script once per src, shared across all widget instances. SSR-safe (the
|
|
25
|
+
* DOM is only touched inside the effect). A failed load evicts the cache entry so a later mount
|
|
26
|
+
* can retry; consumers just see `ready` stay false.
|
|
27
|
+
*/
|
|
28
|
+
const useCaptchaScript = (src) => {
|
|
29
|
+
const [ready, setReady] = useState(false);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
let active = true;
|
|
32
|
+
setReady(false);
|
|
33
|
+
loadScript(src).then(() => {
|
|
34
|
+
if (active) setReady(true);
|
|
35
|
+
}, () => {});
|
|
36
|
+
return () => {
|
|
37
|
+
active = false;
|
|
38
|
+
};
|
|
39
|
+
}, [src]);
|
|
40
|
+
return ready;
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { useCaptchaScript };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=useCaptchaScript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useCaptchaScript.js","names":[],"sources":["../../../src/react/captcha/useCaptchaScript.ts"],"sourcesContent":["'use client'\n\nimport { useEffect, useState } from 'react'\n\nconst pending = new Map<string, Promise<void>>()\n\nconst loadScript = (src: string): Promise<void> => {\n\tconst cached = pending.get(src)\n\tif (cached) {\n\t\treturn cached\n\t}\n\tconst promise = new Promise<void>((resolve, reject) => {\n\t\tconst script = document.createElement('script')\n\t\tscript.src = src\n\t\tscript.async = true\n\t\tscript.addEventListener('load', () => resolve())\n\t\tscript.addEventListener('error', () => {\n\t\t\tpending.delete(src)\n\t\t\tscript.remove()\n\t\t\treject(new Error(`Failed to load ${src}`))\n\t\t})\n\t\tdocument.head.appendChild(script)\n\t})\n\tpending.set(src, promise)\n\treturn promise\n}\n\n/**\n * Load a vendor captcha script once per src, shared across all widget instances. SSR-safe (the\n * DOM is only touched inside the effect). A failed load evicts the cache entry so a later mount\n * can retry; consumers just see `ready` stay false.\n */\nexport const useCaptchaScript = (src: string): boolean => {\n\tconst [ready, setReady] = useState(false)\n\tuseEffect(() => {\n\t\tlet active = true\n\t\tsetReady(false)\n\t\tloadScript(src).then(\n\t\t\t() => {\n\t\t\t\tif (active) {\n\t\t\t\t\tsetReady(true)\n\t\t\t\t}\n\t\t\t},\n\t\t\t() => {}\n\t\t)\n\t\treturn () => {\n\t\t\tactive = false\n\t\t}\n\t}, [src])\n\treturn ready\n}\n"],"mappings":";;;AAIA,MAAM,0BAAU,IAAI,IAA2B;AAE/C,MAAM,cAAc,QAA+B;CAClD,MAAM,SAAS,QAAQ,IAAI,GAAG;CAC9B,IAAI,QACH,OAAO;CAER,MAAM,UAAU,IAAI,SAAe,SAAS,WAAW;EACtD,MAAM,SAAS,SAAS,cAAc,QAAQ;EAC9C,OAAO,MAAM;EACb,OAAO,QAAQ;EACf,OAAO,iBAAiB,cAAc,QAAQ,CAAC;EAC/C,OAAO,iBAAiB,eAAe;GACtC,QAAQ,OAAO,GAAG;GAClB,OAAO,OAAO;GACd,uBAAO,IAAI,MAAM,kBAAkB,KAAK,CAAC;EAC1C,CAAC;EACD,SAAS,KAAK,YAAY,MAAM;CACjC,CAAC;CACD,QAAQ,IAAI,KAAK,OAAO;CACxB,OAAO;AACR;;;;;;AAOA,MAAa,oBAAoB,QAAyB;CACzD,MAAM,CAAC,OAAO,YAAY,SAAS,KAAK;CACxC,gBAAgB;EACf,IAAI,SAAS;EACb,SAAS,KAAK;EACd,WAAW,GAAG,EAAE,WACT;GACL,IAAI,QACH,SAAS,IAAI;EAEf,SACM,CAAC,CACR;EACA,aAAa;GACZ,SAAS;EACV;CACD,GAAG,CAAC,GAAG,CAAC;CACR,OAAO;AACR"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/react/captcha/vendors.ts
|
|
2
|
+
const getTurnstile = () => globalThis.turnstile;
|
|
3
|
+
const getGrecaptcha = () => globalThis.grecaptcha;
|
|
4
|
+
const getHcaptcha = () => globalThis.hcaptcha;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { getGrecaptcha, getHcaptcha, getTurnstile };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=vendors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vendors.js","names":[],"sources":["../../../src/react/captcha/vendors.ts"],"sourcesContent":["export type TurnstileRenderParams = {\n\tsitekey: string\n\tcallback?: (token: string) => void\n\t'expired-callback'?: () => void\n\t'error-callback'?: () => void\n\t[key: string]: unknown\n}\n\nexport type TurnstileApi = {\n\trender(container: HTMLElement, params: TurnstileRenderParams): string\n\treset(widgetId: string): void\n\tremove(widgetId: string): void\n}\n\nexport type RecaptchaRenderParams = {\n\tsitekey: string\n\tcallback?: (token: string) => void\n\t'expired-callback'?: () => void\n\t'error-callback'?: () => void\n\t[key: string]: unknown\n}\n\nexport type GrecaptchaApi = {\n\tready(cb: () => void): void\n\trender(container: HTMLElement, params: RecaptchaRenderParams): number\n\treset(widgetId?: number): void\n\texecute(siteKey: string, options?: { action?: string }): Promise<string>\n}\n\nexport type HcaptchaRenderParams = {\n\tsitekey: string\n\tcallback?: (token: string) => void\n\t'expired-callback'?: () => void\n\t'error-callback'?: () => void\n\t[key: string]: unknown\n}\n\nexport type HcaptchaApi = {\n\trender(container: HTMLElement, params: HcaptchaRenderParams): string\n\treset(widgetId?: string): void\n\tremove(widgetId?: string): void\n}\n\nexport const getTurnstile = (): TurnstileApi | undefined =>\n\t(globalThis as { turnstile?: TurnstileApi }).turnstile\n\nexport const getGrecaptcha = (): GrecaptchaApi | undefined =>\n\t(globalThis as { grecaptcha?: GrecaptchaApi }).grecaptcha\n\nexport const getHcaptcha = (): HcaptchaApi | undefined =>\n\t(globalThis as { hcaptcha?: HcaptchaApi }).hcaptcha\n"],"mappings":";AA2CA,MAAa,qBACX,WAA4C;AAE9C,MAAa,sBACX,WAA8C;AAEhD,MAAa,oBACX,WAA0C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Input.js","names":[],"sources":["../../../src/react/primitives/Input.tsx"],"sourcesContent":["'use client'\n\nexport type InputProps = {\n\tid: string\n\tname: string\n\ttype?: 'text' | 'email' | 'number'\n\tvalue: string\n\tonChange: (value: string) => void\n\tonBlur?: () => void\n\tplaceholder?: string\n\trequired?: boolean\n\tdisabled?: boolean\n\tinvalid?: boolean\n\tdescribedById?: string\n}\n\nexport const Input = ({\n\tid,\n\tname,\n\ttype = 'text',\n\tvalue,\n\tonChange,\n\tonBlur,\n\tplaceholder,\n\trequired,\n\tdisabled,\n\tinvalid,\n\tdescribedById,\n}: InputProps) => (\n\t<input\n\t\tid={id}\n\t\tname={name}\n\t\ttype={type}\n\t\tclassName=\"fb-input\"\n\t\tvalue={value}\n\t\tplaceholder={placeholder}\n\t\trequired={required}\n\t\tdisabled={disabled}\n\t\taria-invalid={invalid || undefined}\n\t\taria-describedby={describedById}\n\t\tonChange={(event) => onChange(event.target.value)}\n\t\tonBlur={onBlur}\n\t/>\n)\n"],"mappings":";;;AAgBA,MAAa,SAAS,EACrB,IACA,MACA,OAAO,QACP,OACA,UACA,QACA,aACA,UACA,UACA,SACA,oBAEA,oBAAC,SAAD;CACK;CACE;CACA;CACN,WAAU;CACH;CACM;CACH;CACA;CACV,gBAAc,WAAW,KAAA;CACzB,oBAAkB;CAClB,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;CACxC;AACR,CAAA"}
|
|
1
|
+
{"version":3,"file":"Input.js","names":[],"sources":["../../../src/react/primitives/Input.tsx"],"sourcesContent":["'use client'\n\nexport type InputProps = {\n\tid: string\n\tname: string\n\ttype?: 'text' | 'email' | 'number' | 'date'\n\tvalue: string\n\tonChange: (value: string) => void\n\tonBlur?: () => void\n\tplaceholder?: string\n\trequired?: boolean\n\tdisabled?: boolean\n\tinvalid?: boolean\n\tdescribedById?: string\n}\n\nexport const Input = ({\n\tid,\n\tname,\n\ttype = 'text',\n\tvalue,\n\tonChange,\n\tonBlur,\n\tplaceholder,\n\trequired,\n\tdisabled,\n\tinvalid,\n\tdescribedById,\n}: InputProps) => (\n\t<input\n\t\tid={id}\n\t\tname={name}\n\t\ttype={type}\n\t\tclassName=\"fb-input\"\n\t\tvalue={value}\n\t\tplaceholder={placeholder}\n\t\trequired={required}\n\t\tdisabled={disabled}\n\t\taria-invalid={invalid || undefined}\n\t\taria-describedby={describedById}\n\t\tonChange={(event) => onChange(event.target.value)}\n\t\tonBlur={onBlur}\n\t/>\n)\n"],"mappings":";;;AAgBA,MAAa,SAAS,EACrB,IACA,MACA,OAAO,QACP,OACA,UACA,QACA,aACA,UACA,UACA,SACA,oBAEA,oBAAC,SAAD;CACK;CACE;CACA;CACN,WAAU;CACH;CACM;CACH;CACA;CACV,gBAAc,WAAW,KAAA;CACzB,oBAAkB;CAClB,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;CACxC;AACR,CAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
|
-
import { useId } from "react";
|
|
5
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { useId } from "react";
|
|
6
6
|
//#region src/react/renderers/calculation.tsx
|
|
7
7
|
/** Read-only renderer for a calculation field: the value is derived, never user-editable, so it shows in an `<output>` rather than an input. */
|
|
8
8
|
const calculationRenderer = defineFieldRenderer(({ field, value, errors, warnings, required }) => {
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Checkbox } from "../primitives/Checkbox.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/checkbox.tsx
|
|
8
8
|
const checkboxRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Checkbox } from "../primitives/Checkbox.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/consent.tsx
|
|
8
8
|
const consentRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { defineFieldRenderer } from "../contract.js";
|
|
3
|
+
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
|
+
import { Input } from "../primitives/Input.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
|
+
//#region src/react/renderers/date.tsx
|
|
8
|
+
const dateRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
|
+
const id = useId();
|
|
10
|
+
const describedById = `${id}-desc`;
|
|
11
|
+
return /* @__PURE__ */ jsx(FieldShell, {
|
|
12
|
+
id,
|
|
13
|
+
label: field.label,
|
|
14
|
+
description: typeof field.description === "string" ? field.description : void 0,
|
|
15
|
+
required,
|
|
16
|
+
errors,
|
|
17
|
+
warnings,
|
|
18
|
+
describedById,
|
|
19
|
+
children: /* @__PURE__ */ jsx(Input, {
|
|
20
|
+
id,
|
|
21
|
+
name,
|
|
22
|
+
type: "date",
|
|
23
|
+
value: value ?? "",
|
|
24
|
+
onChange,
|
|
25
|
+
onBlur,
|
|
26
|
+
placeholder: typeof field.placeholder === "string" ? field.placeholder : void 0,
|
|
27
|
+
required,
|
|
28
|
+
disabled,
|
|
29
|
+
invalid: errors.length > 0,
|
|
30
|
+
describedById
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
export { dateRenderer };
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=date.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date.js","names":[],"sources":["../../../src/react/renderers/date.tsx"],"sourcesContent":["'use client'\n\nimport { useId } from 'react'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { Input } from '../primitives/Input'\n\nexport const dateRenderer = defineFieldRenderer<string>(\n\t({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {\n\t\tconst id = useId()\n\t\tconst describedById = `${id}-desc`\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={errors}\n\t\t\t\twarnings={warnings}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<Input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"date\"\n\t\t\t\t\tvalue={value ?? ''}\n\t\t\t\t\tonChange={onChange}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\tplaceholder={typeof field.placeholder === 'string' ? field.placeholder : undefined}\n\t\t\t\t\trequired={required}\n\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\tinvalid={errors.length > 0}\n\t\t\t\t\tdescribedById={describedById}\n\t\t\t\t/>\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;AAOA,MAAa,eAAe,qBAC1B,EAAE,OAAO,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,UAAU,eAAe;CACnF,MAAM,KAAK,MAAM;CACjB,MAAM,gBAAgB,GAAG,GAAG;CAC5B,OACC,oBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACF;EACE;EACK;YAEf,oBAAC,OAAD;GACK;GACE;GACN,MAAK;GACL,OAAO,SAAS;GACN;GACF;GACR,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;GAC/D;GACA;GACV,SAAS,OAAO,SAAS;GACV;EACf,CAAA;CACU,CAAA;AAEd,CACD"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Input } from "../primitives/Input.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/email.tsx
|
|
8
8
|
const emailRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { keys } from "../../translations/keys.js";
|
|
3
|
+
import { resolveMessage } from "../../validation/message.js";
|
|
4
|
+
import { formatBytes } from "../../uploads/formatBytes.js";
|
|
2
5
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
6
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
7
|
import { uploadFile } from "../uploadFile.js";
|
|
5
|
-
import { useId, useState } from "react";
|
|
6
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { useId, useState } from "react";
|
|
7
10
|
//#region src/react/renderers/file.tsx
|
|
8
|
-
const
|
|
9
|
-
if (!Array.isArray(mimeTypes)) return;
|
|
10
|
-
|
|
11
|
-
return types.length > 0 ? types.join(",") : void 0;
|
|
11
|
+
const typesOf = (mimeTypes) => {
|
|
12
|
+
if (!Array.isArray(mimeTypes)) return [];
|
|
13
|
+
return mimeTypes.filter((entry) => typeof entry === "string");
|
|
12
14
|
};
|
|
13
15
|
/**
|
|
14
16
|
* Headless file renderer: a native file input that uploads the chosen file to the upload collection and
|
|
@@ -16,19 +18,26 @@ const acceptOf = (mimeTypes) => {
|
|
|
16
18
|
* stored doc at submit, so the client value is only the id. Shows the uploaded filename with a clear
|
|
17
19
|
* control and surfaces upload errors. Single file (v1); the upload posts to the default `/api` route.
|
|
18
20
|
*/
|
|
19
|
-
const fileRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
21
|
+
const fileRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled, t }) => {
|
|
20
22
|
const id = useId();
|
|
21
23
|
const describedById = `${id}-desc`;
|
|
22
24
|
const [uploading, setUploading] = useState(false);
|
|
23
25
|
const [filename, setFilename] = useState(void 0);
|
|
24
26
|
const [localError, setLocalError] = useState(void 0);
|
|
25
27
|
const collection = typeof field.relationTo === "string" ? field.relationTo : "form-uploads";
|
|
26
|
-
const
|
|
28
|
+
const types = typesOf(field.mimeTypes);
|
|
29
|
+
const accept = types.length > 0 ? types.join(",") : void 0;
|
|
30
|
+
const maxSize = typeof field.maxSize === "number" && field.maxSize >= 0 ? field.maxSize : void 0;
|
|
31
|
+
const hints = [types.length > 0 ? resolveMessage(t(keys.fileHintAccepted), { types: types.join(", ") }) : void 0, maxSize !== void 0 ? resolveMessage(t(keys.fileHintMaxSize), { max: formatBytes(maxSize) }) : void 0].filter((hint) => typeof hint === "string");
|
|
27
32
|
const allErrors = localError ? [...errors, localError] : errors;
|
|
28
33
|
const displayName = filename ?? (value !== void 0 && value !== null && value !== "" ? "Uploaded file" : void 0);
|
|
29
34
|
const handleChange = async (event) => {
|
|
30
35
|
const selected = event.target.files?.[0];
|
|
31
36
|
if (!selected) return;
|
|
37
|
+
if (maxSize !== void 0 && selected.size > maxSize) {
|
|
38
|
+
setLocalError(resolveMessage(t(keys.fileTooLarge), { max: formatBytes(maxSize) }));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
32
41
|
setUploading(true);
|
|
33
42
|
setLocalError(void 0);
|
|
34
43
|
const result = await uploadFile({
|
|
@@ -68,6 +77,10 @@ const fileRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur
|
|
|
68
77
|
"aria-invalid": allErrors.length > 0,
|
|
69
78
|
"aria-describedby": describedById
|
|
70
79
|
}),
|
|
80
|
+
hints.length > 0 ? /* @__PURE__ */ jsx("p", {
|
|
81
|
+
className: "fb-field__file-hint",
|
|
82
|
+
children: hints.join(" · ")
|
|
83
|
+
}) : null,
|
|
71
84
|
uploading ? /* @__PURE__ */ jsx("p", {
|
|
72
85
|
className: "fb-field__file-status",
|
|
73
86
|
"aria-live": "polite",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file.js","names":[],"sources":["../../../src/react/renderers/file.tsx"],"sourcesContent":["'use client'\n\nimport { type ChangeEvent, useId, useState } from 'react'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { uploadFile } from '../uploadFile'\n\nconst
|
|
1
|
+
{"version":3,"file":"file.js","names":[],"sources":["../../../src/react/renderers/file.tsx"],"sourcesContent":["'use client'\n\nimport { type ChangeEvent, useId, useState } from 'react'\nimport { keys } from '../../translations/keys'\nimport { formatBytes } from '../../uploads/formatBytes'\nimport { resolveMessage } from '../../validation/message'\nimport { defineFieldRenderer } from '../contract'\nimport { FieldShell } from '../primitives/FieldShell'\nimport { uploadFile } from '../uploadFile'\n\nconst typesOf = (mimeTypes: unknown): string[] => {\n\tif (!Array.isArray(mimeTypes)) {\n\t\treturn []\n\t}\n\treturn mimeTypes.filter((entry): entry is string => typeof entry === 'string')\n}\n\n/**\n * Headless file renderer: a native file input that uploads the chosen file to the upload collection and\n * stores the returned id as the field value. The server re-derives filename/mimeType/filesize from the\n * stored doc at submit, so the client value is only the id. Shows the uploaded filename with a clear\n * control and surfaces upload errors. Single file (v1); the upload posts to the default `/api` route.\n */\nexport const fileRenderer = defineFieldRenderer<string | number>(\n\t({ field, name, value, onChange, onBlur, errors, warnings, required, disabled, t }) => {\n\t\tconst id = useId()\n\t\tconst describedById = `${id}-desc`\n\t\tconst [uploading, setUploading] = useState(false)\n\t\tconst [filename, setFilename] = useState<string | undefined>(undefined)\n\t\tconst [localError, setLocalError] = useState<string | undefined>(undefined)\n\n\t\tconst collection = typeof field.relationTo === 'string' ? field.relationTo : 'form-uploads'\n\t\tconst types = typesOf(field.mimeTypes)\n\t\tconst accept = types.length > 0 ? types.join(',') : undefined\n\t\t// Zero is a real limit, matching resolveFileRef server-side (filesize > maxSize rejects any non-empty file).\n\t\tconst maxSize =\n\t\t\ttypeof field.maxSize === 'number' && field.maxSize >= 0 ? field.maxSize : undefined\n\t\tconst hints = [\n\t\t\ttypes.length > 0\n\t\t\t\t? resolveMessage(t(keys.fileHintAccepted), { types: types.join(', ') })\n\t\t\t\t: undefined,\n\t\t\tmaxSize !== undefined\n\t\t\t\t? resolveMessage(t(keys.fileHintMaxSize), { max: formatBytes(maxSize) })\n\t\t\t\t: undefined,\n\t\t].filter((hint): hint is string => typeof hint === 'string')\n\t\tconst allErrors = localError ? [...errors, localError] : errors\n\t\t// Show the just-uploaded filename, or a generic indicator when the field already holds an id (recall/prefill): the filename is not part of the client value.\n\t\tconst hasValue = value !== undefined && value !== null && value !== ''\n\t\tconst displayName = filename ?? (hasValue ? 'Uploaded file' : undefined)\n\n\t\tconst handleChange = async (event: ChangeEvent<HTMLInputElement>) => {\n\t\t\tconst selected = event.target.files?.[0]\n\t\t\tif (!selected) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (maxSize !== undefined && selected.size > maxSize) {\n\t\t\t\tsetLocalError(resolveMessage(t(keys.fileTooLarge), { max: formatBytes(maxSize) }))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tsetUploading(true)\n\t\t\tsetLocalError(undefined)\n\t\t\tconst result = await uploadFile({ file: selected, collection })\n\t\t\tsetUploading(false)\n\t\t\tif (result.ok) {\n\t\t\t\tsetFilename(selected.name)\n\t\t\t\tonChange(result.id)\n\t\t\t} else {\n\t\t\t\tsetLocalError(result.message ?? 'Upload failed')\n\t\t\t}\n\t\t}\n\n\t\tconst clear = () => {\n\t\t\tsetFilename(undefined)\n\t\t\tsetLocalError(undefined)\n\t\t\tonChange('')\n\t\t}\n\n\t\treturn (\n\t\t\t<FieldShell\n\t\t\t\tid={id}\n\t\t\t\tlabel={field.label}\n\t\t\t\tdescription={typeof field.description === 'string' ? field.description : undefined}\n\t\t\t\trequired={required}\n\t\t\t\terrors={allErrors}\n\t\t\t\twarnings={warnings}\n\t\t\t\tdescribedById={describedById}\n\t\t\t>\n\t\t\t\t<input\n\t\t\t\t\tid={id}\n\t\t\t\t\tname={name}\n\t\t\t\t\ttype=\"file\"\n\t\t\t\t\taccept={accept}\n\t\t\t\t\tdisabled={disabled || uploading}\n\t\t\t\t\tonChange={(event) => {\n\t\t\t\t\t\tvoid handleChange(event)\n\t\t\t\t\t}}\n\t\t\t\t\tonBlur={onBlur}\n\t\t\t\t\taria-invalid={allErrors.length > 0}\n\t\t\t\t\taria-describedby={describedById}\n\t\t\t\t/>\n\t\t\t\t{hints.length > 0 ? <p className=\"fb-field__file-hint\">{hints.join(' · ')}</p> : null}\n\t\t\t\t{uploading ? (\n\t\t\t\t\t<p className=\"fb-field__file-status\" aria-live=\"polite\">\n\t\t\t\t\t\tUploading\n\t\t\t\t\t</p>\n\t\t\t\t) : null}\n\t\t\t\t{displayName ? (\n\t\t\t\t\t<p className=\"fb-field__file-name\">\n\t\t\t\t\t\t<span>{displayName}</span>\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\tclassName=\"fb-field__file-clear\"\n\t\t\t\t\t\t\tonClick={clear}\n\t\t\t\t\t\t\tdisabled={disabled}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\tRemove\n\t\t\t\t\t\t</button>\n\t\t\t\t\t</p>\n\t\t\t\t) : null}\n\t\t\t</FieldShell>\n\t\t)\n\t}\n)\n"],"mappings":";;;;;;;;;;AAUA,MAAM,WAAW,cAAiC;CACjD,IAAI,CAAC,MAAM,QAAQ,SAAS,GAC3B,OAAO,CAAC;CAET,OAAO,UAAU,QAAQ,UAA2B,OAAO,UAAU,QAAQ;AAC9E;;;;;;;AAQA,MAAa,eAAe,qBAC1B,EAAE,OAAO,MAAM,OAAO,UAAU,QAAQ,QAAQ,UAAU,UAAU,UAAU,QAAQ;CACtF,MAAM,KAAK,MAAM;CACjB,MAAM,gBAAgB,GAAG,GAAG;CAC5B,MAAM,CAAC,WAAW,gBAAgB,SAAS,KAAK;CAChD,MAAM,CAAC,UAAU,eAAe,SAA6B,KAAA,CAAS;CACtE,MAAM,CAAC,YAAY,iBAAiB,SAA6B,KAAA,CAAS;CAE1E,MAAM,aAAa,OAAO,MAAM,eAAe,WAAW,MAAM,aAAa;CAC7E,MAAM,QAAQ,QAAQ,MAAM,SAAS;CACrC,MAAM,SAAS,MAAM,SAAS,IAAI,MAAM,KAAK,GAAG,IAAI,KAAA;CAEpD,MAAM,UACL,OAAO,MAAM,YAAY,YAAY,MAAM,WAAW,IAAI,MAAM,UAAU,KAAA;CAC3E,MAAM,QAAQ,CACb,MAAM,SAAS,IACZ,eAAe,EAAE,KAAK,gBAAgB,GAAG,EAAE,OAAO,MAAM,KAAK,IAAI,EAAE,CAAC,IACpE,KAAA,GACH,YAAY,KAAA,IACT,eAAe,EAAE,KAAK,eAAe,GAAG,EAAE,KAAK,YAAY,OAAO,EAAE,CAAC,IACrE,KAAA,CACJ,EAAE,QAAQ,SAAyB,OAAO,SAAS,QAAQ;CAC3D,MAAM,YAAY,aAAa,CAAC,GAAG,QAAQ,UAAU,IAAI;CAGzD,MAAM,cAAc,aADH,UAAU,KAAA,KAAa,UAAU,QAAQ,UAAU,KACxB,kBAAkB,KAAA;CAE9D,MAAM,eAAe,OAAO,UAAyC;EACpE,MAAM,WAAW,MAAM,OAAO,QAAQ;EACtC,IAAI,CAAC,UACJ;EAED,IAAI,YAAY,KAAA,KAAa,SAAS,OAAO,SAAS;GACrD,cAAc,eAAe,EAAE,KAAK,YAAY,GAAG,EAAE,KAAK,YAAY,OAAO,EAAE,CAAC,CAAC;GACjF;EACD;EACA,aAAa,IAAI;EACjB,cAAc,KAAA,CAAS;EACvB,MAAM,SAAS,MAAM,WAAW;GAAE,MAAM;GAAU;EAAW,CAAC;EAC9D,aAAa,KAAK;EAClB,IAAI,OAAO,IAAI;GACd,YAAY,SAAS,IAAI;GACzB,SAAS,OAAO,EAAE;EACnB,OACC,cAAc,OAAO,WAAW,eAAe;CAEjD;CAEA,MAAM,cAAc;EACnB,YAAY,KAAA,CAAS;EACrB,cAAc,KAAA,CAAS;EACvB,SAAS,EAAE;CACZ;CAEA,OACC,qBAAC,YAAD;EACK;EACJ,OAAO,MAAM;EACb,aAAa,OAAO,MAAM,gBAAgB,WAAW,MAAM,cAAc,KAAA;EAC/D;EACV,QAAQ;EACE;EACK;YAPhB;GASC,oBAAC,SAAD;IACK;IACE;IACN,MAAK;IACG;IACR,UAAU,YAAY;IACtB,WAAW,UAAU;KACpB,aAAkB,KAAK;IACxB;IACQ;IACR,gBAAc,UAAU,SAAS;IACjC,oBAAkB;GAClB,CAAA;GACA,MAAM,SAAS,IAAI,oBAAC,KAAD;IAAG,WAAU;cAAuB,MAAM,KAAK,KAAK;GAAK,CAAA,IAAI;GAChF,YACA,oBAAC,KAAD;IAAG,WAAU;IAAwB,aAAU;cAAS;GAErD,CAAA,IACA;GACH,cACA,qBAAC,KAAD;IAAG,WAAU;cAAb,CACC,oBAAC,QAAD,EAAA,UAAO,YAAkB,CAAA,GACzB,oBAAC,UAAD;KACC,MAAK;KACL,WAAU;KACV,SAAS;KACC;eACV;IAEO,CAAA,CACN;QACA;EACO;;AAEd,CACD"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { calculationRenderer } from "./calculation.js";
|
|
2
2
|
import { checkboxRenderer } from "./checkbox.js";
|
|
3
3
|
import { consentRenderer } from "./consent.js";
|
|
4
|
+
import { dateRenderer } from "./date.js";
|
|
4
5
|
import { emailRenderer } from "./email.js";
|
|
5
6
|
import { fileRenderer } from "./file.js";
|
|
6
7
|
import { numberRenderer } from "./number.js";
|
|
@@ -14,6 +15,7 @@ const defaultRenderers = {
|
|
|
14
15
|
calculation: calculationRenderer,
|
|
15
16
|
checkbox: checkboxRenderer,
|
|
16
17
|
consent: consentRenderer,
|
|
18
|
+
date: dateRenderer,
|
|
17
19
|
email: emailRenderer,
|
|
18
20
|
file: fileRenderer,
|
|
19
21
|
number: numberRenderer,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/renderers/index.ts"],"sourcesContent":["import type { FieldRenderer } from '../contract'\nimport { calculationRenderer } from './calculation'\nimport { checkboxRenderer } from './checkbox'\nimport { consentRenderer } from './consent'\nimport { emailRenderer } from './email'\nimport { fileRenderer } from './file'\nimport { numberRenderer } from './number'\nimport { repeaterRenderer } from './repeater'\nimport { selectRenderer } from './select'\nimport { textRenderer } from './text'\nimport { textareaRenderer } from './textarea'\n\n/** The built-in field renderers, keyed by field-type slug. Override via the renderer registry. */\nexport const defaultRenderers: Record<string, FieldRenderer> = {\n\tcalculation: calculationRenderer as FieldRenderer,\n\tcheckbox: checkboxRenderer as FieldRenderer,\n\tconsent: consentRenderer as FieldRenderer,\n\temail: emailRenderer as FieldRenderer,\n\tfile: fileRenderer as FieldRenderer,\n\tnumber: numberRenderer as FieldRenderer,\n\trepeater: repeaterRenderer as FieldRenderer,\n\tselect: selectRenderer as FieldRenderer,\n\ttext: textRenderer as FieldRenderer,\n\ttextarea: textareaRenderer as FieldRenderer,\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/renderers/index.ts"],"sourcesContent":["import type { FieldRenderer } from '../contract'\nimport { calculationRenderer } from './calculation'\nimport { checkboxRenderer } from './checkbox'\nimport { consentRenderer } from './consent'\nimport { dateRenderer } from './date'\nimport { emailRenderer } from './email'\nimport { fileRenderer } from './file'\nimport { numberRenderer } from './number'\nimport { repeaterRenderer } from './repeater'\nimport { selectRenderer } from './select'\nimport { textRenderer } from './text'\nimport { textareaRenderer } from './textarea'\n\n/** The built-in field renderers, keyed by field-type slug. Override via the renderer registry. */\nexport const defaultRenderers: Record<string, FieldRenderer> = {\n\tcalculation: calculationRenderer as FieldRenderer,\n\tcheckbox: checkboxRenderer as FieldRenderer,\n\tconsent: consentRenderer as FieldRenderer,\n\tdate: dateRenderer as FieldRenderer,\n\temail: emailRenderer as FieldRenderer,\n\tfile: fileRenderer as FieldRenderer,\n\tnumber: numberRenderer as FieldRenderer,\n\trepeater: repeaterRenderer as FieldRenderer,\n\tselect: selectRenderer as FieldRenderer,\n\ttext: textRenderer as FieldRenderer,\n\ttextarea: textareaRenderer as FieldRenderer,\n}\n"],"mappings":";;;;;;;;;;;;;AAcA,MAAa,mBAAkD;CAC9D,aAAa;CACb,UAAU;CACV,SAAS;CACT,MAAM;CACN,OAAO;CACP,MAAM;CACN,QAAQ;CACR,UAAU;CACV,QAAQ;CACR,MAAM;CACN,UAAU;AACX"}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { defineFieldRenderer } from "../contract.js";
|
|
3
3
|
import { FieldShell } from "../primitives/FieldShell.js";
|
|
4
4
|
import { Input } from "../primitives/Input.js";
|
|
5
|
-
import { useId } from "react";
|
|
6
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useId } from "react";
|
|
7
7
|
//#region src/react/renderers/number.tsx
|
|
8
8
|
const numberRenderer = defineFieldRenderer(({ field, name, value, onChange, onBlur, errors, warnings, required, disabled }) => {
|
|
9
9
|
const id = useId();
|