@form-engine-ts/react 4.0.0 → 4.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 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,
@@ -1291,6 +1293,34 @@ var FIELD_TYPES = [
1291
1293
  "checkbox",
1292
1294
  "radio"
1293
1295
  ];
1296
+ var DEFAULT_FIELD_EDITOR_CONTROL_MODE = "editable";
1297
+ function resolveFieldEditorControls(config = {}) {
1298
+ return {
1299
+ title: config.title ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1300
+ description: config.description ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1301
+ required: config.required ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1302
+ typeSelect: config.typeSelect ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1303
+ options: config.options ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1304
+ displayConditions: config.displayConditions ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1305
+ textLimits: config.textLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1306
+ ratingBounds: config.ratingBounds ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1307
+ numberLimits: config.numberLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE
1308
+ };
1309
+ }
1310
+ function resolveFieldTypeSelectOptions(options, config, context) {
1311
+ let resolved = [...options];
1312
+ if (config?.transform !== void 0) resolved = [...config.transform(resolved, context)];
1313
+ if (config?.order !== void 0) {
1314
+ const ranks = new Map(config.order.map((type, index) => [type, index]));
1315
+ resolved.sort((left, right) => {
1316
+ const leftRank = ranks.get(left.value) ?? config.order?.length ?? 0;
1317
+ const rightRank = ranks.get(right.value) ?? config.order?.length ?? 0;
1318
+ return leftRank - rightRank;
1319
+ });
1320
+ }
1321
+ if (config?.sort !== void 0) resolved.sort((left, right) => config.sort?.(left, right, context) ?? 0);
1322
+ return resolved;
1323
+ }
1294
1324
  var BUILDER_DEFAULTS = {
1295
1325
  "builder.formBuilder": "Form builder",
1296
1326
  "builder.basicSettings": "Basic settings",
@@ -1309,6 +1339,10 @@ var BUILDER_DEFAULTS = {
1309
1339
  "builder.required": "Required",
1310
1340
  "builder.minimum": "Minimum",
1311
1341
  "builder.maximum": "Maximum",
1342
+ "builder.minimumLength": "Minimum length",
1343
+ "builder.maximumLength": "Maximum length",
1344
+ "builder.pattern": "Pattern",
1345
+ "builder.step": "Step",
1312
1346
  "builder.options": "Options",
1313
1347
  "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
1314
1348
  "builder.optionLabelPlaceholder": "Example: Very satisfied",
@@ -1566,6 +1600,8 @@ function FormBuilder({
1566
1600
  createManualTranslationMetadata,
1567
1601
  readOnly = false,
1568
1602
  features,
1603
+ fieldEditorControls,
1604
+ fieldTypeOptions,
1569
1605
  components: componentOverrides,
1570
1606
  slots,
1571
1607
  sectionOrder,
@@ -2330,6 +2366,7 @@ function FormBuilder({
2330
2366
  }
2331
2367
  ) : null }),
2332
2368
  /* @__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) => {
2369
+ const controls = resolveFieldEditorControls(fieldEditorControls);
2333
2370
  const condition = field.displayCondition;
2334
2371
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2335
2372
  const availableSources = schema.fields.slice(0, index);
@@ -2345,6 +2382,8 @@ function FormBuilder({
2345
2382
  ...slots === void 0 ? {} : { slots },
2346
2383
  ...policy === void 0 ? {} : { policy },
2347
2384
  ...features === void 0 ? {} : { features },
2385
+ ...fieldEditorControls === void 0 ? {} : { fieldEditorControls },
2386
+ ...fieldTypeOptions === void 0 ? {} : { fieldTypeOptions },
2348
2387
  readOnly,
2349
2388
  actions,
2350
2389
  components
@@ -2406,7 +2445,7 @@ function FormBuilder({
2406
2445
  }
2407
2446
  ) }),
2408
2447
  /* @__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)(
2448
+ 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
2449
  TextInput,
2411
2450
  {
2412
2451
  id: `builder-field-${field.id}-title`,
@@ -2418,34 +2457,56 @@ function FormBuilder({
2418
2457
  "aria-describedby": field.title.trim().length === 0 ? `builder-field-${field.id}-title-error` : void 0,
2419
2458
  value: field.title,
2420
2459
  placeholder: translate("builder.questionTitlePlaceholder"),
2460
+ readOnly: controls.title === "readOnly",
2421
2461
  onChange: (value) => updateField(field.id, (current) => ({
2422
2462
  ...current,
2423
2463
  title: value.trim().length === 0 ? current.title : value
2424
2464
  }))
2425
2465
  }
2426
2466
  ) }),
2427
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2467
+ 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
2468
  Select,
2429
2469
  {
2430
2470
  id: `builder-field-${field.id}-type`,
2431
2471
  label: translate("builder.type"),
2432
2472
  value: field.type,
2473
+ disabled: controls.typeSelect === "readOnly",
2433
2474
  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)) }))
2475
+ options: resolveFieldTypeSelectOptions(
2476
+ FIELD_TYPES.filter(
2477
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2478
+ ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) })),
2479
+ fieldTypeOptions,
2480
+ { currentType: field.type, allowedTypes: policy?.allowedFieldTypes ?? FIELD_TYPES }
2481
+ )
2437
2482
  }
2438
2483
  ) }),
2439
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2484
+ controls.required === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2440
2485
  Checkbox,
2441
2486
  {
2442
2487
  className: builderClass("form-engine-builder__check"),
2443
2488
  checked: field.required === true,
2489
+ disabled: controls.required === "readOnly",
2444
2490
  onChange: (checked) => updateField(field.id, (current) => ({ ...current, required: checked })),
2445
2491
  label: translate("builder.required")
2446
2492
  }
2447
2493
  )
2448
2494
  ] }),
2495
+ controls.description === "hidden" ? null : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2496
+ TextArea,
2497
+ {
2498
+ id: `builder-field-${field.id}-description`,
2499
+ name: `fields.${field.id}.description`,
2500
+ label: translate("builder.description"),
2501
+ value: field.description ?? "",
2502
+ readOnly: controls.description === "readOnly",
2503
+ onChange: (value) => updateField(field.id, (current) => {
2504
+ if (value.length > 0) return { ...current, description: value };
2505
+ const { description: _description, ...remaining } = current;
2506
+ return remaining;
2507
+ })
2508
+ }
2509
+ ) }),
2449
2510
  !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
2511
  Select,
2451
2512
  {
@@ -2481,12 +2542,13 @@ function FormBuilder({
2481
2542
  })
2482
2543
  }
2483
2544
  ) }),
2484
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2545
+ 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
2546
  TextInput,
2486
2547
  {
2487
2548
  id: `builder-field-${field.id}-${editingLocale}-description`,
2488
2549
  label: translate("builder.pageDescription"),
2489
2550
  value: field.translations?.[editingLocale]?.description ?? "",
2551
+ readOnly: controls.description === "readOnly",
2490
2552
  onChange: (value) => updateManualTranslation({
2491
2553
  locale: editingLocale,
2492
2554
  kind: "field",
@@ -2521,7 +2583,7 @@ function FormBuilder({
2521
2583
  }
2522
2584
  ) }, option.id)) : null
2523
2585
  ] }),
2524
- field.type === "rating" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2586
+ field.type === "rating" && controls.ratingBounds !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2525
2587
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2526
2588
  TextInput,
2527
2589
  {
@@ -2529,6 +2591,7 @@ function FormBuilder({
2529
2591
  label: translate("builder.minimum"),
2530
2592
  type: "number",
2531
2593
  value: String(field.min ?? 1),
2594
+ readOnly: controls.ratingBounds === "readOnly",
2532
2595
  onChange: (value) => {
2533
2596
  const min = Number(value);
2534
2597
  if (!Number.isInteger(min)) return;
@@ -2546,6 +2609,7 @@ function FormBuilder({
2546
2609
  label: translate("builder.maximum"),
2547
2610
  type: "number",
2548
2611
  value: String(field.max ?? 5),
2612
+ readOnly: controls.ratingBounds === "readOnly",
2549
2613
  onChange: (value) => {
2550
2614
  const max = Number(value);
2551
2615
  if (!Number.isInteger(max)) return;
@@ -2557,7 +2621,73 @@ function FormBuilder({
2557
2621
  }
2558
2622
  ) })
2559
2623
  ] }) : null,
2560
- "options" in field ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__options"), children: [
2624
+ (field.type === "text" || field.type === "textarea") && controls.textLimits !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__grid"), children: [
2625
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2626
+ TextInput,
2627
+ {
2628
+ id: `builder-field-${field.id}-min-length`,
2629
+ label: translate("builder.minimumLength"),
2630
+ type: "number",
2631
+ value: field.minLength === void 0 ? "" : String(field.minLength),
2632
+ readOnly: controls.textLimits === "readOnly",
2633
+ onChange: (value) => updateField(field.id, (current) => {
2634
+ if (current.type !== "text" && current.type !== "textarea") return current;
2635
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2636
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, minLength: parsed };
2637
+ })
2638
+ }
2639
+ ) }),
2640
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2641
+ TextInput,
2642
+ {
2643
+ id: `builder-field-${field.id}-max-length`,
2644
+ label: translate("builder.maximumLength"),
2645
+ type: "number",
2646
+ value: field.maxLength === void 0 ? "" : String(field.maxLength),
2647
+ readOnly: controls.textLimits === "readOnly",
2648
+ onChange: (value) => updateField(field.id, (current) => {
2649
+ if (current.type !== "text" && current.type !== "textarea") return current;
2650
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2651
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, maxLength: parsed };
2652
+ })
2653
+ }
2654
+ ) }),
2655
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2656
+ TextInput,
2657
+ {
2658
+ id: `builder-field-${field.id}-pattern`,
2659
+ label: translate("builder.pattern"),
2660
+ value: field.pattern ?? "",
2661
+ readOnly: controls.textLimits === "readOnly",
2662
+ onChange: (value) => updateField(field.id, (current) => {
2663
+ if (current.type !== "text" && current.type !== "textarea") return current;
2664
+ return value.length === 0 ? current : { ...current, pattern: value };
2665
+ })
2666
+ }
2667
+ ) })
2668
+ ] }) : null,
2669
+ 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)(
2670
+ TextInput,
2671
+ {
2672
+ id: `builder-field-${field.id}-${property}`,
2673
+ label: translate(
2674
+ property === "step" ? "builder.step" : property === "min" ? "builder.minimum" : "builder.maximum"
2675
+ ),
2676
+ type: "number",
2677
+ value: field[property] === void 0 ? "" : String(field[property]),
2678
+ readOnly: controls.numberLimits === "readOnly",
2679
+ onChange: (value) => updateField(field.id, (current) => {
2680
+ if (current.type !== "number") return current;
2681
+ if (value.trim().length === 0) {
2682
+ const { [property]: _removed, ...remaining } = current;
2683
+ return remaining;
2684
+ }
2685
+ const parsed = Number(value);
2686
+ return Number.isFinite(parsed) ? { ...current, [property]: parsed } : current;
2687
+ })
2688
+ }
2689
+ ) }, property)) }) : null,
2690
+ "options" in field && controls.options !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__options"), children: [
2561
2691
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { children: translate("builder.options") }),
2562
2692
  field.options.map(
2563
2693
  (option, optionIndex) => OptionEditorSlot === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__option"), children: [
@@ -2568,6 +2698,7 @@ function FormBuilder({
2568
2698
  label: translate("builder.optionLabel", { index: optionIndex + 1 }),
2569
2699
  value: option.label,
2570
2700
  placeholder: translate("builder.optionLabelPlaceholder"),
2701
+ readOnly: controls.options === "readOnly",
2571
2702
  onChange: (value) => value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, value)
2572
2703
  }
2573
2704
  ),
@@ -2577,7 +2708,7 @@ function FormBuilder({
2577
2708
  {
2578
2709
  actionType: "moveUp",
2579
2710
  title: translate("builder.moveUp", { title: option.label }),
2580
- disabled: optionIndex === 0,
2711
+ disabled: controls.options === "readOnly" || optionIndex === 0,
2581
2712
  onClick: () => moveOption(field.id, option.id, optionIndex - 1)
2582
2713
  }
2583
2714
  ),
@@ -2586,7 +2717,7 @@ function FormBuilder({
2586
2717
  {
2587
2718
  actionType: "moveDown",
2588
2719
  title: translate("builder.moveDown", { title: option.label }),
2589
- disabled: optionIndex === field.options.length - 1,
2720
+ disabled: controls.options === "readOnly" || optionIndex === field.options.length - 1,
2590
2721
  onClick: () => moveOption(field.id, option.id, optionIndex + 1)
2591
2722
  }
2592
2723
  ),
@@ -2594,7 +2725,7 @@ function FormBuilder({
2594
2725
  IconButton,
2595
2726
  {
2596
2727
  actionType: "delete",
2597
- disabled: field.options.length === 1,
2728
+ disabled: controls.options === "readOnly" || field.options.length === 1,
2598
2729
  onClick: () => removeOption(field.id, option.id),
2599
2730
  title: translate("builder.remove")
2600
2731
  }
@@ -2612,7 +2743,7 @@ function FormBuilder({
2612
2743
  onMoveUp: () => moveOption(field.id, option.id, optionIndex - 1),
2613
2744
  onMoveDown: () => moveOption(field.id, option.id, optionIndex + 1),
2614
2745
  onRemove: () => removeOption(field.id, option.id),
2615
- readOnly,
2746
+ readOnly: readOnly || controls.options === "readOnly",
2616
2747
  actions,
2617
2748
  components
2618
2749
  }
@@ -2626,7 +2757,7 @@ function FormBuilder({
2626
2757
  index: optionIndex,
2627
2758
  currentLocale: editingLocale,
2628
2759
  translate,
2629
- readOnly,
2760
+ readOnly: readOnly || controls.options === "readOnly",
2630
2761
  actions,
2631
2762
  components
2632
2763
  },
@@ -2638,13 +2769,13 @@ function FormBuilder({
2638
2769
  {
2639
2770
  action: "addOption",
2640
2771
  targetId: field.id,
2641
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2772
+ disabled: controls.options === "readOnly" || policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2642
2773
  onClick: () => addOption(field.id),
2643
2774
  children: translate("builder.addOption")
2644
2775
  }
2645
2776
  )
2646
2777
  ] }) : null,
2647
- conditionsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__condition"), children: [
2778
+ conditionsEnabled && controls.displayConditions !== "hidden" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: builderClass("form-engine-builder__condition"), children: [
2648
2779
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2649
2780
  Select,
2650
2781
  {
@@ -4001,6 +4132,8 @@ function FormRenderer(props) {
4001
4132
  createLocalStorageSubmissionAttemptStore,
4002
4133
  createLocalStorageSubmissionReceiptStore,
4003
4134
  isTranslationUnresolved,
4135
+ resolveFieldEditorControls,
4136
+ resolveFieldTypeSelectOptions,
4004
4137
  resolveInitialFieldType,
4005
4138
  resolveTranslation,
4006
4139
  submissionReceiptQueryKey,
package/dist/index.d.cts CHANGED
@@ -192,6 +192,32 @@ interface BuilderSelectOption<T extends string = string> {
192
192
  readonly kind?: string;
193
193
  readonly metadata?: Readonly<Record<string, unknown>>;
194
194
  }
195
+ type FieldPropertyControlMode = "editable" | "readOnly" | "hidden";
196
+ interface FieldEditorControlsConfig {
197
+ readonly title?: FieldPropertyControlMode;
198
+ readonly description?: FieldPropertyControlMode;
199
+ readonly required?: FieldPropertyControlMode;
200
+ readonly typeSelect?: FieldPropertyControlMode;
201
+ readonly options?: FieldPropertyControlMode;
202
+ readonly displayConditions?: FieldPropertyControlMode;
203
+ readonly textLimits?: FieldPropertyControlMode;
204
+ readonly ratingBounds?: FieldPropertyControlMode;
205
+ readonly numberLimits?: FieldPropertyControlMode;
206
+ }
207
+ interface FieldTypeSelectOptionsContext {
208
+ readonly currentType: QuestionType;
209
+ readonly allowedTypes: readonly QuestionType[];
210
+ }
211
+ type FieldTypeSelectOptionsTransformer = (options: readonly BuilderSelectOption<QuestionType>[], context: FieldTypeSelectOptionsContext) => readonly BuilderSelectOption<QuestionType>[];
212
+ type FieldTypeSelectOptionsSorter = (left: BuilderSelectOption<QuestionType>, right: BuilderSelectOption<QuestionType>, context: FieldTypeSelectOptionsContext) => number;
213
+ interface FieldTypeSelectOptionsConfig {
214
+ /** Transform the generated choices. The returned array is used as-is. */
215
+ readonly transform?: FieldTypeSelectOptionsTransformer;
216
+ /** Sort generated choices after transform. The source array is never mutated. */
217
+ readonly sort?: FieldTypeSelectOptionsSorter;
218
+ /** Optional explicit order applied before `sort`. */
219
+ readonly order?: readonly QuestionType[];
220
+ }
195
221
  interface SelectComponentProps<T extends string = string> extends Omit<InputComponentProps, "value" | "onChange"> {
196
222
  readonly value: T;
197
223
  readonly onChange: (value: T) => void;
@@ -278,6 +304,8 @@ interface BuilderFieldEditorSlotProps extends BuilderSlotBaseProps {
278
304
  readonly localization?: boolean;
279
305
  readonly conditions?: boolean;
280
306
  };
307
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
308
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
281
309
  readonly slots?: Pick<FormBuilderSlots, "fieldTypeSelect" | "fieldEditorHeader">;
282
310
  }
283
311
  interface FieldTypeSelectSlotProps {
@@ -524,6 +552,8 @@ interface SubmissionProtectionProps {
524
552
  }
525
553
  type BeforeSubmit = (values: Readonly<Record<string, unknown>>) => "continue" | "cancel" | Promise<"continue" | "cancel">;
526
554
 
555
+ declare function resolveFieldEditorControls(config?: FieldEditorControlsConfig): Required<FieldEditorControlsConfig>;
556
+ declare function resolveFieldTypeSelectOptions(options: readonly BuilderSelectOption<QuestionType>[], config: FieldTypeSelectOptionsConfig | undefined, context: FieldTypeSelectOptionsContext): readonly BuilderSelectOption<QuestionType>[];
527
557
  declare function resolveInitialFieldType(defaultType?: QuestionType, allowedTypes?: readonly QuestionType[]): QuestionType | null;
528
558
  interface FormBuilderFeatures {
529
559
  readonly pages?: boolean;
@@ -547,13 +577,15 @@ interface FormBuilderProps {
547
577
  readonly createManualTranslationMetadata?: (context: ManualTranslationContext) => Readonly<Record<string, JsonValue>> | undefined;
548
578
  readonly readOnly?: boolean;
549
579
  readonly features?: FormBuilderFeatures;
580
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
581
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
550
582
  readonly components?: FormBuilderComponents;
551
583
  readonly slots?: FormBuilderSlots;
552
584
  readonly sectionOrder?: readonly FormBuilderSectionName[];
553
585
  readonly disableDefaultStyles?: boolean;
554
586
  readonly unstyled?: boolean;
555
587
  }
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;
588
+ 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
589
 
558
590
  type SubmitStatus = "idle" | "submitting" | "success" | "error";
559
591
  interface FormContextValue {
@@ -659,4 +691,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
659
691
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
660
692
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
661
693
 
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 };
694
+ 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
@@ -192,6 +192,32 @@ interface BuilderSelectOption<T extends string = string> {
192
192
  readonly kind?: string;
193
193
  readonly metadata?: Readonly<Record<string, unknown>>;
194
194
  }
195
+ type FieldPropertyControlMode = "editable" | "readOnly" | "hidden";
196
+ interface FieldEditorControlsConfig {
197
+ readonly title?: FieldPropertyControlMode;
198
+ readonly description?: FieldPropertyControlMode;
199
+ readonly required?: FieldPropertyControlMode;
200
+ readonly typeSelect?: FieldPropertyControlMode;
201
+ readonly options?: FieldPropertyControlMode;
202
+ readonly displayConditions?: FieldPropertyControlMode;
203
+ readonly textLimits?: FieldPropertyControlMode;
204
+ readonly ratingBounds?: FieldPropertyControlMode;
205
+ readonly numberLimits?: FieldPropertyControlMode;
206
+ }
207
+ interface FieldTypeSelectOptionsContext {
208
+ readonly currentType: QuestionType;
209
+ readonly allowedTypes: readonly QuestionType[];
210
+ }
211
+ type FieldTypeSelectOptionsTransformer = (options: readonly BuilderSelectOption<QuestionType>[], context: FieldTypeSelectOptionsContext) => readonly BuilderSelectOption<QuestionType>[];
212
+ type FieldTypeSelectOptionsSorter = (left: BuilderSelectOption<QuestionType>, right: BuilderSelectOption<QuestionType>, context: FieldTypeSelectOptionsContext) => number;
213
+ interface FieldTypeSelectOptionsConfig {
214
+ /** Transform the generated choices. The returned array is used as-is. */
215
+ readonly transform?: FieldTypeSelectOptionsTransformer;
216
+ /** Sort generated choices after transform. The source array is never mutated. */
217
+ readonly sort?: FieldTypeSelectOptionsSorter;
218
+ /** Optional explicit order applied before `sort`. */
219
+ readonly order?: readonly QuestionType[];
220
+ }
195
221
  interface SelectComponentProps<T extends string = string> extends Omit<InputComponentProps, "value" | "onChange"> {
196
222
  readonly value: T;
197
223
  readonly onChange: (value: T) => void;
@@ -278,6 +304,8 @@ interface BuilderFieldEditorSlotProps extends BuilderSlotBaseProps {
278
304
  readonly localization?: boolean;
279
305
  readonly conditions?: boolean;
280
306
  };
307
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
308
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
281
309
  readonly slots?: Pick<FormBuilderSlots, "fieldTypeSelect" | "fieldEditorHeader">;
282
310
  }
283
311
  interface FieldTypeSelectSlotProps {
@@ -524,6 +552,8 @@ interface SubmissionProtectionProps {
524
552
  }
525
553
  type BeforeSubmit = (values: Readonly<Record<string, unknown>>) => "continue" | "cancel" | Promise<"continue" | "cancel">;
526
554
 
555
+ declare function resolveFieldEditorControls(config?: FieldEditorControlsConfig): Required<FieldEditorControlsConfig>;
556
+ declare function resolveFieldTypeSelectOptions(options: readonly BuilderSelectOption<QuestionType>[], config: FieldTypeSelectOptionsConfig | undefined, context: FieldTypeSelectOptionsContext): readonly BuilderSelectOption<QuestionType>[];
527
557
  declare function resolveInitialFieldType(defaultType?: QuestionType, allowedTypes?: readonly QuestionType[]): QuestionType | null;
528
558
  interface FormBuilderFeatures {
529
559
  readonly pages?: boolean;
@@ -547,13 +577,15 @@ interface FormBuilderProps {
547
577
  readonly createManualTranslationMetadata?: (context: ManualTranslationContext) => Readonly<Record<string, JsonValue>> | undefined;
548
578
  readonly readOnly?: boolean;
549
579
  readonly features?: FormBuilderFeatures;
580
+ readonly fieldEditorControls?: FieldEditorControlsConfig;
581
+ readonly fieldTypeOptions?: FieldTypeSelectOptionsConfig;
550
582
  readonly components?: FormBuilderComponents;
551
583
  readonly slots?: FormBuilderSlots;
552
584
  readonly sectionOrder?: readonly FormBuilderSectionName[];
553
585
  readonly disableDefaultStyles?: boolean;
554
586
  readonly unstyled?: boolean;
555
587
  }
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;
588
+ 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
589
 
558
590
  type SubmitStatus = "idle" | "submitting" | "success" | "error";
559
591
  interface FormContextValue {
@@ -659,4 +691,4 @@ interface StandaloneFormRendererProps extends FormRendererPresentationProps {
659
691
  type FormRendererProps = FormRendererPresentationProps | StandaloneFormRendererProps;
660
692
  declare function FormRenderer(props: FormRendererProps): react.JSX.Element;
661
693
 
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 };
694
+ 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
@@ -1257,6 +1257,34 @@ var FIELD_TYPES = [
1257
1257
  "checkbox",
1258
1258
  "radio"
1259
1259
  ];
1260
+ var DEFAULT_FIELD_EDITOR_CONTROL_MODE = "editable";
1261
+ function resolveFieldEditorControls(config = {}) {
1262
+ return {
1263
+ title: config.title ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1264
+ description: config.description ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1265
+ required: config.required ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1266
+ typeSelect: config.typeSelect ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1267
+ options: config.options ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1268
+ displayConditions: config.displayConditions ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1269
+ textLimits: config.textLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1270
+ ratingBounds: config.ratingBounds ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE,
1271
+ numberLimits: config.numberLimits ?? DEFAULT_FIELD_EDITOR_CONTROL_MODE
1272
+ };
1273
+ }
1274
+ function resolveFieldTypeSelectOptions(options, config, context) {
1275
+ let resolved = [...options];
1276
+ if (config?.transform !== void 0) resolved = [...config.transform(resolved, context)];
1277
+ if (config?.order !== void 0) {
1278
+ const ranks = new Map(config.order.map((type, index) => [type, index]));
1279
+ resolved.sort((left, right) => {
1280
+ const leftRank = ranks.get(left.value) ?? config.order?.length ?? 0;
1281
+ const rightRank = ranks.get(right.value) ?? config.order?.length ?? 0;
1282
+ return leftRank - rightRank;
1283
+ });
1284
+ }
1285
+ if (config?.sort !== void 0) resolved.sort((left, right) => config.sort?.(left, right, context) ?? 0);
1286
+ return resolved;
1287
+ }
1260
1288
  var BUILDER_DEFAULTS = {
1261
1289
  "builder.formBuilder": "Form builder",
1262
1290
  "builder.basicSettings": "Basic settings",
@@ -1275,6 +1303,10 @@ var BUILDER_DEFAULTS = {
1275
1303
  "builder.required": "Required",
1276
1304
  "builder.minimum": "Minimum",
1277
1305
  "builder.maximum": "Maximum",
1306
+ "builder.minimumLength": "Minimum length",
1307
+ "builder.maximumLength": "Maximum length",
1308
+ "builder.pattern": "Pattern",
1309
+ "builder.step": "Step",
1278
1310
  "builder.options": "Options",
1279
1311
  "builder.optionLabel": "\u9078\u629E\u80A2 / Option Label {{index}}",
1280
1312
  "builder.optionLabelPlaceholder": "Example: Very satisfied",
@@ -1532,6 +1564,8 @@ function FormBuilder({
1532
1564
  createManualTranslationMetadata,
1533
1565
  readOnly = false,
1534
1566
  features,
1567
+ fieldEditorControls,
1568
+ fieldTypeOptions,
1535
1569
  components: componentOverrides,
1536
1570
  slots,
1537
1571
  sectionOrder,
@@ -2296,6 +2330,7 @@ function FormBuilder({
2296
2330
  }
2297
2331
  ) : null }),
2298
2332
  /* @__PURE__ */ jsx(BuilderSectionGroup, { name: "questions", children: /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__list"), children: schema.fields.map((field, index) => {
2333
+ const controls = resolveFieldEditorControls(fieldEditorControls);
2299
2334
  const condition = field.displayCondition;
2300
2335
  const source = condition === void 0 ? void 0 : schema.fields.find((item) => item.id === condition.questionId);
2301
2336
  const availableSources = schema.fields.slice(0, index);
@@ -2311,6 +2346,8 @@ function FormBuilder({
2311
2346
  ...slots === void 0 ? {} : { slots },
2312
2347
  ...policy === void 0 ? {} : { policy },
2313
2348
  ...features === void 0 ? {} : { features },
2349
+ ...fieldEditorControls === void 0 ? {} : { fieldEditorControls },
2350
+ ...fieldTypeOptions === void 0 ? {} : { fieldTypeOptions },
2314
2351
  readOnly,
2315
2352
  actions,
2316
2353
  components
@@ -2372,7 +2409,7 @@ function FormBuilder({
2372
2409
  }
2373
2410
  ) }),
2374
2411
  /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2375
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2412
+ controls.title === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2376
2413
  TextInput,
2377
2414
  {
2378
2415
  id: `builder-field-${field.id}-title`,
@@ -2384,34 +2421,56 @@ function FormBuilder({
2384
2421
  "aria-describedby": field.title.trim().length === 0 ? `builder-field-${field.id}-title-error` : void 0,
2385
2422
  value: field.title,
2386
2423
  placeholder: translate("builder.questionTitlePlaceholder"),
2424
+ readOnly: controls.title === "readOnly",
2387
2425
  onChange: (value) => updateField(field.id, (current) => ({
2388
2426
  ...current,
2389
2427
  title: value.trim().length === 0 ? current.title : value
2390
2428
  }))
2391
2429
  }
2392
2430
  ) }),
2393
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2431
+ controls.typeSelect === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2394
2432
  Select,
2395
2433
  {
2396
2434
  id: `builder-field-${field.id}-type`,
2397
2435
  label: translate("builder.type"),
2398
2436
  value: field.type,
2437
+ disabled: controls.typeSelect === "readOnly",
2399
2438
  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)) }))
2439
+ options: resolveFieldTypeSelectOptions(
2440
+ FIELD_TYPES.filter(
2441
+ (type) => policy?.allowedFieldTypes === void 0 || policy.allowedFieldTypes.includes(type)
2442
+ ).map((type) => ({ value: type, label: translate(fieldTypeKey(type)) })),
2443
+ fieldTypeOptions,
2444
+ { currentType: field.type, allowedTypes: policy?.allowedFieldTypes ?? FIELD_TYPES }
2445
+ )
2403
2446
  }
2404
2447
  ) }),
2405
- /* @__PURE__ */ jsx(
2448
+ controls.required === "hidden" ? null : /* @__PURE__ */ jsx(
2406
2449
  Checkbox,
2407
2450
  {
2408
2451
  className: builderClass("form-engine-builder__check"),
2409
2452
  checked: field.required === true,
2453
+ disabled: controls.required === "readOnly",
2410
2454
  onChange: (checked) => updateField(field.id, (current) => ({ ...current, required: checked })),
2411
2455
  label: translate("builder.required")
2412
2456
  }
2413
2457
  )
2414
2458
  ] }),
