@form-engine-ts/react 4.0.0 → 4.2.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 CHANGED
@@ -72,6 +72,11 @@ controls `pages`, `localization`, and `conditions` authoring surfaces; each defa
72
72
  set, locale addition becomes a selector containing only unregistered allowed locales, and the action is disabled at
73
73
  `maxLocales`.
74
74
 
75
+ `fieldEditorControls` makes the standard field editor's title, description, required flag, type selector, options,
76
+ display conditions, text limits, rating bounds, and number limits independently `editable`, `readOnly`, or `hidden`.
77
+ `fieldTypeOptions` can provide an explicit order, comparator, or transformation for generated type choices; the default
78
+ choices are copied before they are changed.
79
+
75
80
  Visual Builder has two independent design-system extension layers. `components` replaces normalized primitives such as
76
81
  `Button`, `TextInput`, `TextArea`, `Select`, `Checkbox`, `Section`, and `Fieldset`. `slots` replaces complete authoring
77
82
  surfaces: `toolbar`, `fieldEditor`, `optionEditor`, `pages`, `localization`, or `translationActions`. Slot props expose
package/dist/index.cjs CHANGED
@@ -29,6 +29,8 @@ __export(index_exports, {
29
29
  createLocalStorageSubmissionAttemptStore: () => createLocalStorageSubmissionAttemptStore,
30
30
  createLocalStorageSubmissionReceiptStore: () => createLocalStorageSubmissionReceiptStore,
31
31
  isTranslationUnresolved: () => isTranslationUnresolved,
32
+ resolveFieldEditorControls: () => resolveFieldEditorControls,
33
+ resolveFieldTypeSelectOptions: () => resolveFieldTypeSelectOptions,
32
34
  resolveInitialFieldType: () => resolveInitialFieldType,
33
35
  resolveTranslation: () => resolveTranslation,
34
36
  submissionReceiptQueryKey: () => submissionReceiptQueryKey,
@@ -167,6 +169,61 @@ function defaultCreateOption(field, id) {
167
169
  function defaultCreatePage(id, questionIds) {
168
170
  return { id, title: "New page", questionIds };
169
171
  }
172
+ function applyFieldConstraintDefaults(field, policy) {
173
+ const constraint = policy?.fieldConstraints?.[field.type];
174
+ if (constraint === void 0) return field;
175
+ const required = constraint.fixedRequired ?? constraint.defaultRequired ?? field.required;
176
+ if (field.type === "rating") {
177
+ const ratingConstraint = "defaultMin" in constraint || "defaultMax" in constraint || "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
178
+ const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : ratingConstraint !== void 0 && "defaultMin" in ratingConstraint ? ratingConstraint.defaultMin : field.min;
179
+ const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : ratingConstraint !== void 0 && "defaultMax" in ratingConstraint ? ratingConstraint.defaultMax : field.max;
180
+ return {
181
+ ...field,
182
+ required,
183
+ ...min === void 0 ? {} : { min },
184
+ ...max === void 0 ? {} : { max }
185
+ };
186
+ }
187
+ if ((field.type === "text" || field.type === "textarea") && "defaultMaxLength" in constraint) {
188
+ return {
189
+ ...field,
190
+ required,
191
+ ...constraint.defaultMaxLength === void 0 ? {} : { maxLength: constraint.defaultMaxLength }
192
+ };
193
+ }
194
+ return { ...field, required };
195
+ }
196
+ function fieldConstraintError(updated, policy) {
197
+ const constraint = policy?.fieldConstraints?.[updated.type];
198
+ if (constraint === void 0) return void 0;
199
+ if (constraint.fixedRequired !== void 0 && updated.required !== constraint.fixedRequired) {
200
+ return { type: "field_constraint_immutable" };
201
+ }
202
+ if (updated.type === "rating") {
203
+ if ("fixedMin" in constraint && constraint.fixedMin !== void 0 && updated.min !== constraint.fixedMin) {
204
+ return { type: "field_constraint_immutable" };
205
+ }
206
+ if ("fixedMax" in constraint && constraint.fixedMax !== void 0 && updated.max !== constraint.fixedMax) {
207
+ return { type: "field_constraint_immutable" };
208
+ }
209
+ if ("allowedMinRange" in constraint && constraint.allowedMinRange !== void 0 && typeof updated.min === "number" && (updated.min < constraint.allowedMinRange[0] || updated.min > constraint.allowedMinRange[1])) {
210
+ return { type: "field_constraint_violation", property: "min", expected: constraint.allowedMinRange[0] };
211
+ }
212
+ if ("allowedMaxRange" in constraint && constraint.allowedMaxRange !== void 0 && typeof updated.max === "number" && (updated.max < constraint.allowedMaxRange[0] || updated.max > constraint.allowedMaxRange[1])) {
213
+ return { type: "field_constraint_violation", property: "max", expected: constraint.allowedMaxRange[1] };
214
+ }
215
+ }
216
+ if ((updated.type === "text" || updated.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0 && updated.maxLength !== void 0 && updated.maxLength > constraint.maxMaxLength) {
217
+ return { type: "field_constraint_violation", property: "maxLength", expected: constraint.maxMaxLength };
218
+ }
219
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && updated.options.length < constraint.minOptions) {
220
+ return { type: "field_constraint_violation", property: "options", expected: constraint.minOptions };
221
+ }
222
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && updated.options.length > constraint.maxOptions) {
223
+ return { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions };
224
+ }
225
+ return void 0;
226
+ }
170
227
  function move(items, sourceIndex, targetIndex) {
171
228
  if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
172
229
  return void 0;
@@ -222,6 +279,8 @@ function useFormBuilder({
222
279
  const updated = updater(current);
223
280
  if (updated.id !== fieldId)
224
281
  return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
282
+ const constraintError = fieldConstraintError(updated, policy);
283
+ if (constraintError !== void 0) return { success: false, error: constraintError };
225
284
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
226
285
  return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
227
286
  for (const text of [updated.title, updated.description]) {
@@ -233,7 +292,7 @@ function useFormBuilder({
233
292
  onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
234
293
  return { success: true };
235
294
  },
236
- [onChange, policy?.allowedFieldTypes, schema, textPolicyError]
295
+ [onChange, policy, schema, textPolicyError]
237
296
  );
238
297
  const updateOption = (0, import_react.useCallback)(
239
298
  (fieldId, optionId, updater) => {
@@ -306,6 +365,7 @@ function useFormBuilder({
306
365
  return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
307
366
  field = { ...field, options: [option] };
308
367
  }
368
+ field = applyFieldConstraintDefaults(field, policy);
309
369
  const pages = schema.pages?.map((page, index) => ({
310
370
  ...page,
311
371
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
@@ -358,6 +418,12 @@ function useFormBuilder({
358
418
  return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
359
419
  if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
360
420
  return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
421
+ const constraint = policy?.fieldConstraints?.[field.type];
422
+ if (constraint !== void 0 && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length >= constraint.maxOptions)
423
+ return {
424
+ success: false,
425
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions }
426
+ };
361
427
  const ids = new Set(
362
428
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
363
429
  );
@@ -374,7 +440,7 @@ function useFormBuilder({
374
440
  });
375
441
  return { success: true };
376
442
  },
377
- [createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
443
+ [createId, factories.createOption, onChange, policy, policy?.maxOptionsPerField, schema]
378
444
  );
379
445
  const removeOption = (0, import_react.useCallback)(
380
446
  (fieldId, optionId) => {
@@ -384,6 +450,12 @@ function useFormBuilder({
384
450
  return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
385
451
  if (field.options.length <= 1)
386
452
  return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
453
+ const constraint = policy?.fieldConstraints?.[field.type];
454
+ if (constraint !== void 0 && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length <= constraint.minOptions)
455
+ return {
456
+ success: false,
457
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.minOptions }
458
+ };
387
459
  onChange({
388
460
  ...schema,
389
461
  fields: schema.fields.map(
@@ -392,7 +464,7 @@ function useFormBuilder({
392
464
  });
393
465
  return { success: true };
394
466
  },
395
- [onChange, schema]
467
+ [onChange, policy, schema]
396
468
  );
397
469
  const moveOption = (0, import_react.useCallback)(
398
470
  (fieldId, optionId, targetIndex) => {
@@ -423,7 +495,9 @@ function useFormBuilder({
423
495
  if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
424
496
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
425
497
  return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
426
- let transformed = (0, import_core.transformFieldType)(field, type);
498
+ let transformed = applyFieldConstraintDefaults((0, import_core.transformFieldType)(field, type), policy);
499
+ const constraintError = fieldConstraintError(transformed, policy);
500
+ if (constraintError !== void 0) return { success: false, error: constraintError };
427
501
  if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
428
502
  const ids = new Set(
429
503
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
@@ -438,7 +512,7 @@ function useFormBuilder({
438
512
  onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
439
513
  return { success: true };
440
514
  },
441
- [createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
515
+ [createId, factories.createOption, onChange, policy, policy?.allowedFieldTypes, schema]
442
516
  );
443
517
  const addPage = (0, import_react.useCallback)(
444
518
  (questionId) => {
@@ -1291,6 +1365,34 @@ var FIELD_TYPES = [
1291
1365
  "checkbox",
1292
1366
  "radio"
1293
1367
  ];
1368
+ var DEFAULT_FIELD_EDITOR_CONTROL_MODE = "editable";
1369
+ function resolveFieldEditorControls(config = {}) {
1370
+ return {
1371
+ title: config.title ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1372
+ description: config.description ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1373
+ required: config.required ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1374
+ typeSelect: config.typeSelect ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1375
+ options: config.options ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1376
+ displayConditions: config.displayConditions ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1377
+ textLimits: config.textLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1378
+ ratingBounds: config.ratingBounds ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1379
+ numberLimits: config.numberLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE
1380
+ };
1381
+ }
1382
+ function resolveFieldTypeSelectOptions(options, config, context) {
1383
+ let resolved = [...options];
1384
+ if (config?.transform !== void 0) resolved = [...config.transform(resolved, context)];
1385
+ if (config?.order !== void 0) {
1386
+ const ranks = new Map(config.order.map((type, index) => [type, index]));
1387
+ resolved.sort((left, right) => {
1388
+ const leftRank = ranks.get(left.value) ?? config.order?.length ?? 0;
1389
+ const rightRank = ranks.get(right.value) ?? config.order?.length ?? 0;
1390
+ return leftRank - rightRank;
1391
+ });
1392
+ }
1393
+ if (config?.sort !== void 0) resolved.sort((left, right) => config.sort?.(left, right, context) ?? 0);
1394
+ return resolved;
1395
+ }
1294
1396
  var BUILDER_DEFAULTS = {
1295
1397
  "builder.formBuilder": "Form builder",
1296
1398
  "builder.basicSettings": "Basic settings",
@@ -1309,6 +1411,10 @@ var BUILDER_DEFAULTS = {
1309
1411
  "builder.required": "Required",
1310
1412
  "builder.minimum": "Minimum",
1311
1413
  "builder.maximum": "Maximum",
1414
+ "builder.minimumLength": "Minimum length",
1415
+ "builder.maximumLength": "Maximum length",
1416
+ "builder.pattern": "Pattern",
1417
+ "builder.step": "Step",
1312
1418
  "builder.options": "Options",
1313
1419
  "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
1314
1420
  "builder.optionLabelPlaceholder": "Example: Very satisfied",
@@ -1566,6 +1672,8 @@ function FormBuilder({
1566
1672
  createManualTranslationMetadata,
1567
1673
  readOnly = false,
1568
1674
  features,
1675
+ fieldEditorControls,
1676
+ fieldTypeOptions,
1569
1677
  components: componentOverrides,
1570
1678
  slots,
1571
1679
  sectionOrder,
@@ -2330,6 +2438,7 @@ function FormBuilder({
2330
2438
  }
2331
2439
  ) : null }),
2332
2440
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
2441
+ const controls = resolveFieldEditorControls(fieldEditorControls);
2333
2442
  const condition = field.displayCondition;
2334
2443
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2335
2444
  const availableSources = schema.fields.slice(0, index);
@@ -2345,6 +2454,8 @@ function FormBuilder({
2345
2454
  ...slots === void 0 ? {} : { slots },
2346
2455
  ...policy === void 0 ? {} : { policy },
2347
2456
  ...features === void 0 ? {} : { features },
2457
+ ...fieldEditorControls === void 0 ? {} : { fieldEditorControls },
2458
+ ...fieldTypeOptions === void 0 ? {} : { fieldTypeOptions },
2348
2459
  readOnly,
2349
2460
  actions,
2350
2461
  components
@@ -2406,7 +2517,7 @@ function FormBuilder({
2406
2517
  }
2407
2518
  ) }),
2408
2519
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2409
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2520
+ controls.title === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2410
2521
  TextInput,
2411
2522
  {
2412
2523
  id: `builder-field-${field.id}-title`,
@@ -2418,34 +2529,56 @@ function FormBuilder({
2418
2529
  "aria-describedby": field.title.trim().length === 0 ? `builder-field-${field.id}-title-error` : void 0,
2419
2530
  value: field.title,
2420
2531
  placeholder: translate("builder.questionTitlePlaceholder"),
2532
+ readOnly: controls.title === "readOnly",
2421
2533
  onChange: (value) => updateField(field.id, (current) => ({
2422
2534
  ...current,
2423
2535
  title: value.trim().length === 0 ? current.title : value
2424
2536
  }))
2425
2537
  }
2426
2538
  ) }),
2427
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2539
+ controls.typeSelect === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2428
2540
  Select,
2429
2541
  {
2430
2542
  id: `builder-field-${field.id}-type`,
2431
2543
  label: translate("builder.type"),
2432
2544
  value: field.type,
2545
+ disabled: controls.typeSelect === "readOnly",
2433
2546
  onChange: (value) => changeType(field.id, value),
2434
- options: FIELD_TYPES.filter(
2435
- (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2436
- ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) }))
2547
+ options: resolveFieldTypeSelectOptions(
2548
+ FIELD_TYPES.filter(
2549
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2550
+ ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) })),
2551
+ fieldTypeOptions,
2552
+ { currentType: field.type, allowedTypes: policy?.allowedFieldTypes ?? FIELD_TYPES }
2553
+ )
2437
2554
  }
2438
2555
  ) }),
2439
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2556
+ controls.required === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2440
2557
  Checkbox,
2441
2558
  {
2442
2559
  className: builderClass("form-engine-builder__check"),
2443
2560
  checked: field.required === true,
2561
+ disabled: controls.required === "readOnly",
2444
2562
  onChange: (checked) => updateField(field.id, (current) => ({ ...current, required: checked })),
2445
2563
  label: translate("builder.required")
2446
2564
  }
2447
2565
  )
2448
2566
  ] }),
2567
+ controls.description === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2568
+ TextArea,
2569
+ {
2570
+ id: `builder-field-${field.id}-description`,
2571
+ name: `fields.${field.id}.description`,
2572
+ label: translate("builder.description"),
2573
+ value: field.description ?? "",
2574
+ readOnly: controls.description === "readOnly",
2575
+ onChange: (value) => updateField(field.id, (current) => {
2576
+ if (value.length > 0) return { ...current, description: value };
2577
+ const { description: _description, ...remaining } = current;
2578
+ return remaining;
2579
+ })
2580
+ }
2581
+ ) }),
2449
2582
  !pagesEnabled || schema.pages === void 0 ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2450
2583
  Select,
2451
2584
  {
@@ -2481,12 +2614,13 @@ function FormBuilder({
2481
2614
  })
2482
2615
  }
2483
2616
  ) }),
2484
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2617
+ controls.description === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2485
2618
  TextInput,
2486
2619
  {
2487
2620
  id: `builder-field-${field.id}-${editingLocale}-description`,
2488
2621
  label: translate("builder.pageDescription"),
2489
2622
  value: field.translations?.[editingLocale]?.description ?? "",
2623
+ readOnly: controls.description === "readOnly",
2490
2624
  onChange: (value) => updateManualTranslation({
2491
2625
  locale: editingLocale,
2492
2626
  kind: "field",
@@ -2521,7 +2655,7 @@ function FormBuilder({
2521
2655
  }
2522
2656
  ) }, option.id)) : null
2523
2657
  ] }),
2524
- field.type === "rating" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2658
+ field.type === "rating" && controls.ratingBounds !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2525
2659
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2526
2660
  TextInput,
2527
2661
  {
@@ -2529,6 +2663,7 @@ function FormBuilder({
2529
2663
  label: translate("builder.minimum"),
2530
2664
  type: "number",
2531
2665
  value: String(field.min ?? 1),
2666
+ readOnly: controls.ratingBounds === "readOnly",
2532
2667
  onChange: (value) => {
2533
2668
  const min = Number(value);
2534
2669
  if (!Number.isInteger(min)) return;
@@ -2546,6 +2681,7 @@ function FormBuilder({
2546
2681
  label: translate("builder.maximum"),
2547
2682
  type: "number",
2548
2683
  value: String(field.max ?? 5),
2684
+ readOnly: controls.ratingBounds === "readOnly",
2549
2685
  onChange: (value) => {
2550
2686
  const max = Number(value);
2551
2687
  if (!Number.isInteger(max)) return;
@@ -2557,7 +2693,73 @@ function FormBuilder({
2557
2693
  }
2558
2694
  ) })
2559
2695
  ] }) : null,
2560
- "options" in field ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__options"), children: [
2696
+ (field.type === "text" || field.type === "textarea") && controls.textLimits !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2697
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2698
+ TextInput,
2699
+ {
2700
+ id: `builder-field-${field.id}-min-length`,
2701
+ label: translate("builder.minimumLength"),
2702
+ type: "number",
2703
+ value: field.minLength === void 0 ? "" : String(field.minLength),
2704
+ readOnly: controls.textLimits === "readOnly",
2705
+ onChange: (value) => updateField(field.id, (current) => {
2706
+ if (current.type !== "text" && current.type !== "textarea") return current;
2707
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2708
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, minLength: parsed };
2709
+ })
2710
+ }
2711
+ ) }),
2712
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2713
+ TextInput,
2714
+ {
2715
+ id: `builder-field-${field.id}-max-length`,
2716
+ label: translate("builder.maximumLength"),
2717
+ type: "number",
2718
+ value: field.maxLength === void 0 ? "" : String(field.maxLength),
2719
+ readOnly: controls.textLimits === "readOnly",
2720
+ onChange: (value) => updateField(field.id, (current) => {
2721
+ if (current.type !== "text" && current.type !== "textarea") return current;
2722
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2723
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, maxLength: parsed };
2724
+ })
2725
+ }
2726
+ ) }),
2727
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2728
+ TextInput,
2729
+ {
2730
+ id: `builder-field-${field.id}-pattern`,
2731
+ label: translate("builder.pattern"),
2732
+ value: field.pattern ?? "",
2733
+ readOnly: controls.textLimits === "readOnly",
2734
+ onChange: (value) => updateField(field.id, (current) => {
2735
+ if (current.type !== "text" && current.type !== "textarea") return current;
2736
+ return value.length === 0 ? current : { ...current, pattern: value };
2737
+ })
2738
+ }
2739
+ ) })
2740
+ ] }) : null,
2741
+ field.type === "number" && controls.numberLimits !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__grid"), children: ["min", "max", "step"].map((property) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2742
+ TextInput,
2743
+ {
2744
+ id: `builder-field-${field.id}-${property}`,
2745
+ label: translate(
2746
+ property === "step" ? "builder.step" : property === "min" ? "builder.minimum" : "builder.maximum"
2747
+ ),
2748
+ type: "number",
2749
+ value: field[property] === void 0 ? "" : String(field[property]),
2750
+ readOnly: controls.numberLimits === "readOnly",
2751
+ onChange: (value) => updateField(field.id, (current) => {
2752
+ if (current.type !== "number") return current;
2753
+ if (value.trim().length === 0) {
2754
+ const { [property]: _removed, ...remaining } = current;
2755
+ return remaining;
2756
+ }
2757
+ const parsed = Number(value);
2758
+ return Number.isFinite(parsed) ? { ...current, [property]: parsed } : current;
2759
+ })
2760
+ }
2761
+ ) }, property)) }) : null,
2762
+ "options" in field && controls.options !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__options"), children: [
2561
2763
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: translate("builder.options") }),
2562
2764
  field.options.map(
2563
2765
  (option, optionIndex) => OptionEditorSlot === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__option"), children: [
@@ -2568,6 +2770,7 @@ function FormBuilder({
2568
2770
  label: translate("builder.optionLabel", { index: optionIndex + 1 }),
2569
2771
  value: option.label,
2570
2772
  placeholder: translate("builder.optionLabelPlaceholder"),
2773
+ readOnly: controls.options === "readOnly",
2571
2774
  onChange: (value) => value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, value)
2572
2775
  }
2573
2776
  ),
