@form-engine-ts/react 1.0.0 → 2.0.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 +42 -0
- package/dist/index.cjs +1262 -110
- package/dist/index.d.cts +114 -6
- package/dist/index.d.ts +114 -6
- package/dist/index.js +1270 -104
- package/dist/styles.css +77 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,5 +1,387 @@
|
|
|
1
1
|
// src/builder.tsx
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
populateSchemaTranslations,
|
|
4
|
+
sanitizeSchema
|
|
5
|
+
} from "@form-engine-ts/core";
|
|
6
|
+
import { useState } from "react";
|
|
7
|
+
|
|
8
|
+
// src/hooks/useFormBuilder.ts
|
|
9
|
+
import {
|
|
10
|
+
validateFormSchema
|
|
11
|
+
} from "@form-engine-ts/core";
|
|
12
|
+
import { useCallback, useMemo } from "react";
|
|
13
|
+
var DEFAULT_PREFIXES = { field: "q", option: "opt", page: "page" };
|
|
14
|
+
function defaultIdFactory(kind, existingIds) {
|
|
15
|
+
const prefix = DEFAULT_PREFIXES[kind];
|
|
16
|
+
let id;
|
|
17
|
+
do {
|
|
18
|
+
id = `${prefix}_${globalThis.crypto.randomUUID().slice(0, 8)}`;
|
|
19
|
+
} while (existingIds.has(id));
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
function newField(type, id, optionId) {
|
|
23
|
+
const base = { id, title: "New question", required: false };
|
|
24
|
+
if (type === "text" || type === "textarea") return { ...base, type };
|
|
25
|
+
if (type === "number") return { ...base, type };
|
|
26
|
+
if (type === "rating") return { ...base, type, min: 1, max: 5 };
|
|
27
|
+
if (type === "checkbox") return { ...base, type };
|
|
28
|
+
return optionId === void 0 ? void 0 : { ...base, type, options: [{ id: optionId, label: "Option 1" }] };
|
|
29
|
+
}
|
|
30
|
+
function move(items, sourceIndex, targetIndex) {
|
|
31
|
+
if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
|
|
32
|
+
return void 0;
|
|
33
|
+
const result = [...items];
|
|
34
|
+
const [item] = result.splice(sourceIndex, 1);
|
|
35
|
+
if (item === void 0) return void 0;
|
|
36
|
+
result.splice(targetIndex, 0, item);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
function addPolicyIssues(schema, policy, issues) {
|
|
40
|
+
if (policy?.maxFields !== void 0 && schema.fields.length > policy.maxFields) {
|
|
41
|
+
issues.push({
|
|
42
|
+
path: "fields",
|
|
43
|
+
code: "max_fields_exceeded",
|
|
44
|
+
message: `At most ${policy.maxFields} fields are allowed.`
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
schema.fields.forEach((field, index) => {
|
|
48
|
+
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(field.type)) {
|
|
49
|
+
issues.push({
|
|
50
|
+
path: `fields[${index}].type`,
|
|
51
|
+
code: "disallowed_field_type",
|
|
52
|
+
message: `Field type ${field.type} is not allowed.`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (policy?.maxTextLength !== void 0) {
|
|
56
|
+
for (const [property, text] of [
|
|
57
|
+
["title", field.title],
|
|
58
|
+
["description", field.description]
|
|
59
|
+
]) {
|
|
60
|
+
if (text !== void 0 && text.length > policy.maxTextLength) {
|
|
61
|
+
issues.push({
|
|
62
|
+
path: `fields[${index}].${property}`,
|
|
63
|
+
code: "max_text_length_exceeded",
|
|
64
|
+
message: `Text must be at most ${policy.maxTextLength} characters.`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (policy?.maxOptionsPerField !== void 0 && "options" in field && field.options.length > policy.maxOptionsPerField) {
|
|
70
|
+
issues.push({
|
|
71
|
+
path: `fields[${index}].options`,
|
|
72
|
+
code: "max_options_exceeded",
|
|
73
|
+
message: `At most ${policy.maxOptionsPerField} options are allowed.`
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
for (const locale of policy?.requiredLocales ?? []) {
|
|
78
|
+
if (!(schema.supportedLocales ?? []).includes(locale)) {
|
|
79
|
+
issues.push({
|
|
80
|
+
path: "supportedLocales",
|
|
81
|
+
code: "required_locale_missing",
|
|
82
|
+
message: `Required locale ${locale} is missing.`
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (locale === schema.defaultLocale) continue;
|
|
86
|
+
const requiredTranslations = [
|
|
87
|
+
{ path: `translations.${locale}.title`, value: schema.translations?.[locale]?.title },
|
|
88
|
+
...schema.fields.map((field, index) => ({
|
|
89
|
+
path: `fields[${index}].translations.${locale}.title`,
|
|
90
|
+
value: field.translations?.[locale]?.title
|
|
91
|
+
})),
|
|
92
|
+
...schema.fields.flatMap(
|
|
93
|
+
(field, fieldIndex) => "options" in field ? field.options.map((option, optionIndex) => ({
|
|
94
|
+
path: `fields[${fieldIndex}].options[${optionIndex}].translations.${locale}`,
|
|
95
|
+
value: option.translations?.[locale]
|
|
96
|
+
})) : []
|
|
97
|
+
),
|
|
98
|
+
...schema.pages?.flatMap(
|
|
99
|
+
(page, pageIndex) => page.title === void 0 ? [] : [{ path: `pages[${pageIndex}].translations.${locale}.title`, value: page.translations?.[locale]?.title }]
|
|
100
|
+
) ?? []
|
|
101
|
+
];
|
|
102
|
+
for (const translation of requiredTranslations) {
|
|
103
|
+
if (translation.value === void 0 || translation.value.trim().length === 0) {
|
|
104
|
+
issues.push({
|
|
105
|
+
path: translation.path,
|
|
106
|
+
code: "required_translation_missing",
|
|
107
|
+
message: `A translation for required locale ${locale} is missing.`
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function useFormBuilder({
|
|
114
|
+
schema,
|
|
115
|
+
onChange,
|
|
116
|
+
policy,
|
|
117
|
+
idFactory = defaultIdFactory
|
|
118
|
+
}) {
|
|
119
|
+
const createId = useCallback(
|
|
120
|
+
(kind, existingIds) => {
|
|
121
|
+
const id = idFactory(kind, existingIds).trim();
|
|
122
|
+
return id.length > 0 && !existingIds.has(id) ? id : void 0;
|
|
123
|
+
},
|
|
124
|
+
[idFactory]
|
|
125
|
+
);
|
|
126
|
+
const addField = useCallback(
|
|
127
|
+
(type, pageId) => {
|
|
128
|
+
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type)) {
|
|
129
|
+
return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
|
|
130
|
+
}
|
|
131
|
+
if (policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields) {
|
|
132
|
+
return { success: false, error: { type: "max_fields_exceeded", max: policy.maxFields } };
|
|
133
|
+
}
|
|
134
|
+
const fieldId = createId("field", new Set(schema.fields.map((field2) => field2.id)));
|
|
135
|
+
const needsOption = ["select", "radio", "multi-select"].includes(type);
|
|
136
|
+
const optionId = needsOption ? createId(
|
|
137
|
+
"option",
|
|
138
|
+
new Set(
|
|
139
|
+
schema.fields.flatMap((field2) => "options" in field2 ? field2.options.map((option) => option.id) : [])
|
|
140
|
+
)
|
|
141
|
+
) : void 0;
|
|
142
|
+
if (fieldId === void 0 || needsOption && optionId === void 0) {
|
|
143
|
+
return {
|
|
144
|
+
success: false,
|
|
145
|
+
error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const field = newField(type, fieldId, optionId);
|
|
149
|
+
if (field === void 0) {
|
|
150
|
+
return { success: false, error: { type: "invalid_operation", message: "Could not create the field." } };
|
|
151
|
+
}
|
|
152
|
+
const pages = schema.pages?.map((page, index) => ({
|
|
153
|
+
...page,
|
|
154
|
+
questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, fieldId] : page.questionIds
|
|
155
|
+
}));
|
|
156
|
+
if (schema.pages !== void 0 && !schema.pages.some((page) => page.id === pageId) && pageId !== void 0) {
|
|
157
|
+
return { success: false, error: { type: "invalid_operation", message: `Unknown page: ${pageId}` } };
|
|
158
|
+
}
|
|
159
|
+
onChange({ ...schema, fields: [...schema.fields, field], ...pages === void 0 ? {} : { pages } });
|
|
160
|
+
return { success: true };
|
|
161
|
+
},
|
|
162
|
+
[createId, onChange, policy, schema]
|
|
163
|
+
);
|
|
164
|
+
const removeField = useCallback(
|
|
165
|
+
(fieldId) => {
|
|
166
|
+
if (schema.fields.length <= 1 || !schema.fields.some((field) => field.id === fieldId)) return;
|
|
167
|
+
const fields = schema.fields.filter((field) => field.id !== fieldId).map(
|
|
168
|
+
(field) => field.displayCondition?.questionId === fieldId ? (({ displayCondition: _condition, ...candidate }) => candidate)(field) : field
|
|
169
|
+
);
|
|
170
|
+
const remainingPages = schema.pages?.map((page) => ({ ...page, questionIds: page.questionIds.filter((id) => id !== fieldId) })).filter((page) => page.questionIds.length > 0);
|
|
171
|
+
if (schema.pages !== void 0 && remainingPages?.length === 0) {
|
|
172
|
+
const { pages: _pages, ...singlePageSchema } = schema;
|
|
173
|
+
onChange({ ...singlePageSchema, fields });
|
|
174
|
+
} else {
|
|
175
|
+
onChange({ ...schema, fields, ...remainingPages === void 0 ? {} : { pages: remainingPages } });
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
[onChange, schema]
|
|
179
|
+
);
|
|
180
|
+
const moveField = useCallback(
|
|
181
|
+
(fieldId, targetIndex) => {
|
|
182
|
+
const fields = move(
|
|
183
|
+
schema.fields,
|
|
184
|
+
schema.fields.findIndex((field) => field.id === fieldId),
|
|
185
|
+
targetIndex
|
|
186
|
+
);
|
|
187
|
+
if (fields !== void 0) {
|
|
188
|
+
const indexById = new Map(fields.map((field, index) => [field.id, index]));
|
|
189
|
+
const safeFields = fields.map((field, index) => {
|
|
190
|
+
const sourceIndex = field.displayCondition === void 0 ? void 0 : indexById.get(field.displayCondition.questionId);
|
|
191
|
+
if (field.displayCondition === void 0 || sourceIndex !== void 0 && sourceIndex < index) return field;
|
|
192
|
+
const { displayCondition: _condition, ...withoutCondition } = field;
|
|
193
|
+
return withoutCondition;
|
|
194
|
+
});
|
|
195
|
+
onChange({ ...schema, fields: safeFields });
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
[onChange, schema]
|
|
199
|
+
);
|
|
200
|
+
const updateField = useCallback(
|
|
201
|
+
(fieldId, updater) => {
|
|
202
|
+
const current = schema.fields.find((field) => field.id === fieldId);
|
|
203
|
+
if (current === void 0) return;
|
|
204
|
+
const updated = updater(current);
|
|
205
|
+
if (updated.id !== fieldId) return;
|
|
206
|
+
if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type)) return;
|
|
207
|
+
const maxTextLength = policy?.maxTextLength;
|
|
208
|
+
if (maxTextLength !== void 0 && [updated.title, updated.description].some((text) => text !== void 0 && text.length > maxTextLength)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
|
|
212
|
+
},
|
|
213
|
+
[onChange, policy, schema]
|
|
214
|
+
);
|
|
215
|
+
const addOption = useCallback(
|
|
216
|
+
(fieldId) => {
|
|
217
|
+
const field = schema.fields.find((candidate) => candidate.id === fieldId);
|
|
218
|
+
if (field === void 0 || !("options" in field)) {
|
|
219
|
+
return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
|
|
220
|
+
}
|
|
221
|
+
if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField) {
|
|
222
|
+
return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
|
|
223
|
+
}
|
|
224
|
+
const existingIds = new Set(
|
|
225
|
+
schema.fields.flatMap(
|
|
226
|
+
(candidate) => "options" in candidate ? candidate.options.map((option2) => option2.id) : []
|
|
227
|
+
)
|
|
228
|
+
);
|
|
229
|
+
const optionId = createId("option", existingIds);
|
|
230
|
+
if (optionId === void 0) {
|
|
231
|
+
return {
|
|
232
|
+
success: false,
|
|
233
|
+
error: { type: "invalid_operation", message: "idFactory returned a duplicate or empty ID." }
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const option = { id: optionId, label: `Option ${field.options.length + 1}` };
|
|
237
|
+
onChange({
|
|
238
|
+
...schema,
|
|
239
|
+
fields: schema.fields.map(
|
|
240
|
+
(candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: [...candidate.options, option] } : candidate
|
|
241
|
+
)
|
|
242
|
+
});
|
|
243
|
+
return { success: true };
|
|
244
|
+
},
|
|
245
|
+
[createId, onChange, policy, schema]
|
|
246
|
+
);
|
|
247
|
+
const removeOption = useCallback(
|
|
248
|
+
(fieldId, optionId) => {
|
|
249
|
+
const field = schema.fields.find((candidate) => candidate.id === fieldId);
|
|
250
|
+
if (field === void 0 || !("options" in field) || field.options.length <= 1) return;
|
|
251
|
+
onChange({
|
|
252
|
+
...schema,
|
|
253
|
+
fields: schema.fields.map(
|
|
254
|
+
(candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options: candidate.options.filter((option) => option.id !== optionId) } : candidate
|
|
255
|
+
)
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
[onChange, schema]
|
|
259
|
+
);
|
|
260
|
+
const moveOption = useCallback(
|
|
261
|
+
(fieldId, optionId, targetIndex) => {
|
|
262
|
+
const field = schema.fields.find((candidate) => candidate.id === fieldId);
|
|
263
|
+
if (field === void 0 || !("options" in field)) return;
|
|
264
|
+
const options = move(
|
|
265
|
+
field.options,
|
|
266
|
+
field.options.findIndex((option) => option.id === optionId),
|
|
267
|
+
targetIndex
|
|
268
|
+
);
|
|
269
|
+
if (options === void 0) return;
|
|
270
|
+
onChange({
|
|
271
|
+
...schema,
|
|
272
|
+
fields: schema.fields.map(
|
|
273
|
+
(candidate) => candidate.id === fieldId && "options" in candidate ? { ...candidate, options } : candidate
|
|
274
|
+
)
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
[onChange, schema]
|
|
278
|
+
);
|
|
279
|
+
const addPage = useCallback(
|
|
280
|
+
(questionId) => {
|
|
281
|
+
const existingIds = new Set(schema.pages?.map((page) => page.id) ?? []);
|
|
282
|
+
const pageId = createId("page", existingIds);
|
|
283
|
+
if (pageId === void 0) return;
|
|
284
|
+
if (schema.pages === void 0) {
|
|
285
|
+
onChange({ ...schema, pages: [{ id: pageId, questionIds: schema.fields.map((field) => field.id) }] });
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const movableQuestionId = questionId ?? schema.pages.find((page) => page.questionIds.length > 1)?.questionIds.at(-1);
|
|
289
|
+
if (movableQuestionId === void 0) return;
|
|
290
|
+
const sourcePage = schema.pages.find((page) => page.questionIds.includes(movableQuestionId));
|
|
291
|
+
if (sourcePage === void 0 || sourcePage.questionIds.length <= 1) return;
|
|
292
|
+
const pages = [
|
|
293
|
+
...schema.pages.map((page) => ({
|
|
294
|
+
...page,
|
|
295
|
+
questionIds: page.questionIds.filter((id) => id !== movableQuestionId)
|
|
296
|
+
})),
|
|
297
|
+
{ id: pageId, questionIds: [movableQuestionId] }
|
|
298
|
+
];
|
|
299
|
+
onChange({ ...schema, pages });
|
|
300
|
+
},
|
|
301
|
+
[createId, onChange, schema]
|
|
302
|
+
);
|
|
303
|
+
const removePage = useCallback(
|
|
304
|
+
(pageId) => {
|
|
305
|
+
if (schema.pages === void 0) return;
|
|
306
|
+
const index = schema.pages.findIndex((page) => page.id === pageId);
|
|
307
|
+
const removed = schema.pages[index];
|
|
308
|
+
if (removed === void 0) return;
|
|
309
|
+
if (schema.pages.length === 1) {
|
|
310
|
+
const { pages: _pages, ...singlePage } = schema;
|
|
311
|
+
onChange(singlePage);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const targetIndex = index === 0 ? 1 : index - 1;
|
|
315
|
+
onChange({
|
|
316
|
+
...schema,
|
|
317
|
+
pages: schema.pages.map(
|
|
318
|
+
(page, pageIndex) => pageIndex === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
|
|
319
|
+
).filter((page) => page.id !== pageId)
|
|
320
|
+
});
|
|
321
|
+
},
|
|
322
|
+
[onChange, schema]
|
|
323
|
+
);
|
|
324
|
+
const setLocaleTranslation = useCallback(
|
|
325
|
+
(locale, target, property, text) => {
|
|
326
|
+
if (locale.trim().length === 0 || policy?.maxTextLength !== void 0 && text.length > policy.maxTextLength)
|
|
327
|
+
return;
|
|
328
|
+
const supportedLocales = [.../* @__PURE__ */ new Set([...schema.supportedLocales ?? [], locale])];
|
|
329
|
+
if (target === "form" && ["title", "description", "completionMessage"].includes(property)) {
|
|
330
|
+
onChange({
|
|
331
|
+
...schema,
|
|
332
|
+
supportedLocales,
|
|
333
|
+
translations: { ...schema.translations, [locale]: { ...schema.translations?.[locale], [property]: text } }
|
|
334
|
+
});
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const fields = schema.fields.map((field) => {
|
|
338
|
+
if (field.id === target && ["title", "description"].includes(property)) {
|
|
339
|
+
return {
|
|
340
|
+
...field,
|
|
341
|
+
translations: { ...field.translations, [locale]: { ...field.translations?.[locale], [property]: text } }
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (!("options" in field) || property !== "label") return field;
|
|
345
|
+
return {
|
|
346
|
+
...field,
|
|
347
|
+
options: field.options.map(
|
|
348
|
+
(option) => option.id === target ? { ...option, translations: { ...option.translations, [locale]: text } } : option
|
|
349
|
+
)
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
const pages = schema.pages?.map(
|
|
353
|
+
(page) => page.id === target && ["title", "description"].includes(property) ? {
|
|
354
|
+
...page,
|
|
355
|
+
translations: { ...page.translations, [locale]: { ...page.translations?.[locale], [property]: text } }
|
|
356
|
+
} : page
|
|
357
|
+
);
|
|
358
|
+
onChange({ ...schema, supportedLocales, fields, ...pages === void 0 ? {} : { pages } });
|
|
359
|
+
},
|
|
360
|
+
[onChange, policy, schema]
|
|
361
|
+
);
|
|
362
|
+
const validationIssues = useMemo(() => {
|
|
363
|
+
const result = validateFormSchema(schema);
|
|
364
|
+
const issues = result.valid ? [] : [...result.issues];
|
|
365
|
+
addPolicyIssues(schema, policy, issues);
|
|
366
|
+
return issues;
|
|
367
|
+
}, [policy, schema]);
|
|
368
|
+
return {
|
|
369
|
+
schema,
|
|
370
|
+
addField,
|
|
371
|
+
removeField,
|
|
372
|
+
moveField,
|
|
373
|
+
updateField,
|
|
374
|
+
addOption,
|
|
375
|
+
removeOption,
|
|
376
|
+
moveOption,
|
|
377
|
+
addPage,
|
|
378
|
+
removePage,
|
|
379
|
+
setLocaleTranslation,
|
|
380
|
+
validationIssues
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/builder.tsx
|
|
3
385
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
386
|
var FIELD_TYPES = [
|
|
5
387
|
"text",
|
|
@@ -37,6 +419,22 @@ var BUILDER_DEFAULTS = {
|
|
|
37
419
|
"builder.conditionTrue": "true",
|
|
38
420
|
"builder.conditionFalse": "false",
|
|
39
421
|
"builder.addQuestion": "Add question",
|
|
422
|
+
"builder.pages": "Page manager",
|
|
423
|
+
"builder.enablePages": "Enable multi-step pages",
|
|
424
|
+
"builder.addPage": "Add page",
|
|
425
|
+
"builder.newPage": "New page",
|
|
426
|
+
"builder.pageTitle": "Page title",
|
|
427
|
+
"builder.pageDescription": "Page description",
|
|
428
|
+
"builder.pageQuestion": "Question to move to the new page",
|
|
429
|
+
"builder.questionPage": "Page",
|
|
430
|
+
"builder.pageCondition": "Page display condition",
|
|
431
|
+
"builder.localization": "Localization",
|
|
432
|
+
"builder.defaultLocale": "Default locale",
|
|
433
|
+
"builder.supportedLocales": "Supported locales",
|
|
434
|
+
"builder.addLocale": "Add locale",
|
|
435
|
+
"builder.editLocale": "Edit locale",
|
|
436
|
+
"builder.autoTranslate": "Translate all text",
|
|
437
|
+
"builder.translationUnavailable": "Provide an async translation adapter to enable automatic translation.",
|
|
40
438
|
"builder.fieldType.text": "Text",
|
|
41
439
|
"builder.fieldType.textarea": "Textarea",
|
|
42
440
|
"builder.fieldType.number": "Number",
|
|
@@ -109,14 +507,21 @@ function withoutDisplayCondition(field) {
|
|
|
109
507
|
function sanitizeBuilderSchema(schema) {
|
|
110
508
|
const sanitized = sanitizeSchema(schema);
|
|
111
509
|
const indexById = new Map(sanitized.fields.map((field, index) => [field.id, index]));
|
|
510
|
+
const fields = sanitized.fields.map((field, index) => {
|
|
511
|
+
const sourceId = field.displayCondition?.questionId;
|
|
512
|
+
if (sourceId === void 0) return field;
|
|
513
|
+
const sourceIndex = indexById.get(sourceId);
|
|
514
|
+
return sourceIndex !== void 0 && sourceIndex < index ? field : withoutDisplayCondition(field);
|
|
515
|
+
});
|
|
112
516
|
return {
|
|
113
517
|
...sanitized,
|
|
114
|
-
fields
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
518
|
+
fields,
|
|
519
|
+
...sanitized.pages === void 0 ? {} : {
|
|
520
|
+
pages: sanitized.pages.map((page) => ({
|
|
521
|
+
...page,
|
|
522
|
+
questionIds: fields.filter((field) => page.questionIds.includes(field.id)).map((field) => field.id)
|
|
523
|
+
}))
|
|
524
|
+
}
|
|
120
525
|
};
|
|
121
526
|
}
|
|
122
527
|
function conditionWithValue(questionId, operator, value) {
|
|
@@ -160,15 +565,32 @@ function ConditionValueEditor({
|
|
|
160
565
|
}
|
|
161
566
|
);
|
|
162
567
|
}
|
|
163
|
-
function FormBuilder({
|
|
568
|
+
function FormBuilder({
|
|
569
|
+
schema,
|
|
570
|
+
onChange,
|
|
571
|
+
locale = "en",
|
|
572
|
+
translator,
|
|
573
|
+
translationAdapter,
|
|
574
|
+
policy,
|
|
575
|
+
idFactory
|
|
576
|
+
}) {
|
|
577
|
+
const headless = useFormBuilder({
|
|
578
|
+
schema,
|
|
579
|
+
onChange,
|
|
580
|
+
...policy === void 0 ? {} : { policy },
|
|
581
|
+
...idFactory === void 0 ? {} : { idFactory }
|
|
582
|
+
});
|
|
583
|
+
const [newPageQuestionId, setNewPageQuestionId] = useState("");
|
|
584
|
+
const [newLocale, setNewLocale] = useState("");
|
|
585
|
+
const [editingLocale, setEditingLocale] = useState("");
|
|
586
|
+
const [isTranslating, setIsTranslating] = useState(false);
|
|
587
|
+
const [translationError, setTranslationError] = useState(null);
|
|
164
588
|
const translate = (key, params = {}) => {
|
|
165
589
|
const translated = translator?.translate(key, locale, params);
|
|
166
590
|
return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
|
|
167
591
|
};
|
|
168
592
|
const emitSchema = (candidate) => onChange(sanitizeBuilderSchema(candidate));
|
|
169
|
-
const updateField =
|
|
170
|
-
emitSchema({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? update(field) : field) });
|
|
171
|
-
};
|
|
593
|
+
const updateField = headless.updateField;
|
|
172
594
|
const changeType = (fieldId, type) => {
|
|
173
595
|
emitSchema({
|
|
174
596
|
...schema,
|
|
@@ -184,10 +606,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
184
606
|
})
|
|
185
607
|
});
|
|
186
608
|
};
|
|
187
|
-
const removeField =
|
|
188
|
-
if (schema.fields.length === 1) return;
|
|
189
|
-
emitSchema({ ...schema, fields: schema.fields.filter((field) => field.id !== fieldId) });
|
|
190
|
-
};
|
|
609
|
+
const removeField = headless.removeField;
|
|
191
610
|
const moveField = (index, offset) => {
|
|
192
611
|
const target = index + offset;
|
|
193
612
|
if (target < 0 || target >= schema.fields.length) return;
|
|
@@ -199,14 +618,396 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
199
618
|
fields[target] = current;
|
|
200
619
|
emitSchema({ ...schema, fields });
|
|
201
620
|
};
|
|
202
|
-
const addField = () =>
|
|
203
|
-
|
|
621
|
+
const addField = () => headless.addField("text");
|
|
622
|
+
const enablePages = () => {
|
|
623
|
+
if (schema.pages !== void 0) return;
|
|
624
|
+
emitSchema({
|
|
625
|
+
...schema,
|
|
626
|
+
pages: [
|
|
627
|
+
{
|
|
628
|
+
id: createUniqueId("page", /* @__PURE__ */ new Set()),
|
|
629
|
+
title: translate("builder.newPage"),
|
|
630
|
+
questionIds: schema.fields.map((field) => field.id)
|
|
631
|
+
}
|
|
632
|
+
]
|
|
633
|
+
});
|
|
634
|
+
};
|
|
635
|
+
const pageForField = (fieldId) => schema.pages?.find((page) => page.questionIds.includes(fieldId));
|
|
636
|
+
const movablePageQuestions = schema.pages?.flatMap((page) => page.questionIds.length > 1 ? page.questionIds : []) ?? [];
|
|
637
|
+
const addPage = () => {
|
|
638
|
+
if (schema.pages === void 0) {
|
|
639
|
+
enablePages();
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const questionId = movablePageQuestions.includes(newPageQuestionId) ? newPageQuestionId : movablePageQuestions[0];
|
|
643
|
+
if (questionId === void 0) return;
|
|
644
|
+
const pageId = createUniqueId("page", new Set(schema.pages.map((page) => page.id)));
|
|
645
|
+
emitSchema({
|
|
646
|
+
...schema,
|
|
647
|
+
pages: [
|
|
648
|
+
...schema.pages.map((page) => ({
|
|
649
|
+
...page,
|
|
650
|
+
questionIds: page.questionIds.filter((id) => id !== questionId)
|
|
651
|
+
})),
|
|
652
|
+
{ id: pageId, title: translate("builder.newPage"), questionIds: [questionId] }
|
|
653
|
+
]
|
|
654
|
+
});
|
|
655
|
+
setNewPageQuestionId("");
|
|
656
|
+
};
|
|
657
|
+
const removePage = (pageIndex) => {
|
|
658
|
+
if (schema.pages === void 0) return;
|
|
659
|
+
const removed = schema.pages[pageIndex];
|
|
660
|
+
if (removed === void 0) return;
|
|
661
|
+
if (schema.pages.length === 1) {
|
|
662
|
+
const { pages: _pages, ...singlePage } = schema;
|
|
663
|
+
emitSchema(singlePage);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const targetIndex = pageIndex === 0 ? 1 : pageIndex - 1;
|
|
667
|
+
emitSchema({
|
|
668
|
+
...schema,
|
|
669
|
+
pages: schema.pages.map(
|
|
670
|
+
(page, index) => index === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
|
|
671
|
+
).filter((_page, index) => index !== pageIndex)
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
const movePage = (pageIndex, offset) => {
|
|
675
|
+
if (schema.pages === void 0) return;
|
|
676
|
+
const target = pageIndex + offset;
|
|
677
|
+
if (target < 0 || target >= schema.pages.length) return;
|
|
678
|
+
const pages = [...schema.pages];
|
|
679
|
+
const current = pages[pageIndex];
|
|
680
|
+
const other = pages[target];
|
|
681
|
+
if (current === void 0 || other === void 0) return;
|
|
682
|
+
pages[pageIndex] = other;
|
|
683
|
+
pages[target] = current;
|
|
684
|
+
emitSchema({ ...schema, pages });
|
|
685
|
+
};
|
|
686
|
+
const updatePage = (pageId, update) => {
|
|
687
|
+
if (schema.pages === void 0) return;
|
|
688
|
+
emitSchema({ ...schema, pages: schema.pages.map((page) => page.id === pageId ? update(page) : page) });
|
|
689
|
+
};
|
|
690
|
+
const assignFieldToPage = (fieldId, pageId) => {
|
|
691
|
+
if (schema.pages === void 0) return;
|
|
692
|
+
emitSchema({
|
|
693
|
+
...schema,
|
|
694
|
+
pages: schema.pages.map((page) => ({
|
|
695
|
+
...page,
|
|
696
|
+
questionIds: page.id === pageId ? schema.fields.filter((field) => page.questionIds.includes(field.id) || field.id === fieldId).map((field) => field.id) : page.questionIds.filter((id) => id !== fieldId)
|
|
697
|
+
})).filter((page) => page.questionIds.length > 0)
|
|
698
|
+
});
|
|
699
|
+
};
|
|
700
|
+
const addLocale = () => {
|
|
701
|
+
const normalized = newLocale.trim();
|
|
702
|
+
if (normalized.length === 0) return;
|
|
703
|
+
const supportedLocales = [
|
|
704
|
+
.../* @__PURE__ */ new Set([
|
|
705
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
706
|
+
...schema.supportedLocales ?? [],
|
|
707
|
+
normalized
|
|
708
|
+
])
|
|
709
|
+
];
|
|
710
|
+
emitSchema({ ...schema, supportedLocales });
|
|
711
|
+
setEditingLocale(normalized);
|
|
712
|
+
setNewLocale("");
|
|
713
|
+
};
|
|
714
|
+
const translateAll = async () => {
|
|
715
|
+
if (translationAdapter === void 0 || editingLocale.length === 0) return;
|
|
716
|
+
setIsTranslating(true);
|
|
717
|
+
setTranslationError(null);
|
|
718
|
+
try {
|
|
719
|
+
const populated = await populateSchemaTranslations(schema, [editingLocale], translationAdapter, {
|
|
720
|
+
overwrite: "all"
|
|
721
|
+
});
|
|
722
|
+
onChange(populated.schema);
|
|
723
|
+
} catch (cause) {
|
|
724
|
+
setTranslationError(cause instanceof Error ? cause.message : String(cause));
|
|
725
|
+
} finally {
|
|
726
|
+
setIsTranslating(false);
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
const updateFormTranslation = (key, value) => {
|
|
730
|
+
if (editingLocale.length === 0) return;
|
|
731
|
+
const current = schema.translations?.[editingLocale];
|
|
732
|
+
const next = key === "title" ? value.length === 0 ? { description: current?.description } : { ...current, title: value } : value.length === 0 ? { title: current?.title } : { ...current, description: value };
|
|
204
733
|
emitSchema({
|
|
205
734
|
...schema,
|
|
206
|
-
|
|
735
|
+
translations: {
|
|
736
|
+
...schema.translations,
|
|
737
|
+
[editingLocale]: {
|
|
738
|
+
...next.title === void 0 ? {} : { title: next.title },
|
|
739
|
+
...next.description === void 0 ? {} : { description: next.description }
|
|
740
|
+
}
|
|
741
|
+
}
|
|
207
742
|
});
|
|
208
743
|
};
|
|
209
744
|
return /* @__PURE__ */ jsxs("section", { className: "form-engine-builder", "aria-label": translate("builder.formBuilder"), children: [
|
|
745
|
+
/* @__PURE__ */ jsxs("section", { className: "form-engine-builder__pages", "aria-labelledby": "builder-pages-heading", children: [
|
|
746
|
+
/* @__PURE__ */ jsx("h2", { id: "builder-pages-heading", children: translate("builder.pages") }),
|
|
747
|
+
schema.pages === void 0 ? /* @__PURE__ */ jsx("button", { type: "button", onClick: enablePages, children: translate("builder.enablePages") }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
748
|
+
schema.pages.map((page, pageIndex) => {
|
|
749
|
+
const priorQuestionIds = new Set(schema.pages?.slice(0, pageIndex).flatMap((item) => item.questionIds));
|
|
750
|
+
const availableSources = schema.fields.filter((field) => priorQuestionIds.has(field.id));
|
|
751
|
+
const source = schema.fields.find((field) => field.id === page.displayCondition?.questionId);
|
|
752
|
+
return /* @__PURE__ */ jsxs("fieldset", { className: "form-engine-builder__page", children: [
|
|
753
|
+
/* @__PURE__ */ jsx("legend", { children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }),
|
|
754
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__toolbar", children: [
|
|
755
|
+
/* @__PURE__ */ jsx(
|
|
756
|
+
"button",
|
|
757
|
+
{
|
|
758
|
+
type: "button",
|
|
759
|
+
disabled: pageIndex === 0,
|
|
760
|
+
"aria-label": translate("builder.moveUp", { title: page.title ?? page.id }),
|
|
761
|
+
onClick: () => movePage(pageIndex, -1),
|
|
762
|
+
children: "\u2191"
|
|
763
|
+
}
|
|
764
|
+
),
|
|
765
|
+
/* @__PURE__ */ jsx(
|
|
766
|
+
"button",
|
|
767
|
+
{
|
|
768
|
+
type: "button",
|
|
769
|
+
disabled: pageIndex === (schema.pages?.length ?? 0) - 1,
|
|
770
|
+
"aria-label": translate("builder.moveDown", { title: page.title ?? page.id }),
|
|
771
|
+
onClick: () => movePage(pageIndex, 1),
|
|
772
|
+
children: "\u2193"
|
|
773
|
+
}
|
|
774
|
+
),
|
|
775
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: () => removePage(pageIndex), children: translate("builder.deleteAction") })
|
|
776
|
+
] }),
|
|
777
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
778
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
779
|
+
translate("builder.pageTitle"),
|
|
780
|
+
/* @__PURE__ */ jsx(
|
|
781
|
+
"input",
|
|
782
|
+
{
|
|
783
|
+
value: page.title ?? "",
|
|
784
|
+
onChange: (event) => {
|
|
785
|
+
const value = event.currentTarget.value;
|
|
786
|
+
updatePage(page.id, (current) => {
|
|
787
|
+
if (value.length > 0) return { ...current, title: value };
|
|
788
|
+
const { title: _title, ...withoutTitle } = current;
|
|
789
|
+
return withoutTitle;
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
)
|
|
794
|
+
] }),
|
|
795
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
796
|
+
translate("builder.pageDescription"),
|
|
797
|
+
/* @__PURE__ */ jsx(
|
|
798
|
+
"input",
|
|
799
|
+
{
|
|
800
|
+
value: page.description ?? "",
|
|
801
|
+
onChange: (event) => {
|
|
802
|
+
const value = event.currentTarget.value;
|
|
803
|
+
updatePage(page.id, (current) => {
|
|
804
|
+
if (value.length > 0) return { ...current, description: value };
|
|
805
|
+
const { description: _description, ...withoutDescription } = current;
|
|
806
|
+
return withoutDescription;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
)
|
|
811
|
+
] })
|
|
812
|
+
] }),
|
|
813
|
+
editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
|
|
814
|
+
/* @__PURE__ */ jsx("strong", { children: editingLocale }),
|
|
815
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
816
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
817
|
+
translate("builder.pageTitle"),
|
|
818
|
+
/* @__PURE__ */ jsx(
|
|
819
|
+
"input",
|
|
820
|
+
{
|
|
821
|
+
value: page.translations?.[editingLocale]?.title ?? "",
|
|
822
|
+
onChange: (event) => {
|
|
823
|
+
const value = event.currentTarget.value;
|
|
824
|
+
updatePage(page.id, (current) => ({
|
|
825
|
+
...current,
|
|
826
|
+
translations: {
|
|
827
|
+
...current.translations,
|
|
828
|
+
[editingLocale]: {
|
|
829
|
+
...value.length === 0 ? {} : { title: value },
|
|
830
|
+
...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}));
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
)
|
|
837
|
+
] }),
|
|
838
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
839
|
+
translate("builder.pageDescription"),
|
|
840
|
+
/* @__PURE__ */ jsx(
|
|
841
|
+
"input",
|
|
842
|
+
{
|
|
843
|
+
value: page.translations?.[editingLocale]?.description ?? "",
|
|
844
|
+
onChange: (event) => {
|
|
845
|
+
const value = event.currentTarget.value;
|
|
846
|
+
updatePage(page.id, (current) => ({
|
|
847
|
+
...current,
|
|
848
|
+
translations: {
|
|
849
|
+
...current.translations,
|
|
850
|
+
[editingLocale]: {
|
|
851
|
+
...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
|
|
852
|
+
...value.length === 0 ? {} : { description: value }
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
)
|
|
859
|
+
] })
|
|
860
|
+
] })
|
|
861
|
+
] }),
|
|
862
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__condition", children: [
|
|
863
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
864
|
+
translate("builder.pageCondition"),
|
|
865
|
+
/* @__PURE__ */ jsxs(
|
|
866
|
+
"select",
|
|
867
|
+
{
|
|
868
|
+
value: page.displayCondition?.questionId ?? "",
|
|
869
|
+
onChange: (event) => {
|
|
870
|
+
const selected = schema.fields.find((field) => field.id === event.currentTarget.value);
|
|
871
|
+
updatePage(page.id, (current) => {
|
|
872
|
+
if (selected === void 0) {
|
|
873
|
+
const { displayCondition: _condition, ...withoutCondition } = current;
|
|
874
|
+
return withoutCondition;
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
...current,
|
|
878
|
+
displayCondition: conditionWithValue(
|
|
879
|
+
selected.id,
|
|
880
|
+
conditionOperators(selected)[0] ?? "not_empty",
|
|
881
|
+
defaultConditionValue(selected)
|
|
882
|
+
)
|
|
883
|
+
};
|
|
884
|
+
});
|
|
885
|
+
},
|
|
886
|
+
children: [
|
|
887
|
+
/* @__PURE__ */ jsx("option", { value: "", children: translate("builder.alwaysVisible") }),
|
|
888
|
+
availableSources.map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
|
|
889
|
+
]
|
|
890
|
+
}
|
|
891
|
+
)
|
|
892
|
+
] }),
|
|
893
|
+
page.displayCondition !== void 0 && source !== void 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
894
|
+
/* @__PURE__ */ jsx(
|
|
895
|
+
"select",
|
|
896
|
+
{
|
|
897
|
+
"aria-label": translate("builder.conditionOperator"),
|
|
898
|
+
value: page.displayCondition.operator,
|
|
899
|
+
onChange: (event) => {
|
|
900
|
+
const operator = event.currentTarget.value;
|
|
901
|
+
updatePage(page.id, (current) => ({
|
|
902
|
+
...current,
|
|
903
|
+
displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
|
|
904
|
+
}));
|
|
905
|
+
},
|
|
906
|
+
children: conditionOperators(source).map((operator) => /* @__PURE__ */ jsx("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
|
|
907
|
+
}
|
|
908
|
+
),
|
|
909
|
+
/* @__PURE__ */ jsx(
|
|
910
|
+
ConditionValueEditor,
|
|
911
|
+
{
|
|
912
|
+
source,
|
|
913
|
+
condition: page.displayCondition,
|
|
914
|
+
onChange: (condition) => updatePage(page.id, (current) => ({ ...current, displayCondition: condition })),
|
|
915
|
+
translate
|
|
916
|
+
}
|
|
917
|
+
)
|
|
918
|
+
] }) : null
|
|
919
|
+
] })
|
|
920
|
+
] }, page.id);
|
|
921
|
+
}),
|
|
922
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__page-add", children: [
|
|
923
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
924
|
+
translate("builder.pageQuestion"),
|
|
925
|
+
/* @__PURE__ */ jsxs(
|
|
926
|
+
"select",
|
|
927
|
+
{
|
|
928
|
+
value: newPageQuestionId,
|
|
929
|
+
disabled: movablePageQuestions.length === 0,
|
|
930
|
+
onChange: (event) => setNewPageQuestionId(event.currentTarget.value),
|
|
931
|
+
children: [
|
|
932
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
|
|
933
|
+
schema.fields.filter((field) => movablePageQuestions.includes(field.id)).map((field) => /* @__PURE__ */ jsx("option", { value: field.id, children: field.title }, field.id))
|
|
934
|
+
]
|
|
935
|
+
}
|
|
936
|
+
)
|
|
937
|
+
] }),
|
|
938
|
+
/* @__PURE__ */ jsx("button", { type: "button", disabled: movablePageQuestions.length === 0, onClick: addPage, children: translate("builder.addPage") })
|
|
939
|
+
] })
|
|
940
|
+
] })
|
|
941
|
+
] }),
|
|
942
|
+
/* @__PURE__ */ jsxs("section", { className: "form-engine-builder__localization", "aria-labelledby": "builder-localization-heading", children: [
|
|
943
|
+
/* @__PURE__ */ jsx("h2", { id: "builder-localization-heading", children: translate("builder.localization") }),
|
|
944
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
945
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
946
|
+
translate("builder.defaultLocale"),
|
|
947
|
+
/* @__PURE__ */ jsx(
|
|
948
|
+
"input",
|
|
949
|
+
{
|
|
950
|
+
value: schema.defaultLocale ?? "",
|
|
951
|
+
onChange: (event) => {
|
|
952
|
+
const value = event.currentTarget.value.trim();
|
|
953
|
+
emitSchema(
|
|
954
|
+
value.length === 0 ? schema : {
|
|
955
|
+
...schema,
|
|
956
|
+
defaultLocale: value,
|
|
957
|
+
supportedLocales: [.../* @__PURE__ */ new Set([value, ...schema.supportedLocales ?? []])]
|
|
958
|
+
}
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
)
|
|
963
|
+
] }),
|
|
964
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
965
|
+
translate("builder.addLocale"),
|
|
966
|
+
/* @__PURE__ */ jsx("input", { value: newLocale, onChange: (event) => setNewLocale(event.currentTarget.value) })
|
|
967
|
+
] }),
|
|
968
|
+
/* @__PURE__ */ jsx("button", { type: "button", onClick: addLocale, children: translate("builder.addLocale") }),
|
|
969
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
970
|
+
translate("builder.editLocale"),
|
|
971
|
+
/* @__PURE__ */ jsxs("select", { value: editingLocale, onChange: (event) => setEditingLocale(event.currentTarget.value), children: [
|
|
972
|
+
/* @__PURE__ */ jsx("option", { value: "", children: "\u2014" }),
|
|
973
|
+
(schema.supportedLocales ?? []).filter((item) => item !== schema.defaultLocale).map((item) => /* @__PURE__ */ jsx("option", { value: item, children: item }, item))
|
|
974
|
+
] })
|
|
975
|
+
] }),
|
|
976
|
+
/* @__PURE__ */ jsx(
|
|
977
|
+
"button",
|
|
978
|
+
{
|
|
979
|
+
type: "button",
|
|
980
|
+
disabled: translationAdapter === void 0 || editingLocale.length === 0 || isTranslating,
|
|
981
|
+
onClick: () => void translateAll(),
|
|
982
|
+
children: translate("builder.autoTranslate")
|
|
983
|
+
}
|
|
984
|
+
)
|
|
985
|
+
] }),
|
|
986
|
+
translationAdapter === void 0 ? /* @__PURE__ */ jsx("p", { children: translate("builder.translationUnavailable") }) : null,
|
|
987
|
+
translationError === null ? null : /* @__PURE__ */ jsx("p", { className: "form-engine-builder__error", children: translationError }),
|
|
988
|
+
editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
989
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
990
|
+
translate("builder.questionTitle"),
|
|
991
|
+
/* @__PURE__ */ jsx(
|
|
992
|
+
"input",
|
|
993
|
+
{
|
|
994
|
+
value: schema.translations?.[editingLocale]?.title ?? "",
|
|
995
|
+
onChange: (event) => updateFormTranslation("title", event.currentTarget.value)
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
] }),
|
|
999
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1000
|
+
translate("builder.pageDescription"),
|
|
1001
|
+
/* @__PURE__ */ jsx(
|
|
1002
|
+
"input",
|
|
1003
|
+
{
|
|
1004
|
+
value: schema.translations?.[editingLocale]?.description ?? "",
|
|
1005
|
+
onChange: (event) => updateFormTranslation("description", event.currentTarget.value)
|
|
1006
|
+
}
|
|
1007
|
+
)
|
|
1008
|
+
] })
|
|
1009
|
+
] })
|
|
1010
|
+
] }),
|
|
210
1011
|
/* @__PURE__ */ jsx("div", { className: "form-engine-builder__list", children: schema.fields.map((field, index) => {
|
|
211
1012
|
const condition = field.displayCondition;
|
|
212
1013
|
const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
|
|
@@ -267,7 +1068,9 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
267
1068
|
{
|
|
268
1069
|
value: field.type,
|
|
269
1070
|
onChange: (event) => changeType(field.id, event.currentTarget.value),
|
|
270
|
-
children: FIELD_TYPES.
|
|
1071
|
+
children: FIELD_TYPES.filter(
|
|
1072
|
+
(type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
|
|
1073
|
+
).map((type) => /* @__PURE__ */ jsx("option", { value: type, children: translate(fieldTypeKey(type)) }, type))
|
|
271
1074
|
}
|
|
272
1075
|
)
|
|
273
1076
|
] }),
|
|
@@ -283,6 +1086,98 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
283
1086
|
translate("builder.required")
|
|
284
1087
|
] })
|
|
285
1088
|
] }),
|
|
1089
|
+
schema.pages === void 0 ? null : /* @__PURE__ */ jsxs("label", { children: [
|
|
1090
|
+
translate("builder.questionPage"),
|
|
1091
|
+
/* @__PURE__ */ jsx(
|
|
1092
|
+
"select",
|
|
1093
|
+
{
|
|
1094
|
+
value: pageForField(field.id)?.id ?? "",
|
|
1095
|
+
onChange: (event) => assignFieldToPage(field.id, event.currentTarget.value),
|
|
1096
|
+
children: schema.pages.map((page, pageIndex) => /* @__PURE__ */ jsx("option", { value: page.id, children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }, page.id))
|
|
1097
|
+
}
|
|
1098
|
+
)
|
|
1099
|
+
] }),
|
|
1100
|
+
editingLocale.length === 0 ? null : /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__translation-editor", children: [
|
|
1101
|
+
/* @__PURE__ */ jsx("strong", { children: editingLocale }),
|
|
1102
|
+
/* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
1103
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1104
|
+
translate("builder.questionTitle"),
|
|
1105
|
+
/* @__PURE__ */ jsx(
|
|
1106
|
+
"input",
|
|
1107
|
+
{
|
|
1108
|
+
value: field.translations?.[editingLocale]?.title ?? "",
|
|
1109
|
+
onChange: (event) => {
|
|
1110
|
+
const value = event.currentTarget.value;
|
|
1111
|
+
updateField(field.id, (current) => ({
|
|
1112
|
+
...current,
|
|
1113
|
+
translations: {
|
|
1114
|
+
...current.translations,
|
|
1115
|
+
[editingLocale]: {
|
|
1116
|
+
...value.length === 0 ? {} : { title: value },
|
|
1117
|
+
...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
}));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
)
|
|
1124
|
+
] }),
|
|
1125
|
+
/* @__PURE__ */ jsxs("label", { children: [
|
|
1126
|
+
translate("builder.pageDescription"),
|
|
1127
|
+
/* @__PURE__ */ jsx(
|
|
1128
|
+
"input",
|
|
1129
|
+
{
|
|
1130
|
+
value: field.translations?.[editingLocale]?.description ?? "",
|
|
1131
|
+
onChange: (event) => {
|
|
1132
|
+
const value = event.currentTarget.value;
|
|
1133
|
+
updateField(field.id, (current) => ({
|
|
1134
|
+
...current,
|
|
1135
|
+
translations: {
|
|
1136
|
+
...current.translations,
|
|
1137
|
+
[editingLocale]: {
|
|
1138
|
+
...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
|
|
1139
|
+
...value.length === 0 ? {} : { description: value }
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
}));
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
)
|
|
1146
|
+
] })
|
|
1147
|
+
] }),
|
|
1148
|
+
"options" in field ? field.options.map((option, optionIndex) => /* @__PURE__ */ jsxs("label", { children: [
|
|
1149
|
+
translate("builder.optionLabel", { index: optionIndex + 1 }),
|
|
1150
|
+
" (",
|
|
1151
|
+
editingLocale,
|
|
1152
|
+
")",
|
|
1153
|
+
/* @__PURE__ */ jsx(
|
|
1154
|
+
"input",
|
|
1155
|
+
{
|
|
1156
|
+
value: option.translations?.[editingLocale] ?? "",
|
|
1157
|
+
onChange: (event) => {
|
|
1158
|
+
const value = event.currentTarget.value;
|
|
1159
|
+
updateField(field.id, (current) => {
|
|
1160
|
+
if (!("options" in current)) return current;
|
|
1161
|
+
return {
|
|
1162
|
+
...current,
|
|
1163
|
+
options: current.options.map(
|
|
1164
|
+
(candidate) => candidate.id === option.id ? {
|
|
1165
|
+
...candidate,
|
|
1166
|
+
translations: Object.fromEntries([
|
|
1167
|
+
...Object.entries(candidate.translations ?? {}).filter(
|
|
1168
|
+
([localeKey]) => localeKey !== editingLocale
|
|
1169
|
+
),
|
|
1170
|
+
...value.length === 0 ? [] : [[editingLocale, value]]
|
|
1171
|
+
])
|
|
1172
|
+
} : candidate
|
|
1173
|
+
)
|
|
1174
|
+
};
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
)
|
|
1179
|
+
] }, option.id)) : null
|
|
1180
|
+
] }),
|
|
286
1181
|
field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: "form-engine-builder__grid", children: [
|
|
287
1182
|
/* @__PURE__ */ jsxs("label", { children: [
|
|
288
1183
|
translate("builder.minimum"),
|
|
@@ -348,13 +1243,7 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
348
1243
|
{
|
|
349
1244
|
type: "button",
|
|
350
1245
|
disabled: field.options.length === 1,
|
|
351
|
-
onClick: () =>
|
|
352
|
-
field.id,
|
|
353
|
-
(current) => "options" in current ? {
|
|
354
|
-
...current,
|
|
355
|
-
options: current.options.filter((_item, itemIndex) => itemIndex !== optionIndex)
|
|
356
|
-
} : current
|
|
357
|
-
),
|
|
1246
|
+
onClick: () => headless.removeOption(field.id, option.id),
|
|
358
1247
|
children: translate("builder.remove")
|
|
359
1248
|
}
|
|
360
1249
|
)
|
|
@@ -363,22 +1252,8 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
363
1252
|
"button",
|
|
364
1253
|
{
|
|
365
1254
|
type: "button",
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
(current) => "options" in current ? (() => {
|
|
369
|
-
const id = createUniqueId("opt", new Set(current.options.map((option) => option.id)));
|
|
370
|
-
return {
|
|
371
|
-
...current,
|
|
372
|
-
options: [
|
|
373
|
-
...current.options,
|
|
374
|
-
{
|
|
375
|
-
id,
|
|
376
|
-
label: translate("builder.newOptionLabel", { index: current.options.length + 1 })
|
|
377
|
-
}
|
|
378
|
-
]
|
|
379
|
-
};
|
|
380
|
-
})() : current
|
|
381
|
-
),
|
|
1255
|
+
disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
|
|
1256
|
+
onClick: () => headless.addOption(field.id),
|
|
382
1257
|
children: translate("builder.addOption")
|
|
383
1258
|
}
|
|
384
1259
|
)
|
|
@@ -440,7 +1315,16 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
440
1315
|
] })
|
|
441
1316
|
] }, field.id);
|
|
442
1317
|
}) }),
|
|
443
|
-
/* @__PURE__ */ jsx(
|
|
1318
|
+
/* @__PURE__ */ jsx(
|
|
1319
|
+
"button",
|
|
1320
|
+
{
|
|
1321
|
+
className: "form-engine-builder__add",
|
|
1322
|
+
type: "button",
|
|
1323
|
+
disabled: policy?.maxFields !== void 0 && schema.fields.length >= policy.maxFields,
|
|
1324
|
+
onClick: addField,
|
|
1325
|
+
children: translate("builder.addQuestion")
|
|
1326
|
+
}
|
|
1327
|
+
)
|
|
444
1328
|
] });
|
|
445
1329
|
}
|
|
446
1330
|
|
|
@@ -448,10 +1332,13 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
448
1332
|
import {
|
|
449
1333
|
assertValidFormSchema,
|
|
450
1334
|
calculateFieldVisibility,
|
|
1335
|
+
calculatePageVisibility,
|
|
1336
|
+
resolveLocalizedSchema,
|
|
451
1337
|
selectVisibleAnswers,
|
|
452
|
-
validateAnswers
|
|
1338
|
+
validateAnswers,
|
|
1339
|
+
validatePageAnswers
|
|
453
1340
|
} from "@form-engine-ts/core";
|
|
454
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
1341
|
+
import { createContext, useCallback as useCallback2, useContext, useEffect, useMemo as useMemo2, useState as useState2 } from "react";
|
|
455
1342
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
456
1343
|
var FormContext = createContext(null);
|
|
457
1344
|
function issuesByField(issues) {
|
|
@@ -468,26 +1355,30 @@ function FormProvider({
|
|
|
468
1355
|
onSubmit,
|
|
469
1356
|
children
|
|
470
1357
|
}) {
|
|
471
|
-
const validSchema =
|
|
1358
|
+
const validSchema = useMemo2(() => {
|
|
472
1359
|
assertValidFormSchema(schema);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const [
|
|
478
|
-
const [
|
|
479
|
-
const
|
|
1360
|
+
const localized = resolveLocalizedSchema(schema, locale);
|
|
1361
|
+
assertValidFormSchema(localized);
|
|
1362
|
+
return localized;
|
|
1363
|
+
}, [locale, schema]);
|
|
1364
|
+
const [values, setValues] = useState2(() => ({ ...initialValues }));
|
|
1365
|
+
const [errors, setErrors] = useState2({});
|
|
1366
|
+
const [submitStatus, setSubmitStatus] = useState2("idle");
|
|
1367
|
+
const [submitError, setSubmitError] = useState2(null);
|
|
1368
|
+
const [validationPageIndex, setValidationPageIndex] = useState2(null);
|
|
1369
|
+
const visibility = useMemo2(() => calculateFieldVisibility(validSchema, values), [validSchema, values]);
|
|
1370
|
+
const pageVisibility = useMemo2(() => calculatePageVisibility(validSchema, values), [validSchema, values]);
|
|
480
1371
|
useEffect(() => {
|
|
481
1372
|
const fieldIds = new Set(validSchema.fields.map((field) => field.id));
|
|
482
1373
|
setValues((current) => Object.fromEntries(Object.entries(current).filter(([fieldId]) => fieldIds.has(fieldId))));
|
|
483
1374
|
}, [validSchema]);
|
|
484
|
-
const setValue =
|
|
1375
|
+
const setValue = useCallback2(
|
|
485
1376
|
(fieldId, value) => {
|
|
486
1377
|
setValues((current) => {
|
|
487
1378
|
const next = { ...current, [fieldId]: value };
|
|
488
1379
|
setErrors((currentErrors) => {
|
|
489
1380
|
if (Object.keys(currentErrors).length === 0) return currentErrors;
|
|
490
|
-
const result = validateAnswers(validSchema, next);
|
|
1381
|
+
const result = validationPageIndex === null ? validateAnswers(validSchema, next) : validatePageAnswers(validSchema, validationPageIndex, next);
|
|
491
1382
|
return issuesByField(result.issues);
|
|
492
1383
|
});
|
|
493
1384
|
return next;
|
|
@@ -495,52 +1386,89 @@ function FormProvider({
|
|
|
495
1386
|
setSubmitStatus((current) => current === "success" || current === "error" ? "idle" : current);
|
|
496
1387
|
setSubmitError(null);
|
|
497
1388
|
},
|
|
1389
|
+
[validSchema, validationPageIndex]
|
|
1390
|
+
);
|
|
1391
|
+
const restoreValues = useCallback2(
|
|
1392
|
+
(restoredValues) => {
|
|
1393
|
+
const fieldIds = new Set(validSchema.fields.map((field) => field.id));
|
|
1394
|
+
setValues(Object.fromEntries(Object.entries(restoredValues).filter(([fieldId]) => fieldIds.has(fieldId))));
|
|
1395
|
+
setErrors({});
|
|
1396
|
+
setValidationPageIndex(null);
|
|
1397
|
+
setSubmitStatus("idle");
|
|
1398
|
+
setSubmitError(null);
|
|
1399
|
+
},
|
|
498
1400
|
[validSchema]
|
|
499
1401
|
);
|
|
500
|
-
const
|
|
1402
|
+
const validatePage = useCallback2(
|
|
1403
|
+
(pageIndex) => {
|
|
1404
|
+
const result = validatePageAnswers(validSchema, pageIndex, values);
|
|
1405
|
+
setErrors(issuesByField(result.issues));
|
|
1406
|
+
setValidationPageIndex(result.valid ? null : pageIndex);
|
|
1407
|
+
setSubmitStatus("idle");
|
|
1408
|
+
setSubmitError(null);
|
|
1409
|
+
return result;
|
|
1410
|
+
},
|
|
1411
|
+
[validSchema, values]
|
|
1412
|
+
);
|
|
1413
|
+
const reset = useCallback2(() => {
|
|
501
1414
|
setValues({ ...initialValues });
|
|
502
1415
|
setErrors({});
|
|
1416
|
+
setValidationPageIndex(null);
|
|
503
1417
|
setSubmitStatus("idle");
|
|
504
1418
|
setSubmitError(null);
|
|
505
1419
|
}, [initialValues]);
|
|
506
|
-
const submit =
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
1420
|
+
const submit = useCallback2(
|
|
1421
|
+
async (beforeSubmit) => {
|
|
1422
|
+
const validation = validateAnswers(validSchema, values);
|
|
1423
|
+
if (!validation.valid) {
|
|
1424
|
+
setErrors(issuesByField(validation.issues));
|
|
1425
|
+
setValidationPageIndex(null);
|
|
1426
|
+
setSubmitStatus("error");
|
|
1427
|
+
setSubmitError(null);
|
|
1428
|
+
return { status: "invalid", issues: validation.issues };
|
|
1429
|
+
}
|
|
1430
|
+
setErrors({});
|
|
1431
|
+
setValidationPageIndex(null);
|
|
511
1432
|
setSubmitError(null);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
1433
|
+
const visibleValues = selectVisibleAnswers(validSchema, values);
|
|
1434
|
+
try {
|
|
1435
|
+
if (beforeSubmit !== void 0 && await beforeSubmit(visibleValues) === "cancel") {
|
|
1436
|
+
setSubmitStatus("idle");
|
|
1437
|
+
return { status: "cancelled" };
|
|
1438
|
+
}
|
|
1439
|
+
setSubmitStatus("submitting");
|
|
1440
|
+
await onSubmit(visibleValues);
|
|
1441
|
+
if (resetOnSuccess) setValues({ ...initialValues });
|
|
1442
|
+
setSubmitStatus("success");
|
|
1443
|
+
return { status: "success" };
|
|
1444
|
+
} catch (cause) {
|
|
1445
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
1446
|
+
setSubmitError(error);
|
|
1447
|
+
setSubmitStatus("error");
|
|
1448
|
+
return { status: "error", error };
|
|
1449
|
+
}
|
|
1450
|
+
},
|
|
1451
|
+
[initialValues, onSubmit, resetOnSuccess, validSchema, values]
|
|
1452
|
+
);
|
|
1453
|
+
const translate = useCallback2(
|
|
529
1454
|
(key, params) => translator.translate(key, locale, params),
|
|
530
1455
|
[locale, translator]
|
|
531
1456
|
);
|
|
532
|
-
const contextValue =
|
|
1457
|
+
const contextValue = useMemo2(
|
|
533
1458
|
() => ({
|
|
534
1459
|
schema: validSchema,
|
|
535
1460
|
locale,
|
|
536
1461
|
translator,
|
|
537
1462
|
values,
|
|
538
1463
|
visibility,
|
|
1464
|
+
pageVisibility,
|
|
539
1465
|
errors,
|
|
540
1466
|
submitStatus,
|
|
541
1467
|
submitError,
|
|
542
1468
|
isSubmitting: submitStatus === "submitting",
|
|
543
1469
|
setValue,
|
|
1470
|
+
restoreValues,
|
|
1471
|
+
validatePage,
|
|
544
1472
|
reset,
|
|
545
1473
|
submit,
|
|
546
1474
|
translate
|
|
@@ -548,13 +1476,16 @@ function FormProvider({
|
|
|
548
1476
|
[
|
|
549
1477
|
errors,
|
|
550
1478
|
locale,
|
|
1479
|
+
pageVisibility,
|
|
551
1480
|
reset,
|
|
1481
|
+
restoreValues,
|
|
552
1482
|
setValue,
|
|
553
1483
|
submit,
|
|
554
1484
|
submitError,
|
|
555
1485
|
submitStatus,
|
|
556
1486
|
translate,
|
|
557
1487
|
translator,
|
|
1488
|
+
validatePage,
|
|
558
1489
|
validSchema,
|
|
559
1490
|
values,
|
|
560
1491
|
visibility
|
|
@@ -571,7 +1502,7 @@ function useField(fieldId) {
|
|
|
571
1502
|
const form = useForm();
|
|
572
1503
|
const field = form.schema.fields.find((item) => item.id === fieldId);
|
|
573
1504
|
if (field === void 0) throw new Error(`Unknown form field: ${fieldId}`);
|
|
574
|
-
const setValue =
|
|
1505
|
+
const setValue = useCallback2((value) => form.setValue(fieldId, value), [fieldId, form]);
|
|
575
1506
|
return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
|
|
576
1507
|
}
|
|
577
1508
|
|
|
@@ -579,8 +1510,15 @@ function useField(fieldId) {
|
|
|
579
1510
|
import {
|
|
580
1511
|
validateAnswers as validateAnswers2
|
|
581
1512
|
} from "@form-engine-ts/core";
|
|
582
|
-
import {
|
|
583
|
-
|
|
1513
|
+
import {
|
|
1514
|
+
Fragment as Fragment2,
|
|
1515
|
+
useEffect as useEffect2,
|
|
1516
|
+
useId,
|
|
1517
|
+
useMemo as useMemo3,
|
|
1518
|
+
useRef,
|
|
1519
|
+
useState as useState3
|
|
1520
|
+
} from "react";
|
|
1521
|
+
import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
584
1522
|
function describedBy(field, error, helpId, errorId) {
|
|
585
1523
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
586
1524
|
Boolean
|
|
@@ -594,7 +1532,7 @@ function RequiredMark({ required }) {
|
|
|
594
1532
|
] }) : null;
|
|
595
1533
|
}
|
|
596
1534
|
function FieldMessage({ props }) {
|
|
597
|
-
return /* @__PURE__ */ jsxs2(
|
|
1535
|
+
return /* @__PURE__ */ jsxs2(Fragment3, { children: [
|
|
598
1536
|
props.field.description === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.helpId, className: "fe-help", children: props.field.description }),
|
|
599
1537
|
props.error === void 0 ? null : /* @__PURE__ */ jsx3("div", { id: props.errorId, className: "fe-error", children: props.translate(props.error.messageKey, props.error.params) })
|
|
600
1538
|
] });
|
|
@@ -758,59 +1696,287 @@ function DefaultField(props) {
|
|
|
758
1696
|
/* @__PURE__ */ jsx3(FieldMessage, { props })
|
|
759
1697
|
] });
|
|
760
1698
|
}
|
|
761
|
-
function
|
|
1699
|
+
function isRecord(value) {
|
|
1700
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1701
|
+
}
|
|
1702
|
+
function isFormValue(value) {
|
|
1703
|
+
return value === void 0 || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) || Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1704
|
+
}
|
|
1705
|
+
function parseDraft(serialized) {
|
|
1706
|
+
try {
|
|
1707
|
+
const value = JSON.parse(serialized);
|
|
1708
|
+
if (!isRecord(value) || typeof value.formId !== "string" || typeof value.formVersion !== "number" || !Number.isInteger(value.formVersion) || typeof value.savedAt !== "string" || !isRecord(value.values) || !Object.values(value.values).every(isFormValue)) {
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
formId: value.formId,
|
|
1713
|
+
formVersion: value.formVersion,
|
|
1714
|
+
savedAt: value.savedAt,
|
|
1715
|
+
values: Object.fromEntries(
|
|
1716
|
+
Object.entries(value.values).filter((entry) => isFormValue(entry[1]))
|
|
1717
|
+
)
|
|
1718
|
+
};
|
|
1719
|
+
} catch {
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
function ContextFormRenderer({
|
|
762
1724
|
components = {},
|
|
763
1725
|
className = "",
|
|
764
1726
|
successMessageKey,
|
|
765
|
-
errorMessageKey
|
|
1727
|
+
errorMessageKey,
|
|
1728
|
+
autoSaveKey,
|
|
1729
|
+
beforeSubmit,
|
|
1730
|
+
onDraftSave,
|
|
1731
|
+
slots = {}
|
|
766
1732
|
}) {
|
|
767
1733
|
const form = useForm();
|
|
768
1734
|
const prefix = useId().replace(/:/g, "");
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
1735
|
+
const formRef = useRef(null);
|
|
1736
|
+
const loadedDraftKey = useRef(null);
|
|
1737
|
+
const [draftRestored, setDraftRestored] = useState3(false);
|
|
1738
|
+
const [currentPageIndex, setCurrentPageIndex] = useState3(0);
|
|
1739
|
+
const [focusFieldId, setFocusFieldId] = useState3(null);
|
|
1740
|
+
const pages = form.schema.pages;
|
|
1741
|
+
const visiblePageIndexes = useMemo3(
|
|
1742
|
+
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
1743
|
+
[form.pageVisibility, pages]
|
|
1744
|
+
);
|
|
1745
|
+
const activePage = pages?.[currentPageIndex];
|
|
1746
|
+
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
1747
|
+
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
1748
|
+
useEffect2(() => {
|
|
1749
|
+
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
1750
|
+
setCurrentPageIndex(0);
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
1754
|
+
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
1755
|
+
useEffect2(() => {
|
|
1756
|
+
if (focusFieldId === null) return;
|
|
1757
|
+
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
1758
|
+
(element) => element.dataset.fieldId === focusFieldId
|
|
1759
|
+
);
|
|
1760
|
+
const control = fieldContainer?.querySelector("input, select, textarea");
|
|
1761
|
+
if (control !== void 0 && control !== null) {
|
|
1762
|
+
control.focus();
|
|
1763
|
+
setFocusFieldId(null);
|
|
1764
|
+
}
|
|
1765
|
+
}, [focusFieldId]);
|
|
1766
|
+
useEffect2(() => {
|
|
1767
|
+
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1768
|
+
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
1769
|
+
if (loadedDraftKey.current === loadIdentity) return;
|
|
1770
|
+
loadedDraftKey.current = loadIdentity;
|
|
1771
|
+
const serialized = globalThis.localStorage.getItem(autoSaveKey);
|
|
1772
|
+
if (serialized === null) return;
|
|
1773
|
+
const draft = parseDraft(serialized);
|
|
1774
|
+
if (draft === null || draft.formId !== form.schema.id || draft.formVersion !== form.schema.version) return;
|
|
1775
|
+
form.restoreValues(draft.values);
|
|
1776
|
+
setDraftRestored(true);
|
|
1777
|
+
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
1778
|
+
useEffect2(() => {
|
|
1779
|
+
if (form.submitStatus === "success") return;
|
|
1780
|
+
const timeout = globalThis.setTimeout(() => {
|
|
1781
|
+
onDraftSave?.(form.values);
|
|
1782
|
+
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1783
|
+
const draft = {
|
|
1784
|
+
formId: form.schema.id,
|
|
1785
|
+
formVersion: form.schema.version,
|
|
1786
|
+
values: form.values,
|
|
1787
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1788
|
+
};
|
|
1789
|
+
globalThis.localStorage.setItem(autoSaveKey, JSON.stringify(draft));
|
|
1790
|
+
}, 500);
|
|
1791
|
+
return () => globalThis.clearTimeout(timeout);
|
|
1792
|
+
}, [autoSaveKey, form.schema.id, form.schema.version, form.submitStatus, form.values, onDraftSave]);
|
|
1793
|
+
const focusFirstIssue = (fieldId) => {
|
|
1794
|
+
if (fieldId !== void 0) setFocusFieldId(fieldId);
|
|
1795
|
+
};
|
|
1796
|
+
const handleNext = () => {
|
|
1797
|
+
const result = form.validatePage(currentPageIndex);
|
|
1798
|
+
if (!result.valid) {
|
|
1799
|
+
focusFirstIssue(result.issues[0]?.fieldId);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
const nextPageIndex = visiblePageIndexes[activeVisibleIndex + 1];
|
|
1803
|
+
if (nextPageIndex !== void 0) setCurrentPageIndex(nextPageIndex);
|
|
1804
|
+
};
|
|
1805
|
+
const submitValues = async () => {
|
|
772
1806
|
const validation = validateAnswers2(form.schema, form.values);
|
|
773
1807
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
774
|
-
const
|
|
775
|
-
if (
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1808
|
+
const result = await form.submit(beforeSubmit);
|
|
1809
|
+
if (result.status === "invalid") {
|
|
1810
|
+
const invalidPageIndex = pages?.findIndex(
|
|
1811
|
+
(page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
|
|
1812
|
+
);
|
|
1813
|
+
if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
|
|
1814
|
+
focusFirstIssue(firstInvalidFieldId);
|
|
1815
|
+
return result;
|
|
1816
|
+
}
|
|
1817
|
+
if (result.status !== "success") return result;
|
|
1818
|
+
if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
|
|
1819
|
+
globalThis.localStorage.removeItem(autoSaveKey);
|
|
1820
|
+
setDraftRestored(false);
|
|
782
1821
|
}
|
|
1822
|
+
setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
1823
|
+
return result;
|
|
1824
|
+
};
|
|
1825
|
+
const handleSubmit = (event) => {
|
|
1826
|
+
event.preventDefault();
|
|
1827
|
+
void submitValues();
|
|
783
1828
|
};
|
|
784
|
-
|
|
785
|
-
|
|
1829
|
+
const validationIssues = Object.values(form.errors).filter((issue) => issue !== void 0);
|
|
1830
|
+
const canPrev = pages !== void 0 && activeVisibleIndex > 0;
|
|
1831
|
+
const canNext = pages !== void 0 && activeVisibleIndex < visiblePageIndexes.length - 1;
|
|
1832
|
+
const renderSubmitButton = () => slots.renderSubmitButton?.({ isSubmitting: form.isSubmitting, onSubmit: () => void submitValues() }) ?? /* @__PURE__ */ jsx3("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") });
|
|
1833
|
+
return /* @__PURE__ */ jsxs2("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
1834
|
+
slots.renderHeader?.({
|
|
1835
|
+
title: form.schema.title,
|
|
1836
|
+
...form.schema.description === void 0 ? {} : { description: form.schema.description }
|
|
1837
|
+
}) ?? /* @__PURE__ */ jsxs2("header", { className: "fe-header", children: [
|
|
786
1838
|
/* @__PURE__ */ jsx3("h1", { children: form.schema.title }),
|
|
787
|
-
form.schema.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { children: form.schema.description })
|
|
1839
|
+
form.schema.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { children: form.schema.description }),
|
|
1840
|
+
pages === void 0 ? null : /* @__PURE__ */ jsxs2("div", { className: "fe-progress", children: [
|
|
1841
|
+
/* @__PURE__ */ jsx3(
|
|
1842
|
+
"div",
|
|
1843
|
+
{
|
|
1844
|
+
className: "form-progress-bar",
|
|
1845
|
+
role: "progressbar",
|
|
1846
|
+
"aria-valuemin": 1,
|
|
1847
|
+
"aria-valuemax": visiblePageIndexes.length,
|
|
1848
|
+
"aria-valuenow": activeVisibleIndex + 1,
|
|
1849
|
+
children: /* @__PURE__ */ jsx3(
|
|
1850
|
+
"div",
|
|
1851
|
+
{
|
|
1852
|
+
className: "form-progress-fill",
|
|
1853
|
+
style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
|
|
1854
|
+
}
|
|
1855
|
+
)
|
|
1856
|
+
}
|
|
1857
|
+
),
|
|
1858
|
+
/* @__PURE__ */ jsx3("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
|
|
1859
|
+
] }),
|
|
1860
|
+
draftRestored ? /* @__PURE__ */ jsx3("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
|
|
788
1861
|
] }),
|
|
789
|
-
/* @__PURE__ */ jsx3("
|
|
1862
|
+
activePage?.title === void 0 ? null : /* @__PURE__ */ jsx3("h2", { className: "fe-page-title", children: activePage.title }),
|
|
1863
|
+
activePage?.description === void 0 ? null : /* @__PURE__ */ jsx3("p", { className: "fe-page-description", children: activePage.description }),
|
|
1864
|
+
/* @__PURE__ */ jsx3("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
|
|
1865
|
+
const error = form.errors[field.id];
|
|
790
1866
|
const props = {
|
|
791
1867
|
field,
|
|
792
1868
|
value: form.values[field.id],
|
|
793
|
-
error
|
|
1869
|
+
error,
|
|
794
1870
|
setValue: (value) => form.setValue(field.id, value),
|
|
795
1871
|
translate: form.translate,
|
|
796
1872
|
inputId: `${prefix}-${field.id}`,
|
|
797
1873
|
errorId: `${prefix}-${field.id}-error`,
|
|
798
1874
|
helpId: `${prefix}-${field.id}-help`
|
|
799
1875
|
};
|
|
1876
|
+
if (slots.renderField !== void 0) {
|
|
1877
|
+
return /* @__PURE__ */ jsx3(Fragment2, { children: slots.renderField({
|
|
1878
|
+
question: field,
|
|
1879
|
+
value: form.values[field.id],
|
|
1880
|
+
onChange: (value) => {
|
|
1881
|
+
if (isFormValue(value)) form.setValue(field.id, value);
|
|
1882
|
+
},
|
|
1883
|
+
...error === void 0 ? {} : { error }
|
|
1884
|
+
}) }, field.id);
|
|
1885
|
+
}
|
|
800
1886
|
const Component = components[field.type];
|
|
801
1887
|
return Component === void 0 ? /* @__PURE__ */ jsx3(DefaultField, { ...props }, field.id) : /* @__PURE__ */ jsx3(Component, { ...props }, field.id);
|
|
802
1888
|
}) }),
|
|
803
|
-
/* @__PURE__ */
|
|
1889
|
+
validationIssues.length === 0 ? null : slots.renderValidationSummary?.({ issues: validationIssues }) ?? /* @__PURE__ */ jsxs2("div", { className: "fe-validation-summary", role: "alert", children: [
|
|
1890
|
+
validationIssues.length,
|
|
1891
|
+
" validation error",
|
|
1892
|
+
validationIssues.length === 1 ? "" : "s",
|
|
1893
|
+
"."
|
|
1894
|
+
] }),
|
|
1895
|
+
pages === void 0 ? /* @__PURE__ */ jsxs2(Fragment3, { children: [
|
|
1896
|
+
slots.renderNavigation?.({
|
|
1897
|
+
currentPage: 0,
|
|
1898
|
+
totalPages: 1,
|
|
1899
|
+
canPrev: false,
|
|
1900
|
+
canNext: false,
|
|
1901
|
+
onPrev: () => void 0,
|
|
1902
|
+
onNext: () => void 0
|
|
1903
|
+
}),
|
|
1904
|
+
renderSubmitButton()
|
|
1905
|
+
] }) : /* @__PURE__ */ jsxs2("div", { className: "form-step-navigation", children: [
|
|
1906
|
+
slots.renderNavigation?.({
|
|
1907
|
+
currentPage: activeVisibleIndex,
|
|
1908
|
+
totalPages: visiblePageIndexes.length,
|
|
1909
|
+
canPrev,
|
|
1910
|
+
canNext,
|
|
1911
|
+
onPrev: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
|
|
1912
|
+
onNext: handleNext
|
|
1913
|
+
}) ?? /* @__PURE__ */ jsxs2(Fragment3, { children: [
|
|
1914
|
+
canPrev ? /* @__PURE__ */ jsx3(
|
|
1915
|
+
"button",
|
|
1916
|
+
{
|
|
1917
|
+
className: "btn-prev",
|
|
1918
|
+
type: "button",
|
|
1919
|
+
onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
|
|
1920
|
+
children: form.translate("form.back")
|
|
1921
|
+
}
|
|
1922
|
+
) : null,
|
|
1923
|
+
canNext ? /* @__PURE__ */ jsx3("button", { className: "btn-next", type: "button", onClick: handleNext, children: form.translate("form.next") }) : null
|
|
1924
|
+
] }),
|
|
1925
|
+
canNext ? null : renderSubmitButton()
|
|
1926
|
+
] }),
|
|
804
1927
|
/* @__PURE__ */ jsxs2("div", { className: "fe-status", "aria-live": "polite", children: [
|
|
805
|
-
form.submitStatus === "success"
|
|
1928
|
+
form.submitStatus === "success" ? slots.renderCompletion?.({
|
|
1929
|
+
message: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey))
|
|
1930
|
+
}) ?? /* @__PURE__ */ jsx3("div", { role: "status", children: form.schema.completionMessage ?? (successMessageKey === void 0 ? "Submitted." : form.translate(successMessageKey)) }) : null,
|
|
806
1931
|
form.submitStatus === "error" && form.submitError !== null && errorMessageKey !== void 0 ? /* @__PURE__ */ jsx3("div", { role: "alert", children: form.translate(errorMessageKey) }) : null
|
|
807
1932
|
] })
|
|
808
1933
|
] });
|
|
809
1934
|
}
|
|
1935
|
+
var RENDERER_MESSAGES = {
|
|
1936
|
+
"form.submit": "Submit",
|
|
1937
|
+
"form.back": "Back",
|
|
1938
|
+
"form.next": "Next",
|
|
1939
|
+
"form.step": "Step {{current}} / {{total}}",
|
|
1940
|
+
"form.draftRestored": "Draft restored",
|
|
1941
|
+
"validation.required": "This field is required."
|
|
1942
|
+
};
|
|
1943
|
+
var defaultRendererTranslator = {
|
|
1944
|
+
translate(key, _locale, params = {}) {
|
|
1945
|
+
return (RENDERER_MESSAGES[key] ?? key).replace(
|
|
1946
|
+
/\{\{(\w+)\}\}/g,
|
|
1947
|
+
(token, name) => Object.hasOwn(params, name) ? String(params[name]) : token
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
};
|
|
1951
|
+
function FormRenderer(props) {
|
|
1952
|
+
if (!("schema" in props)) return /* @__PURE__ */ jsx3(ContextFormRenderer, { ...props });
|
|
1953
|
+
const {
|
|
1954
|
+
schema,
|
|
1955
|
+
locale = schema.defaultLocale ?? "en",
|
|
1956
|
+
translator = defaultRendererTranslator,
|
|
1957
|
+
initialValues,
|
|
1958
|
+
resetOnSuccess,
|
|
1959
|
+
onSubmit,
|
|
1960
|
+
...rendererProps
|
|
1961
|
+
} = props;
|
|
1962
|
+
return /* @__PURE__ */ jsx3(
|
|
1963
|
+
FormProvider,
|
|
1964
|
+
{
|
|
1965
|
+
schema,
|
|
1966
|
+
locale,
|
|
1967
|
+
translator,
|
|
1968
|
+
onSubmit,
|
|
1969
|
+
...initialValues === void 0 ? {} : { initialValues },
|
|
1970
|
+
...resetOnSuccess === void 0 ? {} : { resetOnSuccess },
|
|
1971
|
+
children: /* @__PURE__ */ jsx3(ContextFormRenderer, { ...rendererProps })
|
|
1972
|
+
}
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
810
1975
|
export {
|
|
811
1976
|
FormBuilder,
|
|
812
1977
|
FormProvider,
|
|
813
1978
|
FormRenderer,
|
|
814
1979
|
useField,
|
|
815
|
-
useForm
|
|
1980
|
+
useForm,
|
|
1981
|
+
useFormBuilder
|
|
816
1982
|
};
|