@dmitryvim/form-builder 0.1.35 → 0.1.38

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.
@@ -4,6 +4,7 @@ const state = {
4
4
  schema: null,
5
5
  formRoot: null,
6
6
  resourceIndex: new Map(),
7
+ externalActions: null, // Store external actions for the current form
7
8
  version: "1.0.0",
8
9
  config: {
9
10
  // File upload configuration
@@ -129,7 +130,7 @@ function validateSchema(schema) {
129
130
  }
130
131
 
131
132
  // Form rendering
132
- function renderForm(schema, prefill) {
133
+ function renderForm(schema, prefill, actions) {
133
134
  const errors = validateSchema(schema);
134
135
  if (errors.length > 0) {
135
136
  console.error("Schema validation errors:", errors);
@@ -137,6 +138,8 @@ function renderForm(schema, prefill) {
137
138
  }
138
139
 
139
140
  state.schema = schema;
141
+ state.externalActions = actions || null;
142
+
140
143
  if (!state.formRoot) {
141
144
  console.error("No form root element set. Call setFormRoot() first.");
142
145
  return;
@@ -160,11 +163,20 @@ function renderForm(schema, prefill) {
160
163
  });
161
164
 
162
165
  state.formRoot.appendChild(formEl);
166
+
167
+ // Render external actions after form is built (only in readonly mode)
168
+ if (
169
+ state.config.readonly &&
170
+ state.externalActions &&
171
+ Array.isArray(state.externalActions)
172
+ ) {
173
+ renderExternalActions();
174
+ }
163
175
  }
164
176
 
165
177
  function renderElement(element, ctx) {
166
178
  const wrapper = document.createElement("div");
167
- wrapper.className = "mb-6";
179
+ wrapper.className = "mb-6 fb-field-wrapper";
168
180
 
169
181
  const label = document.createElement("div");
170
182
  label.className = "flex items-center mb-2";
@@ -278,42 +290,8 @@ function renderElement(element, ctx) {
278
290
  }
279
291
  }
280
292
 
281
- // Add action buttons in readonly mode
282
- if (
283
- state.config.readonly &&
284
- element.actions &&
285
- Array.isArray(element.actions) &&
286
- element.actions.length > 0
287
- ) {
288
- const actionsContainer = document.createElement("div");
289
- actionsContainer.className = "mt-3 flex flex-wrap gap-2";
290
-
291
- element.actions.forEach((action) => {
292
- if (action.value && action.label) {
293
- const actionBtn = document.createElement("button");
294
- actionBtn.type = "button";
295
- actionBtn.className =
296
- "px-3 py-1.5 text-sm bg-blue-50 border border-blue-200 text-blue-700 rounded-lg hover:bg-blue-100 transition-colors";
297
- actionBtn.textContent = action.label;
298
-
299
- actionBtn.addEventListener("click", (e) => {
300
- e.preventDefault();
301
- e.stopPropagation();
302
-
303
- if (
304
- state.config.actionHandler &&
305
- typeof state.config.actionHandler === "function"
306
- ) {
307
- state.config.actionHandler(action.value);
308
- }
309
- });
310
-
311
- actionsContainer.appendChild(actionBtn);
312
- }
313
- });
314
-
315
- wrapper.appendChild(actionsContainer);
316
- }
293
+ // Actions are now only rendered via external actions system in renderExternalActions()
294
+ // element.actions are only used for label lookup, not direct rendering
317
295
 
318
296
  return wrapper;
319
297
  }
@@ -577,6 +555,23 @@ async function renderFilePreview(container, resourceId, options = {}) {
577
555
  }
578
556
  }
579
557
 
558
+ function renderThumbnailForResource(slot, rid, meta) {
559
+ const url = state.config.getThumbnail(rid);
560
+ if (url) {
561
+ const img = document.createElement("img");
562
+ img.className = "w-full h-full object-contain";
563
+ img.alt = meta.name;
564
+ img.src = url;
565
+ slot.appendChild(img);
566
+ } else {
567
+ slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
568
+ <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
569
+ <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
570
+ </svg>
571
+ </div>`;
572
+ }
573
+ }
574
+
580
575
  function renderResourcePills(container, rids, onRemove) {
581
576
  clear(container);
582
577
 
@@ -616,11 +611,11 @@ function renderResourcePills(container, rids, onRemove) {
616
611
  slot.onclick = () => {
617
612
  // Look for file input - check parent containers that have space-y-2 class
618
613
  let filesWrapper = container.parentElement;
619
- while (filesWrapper && !filesWrapper.classList.contains('space-y-2')) {
614
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
620
615
  filesWrapper = filesWrapper.parentElement;
621
616
  }
622
617
  // If no parent with space-y-2, container itself might be the wrapper
623
- if (!filesWrapper && container.classList.contains('space-y-2')) {
618
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
624
619
  filesWrapper = container;
625
620
  }
626
621
  const fileInput = filesWrapper?.querySelector('input[type="file"]');
@@ -641,11 +636,11 @@ function renderResourcePills(container, rids, onRemove) {
641
636
  e.stopPropagation();
642
637
  // Look for file input - check parent containers that have space-y-2 class
643
638
  let filesWrapper = container.parentElement;
644
- while (filesWrapper && !filesWrapper.classList.contains('space-y-2')) {
639
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
645
640
  filesWrapper = filesWrapper.parentElement;
646
641
  }
647
642
  // If no parent with space-y-2, container itself might be the wrapper
648
- if (!filesWrapper && container.classList.contains('space-y-2')) {
643
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
649
644
  filesWrapper = container;
650
645
  }
651
646
  const fileInput = filesWrapper?.querySelector('input[type="file"]');
@@ -701,21 +696,7 @@ function renderResourcePills(container, rids, onRemove) {
701
696
  slot.appendChild(img);
702
697
  } else if (state.config.getThumbnail) {
703
698
  // Use getThumbnail for uploaded files
704
- const img = document.createElement("img");
705
- img.className = "w-full h-full object-contain";
706
- img.alt = meta.name;
707
-
708
- const url = state.config.getThumbnail(rid);
709
- if (url) {
710
- img.src = url;
711
- slot.appendChild(img);
712
- } else {
713
- slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
714
- <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
715
- <path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/>
716
- </svg>
717
- </div>`;
718
- }
699
+ renderThumbnailForResource(slot, rid, meta);
719
700
  } else {
720
701
  slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
721
702
  <svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
@@ -808,11 +789,11 @@ function renderResourcePills(container, rids, onRemove) {
808
789
  slot.onclick = () => {
809
790
  // Look for file input - check parent containers that have space-y-2 class
810
791
  let filesWrapper = container.parentElement;
811
- while (filesWrapper && !filesWrapper.classList.contains('space-y-2')) {
792
+ while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
812
793
  filesWrapper = filesWrapper.parentElement;
813
794
  }
814
795
  // If no parent with space-y-2, container itself might be the wrapper
815
- if (!filesWrapper && container.classList.contains('space-y-2')) {
796
+ if (!filesWrapper && container.classList.contains("space-y-2")) {
816
797
  filesWrapper = container;
817
798
  }
818
799
  const fileInput = filesWrapper?.querySelector('input[type="file"]');
@@ -927,6 +908,280 @@ function addDeleteButton(container, onDelete) {
927
908
  container.appendChild(overlay);
928
909
  }
929
910
 
911
+ // JSON path resolution for external actions (currently unused but kept for future use)
912
+ // eslint-disable-next-line no-unused-vars
913
+ function resolveFieldPath(path, formData) {
914
+ // Remove leading $input_data. prefix if present
915
+ const cleanPath = path.replace(/^\$input_data\./, "");
916
+
917
+ // Split path into segments, handling array notation
918
+ const segments = cleanPath.split(/[.[\]]/).filter(Boolean);
919
+
920
+ // Try to find the corresponding form element
921
+ return findElementByPath(segments, formData);
922
+ }
923
+
924
+ function findElementByPath(segments, data, currentPath = "") {
925
+ if (segments.length === 0) return currentPath;
926
+
927
+ const [head, ...tail] = segments;
928
+
929
+ // Check if this is an array index
930
+ const isArrayIndex = /^\d+$/.test(head);
931
+
932
+ if (isArrayIndex) {
933
+ // Array index case: build path like "fieldName[index]"
934
+ const newPath = currentPath ? `${currentPath}[${head}]` : `[${head}]`;
935
+ return findElementByPath(tail, data, newPath);
936
+ } else {
937
+ // Regular field name
938
+ const newPath = currentPath ? `${currentPath}.${head}` : head;
939
+ return findElementByPath(tail, data, newPath);
940
+ }
941
+ }
942
+
943
+ function findFormElementByFieldPath(fieldPath) {
944
+ // Try to find the form element that corresponds to the field path
945
+ // This looks for elements with name attributes that match the path pattern
946
+
947
+ if (!state.formRoot) return null;
948
+
949
+ // In edit mode, try to find input elements with name attributes
950
+ if (!state.config.readonly) {
951
+ // Try exact match first
952
+ let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
953
+ if (element) return element;
954
+
955
+ // Try with array notation variations
956
+ const variations = [
957
+ fieldPath,
958
+ fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
959
+ fieldPath.replace(/\./g, "[") +
960
+ "]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
961
+ ];
962
+
963
+ for (const variation of variations) {
964
+ element = state.formRoot.querySelector(`[name="${variation}"]`);
965
+ if (element) return element;
966
+ }
967
+ }
968
+
969
+ // In readonly mode, or if no input found, look for field wrappers using data attributes
970
+ // Find the schema element for this field path to match against rendered fields
971
+ const schemaElement = findSchemaElement(fieldPath);
972
+ if (!schemaElement) return null;
973
+
974
+ // Look for field wrappers that contain the field key
975
+ const fieldWrappers = state.formRoot.querySelectorAll('.fb-field-wrapper');
976
+ for (const wrapper of fieldWrappers) {
977
+ // Try to find a label or element that matches this field
978
+ const labelText = schemaElement.label || schemaElement.key;
979
+ const labelElement = wrapper.querySelector('label');
980
+ if (labelElement && (labelElement.textContent === labelText || labelElement.textContent === `${labelText}*`)) {
981
+ // Create a dummy element for the field so actions can attach
982
+ let fieldElement = wrapper.querySelector('.field-placeholder');
983
+ if (!fieldElement) {
984
+ fieldElement = document.createElement('div');
985
+ fieldElement.className = 'field-placeholder';
986
+ fieldElement.style.display = 'none';
987
+ wrapper.appendChild(fieldElement);
988
+ }
989
+ return fieldElement;
990
+ }
991
+ }
992
+
993
+ return null;
994
+ }
995
+
996
+ function renderExternalActions() {
997
+ if (!state.externalActions || !Array.isArray(state.externalActions)) return;
998
+
999
+ // Group actions by related_field (null for form-level actions)
1000
+ const actionsByField = new Map();
1001
+ const formLevelActions = [];
1002
+
1003
+ state.externalActions.forEach((action) => {
1004
+ if (!action.key || !action.value) return;
1005
+
1006
+ if (!action.related_field) {
1007
+ // Form-level action
1008
+ formLevelActions.push(action);
1009
+ } else {
1010
+ // Field-level action
1011
+ if (!actionsByField.has(action.related_field)) {
1012
+ actionsByField.set(action.related_field, []);
1013
+ }
1014
+ actionsByField.get(action.related_field).push(action);
1015
+ }
1016
+ });
1017
+
1018
+ // Render field-level actions
1019
+ actionsByField.forEach((actions, fieldPath) => {
1020
+ // Find the form element for this related field
1021
+ const fieldElement = findFormElementByFieldPath(fieldPath);
1022
+ if (!fieldElement) {
1023
+ console.warn(
1024
+ `External action: Could not find form element for field "${fieldPath}"`,
1025
+ );
1026
+ return;
1027
+ }
1028
+
1029
+ // Find the wrapper element that contains the field using stable class
1030
+ let wrapper = fieldElement.closest(".fb-field-wrapper");
1031
+ if (!wrapper) {
1032
+ wrapper = fieldElement.parentElement;
1033
+ }
1034
+
1035
+ if (!wrapper) {
1036
+ console.warn(
1037
+ `External action: Could not find wrapper for field "${fieldPath}"`,
1038
+ );
1039
+ return;
1040
+ }
1041
+
1042
+ // Remove any existing actions container
1043
+ const existingContainer = wrapper.querySelector(".external-actions-container");
1044
+ if (existingContainer) {
1045
+ existingContainer.remove();
1046
+ }
1047
+
1048
+ // Create actions container
1049
+ const actionsContainer = document.createElement("div");
1050
+ actionsContainer.className =
1051
+ "external-actions-container mt-3 flex flex-wrap gap-2";
1052
+
1053
+ // Find the corresponding schema element for label lookup
1054
+ const schemaElement = findSchemaElement(fieldPath);
1055
+
1056
+ // Create action buttons
1057
+ actions.forEach((action) => {
1058
+ const actionBtn = document.createElement("button");
1059
+ actionBtn.type = "button";
1060
+ actionBtn.className =
1061
+ "bg-white text-gray-700 border border-gray-200 px-3 py-2 text-sm rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm";
1062
+
1063
+ // Resolve action label with priority:
1064
+ // 1. Use explicit label from action if provided
1065
+ // 2. Try to find label from schema element labels using key
1066
+ // 3. Fall back to using key as label
1067
+ const resolvedLabel = resolveActionLabel(action.key, action.label, schemaElement);
1068
+ actionBtn.textContent = resolvedLabel;
1069
+
1070
+ actionBtn.addEventListener("click", (e) => {
1071
+ e.preventDefault();
1072
+ e.stopPropagation();
1073
+
1074
+ if (
1075
+ state.config.actionHandler &&
1076
+ typeof state.config.actionHandler === "function"
1077
+ ) {
1078
+ // Call with value, key, and related_field for the new actions system
1079
+ state.config.actionHandler(action.value, action.key, action.related_field);
1080
+ }
1081
+ });
1082
+
1083
+ actionsContainer.appendChild(actionBtn);
1084
+ });
1085
+
1086
+ wrapper.appendChild(actionsContainer);
1087
+ });
1088
+
1089
+ // Render form-level actions at the bottom of the form
1090
+ if (formLevelActions.length > 0) {
1091
+ renderFormLevelActions(formLevelActions);
1092
+ }
1093
+ }
1094
+
1095
+ function renderFormLevelActions(actions) {
1096
+ if (!state.formRoot) return;
1097
+
1098
+ // Remove any existing form-level actions container
1099
+ const existingContainer = state.formRoot.querySelector(".form-level-actions-container");
1100
+ if (existingContainer) {
1101
+ existingContainer.remove();
1102
+ }
1103
+
1104
+ // Create form-level actions container
1105
+ const actionsContainer = document.createElement("div");
1106
+ actionsContainer.className =
1107
+ "form-level-actions-container mt-6 pt-4 border-t border-gray-200 flex flex-wrap gap-3 justify-center";
1108
+
1109
+ // Create action buttons
1110
+ actions.forEach((action) => {
1111
+ const actionBtn = document.createElement("button");
1112
+ actionBtn.type = "button";
1113
+ actionBtn.className =
1114
+ "bg-white text-gray-700 border border-gray-200 px-4 py-2 text-sm font-medium rounded-lg hover:bg-gray-50 hover:border-gray-300 transition-all duration-200 shadow-sm";
1115
+
1116
+ // Resolve action label with priority:
1117
+ // 1. Use explicit label from action if provided
1118
+ // 2. Try to find label from schema element labels using key
1119
+ // 3. Fall back to using key as label
1120
+ const resolvedLabel = resolveActionLabel(action.key, action.label, null);
1121
+ actionBtn.textContent = resolvedLabel;
1122
+
1123
+ actionBtn.addEventListener("click", (e) => {
1124
+ e.preventDefault();
1125
+ e.stopPropagation();
1126
+
1127
+ if (
1128
+ state.config.actionHandler &&
1129
+ typeof state.config.actionHandler === "function"
1130
+ ) {
1131
+ // Call with value, key, and null related_field for form-level actions
1132
+ state.config.actionHandler(action.value, action.key, null);
1133
+ }
1134
+ });
1135
+
1136
+ actionsContainer.appendChild(actionBtn);
1137
+ });
1138
+
1139
+ // Append to form root
1140
+ state.formRoot.appendChild(actionsContainer);
1141
+ }
1142
+
1143
+ // Helper function to resolve action label
1144
+ function resolveActionLabel(actionKey, externalLabel, schemaElement) {
1145
+ // 1. Try to find label from predefined actions in schema using key (highest priority)
1146
+ if (schemaElement && schemaElement.actions && Array.isArray(schemaElement.actions)) {
1147
+ const predefinedAction = schemaElement.actions.find(a => a.key === actionKey);
1148
+ if (predefinedAction && predefinedAction.label) {
1149
+ return predefinedAction.label;
1150
+ }
1151
+ }
1152
+
1153
+ // 2. Use explicit label from external action if provided
1154
+ if (externalLabel) {
1155
+ return externalLabel;
1156
+ }
1157
+
1158
+ // 3. Fall back to using key as label
1159
+ return actionKey;
1160
+ }
1161
+
1162
+ // Helper function to find schema element by field path
1163
+ function findSchemaElement(fieldPath) {
1164
+ if (!state.schema || !state.schema.elements) return null;
1165
+
1166
+ let currentElements = state.schema.elements;
1167
+ let foundElement = null;
1168
+
1169
+ // Handle paths like 'a.b' or 'a[0].b' by looking for keys in sequence
1170
+ const keys = fieldPath.replace(/\[\d+\]/g, '').split('.').filter(Boolean);
1171
+
1172
+ for (const key of keys) {
1173
+ foundElement = currentElements.find(el => el.key === key);
1174
+ if (!foundElement) {
1175
+ return null; // Key not found at this level
1176
+ }
1177
+ if (foundElement.elements) {
1178
+ currentElements = foundElement.elements;
1179
+ }
1180
+ }
1181
+
1182
+ return foundElement;
1183
+ }
1184
+
930
1185
  function showTooltip(tooltipId, button) {
931
1186
  const tooltip = document.getElementById(tooltipId);
932
1187
  const isCurrentlyVisible = !tooltip.classList.contains("hidden");
@@ -1044,11 +1299,17 @@ function validateForm(skipValidation = false) {
1044
1299
  values.push(val);
1045
1300
 
1046
1301
  if (!skipValidation && val) {
1047
- if (element.minLength !== null && val.length < element.minLength) {
1302
+ if (
1303
+ element.minLength !== null &&
1304
+ val.length < element.minLength
1305
+ ) {
1048
1306
  errors.push(`${key}[${index}]: minLength=${element.minLength}`);
1049
1307
  markValidity(input, `minLength=${element.minLength}`);
1050
1308
  }
1051
- if (element.maxLength !== null && val.length > element.maxLength) {
1309
+ if (
1310
+ element.maxLength !== null &&
1311
+ val.length > element.maxLength
1312
+ ) {
1052
1313
  errors.push(`${key}[${index}]: maxLength=${element.maxLength}`);
1053
1314
  markValidity(input, `maxLength=${element.maxLength}`);
1054
1315
  }
@@ -1073,7 +1334,7 @@ function validateForm(skipValidation = false) {
1073
1334
  if (!skipValidation) {
1074
1335
  const minCount = element.minCount ?? 1;
1075
1336
  const maxCount = element.maxCount ?? 10;
1076
- const nonEmptyValues = values.filter(v => v.trim() !== "");
1337
+ const nonEmptyValues = values.filter((v) => v.trim() !== "");
1077
1338
 
1078
1339
  if (element.required && nonEmptyValues.length === 0) {
1079
1340
  errors.push(`${key}: required`);
@@ -1156,7 +1417,9 @@ function validateForm(skipValidation = false) {
1156
1417
  markValidity(input, `> max=${element.max}`);
1157
1418
  }
1158
1419
 
1159
- const d = Number.isInteger(element.decimals ?? 0) ? element.decimals : 0;
1420
+ const d = Number.isInteger(element.decimals ?? 0)
1421
+ ? element.decimals
1422
+ : 0;
1160
1423
  markValidity(input, null);
1161
1424
  values.push(Number(v.toFixed(d)));
1162
1425
  });
@@ -1165,7 +1428,7 @@ function validateForm(skipValidation = false) {
1165
1428
  if (!skipValidation) {
1166
1429
  const minCount = element.minCount ?? 1;
1167
1430
  const maxCount = element.maxCount ?? 10;
1168
- const nonNullValues = values.filter(v => v !== null);
1431
+ const nonNullValues = values.filter((v) => v !== null);
1169
1432
 
1170
1433
  if (element.required && nonNullValues.length === 0) {
1171
1434
  errors.push(`${key}: required`);
@@ -1229,7 +1492,7 @@ function validateForm(skipValidation = false) {
1229
1492
  if (!skipValidation) {
1230
1493
  const minCount = element.minCount ?? 1;
1231
1494
  const maxCount = element.maxCount ?? 10;
1232
- const nonEmptyValues = values.filter(v => v !== "");
1495
+ const nonEmptyValues = values.filter((v) => v !== "");
1233
1496
 
1234
1497
  if (element.required && nonEmptyValues.length === 0) {
1235
1498
  errors.push(`${key}: required`);
@@ -1261,9 +1524,11 @@ function validateForm(skipValidation = false) {
1261
1524
  // Handle file with multiple property like files type
1262
1525
  // Find the files list by locating the specific file input for this field
1263
1526
  const fullKey = pathJoin(ctx.path, key);
1264
- const pickerInput = scopeRoot.querySelector(`input[type="file"][name="${fullKey}"]`);
1265
- const filesWrapper = pickerInput?.closest('.space-y-2');
1266
- const container = filesWrapper?.querySelector('.files-list') || null;
1527
+ const pickerInput = scopeRoot.querySelector(
1528
+ `input[type="file"][name="${fullKey}"]`,
1529
+ );
1530
+ const filesWrapper = pickerInput?.closest(".space-y-2");
1531
+ const container = filesWrapper?.querySelector(".files-list") || null;
1267
1532
 
1268
1533
  const resourceIds = [];
1269
1534
  if (container) {
@@ -1351,12 +1616,18 @@ function validateForm(skipValidation = false) {
1351
1616
  const items = [];
1352
1617
  // Use full path for nested group element search
1353
1618
  const fullKey = pathJoin(ctx.path, key);
1354
- const itemElements = scopeRoot.querySelectorAll(`[name^="${fullKey}["]`);
1619
+ const itemElements = scopeRoot.querySelectorAll(
1620
+ `[name^="${fullKey}["]`,
1621
+ );
1355
1622
 
1356
1623
  // Extract actual indices from DOM element names instead of assuming sequential numbering
1357
1624
  const actualIndices = new Set();
1358
1625
  itemElements.forEach((el) => {
1359
- const match = el.name.match(new RegExp(`^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\[(\\d+)\\]`));
1626
+ const match = el.name.match(
1627
+ new RegExp(
1628
+ `^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[(\\d+)\\]`,
1629
+ ),
1630
+ );
1360
1631
  if (match) {
1361
1632
  actualIndices.add(parseInt(match[1]));
1362
1633
  }
@@ -1374,7 +1645,8 @@ function validateForm(skipValidation = false) {
1374
1645
  element.elements.forEach((child) => {
1375
1646
  if (child.hidden) {
1376
1647
  // For hidden child elements, use their default value
1377
- itemData[child.key] = child.default !== undefined ? child.default : "";
1648
+ itemData[child.key] =
1649
+ child.default !== undefined ? child.default : "";
1378
1650
  } else {
1379
1651
  const childKey = `${fullKey}[${actualIndex}].${child.key}`;
1380
1652
  itemData[child.key] = validateElement(
@@ -1395,7 +1667,8 @@ function validateForm(skipValidation = false) {
1395
1667
  element.elements.forEach((child) => {
1396
1668
  if (child.hidden) {
1397
1669
  // For hidden child elements, use their default value
1398
- groupData[child.key] = child.default !== undefined ? child.default : "";
1670
+ groupData[child.key] =
1671
+ child.default !== undefined ? child.default : "";
1399
1672
  } else {
1400
1673
  const childKey = `${key}.${child.key}`;
1401
1674
  groupData[child.key] = validateElement(
@@ -1427,7 +1700,8 @@ function validateForm(skipValidation = false) {
1427
1700
  element.elements.forEach((child) => {
1428
1701
  if (child.hidden) {
1429
1702
  // For hidden child elements, use their default value
1430
- itemData[child.key] = child.default !== undefined ? child.default : "";
1703
+ itemData[child.key] =
1704
+ child.default !== undefined ? child.default : "";
1431
1705
  } else {
1432
1706
  const childKey = `${key}[${i}].${child.key}`;
1433
1707
  itemData[child.key] = validateElement(
@@ -1465,7 +1739,8 @@ function validateForm(skipValidation = false) {
1465
1739
  element.elements.forEach((child) => {
1466
1740
  if (child.hidden) {
1467
1741
  // For hidden child elements, use their default value
1468
- containerData[child.key] = child.default !== undefined ? child.default : "";
1742
+ containerData[child.key] =
1743
+ child.default !== undefined ? child.default : "";
1469
1744
  } else {
1470
1745
  const childKey = `${key}.${child.key}`;
1471
1746
  containerData[child.key] = validateElement(
@@ -1550,7 +1825,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1550
1825
 
1551
1826
  const textInput = document.createElement("input");
1552
1827
  textInput.type = "text";
1553
- textInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1828
+ textInput.className =
1829
+ "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1554
1830
  textInput.placeholder = element.placeholder || "Enter text";
1555
1831
  textInput.value = value;
1556
1832
  textInput.readOnly = state.config.readonly;
@@ -1571,15 +1847,16 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1571
1847
 
1572
1848
  function updateRemoveButtons() {
1573
1849
  if (state.config.readonly) return;
1574
- const items = container.querySelectorAll('.multiple-text-item');
1850
+ const items = container.querySelectorAll(".multiple-text-item");
1575
1851
  const currentCount = items.length;
1576
1852
  items.forEach((item) => {
1577
- let removeBtn = item.querySelector('.remove-item-btn');
1853
+ let removeBtn = item.querySelector(".remove-item-btn");
1578
1854
  if (!removeBtn) {
1579
- removeBtn = document.createElement('button');
1580
- removeBtn.type = 'button';
1581
- removeBtn.className = 'remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded';
1582
- removeBtn.innerHTML = '✕';
1855
+ removeBtn = document.createElement("button");
1856
+ removeBtn.type = "button";
1857
+ removeBtn.className =
1858
+ "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
1859
+ removeBtn.innerHTML = "✕";
1583
1860
  removeBtn.onclick = () => {
1584
1861
  const currentIndex = Array.from(container.children).indexOf(item);
1585
1862
  if (container.children.length > minCount) {
@@ -1594,8 +1871,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1594
1871
  }
1595
1872
  const disabled = currentCount <= minCount;
1596
1873
  removeBtn.disabled = disabled;
1597
- removeBtn.style.opacity = disabled ? '0.5' : '1';
1598
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
1874
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
1875
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1599
1876
  });
1600
1877
  }
1601
1878
 
@@ -1606,8 +1883,9 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1606
1883
  if (!state.config.readonly && values.length < maxCount) {
1607
1884
  const addBtn = document.createElement("button");
1608
1885
  addBtn.type = "button";
1609
- addBtn.className = "add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1610
- addBtn.textContent = `+ Add ${element.label || 'Text'}`;
1886
+ addBtn.className =
1887
+ "add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1888
+ addBtn.textContent = `+ Add ${element.label || "Text"}`;
1611
1889
  addBtn.onclick = () => {
1612
1890
  values.push(element.default || "");
1613
1891
  addTextItem(element.default || "");
@@ -1619,7 +1897,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
1619
1897
  }
1620
1898
 
1621
1899
  // Render initial items
1622
- values.forEach(value => addTextItem(value));
1900
+ values.forEach((value) => addTextItem(value));
1623
1901
  updateAddButton();
1624
1902
  updateRemoveButtons();
1625
1903
 
@@ -1679,7 +1957,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1679
1957
  itemWrapper.className = "multiple-textarea-item";
1680
1958
 
1681
1959
  const textareaInput = document.createElement("textarea");
1682
- textareaInput.className = "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1960
+ textareaInput.className =
1961
+ "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
1683
1962
  textareaInput.placeholder = element.placeholder || "Enter text";
1684
1963
  textareaInput.rows = element.rows || 4;
1685
1964
  textareaInput.value = value;
@@ -1701,15 +1980,16 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1701
1980
 
1702
1981
  function updateRemoveButtons() {
1703
1982
  if (state.config.readonly) return;
1704
- const items = container.querySelectorAll('.multiple-textarea-item');
1983
+ const items = container.querySelectorAll(".multiple-textarea-item");
1705
1984
  const currentCount = items.length;
1706
1985
  items.forEach((item) => {
1707
- let removeBtn = item.querySelector('.remove-item-btn');
1986
+ let removeBtn = item.querySelector(".remove-item-btn");
1708
1987
  if (!removeBtn) {
1709
- removeBtn = document.createElement('button');
1710
- removeBtn.type = 'button';
1711
- removeBtn.className = 'remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm';
1712
- removeBtn.innerHTML = '✕ Remove';
1988
+ removeBtn = document.createElement("button");
1989
+ removeBtn.type = "button";
1990
+ removeBtn.className =
1991
+ "remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
1992
+ removeBtn.innerHTML = "✕ Remove";
1713
1993
  removeBtn.onclick = () => {
1714
1994
  const currentIndex = Array.from(container.children).indexOf(item);
1715
1995
  if (container.children.length > minCount) {
@@ -1724,8 +2004,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1724
2004
  }
1725
2005
  const disabled = currentCount <= minCount;
1726
2006
  removeBtn.disabled = disabled;
1727
- removeBtn.style.opacity = disabled ? '0.5' : '1';
1728
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
2007
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2008
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1729
2009
  });
1730
2010
  }
1731
2011
 
@@ -1736,8 +2016,9 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1736
2016
  if (!state.config.readonly && values.length < maxCount) {
1737
2017
  const addBtn = document.createElement("button");
1738
2018
  addBtn.type = "button";
1739
- addBtn.className = "add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1740
- addBtn.textContent = `+ Add ${element.label || 'Textarea'}`;
2019
+ addBtn.className =
2020
+ "add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2021
+ addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
1741
2022
  addBtn.onclick = () => {
1742
2023
  values.push(element.default || "");
1743
2024
  addTextareaItem(element.default || "");
@@ -1749,7 +2030,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
1749
2030
  }
1750
2031
 
1751
2032
  // Render initial items
1752
- values.forEach(value => addTextareaItem(value));
2033
+ values.forEach((value) => addTextareaItem(value));
1753
2034
  updateAddButton();
1754
2035
  updateRemoveButtons();
1755
2036
 
@@ -1813,7 +2094,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1813
2094
 
1814
2095
  const numberInput = document.createElement("input");
1815
2096
  numberInput.type = "number";
1816
- numberInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
2097
+ numberInput.className =
2098
+ "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1817
2099
  numberInput.placeholder = element.placeholder || "0";
1818
2100
  if (element.min !== undefined) numberInput.min = element.min;
1819
2101
  if (element.max !== undefined) numberInput.max = element.max;
@@ -1837,15 +2119,16 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1837
2119
 
1838
2120
  function updateRemoveButtons() {
1839
2121
  if (state.config.readonly) return;
1840
- const items = container.querySelectorAll('.multiple-number-item');
2122
+ const items = container.querySelectorAll(".multiple-number-item");
1841
2123
  const currentCount = items.length;
1842
2124
  items.forEach((item) => {
1843
- let removeBtn = item.querySelector('.remove-item-btn');
2125
+ let removeBtn = item.querySelector(".remove-item-btn");
1844
2126
  if (!removeBtn) {
1845
- removeBtn = document.createElement('button');
1846
- removeBtn.type = 'button';
1847
- removeBtn.className = 'remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded';
1848
- removeBtn.innerHTML = '✕';
2127
+ removeBtn = document.createElement("button");
2128
+ removeBtn.type = "button";
2129
+ removeBtn.className =
2130
+ "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
2131
+ removeBtn.innerHTML = "✕";
1849
2132
  removeBtn.onclick = () => {
1850
2133
  const currentIndex = Array.from(container.children).indexOf(item);
1851
2134
  if (container.children.length > minCount) {
@@ -1860,8 +2143,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1860
2143
  }
1861
2144
  const disabled = currentCount <= minCount;
1862
2145
  removeBtn.disabled = disabled;
1863
- removeBtn.style.opacity = disabled ? '0.5' : '1';
1864
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
2146
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2147
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
1865
2148
  });
1866
2149
  }
1867
2150
 
@@ -1872,8 +2155,9 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1872
2155
  if (!state.config.readonly && values.length < maxCount) {
1873
2156
  const addBtn = document.createElement("button");
1874
2157
  addBtn.type = "button";
1875
- addBtn.className = "add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
1876
- addBtn.textContent = `+ Add ${element.label || 'Number'}`;
2158
+ addBtn.className =
2159
+ "add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2160
+ addBtn.textContent = `+ Add ${element.label || "Number"}`;
1877
2161
  addBtn.onclick = () => {
1878
2162
  values.push(element.default || "");
1879
2163
  addNumberItem(element.default || "");
@@ -1885,7 +2169,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
1885
2169
  }
1886
2170
 
1887
2171
  // Render initial items
1888
- values.forEach(value => addNumberItem(value));
2172
+ values.forEach((value) => addNumberItem(value));
1889
2173
  updateAddButton();
1890
2174
  updateRemoveButtons();
1891
2175
 
@@ -1931,7 +2215,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1931
2215
  const maxCount = element.maxCount ?? 10;
1932
2216
 
1933
2217
  while (values.length < minCount) {
1934
- values.push(element.default || (element.options?.[0]?.value || ""));
2218
+ values.push(element.default || element.options?.[0]?.value || "");
1935
2219
  }
1936
2220
 
1937
2221
  const container = document.createElement("div");
@@ -1953,7 +2237,8 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1953
2237
  itemWrapper.className = "multiple-select-item flex items-center gap-2";
1954
2238
 
1955
2239
  const selectInput = document.createElement("select");
1956
- selectInput.className = "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
2240
+ selectInput.className =
2241
+ "flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
1957
2242
  selectInput.disabled = state.config.readonly;
1958
2243
 
1959
2244
  // Add options
@@ -1983,15 +2268,16 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
1983
2268
 
1984
2269
  function updateRemoveButtons() {
1985
2270
  if (state.config.readonly) return;
1986
- const items = container.querySelectorAll('.multiple-select-item');
2271
+ const items = container.querySelectorAll(".multiple-select-item");
1987
2272
  const currentCount = items.length;
1988
2273
  items.forEach((item) => {
1989
- let removeBtn = item.querySelector('.remove-item-btn');
2274
+ let removeBtn = item.querySelector(".remove-item-btn");
1990
2275
  if (!removeBtn) {
1991
- removeBtn = document.createElement('button');
1992
- removeBtn.type = 'button';
1993
- removeBtn.className = 'remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded';
1994
- removeBtn.innerHTML = '✕';
2276
+ removeBtn = document.createElement("button");
2277
+ removeBtn.type = "button";
2278
+ removeBtn.className =
2279
+ "remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
2280
+ removeBtn.innerHTML = "✕";
1995
2281
  removeBtn.onclick = () => {
1996
2282
  const currentIndex = Array.from(container.children).indexOf(item);
1997
2283
  if (container.children.length > minCount) {
@@ -2006,8 +2292,8 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2006
2292
  }
2007
2293
  const disabled = currentCount <= minCount;
2008
2294
  removeBtn.disabled = disabled;
2009
- removeBtn.style.opacity = disabled ? '0.5' : '1';
2010
- removeBtn.style.pointerEvents = disabled ? 'none' : 'auto';
2295
+ removeBtn.style.opacity = disabled ? "0.5" : "1";
2296
+ removeBtn.style.pointerEvents = disabled ? "none" : "auto";
2011
2297
  });
2012
2298
  }
2013
2299
 
@@ -2018,10 +2304,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2018
2304
  if (!state.config.readonly && values.length < maxCount) {
2019
2305
  const addBtn = document.createElement("button");
2020
2306
  addBtn.type = "button";
2021
- addBtn.className = "add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2022
- addBtn.textContent = `+ Add ${element.label || 'Selection'}`;
2307
+ addBtn.className =
2308
+ "add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
2309
+ addBtn.textContent = `+ Add ${element.label || "Selection"}`;
2023
2310
  addBtn.onclick = () => {
2024
- const defaultValue = element.default || (element.options?.[0]?.value || "");
2311
+ const defaultValue =
2312
+ element.default || element.options?.[0]?.value || "";
2025
2313
  values.push(defaultValue);
2026
2314
  addSelectItem(defaultValue);
2027
2315
  updateAddButton();
@@ -2032,7 +2320,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
2032
2320
  }
2033
2321
 
2034
2322
  // Render initial items
2035
- values.forEach(value => addSelectItem(value));
2323
+ values.forEach((value) => addSelectItem(value));
2036
2324
  updateAddButton();
2037
2325
  updateRemoveButtons();
2038
2326
 
@@ -2321,17 +2609,18 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
2321
2609
  // Show count and min/max info
2322
2610
  const countInfo = document.createElement("div");
2323
2611
  countInfo.className = "text-xs text-gray-500 mt-2";
2324
- const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? 's' : ''}`;
2325
- const minMaxText = minFiles > 0 || maxFiles < Infinity
2326
- ? ` (${minFiles}-${maxFiles} allowed)`
2327
- : '';
2612
+ const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
2613
+ const minMaxText =
2614
+ minFiles > 0 || maxFiles < Infinity
2615
+ ? ` (${minFiles}-${maxFiles} allowed)`
2616
+ : "";
2328
2617
  countInfo.textContent = countText + minMaxText;
2329
2618
 
2330
2619
  // Remove previous count info
2331
- const existingCount = filesWrapper.querySelector('.file-count-info');
2620
+ const existingCount = filesWrapper.querySelector(".file-count-info");
2332
2621
  if (existingCount) existingCount.remove();
2333
2622
 
2334
- countInfo.className += ' file-count-info';
2623
+ countInfo.className += " file-count-info";
2335
2624
  filesWrapper.appendChild(countInfo);
2336
2625
  };
2337
2626
 
@@ -2639,7 +2928,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2639
2928
  prefill: {},
2640
2929
  };
2641
2930
  const item = document.createElement("div");
2642
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2931
+ item.className =
2932
+ "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2643
2933
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2644
2934
 
2645
2935
  element.elements.forEach((child) => {
@@ -2678,7 +2968,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2678
2968
  addBtn.disabled = currentCount >= max;
2679
2969
  addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
2680
2970
  }
2681
- left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? '' : max})</span>`;
2971
+ left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "" : max})</span>`;
2682
2972
  };
2683
2973
 
2684
2974
  if (!state.config.readonly) {
@@ -2693,7 +2983,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2693
2983
  prefill: prefillObj || {},
2694
2984
  };
2695
2985
  const item = document.createElement("div");
2696
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2986
+ item.className =
2987
+ "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2697
2988
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2698
2989
 
2699
2990
  element.elements.forEach((child) => {
@@ -2731,7 +3022,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
2731
3022
  prefill: {},
2732
3023
  };
2733
3024
  const item = document.createElement("div");
2734
- item.className = "containerItem border border-gray-300 rounded-lg p-4 bg-white";
3025
+ item.className =
3026
+ "containerItem border border-gray-300 rounded-lg p-4 bg-white";
2735
3027
  item.setAttribute("data-container-item", `${element.key}[${idx}]`);
2736
3028
 
2737
3029
  element.elements.forEach((child) => {