2459
+ controls.description === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2460
+ TextArea,
2461
+ {
2462
+ id: `builder-field-${field.id}-description`,
2463
+ name: `fields.${field.id}.description`,
2464
+ label: translate("builder.description"),
2465
+ value: field.description ?? "",
2466
+ readOnly: controls.description === "readOnly",
2467
+ onChange: (value) => updateField(field.id, (current) => {
2468
+ if (value.length > 0) return { ...current, description: value };
2469
+ const { description: _description, ...remaining } = current;
2470
+ return remaining;
2471
+ })
2472
+ }
2473
+ ) }),
2415
2474
  !pagesEnabled || schema.pages === void 0 ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2416
2475
  Select,
2417
2476
  {
@@ -2447,12 +2506,13 @@ function FormBuilder({
2447
2506
  })
2448
2507
  }
2449
2508
  ) }),
2450
- /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2509
+ controls.description === "hidden" ? null : /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2451
2510
  TextInput,
2452
2511
  {
2453
2512
  id: `builder-field-${field.id}-${editingLocale}-description`,
2454
2513
  label: translate("builder.pageDescription"),
2455
2514
  value: field.translations?.[editingLocale]?.description ?? "",
2515
+ readOnly: controls.description === "readOnly",
2456
2516
  onChange: (value) => updateManualTranslation({
2457
2517
  locale: editingLocale,
2458
2518
  kind: "field",
@@ -2487,7 +2547,7 @@ function FormBuilder({
2487
2547
  }
2488
2548
  ) }, option.id)) : null