@@ -2577,7 +2780,7 @@ function FormBuilder({
2577
2780
  {
2578
2781
  actionType: "moveUp",
2579
2782
  title: translate("builder.moveUp", { title: option.label }),
2580
- disabled: optionIndex === 0,
2783
+ disabled: controls.options === "readOnly" || optionIndex === 0,
2581
2784
  onClick: () => moveOption(field.id, option.id, optionIndex - 1)
2582
2785
  }
2583
2786
  ),
@@ -2586,7 +2789,7 @@ function FormBuilder({
2586
2789
  {
2587
2790
  actionType: "moveDown",
2588
2791
  title: translate("builder.moveDown", { title: option.label }),
2589
- disabled: optionIndex === field.options.length - 1,
2792
+ disabled: controls.options === "readOnly" || optionIndex === field.options.length - 1,
2590
2793
  onClick: () => moveOption(field.id, option.id, optionIndex + 1)
2591
2794
  }
2592
2795
  ),
@@ -2594,7 +2797,7 @@ function FormBuilder({
2594
2797
  IconButton,
2595
2798
  {
2596
2799
  actionType: "delete",
2597
- disabled: field.options.length === 1,
2800
+ disabled: controls.options === "readOnly" || field.options.length === 1,
2598
2801
  onClick: () => removeOption(field.id, option.id),
2599
2802
  title: translate("builder.remove")
2600
2803
  }
@@ -2612,7 +2815,7 @@ function FormBuilder({
2612
2815
  onMoveUp: () => moveOption(field.id, option.id, optionIndex - 1),
2613
2816
  onMoveDown: () => moveOption(field.id, option.id, optionIndex + 1),
2614
2817
  onRemove: () => removeOption(field.id, option.id),
2615
- readOnly,
2818
+ readOnly: readOnly || controls.options === "readOnly",
2616
2819
  actions,
2617
2820
  components
2618
2821
  }
@@ -2626,7 +2829,7 @@ function FormBuilder({
2626
2829
  index: optionIndex,
2627
2830
  currentLocale: editingLocale,
2628
2831
  translate,
2629
- readOnly,
2832
+ readOnly: readOnly || controls.options === "readOnly",
2630
2833
  actions,
2631
2834
  components
2632
2835
  },
@@ -2638,13 +2841,13 @@ function FormBuilder({
2638
2841
  {
2639
2842
  action: "addOption",
2640
2843
  targetId: field.id,
2641
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2844
+ disabled: controls.options === "readOnly" || policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2642
2845
  onClick: () => addOption(field.id),
2643
2846
  children: translate("builder.addOption")
2644
2847
  }
2645
2848
  )
2646
2849
  ] }) : null,
2647
- conditionsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__condition"), children: [
2850
+ conditionsEnabled && controls.displayConditions !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__condition"), children: [
2648
2851
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2649
2852
  Select,
2650
2853
  {
@@ -4001,6 +4204,8 @@ function FormRenderer(props) {
4001
4204
  createLocalStorageSubmissionAttemptStore,
4002
4205
  createLocalStorageSubmissionReceiptStore,
4003
4206
  isTranslationUnresolved,
4207
+ resolveFieldEditorControls,
4208
+ resolveFieldTypeSelectOptions,
4004
4209
  resolveInitialFieldType,
4005
4210
  resolveTranslation,
4006
4211
  submissionReceiptQueryKey,
package/dist/index.d.cts CHANGED
@@ -60,6 +60,12 @@ type BuilderActionError = {
60
60
  } | {
61
61
  readonly type: "max_locales_exceeded";
62
62
  readonly max: number;
63
+ } | {
64
+ readonly type: "field_constraint_immutable";
65
+ } | {
66
+ readonly type: "field_constraint_violation";
67
+ readonly property: string;
68
+ readonly expected: number;
63
69
  } | {
64
70
  readonly type: "node_not_found";
65
71
  readonly kind: BuilderTextTarget["kind"];
@@ -192,6 +198,32 @@ interface BuilderSelectOption<T extends string = string> {
192
198
  readonly kind?: string;
193
199
  readonly metadata?: Readonly<Record<string, unknown>>;
194
200
  }
201
+ type FieldPropertyControlMode = "editable" | "readOnly" | "hidden";
202
+ interface FieldEditorControlsConfig {
203
+ readonly title?: FieldPropertyControlMode;
204
+ readonly description?: FieldPropertyControlMode;
205
+ readonly required?: FieldPropertyControlMode;
206
+ readonly typeSelect?: FieldPropertyControlMode;
207
+ readonly options?: FieldPropertyControlMode;
208
+ readonly displayConditions?: FieldPropertyControlMode;
209
+ readonly textLimits?: FieldPropertyControlMode;
210
+ readonly ratingBounds?: FieldPropertyControlMode;
211
+ readonly numberLimits?: FieldPropertyControlMode;
212
+ }
213
+ interface FieldTypeSelectOptionsContext {
214
+ readonly currentType: QuestionType;
215
+ readonly allowedTypes: readonly QuestionType[];
216
+ }
217
+ type FieldTypeSelectOptionsTransformer = (options: readonly BuilderSelectOption<QuestionType>[], context: FieldTypeSelectOptionsContext) => readonly BuilderSelectOption<QuestionType>[];
218
+ type FieldTypeSelectOptionsSorter = (left: BuilderSelectOption<QuestionType>, right: BuilderSelectOption<QuestionType>, context: FieldTypeSelectOptionsContext) => number;
219
+ interface FieldTypeSelectOptionsConfig {
220
+ /** Transform the generated choices. The returned array is used as-is. */
221
+ readonly transform?: FieldTypeSelectOptionsTransformer;
222
+ /** Sort generated choices after transform. The source array is never mutated. */
223
+ readonly sort?: FieldTypeSelectOptionsSorter;
224
+ /** Optional explicit order applied before `sort`. */
225
+ readonly order?: readonly QuestionType[];
226
+ }
195
227
  interface SelectComponentProps<T extends string = string> extends Omit<InputComponentProps, "value" | "onChange"> {
196
228
  readonly value: T;
197
229
  readonly onChange: (value: T) => void;
@@ -278,6 +310,8 @@ interface BuilderFieldEditorSlotProps extends BuilderSlotBaseProps {
278
310
  readonly localization?: boolean;
279
311
  readonly conditions?: boolean;
280
312
  };
313
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
314
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
281
315
  readonly slots?: Pick<FormBuilderSlots, "fieldTypeSelect" | "fieldEditorHeader">;
282
316
  }
283
317
  interface FieldTypeSelectSlotProps {
@@ -524,6 +558,8 @@ interface SubmissionProtectionProps {
524
558
  }
525
559
  type BeforeSubmit = (values: Readonly<Record<string, unknown>>) => "continue" | "cancel" | Promise<"continue" | "cancel">;
526
560
 
561
+ declare function resolveFieldEditorControls(config?: FieldEditorControlsConfig): Required<FieldEditorControlsConfig>;
562
+ declare function resolveFieldTypeSelectOptions(options: readonly BuilderSelectOption<QuestionType>[], config: FieldTypeSelectOptionsConfig | undefined, context: FieldTypeSelectOptionsContext): readonly BuilderSelectOption<QuestionType>[];
527
563
  declare function resolveInitialFieldType(defaultType?: QuestionType, allowedTypes?: readonly QuestionType[]): QuestionType | null;
528
564
  interface FormBuilderFeatures {
529
565
  readonly pages?: boolean;
@@ -547,13 +583,15 @@ interface FormBuilderProps {
547
583
  readonly createManualTranslationMetadata?: (context: ManualTranslationContext) => Readonly<Record<string, JsonValue>> | undefined;
548
584
  readonly readOnly?: boolean;
549
585
  readonly features?: FormBuilderFeatures;
586
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
587
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
550
588
  readonly components?: FormBuilderComponents;
551
589
  readonly slots?: FormBuilderSlots;
552
590
  readonly sectionOrder?: readonly FormBuilderSectionName[];
553
591
  readonly disableDefaultStyles?: boolean;
554
592
  readonly unstyled?: boolean;
555
593
  }
556
- declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
594
+ declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, fieldEditorControls, fieldTypeOptions, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
557
595
 
558
596
  type SubmitStatus = "idle" | "submitting" | "success" | "error";
559
597
  interface FormContextValue {
@@ -659,4 +697,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
659
697
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
660
698
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
661
699
 
662
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorHeaderSlotProps, type FieldState, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
700
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.d.ts CHANGED
@@ -60,6 +60,12 @@ type BuilderActionError = {
60
60
  } | {
61
61
  readonly type: "max_locales_exceeded";
62
62
  readonly max: number;
63
+ } | {
64
+ readonly type: "field_constraint_immutable";
65
+ } | {
66
+ readonly type: "field_constraint_violation";
67
+ readonly property: string;
68
+ readonly expected: number;
63
69
  } | {
64
70
  readonly type: "node_not_found";
65
71
  readonly kind: BuilderTextTarget["kind"];
@@ -192,6 +198,32 @@ interface BuilderSelectOption<T extends string = string> {
192
198
  readonly kind?: string;
193
199
  readonly metadata?: Readonly<Record<string, unknown>>;
194
200
  }
201
+ type FieldPropertyControlMode = "editable" | "readOnly" | "hidden";
202
+ interface FieldEditorControlsConfig {
203
+ readonly title?: FieldPropertyControlMode;
204
+ readonly description?: FieldPropertyControlMode;
205
+ readonly required?: FieldPropertyControlMode;
206
+ readonly typeSelect?: FieldPropertyControlMode;
207
+ readonly options?: FieldPropertyControlMode;
208
+ readonly displayConditions?: FieldPropertyControlMode;
209
+ readonly textLimits?: FieldPropertyControlMode;
210
+ readonly ratingBounds?: FieldPropertyControlMode;
211
+ readonly numberLimits?: FieldPropertyControlMode;
212
+ }
213
+ interface FieldTypeSelectOptionsContext {
214
+ readonly currentType: QuestionType;
215
+ readonly allowedTypes: readonly QuestionType[];
216
+ }
217
+ type FieldTypeSelectOptionsTransformer = (options: readonly BuilderSelectOption<QuestionType>[], context: FieldTypeSelectOptionsContext) => readonly BuilderSelectOption<QuestionType>[];
218
+ type FieldTypeSelectOptionsSorter = (left: BuilderSelectOption<QuestionType>, right: BuilderSelectOption<QuestionType>, context: FieldTypeSelectOptionsContext) => number;
219
+ interface FieldTypeSelectOptionsConfig {
220
+ /** Transform the generated choices. The returned array is used as-is. */
221
+ readonly transform?: FieldTypeSelectOptionsTransformer;
222
+ /** Sort generated choices after transform. The source array is never mutated. */
223
+ readonly sort?: FieldTypeSelectOptionsSorter;
224
+ /** Optional explicit order applied before `sort`. */
225
+ readonly order?: readonly QuestionType[];
226
+ }
195
227
  interface SelectComponentProps<T extends string = string> extends Omit<InputComponentProps, "value" | "onChange"> {
196
228
  readonly value: T;
197
229
  readonly onChange: (value: T) => void;
@@ -278,6 +310,8 @@ interface BuilderFieldEditorSlotProps extends BuilderSlotBaseProps {
278
310
  readonly localization?: boolean;
279
311
  readonly conditions?: boolean;
280
312
  };
313
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
314
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
281
315
  readonly slots?: Pick<FormBuilderSlots, "fieldTypeSelect" | "fieldEditorHeader">;
282
316
  }
283
317
  interface FieldTypeSelectSlotProps {
@@ -524,6 +558,8 @@ interface SubmissionProtectionProps {
524
558
  }
525
559
  type BeforeSubmit = (values: Readonly<Record<string, unknown>>) => "continue" | "cancel" | Promise<"continue" | "cancel">;
526
560
 
561
+ declare function resolveFieldEditorControls(config?: FieldEditorControlsConfig): Required<FieldEditorControlsConfig>;
562
+ declare function resolveFieldTypeSelectOptions(options: readonly BuilderSelectOption<QuestionType>[], config: FieldTypeSelectOptionsConfig | undefined, context: FieldTypeSelectOptionsContext): readonly BuilderSelectOption<QuestionType>[];
527
563
  declare function resolveInitialFieldType(defaultType?: QuestionType, allowedTypes?: readonly QuestionType[]): QuestionType | null;
528
564
  interface FormBuilderFeatures {
529
565
  readonly pages?: boolean;
@@ -547,13 +583,15 @@ interface FormBuilderProps {
547
583
  readonly createManualTranslationMetadata?: (context: ManualTranslationContext) => Readonly<Record<string, JsonValue>> | undefined;
548
584
  readonly readOnly?: boolean;
549
585
  readonly features?: FormBuilderFeatures;
586
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
587
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
550
588
  readonly components?: FormBuilderComponents;
551
589
  readonly slots?: FormBuilderSlots;
552
590
  readonly sectionOrder?: readonly FormBuilderSectionName[];
553
591
  readonly disableDefaultStyles?: boolean;
554
592
  readonly unstyled?: boolean;
555
593
  }
556
- declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
594
+ declare function FormBuilder({ schema, onChange, locale, translator, translationAdapter, translationOptions, onTranslationReport, policy, idFactory, factories, className, defaultFieldType, onActionError, createManualTranslationMetadata, readOnly, features, fieldEditorControls, fieldTypeOptions, components: componentOverrides, slots, sectionOrder, disableDefaultStyles, unstyled }: FormBuilderProps): react.JSX.Element;
557
595
 
558
596
  type SubmitStatus = "idle" | "submitting" | "success" | "error";
559
597
  interface FormContextValue {
@@ -659,4 +697,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
659
697
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
660
698
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
661
699
 
662
- export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorHeaderSlotProps, type FieldState, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
700
+ export { BUILDER_TRANSLATION_ALIASES, BUILDER_TRANSLATION_KEYS, type BeforeSubmit, type BuilderActionContext, type BuilderActionError, type BuilderActionIconType, type BuilderActionResult, type BuilderButtonProps, type BuilderCheckboxProps, type BuilderErrorMessageProps, type BuilderFactories, type BuilderFieldEditorSlotProps, type BuilderFieldsetProps, type BuilderIconButtonProps, type BuilderIdKind, type BuilderLocalizationSlotProps, type BuilderOptionEditorSlotProps, type BuilderPagesSlotProps, type BuilderPolicy, type BuilderSectionProps, type BuilderSelectOption, type BuilderSelectProps, type BuilderSlotActions, type BuilderTextAreaProps, type BuilderTextInputProps, type BuilderTextTarget, type BuilderToolbarSlotProps, type BuilderTranslationActionsSlotProps, type BuilderTranslationKey, type ComponentBaseProps, type FieldComponentProps, type FieldComponents, type FieldEditorControlsConfig, type FieldEditorHeaderSlotProps, type FieldPropertyControlMode, type FieldState, type FieldTypeSelectOptionsConfig, type FieldTypeSelectOptionsContext, type FieldTypeSelectOptionsSorter, type FieldTypeSelectOptionsTransformer, type FieldTypeSelectSlotProps, FormBuilder, type FormBuilderActions, type FormBuilderComponents, type FormBuilderFeatures, type FormBuilderOptions, type FormBuilderProps, type FormBuilderResult, type FormBuilderSectionName, type FormBuilderSlots, type FormCompletionSlotProps, type FormContextValue, type FormFieldsSlotProps, FormProvider, type FormProviderProps, FormRenderer, type FormRendererMessages, type FormRendererPresentationProps, type FormRendererProps, type FormRendererSlots, type FormServerErrorPayload, FormSubmissionError, type FormSubmitHandler, type FormSubmitState, type FormSubmitStatus, type FormSubmittedAnswerItem, type FormSuccessRenderMode, type IconButtonProps, type InputComponentProps, type LocalizationSummaryContext, type ManualTranslationContext, type ManualTranslationTarget, type RenderSubmitButtonProps, type SelectComponentProps, type StandaloneFormRendererProps, type SubmissionAttempt, type SubmissionAttemptStore, type SubmissionConfirmationRenderMode, type SubmissionConfirmationSlotProps, type SubmissionGuard, type SubmissionGuardResult, type SubmissionProtectionProps, type SubmissionReceipt, type SubmissionReceiptQuery, type SubmissionReceiptStore, type SubmitContext, type SubmitResponse, type SubmitResult, type SubmitStatus, type UseSubmissionReceiptsResult, createLocalStorageSubmissionAttemptStore, createLocalStorageSubmissionReceiptStore, isTranslationUnresolved, resolveFieldEditorControls, resolveFieldTypeSelectOptions, resolveInitialFieldType, resolveTranslation, submissionReceiptQueryKey, useField, useForm, useFormBuilder, useSubmissionReceipts };
package/dist/index.js CHANGED
@@ -133,6 +133,61 @@ function defaultCreateOption(field, id) {
133
133
  function defaultCreatePage(id, questionIds) {
134
134
  return { id, title: "New page", questionIds };
135
135
  }
136
+ function applyFieldConstraintDefaults(field, policy) {
137
+ const constraint = policy?.fieldConstraints?.[field.type];
138
+ if (constraint === void 0) return field;
139
+ const required = constraint.fixedRequired ?? constraint.defaultRequired ?? field.required;
140
+ if (field.type === "rating") {
141
+ const ratingConstraint = "defaultMin" in constraint || "defaultMax" in constraint || "fixedMin" in constraint || "fixedMax" in constraint ? constraint : void 0;
142
+ const min = ratingConstraint !== void 0 && "fixedMin" in ratingConstraint ? ratingConstraint.fixedMin : ratingConstraint !== void 0 && "defaultMin" in ratingConstraint ? ratingConstraint.defaultMin : field.min;
143
+ const max = ratingConstraint !== void 0 && "fixedMax" in ratingConstraint ? ratingConstraint.fixedMax : ratingConstraint !== void 0 && "defaultMax" in ratingConstraint ? ratingConstraint.defaultMax : field.max;
144
+ return {
145
+ ...field,
146
+ required,
147
+ ...min === void 0 ? {} : { min },
148
+ ...max === void 0 ? {} : { max }
149
+ };
150
+ }
151
+ if ((field.type === "text" || field.type === "textarea") && "defaultMaxLength" in constraint) {
152
+ return {
153
+ ...field,
154
+ required,
155
+ ...constraint.defaultMaxLength === void 0 ? {} : { maxLength: constraint.defaultMaxLength }
156
+ };
157
+ }
158
+ return { ...field, required };
159
+ }
160
+ function fieldConstraintError(updated, policy) {
161
+ const constraint = policy?.fieldConstraints?.[updated.type];
162
+ if (constraint === void 0) return void 0;
163
+ if (constraint.fixedRequired !== void 0 && updated.required !== constraint.fixedRequired) {
164
+ return { type: "field_constraint_immutable" };
165
+ }
166
+ if (updated.type === "rating") {
167
+ if ("fixedMin" in constraint && constraint.fixedMin !== void 0 && updated.min !== constraint.fixedMin) {
168
+ return { type: "field_constraint_immutable" };
169
+ }
170
+ if ("fixedMax" in constraint && constraint.fixedMax !== void 0 && updated.max !== constraint.fixedMax) {
171
+ return { type: "field_constraint_immutable" };
172
+ }
173
+ if ("allowedMinRange" in constraint && constraint.allowedMinRange !== void 0 && typeof updated.min === "number" && (updated.min < constraint.allowedMinRange[0] || updated.min > constraint.allowedMinRange[1])) {
174
+ return { type: "field_constraint_violation", property: "min", expected: constraint.allowedMinRange[0] };
175
+ }
176
+ if ("allowedMaxRange" in constraint && constraint.allowedMaxRange !== void 0 && typeof updated.max === "number" && (updated.max < constraint.allowedMaxRange[0] || updated.max > constraint.allowedMaxRange[1])) {
177
+ return { type: "field_constraint_violation", property: "max", expected: constraint.allowedMaxRange[1] };
178
+ }
179
+ }
180
+ if ((updated.type === "text" || updated.type === "textarea") && "maxMaxLength" in constraint && constraint.maxMaxLength !== void 0 && updated.maxLength !== void 0 && updated.maxLength > constraint.maxMaxLength) {
181
+ return { type: "field_constraint_violation", property: "maxLength", expected: constraint.maxMaxLength };
182
+ }
183
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "minOptions" in constraint && constraint.minOptions !== void 0 && updated.options.length < constraint.minOptions) {
184
+ return { type: "field_constraint_violation", property: "options", expected: constraint.minOptions };
185
+ }
186
+ if ((updated.type === "select" || updated.type === "radio" || updated.type === "multi-select") && "maxOptions" in constraint && constraint.maxOptions !== void 0 && updated.options.length > constraint.maxOptions) {
187
+ return { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions };
188
+ }
189
+ return void 0;
190
+ }
136
191
  function move(items, sourceIndex, targetIndex) {
137
192
  if (sourceIndex < 0 || targetIndex < 0 || targetIndex >= items.length || sourceIndex === targetIndex)
138
193
  return void 0;
@@ -188,6 +243,8 @@ function useFormBuilder({
188
243
  const updated = updater(current);
189
244
  if (updated.id !== fieldId)
190
245
  return { success: false, error: { type: "invalid_id", kind: "field", id: updated.id } };
246
+ const constraintError = fieldConstraintError(updated, policy);
247
+ if (constraintError !== void 0) return { success: false, error: constraintError };
191
248
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(updated.type))
192
249
  return { success: false, error: { type: "disallowed_field_type", fieldType: updated.type } };
193
250
  for (const text of [updated.title, updated.description]) {
@@ -199,7 +256,7 @@ function useFormBuilder({
199
256
  onChange({ ...schema, fields: schema.fields.map((field) => field.id === fieldId ? updated : field) });
200
257
  return { success: true };
201
258
  },
202
- [onChange, policy?.allowedFieldTypes, schema, textPolicyError]
259
+ [onChange, policy, schema, textPolicyError]
203
260
  );
204
261
  const updateOption = useCallback(
205
262
  (fieldId, optionId, updater) => {
@@ -272,6 +329,7 @@ function useFormBuilder({
272
329
  return { success: false, error: { type: "invalid_id", kind: "option", id: option.id } };
273
330
  field = { ...field, options: [option] };
274
331
  }
332
+ field = applyFieldConstraintDefaults(field, policy);
275
333
  const pages = schema.pages?.map((page, index) => ({
276
334
  ...page,
277
335
  questionIds: page.id === pageId || pageId === void 0 && index === (schema.pages?.length ?? 0) - 1 ? [...page.questionIds, field.id] : page.questionIds
@@ -324,6 +382,12 @@ function useFormBuilder({
324
382
  return { success: false, error: { type: "invalid_operation", message: `Field ${fieldId} has no options.` } };
325
383
  if (policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField)
326
384
  return { success: false, error: { type: "max_options_exceeded", max: policy.maxOptionsPerField } };
385
+ const constraint = policy?.fieldConstraints?.[field.type];
386
+ if (constraint !== void 0 && "maxOptions" in constraint && constraint.maxOptions !== void 0 && field.options.length >= constraint.maxOptions)
387
+ return {
388
+ success: false,
389
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.maxOptions }
390
+ };
327
391
  const ids = new Set(
328
392
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
329
393
  );
@@ -340,7 +404,7 @@ function useFormBuilder({
340
404
  });
341
405
  return { success: true };
342
406
  },
343
- [createId, factories.createOption, onChange, policy?.maxOptionsPerField, schema]
407
+ [createId, factories.createOption, onChange, policy, policy?.maxOptionsPerField, schema]
344
408
  );
345
409
  const removeOption = useCallback(
346
410
  (fieldId, optionId) => {
@@ -350,6 +414,12 @@ function useFormBuilder({
350
414
  return { success: false, error: { type: "node_not_found", kind: "option", id: optionId } };
351
415
  if (field.options.length <= 1)
352
416
  return { success: false, error: { type: "invalid_operation", message: "A choice field needs one option." } };
417
+ const constraint = policy?.fieldConstraints?.[field.type];
418
+ if (constraint !== void 0 && "minOptions" in constraint && constraint.minOptions !== void 0 && field.options.length <= constraint.minOptions)
419
+ return {
420
+ success: false,
421
+ error: { type: "field_constraint_violation", property: "options", expected: constraint.minOptions }
422
+ };
353
423
  onChange({
354
424
  ...schema,
355
425
  fields: schema.fields.map(
@@ -358,7 +428,7 @@ function useFormBuilder({
358
428
  });
359
429
  return { success: true };
360
430
  },
361
- [onChange, schema]
431
+ [onChange, policy, schema]
362
432
  );
363
433
  const moveOption = useCallback(
364
434
  (fieldId, optionId, targetIndex) => {
@@ -389,7 +459,9 @@ function useFormBuilder({
389
459
  if (field === void 0) return { success: false, error: { type: "node_not_found", kind: "field", id: fieldId } };
390
460
  if (policy?.allowedFieldTypes !== void 0 && !policy.allowedFieldTypes.includes(type))
391
461
  return { success: false, error: { type: "disallowed_field_type", fieldType: type } };
392
- let transformed = transformFieldType(field, type);
462
+ let transformed = applyFieldConstraintDefaults(transformFieldType(field, type), policy);
463
+ const constraintError = fieldConstraintError(transformed, policy);
464
+ if (constraintError !== void 0) return { success: false, error: constraintError };
393
465
  if (CHOICE_TYPES.includes(type) && !("options" in field) && "options" in transformed) {
394
466
  const ids = new Set(
395
467
  schema.fields.flatMap((item) => "options" in item ? item.options.map((option2) => option2.id) : [])
@@ -404,7 +476,7 @@ function useFormBuilder({
404
476
  onChange({ ...schema, fields: schema.fields.map((item) => item.id === fieldId ? transformed : item) });
405
477
  return { success: true };
406
478
  },
407
- [createId, factories.createOption, onChange, policy?.allowedFieldTypes, schema]
479
+ [createId, factories.createOption, onChange, policy, policy?.allowedFieldTypes, schema]
408
480
  );
409
481
  const addPage = useCallback(
410
482
  (questionId) => {
@@ -1257,6 +1329,34 @@ var FIELD_TYPES = [
1257
1329
  "checkbox",
1258
1330
  "radio"
1259
1331
  ];
1332
+ var DEFAULT_FIELD_EDITOR_CONTROL_MODE = "editable";
1333
+ function resolveFieldEditorControls(config = {}) {
1334
+ return {
1335
+ title: config.title ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1336
+ description: config.description ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1337
+ required: config.required ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1338
+ typeSelect: config.typeSelect ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1339
+ options: config.options ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1340
+ displayConditions: config.displayConditions ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1341
+ textLimits: config.textLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1342
+ ratingBounds: config.ratingBounds ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1343
+ numberLimits: config.numberLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE
1344
+ };
1345
+ }
1346
+ function resolveFieldTypeSelectOptions(options, config, context) {
1347
+ let resolved = [...options];
1348
+ if (config?.transform !== void 0) resolved = [...config.transform(resolved, context)];
1349
+ if (config?.order !== void 0) {
1350
+ const ranks = new Map(config.order.map((type, index) => [type, index]));
1351
+ resolved.sort((left, right) => {
1352
+ const leftRank = ranks.get(left.value) ?? config.order?.length ?? 0;
1353
+ const rightRank = ranks.get(right.value) ?? config.order?.length ?? 0;
1354
+ return leftRank - rightRank;
1355
+ });
1356
+ }
1357
+ if (config?.sort !== void 0) resolved.sort((left, right) => config.sort?.(left, right, context) ?? 0);
1358
+ return resolved;
1359
+ }
1260
1360
  var BUILDER_DEFAULTS = {
1261
1361
  "builder.formBuilder": "Form builder",
1262
1362
  "builder.basicSettings": "Basic settings",
@@ -1275,6 +1375,10 @@ var BUILDER_DEFAULTS = {
1275
1375
  "builder.required": "Required",
1276
1376
  "builder.minimum": "Minimum",
1277
1377
  "builder.maximum": "Maximum",
1378
+ "builder.minimumLength": "Minimum length",
1379
+ "builder.maximumLength": "Maximum length",
1380
+ "builder.pattern": "Pattern",
1381
+ "builder.step": "Step",
1278
1382
  "builder.options": "Options",
1279
1383
  "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
1280
1384
  "builder.optionLabelPlaceholder": "Example: Very satisfied",
@@ -1532,6 +1636,8 @@ function FormBuilder({
1532
1636
  createManualTranslationMetadata,
1533
1637
  readOnly = false,
1534
1638
  features,
1639
+ fieldEditorControls,
1640
+ fieldTypeOptions,
1535
1641
  components: componentOverrides,
1536
1642
  slots,
1537
1643
  sectionOrder,
@@ -2296,6 +2402,7 @@ function FormBuilder({
2296
2402
  }
2297
2403
  ) : null }),
2298
2404
  /* @__PURE__ */ jsx(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
2405
+ const controls = resolveFieldEditorControls(fieldEditorControls);
2299
2406
  const condition = field.displayCondition;
2300
2407
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2301
2408
  const availableSources = schema.fields.slice(0, index);
@@ -2311,6 +2418,8 @@ function FormBuilder({
2311
2418
  ...slots === void 0 ? {} : { slots },
2312
2419
  ...policy === void 0 ? {} : { policy },
2313
2420
  ...features === void 0 ? {} : { features },
2421
+ ...fieldEditorControls === void 0 ? {} : { fieldEditorControls },
2422
+ ...fieldTypeOptions === void 0 ? {} : { fieldTypeOptions },
2314
2423
  readOnly,
2315
2424
  actions,
2316
2425
  components
@@ -2372,7 +2481,7 @@ function FormBuilder({
2372
2481
  }
2373
2482
  ) }),
2374
2483
  /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2375
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2484
+ controls.title === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2376
2485
  TextInput,
2377
2486
  {
2378
2487
  id: `builder-field-${field.id}-title`,
@@ -2384,34 +2493,56 @@ function FormBuilder({
2384
2493
  "aria-describedby": field.title.trim().length === 0 ? `builder-field-${field.id}-title-error` : void 0,
2385
2494
  value: field.title,
2386
2495
  placeholder: translate("builder.questionTitlePlaceholder"),
2496
+ readOnly: controls.title === "readOnly",
2387
2497
  onChange: (value) => updateField(field.id, (current) => ({
2388
2498
  ...current,
2389
2499
  title: value.trim().length === 0 ? current.title : value
2390
2500
  }))
2391
2501
  }
2392
2502
  ) }),
2393
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2503
+ controls.typeSelect === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2394
2504
  Select,
2395
2505
  {
2396
2506
  id: `builder-field-${field.id}-type`,
2397
2507
  label: translate("builder.type"),
2398
2508
  value: field.type,
2509
+ disabled: controls.typeSelect === "readOnly",
2399
2510
  onChange: (value) => changeType(field.id, value),
2400
- options: FIELD_TYPES.filter(
2401
- (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2402
- ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) }))
2511
+ options: resolveFieldTypeSelectOptions(
2512
+ FIELD_TYPES.filter(
2513
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2514
+ ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) })),
2515
+ fieldTypeOptions,
2516
+ { currentType: field.type, allowedTypes: policy?.allowedFieldTypes ?? FIELD_TYPES }
2517
+ )
2403
2518
  }
2404
2519
  ) }),
2405
- /* @__PURE__ */ jsx(
2520
+ controls.required === "hidden" ? null : /* @__PURE__ */ jsx(
2406
2521
  Checkbox,
2407
2522
  {
2408
2523
  className: builderClass("form-engine-builder__check"),
2409
2524
  checked: field.required === true,
2525
+ disabled: controls.required === "readOnly",
2410
2526
  onChange: (checked) => updateField(field.id, (current) => ({ ...current, required: checked })),
2411
2527
  label: translate("builder.required")
2412
2528
  }
2413
2529
  )
2414
2530
  ] }),
2531
+ controls.description === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2532
+ TextArea,
2533
+ {
2534
+ id: `builder-field-${field.id}-description`,
2535
+ name: `fields.${field.id}.description`,
2536
+ label: translate("builder.description"),
2537
+ value: field.description ?? "",
2538
+ readOnly: controls.description === "readOnly",
2539
+ onChange: (value) => updateField(field.id, (current) => {
2540
+ if (value.length > 0) return { ...current, description: value };
2541
+ const { description: _description, ...remaining } = current;
2542
+ return remaining;
2543
+ })
2544
+ }
2545
+ ) }),
2415
2546
  !pagesEnabled || schema.pages === void 0 ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2416
2547
  Select,
2417
2548
  {
@@ -2447,12 +2578,13 @@ function FormBuilder({
2447
2578
  })
2448
2579
  }
2449
2580
  ) }),
2450
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2581
+ controls.description === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2451
2582
  TextInput,
2452
2583
  {
2453
2584
  id: `builder-field-${field.id}-${editingLocale}-description`,
2454
2585
  label: translate("builder.pageDescription"),
2455
2586
  value: field.translations?.[editingLocale]?.description ?? "",
2587
+ readOnly: controls.description === "readOnly",
2456
2588
  onChange: (value) => updateManualTranslation({
2457
2589
  locale: editingLocale,
2458
2590
  kind: "field",
@@ -2487,7 +2619,7 @@ function FormBuilder({
2487
2619
  }
2488
2620
  ) }, option.id)) : null
2489
2621
  ] }),
2490
- field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2622
+ field.type === "rating" && controls.ratingBounds !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2491
2623
  /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2492
2624
  TextInput,
2493
2625
  {
@@ -2495,6 +2627,7 @@ function FormBuilder({
2495
2627
  label: translate("builder.minimum"),
2496
2628
  type: "number",
2497
2629
  value: String(field.min ?? 1),
2630
+ readOnly: controls.ratingBounds === "readOnly",
2498
2631
  onChange: (value) => {
2499
2632
  const min = Number(value);
2500
2633
  if (!Number.isInteger(min)) return;
@@ -2512,6 +2645,7 @@ function FormBuilder({
2512
2645
  label: translate("builder.maximum"),
2513
2646
  type: "number",
2514
2647
  value: String(field.max ?? 5),
2648
+ readOnly: controls.ratingBounds === "readOnly",
2515
2649
  onChange: (value) => {
2516
2650
  const max = Number(value);
2517
2651
  if (!Number.isInteger(max)) return;
@@ -2523,7 +2657,73 @@ function FormBuilder({
2523
2657
  }
2524
2658
  ) })
2525
2659
  ] }) : null,
2526
- "options" in field ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__options"), children: [
2660
+ (field.type === "text" || field.type === "textarea") && controls.textLimits !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2661
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2662
+ TextInput,
2663
+ {
2664
+ id: `builder-field-${field.id}-min-length`,
2665
+ label: translate("builder.minimumLength"),
2666
+ type: "number",
2667
+ value: field.minLength === void 0 ? "" : String(field.minLength),
2668
+ readOnly: controls.textLimits === "readOnly",
2669
+ onChange: (value) => updateField(field.id, (current) => {
2670
+ if (current.type !== "text" && current.type !== "textarea") return current;
2671
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2672
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, minLength: parsed };
2673
+ })
2674
+ }
2675
+ ) }),
2676
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2677
+ TextInput,
2678
+ {
2679
+ id: `builder-field-${field.id}-max-length`,
2680
+ label: translate("builder.maximumLength"),
2681
+ type: "number",
2682
+ value: field.maxLength === void 0 ? "" : String(field.maxLength),
2683
+ readOnly: controls.textLimits === "readOnly",
2684
+ onChange: (value) => updateField(field.id, (current) => {
2685
+ if (current.type !== "text" && current.type !== "textarea") return current;
2686
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2687
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, maxLength: parsed };
2688
+ })
2689
+ }
2690
+ ) }),
2691
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2692
+ TextInput,
2693
+ {
2694
+ id: `builder-field-${field.id}-pattern`,
2695
+ label: translate("builder.pattern"),
2696
+ value: field.pattern ?? "",
2697
+ readOnly: controls.textLimits === "readOnly",
2698
+ onChange: (value) => updateField(field.id, (current) => {
2699
+ if (current.type !== "text" && current.type !== "textarea") return current;
2700
+ return value.length === 0 ? current : { ...current, pattern: value };
2701
+ })
2702
+ }
2703
+ ) })
2704
+ ] }) : null,
2705
+ field.type === "number" && controls.numberLimits !== "hidden" ? /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__grid"), children: ["min", "max", "step"].map((property) => /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2706
+ TextInput,
2707
+ {
2708
+ id: `builder-field-${field.id}-${property}`,
2709
+ label: translate(
2710
+ property === "step" ? "builder.step" : property === "min" ? "builder.minimum" : "builder.maximum"
2711
+ ),
2712
+ type: "number",
2713
+ value: field[property] === void 0 ? "" : String(field[property]),
2714
+ readOnly: controls.numberLimits === "readOnly",
2715
+ onChange: (value) => updateField(field.id, (current) => {
2716
+ if (current.type !== "number") return current;
2717
+ if (value.trim().length === 0) {
2718
+ const { [property]: _removed, ...remaining } = current;
2719
+ return remaining;
2720
+ }
2721
+ const parsed = Number(value);
2722
+ return Number.isFinite(parsed) ? { ...current, [property]: parsed } : current;
2723
+ })
2724
+ }
2725
+ ) }, property)) }) : null,
2726
+ "options" in field && controls.options !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__options"), children: [
2527
2727
  /* @__PURE__ */ jsx("strong", { children: translate("builder.options") }),
