@10x-media/form-builder 0.1.0-beta.25 → 0.1.0-beta.26
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 +22 -0
- package/dist/actions/body/serializeBody.d.ts +10 -14
- package/dist/actions/body/serializeBody.js +5 -10
- package/dist/actions/body/serializeBody.js.map +1 -1
- package/dist/actions/builtin/emailAction.d.ts +7 -0
- package/dist/actions/builtin/emailAction.js +14 -7
- package/dist/actions/builtin/emailAction.js.map +1 -1
- package/dist/actions/defineAction.d.ts +2 -4
- package/dist/actions/defineAction.js.map +1 -1
- package/dist/actions/emailRender.d.ts +5 -5
- package/dist/actions/fromAddresses.d.ts +3 -1
- package/dist/actions/fromAddresses.js.map +1 -1
- package/dist/actions/recipientSources.d.ts +6 -17
- package/dist/actions/recipientSources.js.map +1 -1
- package/dist/actions/submissionContext.d.ts +34 -0
- package/dist/actions/task.js +10 -13
- package/dist/actions/task.js.map +1 -1
- package/dist/aggregation/aggregateResponses.js +1 -1
- package/dist/aggregation/resolveResultsRequest.js +38 -24
- package/dist/aggregation/resolveResultsRequest.js.map +1 -1
- package/dist/client/EndpointOptionsSelect.d.ts +1 -0
- package/dist/client/EndpointOptionsSelect.js +1 -0
- package/dist/client/EndpointOptionsSelect.js.map +1 -1
- package/dist/client/FieldNameSelect.d.ts +1 -0
- package/dist/client/FieldNameSelect.js +1 -0
- package/dist/client/FieldNameSelect.js.map +1 -1
- package/dist/client/RecipientsSelect.d.ts +1 -0
- package/dist/client/RecipientsSelect.js +1 -0
- package/dist/client/RecipientsSelect.js.map +1 -1
- package/dist/collections/formSubmissions.js +3 -3
- package/dist/collections/forms.js +2 -2
- package/dist/exports/rsc.d.ts +1 -1
- package/dist/exports/rsc.js +3 -3
- package/dist/form/findFormAtLocale.d.ts +30 -0
- package/dist/form/findFormAtLocale.js +88 -0
- package/dist/form/findFormAtLocale.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +15 -12
- package/dist/index.js.map +1 -1
- package/dist/options.d.ts +24 -1
- package/dist/plugin/registerCollections.js +2 -2
- package/dist/poll/resolvePollOutcome.js +1 -1
- package/dist/poll/votes/recountPollVotes.js +2 -2
- package/dist/poll/votes/voteTallyHook.js +2 -2
- package/dist/react/Form.d.ts +9 -5
- package/dist/react/Form.js +8 -9
- package/dist/react/Form.js.map +1 -1
- package/dist/react/Poll.js +4 -2
- package/dist/react/Poll.js.map +1 -1
- package/dist/react/fetchResults.d.ts +2 -1
- package/dist/react/fetchResults.js +5 -2
- package/dist/react/fetchResults.js.map +1 -1
- package/dist/react/submitForm.d.ts +4 -4
- package/dist/react/submitForm.js.map +1 -1
- package/dist/submissions/createSubmission.d.ts +6 -0
- package/dist/submissions/createSubmission.js +7 -2
- package/dist/submissions/createSubmission.js.map +1 -1
- package/dist/submissions/submissionLocale.js +27 -6
- package/dist/submissions/submissionLocale.js.map +1 -1
- package/dist/submissions/validateSubmission.js +108 -101
- package/dist/submissions/validateSubmission.js.map +1 -1
- package/dist/submissions/voteChangeEndpoint.js +1 -1
- package/package.json +3 -3
package/dist/react/Form.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\ttype CSSProperties,\n\ttype FormEvent as ReactFormEvent,\n\ttype KeyboardEvent as ReactKeyboardEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport type { BodyConverter } from '../actions/body/converters'\nimport { serializeBody } from '../actions/body/serializeBody'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport { fieldKey, isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type { FormFlow } from '../flow/types'\nimport type {\n\tFormButtonSettings,\n\tFormDocument,\n\tFormPollSettings,\n\tFormResponseSettings,\n} from '../form/types'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver, descriptorsFor } from '../recall/resolver'\nimport {\n\tCAPTCHA_TOKEN_KEY,\n\tCONTEXT_KEY,\n\tDEFAULT_HONEYPOT_FIELD,\n\tHONEYPOT_VALUE_KEY,\n} from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { resolveMessage } from '../validation/message'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { defaultNavigate, type FormAdapters } from './adapters'\nimport { cn } from './cn'\nimport type { RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormContextValue, type FormStepInfo } from './FormContext'\nimport {\n\ttype BackButtonRenderProps,\n\tFormControls,\n\ttype NextButtonRenderProps,\n\ttype SubmitButtonRenderProps,\n} from './FormControls'\nimport { FormFields } from './FormFields'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport {\n\tDEFAULT_STEP_ID,\n\ttype FieldErrors,\n\ttype FormAction,\n\tformReducer,\n\tinitialFormState,\n\tseedFieldValues,\n} from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { validateFieldValue } from './validateField'\n\nexport type {\n\tBackButtonRenderProps,\n\tNextButtonRenderProps,\n\tSubmitButtonRenderProps,\n} from './FormControls'\n// FormResponseSettings, FormButtonSettings, FormPollSettings, and FormDocument live in\n// `../form/types` (no 'use client') so server code (e.g. `toFormDocument` in a Server Component)\n// can use them without pulling in this client module. Re-exported here so `./react` and existing\n// `from './Form'` imports keep working unchanged.\nexport type { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings }\n\n/**\n * The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the\n * form's active converters, so a host can render or toast the resolved response without re-deriving it.\n */\nexport type FormSuccessResponse =\n\t| { type: 'message'; html?: string }\n\t| { type: 'redirect'; url?: string }\n\n/** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */\nexport type FormSuccessResult = {\n\tresponse?: FormSuccessResponse\n\t/** The submitted answers (reserved keys like the honeypot and captcha token excluded), e.g. for `<Poll>` to track the voter's pick. */\n\tvalues?: SubmissionValue[]\n}\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\t/**\n\t * Called after a successful submission with the submission id and the resolved success response\n\t * (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both\n\t * `successBehavior` modes and on the custom-`children` path.\n\t */\n\tonSuccess?: (submissionId?: string, result?: FormSuccessResult) => void\n\tonError?: (message: string) => void\n\t/**\n\t * Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the\n\t * client serializer, so those blocks survive in the success message and in the `onSuccess` response.\n\t */\n\tconverters?: Record<string, BodyConverter>\n\t/**\n\t * What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;\n\t * `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`\n\t * and keep the form usable.\n\t */\n\tsuccessBehavior?: 'replace' | 'reset'\n\t/**\n\t * Client halves of the plugin's registered `calc.functions` (same keys), used for the live calc\n\t * preview only; the server always recomputes authoritatively at submit with the registry's own\n\t * functions.\n\t */\n\tcalcFunctions?: Record<string, (args: number[]) => number>\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\t/**\n\t * The visitor's locale: drives renderer strings and value formatting (`'en'` when absent) and,\n\t * when passed, is sent with the submission so the server stores it and the post-submit actions\n\t * (confirmation emails included) render in it. Absent, the submission takes the host's default locale.\n\t */\n\tlocale?: string\n\tlayout?: boolean\n\t/** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */\n\tsubmitLabel?: string\n\t/** \"Next\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */\n\tnextLabel?: string\n\t/** \"Back\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */\n\tprevLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Defaults to `'page'` when omitted. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/**\n\t * Accessible name for an overlay surface (modal/drawer). Hosts choosing between a trigger\n\t * label and the form's own admin title should prefer `form.title` when set, falling back to\n\t * their own label otherwise.\n\t */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/**\n\t * A signed form-context token from `signFormContext` (server/RSC), tying this render to a document\n\t * (e.g. the profile page it is on). Carried alongside the answers, verified server-side, and\n\t * available to `email.recipientSources`; it is never stored as an answer.\n\t */\n\tcontext?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */\n\theader?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button in multi-step forms. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default \"Back\" button in multi-step forms. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n\t/**\n\t * Host-owned effects (navigation, vote persistence, script loading). Each member defaults to\n\t * the built-in DOM behavior; see {@link FormAdapters}. Pass a stable reference (module scope or\n\t * `useMemo`), matching the other injectable props.\n\t */\n\tadapters?: FormAdapters\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** Visually hidden but screen-reader-announced, for the \"Step X of Y\" live region (no host CSS required). */\nconst SR_ONLY: CSSProperties = {\n\tposition: 'absolute',\n\twidth: 1,\n\theight: 1,\n\tpadding: 0,\n\tmargin: -1,\n\toverflow: 'hidden',\n\tclip: 'rect(0, 0, 0, 0)',\n\twhiteSpace: 'nowrap',\n\tborder: 0,\n}\n\n/** The base field name of an error key: the part before a repeater composite suffix (`name[0].sub`). */\nconst baseFieldKey = (key: string): string => {\n\tconst bracket = key.indexOf('[')\n\treturn bracket === -1 ? key : key.slice(0, bracket)\n}\n\n/** The id of the first flow step (in order) that owns an errored field, or undefined when none does. */\nconst firstStepWithError = (flow: FormFlow, errors: FieldErrors): string | undefined => {\n\tconst errored = new Set(Object.keys(errors).map(baseFieldKey))\n\treturn flow.steps.find((flowStep) => flowStep.fields.some((key) => errored.has(key)))?.id\n}\n\n/** The first focusable element under `root` that a step transition should land on (skips hidden/honeypot). */\nconst focusFirstIn = (root: HTMLElement | null, selector: string): void => {\n\tconst target = root?.querySelector<HTMLElement>(selector)\n\ttarget?.focus()\n}\n\n/** A stored button label counts only when it is a non-empty string; anything else falls through. */\nconst storedLabel = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tconverters,\n\tsuccessBehavior = 'replace',\n\tcalcFunctions,\n\tevents,\n\tt,\n\tlocale: localeProp,\n\tlayout,\n\tsubmitLabel,\n\tnextLabel,\n\tprevLabel,\n\tcloseLabel,\n\tsuccessMessage,\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tcontext,\n\tchildren,\n\theader,\n\tclassName,\n\trenderSubmit,\n\trenderNext,\n\trenderBack,\n\tsubmitButtonClassName,\n\tnextButtonClassName,\n\tbackButtonClassName,\n\tadapters,\n}: FormProps) => {\n\tconst locale = localeProp ?? 'en'\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tpresentationRegistry.get(DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\tconst resolvedCloseLabel = closeLabel ?? translate(keys.formClose)\n\tconst resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess)\n\tconst docButtons: FormButtonSettings | undefined = form.buttons\n\tconst labels = useMemo(\n\t\t() => ({\n\t\t\tprev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),\n\t\t\tnext: nextLabel ?? storedLabel(docButtons?.nextLabel) ?? translate(keys.formNext),\n\t\t\tsubmit: submitLabel ?? storedLabel(docButtons?.submitLabel) ?? translate(keys.formSubmit),\n\t\t}),\n\t\t[prevLabel, nextLabel, submitLabel, docButtons, translate]\n\t)\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...seedFieldValues(fields),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and\n\t// submit. Server-resolved source values ride the document (`calcResolved`); custom function halves\n\t// come from the `calcFunctions` prop (functions never serialize).\n\tconst effectiveValues = useMemo(\n\t\t() =>\n\t\t\tcomputeCalcFields(form.fields, state.values, {\n\t\t\t\t...(form.calcResolved ?? {}),\n\t\t\t\t...(calcFunctions ? { functions: calcFunctions } : {}),\n\t\t\t}),\n\t\t[form.fields, state.values, form.calcResolved, calcFunctions]\n\t)\n\n\t// The calc fields' slice of effectiveValues, exposed on context for composed frontends.\n\tconst calcValues = useMemo(() => {\n\t\tconst values: Record<string, number> = {}\n\t\tfor (const field of form.fields) {\n\t\t\tif (calcExpressionOf(field) && isNamedField(field)) {\n\t\t\t\tconst computed = effectiveValues[field.name]\n\t\t\t\tvalues[field.name] = typeof computed === 'number' ? computed : 0\n\t\t\t}\n\t\t}\n\t\treturn values\n\t}, [form.fields, effectiveValues])\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when the form is flagged `multistep` and its flow declares two or more\n\t// steps. With the flag off the form renders as a single page even if flow data is still stored.\n\tconst flow =\n\t\tform.multistep === true && form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst advancingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\t// Per-step error reveal: a field maps to the id of the flow step whose `fields` include it, else the\n\t// default step (single-step forms, or a field assigned to no step), so a single-step form collapses to\n\t// one step and today's behavior.\n\tconst stepIdOfField = useMemo(() => {\n\t\tconst map = new Map<string, string>()\n\t\tif (flow) {\n\t\t\tfor (const flowStep of flow.steps) {\n\t\t\t\tfor (const key of flowStep.fields) {\n\t\t\t\t\tmap.set(key, flowStep.id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (fieldKey: string): string => map.get(fieldKey) ?? DEFAULT_STEP_ID\n\t}, [flow])\n\tconst allStepIds = useMemo(\n\t\t() => (flow ? flow.steps.map((flowStep) => flowStep.id) : [DEFAULT_STEP_ID]),\n\t\t[flow]\n\t)\n\n\t// Focus management for step transitions and blocked advances/submits. A pending request is performed\n\t// by an effect after the render it triggers, so focus lands on the DOM that reflects the new state.\n\tconst formRef = useRef<HTMLFormElement>(null)\n\tconst pendingFocusRef = useRef<'stepStart' | 'firstInvalid' | null>(null)\n\tconst [focusNonce, setFocusNonce] = useState(0)\n\tconst requestFocus = (intent: 'stepStart' | 'firstInvalid') => {\n\t\tpendingFocusRef.current = intent\n\t\tsetFocusNonce((nonce) => nonce + 1)\n\t}\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\n\t/** Visible, answered named fields. Display-only ('none' kind) and nameless (bare) fields never contribute. */\n\tconst answerableFields = (): NamedFormFieldInstance[] =>\n\t\tvisibleFields(form.fields, effectiveValues)\n\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t.filter(isNamedField)\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\n\t/** Answered visible fields as raw submission values, sent to the server on submit. */\n\tconst answeredValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\n\t/**\n\t * Same fields, formatted via `recall` (option labels, Yes/No, localized dates): recall-fidelity\n\t * values for client-rendered templates, matching what the server gives email-team's body.\n\t */\n\tconst formattedValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: recall(field.name) }))\n\n\t/**\n\t * The recall-resolved success response: a redirect's url, or the response message serialized with\n\t * the active `converters` (so host blocks survive). Shared by the success screen and the value\n\t * handed to `onSuccess`, so both stay identical.\n\t */\n\tconst resolveSuccessResponse = (): FormSuccessResponse | undefined => {\n\t\tconst response = form.response\n\t\tif (response?.type === 'redirect') {\n\t\t\treturn { type: 'redirect', url: response.redirect?.url ?? undefined }\n\t\t}\n\t\tconst message =\n\t\t\tresponse?.type === 'message' || response?.type == null ? response?.message : undefined\n\t\tif (!message) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn {\n\t\t\ttype: 'message',\n\t\t\thtml: serializeBody(message, {\n\t\t\t\tvalues: formattedValues(),\n\t\t\t\tdescriptors: descriptorsFor(answerableFields()),\n\t\t\t\tconverters,\n\t\t\t}),\n\t\t}\n\t}\n\n\t// Steps address fields by key: machine names for named fields, block row ids for bare blocks.\n\t// `step.fields` is membership only; render order follows `form.fields` (the order of `visible`),\n\t// so a step shows its fields in the form's field order, not the flow-builder entry order.\n\tconst stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepKeySet = new Set(stepKeys)\n\tconst stepVisible: FormFieldInstance[] = visible.filter((field) =>\n\t\tstepKeySet.has(fieldKey(field))\n\t)\n\n\t// Validate the sub-fields of the given repeaters, one pass per visible row, returning composite-key\n\t// errors (`fieldName[rowIndex].subFieldName`). Shared by submit and step navigation so both mirror\n\t// the server's per-row pass and neither lets an invalid required sub-field slip through.\n\tconst validateRepeaterSubFields = async (\n\t\trepeaters: FormFieldInstance[]\n\t): Promise<FieldErrors> => {\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const field of repeaters.filter(\n\t\t\t(f): f is NamedFormFieldInstance => f.blockType === 'repeater' && isNamedField(f)\n\t\t)) {\n\t\t\tconst rows = Array.isArray(effectiveValues[field.name])\n\t\t\t\t? (effectiveValues[field.name] as Array<Record<string, unknown>>)\n\t\t\t\t: []\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tif (!evaluateCondition(subField.visibleWhen, row)) continue\n\t\t\t\t\tif (!evaluateCondition(subField.validateWhen, row)) continue\n\t\t\t\t\tconst subResult = await validateFieldValue({\n\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})\n\t\t\t\t\tconst compositeKey = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (subResult.errors.length > 0) errors[compositeKey] = subResult.errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors\n\t}\n\n\tconst goNext = async () => {\n\t\t// Re-entrancy guard: a double-click during async validation must not push the same step onto\n\t\t// history twice (which would need two Back presses to undo).\n\t\tif (!flow || !currentStepId || advancingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tadvancingRef.current = true\n\t\ttry {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tstepVisible\n\t\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t\t.filter(isNamedField)\n\t\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t\t.map(async (field) => ({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\t\tregistry,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt: translate,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}))\n\t\t\t)\n\t\t\tlet hasError = false\n\t\t\tfor (const result of results) {\n\t\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\t\tname: result.field.name,\n\t\t\t\t\terrors: result.errors,\n\t\t\t\t})\n\t\t\t\tif (result.errors.length > 0) {\n\t\t\t\t\thasError = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Mirror submit: validate the step's repeater sub-fields too, so a required sub-field can't be\n\t\t\t// skipped past on a non-terminal step (errors surface inline via the composite key).\n\t\t\tconst repeaterErrors = await validateRepeaterSubFields(stepVisible)\n\t\t\tfor (const [compositeKey, errs] of Object.entries(repeaterErrors)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name: compositeKey, errors: errs })\n\t\t\t\thasError = true\n\t\t\t}\n\t\t\tif (hasError) {\n\t\t\t\t// Mark this step attempted so its fields reveal their errors, then focus the first invalid one.\n\t\t\t\trawDispatch({ type: 'MARK_STEP_ATTEMPTED', stepId: currentStepId })\n\t\t\t\trequestFocus('firstInvalid')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = resolveNextStepId(flow, currentStepId, effectiveValues)\n\t\t\tif (!next) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'step.completed',\n\t\t\t\tstepId: currentStepId,\n\t\t\t})\n\t\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\t\tsetCurrentStepId(next)\n\t\t\trequestFocus('stepStart')\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t\t} finally {\n\t\t\tadvancingRef.current = false\n\t\t}\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\trequestFocus('stepStart')\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\t// Jump to an earlier step (from the terminal Submit when an earlier step is invalid), rebuilding the\n\t// Back stack as the flow's linear path up to it so Back still works.\n\tconst navigateToStep = (stepId: string) => {\n\t\tif (!flow || stepId === currentStepId) {\n\t\t\treturn\n\t\t}\n\t\tconst idx = flow.steps.findIndex((flowStep) => flowStep.id === stepId)\n\t\tif (idx < 0) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory(flow.steps.slice(0, idx).map((flowStep) => flowStep.id))\n\t\tsetCurrentStepId(stepId)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId })\n\t}\n\n\t// Perform a pending focus request after the render it triggered, so it lands on the DOM that reflects\n\t// the new state (a changed step, or freshly revealed errors). Null on mount, so the first render never\n\t// steals focus. currentStepId and focusNonce are intentional re-run triggers: a blocked advance keeps\n\t// the same step, so the nonce forces a fresh run; the body reads only refs, hence the ignore.\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: currentStepId/focusNonce are re-run triggers, not read in the body\n\tuseEffect(() => {\n\t\tconst intent = pendingFocusRef.current\n\t\tif (!intent) {\n\t\t\treturn\n\t\t}\n\t\tpendingFocusRef.current = null\n\t\tif (intent === 'firstInvalid') {\n\t\t\tfocusFirstIn(formRef.current, '[aria-invalid=\"true\"]')\n\t\t\treturn\n\t\t}\n\t\tconst region = formRef.current?.querySelector<HTMLElement>('[data-fb-step-region]')\n\t\tif (region) {\n\t\t\tregion.focus()\n\t\t} else {\n\t\t\tfocusFirstIn(formRef.current, 'input:not([type=\"hidden\"]), select, textarea')\n\t\t}\n\t}, [currentStepId, focusNonce])\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// Display-only ('none' kind, e.g. message) and nameless (bare) fields are skipped too, mirroring the server.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t.filter(isNamedField)\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sub-fields within each visible repeater (composite keys `fieldName[rowIndex].subFieldName`),\n\t\t// mirroring the server's per-row pass, so the repeater renderer surfaces them inline.\n\t\tObject.assign(errors, await validateRepeaterSubFields(visible))\n\n\t\tconst hasErrors = Object.keys(errors).length > 0\n\t\t// A terminal submit validates every step, so every step is attempted (its errors may now reveal);\n\t\t// a valid submit clears stale errors without marking anything.\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors, steps: hasErrors ? allStepIds : [] })\n\t\tif (hasErrors) {\n\t\t\tsubmittingRef.current = false\n\t\t\t// On a multi-step form, route to the first step that owns an invalid field rather than failing in\n\t\t\t// place on the terminal step, then focus its first invalid field.\n\t\t\tif (flow && currentStepId) {\n\t\t\t\tconst target = firstStepWithError(flow, errors)\n\t\t\t\tif (target) {\n\t\t\t\t\tnavigateToStep(target)\n\t\t\t\t}\n\t\t\t}\n\t\t\trequestFocus('firstInvalid')\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = answeredValues()\n\t\t// Snapshot before the reserved keys (honeypot, captcha, context) are appended below, so the\n\t\t// success result reports answers only.\n\t\tconst answers = [...values]\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\t// Submit the decoy under a reserved key, not its cosmetic DOM name, so a real field sharing\n\t\t\t\t// that name is never stripped or mistaken for the honeypot on the server.\n\t\t\t\tvalues.push({ field: HONEYPOT_VALUE_KEY, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tif (context) {\n\t\t\t// Rides under a reserved key like the captcha token: verified and stored separately server-side,\n\t\t\t// never merged into the answers.\n\t\t\tvalues.push({ field: CONTEXT_KEY, value: context })\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit({ formId: form.id, values, ...(localeProp ? { locale: localeProp } : {}) })\n\t\t\t: await submitForm({ formId: form.id, values, apiRoute, locale: localeProp })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\t// A submission happened, so the unmount effect must not emit `form.abandoned`, in either mode.\n\t\t\tsubmittedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\t// Resolve the response before a reset clears the answers the recall reads from.\n\t\t\tonSuccess?.(result.submissionId, { response: resolveSuccessResponse(), values: answers })\n\t\t\tif (successBehavior === 'reset') {\n\t\t\t\t// Reset in place: the host handles feedback (e.g. a toast via onSuccess); no success screen.\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'RESET',\n\t\t\t\t\tvalues: { ...seedFieldValues(form.fields), ...(initialValues ?? {}) },\n\t\t\t\t})\n\t\t\t\t// The reducer holds only values/errors; flow position and the lifecycle `started` ref live\n\t\t\t\t// outside it. Reset them too so a multi-step form returns to its first step (not stranded on\n\t\t\t\t// the terminal one) and a fresh fill re-emits `form.started`. `submittedRef` stays set: a\n\t\t\t\t// submission did happen, so the unmount guard must not report the completed form abandoned.\n\t\t\t\tif (flow) {\n\t\t\t\t\tsetCurrentStepId(firstStepId(flow))\n\t\t\t\t\tsetHistory([])\n\t\t\t\t}\n\t\t\t\tstartedRef.current = false\n\t\t\t} else {\n\t\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\t\thandleClose()\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst redirectUrl =\n\t\t\t\tform.response?.type === 'redirect' ? form.response.redirect?.url : undefined\n\t\t\t// Part of submit handling, not rendering: fires on the custom-`children` path too. With a\n\t\t\t// host `navigate` adapter the plugin never touches window.location (no fallback, a throw\n\t\t\t// propagates); push semantics stay the default, passed as intent so the adapter decides how.\n\t\t\tif (typeof redirectUrl === 'string' && redirectUrl.length > 0) {\n\t\t\t\tconst navigate = adapters?.navigate ?? defaultNavigate\n\t\t\t\tnavigate(redirectUrl, { replace: false })\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors, steps: allStepIds })\n\t\t\t}\n\t\t\tconst message = result.message ?? translate(keys.formSubmitFailed)\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\tconst isTerminalStep =\n\t\tflow && currentStepId ? isTerminalStepId(flow, currentStepId, effectiveValues) : true\n\n\t// Enter on a multi-step form: on a non-terminal step, advance like Next (validate, then advance or\n\t// reveal + focus); on the terminal step, fall through to native submit (guarded by `submittingRef`).\n\t// A textarea (Enter = newline), select (confirm), and buttons/submit inputs (activation) are exempt.\n\t// Single-step forms keep native Enter-to-submit. The `<form>` is the plugin's even in children mode,\n\t// so this is the only place a host could get this behavior.\n\tconst handleKeyDown = (event: ReactKeyboardEvent<HTMLFormElement>) => {\n\t\tif (!flow || event.key !== 'Enter') {\n\t\t\treturn\n\t\t}\n\t\tconst target = event.target\n\t\tif (\n\t\t\ttarget instanceof HTMLTextAreaElement ||\n\t\t\ttarget instanceof HTMLSelectElement ||\n\t\t\ttarget instanceof HTMLButtonElement\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\ttarget instanceof HTMLInputElement &&\n\t\t\t(target.type === 'submit' || target.type === 'button')\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (!isTerminalStep) {\n\t\t\tevent.preventDefault()\n\t\t\tvoid goNext()\n\t\t}\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: isTerminalStep,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\t// The \"Step X of Y\" announcement (aria-live + the step region's accessible name).\n\tconst stepStatusText = flow\n\t\t? resolveMessage(translate(keys.formStepStatus), {\n\t\t\t\tcurrent: String(step.stepIndex + 1),\n\t\t\t\ttotal: String(step.stepCount),\n\t\t\t})\n\t\t: ''\n\t// Whether the current step has been attempted and still has a revealed error (drives the step-level alert).\n\tconst currentStepHasError =\n\t\tflow != null &&\n\t\tcurrentStepId != null &&\n\t\tstate.attemptedSteps.has(currentStepId) &&\n\t\tObject.entries(state.errors).some(\n\t\t\t([key, errs]) => errs.length > 0 && stepIdOfField(baseFieldKey(key)) === currentStepId\n\t\t)\n\n\tconst renderedFields = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\tconst contextValue: FormContextValue = {\n\t\tform,\n\t\tstate,\n\t\tdispatch,\n\t\tvalidateField,\n\t\tlocale,\n\t\tstep,\n\t\trendererRegistry,\n\t\tlabels,\n\t\tt: translate,\n\t\teffectiveValues,\n\t\tcalcValues,\n\t\trecall,\n\t\trenderedFields,\n\t\tconverters,\n\t\tstepIdOfField,\n\t}\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={resolvedCloseLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tref={formRef}\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\tconst successResponse = resolveSuccessResponse()\n\t\tconst responseHtml = successResponse?.type === 'message' ? successResponse.html : undefined\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\tresponseHtml ? (\n\t\t\t\t\t\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: responseHtml }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{interpolate(resolvedSuccessMessage, recall)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tref={formRef}\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t{header}\n\t\t\t\t\t{flow ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{/* Announces \"Step X of Y\" politely on each step change. A bare aria-live region, not\n\t\t\t\t\t\t\t role=status, so the single status role stays with the form's success outcome. */}\n\t\t\t\t\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" style={SR_ONLY}>\n\t\t\t\t\t\t\t\t{stepStatusText}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/* Focus lands here on a step change (the region's start), so keyboard/SR users move with it.\n\t\t\t\t\t\t\t A plain focusable container, not a named group; the aria-live region above does the announcing. */}\n\t\t\t\t\t\t\t<div data-fb-step-region tabIndex={-1} className=\"fb-form__step\">\n\t\t\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{currentStepHasError ? (\n\t\t\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__step-error\">\n\t\t\t\t\t\t\t\t\t{translate(keys.formStepInvalid)}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t)}\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<FormControls\n\t\t\t\t\t\tbackButtonClassName={backButtonClassName}\n\t\t\t\t\t\tnextButtonClassName={nextButtonClassName}\n\t\t\t\t\t\tsubmitButtonClassName={submitButtonClassName}\n\t\t\t\t\t\trenderBack={renderBack}\n\t\t\t\t\t\trenderNext={renderNext}\n\t\t\t\t\t\trenderSubmit={renderSubmit}\n\t\t\t\t\t/>\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Form.js","names":[],"sources":["../../src/react/Form.tsx"],"sourcesContent":["'use client'\n\nimport {\n\ttype CSSProperties,\n\ttype FormEvent as ReactFormEvent,\n\ttype KeyboardEvent as ReactKeyboardEvent,\n\ttype ReactNode,\n\tuseCallback,\n\tuseEffect,\n\tuseMemo,\n\tuseReducer,\n\tuseRef,\n\tuseState,\n} from 'react'\nimport type { BodyConverter } from '../actions/body/converters'\nimport { serializeBody } from '../actions/body/serializeBody'\nimport { calcExpressionOf, computeCalcFields } from '../calc/computeCalcFields'\nimport { evaluateCondition } from '../conditions/evaluate'\nimport { noopEventSink } from '../events/noopSink'\nimport type { FormEventSink } from '../events/types'\nimport { fieldKey, isNamedField, type NamedFormFieldInstance } from '../fields/fieldKey'\nimport type { AnyFormFieldDefinition } from '../fields/types'\nimport { firstStepId, isTerminalStepId, resolveNextStepId, stepFieldNames } from '../flow/engine'\nimport type { FormFlow } from '../flow/types'\nimport type {\n\tFormButtonSettings,\n\tFormDocument,\n\tFormPollSettings,\n\tFormResponseSettings,\n} from '../form/types'\nimport {\n\tDEFAULT_PRESENTATION_NAME,\n\tdefaultPresentationDescriptors,\n} from '../presentations/defaults'\nimport { interpolate } from '../recall/interpolate'\nimport { buildRecallResolver, descriptorsFor } from '../recall/resolver'\nimport {\n\tCAPTCHA_TOKEN_KEY,\n\tCONTEXT_KEY,\n\tDEFAULT_HONEYPOT_FIELD,\n\tHONEYPOT_VALUE_KEY,\n} from '../spam/constants'\nimport type { FormFieldInstance, SubmissionValue } from '../submissions/types'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport { resolveMessage } from '../validation/message'\nimport type { AnyValidationRuleDefinition } from '../validation/types'\nimport { defaultNavigate, type FormAdapters } from './adapters'\nimport { cn } from './cn'\nimport type { RendererTranslate } from './contract'\nimport { emitFormEvent } from './events'\nimport { FormContext, type FormContextValue, type FormStepInfo } from './FormContext'\nimport {\n\ttype BackButtonRenderProps,\n\tFormControls,\n\ttype NextButtonRenderProps,\n\ttype SubmitButtonRenderProps,\n} from './FormControls'\nimport { FormFields } from './FormFields'\nimport { Honeypot } from './Honeypot'\nimport { defaultPresentations } from './presentation/presentations'\nimport { type PresentationsConfig, resolvePresentations } from './presentation/registry'\nimport type { FormPresentation } from './presentation/types'\nimport { type RenderersConfig, resolveRenderers } from './registry'\nimport { defaultRenderers } from './renderers'\nimport { buildFieldTypeRegistry, buildValidationRuleRegistry, visibleFields } from './resolveForm'\nimport {\n\tDEFAULT_STEP_ID,\n\ttype FieldErrors,\n\ttype FormAction,\n\tformReducer,\n\tinitialFormState,\n\tseedFieldValues,\n} from './state'\nimport { type SubmitFormResult, type SubmitHandler, submitForm } from './submitForm'\nimport { validateFieldValue } from './validateField'\n\nexport type {\n\tBackButtonRenderProps,\n\tNextButtonRenderProps,\n\tSubmitButtonRenderProps,\n} from './FormControls'\n// FormResponseSettings, FormButtonSettings, FormPollSettings, and FormDocument live in\n// `../form/types` (no 'use client') so server code (e.g. `toFormDocument` in a Server Component)\n// can use them without pulling in this client module. Re-exported here so `./react` and existing\n// `from './Form'` imports keep working unchanged.\nexport type { FormButtonSettings, FormDocument, FormPollSettings, FormResponseSettings }\n\n/**\n * The success response passed to `onSuccess`, recall-resolved and (for a message) serialized with the\n * form's active converters, so a host can render or toast the resolved response without re-deriving it.\n */\nexport type FormSuccessResponse =\n\t| { type: 'message'; html?: string }\n\t| { type: 'redirect'; url?: string }\n\n/** The second argument to `onSuccess`: the resolved success response (an object, so it can grow). */\nexport type FormSuccessResult = {\n\tresponse?: FormSuccessResponse\n\t/** The submitted answers (reserved keys like the honeypot and captcha token excluded), e.g. for `<Poll>` to track the voter's pick. */\n\tvalues?: SubmissionValue[]\n}\n\nexport type FormProps = {\n\tform: FormDocument\n\tfieldTypes?: AnyFormFieldDefinition[]\n\trules?: AnyValidationRuleDefinition[]\n\trenderers?: RenderersConfig\n\tapiRoute?: string\n\tonSubmit?: SubmitHandler\n\t/**\n\t * Called after a successful submission with the submission id and the resolved success response\n\t * (recall-applied, serialized with `converters`), so a host can toast or render it. Fires in both\n\t * `successBehavior` modes and on the custom-`children` path.\n\t */\n\tonSuccess?: (submissionId?: string, result?: FormSuccessResult) => void\n\tonError?: (message: string) => void\n\t/**\n\t * Custom Lexical block converters (e.g. host `icon`/`badge` blocks) spread over the defaults for the\n\t * client serializer, so those blocks survive in the success message and in the `onSuccess` response.\n\t */\n\tconverters?: Record<string, BodyConverter>\n\t/**\n\t * What happens on a successful submit. `'replace'` (default) swaps the form for the success screen;\n\t * `'reset'` clears the fields in place and shows no success screen, so a host can toast via `onSuccess`\n\t * and keep the form usable.\n\t */\n\tsuccessBehavior?: 'replace' | 'reset'\n\t/**\n\t * Client halves of the plugin's registered `calc.functions` (same keys), used for the live calc\n\t * preview only; the server always recomputes authoritatively at submit with the registry's own\n\t * functions.\n\t */\n\tcalcFunctions?: Record<string, (args: number[]) => number>\n\tevents?: FormEventSink\n\tt?: RendererTranslate\n\t/** The visitor's locale for renderer strings and value formatting, e.g. `en-US` (`'en'` when absent). */\n\tlocale?: string\n\t/**\n\t * The Payload content locale to submit under, one of the host's `localization` codes (`en`, `de`),\n\t * sent as `?locale=` so the server stores it on the submission and the post-submit actions\n\t * (confirmation emails included) render in it. A `<Poll>` also reads its results in it. Separate\n\t * from `locale` because a formatting tag like `en-US` is usually not a content locale code.\n\t * Absent, the submission takes the host's default locale.\n\t */\n\tsubmissionLocale?: string\n\tlayout?: boolean\n\t/** Submit button label. Precedence: this prop, then the form's `buttons.submitLabel`, then the translated default. */\n\tsubmitLabel?: string\n\t/** \"Next\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.nextLabel`, then the translated default. */\n\tnextLabel?: string\n\t/** \"Back\" button label for multi-step forms. Precedence: this prop, then the form's `buttons.prevLabel`, then the translated default. */\n\tprevLabel?: string\n\t/** Label for the overlay close control (modal/drawer). */\n\tcloseLabel?: string\n\tsuccessMessage?: string\n\t/** Active presentation: a name into the registry or an inline presentation. Defaults to `'page'` when omitted. */\n\tpresentation?: string | FormPresentation\n\t/** Per-render presentation overrides merged onto the defaults (add, replace, or `false` to remove). */\n\tpresentations?: PresentationsConfig\n\t/** Invoked when an overlay presentation dismisses (close button, Escape, outside click, or `dismissOnSuccess`). */\n\tonClose?: () => void\n\t/**\n\t * Accessible name for an overlay surface (modal/drawer). Hosts choosing between a trigger\n\t * label and the form's own admin title should prefer `form.title` when set, falling back to\n\t * their own label otherwise.\n\t */\n\ttitle?: string\n\t/** Seed initial field values (e.g. from `valuesFromSearchParams`). Still validated on submit. */\n\tinitialValues?: Record<string, unknown>\n\t/** Honeypot decoy (on by default). `false` removes it; `{ name }` matches a customized server `spam.honeypot.fieldName`. */\n\thoneypot?: false | { name?: string }\n\t/** A token from your captcha widget; verified server-side when a captcha provider is configured. */\n\tcaptchaToken?: string\n\t/**\n\t * A signed form-context token from `signFormContext` (server/RSC), tying this render to a document\n\t * (e.g. the profile page it is on). Carried alongside the answers, verified server-side, and\n\t * available to `email.recipientSources`; it is never stored as an answer.\n\t */\n\tcontext?: string\n\t/** Custom layout: render fields with `useField`/`useFormState` instead of the auto-rendered field loop. */\n\tchildren?: ReactNode\n\t/** Chrome rendered inside the form, above the fields, in default mode (e.g. `<FormSteps />`). */\n\theader?: ReactNode\n\t/** Additional CSS class names applied to the root `<form>` element (and the success node). */\n\tclassName?: string\n\t/** Replace the default submit button entirely. Receives the resolved label and submitting state. */\n\trenderSubmit?: (props: SubmitButtonRenderProps) => ReactNode\n\t/** Replace the default \"Next\" button in multi-step forms. */\n\trenderNext?: (props: NextButtonRenderProps) => ReactNode\n\t/** Replace the default \"Back\" button in multi-step forms. */\n\trenderBack?: (props: BackButtonRenderProps) => ReactNode\n\t/** CSS class forwarded to the default submit `<button>`. Ignored when `renderSubmit` is provided. */\n\tsubmitButtonClassName?: string\n\t/** CSS class forwarded to the default \"Next\" `<button>`. Ignored when `renderNext` is provided. */\n\tnextButtonClassName?: string\n\t/** CSS class forwarded to the default \"Back\" `<button>`. Ignored when `renderBack` is provided. */\n\tbackButtonClassName?: string\n\t/**\n\t * Host-owned effects (navigation, vote persistence, script loading). Each member defaults to\n\t * the built-in DOM behavior; see {@link FormAdapters}. Pass a stable reference (module scope or\n\t * `useMemo`), matching the other injectable props.\n\t */\n\tadapters?: FormAdapters\n}\n\nconst isEmpty = (value: unknown): boolean =>\n\tvalue == null || value === '' || (Array.isArray(value) && value.length === 0)\n\n/** Visually hidden but screen-reader-announced, for the \"Step X of Y\" live region (no host CSS required). */\nconst SR_ONLY: CSSProperties = {\n\tposition: 'absolute',\n\twidth: 1,\n\theight: 1,\n\tpadding: 0,\n\tmargin: -1,\n\toverflow: 'hidden',\n\tclip: 'rect(0, 0, 0, 0)',\n\twhiteSpace: 'nowrap',\n\tborder: 0,\n}\n\n/** The base field name of an error key: the part before a repeater composite suffix (`name[0].sub`). */\nconst baseFieldKey = (key: string): string => {\n\tconst bracket = key.indexOf('[')\n\treturn bracket === -1 ? key : key.slice(0, bracket)\n}\n\n/** The id of the first flow step (in order) that owns an errored field, or undefined when none does. */\nconst firstStepWithError = (flow: FormFlow, errors: FieldErrors): string | undefined => {\n\tconst errored = new Set(Object.keys(errors).map(baseFieldKey))\n\treturn flow.steps.find((flowStep) => flowStep.fields.some((key) => errored.has(key)))?.id\n}\n\n/** The first focusable element under `root` that a step transition should land on (skips hidden/honeypot). */\nconst focusFirstIn = (root: HTMLElement | null, selector: string): void => {\n\tconst target = root?.querySelector<HTMLElement>(selector)\n\ttarget?.focus()\n}\n\n/** A stored button label counts only when it is a non-empty string; anything else falls through. */\nconst storedLabel = (value: unknown): string | undefined =>\n\ttypeof value === 'string' && value.length > 0 ? value : undefined\n\n/** The headless form controller: state, progressive client validation, conditional visibility, submission, events. */\nexport const Form = ({\n\tform,\n\tfieldTypes,\n\trules,\n\trenderers,\n\tapiRoute,\n\tonSubmit,\n\tonSuccess,\n\tonError,\n\tconverters,\n\tsuccessBehavior = 'replace',\n\tcalcFunctions,\n\tevents,\n\tt,\n\tlocale: localeProp,\n\tsubmissionLocale,\n\tlayout,\n\tsubmitLabel,\n\tnextLabel,\n\tprevLabel,\n\tcloseLabel,\n\tsuccessMessage,\n\tpresentation,\n\tpresentations,\n\tonClose,\n\ttitle,\n\tinitialValues,\n\thoneypot,\n\tcaptchaToken,\n\tcontext,\n\tchildren,\n\theader,\n\tclassName,\n\trenderSubmit,\n\trenderNext,\n\trenderBack,\n\tsubmitButtonClassName,\n\tnextButtonClassName,\n\tbackButtonClassName,\n\tadapters,\n}: FormProps) => {\n\tconst locale = localeProp ?? 'en'\n\tconst honeypotName = honeypot === false ? null : (honeypot?.name ?? DEFAULT_HONEYPOT_FIELD)\n\tconst honeypotRef = useRef<HTMLInputElement>(null)\n\tconst registry = useMemo(() => buildFieldTypeRegistry(fieldTypes), [fieldTypes])\n\tconst ruleRegistry = useMemo(() => buildValidationRuleRegistry(rules), [rules])\n\tconst rendererRegistry = useMemo(() => resolveRenderers(defaultRenderers, renderers), [renderers])\n\tconst presentationRegistry = useMemo(\n\t\t() => resolvePresentations(defaultPresentations, presentations),\n\t\t[presentations]\n\t)\n\tconst activePresentation: FormPresentation =\n\t\ttypeof presentation === 'object'\n\t\t\t? presentation\n\t\t\t: (presentationRegistry.get(presentation ?? DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tpresentationRegistry.get(DEFAULT_PRESENTATION_NAME) ??\n\t\t\t\tdefaultPresentationDescriptors.page)\n\tconst fieldsByName = useMemo(\n\t\t() => new Map(form.fields.filter(isNamedField).map((field) => [field.name, field])),\n\t\t[form.fields]\n\t)\n\tconst translate = useMemo<RendererTranslate>(() => t ?? makeTranslate(en), [t])\n\tconst resolvedCloseLabel = closeLabel ?? translate(keys.formClose)\n\tconst resolvedSuccessMessage = successMessage ?? translate(keys.formSuccess)\n\tconst docButtons: FormButtonSettings | undefined = form.buttons\n\tconst labels = useMemo(\n\t\t() => ({\n\t\t\tprev: prevLabel ?? storedLabel(docButtons?.prevLabel) ?? translate(keys.formBack),\n\t\t\tnext: nextLabel ?? storedLabel(docButtons?.nextLabel) ?? translate(keys.formNext),\n\t\t\tsubmit: submitLabel ?? storedLabel(docButtons?.submitLabel) ?? translate(keys.formSubmit),\n\t\t}),\n\t\t[prevLabel, nextLabel, submitLabel, docButtons, translate]\n\t)\n\n\t// Latest-value refs so event emission and the mount/unmount effect tolerate an inline `events` prop or a changing form id.\n\tconst sinkRef = useRef<FormEventSink>(noopEventSink)\n\tsinkRef.current = events ?? noopEventSink\n\tconst formIdRef = useRef('')\n\tformIdRef.current = String(form.id)\n\n\tconst [state, rawDispatch] = useReducer(formReducer, form.fields, (fields) =>\n\t\tinitialFormState({\n\t\t\t...seedFieldValues(fields),\n\t\t\t...(initialValues ?? {}),\n\t\t})\n\t)\n\n\t// Authoritative values for derived (calc) fields, recomputed from user answers on every change. The\n\t// server recomputes these too at submit; the client copy drives the live calc renderer, recall, and\n\t// submit. Server-resolved source values ride the document (`calcResolved`); custom function halves\n\t// come from the `calcFunctions` prop (functions never serialize).\n\tconst effectiveValues = useMemo(\n\t\t() =>\n\t\t\tcomputeCalcFields(form.fields, state.values, {\n\t\t\t\t...(form.calcResolved ?? {}),\n\t\t\t\t...(calcFunctions ? { functions: calcFunctions } : {}),\n\t\t\t}),\n\t\t[form.fields, state.values, form.calcResolved, calcFunctions]\n\t)\n\n\t// The calc fields' slice of effectiveValues, exposed on context for composed frontends.\n\tconst calcValues = useMemo(() => {\n\t\tconst values: Record<string, number> = {}\n\t\tfor (const field of form.fields) {\n\t\t\tif (calcExpressionOf(field) && isNamedField(field)) {\n\t\t\t\tconst computed = effectiveValues[field.name]\n\t\t\t\tvalues[field.name] = typeof computed === 'number' ? computed : 0\n\t\t\t}\n\t\t}\n\t\treturn values\n\t}, [form.fields, effectiveValues])\n\n\tconst recall = useMemo(\n\t\t() =>\n\t\t\tbuildRecallResolver({\n\t\t\t\tfields: form.fields,\n\t\t\t\tvalues: effectiveValues,\n\t\t\t\tregistry,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}),\n\t\t[form.fields, effectiveValues, registry, locale, translate]\n\t)\n\n\t// Multi-step is active only when the form is flagged `multistep` and its flow declares two or more\n\t// steps. With the flag off the form renders as a single page even if flow data is still stored.\n\tconst flow =\n\t\tform.multistep === true && form.flow && form.flow.steps.length >= 2 ? form.flow : undefined\n\tconst [currentStepId, setCurrentStepId] = useState<string | undefined>(() =>\n\t\tflow ? firstStepId(flow) : undefined\n\t)\n\tconst [history, setHistory] = useState<string[]>([])\n\n\tconst startedRef = useRef(false)\n\tconst submittedRef = useRef(false)\n\tconst submittingRef = useRef(false)\n\tconst advancingRef = useRef(false)\n\tconst flowRef = useRef(flow)\n\tflowRef.current = flow\n\n\t// Per-step error reveal: a field maps to the id of the flow step whose `fields` include it, else the\n\t// default step (single-step forms, or a field assigned to no step), so a single-step form collapses to\n\t// one step and today's behavior.\n\tconst stepIdOfField = useMemo(() => {\n\t\tconst map = new Map<string, string>()\n\t\tif (flow) {\n\t\t\tfor (const flowStep of flow.steps) {\n\t\t\t\tfor (const key of flowStep.fields) {\n\t\t\t\t\tmap.set(key, flowStep.id)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn (fieldKey: string): string => map.get(fieldKey) ?? DEFAULT_STEP_ID\n\t}, [flow])\n\tconst allStepIds = useMemo(\n\t\t() => (flow ? flow.steps.map((flowStep) => flowStep.id) : [DEFAULT_STEP_ID]),\n\t\t[flow]\n\t)\n\n\t// Focus management for step transitions and blocked advances/submits. A pending request is performed\n\t// by an effect after the render it triggers, so focus lands on the DOM that reflects the new state.\n\tconst formRef = useRef<HTMLFormElement>(null)\n\tconst pendingFocusRef = useRef<'stepStart' | 'firstInvalid' | null>(null)\n\tconst [focusNonce, setFocusNonce] = useState(0)\n\tconst requestFocus = (intent: 'stepStart' | 'firstInvalid') => {\n\t\tpendingFocusRef.current = intent\n\t\tsetFocusNonce((nonce) => nonce + 1)\n\t}\n\n\tconst dispatch = useCallback((action: FormAction) => {\n\t\tif (action.type === 'SET_VALUE' && !startedRef.current) {\n\t\t\tstartedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.started' })\n\t\t}\n\t\trawDispatch(action)\n\t}, [])\n\n\tconst validateField = useCallback(\n\t\t(name: string, value: unknown) => {\n\t\t\tconst field = fieldsByName.get(name)\n\t\t\tif (!field) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst answers = { ...effectiveValues, [name]: value }\n\t\t\t// Mirror the server: a field whose `validateWhen` is unmet is not validated; clear any stale error.\n\t\t\tif (!evaluateCondition(field.validateWhen, answers)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors: [] })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tvoid validateFieldValue({\n\t\t\t\tfield,\n\t\t\t\tvalue,\n\t\t\t\tregistry,\n\t\t\t\truleRegistry,\n\t\t\t\tanswers,\n\t\t\t\tlocale,\n\t\t\t\tt: translate,\n\t\t\t}).then(({ errors }) => {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name, errors })\n\t\t\t\tconst [firstError] = errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t\t[fieldsByName, effectiveValues, registry, ruleRegistry, locale, translate]\n\t)\n\n\tconst visible = visibleFields(form.fields, effectiveValues)\n\n\t/** Visible, answered named fields. Display-only ('none' kind) and nameless (bare) fields never contribute. */\n\tconst answerableFields = (): NamedFormFieldInstance[] =>\n\t\tvisibleFields(form.fields, effectiveValues)\n\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t.filter(isNamedField)\n\t\t\t.filter((field) => !isEmpty(effectiveValues[field.name]))\n\n\t/** Answered visible fields as raw submission values, sent to the server on submit. */\n\tconst answeredValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: effectiveValues[field.name] }))\n\n\t/**\n\t * Same fields, formatted via `recall` (option labels, Yes/No, localized dates): recall-fidelity\n\t * values for client-rendered templates, matching what the server gives email-team's body.\n\t */\n\tconst formattedValues = (): SubmissionValue[] =>\n\t\tanswerableFields().map((field) => ({ field: field.name, value: recall(field.name) }))\n\n\t/**\n\t * The recall-resolved success response: a redirect's url, or the response message serialized with\n\t * the active `converters` (so host blocks survive). Shared by the success screen and the value\n\t * handed to `onSuccess`, so both stay identical.\n\t */\n\tconst resolveSuccessResponse = (): FormSuccessResponse | undefined => {\n\t\tconst response = form.response\n\t\tif (response?.type === 'redirect') {\n\t\t\treturn { type: 'redirect', url: response.redirect?.url ?? undefined }\n\t\t}\n\t\tconst message =\n\t\t\tresponse?.type === 'message' || response?.type == null ? response?.message : undefined\n\t\tif (!message) {\n\t\t\treturn undefined\n\t\t}\n\t\treturn {\n\t\t\ttype: 'message',\n\t\t\thtml: serializeBody(message, {\n\t\t\t\tvalues: formattedValues(),\n\t\t\t\tdescriptors: descriptorsFor(answerableFields()),\n\t\t\t\tconverters,\n\t\t\t}),\n\t\t}\n\t}\n\n\t// Steps address fields by key: machine names for named fields, block row ids for bare blocks.\n\t// `step.fields` is membership only; render order follows `form.fields` (the order of `visible`),\n\t// so a step shows its fields in the form's field order, not the flow-builder entry order.\n\tconst stepKeys = flow && currentStepId ? stepFieldNames(flow, currentStepId) : []\n\tconst stepKeySet = new Set(stepKeys)\n\tconst stepVisible: FormFieldInstance[] = visible.filter((field) =>\n\t\tstepKeySet.has(fieldKey(field))\n\t)\n\n\t// Validate the sub-fields of the given repeaters, one pass per visible row, returning composite-key\n\t// errors (`fieldName[rowIndex].subFieldName`). Shared by submit and step navigation so both mirror\n\t// the server's per-row pass and neither lets an invalid required sub-field slip through.\n\tconst validateRepeaterSubFields = async (\n\t\trepeaters: FormFieldInstance[]\n\t): Promise<FieldErrors> => {\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const field of repeaters.filter(\n\t\t\t(f): f is NamedFormFieldInstance => f.blockType === 'repeater' && isNamedField(f)\n\t\t)) {\n\t\t\tconst rows = Array.isArray(effectiveValues[field.name])\n\t\t\t\t? (effectiveValues[field.name] as Array<Record<string, unknown>>)\n\t\t\t\t: []\n\t\t\tconst subFields = (\n\t\t\t\tArray.isArray(field.subFields) ? (field.subFields as FormFieldInstance[]) : []\n\t\t\t).filter(isNamedField)\n\t\t\tfor (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {\n\t\t\t\tconst row = rows[rowIndex] ?? {}\n\t\t\t\tfor (const subField of subFields) {\n\t\t\t\t\tif (!evaluateCondition(subField.visibleWhen, row)) continue\n\t\t\t\t\tif (!evaluateCondition(subField.validateWhen, row)) continue\n\t\t\t\t\tconst subResult = await validateFieldValue({\n\t\t\t\t\t\tfield: subField,\n\t\t\t\t\t\tvalue: row[subField.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: row,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})\n\t\t\t\t\tconst compositeKey = `${field.name}[${rowIndex}].${subField.name}`\n\t\t\t\t\tif (subResult.errors.length > 0) errors[compositeKey] = subResult.errors\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn errors\n\t}\n\n\tconst goNext = async () => {\n\t\t// Re-entrancy guard: a double-click during async validation must not push the same step onto\n\t\t// history twice (which would need two Back presses to undo).\n\t\tif (!flow || !currentStepId || advancingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tadvancingRef.current = true\n\t\ttry {\n\t\t\tconst results = await Promise.all(\n\t\t\t\tstepVisible\n\t\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t\t.filter(isNamedField)\n\t\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t\t.map(async (field) => ({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\t\tfield,\n\t\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\t\tregistry,\n\t\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\t\tlocale,\n\t\t\t\t\t\t\tt: translate,\n\t\t\t\t\t\t})),\n\t\t\t\t\t}))\n\t\t\t)\n\t\t\tlet hasError = false\n\t\t\tfor (const result of results) {\n\t\t\t\trawDispatch({ type: 'TOUCH', name: result.field.name })\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'SET_FIELD_ISSUES',\n\t\t\t\t\tname: result.field.name,\n\t\t\t\t\terrors: result.errors,\n\t\t\t\t})\n\t\t\t\tif (result.errors.length > 0) {\n\t\t\t\t\thasError = true\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Mirror submit: validate the step's repeater sub-fields too, so a required sub-field can't be\n\t\t\t// skipped past on a non-terminal step (errors surface inline via the composite key).\n\t\t\tconst repeaterErrors = await validateRepeaterSubFields(stepVisible)\n\t\t\tfor (const [compositeKey, errs] of Object.entries(repeaterErrors)) {\n\t\t\t\trawDispatch({ type: 'SET_FIELD_ISSUES', name: compositeKey, errors: errs })\n\t\t\t\thasError = true\n\t\t\t}\n\t\t\tif (hasError) {\n\t\t\t\t// Mark this step attempted so its fields reveal their errors, then focus the first invalid one.\n\t\t\t\trawDispatch({ type: 'MARK_STEP_ATTEMPTED', stepId: currentStepId })\n\t\t\t\trequestFocus('firstInvalid')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst next = resolveNextStepId(flow, currentStepId, effectiveValues)\n\t\t\tif (!next) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'step.completed',\n\t\t\t\tstepId: currentStepId,\n\t\t\t})\n\t\t\tsetHistory((prev) => [...prev, currentStepId])\n\t\t\tsetCurrentStepId(next)\n\t\t\trequestFocus('stepStart')\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: next })\n\t\t} finally {\n\t\t\tadvancingRef.current = false\n\t\t}\n\t}\n\n\tconst goBack = () => {\n\t\tconst prev = history[history.length - 1]\n\t\tif (prev === undefined) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory((entries) => entries.slice(0, -1))\n\t\tsetCurrentStepId(prev)\n\t\trequestFocus('stepStart')\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: prev })\n\t}\n\n\t// Jump to an earlier step (from the terminal Submit when an earlier step is invalid), rebuilding the\n\t// Back stack as the flow's linear path up to it so Back still works.\n\tconst navigateToStep = (stepId: string) => {\n\t\tif (!flow || stepId === currentStepId) {\n\t\t\treturn\n\t\t}\n\t\tconst idx = flow.steps.findIndex((flowStep) => flowStep.id === stepId)\n\t\tif (idx < 0) {\n\t\t\treturn\n\t\t}\n\t\tsetHistory(flow.steps.slice(0, idx).map((flowStep) => flowStep.id))\n\t\tsetCurrentStepId(stepId)\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId })\n\t}\n\n\t// Perform a pending focus request after the render it triggered, so it lands on the DOM that reflects\n\t// the new state (a changed step, or freshly revealed errors). Null on mount, so the first render never\n\t// steals focus. currentStepId and focusNonce are intentional re-run triggers: a blocked advance keeps\n\t// the same step, so the nonce forces a fresh run; the body reads only refs, hence the ignore.\n\t// biome-ignore lint/correctness/useExhaustiveDependencies: currentStepId/focusNonce are re-run triggers, not read in the body\n\tuseEffect(() => {\n\t\tconst intent = pendingFocusRef.current\n\t\tif (!intent) {\n\t\t\treturn\n\t\t}\n\t\tpendingFocusRef.current = null\n\t\tif (intent === 'firstInvalid') {\n\t\t\tfocusFirstIn(formRef.current, '[aria-invalid=\"true\"]')\n\t\t\treturn\n\t\t}\n\t\tconst region = formRef.current?.querySelector<HTMLElement>('[data-fb-step-region]')\n\t\tif (region) {\n\t\t\tregion.focus()\n\t\t} else {\n\t\t\tfocusFirstIn(formRef.current, 'input:not([type=\"hidden\"]), select, textarea')\n\t\t}\n\t}, [currentStepId, focusNonce])\n\n\tuseEffect(() => {\n\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.viewed' })\n\t\tconst mountFlow = flowRef.current\n\t\tif (mountFlow) {\n\t\t\tconst first = firstStepId(mountFlow)\n\t\t\tif (first) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'step.viewed', stepId: first })\n\t\t\t}\n\t\t}\n\t\treturn () => {\n\t\t\tif (!submittedRef.current && !submittingRef.current) {\n\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, { type: 'form.abandoned' })\n\t\t\t}\n\t\t}\n\t}, [])\n\n\tconst handleClose = useCallback(() => {\n\t\tonClose?.()\n\t}, [onClose])\n\n\tconst handleSubmit = async (event: ReactFormEvent<HTMLFormElement>) => {\n\t\tevent.preventDefault()\n\t\t// Re-entrancy guard: claim the in-flight slot before the async validation window so a fast second\n\t\t// activation (double-click, Enter + click) cannot reach the transport and POST the submission twice.\n\t\tif (submittingRef.current) {\n\t\t\treturn\n\t\t}\n\t\tsubmittingRef.current = true\n\t\tconst visible = visibleFields(form.fields, effectiveValues)\n\t\tconst results = await Promise.all(\n\t\t\t// Calc fields carry no rules and have no input; they are always satisfied, so skip validating them.\n\t\t\t// Display-only ('none' kind, e.g. message) and nameless (bare) fields are skipped too, mirroring the server.\n\t\t\t// A field whose `validateWhen` is unmet is skipped too, mirroring the server (no client/server divergence).\n\t\t\tvisible\n\t\t\t\t.filter((field) => !calcExpressionOf(field))\n\t\t\t\t.filter((field) => registry.get(field.blockType)?.value !== 'none')\n\t\t\t\t.filter(isNamedField)\n\t\t\t\t.filter((field) => evaluateCondition(field.validateWhen, effectiveValues))\n\t\t\t\t.map(async (field) => ({\n\t\t\t\t\tfield,\n\t\t\t\t\t...(await validateFieldValue({\n\t\t\t\t\t\tfield,\n\t\t\t\t\t\tvalue: effectiveValues[field.name],\n\t\t\t\t\t\tregistry,\n\t\t\t\t\t\truleRegistry,\n\t\t\t\t\t\tanswers: effectiveValues,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t\tt: translate,\n\t\t\t\t\t})),\n\t\t\t\t}))\n\t\t)\n\t\tconst errors: FieldErrors = {}\n\t\tfor (const result of results) {\n\t\t\tif (result.errors.length > 0) {\n\t\t\t\terrors[result.field.name] = result.errors\n\t\t\t\tconst [firstError] = result.errors\n\t\t\t\tif (firstError !== undefined) {\n\t\t\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\t\t\ttype: 'field.errored',\n\t\t\t\t\t\tfield: result.field.name,\n\t\t\t\t\t\tmessage: firstError,\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Validate sub-fields within each visible repeater (composite keys `fieldName[rowIndex].subFieldName`),\n\t\t// mirroring the server's per-row pass, so the repeater renderer surfaces them inline.\n\t\tObject.assign(errors, await validateRepeaterSubFields(visible))\n\n\t\tconst hasErrors = Object.keys(errors).length > 0\n\t\t// A terminal submit validates every step, so every step is attempted (its errors may now reveal);\n\t\t// a valid submit clears stale errors without marking anything.\n\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors, steps: hasErrors ? allStepIds : [] })\n\t\tif (hasErrors) {\n\t\t\tsubmittingRef.current = false\n\t\t\t// On a multi-step form, route to the first step that owns an invalid field rather than failing in\n\t\t\t// place on the terminal step, then focus its first invalid field.\n\t\t\tif (flow && currentStepId) {\n\t\t\t\tconst target = firstStepWithError(flow, errors)\n\t\t\t\tif (target) {\n\t\t\t\t\tnavigateToStep(target)\n\t\t\t\t}\n\t\t\t}\n\t\t\trequestFocus('firstInvalid')\n\t\t\treturn\n\t\t}\n\t\trawDispatch({ type: 'SUBMIT_START' })\n\t\tconst values: SubmissionValue[] = answeredValues()\n\t\t// Snapshot before the reserved keys (honeypot, captcha, context) are appended below, so the\n\t\t// success result reports answers only.\n\t\tconst answers = [...values]\n\t\tif (honeypotName) {\n\t\t\tconst decoy = honeypotRef.current?.value ?? ''\n\t\t\tif (decoy !== '') {\n\t\t\t\t// Submit the decoy under a reserved key, not its cosmetic DOM name, so a real field sharing\n\t\t\t\t// that name is never stripped or mistaken for the honeypot on the server.\n\t\t\t\tvalues.push({ field: HONEYPOT_VALUE_KEY, value: decoy })\n\t\t\t}\n\t\t}\n\t\tif (captchaToken) {\n\t\t\tvalues.push({ field: CAPTCHA_TOKEN_KEY, value: captchaToken })\n\t\t}\n\t\tif (context) {\n\t\t\t// Rides under a reserved key like the captcha token: verified and stored separately server-side,\n\t\t\t// never merged into the answers.\n\t\t\tvalues.push({ field: CONTEXT_KEY, value: context })\n\t\t}\n\t\tconst input = {\n\t\t\tformId: form.id,\n\t\t\tvalues,\n\t\t\t...(submissionLocale ? { locale: submissionLocale } : {}),\n\t\t}\n\t\tconst result: SubmitFormResult = onSubmit\n\t\t\t? await onSubmit(input)\n\t\t\t: await submitForm({ ...input, apiRoute })\n\t\tsubmittingRef.current = false\n\t\tif (result.ok) {\n\t\t\t// A submission happened, so the unmount effect must not emit `form.abandoned`, in either mode.\n\t\t\tsubmittedRef.current = true\n\t\t\temitFormEvent(sinkRef.current, formIdRef.current, {\n\t\t\t\ttype: 'submission.created',\n\t\t\t\tsubmissionId: result.submissionId,\n\t\t\t})\n\t\t\t// Resolve the response before a reset clears the answers the recall reads from.\n\t\t\tonSuccess?.(result.submissionId, { response: resolveSuccessResponse(), values: answers })\n\t\t\tif (successBehavior === 'reset') {\n\t\t\t\t// Reset in place: the host handles feedback (e.g. a toast via onSuccess); no success screen.\n\t\t\t\trawDispatch({\n\t\t\t\t\ttype: 'RESET',\n\t\t\t\t\tvalues: { ...seedFieldValues(form.fields), ...(initialValues ?? {}) },\n\t\t\t\t})\n\t\t\t\t// The reducer holds only values/errors; flow position and the lifecycle `started` ref live\n\t\t\t\t// outside it. Reset them too so a multi-step form returns to its first step (not stranded on\n\t\t\t\t// the terminal one) and a fresh fill re-emits `form.started`. `submittedRef` stays set: a\n\t\t\t\t// submission did happen, so the unmount guard must not report the completed form abandoned.\n\t\t\t\tif (flow) {\n\t\t\t\t\tsetCurrentStepId(firstStepId(flow))\n\t\t\t\t\tsetHistory([])\n\t\t\t\t}\n\t\t\t\tstartedRef.current = false\n\t\t\t} else {\n\t\t\t\trawDispatch({ type: 'SUBMIT_SUCCESS' })\n\t\t\t\tif (activePresentation.dismissOnSuccess) {\n\t\t\t\t\thandleClose()\n\t\t\t\t}\n\t\t\t}\n\t\t\tconst redirectUrl =\n\t\t\t\tform.response?.type === 'redirect' ? form.response.redirect?.url : undefined\n\t\t\t// Part of submit handling, not rendering: fires on the custom-`children` path too. With a\n\t\t\t// host `navigate` adapter the plugin never touches window.location (no fallback, a throw\n\t\t\t// propagates); push semantics stay the default, passed as intent so the adapter decides how.\n\t\t\tif (typeof redirectUrl === 'string' && redirectUrl.length > 0) {\n\t\t\t\tconst navigate = adapters?.navigate ?? defaultNavigate\n\t\t\t\tnavigate(redirectUrl, { replace: false })\n\t\t\t}\n\t\t} else {\n\t\t\tif (result.fieldErrors) {\n\t\t\t\trawDispatch({ type: 'SET_ALL_ISSUES', errors: result.fieldErrors, steps: allStepIds })\n\t\t\t}\n\t\t\tconst message = result.message ?? translate(keys.formSubmitFailed)\n\t\t\trawDispatch({ type: 'SUBMIT_ERROR', message })\n\t\t\tonError?.(message)\n\t\t}\n\t}\n\n\tconst isTerminalStep =\n\t\tflow && currentStepId ? isTerminalStepId(flow, currentStepId, effectiveValues) : true\n\n\t// Enter on a multi-step form: on a non-terminal step, advance like Next (validate, then advance or\n\t// reveal + focus); on the terminal step, fall through to native submit (guarded by `submittingRef`).\n\t// A textarea (Enter = newline), select (confirm), and buttons/submit inputs (activation) are exempt.\n\t// Single-step forms keep native Enter-to-submit. The `<form>` is the plugin's even in children mode,\n\t// so this is the only place a host could get this behavior.\n\tconst handleKeyDown = (event: ReactKeyboardEvent<HTMLFormElement>) => {\n\t\tif (!flow || event.key !== 'Enter') {\n\t\t\treturn\n\t\t}\n\t\tconst target = event.target\n\t\tif (\n\t\t\ttarget instanceof HTMLTextAreaElement ||\n\t\t\ttarget instanceof HTMLSelectElement ||\n\t\t\ttarget instanceof HTMLButtonElement\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (\n\t\t\ttarget instanceof HTMLInputElement &&\n\t\t\t(target.type === 'submit' || target.type === 'button')\n\t\t) {\n\t\t\treturn\n\t\t}\n\t\tif (!isTerminalStep) {\n\t\t\tevent.preventDefault()\n\t\t\tvoid goNext()\n\t\t}\n\t}\n\n\tconst step: FormStepInfo = flow\n\t\t? {\n\t\t\t\tflow,\n\t\t\t\tcurrentStepId,\n\t\t\t\tstepIndex: flow.steps.findIndex((s) => s.id === currentStepId),\n\t\t\t\tstepCount: flow.steps.length,\n\t\t\t\tisFirst: history.length === 0,\n\t\t\t\tisTerminal: isTerminalStep,\n\t\t\t\tgoNext: () => {\n\t\t\t\t\tvoid goNext()\n\t\t\t\t},\n\t\t\t\tgoBack,\n\t\t\t}\n\t\t: {\n\t\t\t\tstepIndex: 0,\n\t\t\t\tstepCount: 1,\n\t\t\t\tisFirst: true,\n\t\t\t\tisTerminal: true,\n\t\t\t\tgoNext: () => {},\n\t\t\t\tgoBack: () => {},\n\t\t\t}\n\n\t// The \"Step X of Y\" announcement (aria-live + the step region's accessible name).\n\tconst stepStatusText = flow\n\t\t? resolveMessage(translate(keys.formStepStatus), {\n\t\t\t\tcurrent: String(step.stepIndex + 1),\n\t\t\t\ttotal: String(step.stepCount),\n\t\t\t})\n\t\t: ''\n\t// Whether the current step has been attempted and still has a revealed error (drives the step-level alert).\n\tconst currentStepHasError =\n\t\tflow != null &&\n\t\tcurrentStepId != null &&\n\t\tstate.attemptedSteps.has(currentStepId) &&\n\t\tObject.entries(state.errors).some(\n\t\t\t([key, errs]) => errs.length > 0 && stepIdOfField(baseFieldKey(key)) === currentStepId\n\t\t)\n\n\tconst renderedFields = (flow ? stepVisible : visible).filter(\n\t\t(field) => field.hidden !== true && field.calcDisplay !== false\n\t)\n\n\tconst contextValue: FormContextValue = {\n\t\tform,\n\t\tstate,\n\t\tdispatch,\n\t\tvalidateField,\n\t\tlocale,\n\t\tstep,\n\t\trendererRegistry,\n\t\tlabels,\n\t\tt: translate,\n\t\teffectiveValues,\n\t\tcalcValues,\n\t\trecall,\n\t\trenderedFields,\n\t\tconverters,\n\t\tstepIdOfField,\n\t}\n\n\tconst PresentationWrapper = activePresentation.Wrapper\n\tconst wrap = (content: ReactNode): ReactNode =>\n\t\tPresentationWrapper ? (\n\t\t\t<PresentationWrapper\n\t\t\t\tpresentation={activePresentation}\n\t\t\t\topen\n\t\t\t\tonClose={handleClose}\n\t\t\t\ttitle={title}\n\t\t\t\tcloseLabel={resolvedCloseLabel}\n\t\t\t>\n\t\t\t\t{content}\n\t\t\t</PresentationWrapper>\n\t\t) : (\n\t\t\tcontent\n\t\t)\n\n\tif (children !== undefined) {\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\t<form\n\t\t\t\t\t\tref={formRef}\n\t\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\t\tnoValidate\n\t\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t>\n\t\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t\t{children}\n\t\t\t\t\t</form>\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\tif (state.submitted) {\n\t\tconst successResponse = resolveSuccessResponse()\n\t\tconst responseHtml = successResponse?.type === 'message' ? successResponse.html : undefined\n\t\treturn (\n\t\t\t<FormContext.Provider value={contextValue}>\n\t\t\t\t{wrap(\n\t\t\t\t\tresponseHtml ? (\n\t\t\t\t\t\t// Safe to inject: serializeBody HTML-escapes all text (recall values included) and sanitizes link URLs.\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t\t// biome-ignore lint/security/noDangerouslySetInnerHtml: HTML is produced by our escaping serializer, never raw user input\n\t\t\t\t\t\t\tdangerouslySetInnerHTML={{ __html: responseHtml }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<p\n\t\t\t\t\t\t\trole=\"status\"\n\t\t\t\t\t\t\tclassName={cn('fb-form__success', className)}\n\t\t\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{interpolate(resolvedSuccessMessage, recall)}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t)\n\t\t\t\t)}\n\t\t\t</FormContext.Provider>\n\t\t)\n\t}\n\n\treturn (\n\t\t<FormContext.Provider value={contextValue}>\n\t\t\t{wrap(\n\t\t\t\t<form\n\t\t\t\t\tref={formRef}\n\t\t\t\t\tclassName={cn('fb-form-root', className)}\n\t\t\t\t\tnoValidate\n\t\t\t\t\tonSubmit={handleSubmit}\n\t\t\t\t\tonKeyDown={handleKeyDown}\n\t\t\t\t\tdata-fb-presentation={activePresentation.name}\n\t\t\t\t\tdata-fb-density={activePresentation.density}\n\t\t\t\t>\n\t\t\t\t\t{honeypotName ? <Honeypot name={honeypotName} inputRef={honeypotRef} /> : null}\n\t\t\t\t\t{header}\n\t\t\t\t\t{flow ? (\n\t\t\t\t\t\t<>\n\t\t\t\t\t\t\t{/* Announces \"Step X of Y\" politely on each step change. A bare aria-live region, not\n\t\t\t\t\t\t\t role=status, so the single status role stays with the form's success outcome. */}\n\t\t\t\t\t\t\t<div aria-live=\"polite\" aria-atomic=\"true\" style={SR_ONLY}>\n\t\t\t\t\t\t\t\t{stepStatusText}\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{/* Focus lands here on a step change (the region's start), so keyboard/SR users move with it.\n\t\t\t\t\t\t\t A plain focusable container, not a named group; the aria-live region above does the announcing. */}\n\t\t\t\t\t\t\t<div data-fb-step-region tabIndex={-1} className=\"fb-form__step\">\n\t\t\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t{currentStepHasError ? (\n\t\t\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__step-error\">\n\t\t\t\t\t\t\t\t\t{translate(keys.formStepInvalid)}\n\t\t\t\t\t\t\t\t</p>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</>\n\t\t\t\t\t) : (\n\t\t\t\t\t\t<FormFields layout={layout} />\n\t\t\t\t\t)}\n\t\t\t\t\t{state.submitError ? (\n\t\t\t\t\t\t<p role=\"alert\" className=\"fb-form__submit-error\">\n\t\t\t\t\t\t\t{state.submitError}\n\t\t\t\t\t\t</p>\n\t\t\t\t\t) : null}\n\t\t\t\t\t<FormControls\n\t\t\t\t\t\tbackButtonClassName={backButtonClassName}\n\t\t\t\t\t\tnextButtonClassName={nextButtonClassName}\n\t\t\t\t\t\tsubmitButtonClassName={submitButtonClassName}\n\t\t\t\t\t\trenderBack={renderBack}\n\t\t\t\t\t\trenderNext={renderNext}\n\t\t\t\t\t\trenderSubmit={renderSubmit}\n\t\t\t\t\t/>\n\t\t\t\t</form>\n\t\t\t)}\n\t\t</FormContext.Provider>\n\t)\n}\n"],"mappings":"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"}
|
package/dist/react/Poll.js
CHANGED
|
@@ -68,7 +68,8 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
|
|
|
68
68
|
const result = await fetchResultsImpl({
|
|
69
69
|
formId: formProps.form.id,
|
|
70
70
|
field: resultsField,
|
|
71
|
-
apiRoute
|
|
71
|
+
apiRoute,
|
|
72
|
+
locale: formProps.submissionLocale
|
|
72
73
|
});
|
|
73
74
|
if (result.ok) {
|
|
74
75
|
setResults(result.results);
|
|
@@ -78,7 +79,8 @@ const Poll = ({ resultsField, storageKey, hasVoted, fetchResultsImpl = fetchForm
|
|
|
78
79
|
fetchResultsImpl,
|
|
79
80
|
formProps.form.id,
|
|
80
81
|
resultsField,
|
|
81
|
-
apiRoute
|
|
82
|
+
apiRoute,
|
|
83
|
+
formProps.submissionLocale
|
|
82
84
|
]);
|
|
83
85
|
const resultsError = /* @__PURE__ */ jsx("p", {
|
|
84
86
|
className: "fb-poll__error",
|
package/dist/react/Poll.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { answerValues } from '../poll/votes/answerValues'\nimport type { VotedSubmission } from '../submissions/resolveVotedSubmission'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { VoteStorage } from './adapters'\nimport { Form, type FormProps } from './Form'\nimport { FormResults, type FormResultsProps } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */\n\tresultsField: string\n\t/**\n\t * Key for the per-browser voted guard (localStorage by default; overridable or disabled via\n\t * `adapters.voteStorage`). Default `fb-poll-{form.id}`.\n\t */\n\tstorageKey?: string\n\t/**\n\t * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;\n\t * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`\n\t * option pass `hasVotedCookie(cookieHeader, form.id)` here.\n\t */\n\thasVoted?: boolean\n\t/**\n\t * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module\n\t * scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.\n\t */\n\tfetchResultsImpl?: typeof fetchFormResults\n\t/**\n\t * The voter's server-resolved current vote (`resolveVotedSubmission`, read from the httpOnly voted\n\t * cookie). Presence implies voted; `pick` marks the voter's option in results and `value` prefills\n\t * the form when they change their vote. Read at mount: after a client-side change the component\n\t * tracks the new pick itself.\n\t */\n\tcurrentVote?: Pick<VotedSubmission, 'value' | 'pick'> | null\n\t/**\n\t * Replaces the built-in `<FormResults>` wherever the poll shows results (after voting, when\n\t * closed, and for a recorded outcome). Receives exactly what `<FormResults>` receives at that\n\t * site, so the default is `(props) => <FormResults {...props} />`; surrounding chrome (the\n\t * closed/final notices, the change-vote button) stays the poll's.\n\t */\n\trenderResults?: (props: FormResultsProps) => ReactNode\n}\n\nconst localStorageVoteStorage: VoteStorage = {\n\tread: (key) => {\n\t\ttry {\n\t\t\treturn window.localStorage.getItem(key) != null\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t},\n\twrite: (key) => {\n\t\ttry {\n\t\t\twindow.localStorage.setItem(key, '1')\n\t\t} catch {\n\t\t\t// Private mode / storage disabled: the guard is best-effort UX, never integrity.\n\t\t}\n\t},\n}\n\n/** `false` disables client persistence entirely (`hasVoted` still marks voted). */\nconst noopVoteStorage: VoteStorage = { read: () => false, write: () => {} }\n\nconst resolveVoteStorage = (configured: VoteStorage | false | undefined): VoteStorage => {\n\tif (configured === false) {\n\t\treturn noopVoteStorage\n\t}\n\treturn configured ?? localStorageVoteStorage\n}\n\n/**\n * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows\n * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated\n * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open\n * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A\n * recorded `outcome.winningValues` (via `resolvePollOutcome`) supersedes everything: a translated final\n * notice plus results with every winning bucket highlighted (a tie highlights more than one). A\n * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`\n * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not\n * integrity (bypassable): server-enforced\n * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.\n */\nexport const Poll = ({\n\tresultsField,\n\tstorageKey,\n\thasVoted,\n\tfetchResultsImpl = fetchFormResults,\n\tapiRoute,\n\tonSuccess,\n\tcurrentVote,\n\trenderResults,\n\t...formProps\n}: PollProps) => {\n\tconst renderResultsView =\n\t\trenderResults ?? ((props: FormResultsProps): ReactNode => <FormResults {...props} />)\n\tconst key = storageKey ?? `fb-poll-${formProps.form.id}`\n\tconst poll = formProps.form.poll\n\tconst closed = isPollClosed(poll)\n\tconst allowChange = pollConfigOf(poll)?.allowChange === true\n\tconst winningValues = poll?.outcome?.winningValues\n\tconst finalized = Array.isArray(winningValues) && winningValues.length > 0\n\tconst resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === 'afterClose'\n\t// Server-known voted state renders the results view on the very first paint; the effect below\n\t// only adds the localStorage read.\n\tconst [voted, setVoted] = useState(hasVoted === true || currentVote != null)\n\tconst [changing, setChanging] = useState(false)\n\tconst [pick, setPick] = useState<string[] | undefined>(currentVote?.pick)\n\tconst [prefill, setPrefill] = useState<unknown>(currentVote?.value)\n\t// Changing is offered only when the signed cookie verifiably identifies a submission: at mount\n\t// via a resolved `currentVote`, or after an in-session vote (the server just issued the cookie).\n\t// A localStorage-only voted flag proves nothing about the cookie, and a \"change\" without one\n\t// would create a second submission instead of updating.\n\tconst [changeReady, setChangeReady] = useState(currentVote != null)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\tconst [loadFailed, setLoadFailed] = useState(false)\n\tconst translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t])\n\tconst configuredVoteStorage = formProps.adapters?.voteStorage\n\tconst voteStorage = useMemo(\n\t\t() => resolveVoteStorage(configuredVoteStorage),\n\t\t[configuredVoteStorage]\n\t)\n\n\tconst loadResults = useCallback(async () => {\n\t\tconst result: FetchResultsResult = await fetchResultsImpl({\n\t\t\tformId: formProps.form.id,\n\t\t\tfield: resultsField,\n\t\t\tapiRoute,\n\t\t})\n\t\t// A failed load surfaces as an error, not an empty result set: `[]` would read as \"no votes yet\".\n\t\tif (result.ok) {\n\t\t\tsetResults(result.results)\n\t\t\tsetLoadFailed(false)\n\t\t} else {\n\t\t\tsetLoadFailed(true)\n\t\t}\n\t}, [fetchResultsImpl, formProps.form.id, resultsField, apiRoute])\n\n\tconst resultsError = (\n\t\t<p className=\"fb-poll__error\" role=\"alert\">\n\t\t\t{translate(keys.pollResultsError)}\n\t\t</p>\n\t)\n\n\tuseEffect(() => {\n\t\tconst already = hasVoted === true || currentVote != null || voteStorage.read(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t}\n\t\tif ((already && !resultsAwaitClose) || closed || finalized) {\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, currentVote, key, loadResults, closed, resultsAwaitClose, finalized, voteStorage])\n\n\tconst handleSuccess = useCallback<NonNullable<FormProps['onSuccess']>>(\n\t\t(submissionId, result) => {\n\t\t\tvoteStorage.write(key)\n\t\t\tsetVoted(true)\n\t\t\tsetChanging(false)\n\t\t\tsetChangeReady(true)\n\t\t\t// Track the pick client-side so the \"your vote\" marker is fresh right after a (re-)vote,\n\t\t\t// without waiting for a server-resolved currentVote on the next page load.\n\t\t\tif (result?.values) {\n\t\t\t\tconst entry = result.values.find((row) => row.field === resultsField)\n\t\t\t\tsetPrefill(entry?.value)\n\t\t\t\tsetPick(answerValues(result.values, resultsField))\n\t\t\t}\n\t\t\tif (!resultsAwaitClose) {\n\t\t\t\tvoid loadResults()\n\t\t\t}\n\t\t\t// Forward the resolved success response so a Poll host gets the same onSuccess payload as a Form host.\n\t\t\tonSuccess?.(submissionId, result)\n\t\t},\n\t\t[key, loadResults, onSuccess, resultsAwaitClose, voteStorage, resultsField]\n\t)\n\n\tif (finalized) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--final\">\n\t\t\t\t<p className=\"fb-poll__final\">{translate(keys.pollFinalResult)}</p>\n\t\t\t\t{loadFailed\n\t\t\t\t\t? resultsError\n\t\t\t\t\t: results\n\t\t\t\t\t\t? renderResultsView({\n\t\t\t\t\t\t\t\tresults,\n\t\t\t\t\t\t\t\twinningValues,\n\t\t\t\t\t\t\t\tcurrentValues: pick,\n\t\t\t\t\t\t\t\tt: formProps.t,\n\t\t\t\t\t\t\t\tlocale: formProps.locale,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (closed) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--closed\">\n\t\t\t\t<p className=\"fb-poll__closed\">{translate(keys.pollClosed)}</p>\n\t\t\t\t{loadFailed\n\t\t\t\t\t? resultsError\n\t\t\t\t\t: results\n\t\t\t\t\t\t? renderResultsView({\n\t\t\t\t\t\t\t\tresults,\n\t\t\t\t\t\t\t\tcurrentValues: pick,\n\t\t\t\t\t\t\t\tt: formProps.t,\n\t\t\t\t\t\t\t\tlocale: formProps.locale,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (voted && !changing) {\n\t\tif (resultsAwaitClose) {\n\t\t\treturn <p className=\"fb-poll__await-close\">{translate(keys.pollResultsAfterClose)}</p>\n\t\t}\n\t\tif (loadFailed) {\n\t\t\treturn resultsError\n\t\t}\n\t\treturn results ? (\n\t\t\t<div className=\"fb-poll fb-poll--voted\">\n\t\t\t\t{renderResultsView({\n\t\t\t\t\tresults,\n\t\t\t\t\tcurrentValues: pick,\n\t\t\t\t\tt: formProps.t,\n\t\t\t\t\tlocale: formProps.locale,\n\t\t\t\t})}\n\t\t\t\t{allowChange && changeReady ? (\n\t\t\t\t\t<button className=\"fb-poll__change\" type=\"button\" onClick={() => setChanging(true)}>\n\t\t\t\t\t\t{translate(keys.pollChangeVote)}\n\t\t\t\t\t</button>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t) : null\n\t}\n\n\t// While changing, the current pick prefills the results field; the server identifies the\n\t// submission to update from the httpOnly voted cookie, so the client sends a normal submit.\n\tconst initialValues =\n\t\tchanging && prefill !== undefined\n\t\t\t? { ...(formProps.initialValues ?? {}), [resultsField]: prefill }\n\t\t\t: formProps.initialValues\n\treturn (\n\t\t<Form\n\t\t\t{...formProps}\n\t\t\tinitialValues={initialValues}\n\t\t\tapiRoute={apiRoute}\n\t\t\tonSuccess={handleSuccess}\n\t\t/>\n\t)\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Poll.js","names":[],"sources":["../../src/react/Poll.tsx"],"sourcesContent":["'use client'\n\nimport type { ReactNode } from 'react'\nimport { useCallback, useEffect, useMemo, useState } from 'react'\nimport type { FieldAggregation } from '../aggregation/types'\nimport { isPollClosed, pollConfigOf } from '../form/pollState'\nimport { answerValues } from '../poll/votes/answerValues'\nimport type { VotedSubmission } from '../submissions/resolveVotedSubmission'\nimport { en } from '../translations/en'\nimport { keys } from '../translations/keys'\nimport { makeTranslate } from '../translations/makeTranslate'\nimport type { VoteStorage } from './adapters'\nimport { Form, type FormProps } from './Form'\nimport { FormResults, type FormResultsProps } from './FormResults'\nimport { type FetchResultsResult, fetchFormResults } from './fetchResults'\n\nexport type PollProps = FormProps & {\n\t/** The choice field whose results are shown after voting (should match the form's public `poll.resultsField`). */\n\tresultsField: string\n\t/**\n\t * Key for the per-browser voted guard (localStorage by default; overridable or disabled via\n\t * `adapters.voteStorage`). Default `fb-poll-{form.id}`.\n\t */\n\tstorageKey?: string\n\t/**\n\t * Server-known voted state, ORed with the localStorage guard: `true` marks the visitor as voted;\n\t * `false`/omitted falls back to localStorage. SSR hosts using the plugin's `poll.votedCookie`\n\t * option pass `hasVotedCookie(cookieHeader, form.id)` here.\n\t */\n\thasVoted?: boolean\n\t/**\n\t * Injectable results fetch (testing); defaults to `fetchFormResults`. Pass a stable reference (module\n\t * scope, `useCallback`, or `useMemo`): an inline function re-runs the load effect and double-fetches.\n\t */\n\tfetchResultsImpl?: typeof fetchFormResults\n\t/**\n\t * The voter's server-resolved current vote (`resolveVotedSubmission`, read from the httpOnly voted\n\t * cookie). Presence implies voted; `pick` marks the voter's option in results and `value` prefills\n\t * the form when they change their vote. Read at mount: after a client-side change the component\n\t * tracks the new pick itself.\n\t */\n\tcurrentVote?: Pick<VotedSubmission, 'value' | 'pick'> | null\n\t/**\n\t * Replaces the built-in `<FormResults>` wherever the poll shows results (after voting, when\n\t * closed, and for a recorded outcome). Receives exactly what `<FormResults>` receives at that\n\t * site, so the default is `(props) => <FormResults {...props} />`; surrounding chrome (the\n\t * closed/final notices, the change-vote button) stays the poll's.\n\t */\n\trenderResults?: (props: FormResultsProps) => ReactNode\n}\n\nconst localStorageVoteStorage: VoteStorage = {\n\tread: (key) => {\n\t\ttry {\n\t\t\treturn window.localStorage.getItem(key) != null\n\t\t} catch {\n\t\t\treturn false\n\t\t}\n\t},\n\twrite: (key) => {\n\t\ttry {\n\t\t\twindow.localStorage.setItem(key, '1')\n\t\t} catch {\n\t\t\t// Private mode / storage disabled: the guard is best-effort UX, never integrity.\n\t\t}\n\t},\n}\n\n/** `false` disables client persistence entirely (`hasVoted` still marks voted). */\nconst noopVoteStorage: VoteStorage = { read: () => false, write: () => {} }\n\nconst resolveVoteStorage = (configured: VoteStorage | false | undefined): VoteStorage => {\n\tif (configured === false) {\n\t\treturn noopVoteStorage\n\t}\n\treturn configured ?? localStorageVoteStorage\n}\n\n/**\n * A poll: renders `<Form>` while open and not yet voted, then fetches the aggregate results and shows\n * `<FormResults>`. Lifecycle comes from `form.poll`: past `closesAt` the poll is closed (a translated\n * notice plus results, which the endpoint serves for any visibility once closed); a voted-but-open\n * `afterClose` poll shows a translated wait notice instead of fetching (the endpoint would refuse). A\n * recorded `outcome.winningValues` (via `resolvePollOutcome`) supersedes everything: a translated final\n * notice plus results with every winning bucket highlighted (a tie highlights more than one). A\n * per-browser localStorage flag (`storageKey`) skips straight to results on revisit; `hasVoted: true`\n * (e.g. from the server-set voted cookie) marks voted regardless of localStorage. The guard is UX, not\n * integrity (bypassable): server-enforced\n * one-per-identity dedup composes via `req.user` (authed forms) or a `notAlreadySubmitted` rule.\n */\nexport const Poll = ({\n\tresultsField,\n\tstorageKey,\n\thasVoted,\n\tfetchResultsImpl = fetchFormResults,\n\tapiRoute,\n\tonSuccess,\n\tcurrentVote,\n\trenderResults,\n\t...formProps\n}: PollProps) => {\n\tconst renderResultsView =\n\t\trenderResults ?? ((props: FormResultsProps): ReactNode => <FormResults {...props} />)\n\tconst key = storageKey ?? `fb-poll-${formProps.form.id}`\n\tconst poll = formProps.form.poll\n\tconst closed = isPollClosed(poll)\n\tconst allowChange = pollConfigOf(poll)?.allowChange === true\n\tconst winningValues = poll?.outcome?.winningValues\n\tconst finalized = Array.isArray(winningValues) && winningValues.length > 0\n\tconst resultsAwaitClose = !closed && !finalized && poll?.resultsVisibility === 'afterClose'\n\t// Server-known voted state renders the results view on the very first paint; the effect below\n\t// only adds the localStorage read.\n\tconst [voted, setVoted] = useState(hasVoted === true || currentVote != null)\n\tconst [changing, setChanging] = useState(false)\n\tconst [pick, setPick] = useState<string[] | undefined>(currentVote?.pick)\n\tconst [prefill, setPrefill] = useState<unknown>(currentVote?.value)\n\t// Changing is offered only when the signed cookie verifiably identifies a submission: at mount\n\t// via a resolved `currentVote`, or after an in-session vote (the server just issued the cookie).\n\t// A localStorage-only voted flag proves nothing about the cookie, and a \"change\" without one\n\t// would create a second submission instead of updating.\n\tconst [changeReady, setChangeReady] = useState(currentVote != null)\n\tconst [results, setResults] = useState<FieldAggregation[] | null>(null)\n\tconst [loadFailed, setLoadFailed] = useState(false)\n\tconst translate = useMemo(() => formProps.t ?? makeTranslate(en), [formProps.t])\n\tconst configuredVoteStorage = formProps.adapters?.voteStorage\n\tconst voteStorage = useMemo(\n\t\t() => resolveVoteStorage(configuredVoteStorage),\n\t\t[configuredVoteStorage]\n\t)\n\n\tconst loadResults = useCallback(async () => {\n\t\tconst result: FetchResultsResult = await fetchResultsImpl({\n\t\t\tformId: formProps.form.id,\n\t\t\tfield: resultsField,\n\t\t\tapiRoute,\n\t\t\tlocale: formProps.submissionLocale,\n\t\t})\n\t\t// A failed load surfaces as an error, not an empty result set: `[]` would read as \"no votes yet\".\n\t\tif (result.ok) {\n\t\t\tsetResults(result.results)\n\t\t\tsetLoadFailed(false)\n\t\t} else {\n\t\t\tsetLoadFailed(true)\n\t\t}\n\t}, [fetchResultsImpl, formProps.form.id, resultsField, apiRoute, formProps.submissionLocale])\n\n\tconst resultsError = (\n\t\t<p className=\"fb-poll__error\" role=\"alert\">\n\t\t\t{translate(keys.pollResultsError)}\n\t\t</p>\n\t)\n\n\tuseEffect(() => {\n\t\tconst already = hasVoted === true || currentVote != null || voteStorage.read(key)\n\t\tif (already) {\n\t\t\tsetVoted(true)\n\t\t}\n\t\tif ((already && !resultsAwaitClose) || closed || finalized) {\n\t\t\tvoid loadResults()\n\t\t}\n\t}, [hasVoted, currentVote, key, loadResults, closed, resultsAwaitClose, finalized, voteStorage])\n\n\tconst handleSuccess = useCallback<NonNullable<FormProps['onSuccess']>>(\n\t\t(submissionId, result) => {\n\t\t\tvoteStorage.write(key)\n\t\t\tsetVoted(true)\n\t\t\tsetChanging(false)\n\t\t\tsetChangeReady(true)\n\t\t\t// Track the pick client-side so the \"your vote\" marker is fresh right after a (re-)vote,\n\t\t\t// without waiting for a server-resolved currentVote on the next page load.\n\t\t\tif (result?.values) {\n\t\t\t\tconst entry = result.values.find((row) => row.field === resultsField)\n\t\t\t\tsetPrefill(entry?.value)\n\t\t\t\tsetPick(answerValues(result.values, resultsField))\n\t\t\t}\n\t\t\tif (!resultsAwaitClose) {\n\t\t\t\tvoid loadResults()\n\t\t\t}\n\t\t\t// Forward the resolved success response so a Poll host gets the same onSuccess payload as a Form host.\n\t\t\tonSuccess?.(submissionId, result)\n\t\t},\n\t\t[key, loadResults, onSuccess, resultsAwaitClose, voteStorage, resultsField]\n\t)\n\n\tif (finalized) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--final\">\n\t\t\t\t<p className=\"fb-poll__final\">{translate(keys.pollFinalResult)}</p>\n\t\t\t\t{loadFailed\n\t\t\t\t\t? resultsError\n\t\t\t\t\t: results\n\t\t\t\t\t\t? renderResultsView({\n\t\t\t\t\t\t\t\tresults,\n\t\t\t\t\t\t\t\twinningValues,\n\t\t\t\t\t\t\t\tcurrentValues: pick,\n\t\t\t\t\t\t\t\tt: formProps.t,\n\t\t\t\t\t\t\t\tlocale: formProps.locale,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (closed) {\n\t\treturn (\n\t\t\t<div className=\"fb-poll fb-poll--closed\">\n\t\t\t\t<p className=\"fb-poll__closed\">{translate(keys.pollClosed)}</p>\n\t\t\t\t{loadFailed\n\t\t\t\t\t? resultsError\n\t\t\t\t\t: results\n\t\t\t\t\t\t? renderResultsView({\n\t\t\t\t\t\t\t\tresults,\n\t\t\t\t\t\t\t\tcurrentValues: pick,\n\t\t\t\t\t\t\t\tt: formProps.t,\n\t\t\t\t\t\t\t\tlocale: formProps.locale,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t: null}\n\t\t\t</div>\n\t\t)\n\t}\n\n\tif (voted && !changing) {\n\t\tif (resultsAwaitClose) {\n\t\t\treturn <p className=\"fb-poll__await-close\">{translate(keys.pollResultsAfterClose)}</p>\n\t\t}\n\t\tif (loadFailed) {\n\t\t\treturn resultsError\n\t\t}\n\t\treturn results ? (\n\t\t\t<div className=\"fb-poll fb-poll--voted\">\n\t\t\t\t{renderResultsView({\n\t\t\t\t\tresults,\n\t\t\t\t\tcurrentValues: pick,\n\t\t\t\t\tt: formProps.t,\n\t\t\t\t\tlocale: formProps.locale,\n\t\t\t\t})}\n\t\t\t\t{allowChange && changeReady ? (\n\t\t\t\t\t<button className=\"fb-poll__change\" type=\"button\" onClick={() => setChanging(true)}>\n\t\t\t\t\t\t{translate(keys.pollChangeVote)}\n\t\t\t\t\t</button>\n\t\t\t\t) : null}\n\t\t\t</div>\n\t\t) : null\n\t}\n\n\t// While changing, the current pick prefills the results field; the server identifies the\n\t// submission to update from the httpOnly voted cookie, so the client sends a normal submit.\n\tconst initialValues =\n\t\tchanging && prefill !== undefined\n\t\t\t? { ...(formProps.initialValues ?? {}), [resultsField]: prefill }\n\t\t\t: formProps.initialValues\n\treturn (\n\t\t<Form\n\t\t\t{...formProps}\n\t\t\tinitialValues={initialValues}\n\t\t\tapiRoute={apiRoute}\n\t\t\tonSuccess={handleSuccess}\n\t\t/>\n\t)\n}\n"],"mappings":"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"}
|
|
@@ -4,7 +4,8 @@ import { FieldAggregation } from "../aggregation/types.js";
|
|
|
4
4
|
type FetchResultsInput = {
|
|
5
5
|
formId: number | string; /** Restrict to one field. Omit to get every public/enumerable field. */
|
|
6
6
|
field?: string; /** Payload API route prefix; defaults to `/api`. */
|
|
7
|
-
apiRoute?: string; /**
|
|
7
|
+
apiRoute?: string; /** The Payload content locale (a `localization` code) option labels are returned in, sent as `?locale=`. */
|
|
8
|
+
locale?: string; /** Injectable for testing; defaults to global `fetch`. */
|
|
8
9
|
fetchImpl?: typeof fetch;
|
|
9
10
|
};
|
|
10
11
|
type FetchResultsResult = {
|
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
* access by the form's poll opt-in and results visibility. Pure: inject `fetchImpl` in tests.
|
|
6
6
|
*/
|
|
7
7
|
const fetchFormResults = async (input) => {
|
|
8
|
-
const { formId, field, apiRoute = "/api", fetchImpl = fetch } = input;
|
|
9
|
-
const
|
|
8
|
+
const { formId, field, apiRoute = "/api", locale, fetchImpl = fetch } = input;
|
|
9
|
+
const params = new URLSearchParams();
|
|
10
|
+
if (field) params.set("field", field);
|
|
11
|
+
if (locale) params.set("locale", locale);
|
|
12
|
+
const query = params.size > 0 ? `?${params}` : "";
|
|
10
13
|
let response;
|
|
11
14
|
try {
|
|
12
15
|
response = await fetchImpl(`${apiRoute}/forms/${formId}/results${query}`, { method: "GET" });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetchResults.js","names":[],"sources":["../../src/react/fetchResults.ts"],"sourcesContent":["import type { FieldAggregation } from '../aggregation/types'\n\nexport type FetchResultsInput = {\n\tformId: number | string\n\t/** Restrict to one field. Omit to get every public/enumerable field. */\n\tfield?: string\n\t/** Payload API route prefix; defaults to `/api`. */\n\tapiRoute?: string\n\t/** Injectable for testing; defaults to global `fetch`. */\n\tfetchImpl?: typeof fetch\n}\n\nexport type FetchResultsResult =\n\t| { ok: true; results: FieldAggregation[] }\n\t| { ok: false; message?: string }\n\n/**\n * Fetch aggregate poll/survey results from the form-builder results endpoint\n * (`GET {apiRoute}/forms/:id/results`). Returns the server-resolved aggregations; the endpoint gates public\n * access by the form's poll opt-in and results visibility. Pure: inject `fetchImpl` in tests.\n */\nexport const fetchFormResults = async (input: FetchResultsInput): Promise<FetchResultsResult> => {\n\tconst { formId, field, apiRoute = '/api', fetchImpl = fetch } = input\n\tconst
|
|
1
|
+
{"version":3,"file":"fetchResults.js","names":[],"sources":["../../src/react/fetchResults.ts"],"sourcesContent":["import type { FieldAggregation } from '../aggregation/types'\n\nexport type FetchResultsInput = {\n\tformId: number | string\n\t/** Restrict to one field. Omit to get every public/enumerable field. */\n\tfield?: string\n\t/** Payload API route prefix; defaults to `/api`. */\n\tapiRoute?: string\n\t/** The Payload content locale (a `localization` code) option labels are returned in, sent as `?locale=`. */\n\tlocale?: string\n\t/** Injectable for testing; defaults to global `fetch`. */\n\tfetchImpl?: typeof fetch\n}\n\nexport type FetchResultsResult =\n\t| { ok: true; results: FieldAggregation[] }\n\t| { ok: false; message?: string }\n\n/**\n * Fetch aggregate poll/survey results from the form-builder results endpoint\n * (`GET {apiRoute}/forms/:id/results`). Returns the server-resolved aggregations; the endpoint gates public\n * access by the form's poll opt-in and results visibility. Pure: inject `fetchImpl` in tests.\n */\nexport const fetchFormResults = async (input: FetchResultsInput): Promise<FetchResultsResult> => {\n\tconst { formId, field, apiRoute = '/api', locale, fetchImpl = fetch } = input\n\tconst params = new URLSearchParams()\n\tif (field) {\n\t\tparams.set('field', field)\n\t}\n\tif (locale) {\n\t\tparams.set('locale', locale)\n\t}\n\tconst query = params.size > 0 ? `?${params}` : ''\n\tlet response: Response\n\ttry {\n\t\tresponse = await fetchImpl(`${apiRoute}/forms/${formId}/results${query}`, { method: 'GET' })\n\t} catch (error) {\n\t\treturn { ok: false, message: error instanceof Error ? error.message : 'Network error' }\n\t}\n\tif (!response.ok) {\n\t\treturn { ok: false, message: `Request failed (${response.status})` }\n\t}\n\tconst body = (await response.json().catch(() => ({}))) as { results?: FieldAggregation[] }\n\treturn { ok: true, results: body.results ?? [] }\n}\n"],"mappings":";;;;;;AAuBA,MAAa,mBAAmB,OAAO,UAA0D;CAChG,MAAM,EAAE,QAAQ,OAAO,WAAW,QAAQ,QAAQ,YAAY,UAAU;CACxE,MAAM,SAAS,IAAI,gBAAgB;CACnC,IAAI,OACH,OAAO,IAAI,SAAS,KAAK;CAE1B,IAAI,QACH,OAAO,IAAI,UAAU,MAAM;CAE5B,MAAM,QAAQ,OAAO,OAAO,IAAI,IAAI,WAAW;CAC/C,IAAI;CACJ,IAAI;EACH,WAAW,MAAM,UAAU,GAAG,SAAS,SAAS,OAAO,UAAU,SAAS,EAAE,QAAQ,MAAM,CAAC;CAC5F,SAAS,OAAO;EACf,OAAO;GAAE,IAAI;GAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU;EAAgB;CACvF;CACA,IAAI,CAAC,SAAS,IACb,OAAO;EAAE,IAAI;EAAO,SAAS,mBAAmB,SAAS,OAAO;CAAG;CAGpE,OAAO;EAAE,IAAI;EAAM,UAAS,MADR,SAAS,KAAK,EAAE,aAAa,CAAC,EAAE,GACnB,WAAW,CAAC;CAAE;AAChD"}
|
|
@@ -6,9 +6,9 @@ type SubmitFormInput = {
|
|
|
6
6
|
values: SubmissionValue[]; /** Payload API route prefix; defaults to `/api`. */
|
|
7
7
|
apiRoute?: string;
|
|
8
8
|
/**
|
|
9
|
-
* The
|
|
10
|
-
* the post-submit actions (confirmation emails included) render in it. Absent,
|
|
11
|
-
* back to the host's default locale.
|
|
9
|
+
* The Payload content locale (a `localization` code), sent as `?locale=` so the server stamps it on
|
|
10
|
+
* the submission and the post-submit actions (confirmation emails included) render in it. Absent,
|
|
11
|
+
* the server falls back to the host's default locale.
|
|
12
12
|
*/
|
|
13
13
|
locale?: string; /** Injectable for testing; defaults to global `fetch`. */
|
|
14
14
|
fetchImpl?: typeof fetch;
|
|
@@ -30,7 +30,7 @@ type SubmitFormResult = {
|
|
|
30
30
|
declare const submitForm: (input: SubmitFormInput) => Promise<SubmitFormResult>;
|
|
31
31
|
/**
|
|
32
32
|
* A consumer override for the transport: given the form id + values, resolve to a submit result.
|
|
33
|
-
* `locale` is the `<Form>`'s
|
|
33
|
+
* `locale` is the `<Form>`'s `submissionLocale` prop (absent when the prop was not passed); forward
|
|
34
34
|
* it as `?locale=` so the submission and its emails carry the visitor's locale.
|
|
35
35
|
*/
|
|
36
36
|
type SubmitHandler = (input: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submitForm.js","names":[],"sources":["../../src/react/submitForm.ts"],"sourcesContent":["import type { SubmissionValue } from '../submissions/types'\n\nexport type SubmitFormInput = {\n\tformId: number | string\n\tvalues: SubmissionValue[]\n\t/** Payload API route prefix; defaults to `/api`. */\n\tapiRoute?: string\n\t/**\n\t * The
|
|
1
|
+
{"version":3,"file":"submitForm.js","names":[],"sources":["../../src/react/submitForm.ts"],"sourcesContent":["import type { SubmissionValue } from '../submissions/types'\n\nexport type SubmitFormInput = {\n\tformId: number | string\n\tvalues: SubmissionValue[]\n\t/** Payload API route prefix; defaults to `/api`. */\n\tapiRoute?: string\n\t/**\n\t * The Payload content locale (a `localization` code), sent as `?locale=` so the server stamps it on\n\t * the submission and the post-submit actions (confirmation emails included) render in it. Absent,\n\t * the server falls back to the host's default locale.\n\t */\n\tlocale?: string\n\t/** Injectable for testing; defaults to global `fetch`. */\n\tfetchImpl?: typeof fetch\n}\n\nexport type SubmitFormResult =\n\t| { ok: true; submissionId?: string }\n\t| { ok: false; fieldErrors?: Record<string, string[]>; message?: string }\n\ntype ValidationErrorBody = {\n\terrors?: Array<{\n\t\tmessage?: string\n\t\tdata?: { errors?: Array<{ path?: string; message?: string }> }\n\t}>\n}\n\nconst toFieldErrors = (body: ValidationErrorBody): Record<string, string[]> => {\n\tconst nested = body.errors?.[0]?.data?.errors ?? []\n\tconst map: Record<string, string[]> = {}\n\tfor (const entry of nested) {\n\t\tif (typeof entry.path === 'string' && typeof entry.message === 'string') {\n\t\t\tmap[entry.path] = [...(map[entry.path] ?? []), entry.message]\n\t\t}\n\t}\n\treturn map\n}\n\n/**\n * The default submission transport: POST `{apiRoute}/form-submissions` (with `?locale=` when a\n * `locale` is given) carrying `{ form, values }`. On 201\n * returns the created submission id; on a 400 Payload `ValidationError` maps `data.errors[].path` to\n * per-field messages; otherwise returns a generic message. Pure: inject `fetchImpl` in tests.\n */\nexport const submitForm = async (input: SubmitFormInput): Promise<SubmitFormResult> => {\n\tconst { formId, values, apiRoute = '/api', locale, fetchImpl = fetch } = input\n\tconst query = locale ? `?locale=${encodeURIComponent(locale)}` : ''\n\tlet response: Response\n\ttry {\n\t\tresponse = await fetchImpl(`${apiRoute}/form-submissions${query}`, {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ form: formId, values }),\n\t\t})\n\t} catch (error) {\n\t\treturn { ok: false, message: error instanceof Error ? error.message : 'Network error' }\n\t}\n\tif (response.ok) {\n\t\tconst data = (await response.json().catch(() => ({}))) as { doc?: { id?: number | string } }\n\t\tconst id = data.doc?.id\n\t\treturn { ok: true, submissionId: id === undefined ? undefined : String(id) }\n\t}\n\tif (response.status === 400) {\n\t\tconst body = (await response.json().catch(() => ({}))) as ValidationErrorBody\n\t\tconst fieldErrors = toFieldErrors(body)\n\t\tif (Object.keys(fieldErrors).length > 0) {\n\t\t\treturn { ok: false, fieldErrors }\n\t\t}\n\t\treturn { ok: false, message: body.errors?.[0]?.message ?? 'Validation failed' }\n\t}\n\t// A non-400 failure can still carry a server-authored message worth showing verbatim, e.g. the\n\t// translated essential-action rejection; fall back to the generic line when there is none.\n\tconst body = (await response.json().catch(() => ({}))) as ValidationErrorBody\n\tconst message = body.errors?.[0]?.message\n\treturn { ok: false, message: message ?? `Request failed (${response.status})` }\n}\n\n/**\n * A consumer override for the transport: given the form id + values, resolve to a submit result.\n * `locale` is the `<Form>`'s `submissionLocale` prop (absent when the prop was not passed); forward\n * it as `?locale=` so the submission and its emails carry the visitor's locale.\n */\nexport type SubmitHandler = (input: {\n\tformId: number | string\n\tvalues: SubmissionValue[]\n\tlocale?: string\n}) => Promise<SubmitFormResult>\n"],"mappings":";AA4BA,MAAM,iBAAiB,SAAwD;CAC9E,MAAM,SAAS,KAAK,SAAS,IAAI,MAAM,UAAU,CAAC;CAClD,MAAM,MAAgC,CAAC;CACvC,KAAK,MAAM,SAAS,QACnB,IAAI,OAAO,MAAM,SAAS,YAAY,OAAO,MAAM,YAAY,UAC9D,IAAI,MAAM,QAAQ,CAAC,GAAI,IAAI,MAAM,SAAS,CAAC,GAAI,MAAM,OAAO;CAG9D,OAAO;AACR;;;;;;;AAQA,MAAa,aAAa,OAAO,UAAsD;CACtF,MAAM,EAAE,QAAQ,QAAQ,WAAW,QAAQ,QAAQ,YAAY,UAAU;CACzE,MAAM,QAAQ,SAAS,WAAW,mBAAmB,MAAM,MAAM;CACjE,IAAI;CACJ,IAAI;EACH,WAAW,MAAM,UAAU,GAAG,SAAS,mBAAmB,SAAS;GAClE,QAAQ;GACR,SAAS,EAAE,gBAAgB,mBAAmB;GAC9C,MAAM,KAAK,UAAU;IAAE,MAAM;IAAQ;GAAO,CAAC;EAC9C,CAAC;CACF,SAAS,OAAO;EACf,OAAO;GAAE,IAAI;GAAO,SAAS,iBAAiB,QAAQ,MAAM,UAAU;EAAgB;CACvF;CACA,IAAI,SAAS,IAAI;EAEhB,MAAM,MAAK,MADS,SAAS,KAAK,EAAE,aAAa,CAAC,EAAE,GACpC,KAAK;EACrB,OAAO;GAAE,IAAI;GAAM,cAAc,OAAO,KAAA,IAAY,KAAA,IAAY,OAAO,EAAE;EAAE;CAC5E;CACA,IAAI,SAAS,WAAW,KAAK;EAC5B,MAAM,OAAQ,MAAM,SAAS,KAAK,EAAE,aAAa,CAAC,EAAE;EACpD,MAAM,cAAc,cAAc,IAAI;EACtC,IAAI,OAAO,KAAK,WAAW,EAAE,SAAS,GACrC,OAAO;GAAE,IAAI;GAAO;EAAY;EAEjC,OAAO;GAAE,IAAI;GAAO,SAAS,KAAK,SAAS,IAAI,WAAW;EAAoB;CAC/E;CAKA,OAAO;EAAE,IAAI;EAAO,UADJ,MADI,SAAS,KAAK,EAAE,aAAa,CAAC,EAAE,GAC/B,SAAS,IAAI,WACM,mBAAmB,SAAS,OAAO;CAAG;AAC/E"}
|
|
@@ -7,6 +7,12 @@ type CreateSubmissionArgs = {
|
|
|
7
7
|
form: number | string;
|
|
8
8
|
values: SubmissionValue[]; /** Thread the incoming request when there is one, so spam metadata (IP, user agent) and per-identity rate limiting see it. */
|
|
9
9
|
req?: PayloadRequest;
|
|
10
|
+
/**
|
|
11
|
+
* The submission's content locale, what `<Form>`'s `submissionLocale` sends as `?locale=`: stored on
|
|
12
|
+
* the submission and used to render its post-submit actions. Clamped like the browser's (a
|
|
13
|
+
* localized host keeps only its configured codes). Absent, `req.locale` or the default locale applies.
|
|
14
|
+
*/
|
|
15
|
+
locale?: string;
|
|
10
16
|
};
|
|
11
17
|
/** The stored shape of a `form-submissions` document, as written by the collection's own hooks. */
|
|
12
18
|
type CreatedSubmission = {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SUBMISSION_LOCALE_CONTEXT_KEY } from "./submissionLocale.js";
|
|
1
2
|
import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
|
|
2
3
|
//#region src/submissions/createSubmission.ts
|
|
3
4
|
/**
|
|
@@ -11,14 +12,18 @@ import { FORM_SUBMISSIONS_SLUG } from "../collections/formSubmissions.js";
|
|
|
11
12
|
*/
|
|
12
13
|
const createSubmission = (payload, args) => {
|
|
13
14
|
const create = payload.create;
|
|
14
|
-
|
|
15
|
+
const created = create({
|
|
15
16
|
collection: FORM_SUBMISSIONS_SLUG,
|
|
16
17
|
data: {
|
|
17
18
|
form: args.form,
|
|
18
19
|
values: args.values
|
|
19
20
|
},
|
|
20
|
-
req: args.req
|
|
21
|
+
req: args.req,
|
|
22
|
+
...args.locale ? { context: { [SUBMISSION_LOCALE_CONTEXT_KEY]: args.locale } } : {}
|
|
21
23
|
});
|
|
24
|
+
return args.locale && args.req ? created.finally(() => {
|
|
25
|
+
delete args.req?.context?.[SUBMISSION_LOCALE_CONTEXT_KEY];
|
|
26
|
+
}) : created;
|
|
22
27
|
};
|
|
23
28
|
//#endregion
|
|
24
29
|
export { createSubmission };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createSubmission.js","names":[],"sources":["../../src/submissions/createSubmission.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport type { ConsentProofEntry } from './runSubmission'\nimport type { SubmissionDescriptor, SubmissionValue } from './types'\n\nexport type CreateSubmissionArgs = {\n\tform: number | string\n\tvalues: SubmissionValue[]\n\t/** Thread the incoming request when there is one, so spam metadata (IP, user agent) and per-identity rate limiting see it. */\n\treq?: PayloadRequest\n}\n\n/** The stored shape of a `form-submissions` document, as written by the collection's own hooks. */\nexport type CreatedSubmission = {\n\tid: number | string\n\tform: number | string\n\tstatus: 'complete' | 'partial'\n\tlocale?: string\n\tvalues?: SubmissionValue[]\n\tdescriptors?: SubmissionDescriptor[]\n\tconsent?: ConsentProofEntry[]\n\tmeta?: Record<string, unknown>\n\tcreatedAt: string\n\tupdatedAt: string\n}\n\n/**\n * Server-side counterpart to the browser `submitForm` helper: a typed wrapper over `payload.create`\n * for the `form-submissions` collection. Submitting this way runs the exact same pipeline a browser\n * or raw REST submit does, server-authoritative validation, spam metadata capture when `req` is\n * threaded through, descriptor snapshotting, consent capture, and post-submit action dispatch, since\n * all of it lives in the collection's own hooks rather than in a route handler. Use it from server\n * code (a script, a queued job, another collection's hook) instead of hand-rolling the collection\n * slug and body shape.\n */\nexport const createSubmission = (\n\tpayload: Payload,\n\targs: CreateSubmissionArgs\n): Promise<CreatedSubmission> => {\n\t// `form-submissions` is registered by this plugin at runtime, so a host's generated Payload types\n\t// need not describe it, and when they do, pin its `form` relationship to whichever single id type\n\t// (string or number) that host's database uses. `form` here accepts either, so create through a\n\t// slug-agnostic signature instead of the host's own shape (same idiom as `registerReliability`'s\n\t// locks-collection create in the jobs plugin).\n\tconst create = payload.create as unknown as (options: {\n\t\tcollection: string\n\t\tdata: { form: number | string; values: SubmissionValue[] }\n\t\treq?: PayloadRequest\n\t}) => Promise<CreatedSubmission>\n\
|
|
1
|
+
{"version":3,"file":"createSubmission.js","names":[],"sources":["../../src/submissions/createSubmission.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\nimport { FORM_SUBMISSIONS_SLUG } from '../collections/formSubmissions'\nimport type { ConsentProofEntry } from './runSubmission'\nimport { SUBMISSION_LOCALE_CONTEXT_KEY } from './submissionLocale'\nimport type { SubmissionDescriptor, SubmissionValue } from './types'\n\nexport type CreateSubmissionArgs = {\n\tform: number | string\n\tvalues: SubmissionValue[]\n\t/** Thread the incoming request when there is one, so spam metadata (IP, user agent) and per-identity rate limiting see it. */\n\treq?: PayloadRequest\n\t/**\n\t * The submission's content locale, what `<Form>`'s `submissionLocale` sends as `?locale=`: stored on\n\t * the submission and used to render its post-submit actions. Clamped like the browser's (a\n\t * localized host keeps only its configured codes). Absent, `req.locale` or the default locale applies.\n\t */\n\tlocale?: string\n}\n\n/** The stored shape of a `form-submissions` document, as written by the collection's own hooks. */\nexport type CreatedSubmission = {\n\tid: number | string\n\tform: number | string\n\tstatus: 'complete' | 'partial'\n\tlocale?: string\n\tvalues?: SubmissionValue[]\n\tdescriptors?: SubmissionDescriptor[]\n\tconsent?: ConsentProofEntry[]\n\tmeta?: Record<string, unknown>\n\tcreatedAt: string\n\tupdatedAt: string\n}\n\n/**\n * Server-side counterpart to the browser `submitForm` helper: a typed wrapper over `payload.create`\n * for the `form-submissions` collection. Submitting this way runs the exact same pipeline a browser\n * or raw REST submit does, server-authoritative validation, spam metadata capture when `req` is\n * threaded through, descriptor snapshotting, consent capture, and post-submit action dispatch, since\n * all of it lives in the collection's own hooks rather than in a route handler. Use it from server\n * code (a script, a queued job, another collection's hook) instead of hand-rolling the collection\n * slug and body shape.\n */\nexport const createSubmission = (\n\tpayload: Payload,\n\targs: CreateSubmissionArgs\n): Promise<CreatedSubmission> => {\n\t// `form-submissions` is registered by this plugin at runtime, so a host's generated Payload types\n\t// need not describe it, and when they do, pin its `form` relationship to whichever single id type\n\t// (string or number) that host's database uses. `form` here accepts either, so create through a\n\t// slug-agnostic signature instead of the host's own shape (same idiom as `registerReliability`'s\n\t// locks-collection create in the jobs plugin).\n\tconst create = payload.create as unknown as (options: {\n\t\tcollection: string\n\t\tdata: { form: number | string; values: SubmissionValue[] }\n\t\treq?: PayloadRequest\n\t\tcontext?: Record<string, unknown>\n\t}) => Promise<CreatedSubmission>\n\tconst created = create({\n\t\tcollection: FORM_SUBMISSIONS_SLUG,\n\t\tdata: { form: args.form, values: args.values },\n\t\treq: args.req,\n\t\t...(args.locale ? { context: { [SUBMISSION_LOCALE_CONTEXT_KEY]: args.locale } } : {}),\n\t})\n\t// `validateSubmission` consumes the key, but a create rejected before it runs (the spam guard's\n\t// rate limit) would leave it on the host's `req.context` for its next create.\n\treturn args.locale && args.req\n\t\t? created.finally(() => {\n\t\t\t\tdelete args.req?.context?.[SUBMISSION_LOCALE_CONTEXT_KEY]\n\t\t\t})\n\t\t: created\n}\n"],"mappings":";;;;;;;;;;;;AA0CA,MAAa,oBACZ,SACA,SACgC;CAMhC,MAAM,SAAS,QAAQ;CAMvB,MAAM,UAAU,OAAO;EACtB,YAAY;EACZ,MAAM;GAAE,MAAM,KAAK;GAAM,QAAQ,KAAK;EAAO;EAC7C,KAAK,KAAK;EACV,GAAI,KAAK,SAAS,EAAE,SAAS,GAAG,gCAAgC,KAAK,OAAO,EAAE,IAAI,CAAC;CACpF,CAAC;CAGD,OAAO,KAAK,UAAU,KAAK,MACxB,QAAQ,cAAc;EACtB,OAAO,KAAK,KAAK,UAAU;CAC5B,CAAC,IACA;AACJ"}
|
|
@@ -1,18 +1,39 @@
|
|
|
1
1
|
//#region src/submissions/submissionLocale.ts
|
|
2
|
-
/** A plain language tag (`uk`, `pt-BR`, `
|
|
3
|
-
const LOCALE_TAG = /^[A-Za-z]{2,8}(
|
|
2
|
+
/** A plain language tag (`uk`, `pt-BR`, `zh-Hant`), bounded so a stored locale stays a short identifier. */
|
|
3
|
+
const LOCALE_TAG = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8}){0,4}$/;
|
|
4
|
+
/**
|
|
5
|
+
* `locale` as a canonical BCP-47 tag, or `undefined` when it is not one. An underscore separator
|
|
6
|
+
* (`zh_Hant`) is normalized to a hyphen first: the stored value reaches `Intl` consumers (a field
|
|
7
|
+
* type's `format`), and `Intl` throws a `RangeError` on an underscore.
|
|
8
|
+
*/
|
|
9
|
+
const canonicalTag = (locale) => {
|
|
10
|
+
const tag = locale.replaceAll("_", "-");
|
|
11
|
+
if (tag === "all" || !LOCALE_TAG.test(tag)) return;
|
|
12
|
+
try {
|
|
13
|
+
return Intl.getCanonicalLocales(tag)[0];
|
|
14
|
+
} catch {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
4
18
|
/**
|
|
5
19
|
* The locale a submission is stored and processed under, from the visitor-controlled `req.locale`.
|
|
6
20
|
* On a localized host only a configured locale code is accepted, anything else (`all`, `*`, a code
|
|
7
21
|
* outside `localeCodes`, which Payload passes through when `localization.fallback` is false) becomes
|
|
8
|
-
* the default locale. Without localization any
|
|
9
|
-
* wrapper can still localize its own strings, and anything else becomes `'en'`.
|
|
22
|
+
* the default locale. Without localization any valid language tag is kept in its canonical form, so a
|
|
23
|
+
* `richText.serialize` wrapper can still localize its own strings, and anything else becomes `'en'`.
|
|
10
24
|
*/
|
|
11
25
|
const resolveSubmissionLocale = (locale, localization) => {
|
|
12
26
|
if (localization) return typeof locale === "string" && localization.localeCodes.includes(locale) ? locale : localization.defaultLocale;
|
|
13
|
-
return typeof locale === "string"
|
|
27
|
+
return (typeof locale === "string" ? canonicalTag(locale) : void 0) ?? "en";
|
|
14
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* `req.context` key carrying `createSubmission`'s explicit `locale` into the create hook. The local
|
|
31
|
+
* API drops its own `locale` arg on a host without localization, so the context is the one channel
|
|
32
|
+
* that reaches `validateSubmission` either way. Read once and removed there, so it cannot leak into
|
|
33
|
+
* a later create on the same request.
|
|
34
|
+
*/
|
|
35
|
+
const SUBMISSION_LOCALE_CONTEXT_KEY = "formBuilderSubmissionLocale";
|
|
15
36
|
//#endregion
|
|
16
|
-
export { resolveSubmissionLocale };
|
|
37
|
+
export { SUBMISSION_LOCALE_CONTEXT_KEY, resolveSubmissionLocale };
|
|
17
38
|
|
|
18
39
|
//# sourceMappingURL=submissionLocale.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"submissionLocale.js","names":[],"sources":["../../src/submissions/submissionLocale.ts"],"sourcesContent":["import type { SanitizedConfig } from 'payload'\n\n/** A plain language tag (`uk`, `pt-BR`, `
|
|
1
|
+
{"version":3,"file":"submissionLocale.js","names":[],"sources":["../../src/submissions/submissionLocale.ts"],"sourcesContent":["import type { SanitizedConfig } from 'payload'\n\n/** A plain language tag (`uk`, `pt-BR`, `zh-Hant`), bounded so a stored locale stays a short identifier. */\nconst LOCALE_TAG = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8}){0,4}$/\n\n/**\n * `locale` as a canonical BCP-47 tag, or `undefined` when it is not one. An underscore separator\n * (`zh_Hant`) is normalized to a hyphen first: the stored value reaches `Intl` consumers (a field\n * type's `format`), and `Intl` throws a `RangeError` on an underscore.\n */\nconst canonicalTag = (locale: string): string | undefined => {\n\tconst tag = locale.replaceAll('_', '-')\n\tif (tag === 'all' || !LOCALE_TAG.test(tag)) {\n\t\treturn undefined\n\t}\n\ttry {\n\t\treturn Intl.getCanonicalLocales(tag)[0]\n\t} catch {\n\t\treturn undefined\n\t}\n}\n\n/**\n * The locale a submission is stored and processed under, from the visitor-controlled `req.locale`.\n * On a localized host only a configured locale code is accepted, anything else (`all`, `*`, a code\n * outside `localeCodes`, which Payload passes through when `localization.fallback` is false) becomes\n * the default locale. Without localization any valid language tag is kept in its canonical form, so a\n * `richText.serialize` wrapper can still localize its own strings, and anything else becomes `'en'`.\n */\nexport const resolveSubmissionLocale = (\n\tlocale: unknown,\n\tlocalization: SanitizedConfig['localization']\n): string => {\n\tif (localization) {\n\t\treturn typeof locale === 'string' && localization.localeCodes.includes(locale)\n\t\t\t? locale\n\t\t\t: localization.defaultLocale\n\t}\n\treturn (typeof locale === 'string' ? canonicalTag(locale) : undefined) ?? 'en'\n}\n\n/**\n * `req.context` key carrying `createSubmission`'s explicit `locale` into the create hook. The local\n * API drops its own `locale` arg on a host without localization, so the context is the one channel\n * that reaches `validateSubmission` either way. Read once and removed there, so it cannot leak into\n * a later create on the same request.\n */\nexport const SUBMISSION_LOCALE_CONTEXT_KEY = 'formBuilderSubmissionLocale'\n"],"mappings":";;AAGA,MAAM,aAAa;;;;;;AAOnB,MAAM,gBAAgB,WAAuC;CAC5D,MAAM,MAAM,OAAO,WAAW,KAAK,GAAG;CACtC,IAAI,QAAQ,SAAS,CAAC,WAAW,KAAK,GAAG,GACxC;CAED,IAAI;EACH,OAAO,KAAK,oBAAoB,GAAG,EAAE;CACtC,QAAQ;EACP;CACD;AACD;;;;;;;;AASA,MAAa,2BACZ,QACA,iBACY;CACZ,IAAI,cACH,OAAO,OAAO,WAAW,YAAY,aAAa,YAAY,SAAS,MAAM,IAC1E,SACA,aAAa;CAEjB,QAAQ,OAAO,WAAW,WAAW,aAAa,MAAM,IAAI,KAAA,MAAc;AAC3E;;;;;;;AAQA,MAAa,gCAAgC"}
|