@dmitryvim/form-builder 0.1.35 → 0.1.37
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/dist/demo.js +164 -22
- package/dist/elements.html +588 -160
- package/dist/elements.js +270 -232
- package/dist/form-builder.js +261 -85
- package/dist/index.html +70 -20
- package/package.json +1 -1
package/dist/form-builder.js
CHANGED
|
@@ -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";
|
|
@@ -293,7 +305,7 @@ function renderElement(element, ctx) {
|
|
|
293
305
|
const actionBtn = document.createElement("button");
|
|
294
306
|
actionBtn.type = "button";
|
|
295
307
|
actionBtn.className =
|
|
296
|
-
"px-3 py-
|
|
308
|
+
"px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
|
|
297
309
|
actionBtn.textContent = action.label;
|
|
298
310
|
|
|
299
311
|
actionBtn.addEventListener("click", (e) => {
|
|
@@ -304,7 +316,15 @@ function renderElement(element, ctx) {
|
|
|
304
316
|
state.config.actionHandler &&
|
|
305
317
|
typeof state.config.actionHandler === "function"
|
|
306
318
|
) {
|
|
307
|
-
|
|
319
|
+
// For schema-based actions (old system), call with just the value for backward compatibility
|
|
320
|
+
// Check if the handler expects 2 parameters (new system) or 1 parameter (old system)
|
|
321
|
+
if (state.config.actionHandler.length > 1) {
|
|
322
|
+
// New system: pass related_field (element key) and value
|
|
323
|
+
state.config.actionHandler(element.key, action.value);
|
|
324
|
+
} else {
|
|
325
|
+
// Old system: pass only value for backward compatibility
|
|
326
|
+
state.config.actionHandler(action.value);
|
|
327
|
+
}
|
|
308
328
|
}
|
|
309
329
|
});
|
|
310
330
|
|
|
@@ -616,11 +636,11 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
616
636
|
slot.onclick = () => {
|
|
617
637
|
// Look for file input - check parent containers that have space-y-2 class
|
|
618
638
|
let filesWrapper = container.parentElement;
|
|
619
|
-
while (filesWrapper && !filesWrapper.classList.contains(
|
|
639
|
+
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
620
640
|
filesWrapper = filesWrapper.parentElement;
|
|
621
641
|
}
|
|
622
642
|
// If no parent with space-y-2, container itself might be the wrapper
|
|
623
|
-
if (!filesWrapper && container.classList.contains(
|
|
643
|
+
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
624
644
|
filesWrapper = container;
|
|
625
645
|
}
|
|
626
646
|
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
@@ -641,11 +661,11 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
641
661
|
e.stopPropagation();
|
|
642
662
|
// Look for file input - check parent containers that have space-y-2 class
|
|
643
663
|
let filesWrapper = container.parentElement;
|
|
644
|
-
while (filesWrapper && !filesWrapper.classList.contains(
|
|
664
|
+
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
645
665
|
filesWrapper = filesWrapper.parentElement;
|
|
646
666
|
}
|
|
647
667
|
// If no parent with space-y-2, container itself might be the wrapper
|
|
648
|
-
if (!filesWrapper && container.classList.contains(
|
|
668
|
+
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
649
669
|
filesWrapper = container;
|
|
650
670
|
}
|
|
651
671
|
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
@@ -808,11 +828,11 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
808
828
|
slot.onclick = () => {
|
|
809
829
|
// Look for file input - check parent containers that have space-y-2 class
|
|
810
830
|
let filesWrapper = container.parentElement;
|
|
811
|
-
while (filesWrapper && !filesWrapper.classList.contains(
|
|
831
|
+
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
812
832
|
filesWrapper = filesWrapper.parentElement;
|
|
813
833
|
}
|
|
814
834
|
// If no parent with space-y-2, container itself might be the wrapper
|
|
815
|
-
if (!filesWrapper && container.classList.contains(
|
|
835
|
+
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
816
836
|
filesWrapper = container;
|
|
817
837
|
}
|
|
818
838
|
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
@@ -927,6 +947,125 @@ function addDeleteButton(container, onDelete) {
|
|
|
927
947
|
container.appendChild(overlay);
|
|
928
948
|
}
|
|
929
949
|
|
|
950
|
+
// JSON path resolution for external actions (currently unused but kept for future use)
|
|
951
|
+
// eslint-disable-next-line no-unused-vars
|
|
952
|
+
function resolveFieldPath(path, formData) {
|
|
953
|
+
// Remove leading $input_data. prefix if present
|
|
954
|
+
const cleanPath = path.replace(/^\$input_data\./, "");
|
|
955
|
+
|
|
956
|
+
// Split path into segments, handling array notation
|
|
957
|
+
const segments = cleanPath.split(/[.[\]]/).filter(Boolean);
|
|
958
|
+
|
|
959
|
+
// Try to find the corresponding form element
|
|
960
|
+
return findElementByPath(segments, formData);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function findElementByPath(segments, data, currentPath = "") {
|
|
964
|
+
if (segments.length === 0) return currentPath;
|
|
965
|
+
|
|
966
|
+
const [head, ...tail] = segments;
|
|
967
|
+
|
|
968
|
+
// Check if this is an array index
|
|
969
|
+
const isArrayIndex = /^\d+$/.test(head);
|
|
970
|
+
|
|
971
|
+
if (isArrayIndex) {
|
|
972
|
+
// Array index case: build path like "fieldName[index]"
|
|
973
|
+
const newPath = currentPath ? `${currentPath}[${head}]` : `[${head}]`;
|
|
974
|
+
return findElementByPath(tail, data, newPath);
|
|
975
|
+
} else {
|
|
976
|
+
// Regular field name
|
|
977
|
+
const newPath = currentPath ? `${currentPath}.${head}` : head;
|
|
978
|
+
return findElementByPath(tail, data, newPath);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function findFormElementByFieldPath(fieldPath) {
|
|
983
|
+
// Try to find the form element that corresponds to the field path
|
|
984
|
+
// This looks for elements with name attributes that match the path pattern
|
|
985
|
+
|
|
986
|
+
if (!state.formRoot) return null;
|
|
987
|
+
|
|
988
|
+
// Try exact match first
|
|
989
|
+
let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
|
|
990
|
+
if (element) return element;
|
|
991
|
+
|
|
992
|
+
// Try with array notation variations
|
|
993
|
+
const variations = [
|
|
994
|
+
fieldPath,
|
|
995
|
+
fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
|
|
996
|
+
fieldPath.replace(/\./g, "[") +
|
|
997
|
+
"]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
|
|
998
|
+
];
|
|
999
|
+
|
|
1000
|
+
for (const variation of variations) {
|
|
1001
|
+
element = state.formRoot.querySelector(`[name="${variation}"]`);
|
|
1002
|
+
if (element) return element;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function renderExternalActions() {
|
|
1009
|
+
if (!state.externalActions || !Array.isArray(state.externalActions)) return;
|
|
1010
|
+
|
|
1011
|
+
state.externalActions.forEach((action) => {
|
|
1012
|
+
if (!action.related_field || !action.value || !action.label) return;
|
|
1013
|
+
|
|
1014
|
+
// Find the form element for this related field
|
|
1015
|
+
const fieldElement = findFormElementByFieldPath(action.related_field);
|
|
1016
|
+
if (!fieldElement) {
|
|
1017
|
+
console.warn(
|
|
1018
|
+
`External action: Could not find form element for field "${action.related_field}"`,
|
|
1019
|
+
);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Find the wrapper element that contains the field using stable class
|
|
1024
|
+
let wrapper = fieldElement.closest(".fb-field-wrapper");
|
|
1025
|
+
if (!wrapper) {
|
|
1026
|
+
wrapper = fieldElement.parentElement;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (!wrapper) {
|
|
1030
|
+
console.warn(
|
|
1031
|
+
`External action: Could not find wrapper for field "${action.related_field}"`,
|
|
1032
|
+
);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Check if we already added external actions to this wrapper
|
|
1037
|
+
if (wrapper.querySelector(".external-actions-container")) return;
|
|
1038
|
+
|
|
1039
|
+
// Create actions container
|
|
1040
|
+
const actionsContainer = document.createElement("div");
|
|
1041
|
+
actionsContainer.className =
|
|
1042
|
+
"external-actions-container mt-4 flex flex-wrap gap-2";
|
|
1043
|
+
|
|
1044
|
+
// Create action button
|
|
1045
|
+
const actionBtn = document.createElement("button");
|
|
1046
|
+
actionBtn.type = "button";
|
|
1047
|
+
actionBtn.className =
|
|
1048
|
+
"px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
|
|
1049
|
+
actionBtn.textContent = action.label;
|
|
1050
|
+
|
|
1051
|
+
actionBtn.addEventListener("click", (e) => {
|
|
1052
|
+
e.preventDefault();
|
|
1053
|
+
e.stopPropagation();
|
|
1054
|
+
|
|
1055
|
+
if (
|
|
1056
|
+
state.config.actionHandler &&
|
|
1057
|
+
typeof state.config.actionHandler === "function"
|
|
1058
|
+
) {
|
|
1059
|
+
// Call with both related_field and value for the new actions system
|
|
1060
|
+
state.config.actionHandler(action.related_field, action.value);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
actionsContainer.appendChild(actionBtn);
|
|
1065
|
+
wrapper.appendChild(actionsContainer);
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
930
1069
|
function showTooltip(tooltipId, button) {
|
|
931
1070
|
const tooltip = document.getElementById(tooltipId);
|
|
932
1071
|
const isCurrentlyVisible = !tooltip.classList.contains("hidden");
|
|
@@ -1044,11 +1183,17 @@ function validateForm(skipValidation = false) {
|
|
|
1044
1183
|
values.push(val);
|
|
1045
1184
|
|
|
1046
1185
|
if (!skipValidation && val) {
|
|
1047
|
-
if (
|
|
1186
|
+
if (
|
|
1187
|
+
element.minLength !== null &&
|
|
1188
|
+
val.length < element.minLength
|
|
1189
|
+
) {
|
|
1048
1190
|
errors.push(`${key}[${index}]: minLength=${element.minLength}`);
|
|
1049
1191
|
markValidity(input, `minLength=${element.minLength}`);
|
|
1050
1192
|
}
|
|
1051
|
-
if (
|
|
1193
|
+
if (
|
|
1194
|
+
element.maxLength !== null &&
|
|
1195
|
+
val.length > element.maxLength
|
|
1196
|
+
) {
|
|
1052
1197
|
errors.push(`${key}[${index}]: maxLength=${element.maxLength}`);
|
|
1053
1198
|
markValidity(input, `maxLength=${element.maxLength}`);
|
|
1054
1199
|
}
|
|
@@ -1073,7 +1218,7 @@ function validateForm(skipValidation = false) {
|
|
|
1073
1218
|
if (!skipValidation) {
|
|
1074
1219
|
const minCount = element.minCount ?? 1;
|
|
1075
1220
|
const maxCount = element.maxCount ?? 10;
|
|
1076
|
-
const nonEmptyValues = values.filter(v => v.trim() !== "");
|
|
1221
|
+
const nonEmptyValues = values.filter((v) => v.trim() !== "");
|
|
1077
1222
|
|
|
1078
1223
|
if (element.required && nonEmptyValues.length === 0) {
|
|
1079
1224
|
errors.push(`${key}: required`);
|
|
@@ -1156,7 +1301,9 @@ function validateForm(skipValidation = false) {
|
|
|
1156
1301
|
markValidity(input, `> max=${element.max}`);
|
|
1157
1302
|
}
|
|
1158
1303
|
|
|
1159
|
-
const d = Number.isInteger(element.decimals ?? 0)
|
|
1304
|
+
const d = Number.isInteger(element.decimals ?? 0)
|
|
1305
|
+
? element.decimals
|
|
1306
|
+
: 0;
|
|
1160
1307
|
markValidity(input, null);
|
|
1161
1308
|
values.push(Number(v.toFixed(d)));
|
|
1162
1309
|
});
|
|
@@ -1165,7 +1312,7 @@ function validateForm(skipValidation = false) {
|
|
|
1165
1312
|
if (!skipValidation) {
|
|
1166
1313
|
const minCount = element.minCount ?? 1;
|
|
1167
1314
|
const maxCount = element.maxCount ?? 10;
|
|
1168
|
-
const nonNullValues = values.filter(v => v !== null);
|
|
1315
|
+
const nonNullValues = values.filter((v) => v !== null);
|
|
1169
1316
|
|
|
1170
1317
|
if (element.required && nonNullValues.length === 0) {
|
|
1171
1318
|
errors.push(`${key}: required`);
|
|
@@ -1229,7 +1376,7 @@ function validateForm(skipValidation = false) {
|
|
|
1229
1376
|
if (!skipValidation) {
|
|
1230
1377
|
const minCount = element.minCount ?? 1;
|
|
1231
1378
|
const maxCount = element.maxCount ?? 10;
|
|
1232
|
-
const nonEmptyValues = values.filter(v => v !== "");
|
|
1379
|
+
const nonEmptyValues = values.filter((v) => v !== "");
|
|
1233
1380
|
|
|
1234
1381
|
if (element.required && nonEmptyValues.length === 0) {
|
|
1235
1382
|
errors.push(`${key}: required`);
|
|
@@ -1261,9 +1408,11 @@ function validateForm(skipValidation = false) {
|
|
|
1261
1408
|
// Handle file with multiple property like files type
|
|
1262
1409
|
// Find the files list by locating the specific file input for this field
|
|
1263
1410
|
const fullKey = pathJoin(ctx.path, key);
|
|
1264
|
-
const pickerInput = scopeRoot.querySelector(
|
|
1265
|
-
|
|
1266
|
-
|
|
1411
|
+
const pickerInput = scopeRoot.querySelector(
|
|
1412
|
+
`input[type="file"][name="${fullKey}"]`,
|
|
1413
|
+
);
|
|
1414
|
+
const filesWrapper = pickerInput?.closest(".space-y-2");
|
|
1415
|
+
const container = filesWrapper?.querySelector(".files-list") || null;
|
|
1267
1416
|
|
|
1268
1417
|
const resourceIds = [];
|
|
1269
1418
|
if (container) {
|
|
@@ -1351,12 +1500,18 @@ function validateForm(skipValidation = false) {
|
|
|
1351
1500
|
const items = [];
|
|
1352
1501
|
// Use full path for nested group element search
|
|
1353
1502
|
const fullKey = pathJoin(ctx.path, key);
|
|
1354
|
-
const itemElements = scopeRoot.querySelectorAll(
|
|
1503
|
+
const itemElements = scopeRoot.querySelectorAll(
|
|
1504
|
+
`[name^="${fullKey}["]`,
|
|
1505
|
+
);
|
|
1355
1506
|
|
|
1356
1507
|
// Extract actual indices from DOM element names instead of assuming sequential numbering
|
|
1357
1508
|
const actualIndices = new Set();
|
|
1358
1509
|
itemElements.forEach((el) => {
|
|
1359
|
-
const match = el.name.match(
|
|
1510
|
+
const match = el.name.match(
|
|
1511
|
+
new RegExp(
|
|
1512
|
+
`^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[(\\d+)\\]`,
|
|
1513
|
+
),
|
|
1514
|
+
);
|
|
1360
1515
|
if (match) {
|
|
1361
1516
|
actualIndices.add(parseInt(match[1]));
|
|
1362
1517
|
}
|
|
@@ -1374,7 +1529,8 @@ function validateForm(skipValidation = false) {
|
|
|
1374
1529
|
element.elements.forEach((child) => {
|
|
1375
1530
|
if (child.hidden) {
|
|
1376
1531
|
// For hidden child elements, use their default value
|
|
1377
|
-
itemData[child.key] =
|
|
1532
|
+
itemData[child.key] =
|
|
1533
|
+
child.default !== undefined ? child.default : "";
|
|
1378
1534
|
} else {
|
|
1379
1535
|
const childKey = `${fullKey}[${actualIndex}].${child.key}`;
|
|
1380
1536
|
itemData[child.key] = validateElement(
|
|
@@ -1395,7 +1551,8 @@ function validateForm(skipValidation = false) {
|
|
|
1395
1551
|
element.elements.forEach((child) => {
|
|
1396
1552
|
if (child.hidden) {
|
|
1397
1553
|
// For hidden child elements, use their default value
|
|
1398
|
-
groupData[child.key] =
|
|
1554
|
+
groupData[child.key] =
|
|
1555
|
+
child.default !== undefined ? child.default : "";
|
|
1399
1556
|
} else {
|
|
1400
1557
|
const childKey = `${key}.${child.key}`;
|
|
1401
1558
|
groupData[child.key] = validateElement(
|
|
@@ -1427,7 +1584,8 @@ function validateForm(skipValidation = false) {
|
|
|
1427
1584
|
element.elements.forEach((child) => {
|
|
1428
1585
|
if (child.hidden) {
|
|
1429
1586
|
// For hidden child elements, use their default value
|
|
1430
|
-
itemData[child.key] =
|
|
1587
|
+
itemData[child.key] =
|
|
1588
|
+
child.default !== undefined ? child.default : "";
|
|
1431
1589
|
} else {
|
|
1432
1590
|
const childKey = `${key}[${i}].${child.key}`;
|
|
1433
1591
|
itemData[child.key] = validateElement(
|
|
@@ -1465,7 +1623,8 @@ function validateForm(skipValidation = false) {
|
|
|
1465
1623
|
element.elements.forEach((child) => {
|
|
1466
1624
|
if (child.hidden) {
|
|
1467
1625
|
// For hidden child elements, use their default value
|
|
1468
|
-
containerData[child.key] =
|
|
1626
|
+
containerData[child.key] =
|
|
1627
|
+
child.default !== undefined ? child.default : "";
|
|
1469
1628
|
} else {
|
|
1470
1629
|
const childKey = `${key}.${child.key}`;
|
|
1471
1630
|
containerData[child.key] = validateElement(
|
|
@@ -1550,7 +1709,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1550
1709
|
|
|
1551
1710
|
const textInput = document.createElement("input");
|
|
1552
1711
|
textInput.type = "text";
|
|
1553
|
-
textInput.className =
|
|
1712
|
+
textInput.className =
|
|
1713
|
+
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1554
1714
|
textInput.placeholder = element.placeholder || "Enter text";
|
|
1555
1715
|
textInput.value = value;
|
|
1556
1716
|
textInput.readOnly = state.config.readonly;
|
|
@@ -1571,15 +1731,16 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1571
1731
|
|
|
1572
1732
|
function updateRemoveButtons() {
|
|
1573
1733
|
if (state.config.readonly) return;
|
|
1574
|
-
const items = container.querySelectorAll(
|
|
1734
|
+
const items = container.querySelectorAll(".multiple-text-item");
|
|
1575
1735
|
const currentCount = items.length;
|
|
1576
1736
|
items.forEach((item) => {
|
|
1577
|
-
let removeBtn = item.querySelector(
|
|
1737
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1578
1738
|
if (!removeBtn) {
|
|
1579
|
-
removeBtn = document.createElement(
|
|
1580
|
-
removeBtn.type =
|
|
1581
|
-
removeBtn.className =
|
|
1582
|
-
|
|
1739
|
+
removeBtn = document.createElement("button");
|
|
1740
|
+
removeBtn.type = "button";
|
|
1741
|
+
removeBtn.className =
|
|
1742
|
+
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
1743
|
+
removeBtn.innerHTML = "✕";
|
|
1583
1744
|
removeBtn.onclick = () => {
|
|
1584
1745
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1585
1746
|
if (container.children.length > minCount) {
|
|
@@ -1594,8 +1755,8 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1594
1755
|
}
|
|
1595
1756
|
const disabled = currentCount <= minCount;
|
|
1596
1757
|
removeBtn.disabled = disabled;
|
|
1597
|
-
removeBtn.style.opacity = disabled ?
|
|
1598
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
1758
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1759
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1599
1760
|
});
|
|
1600
1761
|
}
|
|
1601
1762
|
|
|
@@ -1606,8 +1767,9 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1606
1767
|
if (!state.config.readonly && values.length < maxCount) {
|
|
1607
1768
|
const addBtn = document.createElement("button");
|
|
1608
1769
|
addBtn.type = "button";
|
|
1609
|
-
addBtn.className =
|
|
1610
|
-
|
|
1770
|
+
addBtn.className =
|
|
1771
|
+
"add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
1772
|
+
addBtn.textContent = `+ Add ${element.label || "Text"}`;
|
|
1611
1773
|
addBtn.onclick = () => {
|
|
1612
1774
|
values.push(element.default || "");
|
|
1613
1775
|
addTextItem(element.default || "");
|
|
@@ -1619,7 +1781,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1619
1781
|
}
|
|
1620
1782
|
|
|
1621
1783
|
// Render initial items
|
|
1622
|
-
values.forEach(value => addTextItem(value));
|
|
1784
|
+
values.forEach((value) => addTextItem(value));
|
|
1623
1785
|
updateAddButton();
|
|
1624
1786
|
updateRemoveButtons();
|
|
1625
1787
|
|
|
@@ -1679,7 +1841,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1679
1841
|
itemWrapper.className = "multiple-textarea-item";
|
|
1680
1842
|
|
|
1681
1843
|
const textareaInput = document.createElement("textarea");
|
|
1682
|
-
textareaInput.className =
|
|
1844
|
+
textareaInput.className =
|
|
1845
|
+
"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
1846
|
textareaInput.placeholder = element.placeholder || "Enter text";
|
|
1684
1847
|
textareaInput.rows = element.rows || 4;
|
|
1685
1848
|
textareaInput.value = value;
|
|
@@ -1701,15 +1864,16 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1701
1864
|
|
|
1702
1865
|
function updateRemoveButtons() {
|
|
1703
1866
|
if (state.config.readonly) return;
|
|
1704
|
-
const items = container.querySelectorAll(
|
|
1867
|
+
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
1705
1868
|
const currentCount = items.length;
|
|
1706
1869
|
items.forEach((item) => {
|
|
1707
|
-
let removeBtn = item.querySelector(
|
|
1870
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1708
1871
|
if (!removeBtn) {
|
|
1709
|
-
removeBtn = document.createElement(
|
|
1710
|
-
removeBtn.type =
|
|
1711
|
-
removeBtn.className =
|
|
1712
|
-
|
|
1872
|
+
removeBtn = document.createElement("button");
|
|
1873
|
+
removeBtn.type = "button";
|
|
1874
|
+
removeBtn.className =
|
|
1875
|
+
"remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
|
|
1876
|
+
removeBtn.innerHTML = "✕ Remove";
|
|
1713
1877
|
removeBtn.onclick = () => {
|
|
1714
1878
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1715
1879
|
if (container.children.length > minCount) {
|
|
@@ -1724,8 +1888,8 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1724
1888
|
}
|
|
1725
1889
|
const disabled = currentCount <= minCount;
|
|
1726
1890
|
removeBtn.disabled = disabled;
|
|
1727
|
-
removeBtn.style.opacity = disabled ?
|
|
1728
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
1891
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1892
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1729
1893
|
});
|
|
1730
1894
|
}
|
|
1731
1895
|
|
|
@@ -1736,8 +1900,9 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1736
1900
|
if (!state.config.readonly && values.length < maxCount) {
|
|
1737
1901
|
const addBtn = document.createElement("button");
|
|
1738
1902
|
addBtn.type = "button";
|
|
1739
|
-
addBtn.className =
|
|
1740
|
-
|
|
1903
|
+
addBtn.className =
|
|
1904
|
+
"add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
1905
|
+
addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
|
|
1741
1906
|
addBtn.onclick = () => {
|
|
1742
1907
|
values.push(element.default || "");
|
|
1743
1908
|
addTextareaItem(element.default || "");
|
|
@@ -1749,7 +1914,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
1749
1914
|
}
|
|
1750
1915
|
|
|
1751
1916
|
// Render initial items
|
|
1752
|
-
values.forEach(value => addTextareaItem(value));
|
|
1917
|
+
values.forEach((value) => addTextareaItem(value));
|
|
1753
1918
|
updateAddButton();
|
|
1754
1919
|
updateRemoveButtons();
|
|
1755
1920
|
|
|
@@ -1813,7 +1978,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1813
1978
|
|
|
1814
1979
|
const numberInput = document.createElement("input");
|
|
1815
1980
|
numberInput.type = "number";
|
|
1816
|
-
numberInput.className =
|
|
1981
|
+
numberInput.className =
|
|
1982
|
+
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1817
1983
|
numberInput.placeholder = element.placeholder || "0";
|
|
1818
1984
|
if (element.min !== undefined) numberInput.min = element.min;
|
|
1819
1985
|
if (element.max !== undefined) numberInput.max = element.max;
|
|
@@ -1837,15 +2003,16 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1837
2003
|
|
|
1838
2004
|
function updateRemoveButtons() {
|
|
1839
2005
|
if (state.config.readonly) return;
|
|
1840
|
-
const items = container.querySelectorAll(
|
|
2006
|
+
const items = container.querySelectorAll(".multiple-number-item");
|
|
1841
2007
|
const currentCount = items.length;
|
|
1842
2008
|
items.forEach((item) => {
|
|
1843
|
-
let removeBtn = item.querySelector(
|
|
2009
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1844
2010
|
if (!removeBtn) {
|
|
1845
|
-
removeBtn = document.createElement(
|
|
1846
|
-
removeBtn.type =
|
|
1847
|
-
removeBtn.className =
|
|
1848
|
-
|
|
2011
|
+
removeBtn = document.createElement("button");
|
|
2012
|
+
removeBtn.type = "button";
|
|
2013
|
+
removeBtn.className =
|
|
2014
|
+
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2015
|
+
removeBtn.innerHTML = "✕";
|
|
1849
2016
|
removeBtn.onclick = () => {
|
|
1850
2017
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1851
2018
|
if (container.children.length > minCount) {
|
|
@@ -1860,8 +2027,8 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1860
2027
|
}
|
|
1861
2028
|
const disabled = currentCount <= minCount;
|
|
1862
2029
|
removeBtn.disabled = disabled;
|
|
1863
|
-
removeBtn.style.opacity = disabled ?
|
|
1864
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
2030
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2031
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1865
2032
|
});
|
|
1866
2033
|
}
|
|
1867
2034
|
|
|
@@ -1872,8 +2039,9 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1872
2039
|
if (!state.config.readonly && values.length < maxCount) {
|
|
1873
2040
|
const addBtn = document.createElement("button");
|
|
1874
2041
|
addBtn.type = "button";
|
|
1875
|
-
addBtn.className =
|
|
1876
|
-
|
|
2042
|
+
addBtn.className =
|
|
2043
|
+
"add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2044
|
+
addBtn.textContent = `+ Add ${element.label || "Number"}`;
|
|
1877
2045
|
addBtn.onclick = () => {
|
|
1878
2046
|
values.push(element.default || "");
|
|
1879
2047
|
addNumberItem(element.default || "");
|
|
@@ -1885,7 +2053,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1885
2053
|
}
|
|
1886
2054
|
|
|
1887
2055
|
// Render initial items
|
|
1888
|
-
values.forEach(value => addNumberItem(value));
|
|
2056
|
+
values.forEach((value) => addNumberItem(value));
|
|
1889
2057
|
updateAddButton();
|
|
1890
2058
|
updateRemoveButtons();
|
|
1891
2059
|
|
|
@@ -1931,7 +2099,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1931
2099
|
const maxCount = element.maxCount ?? 10;
|
|
1932
2100
|
|
|
1933
2101
|
while (values.length < minCount) {
|
|
1934
|
-
values.push(element.default ||
|
|
2102
|
+
values.push(element.default || element.options?.[0]?.value || "");
|
|
1935
2103
|
}
|
|
1936
2104
|
|
|
1937
2105
|
const container = document.createElement("div");
|
|
@@ -1953,7 +2121,8 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1953
2121
|
itemWrapper.className = "multiple-select-item flex items-center gap-2";
|
|
1954
2122
|
|
|
1955
2123
|
const selectInput = document.createElement("select");
|
|
1956
|
-
selectInput.className =
|
|
2124
|
+
selectInput.className =
|
|
2125
|
+
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1957
2126
|
selectInput.disabled = state.config.readonly;
|
|
1958
2127
|
|
|
1959
2128
|
// Add options
|
|
@@ -1983,15 +2152,16 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1983
2152
|
|
|
1984
2153
|
function updateRemoveButtons() {
|
|
1985
2154
|
if (state.config.readonly) return;
|
|
1986
|
-
const items = container.querySelectorAll(
|
|
2155
|
+
const items = container.querySelectorAll(".multiple-select-item");
|
|
1987
2156
|
const currentCount = items.length;
|
|
1988
2157
|
items.forEach((item) => {
|
|
1989
|
-
let removeBtn = item.querySelector(
|
|
2158
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1990
2159
|
if (!removeBtn) {
|
|
1991
|
-
removeBtn = document.createElement(
|
|
1992
|
-
removeBtn.type =
|
|
1993
|
-
removeBtn.className =
|
|
1994
|
-
|
|
2160
|
+
removeBtn = document.createElement("button");
|
|
2161
|
+
removeBtn.type = "button";
|
|
2162
|
+
removeBtn.className =
|
|
2163
|
+
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2164
|
+
removeBtn.innerHTML = "✕";
|
|
1995
2165
|
removeBtn.onclick = () => {
|
|
1996
2166
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1997
2167
|
if (container.children.length > minCount) {
|
|
@@ -2006,8 +2176,8 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2006
2176
|
}
|
|
2007
2177
|
const disabled = currentCount <= minCount;
|
|
2008
2178
|
removeBtn.disabled = disabled;
|
|
2009
|
-
removeBtn.style.opacity = disabled ?
|
|
2010
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
2179
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2180
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2011
2181
|
});
|
|
2012
2182
|
}
|
|
2013
2183
|
|
|
@@ -2018,10 +2188,12 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2018
2188
|
if (!state.config.readonly && values.length < maxCount) {
|
|
2019
2189
|
const addBtn = document.createElement("button");
|
|
2020
2190
|
addBtn.type = "button";
|
|
2021
|
-
addBtn.className =
|
|
2022
|
-
|
|
2191
|
+
addBtn.className =
|
|
2192
|
+
"add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2193
|
+
addBtn.textContent = `+ Add ${element.label || "Selection"}`;
|
|
2023
2194
|
addBtn.onclick = () => {
|
|
2024
|
-
const defaultValue =
|
|
2195
|
+
const defaultValue =
|
|
2196
|
+
element.default || element.options?.[0]?.value || "";
|
|
2025
2197
|
values.push(defaultValue);
|
|
2026
2198
|
addSelectItem(defaultValue);
|
|
2027
2199
|
updateAddButton();
|
|
@@ -2032,7 +2204,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2032
2204
|
}
|
|
2033
2205
|
|
|
2034
2206
|
// Render initial items
|
|
2035
|
-
values.forEach(value => addSelectItem(value));
|
|
2207
|
+
values.forEach((value) => addSelectItem(value));
|
|
2036
2208
|
updateAddButton();
|
|
2037
2209
|
updateRemoveButtons();
|
|
2038
2210
|
|
|
@@ -2321,17 +2493,18 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2321
2493
|
// Show count and min/max info
|
|
2322
2494
|
const countInfo = document.createElement("div");
|
|
2323
2495
|
countInfo.className = "text-xs text-gray-500 mt-2";
|
|
2324
|
-
const countText = `${initialFiles.length} file${initialFiles.length !== 1 ?
|
|
2325
|
-
const minMaxText =
|
|
2326
|
-
|
|
2327
|
-
|
|
2496
|
+
const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
|
|
2497
|
+
const minMaxText =
|
|
2498
|
+
minFiles > 0 || maxFiles < Infinity
|
|
2499
|
+
? ` (${minFiles}-${maxFiles} allowed)`
|
|
2500
|
+
: "";
|
|
2328
2501
|
countInfo.textContent = countText + minMaxText;
|
|
2329
2502
|
|
|
2330
2503
|
// Remove previous count info
|
|
2331
|
-
const existingCount = filesWrapper.querySelector(
|
|
2504
|
+
const existingCount = filesWrapper.querySelector(".file-count-info");
|
|
2332
2505
|
if (existingCount) existingCount.remove();
|
|
2333
2506
|
|
|
2334
|
-
countInfo.className +=
|
|
2507
|
+
countInfo.className += " file-count-info";
|
|
2335
2508
|
filesWrapper.appendChild(countInfo);
|
|
2336
2509
|
};
|
|
2337
2510
|
|
|
@@ -2639,7 +2812,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2639
2812
|
prefill: {},
|
|
2640
2813
|
};
|
|
2641
2814
|
const item = document.createElement("div");
|
|
2642
|
-
item.className =
|
|
2815
|
+
item.className =
|
|
2816
|
+
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2643
2817
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
2644
2818
|
|
|
2645
2819
|
element.elements.forEach((child) => {
|
|
@@ -2678,7 +2852,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2678
2852
|
addBtn.disabled = currentCount >= max;
|
|
2679
2853
|
addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
2680
2854
|
}
|
|
2681
|
-
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ?
|
|
2855
|
+
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "∞" : max})</span>`;
|
|
2682
2856
|
};
|
|
2683
2857
|
|
|
2684
2858
|
if (!state.config.readonly) {
|
|
@@ -2693,7 +2867,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2693
2867
|
prefill: prefillObj || {},
|
|
2694
2868
|
};
|
|
2695
2869
|
const item = document.createElement("div");
|
|
2696
|
-
item.className =
|
|
2870
|
+
item.className =
|
|
2871
|
+
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2697
2872
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
2698
2873
|
|
|
2699
2874
|
element.elements.forEach((child) => {
|
|
@@ -2731,7 +2906,8 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
2731
2906
|
prefill: {},
|
|
2732
2907
|
};
|
|
2733
2908
|
const item = document.createElement("div");
|
|
2734
|
-
item.className =
|
|
2909
|
+
item.className =
|
|
2910
|
+
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
2735
2911
|
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
2736
2912
|
|
|
2737
2913
|
element.elements.forEach((child) => {
|