@form-engine-ts/react 4.3.2 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -2
- package/dist/index.cjs +559 -139
- package/dist/index.d.cts +97 -8
- package/dist/index.d.ts +97 -8
- package/dist/index.js +559 -136
- package/dist/styles.css +20 -9
- package/package.json +3 -3
package/dist/index.cjs
CHANGED
|
@@ -29,6 +29,7 @@ __export(index_exports, {
|
|
|
29
29
|
createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
|
|
30
30
|
createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
|
|
31
31
|
isTranslationUnresolved: () => isTranslationUnresolved,
|
|
32
|
+
resolveChoiceFieldLayout: () => resolveChoiceFieldLayout,
|
|
32
33
|
resolveFieldEditorControls: () => resolveFieldEditorControls,
|
|
33
34
|
resolveFieldTypeSelectOptions: () => resolveFieldTypeSelectOptions,
|
|
34
35
|
resolveInitialFieldType: () => resolveInitialFieldType,
|
|
@@ -37,7 +38,8 @@ __export(index_exports, {
|
|
|
37
38
|
useField: () => useField,
|
|
38
39
|
useForm: () => useForm,
|
|
39
40
|
useFormBuilder: () => useFormBuilder,
|
|
40
|
-
useSubmissionReceipts: () => useSubmissionReceipts
|
|
41
|
+
useSubmissionReceipts: () => useSubmissionReceipts,
|
|
42
|
+
useTranslationWorkspace: () => useTranslationWorkspace
|
|
41
43
|
});
|
|
42
44
|
module.exports = __toCommonJS(index_exports);
|
|
43
45
|
|
|
@@ -233,8 +235,20 @@ function move(items, sourceIndex, targetIndex) {
|
|
|
233
235
|
result.splice(targetIndex, 0, item);
|
|
234
236
|
return result;
|
|
235
237
|
}
|
|
238
|
+
function displayRuleSourceIds(field) {
|
|
239
|
+
if (field.displayRule === void 0) return [];
|
|
240
|
+
const ids = [];
|
|
241
|
+
const visit = (group) => {
|
|
242
|
+
for (const condition of group.conditions) {
|
|
243
|
+
if ("logic" in condition) visit(condition);
|
|
244
|
+
else ids.push(condition.fieldId);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
visit(field.displayRule.condition);
|
|
248
|
+
return ids;
|
|
249
|
+
}
|
|
236
250
|
function withoutDisplayCondition(field) {
|
|
237
|
-
const { displayCondition: _displayCondition, ...rest } = field;
|
|
251
|
+
const { displayCondition: _displayCondition, displayRule: _displayRule, ...rest } = field;
|
|
238
252
|
return rest;
|
|
239
253
|
}
|
|
240
254
|
function removeLocalizedProperty(translations, locale, property) {
|
|
@@ -257,8 +271,23 @@ function useFormBuilder({
|
|
|
257
271
|
onChange,
|
|
258
272
|
policy,
|
|
259
273
|
idFactory = defaultIdFactory,
|
|
260
|
-
factories = {}
|
|
274
|
+
factories = {},
|
|
275
|
+
fieldEditorMode = "all",
|
|
276
|
+
activeFieldId: controlledActiveFieldId,
|
|
277
|
+
defaultActiveFieldId,
|
|
278
|
+
onActiveFieldChange
|
|
261
279
|
}) {
|
|
280
|
+
const [internalActiveFieldId, setInternalActiveFieldId] = (0, import_react.useState)(
|
|
281
|
+
defaultActiveFieldId ?? (fieldEditorMode === "single" ? schema.fields[0]?.id : void 0)
|
|
282
|
+
);
|
|
283
|
+
const activeFieldId = controlledActiveFieldId ?? internalActiveFieldId;
|
|
284
|
+
const setActiveFieldId = (0, import_react.useCallback)(
|
|
285
|
+
(fieldId) => {
|
|
286
|
+
if (controlledActiveFieldId === void 0) setInternalActiveFieldId(fieldId);
|
|
287
|
+
onActiveFieldChange?.(fieldId);
|
|
288
|
+
},
|
|
289
|
+
[controlledActiveFieldId, onActiveFieldChange]
|
|
290
|
+
);
|
|
262
291
|
const createId = (0, import_react.useCallback)(
|
|
263
292
|
(kind, existingIds) => {
|
|
264
293
|
const rawId = idFactory(kind, existingIds);
|
|
@@ -371,9 +400,10 @@ function useFormBuilder({
|
|
|
371
400
|
questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
|
|
372
401
|
}));
|
|
373
402
|
onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
|
|
403
|
+
setActiveFieldId(field.id);
|
|
374
404
|
return { success: true };
|
|
375
405
|
},
|
|
376
|
-
[createId, factories, onChange, policy, schema]
|
|
406
|
+
[createId, factories, onChange, policy, schema, setActiveFieldId]
|
|
377
407
|
);
|
|
378
408
|
const removeField = (0, import_react.useCallback)(
|
|
379
409
|
(fieldId) => {
|
|
@@ -381,15 +411,19 @@ function useFormBuilder({
|
|
|
381
411
|
return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
|
|
382
412
|
if (schema.fields.length <= 1)
|
|
383
413
|
return { success: false, error: { type: "invalid_operation", message: "A form must contain one field." } };
|
|
384
|
-
const
|
|
414
|
+
const removedIndex = schema.fields.findIndex((field) => field.id === fieldId);
|
|
415
|
+
const fields = schema.fields.filter((field) => field.id !== fieldId).map(
|
|
416
|
+
(field) => field.displayCondition?.questionId === fieldId || displayRuleSourceIds(field).includes(fieldId) ? withoutDisplayCondition(field) : field
|
|
417
|
+
);
|
|
385
418
|
const pages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
|
|
386
419
|
if (schema.pages !== void 0 && pages?.length === 0) {
|
|
387
420
|
const { pages: _pages, ...single } = schema;
|
|
388
421
|
onChange({ ...single, fields });
|
|
389
422
|
} else onChange({ ...schema, fields, ...pages === void 0 ? {} : { pages } });
|
|
423
|
+
if (activeFieldId === fieldId) setActiveFieldId(fields[removedIndex]?.id ?? fields.at(-1)?.id);
|
|
390
424
|
return { success: true };
|
|
391
425
|
},
|
|
392
|
-
[onChange, schema]
|
|
426
|
+
[activeFieldId, onChange, schema, setActiveFieldId]
|
|
393
427
|
);
|
|
394
428
|
const moveField = (0, import_react.useCallback)(
|
|
395
429
|
(fieldId, targetIndex) => {
|
|
@@ -402,8 +436,8 @@ function useFormBuilder({
|
|
|
402
436
|
onChange({
|
|
403
437
|
...schema,
|
|
404
438
|
fields: fields.map((field, index) => {
|
|
405
|
-
const
|
|
406
|
-
return source
|
|
439
|
+
const sources = field.displayCondition?.questionId === void 0 ? displayRuleSourceIds(field) : [field.displayCondition.questionId];
|
|
440
|
+
return sources.every((source) => (indexById.get(source) ?? index) < index) ? field : withoutDisplayCondition(field);
|
|
407
441
|
})
|
|
408
442
|
});
|
|
409
443
|
return { success: true };
|
|
@@ -827,6 +861,14 @@ function useFormBuilder({
|
|
|
827
861
|
const result = (0, import_core.validateFormSchema)(schema, policy === void 0 ? {} : { policy });
|
|
828
862
|
return result.valid ? [] : result.issues;
|
|
829
863
|
}, [policy, schema]);
|
|
864
|
+
const getFieldEditorProps = (0, import_react.useCallback)(
|
|
865
|
+
(fieldId) => ({
|
|
866
|
+
isActive: activeFieldId === fieldId,
|
|
867
|
+
isVisible: fieldEditorMode === "all" || activeFieldId === fieldId,
|
|
868
|
+
onSelect: () => setActiveFieldId(fieldId)
|
|
869
|
+
}),
|
|
870
|
+
[activeFieldId, fieldEditorMode, setActiveFieldId]
|
|
871
|
+
);
|
|
830
872
|
return {
|
|
831
873
|
schema,
|
|
832
874
|
addField,
|
|
@@ -848,7 +890,10 @@ function useFormBuilder({
|
|
|
848
890
|
setLocaleTranslation,
|
|
849
891
|
addLocale,
|
|
850
892
|
setDefaultLocale,
|
|
851
|
-
validationIssues
|
|
893
|
+
validationIssues,
|
|
894
|
+
...activeFieldId === void 0 ? {} : { activeFieldId },
|
|
895
|
+
setActiveFieldId,
|
|
896
|
+
getFieldEditorProps
|
|
852
897
|
};
|
|
853
898
|
}
|
|
854
899
|
|
|
@@ -1501,7 +1546,10 @@ var BUILDER_DEFAULTS = {
|
|
|
1501
1546
|
"builder.operator.equals": "equals",
|
|
1502
1547
|
"builder.operator.not_equals": "does not equal",
|
|
1503
1548
|
"builder.operator.contains": "contains",
|
|
1504
|
-
"builder.operator.not_empty": "is not empty"
|
|
1549
|
+
"builder.operator.not_empty": "is not empty",
|
|
1550
|
+
"builder.submissionSettings": "Submission settings",
|
|
1551
|
+
"builder.showConfirmationBeforeSubmit": "Show confirmation before submit",
|
|
1552
|
+
"builder.confirmationRenderMode": "Confirmation display mode"
|
|
1505
1553
|
};
|
|
1506
1554
|
function BuilderSectionGroup({ children }) {
|
|
1507
1555
|
return children;
|
|
@@ -1678,7 +1726,12 @@ function FormBuilder({
|
|
|
1678
1726
|
slots,
|
|
1679
1727
|
sectionOrder,
|
|
1680
1728
|
disableDefaultStyles = false,
|
|
1681
|
-
unstyled = false
|
|
1729
|
+
unstyled = false,
|
|
1730
|
+
fieldEditorMode = "all",
|
|
1731
|
+
activeFieldId,
|
|
1732
|
+
defaultActiveFieldId,
|
|
1733
|
+
onActiveFieldChange,
|
|
1734
|
+
submissionSettingsOptions
|
|
1682
1735
|
}) {
|
|
1683
1736
|
const resolvedComponents = { ...DEFAULT_COMPONENTS, ...componentOverrides };
|
|
1684
1737
|
const components = {
|
|
@@ -1701,7 +1754,11 @@ function FormBuilder({
|
|
|
1701
1754
|
onChange,
|
|
1702
1755
|
...policy === void 0 ? {} : { policy },
|
|
1703
1756
|
...idFactory === void 0 ? {} : { idFactory },
|
|
1704
|
-
...factories === void 0 ? {} : { factories }
|
|
1757
|
+
...factories === void 0 ? {} : { factories },
|
|
1758
|
+
fieldEditorMode,
|
|
1759
|
+
...activeFieldId === void 0 ? {} : { activeFieldId },
|
|
1760
|
+
...defaultActiveFieldId === void 0 ? {} : { defaultActiveFieldId },
|
|
1761
|
+
...onActiveFieldChange === void 0 ? {} : { onActiveFieldChange }
|
|
1705
1762
|
});
|
|
1706
1763
|
const [newPageQuestionId, setNewPageQuestionId] = (0, import_react2.useState)("");
|
|
1707
1764
|
const [newLocale, setNewLocale] = (0, import_react2.useState)("");
|
|
@@ -2041,6 +2098,47 @@ function FormBuilder({
|
|
|
2041
2098
|
] })
|
|
2042
2099
|
}
|
|
2043
2100
|
) }),
|
|
2101
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "submissionSettings", children: submissionSettingsOptions?.enabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2102
|
+
Section,
|
|
2103
|
+
{
|
|
2104
|
+
className: builderClass("form-engine-builder__submission-settings"),
|
|
2105
|
+
title: translate("builder.submissionSettings"),
|
|
2106
|
+
children: [
|
|
2107
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2108
|
+
Checkbox,
|
|
2109
|
+
{
|
|
2110
|
+
id: "builder-show-confirmation",
|
|
2111
|
+
label: translate("builder.showConfirmationBeforeSubmit"),
|
|
2112
|
+
checked: schema.submissionSettings?.showConfirmationBeforeSubmit === true,
|
|
2113
|
+
onChange: (checked) => onChange({
|
|
2114
|
+
...schema,
|
|
2115
|
+
submissionSettings: { ...schema.submissionSettings, showConfirmationBeforeSubmit: checked }
|
|
2116
|
+
})
|
|
2117
|
+
}
|
|
2118
|
+
),
|
|
2119
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2120
|
+
Select,
|
|
2121
|
+
{
|
|
2122
|
+
id: "builder-confirmation-render-mode",
|
|
2123
|
+
label: translate("builder.confirmationRenderMode"),
|
|
2124
|
+
value: schema.submissionSettings?.confirmationRenderMode ?? "inline",
|
|
2125
|
+
options: [
|
|
2126
|
+
{ value: "dialog", label: "Dialog" },
|
|
2127
|
+
{ value: "inline", label: "Inline" },
|
|
2128
|
+
{ value: "replace", label: "Replace" }
|
|
2129
|
+
],
|
|
2130
|
+
onChange: (value) => {
|
|
2131
|
+
if (value !== "dialog" && value !== "inline" && value !== "replace") return;
|
|
2132
|
+
onChange({
|
|
2133
|
+
...schema,
|
|
2134
|
+
submissionSettings: { ...schema.submissionSettings, confirmationRenderMode: value }
|
|
2135
|
+
});
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
)
|
|
2139
|
+
]
|
|
2140
|
+
}
|
|
2141
|
+
) : null }),
|
|
2044
2142
|
resolvedSectionOrder === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "completionMessage", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Section, { headingId: "builder-completion-message-heading", title: translate("builder.completionMessage"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2045
2143
|
TextInput,
|
|
2046
2144
|
{
|
|
@@ -2439,9 +2537,25 @@ function FormBuilder({
|
|
|
2439
2537
|
) : null }),
|
|
2440
2538
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
|
|
2441
2539
|
const controls = resolveFieldEditorControls(fieldEditorControls);
|
|
2540
|
+
const editorState = headless.getFieldEditorProps?.(field.id) ?? {
|
|
2541
|
+
isActive: true,
|
|
2542
|
+
isVisible: true,
|
|
2543
|
+
onSelect: () => void 0
|
|
2544
|
+
};
|
|
2442
2545
|
const condition = field.displayCondition;
|
|
2443
2546
|
const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
|
|
2444
2547
|
const availableSources = schema.fields.slice(0, index);
|
|
2548
|
+
if (!editorState.isVisible) {
|
|
2549
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2550
|
+
"div",
|
|
2551
|
+
{
|
|
2552
|
+
className: builderClass("form-engine-builder__question-preview"),
|
|
2553
|
+
"data-field-id": field.id,
|
|
2554
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: editorState.onSelect, children: field.title })
|
|
2555
|
+
},
|
|
2556
|
+
field.id
|
|
2557
|
+
);
|
|
2558
|
+
}
|
|
2445
2559
|
if (FieldEditorSlot !== void 0) {
|
|
2446
2560
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2447
2561
|
FieldEditorSlot,
|
|
@@ -3149,8 +3263,232 @@ function useField(fieldId) {
|
|
|
3149
3263
|
return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
|
|
3150
3264
|
}
|
|
3151
3265
|
|
|
3152
|
-
// src/
|
|
3266
|
+
// src/hooks/useTranslationWorkspace.ts
|
|
3267
|
+
var import_core4 = require("@form-engine-ts/core");
|
|
3153
3268
|
var import_react4 = require("react");
|
|
3269
|
+
function updateTranslationMap(translations, locale, property, text) {
|
|
3270
|
+
if (property === "label") return translations ?? {};
|
|
3271
|
+
const current = translations?.[locale];
|
|
3272
|
+
const next = { ...current, [property]: text };
|
|
3273
|
+
return { ...translations, [locale]: next };
|
|
3274
|
+
}
|
|
3275
|
+
function asAsyncAdapter(adapter) {
|
|
3276
|
+
if ("translateBatch" in adapter) return adapter;
|
|
3277
|
+
return {
|
|
3278
|
+
translateText: async (text, locale, sourceLocale) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text,
|
|
3279
|
+
translateBatch: async (texts, locale, sourceLocale) => texts.map(
|
|
3280
|
+
(text) => adapter.translate(text, locale, sourceLocale === void 0 ? void 0 : { sourceLocale }) ?? text
|
|
3281
|
+
)
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function manualMetadata(sourceText, sourceLocale) {
|
|
3285
|
+
return {
|
|
3286
|
+
sourceLocale,
|
|
3287
|
+
sourceTextHash: (0, import_core4.computeSourceTextHash)(sourceText),
|
|
3288
|
+
translationSource: "manual",
|
|
3289
|
+
editedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3290
|
+
};
|
|
3291
|
+
}
|
|
3292
|
+
function setNodeMetadata(node, slot, text, sourceLocale) {
|
|
3293
|
+
const localeMetadata = node.translationMetadata?.[slot.locale];
|
|
3294
|
+
const nextMetadata = text.trim().length === 0 ? Object.fromEntries(Object.entries(localeMetadata ?? {}).filter(([key]) => key !== slot.property)) : { ...localeMetadata, [slot.property]: manualMetadata(slot.sourceText, sourceLocale) };
|
|
3295
|
+
return { ...node, translationMetadata: { ...node.translationMetadata, [slot.locale]: nextMetadata } };
|
|
3296
|
+
}
|
|
3297
|
+
function updateSchemaTranslation(schema, slot, text, sourceLocale) {
|
|
3298
|
+
if (slot.kind === "form") {
|
|
3299
|
+
return setNodeMetadata(
|
|
3300
|
+
{ ...schema, translations: updateTranslationMap(schema.translations, slot.locale, slot.property, text) },
|
|
3301
|
+
slot,
|
|
3302
|
+
text,
|
|
3303
|
+
sourceLocale
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
if (slot.kind === "field") {
|
|
3307
|
+
return {
|
|
3308
|
+
...schema,
|
|
3309
|
+
fields: schema.fields.map((field) => {
|
|
3310
|
+
if (field.id !== slot.nodeId) return field;
|
|
3311
|
+
const translations = updateTranslationMap(field.translations, slot.locale, slot.property, text);
|
|
3312
|
+
return setNodeMetadata({ ...field, translations }, slot, text, sourceLocale);
|
|
3313
|
+
})
|
|
3314
|
+
};
|
|
3315
|
+
}
|
|
3316
|
+
if (slot.kind === "option") {
|
|
3317
|
+
return {
|
|
3318
|
+
...schema,
|
|
3319
|
+
fields: schema.fields.map((field) => {
|
|
3320
|
+
if (!("options" in field)) return field;
|
|
3321
|
+
return {
|
|
3322
|
+
...field,
|
|
3323
|
+
options: field.options.map((option) => {
|
|
3324
|
+
if (option.id !== slot.nodeId) return option;
|
|
3325
|
+
const translations = text.trim().length === 0 ? Object.fromEntries(
|
|
3326
|
+
Object.entries(option.translations ?? {}).filter(([locale]) => locale !== slot.locale)
|
|
3327
|
+
) : { ...option.translations, [slot.locale]: text };
|
|
3328
|
+
return setNodeMetadata({ ...option, translations }, slot, text, sourceLocale);
|
|
3329
|
+
})
|
|
3330
|
+
};
|
|
3331
|
+
})
|
|
3332
|
+
};
|
|
3333
|
+
}
|
|
3334
|
+
return {
|
|
3335
|
+
...schema,
|
|
3336
|
+
...schema.pages === void 0 ? {} : {
|
|
3337
|
+
pages: schema.pages.map((page) => {
|
|
3338
|
+
if (page.id !== slot.nodeId) return page;
|
|
3339
|
+
const translations = updateTranslationMap(page.translations, slot.locale, slot.property, text);
|
|
3340
|
+
return setNodeMetadata({ ...page, translations }, slot, text, sourceLocale);
|
|
3341
|
+
})
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
}
|
|
3345
|
+
function useTranslationWorkspace({
|
|
3346
|
+
schema,
|
|
3347
|
+
onChange,
|
|
3348
|
+
sourceLocale = schema.defaultLocale ?? "en",
|
|
3349
|
+
targetLocale,
|
|
3350
|
+
translationAdapter,
|
|
3351
|
+
readOnly = false
|
|
3352
|
+
}) {
|
|
3353
|
+
const [draftSchema, setDraftSchema] = (0, import_react4.useState)(schema);
|
|
3354
|
+
const [selectedLocale, setSelectedLocale] = (0, import_react4.useState)(targetLocale ?? "");
|
|
3355
|
+
const [isTranslating, setIsTranslating] = (0, import_react4.useState)(false);
|
|
3356
|
+
const [error, setError] = (0, import_react4.useState)();
|
|
3357
|
+
const currentSchema = onChange === void 0 ? draftSchema : schema;
|
|
3358
|
+
const targetLocales = (0, import_react4.useMemo)(() => {
|
|
3359
|
+
const locales = (0, import_core4.collectSchemaLocales)(currentSchema).allUniqueLocales;
|
|
3360
|
+
return [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], ...locales])].filter(
|
|
3361
|
+
(locale) => locale !== sourceLocale
|
|
3362
|
+
);
|
|
3363
|
+
}, [currentSchema, sourceLocale]);
|
|
3364
|
+
const activeLocale = selectedLocale.length > 0 ? selectedLocale : targetLocales[0] ?? "";
|
|
3365
|
+
const slots = (0, import_react4.useMemo)(
|
|
3366
|
+
() => activeLocale.length === 0 ? [] : (0, import_core4.collectTranslationSlots)(currentSchema, activeLocale),
|
|
3367
|
+
[activeLocale, currentSchema]
|
|
3368
|
+
);
|
|
3369
|
+
const summary = (0, import_react4.useMemo)(() => {
|
|
3370
|
+
const counts = {
|
|
3371
|
+
missing: 0,
|
|
3372
|
+
translated: 0,
|
|
3373
|
+
stale: 0,
|
|
3374
|
+
manual: 0,
|
|
3375
|
+
"manual-stale": 0
|
|
3376
|
+
};
|
|
3377
|
+
for (const slot of slots) counts[slot.status ?? "missing"] += 1;
|
|
3378
|
+
const complete = counts.translated + counts.manual;
|
|
3379
|
+
return {
|
|
3380
|
+
totalSlots: slots.length,
|
|
3381
|
+
translatedCount: complete,
|
|
3382
|
+
missingCount: counts.missing,
|
|
3383
|
+
staleCount: counts.stale + counts["manual-stale"],
|
|
3384
|
+
manualCount: counts.manual + counts["manual-stale"],
|
|
3385
|
+
completionPercentage: slots.length === 0 ? 100 : Math.round(complete / slots.length * 100)
|
|
3386
|
+
};
|
|
3387
|
+
}, [slots]);
|
|
3388
|
+
const commit = (0, import_react4.useCallback)(
|
|
3389
|
+
(next) => {
|
|
3390
|
+
setDraftSchema(next);
|
|
3391
|
+
onChange?.(next);
|
|
3392
|
+
},
|
|
3393
|
+
[onChange]
|
|
3394
|
+
);
|
|
3395
|
+
const setTranslation = (0, import_react4.useCallback)(
|
|
3396
|
+
(slot, text) => {
|
|
3397
|
+
if (readOnly) return;
|
|
3398
|
+
commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
|
|
3399
|
+
},
|
|
3400
|
+
[commit, currentSchema, readOnly, sourceLocale]
|
|
3401
|
+
);
|
|
3402
|
+
const addLocale = (0, import_react4.useCallback)(
|
|
3403
|
+
(locale) => {
|
|
3404
|
+
if (readOnly || locale.trim().length === 0) return;
|
|
3405
|
+
const normalized = locale.trim();
|
|
3406
|
+
commit({
|
|
3407
|
+
...currentSchema,
|
|
3408
|
+
supportedLocales: [.../* @__PURE__ */ new Set([...currentSchema.supportedLocales ?? [], normalized])]
|
|
3409
|
+
});
|
|
3410
|
+
setSelectedLocale(normalized);
|
|
3411
|
+
},
|
|
3412
|
+
[commit, currentSchema, readOnly]
|
|
3413
|
+
);
|
|
3414
|
+
const removeLocale = (0, import_react4.useCallback)(
|
|
3415
|
+
(locale) => {
|
|
3416
|
+
if (readOnly || locale === sourceLocale) return;
|
|
3417
|
+
commit({
|
|
3418
|
+
...currentSchema,
|
|
3419
|
+
supportedLocales: (currentSchema.supportedLocales ?? []).filter((candidate) => candidate !== locale)
|
|
3420
|
+
});
|
|
3421
|
+
if (activeLocale === locale) setSelectedLocale("");
|
|
3422
|
+
},
|
|
3423
|
+
[activeLocale, commit, currentSchema, readOnly, sourceLocale]
|
|
3424
|
+
);
|
|
3425
|
+
const translateAll = (0, import_react4.useCallback)(
|
|
3426
|
+
async (options = {}) => {
|
|
3427
|
+
if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
|
|
3428
|
+
if (activeLocale.length === 0) throw new Error("A target locale is required.");
|
|
3429
|
+
setIsTranslating(true);
|
|
3430
|
+
setError(void 0);
|
|
3431
|
+
try {
|
|
3432
|
+
const populated = await (0, import_core4.populateSchemaTranslations)(
|
|
3433
|
+
currentSchema,
|
|
3434
|
+
[activeLocale],
|
|
3435
|
+
asAsyncAdapter(translationAdapter),
|
|
3436
|
+
{
|
|
3437
|
+
overwrite: "stale-and-missing",
|
|
3438
|
+
preserveManualTranslations: true,
|
|
3439
|
+
...options
|
|
3440
|
+
}
|
|
3441
|
+
);
|
|
3442
|
+
commit(populated.schema);
|
|
3443
|
+
return populated.report;
|
|
3444
|
+
} catch (cause) {
|
|
3445
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3446
|
+
setError(message);
|
|
3447
|
+
throw cause;
|
|
3448
|
+
} finally {
|
|
3449
|
+
setIsTranslating(false);
|
|
3450
|
+
}
|
|
3451
|
+
},
|
|
3452
|
+
[activeLocale, commit, currentSchema, translationAdapter]
|
|
3453
|
+
);
|
|
3454
|
+
const translateSlot = (0, import_react4.useCallback)(
|
|
3455
|
+
async (slot) => {
|
|
3456
|
+
if (translationAdapter === void 0) throw new Error("A translation adapter is required.");
|
|
3457
|
+
if (readOnly) return;
|
|
3458
|
+
setIsTranslating(true);
|
|
3459
|
+
setError(void 0);
|
|
3460
|
+
try {
|
|
3461
|
+
const text = await asAsyncAdapter(translationAdapter).translateText(slot.sourceText, slot.locale, sourceLocale);
|
|
3462
|
+
commit(updateSchemaTranslation(currentSchema, slot, text, sourceLocale));
|
|
3463
|
+
} catch (cause) {
|
|
3464
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
3465
|
+
setError(message);
|
|
3466
|
+
throw cause;
|
|
3467
|
+
} finally {
|
|
3468
|
+
setIsTranslating(false);
|
|
3469
|
+
}
|
|
3470
|
+
},
|
|
3471
|
+
[commit, currentSchema, readOnly, sourceLocale, translationAdapter]
|
|
3472
|
+
);
|
|
3473
|
+
return {
|
|
3474
|
+
sourceLocale,
|
|
3475
|
+
targetLocale: activeLocale,
|
|
3476
|
+
targetLocales,
|
|
3477
|
+
setTargetLocale: setSelectedLocale,
|
|
3478
|
+
slots,
|
|
3479
|
+
summary,
|
|
3480
|
+
addLocale,
|
|
3481
|
+
removeLocale,
|
|
3482
|
+
setTranslation,
|
|
3483
|
+
translateAll,
|
|
3484
|
+
translateSlot,
|
|
3485
|
+
isTranslating,
|
|
3486
|
+
...error === void 0 ? {} : { error }
|
|
3487
|
+
};
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// src/receipt.ts
|
|
3491
|
+
var import_react5 = require("react");
|
|
3154
3492
|
function submissionReceiptQueryKey(formId, formVersion) {
|
|
3155
3493
|
return `${formId}:v${formVersion}`;
|
|
3156
3494
|
}
|
|
@@ -3221,19 +3559,19 @@ function createLocalStorageSubmissionReceiptStore(options = {}) {
|
|
|
3221
3559
|
}
|
|
3222
3560
|
function useSubmissionReceipts(store, queries) {
|
|
3223
3561
|
const querySignature = JSON.stringify(queries.map(({ formId, formVersion }) => [formId, formVersion]));
|
|
3224
|
-
const stableQueries = (0,
|
|
3562
|
+
const stableQueries = (0, import_react5.useMemo)(() => {
|
|
3225
3563
|
const parsed = JSON.parse(querySignature);
|
|
3226
3564
|
if (!Array.isArray(parsed)) return [];
|
|
3227
3565
|
return parsed.flatMap(
|
|
3228
3566
|
(entry) => Array.isArray(entry) && typeof entry[0] === "string" && typeof entry[1] === "number" && Number.isSafeInteger(entry[1]) ? [{ formId: entry[0], formVersion: entry[1] }] : []
|
|
3229
3567
|
);
|
|
3230
3568
|
}, [querySignature]);
|
|
3231
|
-
const [state, setState] = (0,
|
|
3569
|
+
const [state, setState] = (0, import_react5.useState)({
|
|
3232
3570
|
receipts: /* @__PURE__ */ new Map(),
|
|
3233
3571
|
isLoading: stableQueries.length > 0,
|
|
3234
3572
|
error: null
|
|
3235
3573
|
});
|
|
3236
|
-
(0,
|
|
3574
|
+
(0, import_react5.useEffect)(() => {
|
|
3237
3575
|
let active = true;
|
|
3238
3576
|
if (stableQueries.length === 0) {
|
|
3239
3577
|
setState({ receipts: /* @__PURE__ */ new Map(), isLoading: false, error: null });
|
|
@@ -3267,9 +3605,17 @@ function useSubmissionReceipts(store, queries) {
|
|
|
3267
3605
|
}
|
|
3268
3606
|
|
|
3269
3607
|
// src/renderer.tsx
|
|
3270
|
-
var
|
|
3271
|
-
var
|
|
3608
|
+
var import_core5 = require("@form-engine-ts/core");
|
|
3609
|
+
var import_react6 = require("react");
|
|
3272
3610
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
3611
|
+
var isChoiceFieldType = (type) => type === "radio" || type === "checkbox" || type === "multi-select" || type === "select";
|
|
3612
|
+
function resolveChoiceFieldLayout(type, appearance, groupedChoiceFieldsLegacy = false) {
|
|
3613
|
+
if (groupedChoiceFieldsLegacy) return "grouped";
|
|
3614
|
+
const config = appearance?.choiceField;
|
|
3615
|
+
if (config === void 0) return "default";
|
|
3616
|
+
if (typeof config === "string") return config;
|
|
3617
|
+
return config[type] ?? "default";
|
|
3618
|
+
}
|
|
3273
3619
|
function describedBy(field, error, helpId, errorId) {
|
|
3274
3620
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
3275
3621
|
Boolean
|
|
@@ -3297,51 +3643,98 @@ function GroupedChoiceDescription({ props }) {
|
|
|
3297
3643
|
function GroupedChoiceError({ props }) {
|
|
3298
3644
|
return props.error === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { id: props.errorId, className: "fe-field-error", role: "alert", children: props.translate(props.error.messageKey, props.error.params) });
|
|
3299
3645
|
}
|
|
3646
|
+
function ChoiceGroupFrame({
|
|
3647
|
+
props,
|
|
3648
|
+
children,
|
|
3649
|
+
className,
|
|
3650
|
+
slotProps,
|
|
3651
|
+
renderChoiceGroup,
|
|
3652
|
+
disabled,
|
|
3653
|
+
readOnly
|
|
3654
|
+
}) {
|
|
3655
|
+
const { field, error, translate } = props;
|
|
3656
|
+
const groupError = error === void 0 ? void 0 : { ...error, message: translate(error.messageKey, error.params) };
|
|
3657
|
+
const groupProps = {
|
|
3658
|
+
field,
|
|
3659
|
+
title: field.title,
|
|
3660
|
+
...field.description === void 0 ? {} : { description: field.description },
|
|
3661
|
+
...field.required === void 0 ? {} : { required: field.required },
|
|
3662
|
+
...groupError === void 0 ? {} : { error: groupError },
|
|
3663
|
+
...disabled === void 0 ? {} : { disabled },
|
|
3664
|
+
...readOnly === void 0 ? {} : { readOnly },
|
|
3665
|
+
children,
|
|
3666
|
+
className
|
|
3667
|
+
};
|
|
3668
|
+
if (renderChoiceGroup !== void 0) return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: renderChoiceGroup(groupProps) });
|
|
3669
|
+
return (
|
|
3670
|
+
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: The grouped fieldset exposes the required state for the complete choice question.
|
|
3671
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
3672
|
+
"fieldset",
|
|
3673
|
+
{
|
|
3674
|
+
className,
|
|
3675
|
+
"data-field-id": field.id,
|
|
3676
|
+
disabled,
|
|
3677
|
+
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3678
|
+
"aria-invalid": Boolean(error),
|
|
3679
|
+
"aria-required": field.required,
|
|
3680
|
+
style: slotProps?.style,
|
|
3681
|
+
children: [
|
|
3682
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("legend", { className: "fe-choice-legend", children: [
|
|
3683
|
+
field.title,
|
|
3684
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RequiredMark, { required: field.required, className: "fe-required-badge" })
|
|
3685
|
+
] }),
|
|
3686
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceDescription, { props }),
|
|
3687
|
+
children,
|
|
3688
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
|
|
3689
|
+
]
|
|
3690
|
+
}
|
|
3691
|
+
)
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3300
3694
|
function DefaultField({
|
|
3301
3695
|
groupedChoiceFields,
|
|
3696
|
+
appearance,
|
|
3697
|
+
choiceGroupSlotProps,
|
|
3698
|
+
renderChoiceGroup,
|
|
3699
|
+
disabled,
|
|
3700
|
+
readOnly,
|
|
3302
3701
|
...props
|
|
3303
3702
|
}) {
|
|
3304
3703
|
const { field, value, setValue, inputId, error, translate } = props;
|
|
3704
|
+
const isGroupedChoiceField = isChoiceFieldType(field.type) && resolveChoiceFieldLayout(field.type, appearance, groupedChoiceFields) === "grouped";
|
|
3705
|
+
const choiceGroupClassName = ["fe-choice-group", `fe-field--${field.type}`, choiceGroupSlotProps?.className].filter((item) => item !== void 0 && item.length > 0).join(" ");
|
|
3305
3706
|
const ariaProps = {
|
|
3306
3707
|
"aria-describedby": describedBy(field, error, props.helpId, props.errorId),
|
|
3307
3708
|
"aria-invalid": error === void 0 ? void 0 : true
|
|
3308
3709
|
};
|
|
3309
3710
|
if (field.type === "checkbox") {
|
|
3310
|
-
if (
|
|
3311
|
-
return (
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
}
|
|
3338
|
-
),
|
|
3339
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.title })
|
|
3340
|
-
] }) }),
|
|
3341
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GroupedChoiceError, { props })
|
|
3342
|
-
]
|
|
3343
|
-
}
|
|
3344
|
-
)
|
|
3711
|
+
if (isGroupedChoiceField) {
|
|
3712
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3713
|
+
ChoiceGroupFrame,
|
|
3714
|
+
{
|
|
3715
|
+
props,
|
|
3716
|
+
className: choiceGroupClassName,
|
|
3717
|
+
slotProps: choiceGroupSlotProps,
|
|
3718
|
+
renderChoiceGroup,
|
|
3719
|
+
disabled,
|
|
3720
|
+
readOnly,
|
|
3721
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: inputId, children: [
|
|
3722
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3723
|
+
"input",
|
|
3724
|
+
{
|
|
3725
|
+
id: inputId,
|
|
3726
|
+
name: field.id,
|
|
3727
|
+
type: "checkbox",
|
|
3728
|
+
checked: value === true,
|
|
3729
|
+
"aria-label": field.title,
|
|
3730
|
+
disabled,
|
|
3731
|
+
readOnly,
|
|
3732
|
+
onChange: (event) => setValue(event.currentTarget.checked)
|
|
3733
|
+
}
|
|
3734
|
+
),
|
|
3735
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: field.title })
|
|
3736
|
+
] }) })
|
|
3737
|
+
}
|
|
3345
3738
|
);
|
|
3346
3739
|
}
|
|
3347
3740
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-field fe-field--checkbox", "data-field-id": field.id, children: [
|
|
@@ -3367,56 +3760,55 @@ function DefaultField({
|
|
|
3367
3760
|
}
|
|
3368
3761
|
if (field.type === "radio" || field.type === "multi-select") {
|
|
3369
3762
|
const selected = Array.isArray(value) ? value : [];
|
|
3370
|
-
if (
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
)
|
|
3763
|
+
if (isGroupedChoiceField) {
|
|
3764
|
+
const isRadio = field.type === "radio";
|
|
3765
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3766
|
+
ChoiceGroupFrame,
|
|
3767
|
+
{
|
|
3768
|
+
props,
|
|
3769
|
+
className: choiceGroupClassName,
|
|
3770
|
+
slotProps: choiceGroupSlotProps,
|
|
3771
|
+
renderChoiceGroup,
|
|
3772
|
+
disabled,
|
|
3773
|
+
readOnly,
|
|
3774
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: field.options.map((option, index) => {
|
|
3775
|
+
const optionId = `${inputId}-${index}`;
|
|
3776
|
+
const checked = isRadio ? value === option.id : selected.includes(option.id);
|
|
3777
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { className: "fe-choice-option", htmlFor: optionId, children: [
|
|
3778
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3779
|
+
"input",
|
|
3780
|
+
{
|
|
3781
|
+
id: optionId,
|
|
3782
|
+
name: field.id,
|
|
3783
|
+
type: isRadio ? "radio" : "checkbox",
|
|
3784
|
+
value: option.id,
|
|
3785
|
+
checked,
|
|
3786
|
+
disabled,
|
|
3787
|
+
readOnly,
|
|
3788
|
+
onKeyDown: isRadio ? (event) => {
|
|
3789
|
+
if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "ArrowUp" && event.key !== "ArrowLeft")
|
|
3790
|
+
return;
|
|
3791
|
+
event.preventDefault();
|
|
3792
|
+
const offset = event.key === "ArrowDown" || event.key === "ArrowRight" ? 1 : -1;
|
|
3793
|
+
const nextIndex = (index + offset + field.options.length) % field.options.length;
|
|
3794
|
+
const nextOption = field.options[nextIndex];
|
|
3795
|
+
if (nextOption === void 0) return;
|
|
3796
|
+
setValue(nextOption.id);
|
|
3797
|
+
document.getElementById(`${inputId}-${nextIndex}`)?.focus();
|
|
3798
|
+
} : void 0,
|
|
3799
|
+
onChange: (event) => {
|
|
3800
|
+
if (isRadio) setValue(option.id);
|
|
3801
|
+
else
|
|
3802
|
+
setValue(
|
|
3803
|
+
event.currentTarget.checked ? [...selected, option.id] : selected.filter((item) => item !== option.id)
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
),
|
|
3808
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: option.label })
|
|
3809
|
+
] }, option.id);
|
|
3810
|
+
}) })
|
|
3811
|
+
}
|
|
3420
3812
|
);
|
|
3421
3813
|
}
|
|
3422
3814
|
if (field.type === "radio") {
|
|
@@ -3553,6 +3945,7 @@ function DefaultField({
|
|
|
3553
3945
|
...ariaProps,
|
|
3554
3946
|
id: inputId,
|
|
3555
3947
|
name: field.id,
|
|
3948
|
+
disabled,
|
|
3556
3949
|
value: typeof value === "string" ? value : "",
|
|
3557
3950
|
onChange: (event) => setValue(event.currentTarget.value || void 0),
|
|
3558
3951
|
children: [
|
|
@@ -3576,6 +3969,20 @@ function DefaultField({
|
|
|
3576
3969
|
}
|
|
3577
3970
|
);
|
|
3578
3971
|
}
|
|
3972
|
+
if (isGroupedChoiceField) {
|
|
3973
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
3974
|
+
ChoiceGroupFrame,
|
|
3975
|
+
{
|
|
3976
|
+
props,
|
|
3977
|
+
className: choiceGroupClassName,
|
|
3978
|
+
slotProps: choiceGroupSlotProps,
|
|
3979
|
+
renderChoiceGroup,
|
|
3980
|
+
disabled,
|
|
3981
|
+
readOnly,
|
|
3982
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-choice-options", children: control })
|
|
3983
|
+
}
|
|
3984
|
+
);
|
|
3985
|
+
}
|
|
3579
3986
|
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: `fe-field fe-field--${field.type}`, "data-field-id": field.id, children: [
|
|
3580
3987
|
label,
|
|
3581
3988
|
control,
|
|
@@ -3696,6 +4103,7 @@ function ContextFormRenderer({
|
|
|
3696
4103
|
onDraftSave,
|
|
3697
4104
|
successRenderMode = "append",
|
|
3698
4105
|
appearance,
|
|
4106
|
+
slotProps,
|
|
3699
4107
|
groupedChoiceFields = false,
|
|
3700
4108
|
submissionConfirmation,
|
|
3701
4109
|
submissionConfirmationRenderMode,
|
|
@@ -3709,42 +4117,41 @@ function ContextFormRenderer({
|
|
|
3709
4117
|
slots = {}
|
|
3710
4118
|
}) {
|
|
3711
4119
|
const form = useForm();
|
|
3712
|
-
const
|
|
3713
|
-
const
|
|
3714
|
-
const
|
|
3715
|
-
const
|
|
3716
|
-
const [
|
|
3717
|
-
const [
|
|
3718
|
-
const [
|
|
3719
|
-
const [
|
|
3720
|
-
const [
|
|
3721
|
-
const [
|
|
3722
|
-
const [
|
|
3723
|
-
const [
|
|
3724
|
-
const
|
|
3725
|
-
const
|
|
3726
|
-
const
|
|
3727
|
-
const
|
|
3728
|
-
const confirmationRef = (0, import_react5.useRef)(null);
|
|
4120
|
+
const prefix = (0, import_react6.useId)().replace(/:/g, "");
|
|
4121
|
+
const formRef = (0, import_react6.useRef)(null);
|
|
4122
|
+
const loadedDraftKey = (0, import_react6.useRef)(null);
|
|
4123
|
+
const [draftRestored, setDraftRestored] = (0, import_react6.useState)(false);
|
|
4124
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react6.useState)(0);
|
|
4125
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react6.useState)(null);
|
|
4126
|
+
const [confirmation, setConfirmation] = (0, import_react6.useState)(null);
|
|
4127
|
+
const [guardMessage, setGuardMessage] = (0, import_react6.useState)(null);
|
|
4128
|
+
const [guardsPending, setGuardsPending] = (0, import_react6.useState)(false);
|
|
4129
|
+
const [receipt, setReceipt] = (0, import_react6.useState)(null);
|
|
4130
|
+
const [completionData, setCompletionData] = (0, import_react6.useState)(null);
|
|
4131
|
+
const [receiptLoaded, setReceiptLoaded] = (0, import_react6.useState)(receiptStore === void 0);
|
|
4132
|
+
const rendererSubmissionInFlight = (0, import_react6.useRef)(false);
|
|
4133
|
+
const fallbackAttemptId = (0, import_react6.useRef)(null);
|
|
4134
|
+
const completionRef = (0, import_react6.useRef)(null);
|
|
4135
|
+
const confirmationRef = (0, import_react6.useRef)(null);
|
|
3729
4136
|
const pages = form.schema.pages;
|
|
3730
|
-
const visiblePageIndexes = (0,
|
|
4137
|
+
const visiblePageIndexes = (0, import_react6.useMemo)(
|
|
3731
4138
|
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
3732
4139
|
[form.pageVisibility, pages]
|
|
3733
4140
|
);
|
|
3734
4141
|
const activePage = pages?.[currentPageIndex];
|
|
3735
4142
|
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
3736
4143
|
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
3737
|
-
const visibleValues = (0,
|
|
3738
|
-
const visibleItems = (0,
|
|
4144
|
+
const visibleValues = (0, import_react6.useMemo)(() => (0, import_core5.selectVisibleAnswers)(form.schema, form.values), [form.schema, form.values]);
|
|
4145
|
+
const visibleItems = (0, import_react6.useMemo)(
|
|
3739
4146
|
() => buildSubmittedItems(form.schema, form.values, form.visibility, (key) => form.translate(key), false),
|
|
3740
4147
|
[form.schema, form.translate, form.values, form.visibility]
|
|
3741
4148
|
);
|
|
3742
|
-
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? "inline";
|
|
3743
|
-
const confirmationEnabled = submissionConfirmation?.enabled
|
|
4149
|
+
const confirmationRenderMode = submissionConfirmation?.renderMode ?? submissionConfirmationRenderMode ?? form.schema.submissionSettings?.confirmationRenderMode ?? "inline";
|
|
4150
|
+
const confirmationEnabled = submissionConfirmation?.enabled ?? form.schema.submissionSettings?.showConfirmationBeforeSubmit ?? false;
|
|
3744
4151
|
const submitState = confirmation === null && !guardsPending ? form.submitStatus : "confirming";
|
|
3745
4152
|
const interactionLocked = submitState === "confirming" || submitState === "submitting";
|
|
3746
4153
|
const isReplaceMode = successRenderMode === "replace" || hideFormOnSuccess;
|
|
3747
|
-
const resolveMessage = (0,
|
|
4154
|
+
const resolveMessage = (0, import_react6.useCallback)(
|
|
3748
4155
|
(key, fallback) => {
|
|
3749
4156
|
const defaultText = fallback ?? DEFAULT_RENDERER_MESSAGES[form.locale.toLowerCase().startsWith("ja") ? "ja" : "en"][key] ?? key;
|
|
3750
4157
|
const configured = messages[key];
|
|
@@ -3752,15 +4159,15 @@ function ContextFormRenderer({
|
|
|
3752
4159
|
},
|
|
3753
4160
|
[form.locale, messageResolver, messages]
|
|
3754
4161
|
);
|
|
3755
|
-
const fieldTranslate = (0,
|
|
4162
|
+
const fieldTranslate = (0, import_react6.useCallback)(
|
|
3756
4163
|
(key, params) => key === "validation.required" && (messages.requiredField !== void 0 || messageResolver !== void 0) ? resolveMessage("requiredField") : form.translate(key, params),
|
|
3757
4164
|
[form.translate, messageResolver, messages.requiredField, resolveMessage]
|
|
3758
4165
|
);
|
|
3759
|
-
const focusSubmitButton = (0,
|
|
4166
|
+
const focusSubmitButton = (0, import_react6.useCallback)(() => {
|
|
3760
4167
|
const button = formRef.current?.querySelector(".fe-submit, button[type='submit'], button");
|
|
3761
4168
|
button?.focus();
|
|
3762
4169
|
}, []);
|
|
3763
|
-
(0,
|
|
4170
|
+
(0, import_react6.useEffect)(() => {
|
|
3764
4171
|
let active = true;
|
|
3765
4172
|
if (receiptStore === void 0) {
|
|
3766
4173
|
setReceipt(null);
|
|
@@ -3781,14 +4188,14 @@ function ContextFormRenderer({
|
|
|
3781
4188
|
active = false;
|
|
3782
4189
|
};
|
|
3783
4190
|
}, [form.schema.id, form.schema.version, receiptStore]);
|
|
3784
|
-
(0,
|
|
4191
|
+
(0, import_react6.useEffect)(() => {
|
|
3785
4192
|
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
3786
4193
|
setCurrentPageIndex(0);
|
|
3787
4194
|
return;
|
|
3788
4195
|
}
|
|
3789
4196
|
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
3790
4197
|
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
3791
|
-
(0,
|
|
4198
|
+
(0, import_react6.useEffect)(() => {
|
|
3792
4199
|
if (focusFieldId === null) return;
|
|
3793
4200
|
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
3794
4201
|
(element) => element.dataset.fieldId === focusFieldId
|
|
@@ -3800,11 +4207,11 @@ function ContextFormRenderer({
|
|
|
3800
4207
|
setFocusFieldId(null);
|
|
3801
4208
|
}
|
|
3802
4209
|
}, [focusFieldId]);
|
|
3803
|
-
(0,
|
|
4210
|
+
(0, import_react6.useEffect)(() => {
|
|
3804
4211
|
if (!isReplaceMode || form.submitStatus !== "success") return;
|
|
3805
4212
|
completionRef.current?.focus();
|
|
3806
4213
|
}, [form.submitStatus, isReplaceMode]);
|
|
3807
|
-
(0,
|
|
4214
|
+
(0, import_react6.useEffect)(() => {
|
|
3808
4215
|
if (confirmation === null) return;
|
|
3809
4216
|
const confirmButton = confirmationRef.current?.querySelector("[data-fe-confirm], button");
|
|
3810
4217
|
confirmButton?.focus();
|
|
@@ -3836,7 +4243,7 @@ function ContextFormRenderer({
|
|
|
3836
4243
|
globalThis.addEventListener("keydown", onKeyDown);
|
|
3837
4244
|
return () => globalThis.removeEventListener("keydown", onKeyDown);
|
|
3838
4245
|
}, [confirmation, confirmationRenderMode, focusSubmitButton]);
|
|
3839
|
-
(0,
|
|
4246
|
+
(0, import_react6.useEffect)(() => {
|
|
3840
4247
|
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
3841
4248
|
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
3842
4249
|
if (loadedDraftKey.current === loadIdentity) return;
|
|
@@ -3848,7 +4255,7 @@ function ContextFormRenderer({
|
|
|
3848
4255
|
form.restoreValues(draft.values);
|
|
3849
4256
|
setDraftRestored(true);
|
|
3850
4257
|
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
3851
|
-
(0,
|
|
4258
|
+
(0, import_react6.useEffect)(() => {
|
|
3852
4259
|
if (form.submitStatus === "success") return;
|
|
3853
4260
|
const timeout = globalThis.setTimeout(() => {
|
|
3854
4261
|
onDraftSave?.(form.values);
|
|
@@ -3904,7 +4311,7 @@ function ContextFormRenderer({
|
|
|
3904
4311
|
if (rendererSubmissionInFlight.current || submitState === "submitting" || submitState === "success" || confirmation !== null && !guardsConfirmed) {
|
|
3905
4312
|
return { status: "cancelled" };
|
|
3906
4313
|
}
|
|
3907
|
-
const validation = (0,
|
|
4314
|
+
const validation = (0, import_core5.validateAnswers)(form.schema, form.values);
|
|
3908
4315
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
3909
4316
|
if (validation.valid && !guardsConfirmed) {
|
|
3910
4317
|
rendererSubmissionInFlight.current = true;
|
|
@@ -4206,7 +4613,7 @@ function ContextFormRenderer({
|
|
|
4206
4613
|
...slots.renderCharacterCount === void 0 ? {} : { renderCharacterCount: slots.renderCharacterCount }
|
|
4207
4614
|
};
|
|
4208
4615
|
if (slots.renderField !== void 0) {
|
|
4209
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4616
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_react6.Fragment, { children: slots.renderField({
|
|
4210
4617
|
question: field,
|
|
4211
4618
|
value: form.values[field.id],
|
|
4212
4619
|
onChange: (value) => {
|
|
@@ -4216,7 +4623,18 @@ function ContextFormRenderer({
|
|
|
4216
4623
|
}) }, field.id);
|
|
4217
4624
|
}
|
|
4218
4625
|
const Component = components[field.type];
|
|
4219
|
-
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4626
|
+
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
4627
|
+
DefaultField,
|
|
4628
|
+
{
|
|
4629
|
+
...props,
|
|
4630
|
+
groupedChoiceFields,
|
|
4631
|
+
appearance,
|
|
4632
|
+
choiceGroupSlotProps: slotProps?.choiceGroup,
|
|
4633
|
+
renderChoiceGroup: slots.renderChoiceGroup,
|
|
4634
|
+
disabled: interactionLocked
|
|
4635
|
+
},
|
|
4636
|
+
field.id
|
|
4637
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
|
|
4220
4638
|
});
|
|
4221
4639
|
const fieldClassName = `fe-fields${fieldsClassName === void 0 ? "" : ` ${fieldsClassName}`}`;
|
|
4222
4640
|
return slots.renderFields?.({ children: fieldChildren, className: fieldClassName }) ?? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: fieldClassName, children: fieldChildren });
|
|
@@ -4356,6 +4774,7 @@ function FormRenderer(props) {
|
|
|
4356
4774
|
createLocalStorageSubmissionAttemptStore,
|
|
4357
4775
|
createLocalStorageSubmissionReceiptStore,
|
|
4358
4776
|
isTranslationUnresolved,
|
|
4777
|
+
resolveChoiceFieldLayout,
|
|
4359
4778
|
resolveFieldEditorControls,
|
|
4360
4779
|
resolveFieldTypeSelectOptions,
|
|
4361
4780
|
resolveInitialFieldType,
|
|
@@ -4364,5 +4783,6 @@ function FormRenderer(props) {
|
|
|
4364
4783
|
useField,
|
|
4365
4784
|
useForm,
|
|
4366
4785
|
useFormBuilder,
|
|
4367
|
-
useSubmissionReceipts
|
|
4786
|
+
useSubmissionReceipts,
|
|
4787
|
+
useTranslationWorkspace
|
|
4368
4788
|
});
|