2489
2549
  ] }),
2490
- field.type === "rating" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2550
+ field.type === "rating" && controls.ratingBounds !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2491
2551
  /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2492
2552
  TextInput,
2493
2553
  {
@@ -2495,6 +2555,7 @@ function FormBuilder({
2495
2555
  label: translate("builder.minimum"),
2496
2556
  type: "number",
2497
2557
  value: String(field.min ?? 1),
2558
+ readOnly: controls.ratingBounds === "readOnly",
2498
2559
  onChange: (value) => {
2499
2560
  const min = Number(value);
2500
2561
  if (!Number.isInteger(min)) return;
@@ -2512,6 +2573,7 @@ function FormBuilder({
2512
2573
  label: translate("builder.maximum"),
2513
2574
  type: "number",
2514
2575
  value: String(field.max ?? 5),
2576
+ readOnly: controls.ratingBounds === "readOnly",
2515
2577
  onChange: (value) => {
2516
2578
  const max = Number(value);
2517
2579
  if (!Number.isInteger(max)) return;
@@ -2523,7 +2585,73 @@ function FormBuilder({
2523
2585
  }
2524
2586
  ) })
2525
2587
  ] }) : null,
2526
- "options" in field ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__options"), children: [
2588
+ (field.type === "text" || field.type === "textarea") && controls.textLimits !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__grid"), children: [
2589
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2590
+ TextInput,
2591
+ {
2592
+ id: `builder-field-${field.id}-min-length`,
2593
+ label: translate("builder.minimumLength"),
2594
+ type: "number",
2595
+ value: field.minLength === void 0 ? "" : String(field.minLength),
2596
+ readOnly: controls.textLimits === "readOnly",
2597
+ onChange: (value) => updateField(field.id, (current) => {
2598
+ if (current.type !== "text" && current.type !== "textarea") return current;
2599
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2600
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, minLength: parsed };
2601
+ })
2602
+ }
2603
+ ) }),
2604
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2605
+ TextInput,
2606
+ {
2607
+ id: `builder-field-${field.id}-max-length`,
2608
+ label: translate("builder.maximumLength"),
2609
+ type: "number",
2610
+ value: field.maxLength === void 0 ? "" : String(field.maxLength),
2611
+ readOnly: controls.textLimits === "readOnly",
2612
+ onChange: (value) => updateField(field.id, (current) => {
2613
+ if (current.type !== "text" && current.type !== "textarea") return current;
2614
+ const parsed = value.trim().length === 0 ? void 0 : Number(value);
2615
+ return parsed === void 0 || !Number.isInteger(parsed) || parsed < 0 ? current : { ...current, maxLength: parsed };
2616
+ })
2617
+ }
2618
+ ) }),
2619
+ /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2620
+ TextInput,
2621
+ {
2622
+ id: `builder-field-${field.id}-pattern`,
2623
+ label: translate("builder.pattern"),
2624
+ value: field.pattern ?? "",
2625
+ readOnly: controls.textLimits === "readOnly",
2626
+ onChange: (value) => updateField(field.id, (current) => {
2627
+ if (current.type !== "text" && current.type !== "textarea") return current;
2628
+ return value.length === 0 ? current : { ...current, pattern: value };
2629
+ })
2630
+ }
2631
+ ) })
2632
+ ] }) : null,
2633
+ 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(
2634
+ TextInput,
2635
+ {
2636
+ id: `builder-field-${field.id}-${property}`,
2637
+ label: translate(
2638
+ property === "step" ? "builder.step" : property === "min" ? "builder.minimum" : "builder.maximum"
2639
+ ),
2640
+ type: "number",
2641
+ value: field[property] === void 0 ? "" : String(field[property]),
2642
+ readOnly: controls.numberLimits === "readOnly",
2643
+ onChange: (value) => updateField(field.id, (current) => {
2644
+ if (current.type !== "number") return current;
2645
+ if (value.trim().length === 0) {
2646
+ const { [property]: _removed, ...remaining } = current;
2647
+ return remaining;
2648
+ }
2649
+ const parsed = Number(value);
2650
+ return Number.isFinite(parsed) ? { ...current, [property]: parsed } : current;
2651
+ })
2652
+ }
2653
+ ) }, property)) }) : null,
2654
+ "options" in field && controls.options !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__options"), children: [
2527
2655
  /* @__PURE__ */ jsx("strong", { children: translate("builder.options") }),
