@form-engine-ts/react 1.0.0 → 1.1.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 +12 -0
- package/dist/index.cjs +723 -42
- package/dist/index.d.cts +8 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +719 -32
- package/dist/styles.css +77 -1
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
30
|
|
|
31
31
|
// src/builder.tsx
|
|
32
32
|
var import_core = require("@form-engine-ts/core");
|
|
33
|
+
var import_react = require("react");
|
|
33
34
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
34
35
|
var FIELD_TYPES = [
|
|
35
36
|
"text",
|
|
@@ -67,6 +68,22 @@ var BUILDER_DEFAULTS = {
|
|
|
67
68
|
"builder.conditionTrue": "true",
|
|
68
69
|
"builder.conditionFalse": "false",
|
|
69
70
|
"builder.addQuestion": "Add question",
|
|
71
|
+
"builder.pages": "Page manager",
|
|
72
|
+
"builder.enablePages": "Enable multi-step pages",
|
|
73
|
+
"builder.addPage": "Add page",
|
|
74
|
+
"builder.newPage": "New page",
|
|
75
|
+
"builder.pageTitle": "Page title",
|
|
76
|
+
"builder.pageDescription": "Page description",
|
|
77
|
+
"builder.pageQuestion": "Question to move to the new page",
|
|
78
|
+
"builder.questionPage": "Page",
|
|
79
|
+
"builder.pageCondition": "Page display condition",
|
|
80
|
+
"builder.localization": "Localization",
|
|
81
|
+
"builder.defaultLocale": "Default locale",
|
|
82
|
+
"builder.supportedLocales": "Supported locales",
|
|
83
|
+
"builder.addLocale": "Add locale",
|
|
84
|
+
"builder.editLocale": "Edit locale",
|
|
85
|
+
"builder.autoTranslate": "Translate all text",
|
|
86
|
+
"builder.translationUnavailable": "Provide an async translation adapter to enable automatic translation.",
|
|
70
87
|
"builder.fieldType.text": "Text",
|
|
71
88
|
"builder.fieldType.textarea": "Textarea",
|
|
72
89
|
"builder.fieldType.number": "Number",
|
|
@@ -139,14 +156,21 @@ function withoutDisplayCondition(field) {
|
|
|
139
156
|
function sanitizeBuilderSchema(schema) {
|
|
140
157
|
const sanitized = (0, import_core.sanitizeSchema)(schema);
|
|
141
158
|
const indexById = new Map(sanitized.fields.map((field, index) => [field.id, index]));
|
|
159
|
+
const fields = sanitized.fields.map((field, index) => {
|
|
160
|
+
const sourceId = field.displayCondition?.questionId;
|
|
161
|
+
if (sourceId === void 0) return field;
|
|
162
|
+
const sourceIndex = indexById.get(sourceId);
|
|
163
|
+
return sourceIndex !== void 0 && sourceIndex < index ? field : withoutDisplayCondition(field);
|
|
164
|
+
});
|
|
142
165
|
return {
|
|
143
166
|
...sanitized,
|
|
144
|
-
fields
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
167
|
+
fields,
|
|
168
|
+
...sanitized.pages === void 0 ? {} : {
|
|
169
|
+
pages: sanitized.pages.map((page) => ({
|
|
170
|
+
...page,
|
|
171
|
+
questionIds: fields.filter((field) => page.questionIds.includes(field.id)).map((field) => field.id)
|
|
172
|
+
}))
|
|
173
|
+
}
|
|
150
174
|
};
|
|
151
175
|
}
|
|
152
176
|
function conditionWithValue(questionId, operator, value) {
|
|
@@ -190,7 +214,12 @@ function ConditionValueEditor({
|
|
|
190
214
|
}
|
|
191
215
|
);
|
|
192
216
|
}
|
|
193
|
-
function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
217
|
+
function FormBuilder({ schema, onChange, locale = "en", translator, translationAdapter }) {
|
|
218
|
+
const [newPageQuestionId, setNewPageQuestionId] = (0, import_react.useState)("");
|
|
219
|
+
const [newLocale, setNewLocale] = (0, import_react.useState)("");
|
|
220
|
+
const [editingLocale, setEditingLocale] = (0, import_react.useState)("");
|
|
221
|
+
const [isTranslating, setIsTranslating] = (0, import_react.useState)(false);
|
|
222
|
+
const [translationError, setTranslationError] = (0, import_react.useState)(null);
|
|
194
223
|
const translate = (key, params = {}) => {
|
|
195
224
|
const translated = translator?.translate(key, locale, params);
|
|
196
225
|
return translated === void 0 ? interpolate(BUILDER_DEFAULTS[key] ?? key, params) : translated;
|
|
@@ -231,12 +260,405 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
231
260
|
};
|
|
232
261
|
const addField = () => {
|
|
233
262
|
const id = createUniqueId("q", new Set(schema.fields.map((field) => field.id)));
|
|
234
|
-
|
|
263
|
+
const nextSchema = {
|
|
235
264
|
...schema,
|
|
236
265
|
fields: [...schema.fields, { id, type: "text", title: translate("builder.newQuestionTitle"), required: false }]
|
|
266
|
+
};
|
|
267
|
+
emitSchema(
|
|
268
|
+
schema.pages === void 0 ? nextSchema : {
|
|
269
|
+
...nextSchema,
|
|
270
|
+
pages: schema.pages.map(
|
|
271
|
+
(page, index) => index === (schema.pages?.length ?? 0) - 1 ? { ...page, questionIds: [...page.questionIds, id] } : page
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
const enablePages = () => {
|
|
277
|
+
if (schema.pages !== void 0) return;
|
|
278
|
+
emitSchema({
|
|
279
|
+
...schema,
|
|
280
|
+
pages: [
|
|
281
|
+
{
|
|
282
|
+
id: createUniqueId("page", /* @__PURE__ */ new Set()),
|
|
283
|
+
title: translate("builder.newPage"),
|
|
284
|
+
questionIds: schema.fields.map((field) => field.id)
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
const pageForField = (fieldId) => schema.pages?.find((page) => page.questionIds.includes(fieldId));
|
|
290
|
+
const movablePageQuestions = schema.pages?.flatMap((page) => page.questionIds.length > 1 ? page.questionIds : []) ?? [];
|
|
291
|
+
const addPage = () => {
|
|
292
|
+
if (schema.pages === void 0) {
|
|
293
|
+
enablePages();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const questionId = movablePageQuestions.includes(newPageQuestionId) ? newPageQuestionId : movablePageQuestions[0];
|
|
297
|
+
if (questionId === void 0) return;
|
|
298
|
+
const pageId = createUniqueId("page", new Set(schema.pages.map((page) => page.id)));
|
|
299
|
+
emitSchema({
|
|
300
|
+
...schema,
|
|
301
|
+
pages: [
|
|
302
|
+
...schema.pages.map((page) => ({
|
|
303
|
+
...page,
|
|
304
|
+
questionIds: page.questionIds.filter((id) => id !== questionId)
|
|
305
|
+
})),
|
|
306
|
+
{ id: pageId, title: translate("builder.newPage"), questionIds: [questionId] }
|
|
307
|
+
]
|
|
308
|
+
});
|
|
309
|
+
setNewPageQuestionId("");
|
|
310
|
+
};
|
|
311
|
+
const removePage = (pageIndex) => {
|
|
312
|
+
if (schema.pages === void 0) return;
|
|
313
|
+
const removed = schema.pages[pageIndex];
|
|
314
|
+
if (removed === void 0) return;
|
|
315
|
+
if (schema.pages.length === 1) {
|
|
316
|
+
const { pages: _pages, ...singlePage } = schema;
|
|
317
|
+
emitSchema(singlePage);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const targetIndex = pageIndex === 0 ? 1 : pageIndex - 1;
|
|
321
|
+
emitSchema({
|
|
322
|
+
...schema,
|
|
323
|
+
pages: schema.pages.map(
|
|
324
|
+
(page, index) => index === targetIndex ? { ...page, questionIds: [...page.questionIds, ...removed.questionIds] } : page
|
|
325
|
+
).filter((_page, index) => index !== pageIndex)
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
const movePage = (pageIndex, offset) => {
|
|
329
|
+
if (schema.pages === void 0) return;
|
|
330
|
+
const target = pageIndex + offset;
|
|
331
|
+
if (target < 0 || target >= schema.pages.length) return;
|
|
332
|
+
const pages = [...schema.pages];
|
|
333
|
+
const current = pages[pageIndex];
|
|
334
|
+
const other = pages[target];
|
|
335
|
+
if (current === void 0 || other === void 0) return;
|
|
336
|
+
pages[pageIndex] = other;
|
|
337
|
+
pages[target] = current;
|
|
338
|
+
emitSchema({ ...schema, pages });
|
|
339
|
+
};
|
|
340
|
+
const updatePage = (pageId, update) => {
|
|
341
|
+
if (schema.pages === void 0) return;
|
|
342
|
+
emitSchema({ ...schema, pages: schema.pages.map((page) => page.id === pageId ? update(page) : page) });
|
|
343
|
+
};
|
|
344
|
+
const assignFieldToPage = (fieldId, pageId) => {
|
|
345
|
+
if (schema.pages === void 0) return;
|
|
346
|
+
emitSchema({
|
|
347
|
+
...schema,
|
|
348
|
+
pages: schema.pages.map((page) => ({
|
|
349
|
+
...page,
|
|
350
|
+
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)
|
|
351
|
+
})).filter((page) => page.questionIds.length > 0)
|
|
352
|
+
});
|
|
353
|
+
};
|
|
354
|
+
const addLocale = () => {
|
|
355
|
+
const normalized = newLocale.trim();
|
|
356
|
+
if (normalized.length === 0) return;
|
|
357
|
+
const supportedLocales = [
|
|
358
|
+
.../* @__PURE__ */ new Set([
|
|
359
|
+
...schema.defaultLocale === void 0 ? [] : [schema.defaultLocale],
|
|
360
|
+
...schema.supportedLocales ?? [],
|
|
361
|
+
normalized
|
|
362
|
+
])
|
|
363
|
+
];
|
|
364
|
+
emitSchema({ ...schema, supportedLocales });
|
|
365
|
+
setEditingLocale(normalized);
|
|
366
|
+
setNewLocale("");
|
|
367
|
+
};
|
|
368
|
+
const translateAll = async () => {
|
|
369
|
+
if (translationAdapter === void 0 || editingLocale.length === 0) return;
|
|
370
|
+
setIsTranslating(true);
|
|
371
|
+
setTranslationError(null);
|
|
372
|
+
try {
|
|
373
|
+
onChange(await (0, import_core.populateSchemaTranslations)(schema, [editingLocale], translationAdapter));
|
|
374
|
+
} catch (cause) {
|
|
375
|
+
setTranslationError(cause instanceof Error ? cause.message : String(cause));
|
|
376
|
+
} finally {
|
|
377
|
+
setIsTranslating(false);
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
const updateFormTranslation = (key, value) => {
|
|
381
|
+
if (editingLocale.length === 0) return;
|
|
382
|
+
const current = schema.translations?.[editingLocale];
|
|
383
|
+
const next = key === "title" ? value.length === 0 ? { description: current?.description } : { ...current, title: value } : value.length === 0 ? { title: current?.title } : { ...current, description: value };
|
|
384
|
+
emitSchema({
|
|
385
|
+
...schema,
|
|
386
|
+
translations: {
|
|
387
|
+
...schema.translations,
|
|
388
|
+
[editingLocale]: {
|
|
389
|
+
...next.title === void 0 ? {} : { title: next.title },
|
|
390
|
+
...next.description === void 0 ? {} : { description: next.description }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
237
393
|
});
|
|
238
394
|
};
|
|
239
395
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "form-engine-builder", "aria-label": translate("builder.formBuilder"), children: [
|
|
396
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "form-engine-builder__pages", "aria-labelledby": "builder-pages-heading", children: [
|
|
397
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { id: "builder-pages-heading", children: translate("builder.pages") }),
|
|
398
|
+
schema.pages === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: enablePages, children: translate("builder.enablePages") }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
399
|
+
schema.pages.map((page, pageIndex) => {
|
|
400
|
+
const priorQuestionIds = new Set(schema.pages?.slice(0, pageIndex).flatMap((item) => item.questionIds));
|
|
401
|
+
const availableSources = schema.fields.filter((field) => priorQuestionIds.has(field.id));
|
|
402
|
+
const source = schema.fields.find((field) => field.id === page.displayCondition?.questionId);
|
|
403
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("fieldset", { className: "form-engine-builder__page", children: [
|
|
404
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("legend", { children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }),
|
|
405
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__toolbar", children: [
|
|
406
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
407
|
+
"button",
|
|
408
|
+
{
|
|
409
|
+
type: "button",
|
|
410
|
+
disabled: pageIndex === 0,
|
|
411
|
+
"aria-label": translate("builder.moveUp", { title: page.title ?? page.id }),
|
|
412
|
+
onClick: () => movePage(pageIndex, -1),
|
|
413
|
+
children: "\u2191"
|
|
414
|
+
}
|
|
415
|
+
),
|
|
416
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
417
|
+
"button",
|
|
418
|
+
{
|
|
419
|
+
type: "button",
|
|
420
|
+
disabled: pageIndex === (schema.pages?.length ?? 0) - 1,
|
|
421
|
+
"aria-label": translate("builder.moveDown", { title: page.title ?? page.id }),
|
|
422
|
+
onClick: () => movePage(pageIndex, 1),
|
|
423
|
+
children: "\u2193"
|
|
424
|
+
}
|
|
425
|
+
),
|
|
426
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: () => removePage(pageIndex), children: translate("builder.deleteAction") })
|
|
427
|
+
] }),
|
|
428
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
429
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
430
|
+
translate("builder.pageTitle"),
|
|
431
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
432
|
+
"input",
|
|
433
|
+
{
|
|
434
|
+
value: page.title ?? "",
|
|
435
|
+
onChange: (event) => {
|
|
436
|
+
const value = event.currentTarget.value;
|
|
437
|
+
updatePage(page.id, (current) => {
|
|
438
|
+
if (value.length > 0) return { ...current, title: value };
|
|
439
|
+
const { title: _title, ...withoutTitle } = current;
|
|
440
|
+
return withoutTitle;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
)
|
|
445
|
+
] }),
|
|
446
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
447
|
+
translate("builder.pageDescription"),
|
|
448
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
449
|
+
"input",
|
|
450
|
+
{
|
|
451
|
+
value: page.description ?? "",
|
|
452
|
+
onChange: (event) => {
|
|
453
|
+
const value = event.currentTarget.value;
|
|
454
|
+
updatePage(page.id, (current) => {
|
|
455
|
+
if (value.length > 0) return { ...current, description: value };
|
|
456
|
+
const { description: _description, ...withoutDescription } = current;
|
|
457
|
+
return withoutDescription;
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
] })
|
|
463
|
+
] }),
|
|
464
|
+
editingLocale.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__translation-editor", children: [
|
|
465
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: editingLocale }),
|
|
466
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
467
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
468
|
+
translate("builder.pageTitle"),
|
|
469
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
470
|
+
"input",
|
|
471
|
+
{
|
|
472
|
+
value: page.translations?.[editingLocale]?.title ?? "",
|
|
473
|
+
onChange: (event) => {
|
|
474
|
+
const value = event.currentTarget.value;
|
|
475
|
+
updatePage(page.id, (current) => ({
|
|
476
|
+
...current,
|
|
477
|
+
translations: {
|
|
478
|
+
...current.translations,
|
|
479
|
+
[editingLocale]: {
|
|
480
|
+
...value.length === 0 ? {} : { title: value },
|
|
481
|
+
...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
] }),
|
|
489
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
490
|
+
translate("builder.pageDescription"),
|
|
491
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
492
|
+
"input",
|
|
493
|
+
{
|
|
494
|
+
value: page.translations?.[editingLocale]?.description ?? "",
|
|
495
|
+
onChange: (event) => {
|
|
496
|
+
const value = event.currentTarget.value;
|
|
497
|
+
updatePage(page.id, (current) => ({
|
|
498
|
+
...current,
|
|
499
|
+
translations: {
|
|
500
|
+
...current.translations,
|
|
501
|
+
[editingLocale]: {
|
|
502
|
+
...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
|
|
503
|
+
...value.length === 0 ? {} : { description: value }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}));
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
] })
|
|
511
|
+
] })
|
|
512
|
+
] }),
|
|
513
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__condition", children: [
|
|
514
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
515
|
+
translate("builder.pageCondition"),
|
|
516
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
517
|
+
"select",
|
|
518
|
+
{
|
|
519
|
+
value: page.displayCondition?.questionId ?? "",
|
|
520
|
+
onChange: (event) => {
|
|
521
|
+
const selected = schema.fields.find((field) => field.id === event.currentTarget.value);
|
|
522
|
+
updatePage(page.id, (current) => {
|
|
523
|
+
if (selected === void 0) {
|
|
524
|
+
const { displayCondition: _condition, ...withoutCondition } = current;
|
|
525
|
+
return withoutCondition;
|
|
526
|
+
}
|
|
527
|
+
return {
|
|
528
|
+
...current,
|
|
529
|
+
displayCondition: conditionWithValue(
|
|
530
|
+
selected.id,
|
|
531
|
+
conditionOperators(selected)[0] ?? "not_empty",
|
|
532
|
+
defaultConditionValue(selected)
|
|
533
|
+
)
|
|
534
|
+
};
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
children: [
|
|
538
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: translate("builder.alwaysVisible") }),
|
|
539
|
+
availableSources.map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: field.id, children: field.title }, field.id))
|
|
540
|
+
]
|
|
541
|
+
}
|
|
542
|
+
)
|
|
543
|
+
] }),
|
|
544
|
+
page.displayCondition !== void 0 && source !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
545
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
546
|
+
"select",
|
|
547
|
+
{
|
|
548
|
+
"aria-label": translate("builder.conditionOperator"),
|
|
549
|
+
value: page.displayCondition.operator,
|
|
550
|
+
onChange: (event) => {
|
|
551
|
+
const operator = event.currentTarget.value;
|
|
552
|
+
updatePage(page.id, (current) => ({
|
|
553
|
+
...current,
|
|
554
|
+
displayCondition: conditionWithValue(source.id, operator, defaultConditionValue(source))
|
|
555
|
+
}));
|
|
556
|
+
},
|
|
557
|
+
children: conditionOperators(source).map((operator) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: operator, children: translate(operatorKey(operator)) }, operator))
|
|
558
|
+
}
|
|
559
|
+
),
|
|
560
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
561
|
+
ConditionValueEditor,
|
|
562
|
+
{
|
|
563
|
+
source,
|
|
564
|
+
condition: page.displayCondition,
|
|
565
|
+
onChange: (condition) => updatePage(page.id, (current) => ({ ...current, displayCondition: condition })),
|
|
566
|
+
translate
|
|
567
|
+
}
|
|
568
|
+
)
|
|
569
|
+
] }) : null
|
|
570
|
+
] })
|
|
571
|
+
] }, page.id);
|
|
572
|
+
}),
|
|
573
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__page-add", children: [
|
|
574
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
575
|
+
translate("builder.pageQuestion"),
|
|
576
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
577
|
+
"select",
|
|
578
|
+
{
|
|
579
|
+
value: newPageQuestionId,
|
|
580
|
+
disabled: movablePageQuestions.length === 0,
|
|
581
|
+
onChange: (event) => setNewPageQuestionId(event.currentTarget.value),
|
|
582
|
+
children: [
|
|
583
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "\u2014" }),
|
|
584
|
+
schema.fields.filter((field) => movablePageQuestions.includes(field.id)).map((field) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: field.id, children: field.title }, field.id))
|
|
585
|
+
]
|
|
586
|
+
}
|
|
587
|
+
)
|
|
588
|
+
] }),
|
|
589
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", disabled: movablePageQuestions.length === 0, onClick: addPage, children: translate("builder.addPage") })
|
|
590
|
+
] })
|
|
591
|
+
] })
|
|
592
|
+
] }),
|
|
593
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "form-engine-builder__localization", "aria-labelledby": "builder-localization-heading", children: [
|
|
594
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { id: "builder-localization-heading", children: translate("builder.localization") }),
|
|
595
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
596
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
597
|
+
translate("builder.defaultLocale"),
|
|
598
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
599
|
+
"input",
|
|
600
|
+
{
|
|
601
|
+
value: schema.defaultLocale ?? "",
|
|
602
|
+
onChange: (event) => {
|
|
603
|
+
const value = event.currentTarget.value.trim();
|
|
604
|
+
emitSchema(
|
|
605
|
+
value.length === 0 ? schema : {
|
|
606
|
+
...schema,
|
|
607
|
+
defaultLocale: value,
|
|
608
|
+
supportedLocales: [.../* @__PURE__ */ new Set([value, ...schema.supportedLocales ?? []])]
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
)
|
|
614
|
+
] }),
|
|
615
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
616
|
+
translate("builder.addLocale"),
|
|
617
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { value: newLocale, onChange: (event) => setNewLocale(event.currentTarget.value) })
|
|
618
|
+
] }),
|
|
619
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", onClick: addLocale, children: translate("builder.addLocale") }),
|
|
620
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
621
|
+
translate("builder.editLocale"),
|
|
622
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("select", { value: editingLocale, onChange: (event) => setEditingLocale(event.currentTarget.value), children: [
|
|
623
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "", children: "\u2014" }),
|
|
624
|
+
(schema.supportedLocales ?? []).filter((item) => item !== schema.defaultLocale).map((item) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: item, children: item }, item))
|
|
625
|
+
] })
|
|
626
|
+
] }),
|
|
627
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
628
|
+
"button",
|
|
629
|
+
{
|
|
630
|
+
type: "button",
|
|
631
|
+
disabled: translationAdapter === void 0 || editingLocale.length === 0 || isTranslating,
|
|
632
|
+
onClick: () => void translateAll(),
|
|
633
|
+
children: translate("builder.autoTranslate")
|
|
634
|
+
}
|
|
635
|
+
)
|
|
636
|
+
] }),
|
|
637
|
+
translationAdapter === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { children: translate("builder.translationUnavailable") }) : null,
|
|
638
|
+
translationError === null ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "form-engine-builder__error", children: translationError }),
|
|
639
|
+
editingLocale.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
640
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
641
|
+
translate("builder.questionTitle"),
|
|
642
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
643
|
+
"input",
|
|
644
|
+
{
|
|
645
|
+
value: schema.translations?.[editingLocale]?.title ?? "",
|
|
646
|
+
onChange: (event) => updateFormTranslation("title", event.currentTarget.value)
|
|
647
|
+
}
|
|
648
|
+
)
|
|
649
|
+
] }),
|
|
650
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
651
|
+
translate("builder.pageDescription"),
|
|
652
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
653
|
+
"input",
|
|
654
|
+
{
|
|
655
|
+
value: schema.translations?.[editingLocale]?.description ?? "",
|
|
656
|
+
onChange: (event) => updateFormTranslation("description", event.currentTarget.value)
|
|
657
|
+
}
|
|
658
|
+
)
|
|
659
|
+
] })
|
|
660
|
+
] })
|
|
661
|
+
] }),
|
|
240
662
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "form-engine-builder__list", children: schema.fields.map((field, index) => {
|
|
241
663
|
const condition = field.displayCondition;
|
|
242
664
|
const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
|
|
@@ -313,6 +735,98 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
313
735
|
translate("builder.required")
|
|
314
736
|
] })
|
|
315
737
|
] }),
|
|
738
|
+
schema.pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
739
|
+
translate("builder.questionPage"),
|
|
740
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
741
|
+
"select",
|
|
742
|
+
{
|
|
743
|
+
value: pageForField(field.id)?.id ?? "",
|
|
744
|
+
onChange: (event) => assignFieldToPage(field.id, event.currentTarget.value),
|
|
745
|
+
children: schema.pages.map((page, pageIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: page.id, children: page.title ?? `${translate("builder.newPage")} ${pageIndex + 1}` }, page.id))
|
|
746
|
+
}
|
|
747
|
+
)
|
|
748
|
+
] }),
|
|
749
|
+
editingLocale.length === 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__translation-editor", children: [
|
|
750
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: editingLocale }),
|
|
751
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
752
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
753
|
+
translate("builder.questionTitle"),
|
|
754
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
755
|
+
"input",
|
|
756
|
+
{
|
|
757
|
+
value: field.translations?.[editingLocale]?.title ?? "",
|
|
758
|
+
onChange: (event) => {
|
|
759
|
+
const value = event.currentTarget.value;
|
|
760
|
+
updateField(field.id, (current) => ({
|
|
761
|
+
...current,
|
|
762
|
+
translations: {
|
|
763
|
+
...current.translations,
|
|
764
|
+
[editingLocale]: {
|
|
765
|
+
...value.length === 0 ? {} : { title: value },
|
|
766
|
+
...current.translations?.[editingLocale]?.description === void 0 ? {} : { description: current.translations[editingLocale]?.description }
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
)
|
|
773
|
+
] }),
|
|
774
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
775
|
+
translate("builder.pageDescription"),
|
|
776
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
777
|
+
"input",
|
|
778
|
+
{
|
|
779
|
+
value: field.translations?.[editingLocale]?.description ?? "",
|
|
780
|
+
onChange: (event) => {
|
|
781
|
+
const value = event.currentTarget.value;
|
|
782
|
+
updateField(field.id, (current) => ({
|
|
783
|
+
...current,
|
|
784
|
+
translations: {
|
|
785
|
+
...current.translations,
|
|
786
|
+
[editingLocale]: {
|
|
787
|
+
...current.translations?.[editingLocale]?.title === void 0 ? {} : { title: current.translations[editingLocale]?.title },
|
|
788
|
+
...value.length === 0 ? {} : { description: value }
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
)
|
|
795
|
+
] })
|
|
796
|
+
] }),
|
|
797
|
+
"options" in field ? field.options.map((option, optionIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
798
|
+
translate("builder.optionLabel", { index: optionIndex + 1 }),
|
|
799
|
+
" (",
|
|
800
|
+
editingLocale,
|
|
801
|
+
")",
|
|
802
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
803
|
+
"input",
|
|
804
|
+
{
|
|
805
|
+
value: option.translations?.[editingLocale] ?? "",
|
|
806
|
+
onChange: (event) => {
|
|
807
|
+
const value = event.currentTarget.value;
|
|
808
|
+
updateField(field.id, (current) => {
|
|
809
|
+
if (!("options" in current)) return current;
|
|
810
|
+
return {
|
|
811
|
+
...current,
|
|
812
|
+
options: current.options.map(
|
|
813
|
+
(candidate) => candidate.id === option.id ? {
|
|
814
|
+
...candidate,
|
|
815
|
+
translations: Object.fromEntries([
|
|
816
|
+
...Object.entries(candidate.translations ?? {}).filter(
|
|
817
|
+
([localeKey]) => localeKey !== editingLocale
|
|
818
|
+
),
|
|
819
|
+
...value.length === 0 ? [] : [[editingLocale, value]]
|
|
820
|
+
])
|
|
821
|
+
} : candidate
|
|
822
|
+
)
|
|
823
|
+
};
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
)
|
|
828
|
+
] }, option.id)) : null
|
|
829
|
+
] }),
|
|
316
830
|
field.type === "rating" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "form-engine-builder__grid", children: [
|
|
317
831
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { children: [
|
|
318
832
|
translate("builder.minimum"),
|
|
@@ -476,9 +990,9 @@ function FormBuilder({ schema, onChange, locale = "en", translator }) {
|
|
|
476
990
|
|
|
477
991
|
// src/context.tsx
|
|
478
992
|
var import_core2 = require("@form-engine-ts/core");
|
|
479
|
-
var
|
|
993
|
+
var import_react2 = require("react");
|
|
480
994
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
481
|
-
var FormContext = (0,
|
|
995
|
+
var FormContext = (0, import_react2.createContext)(null);
|
|
482
996
|
function issuesByField(issues) {
|
|
483
997
|
const result = {};
|
|
484
998
|
for (const issue of issues) result[issue.fieldId] ??= issue;
|
|
@@ -493,26 +1007,30 @@ function FormProvider({
|
|
|
493
1007
|
onSubmit,
|
|
494
1008
|
children
|
|
495
1009
|
}) {
|
|
496
|
-
const validSchema = (0,
|
|
1010
|
+
const validSchema = (0, import_react2.useMemo)(() => {
|
|
497
1011
|
(0, import_core2.assertValidFormSchema)(schema);
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const [
|
|
503
|
-
const [
|
|
504
|
-
const
|
|
505
|
-
(0,
|
|
1012
|
+
const localized = (0, import_core2.resolveLocalizedSchema)(schema, locale);
|
|
1013
|
+
(0, import_core2.assertValidFormSchema)(localized);
|
|
1014
|
+
return localized;
|
|
1015
|
+
}, [locale, schema]);
|
|
1016
|
+
const [values, setValues] = (0, import_react2.useState)(() => ({ ...initialValues }));
|
|
1017
|
+
const [errors, setErrors] = (0, import_react2.useState)({});
|
|
1018
|
+
const [submitStatus, setSubmitStatus] = (0, import_react2.useState)("idle");
|
|
1019
|
+
const [submitError, setSubmitError] = (0, import_react2.useState)(null);
|
|
1020
|
+
const [validationPageIndex, setValidationPageIndex] = (0, import_react2.useState)(null);
|
|
1021
|
+
const visibility = (0, import_react2.useMemo)(() => (0, import_core2.calculateFieldVisibility)(validSchema, values), [validSchema, values]);
|
|
1022
|
+
const pageVisibility = (0, import_react2.useMemo)(() => (0, import_core2.calculatePageVisibility)(validSchema, values), [validSchema, values]);
|
|
1023
|
+
(0, import_react2.useEffect)(() => {
|
|
506
1024
|
const fieldIds = new Set(validSchema.fields.map((field) => field.id));
|
|
507
1025
|
setValues((current) => Object.fromEntries(Object.entries(current).filter(([fieldId]) => fieldIds.has(fieldId))));
|
|
508
1026
|
}, [validSchema]);
|
|
509
|
-
const setValue = (0,
|
|
1027
|
+
const setValue = (0, import_react2.useCallback)(
|
|
510
1028
|
(fieldId, value) => {
|
|
511
1029
|
setValues((current) => {
|
|
512
1030
|
const next = { ...current, [fieldId]: value };
|
|
513
1031
|
setErrors((currentErrors) => {
|
|
514
1032
|
if (Object.keys(currentErrors).length === 0) return currentErrors;
|
|
515
|
-
const result = (0, import_core2.validateAnswers)(validSchema, next);
|
|
1033
|
+
const result = validationPageIndex === null ? (0, import_core2.validateAnswers)(validSchema, next) : (0, import_core2.validatePageAnswers)(validSchema, validationPageIndex, next);
|
|
516
1034
|
return issuesByField(result.issues);
|
|
517
1035
|
});
|
|
518
1036
|
return next;
|
|
@@ -520,23 +1038,48 @@ function FormProvider({
|
|
|
520
1038
|
setSubmitStatus((current) => current === "success" || current === "error" ? "idle" : current);
|
|
521
1039
|
setSubmitError(null);
|
|
522
1040
|
},
|
|
1041
|
+
[validSchema, validationPageIndex]
|
|
1042
|
+
);
|
|
1043
|
+
const restoreValues = (0, import_react2.useCallback)(
|
|
1044
|
+
(restoredValues) => {
|
|
1045
|
+
const fieldIds = new Set(validSchema.fields.map((field) => field.id));
|
|
1046
|
+
setValues(Object.fromEntries(Object.entries(restoredValues).filter(([fieldId]) => fieldIds.has(fieldId))));
|
|
1047
|
+
setErrors({});
|
|
1048
|
+
setValidationPageIndex(null);
|
|
1049
|
+
setSubmitStatus("idle");
|
|
1050
|
+
setSubmitError(null);
|
|
1051
|
+
},
|
|
523
1052
|
[validSchema]
|
|
524
1053
|
);
|
|
525
|
-
const
|
|
1054
|
+
const validatePage = (0, import_react2.useCallback)(
|
|
1055
|
+
(pageIndex) => {
|
|
1056
|
+
const result = (0, import_core2.validatePageAnswers)(validSchema, pageIndex, values);
|
|
1057
|
+
setErrors(issuesByField(result.issues));
|
|
1058
|
+
setValidationPageIndex(result.valid ? null : pageIndex);
|
|
1059
|
+
setSubmitStatus("idle");
|
|
1060
|
+
setSubmitError(null);
|
|
1061
|
+
return result;
|
|
1062
|
+
},
|
|
1063
|
+
[validSchema, values]
|
|
1064
|
+
);
|
|
1065
|
+
const reset = (0, import_react2.useCallback)(() => {
|
|
526
1066
|
setValues({ ...initialValues });
|
|
527
1067
|
setErrors({});
|
|
1068
|
+
setValidationPageIndex(null);
|
|
528
1069
|
setSubmitStatus("idle");
|
|
529
1070
|
setSubmitError(null);
|
|
530
1071
|
}, [initialValues]);
|
|
531
|
-
const submit = (0,
|
|
1072
|
+
const submit = (0, import_react2.useCallback)(async () => {
|
|
532
1073
|
const validation = (0, import_core2.validateAnswers)(validSchema, values);
|
|
533
1074
|
if (!validation.valid) {
|
|
534
1075
|
setErrors(issuesByField(validation.issues));
|
|
1076
|
+
setValidationPageIndex(null);
|
|
535
1077
|
setSubmitStatus("error");
|
|
536
1078
|
setSubmitError(null);
|
|
537
1079
|
return false;
|
|
538
1080
|
}
|
|
539
1081
|
setErrors({});
|
|
1082
|
+
setValidationPageIndex(null);
|
|
540
1083
|
setSubmitStatus("submitting");
|
|
541
1084
|
setSubmitError(null);
|
|
542
1085
|
try {
|
|
@@ -550,22 +1093,25 @@ function FormProvider({
|
|
|
550
1093
|
return false;
|
|
551
1094
|
}
|
|
552
1095
|
}, [initialValues, onSubmit, resetOnSuccess, validSchema, values]);
|
|
553
|
-
const translate = (0,
|
|
1096
|
+
const translate = (0, import_react2.useCallback)(
|
|
554
1097
|
(key, params) => translator.translate(key, locale, params),
|
|
555
1098
|
[locale, translator]
|
|
556
1099
|
);
|
|
557
|
-
const contextValue = (0,
|
|
1100
|
+
const contextValue = (0, import_react2.useMemo)(
|
|
558
1101
|
() => ({
|
|
559
1102
|
schema: validSchema,
|
|
560
1103
|
locale,
|
|
561
1104
|
translator,
|
|
562
1105
|
values,
|
|
563
1106
|
visibility,
|
|
1107
|
+
pageVisibility,
|
|
564
1108
|
errors,
|
|
565
1109
|
submitStatus,
|
|
566
1110
|
submitError,
|
|
567
1111
|
isSubmitting: submitStatus === "submitting",
|
|
568
1112
|
setValue,
|
|
1113
|
+
restoreValues,
|
|
1114
|
+
validatePage,
|
|
569
1115
|
reset,
|
|
570
1116
|
submit,
|
|
571
1117
|
translate
|
|
@@ -573,13 +1119,16 @@ function FormProvider({
|
|
|
573
1119
|
[
|
|
574
1120
|
errors,
|
|
575
1121
|
locale,
|
|
1122
|
+
pageVisibility,
|
|
576
1123
|
reset,
|
|
1124
|
+
restoreValues,
|
|
577
1125
|
setValue,
|
|
578
1126
|
submit,
|
|
579
1127
|
submitError,
|
|
580
1128
|
submitStatus,
|
|
581
1129
|
translate,
|
|
582
1130
|
translator,
|
|
1131
|
+
validatePage,
|
|
583
1132
|
validSchema,
|
|
584
1133
|
values,
|
|
585
1134
|
visibility
|
|
@@ -588,7 +1137,7 @@ function FormProvider({
|
|
|
588
1137
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(FormContext.Provider, { value: contextValue, children });
|
|
589
1138
|
}
|
|
590
1139
|
function useForm() {
|
|
591
|
-
const context = (0,
|
|
1140
|
+
const context = (0, import_react2.useContext)(FormContext);
|
|
592
1141
|
if (context === null) throw new Error("useForm must be called inside a FormProvider.");
|
|
593
1142
|
return context;
|
|
594
1143
|
}
|
|
@@ -596,13 +1145,13 @@ function useField(fieldId) {
|
|
|
596
1145
|
const form = useForm();
|
|
597
1146
|
const field = form.schema.fields.find((item) => item.id === fieldId);
|
|
598
1147
|
if (field === void 0) throw new Error(`Unknown form field: ${fieldId}`);
|
|
599
|
-
const setValue = (0,
|
|
1148
|
+
const setValue = (0, import_react2.useCallback)((value) => form.setValue(fieldId, value), [fieldId, form]);
|
|
600
1149
|
return { field, value: form.values[fieldId], error: form.errors[fieldId], setValue };
|
|
601
1150
|
}
|
|
602
1151
|
|
|
603
1152
|
// src/renderer.tsx
|
|
604
1153
|
var import_core3 = require("@form-engine-ts/core");
|
|
605
|
-
var
|
|
1154
|
+
var import_react3 = require("react");
|
|
606
1155
|
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
607
1156
|
function describedBy(field, error, helpId, errorId) {
|
|
608
1157
|
const ids = [field.description === void 0 ? void 0 : helpId, error === void 0 ? void 0 : errorId].filter(
|
|
@@ -781,35 +1330,156 @@ function DefaultField(props) {
|
|
|
781
1330
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(FieldMessage, { props })
|
|
782
1331
|
] });
|
|
783
1332
|
}
|
|
1333
|
+
function isRecord(value) {
|
|
1334
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1335
|
+
}
|
|
1336
|
+
function isFormValue(value) {
|
|
1337
|
+
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");
|
|
1338
|
+
}
|
|
1339
|
+
function parseDraft(serialized) {
|
|
1340
|
+
try {
|
|
1341
|
+
const value = JSON.parse(serialized);
|
|
1342
|
+
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)) {
|
|
1343
|
+
return null;
|
|
1344
|
+
}
|
|
1345
|
+
return {
|
|
1346
|
+
formId: value.formId,
|
|
1347
|
+
formVersion: value.formVersion,
|
|
1348
|
+
savedAt: value.savedAt,
|
|
1349
|
+
values: Object.fromEntries(
|
|
1350
|
+
Object.entries(value.values).filter((entry) => isFormValue(entry[1]))
|
|
1351
|
+
)
|
|
1352
|
+
};
|
|
1353
|
+
} catch {
|
|
1354
|
+
return null;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
784
1357
|
function FormRenderer({
|
|
785
1358
|
components = {},
|
|
786
1359
|
className = "",
|
|
787
1360
|
successMessageKey,
|
|
788
|
-
errorMessageKey
|
|
1361
|
+
errorMessageKey,
|
|
1362
|
+
autoSaveKey
|
|
789
1363
|
}) {
|
|
790
1364
|
const form = useForm();
|
|
791
|
-
const prefix = (0,
|
|
1365
|
+
const prefix = (0, import_react3.useId)().replace(/:/g, "");
|
|
1366
|
+
const formRef = (0, import_react3.useRef)(null);
|
|
1367
|
+
const loadedDraftKey = (0, import_react3.useRef)(null);
|
|
1368
|
+
const [draftRestored, setDraftRestored] = (0, import_react3.useState)(false);
|
|
1369
|
+
const [currentPageIndex, setCurrentPageIndex] = (0, import_react3.useState)(0);
|
|
1370
|
+
const [focusFieldId, setFocusFieldId] = (0, import_react3.useState)(null);
|
|
1371
|
+
const pages = form.schema.pages;
|
|
1372
|
+
const visiblePageIndexes = (0, import_react3.useMemo)(
|
|
1373
|
+
() => pages === void 0 ? [] : pages.flatMap((page, index) => form.pageVisibility[page.id] === true ? [index] : []),
|
|
1374
|
+
[form.pageVisibility, pages]
|
|
1375
|
+
);
|
|
1376
|
+
const activePage = pages?.[currentPageIndex];
|
|
1377
|
+
const activeVisibleIndex = visiblePageIndexes.indexOf(currentPageIndex);
|
|
1378
|
+
const fieldIds = activePage === void 0 ? void 0 : new Set(activePage.questionIds);
|
|
1379
|
+
(0, import_react3.useEffect)(() => {
|
|
1380
|
+
if (pages === void 0 || visiblePageIndexes.length === 0) {
|
|
1381
|
+
setCurrentPageIndex(0);
|
|
1382
|
+
return;
|
|
1383
|
+
}
|
|
1384
|
+
if (!visiblePageIndexes.includes(currentPageIndex)) setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
1385
|
+
}, [currentPageIndex, pages, visiblePageIndexes]);
|
|
1386
|
+
(0, import_react3.useEffect)(() => {
|
|
1387
|
+
if (focusFieldId === null) return;
|
|
1388
|
+
const fieldContainer = [...formRef.current?.querySelectorAll("[data-field-id]") ?? []].find(
|
|
1389
|
+
(element) => element.dataset.fieldId === focusFieldId
|
|
1390
|
+
);
|
|
1391
|
+
const control = fieldContainer?.querySelector("input, select, textarea");
|
|
1392
|
+
if (control !== void 0 && control !== null) {
|
|
1393
|
+
control.focus();
|
|
1394
|
+
setFocusFieldId(null);
|
|
1395
|
+
}
|
|
1396
|
+
}, [focusFieldId]);
|
|
1397
|
+
(0, import_react3.useEffect)(() => {
|
|
1398
|
+
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1399
|
+
const loadIdentity = `${autoSaveKey}:${form.schema.id}:${form.schema.version}`;
|
|
1400
|
+
if (loadedDraftKey.current === loadIdentity) return;
|
|
1401
|
+
loadedDraftKey.current = loadIdentity;
|
|
1402
|
+
const serialized = globalThis.localStorage.getItem(autoSaveKey);
|
|
1403
|
+
if (serialized === null) return;
|
|
1404
|
+
const draft = parseDraft(serialized);
|
|
1405
|
+
if (draft === null || draft.formId !== form.schema.id || draft.formVersion !== form.schema.version) return;
|
|
1406
|
+
form.restoreValues(draft.values);
|
|
1407
|
+
setDraftRestored(true);
|
|
1408
|
+
}, [autoSaveKey, form.restoreValues, form.schema.id, form.schema.version]);
|
|
1409
|
+
(0, import_react3.useEffect)(() => {
|
|
1410
|
+
if (autoSaveKey === void 0 || typeof globalThis.localStorage === "undefined") return;
|
|
1411
|
+
if (form.submitStatus === "success") return;
|
|
1412
|
+
const timeout = globalThis.setTimeout(() => {
|
|
1413
|
+
const draft = {
|
|
1414
|
+
formId: form.schema.id,
|
|
1415
|
+
formVersion: form.schema.version,
|
|
1416
|
+
values: form.values,
|
|
1417
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1418
|
+
};
|
|
1419
|
+
globalThis.localStorage.setItem(autoSaveKey, JSON.stringify(draft));
|
|
1420
|
+
}, 500);
|
|
1421
|
+
return () => globalThis.clearTimeout(timeout);
|
|
1422
|
+
}, [autoSaveKey, form.schema.id, form.schema.version, form.submitStatus, form.values]);
|
|
1423
|
+
const focusFirstIssue = (fieldId) => {
|
|
1424
|
+
if (fieldId !== void 0) setFocusFieldId(fieldId);
|
|
1425
|
+
};
|
|
1426
|
+
const handleNext = () => {
|
|
1427
|
+
const result = form.validatePage(currentPageIndex);
|
|
1428
|
+
if (!result.valid) {
|
|
1429
|
+
focusFirstIssue(result.issues[0]?.fieldId);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
const nextPageIndex = visiblePageIndexes[activeVisibleIndex + 1];
|
|
1433
|
+
if (nextPageIndex !== void 0) setCurrentPageIndex(nextPageIndex);
|
|
1434
|
+
};
|
|
792
1435
|
const handleSubmit = async (event) => {
|
|
793
1436
|
event.preventDefault();
|
|
794
|
-
const formElement = event.currentTarget;
|
|
795
1437
|
const validation = (0, import_core3.validateAnswers)(form.schema, form.values);
|
|
796
1438
|
const firstInvalidFieldId = validation.issues[0]?.fieldId;
|
|
797
1439
|
const valid = await form.submit();
|
|
798
|
-
if (!valid
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1440
|
+
if (!valid) {
|
|
1441
|
+
const invalidPageIndex = pages?.findIndex(
|
|
1442
|
+
(page) => firstInvalidFieldId === void 0 ? false : page.questionIds.includes(firstInvalidFieldId)
|
|
1443
|
+
);
|
|
1444
|
+
if (invalidPageIndex !== void 0 && invalidPageIndex >= 0) setCurrentPageIndex(invalidPageIndex);
|
|
1445
|
+
focusFirstIssue(firstInvalidFieldId);
|
|
1446
|
+
return;
|
|
805
1447
|
}
|
|
1448
|
+
if (autoSaveKey !== void 0 && typeof globalThis.localStorage !== "undefined") {
|
|
1449
|
+
globalThis.localStorage.removeItem(autoSaveKey);
|
|
1450
|
+
setDraftRestored(false);
|
|
1451
|
+
}
|
|
1452
|
+
setCurrentPageIndex(visiblePageIndexes[0] ?? 0);
|
|
806
1453
|
};
|
|
807
|
-
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
1454
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { ref: formRef, className: `fe-form ${className}`.trim(), noValidate: true, onSubmit: handleSubmit, children: [
|
|
808
1455
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("header", { className: "fe-header", children: [
|
|
809
1456
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h1", { children: form.schema.title }),
|
|
810
|
-
form.schema.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: form.schema.description })
|
|
1457
|
+
form.schema.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { children: form.schema.description }),
|
|
1458
|
+
pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-progress", children: [
|
|
1459
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1460
|
+
"div",
|
|
1461
|
+
{
|
|
1462
|
+
className: "form-progress-bar",
|
|
1463
|
+
role: "progressbar",
|
|
1464
|
+
"aria-valuemin": 1,
|
|
1465
|
+
"aria-valuemax": visiblePageIndexes.length,
|
|
1466
|
+
"aria-valuenow": activeVisibleIndex + 1,
|
|
1467
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1468
|
+
"div",
|
|
1469
|
+
{
|
|
1470
|
+
className: "form-progress-fill",
|
|
1471
|
+
style: { width: `${(activeVisibleIndex + 1) / visiblePageIndexes.length * 100}%` }
|
|
1472
|
+
}
|
|
1473
|
+
)
|
|
1474
|
+
}
|
|
1475
|
+
),
|
|
1476
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: form.translate("form.step", { current: activeVisibleIndex + 1, total: visiblePageIndexes.length }) })
|
|
1477
|
+
] }),
|
|
1478
|
+
draftRestored ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "form-draft-badge", children: form.translate("form.draftRestored") }) : null
|
|
811
1479
|
] }),
|
|
812
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("
|
|
1480
|
+
activePage?.title === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("h2", { className: "fe-page-title", children: activePage.title }),
|
|
1481
|
+
activePage?.description === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "fe-page-description", children: activePage.description }),
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "fe-fields", children: form.schema.fields.filter((field) => form.visibility[field.id] === true && (fieldIds === void 0 || fieldIds.has(field.id))).map((field) => {
|
|
813
1483
|
const props = {
|
|
814
1484
|
field,
|
|
815
1485
|
value: form.values[field.id],
|
|
@@ -823,7 +1493,18 @@ function FormRenderer({
|
|
|
823
1493
|
const Component = components[field.type];
|
|
824
1494
|
return Component === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(DefaultField, { ...props }, field.id) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Component, { ...props }, field.id);
|
|
825
1495
|
}) }),
|
|
826
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") }),
|
|
1496
|
+
pages === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "form-step-navigation", children: [
|
|
1497
|
+
activeVisibleIndex > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
1498
|
+
"button",
|
|
1499
|
+
{
|
|
1500
|
+
className: "btn-prev",
|
|
1501
|
+
type: "button",
|
|
1502
|
+
onClick: () => setCurrentPageIndex(visiblePageIndexes[activeVisibleIndex - 1] ?? 0),
|
|
1503
|
+
children: form.translate("form.back")
|
|
1504
|
+
}
|
|
1505
|
+
) : null,
|
|
1506
|
+
activeVisibleIndex < visiblePageIndexes.length - 1 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "btn-next", type: "button", onClick: handleNext, children: form.translate("form.next") }) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { className: "fe-submit", type: "submit", disabled: form.isSubmitting, children: form.translate(form.schema.submitLabelKey ?? "form.submit") })
|
|
1507
|
+
] }),
|
|
827
1508
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "fe-status", "aria-live": "polite", children: [
|
|
828
1509
|
form.submitStatus === "success" && successMessageKey !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "status", children: form.translate(successMessageKey) }) : null,
|
|
829
1510
|
form.submitStatus === "error" && form.submitError !== null && errorMessageKey !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { role: "alert", children: form.translate(errorMessageKey) }) : null
|