@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.
- package/README.md +10 -5
- package/dist/demo.js +182 -54
- package/dist/elements.html +588 -160
- package/dist/elements.js +270 -232
- package/dist/form-builder.js +426 -134
- package/dist/index.html +70 -20
- package/package.json +1 -1
- package/docs/13_form_builder.html +0 -1337
- package/docs/REQUIREMENTS.md +0 -313
- package/docs/integration.md +0 -480
- package/docs/schema.md +0 -433
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";
|
|
@@ -278,42 +290,8 @@ function renderElement(element, ctx) {
|
|
|
278
290
|
}
|
|
279
291
|
}
|
|
280
292
|
|
|
281
|
-
//
|
|
282
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 (
|
|
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 (
|
|
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)
|
|
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(
|
|
1265
|
-
|
|
1266
|
-
|
|
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(
|
|
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(
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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] =
|
|
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 =
|
|
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(
|
|
1850
|
+
const items = container.querySelectorAll(".multiple-text-item");
|
|
1575
1851
|
const currentCount = items.length;
|
|
1576
1852
|
items.forEach((item) => {
|
|
1577
|
-
let removeBtn = item.querySelector(
|
|
1853
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1578
1854
|
if (!removeBtn) {
|
|
1579
|
-
removeBtn = document.createElement(
|
|
1580
|
-
removeBtn.type =
|
|
1581
|
-
removeBtn.className =
|
|
1582
|
-
|
|
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 ?
|
|
1598
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
1610
|
-
|
|
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 =
|
|
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(
|
|
1983
|
+
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
1705
1984
|
const currentCount = items.length;
|
|
1706
1985
|
items.forEach((item) => {
|
|
1707
|
-
let removeBtn = item.querySelector(
|
|
1986
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1708
1987
|
if (!removeBtn) {
|
|
1709
|
-
removeBtn = document.createElement(
|
|
1710
|
-
removeBtn.type =
|
|
1711
|
-
removeBtn.className =
|
|
1712
|
-
|
|
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 ?
|
|
1728
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
1740
|
-
|
|
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 =
|
|
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(
|
|
2122
|
+
const items = container.querySelectorAll(".multiple-number-item");
|
|
1841
2123
|
const currentCount = items.length;
|
|
1842
2124
|
items.forEach((item) => {
|
|
1843
|
-
let removeBtn = item.querySelector(
|
|
2125
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1844
2126
|
if (!removeBtn) {
|
|
1845
|
-
removeBtn = document.createElement(
|
|
1846
|
-
removeBtn.type =
|
|
1847
|
-
removeBtn.className =
|
|
1848
|
-
|
|
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 ?
|
|
1864
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
1876
|
-
|
|
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 ||
|
|
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 =
|
|
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(
|
|
2271
|
+
const items = container.querySelectorAll(".multiple-select-item");
|
|
1987
2272
|
const currentCount = items.length;
|
|
1988
2273
|
items.forEach((item) => {
|
|
1989
|
-
let removeBtn = item.querySelector(
|
|
2274
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1990
2275
|
if (!removeBtn) {
|
|
1991
|
-
removeBtn = document.createElement(
|
|
1992
|
-
removeBtn.type =
|
|
1993
|
-
removeBtn.className =
|
|
1994
|
-
|
|
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 ?
|
|
2010
|
-
removeBtn.style.pointerEvents = disabled ?
|
|
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 =
|
|
2022
|
-
|
|
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 =
|
|
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 ?
|
|
2325
|
-
const minMaxText =
|
|
2326
|
-
|
|
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(
|
|
2620
|
+
const existingCount = filesWrapper.querySelector(".file-count-info");
|
|
2332
2621
|
if (existingCount) existingCount.remove();
|
|
2333
2622
|
|
|
2334
|
-
countInfo.className +=
|
|
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 =
|
|
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 ?
|
|
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 =
|
|
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 =
|
|
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) => {
|