2528
2728
  field.options.map(
2529
2729
  (option, optionIndex) => OptionEditorSlot === void 0 ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__option"), children: [
@@ -2534,6 +2734,7 @@ function FormBuilder({
2534
2734
  label: translate("builder.optionLabel", { index: optionIndex + 1 }),
2535
2735
  value: option.label,
2536
2736
  placeholder: translate("builder.optionLabelPlaceholder"),
2737
+ readOnly: controls.options === "readOnly",
2537
2738
  onChange: (value) => value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, value)
2538
2739
  }
2539
2740
  ),
@@ -2543,7 +2744,7 @@ function FormBuilder({
2543
2744
  {
2544
2745
  actionType: "moveUp",
2545
2746
  title: translate("builder.moveUp", { title: option.label }),
2546
- disabled: optionIndex === 0,
2747
+ disabled: controls.options === "readOnly" || optionIndex === 0,
2547
2748
  onClick: () => moveOption(field.id, option.id, optionIndex - 1)
2548
2749
  }
2549
2750
  ),
@@ -2552,7 +2753,7 @@ function FormBuilder({
2552
2753
  {
2553
2754
  actionType: "moveDown",
2554
2755
  title: translate("builder.moveDown", { title: option.label }),
2555
- disabled: optionIndex === field.options.length - 1,
2756
+ disabled: controls.options === "readOnly" || optionIndex === field.options.length - 1,
2556
2757
  onClick: () => moveOption(field.id, option.id, optionIndex + 1)
2557
2758
  }
2558
2759
  ),
@@ -2560,7 +2761,7 @@ function FormBuilder({
2560
2761
  IconButton,
2561
2762
  {
2562
2763
  actionType: "delete",
2563
- disabled: field.options.length === 1,
2764
+ disabled: controls.options === "readOnly" || field.options.length === 1,
2564
2765
  onClick: () => removeOption(field.id, option.id),
2565
2766
  title: translate("builder.remove")
2566
2767
  }
@@ -2578,7 +2779,7 @@ function FormBuilder({
2578
2779
  onMoveUp: () => moveOption(field.id, option.id, optionIndex - 1),
2579
2780
  onMoveDown: () => moveOption(field.id, option.id, optionIndex + 1),
2580
2781
  onRemove: () => removeOption(field.id, option.id),
2581
- readOnly,
2782
+ readOnly: readOnly || controls.options === "readOnly",
2582
2783
  actions,
2583
2784
  components
2584
2785
  }
@@ -2592,7 +2793,7 @@ function FormBuilder({
2592
2793
  index: optionIndex,
2593
2794
  currentLocale: editingLocale,
2594
2795
  translate,
2595
- readOnly,
2796
+ readOnly: readOnly || controls.options === "readOnly",
2596
2797
  actions,
2597
2798
  components
2598
2799
  },
@@ -2604,13 +2805,13 @@ function FormBuilder({
2604
2805
  {
2605
2806
  action: "addOption",
2606
2807
  targetId: field.id,
2607
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2808
+ disabled: controls.options === "readOnly" || policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2608
2809
  onClick: () => addOption(field.id),
2609
2810
  children: translate("builder.addOption")
2610
2811
  }
2611
2812
  )
2612
2813
  ] }) : null,
2613
- conditionsEnabled ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__condition"), children: [
2814
+ conditionsEnabled && controls.displayConditions !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__condition"), children: [
2614
2815
  /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2615
2816
  Select,
2616
2817
  {
@@ -3985,6 +4186,8 @@ export {
3985
4186
  createLocalStorageSubmissionAttemptStore,
3986
4187
  createLocalStorageSubmissionReceiptStore,
3987
4188
  isTranslationUnresolved,
4189
+ resolveFieldEditorControls,
4190
+ resolveFieldTypeSelectOptions,
3988
4191
  resolveInitialFieldType,
3989
4192
  resolveTranslation,
3990
4193
  submissionReceiptQueryKey,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/react",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -42,8 +42,8 @@
42
42
  "typescript"
43
43
  ],
44
44
  "dependencies": {
45
- "@form-engine-ts/core": "4.0.0",
46
- "@form-engine-ts/privacy": "4.0.0"
45
+ "@form-engine-ts/core": "4.2.0",
46
+ "@form-engine-ts/privacy": "4.2.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",