2528
2656
  field.options.map(
2529
2657
  (option, optionIndex) => OptionEditorSlot === void 0 ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__option"), children: [
@@ -2534,6 +2662,7 @@ function FormBuilder({
2534
2662
  label: translate("builder.optionLabel", { index: optionIndex + 1 }),
2535
2663
  value: option.label,
2536
2664
  placeholder: translate("builder.optionLabelPlaceholder"),
2665
+ readOnly: controls.options === "readOnly",
2537
2666
  onChange: (value) => value.trim().length === 0 ? void 0 : updateOption(field.id, option.id, value)
2538
2667
  }
2539
2668
  ),
@@ -2543,7 +2672,7 @@ function FormBuilder({
2543
2672
  {
2544
2673
  actionType: "moveUp",
2545
2674
  title: translate("builder.moveUp", { title: option.label }),
2546
- disabled: optionIndex === 0,
2675
+ disabled: controls.options === "readOnly" || optionIndex === 0,
2547
2676
  onClick: () => moveOption(field.id, option.id, optionIndex - 1)
2548
2677
  }
2549
2678
  ),
@@ -2552,7 +2681,7 @@ function FormBuilder({
2552
2681
  {
2553
2682
  actionType: "moveDown",
2554
2683
  title: translate("builder.moveDown", { title: option.label }),
2555
- disabled: optionIndex === field.options.length - 1,
2684
+ disabled: controls.options === "readOnly" || optionIndex === field.options.length - 1,
2556
2685
  onClick: () => moveOption(field.id, option.id, optionIndex + 1)
2557
2686
  }
2558
2687
  ),
@@ -2560,7 +2689,7 @@ function FormBuilder({
2560
2689
  IconButton,
2561
2690
  {
2562
2691
  actionType: "delete",
2563
- disabled: field.options.length === 1,
2692
+ disabled: controls.options === "readOnly" || field.options.length === 1,
2564
2693
  onClick: () => removeOption(field.id, option.id),
2565
2694
  title: translate("builder.remove")
2566
2695
  }
@@ -2578,7 +2707,7 @@ function FormBuilder({
2578
2707
  onMoveUp: () => moveOption(field.id, option.id, optionIndex - 1),
2579
2708
  onMoveDown: () => moveOption(field.id, option.id, optionIndex + 1),
2580
2709
  onRemove: () => removeOption(field.id, option.id),
2581
- readOnly,
2710
+ readOnly: readOnly || controls.options === "readOnly",
2582
2711
  actions,
2583
2712
  components
2584
2713
  }
@@ -2592,7 +2721,7 @@ function FormBuilder({
2592
2721
  index: optionIndex,
2593
2722
  currentLocale: editingLocale,
2594
2723
  translate,
2595
- readOnly,
2724
+ readOnly: readOnly || controls.options === "readOnly",
2596
2725
  actions,
2597
2726
  components
2598
2727
  },
@@ -2604,13 +2733,13 @@ function FormBuilder({
2604
2733
  {
2605
2734
  action: "addOption",
2606
2735
  targetId: field.id,
2607
- disabled: policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2736
+ disabled: controls.options === "readOnly" || policy?.maxOptionsPerField !== void 0 && field.options.length >= policy.maxOptionsPerField,
2608
2737
  onClick: () => addOption(field.id),
2609
2738
  children: translate("builder.addOption")
2610
2739
  }
2611
2740
  )
2612
2741
  ] }) : null,
2613
- conditionsEnabled ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__condition"), children: [
2742
+ conditionsEnabled && controls.displayConditions !== "hidden" ? /* @__PURE__ */ jsxs("div", { className: builderClass("form-engine-builder__condition"), children: [
2614
2743
  /* @__PURE__ */ jsx("div", { className: builderClass("form-engine-builder__field"), children: /* @__PURE__ */ jsx(
2615
2744
  Select,
2616
2745
  {
@@ -3985,6 +4114,8 @@ export {
3985
4114
  createLocalStorageSubmissionAttemptStore,
3986
4115
  createLocalStorageSubmissionReceiptStore,
3987
4116
  isTranslationUnresolved,
4117
+ resolveFieldEditorControls,
4118
+ resolveFieldTypeSelectOptions,
3988
4119
  resolveInitialFieldType,
3989
4120
  resolveTranslation,
3990
4121
  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.1.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.1.0",
46
+ "@form-engine-ts/privacy": "4.1.0"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "react": ">=18.2 <20",