@dmitryvim/form-builder 0.1.37 → 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 +60 -74
- package/dist/form-builder.js +201 -85
- package/dist/index.html +1 -1
- 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/README.md
CHANGED
|
@@ -105,9 +105,9 @@ npm install @dmitryvim/form-builder
|
|
|
105
105
|
"extensions": ["jpg", "png", "webp"]
|
|
106
106
|
},
|
|
107
107
|
"actions": [
|
|
108
|
-
{ "
|
|
109
|
-
{ "
|
|
110
|
-
{ "
|
|
108
|
+
{ "key": "enhance", "label": "Enhance Quality" },
|
|
109
|
+
{ "key": "crop", "label": "Auto Crop" },
|
|
110
|
+
{ "key": "retry", "label": "Try Again" }
|
|
111
111
|
]
|
|
112
112
|
},
|
|
113
113
|
{
|
|
@@ -152,9 +152,14 @@ FormBuilder.setUploadHandler(async (file) => {
|
|
|
152
152
|
// Your upload logic - return resource ID
|
|
153
153
|
return "resource-123";
|
|
154
154
|
});
|
|
155
|
-
FormBuilder.setActionHandler((value) => {
|
|
155
|
+
FormBuilder.setActionHandler((value, key, relatedField) => {
|
|
156
156
|
// Handle action button clicks
|
|
157
|
-
console.log("Action clicked:", value);
|
|
157
|
+
console.log("Action clicked:", { value, key, relatedField });
|
|
158
|
+
if (relatedField) {
|
|
159
|
+
// Field-level action
|
|
160
|
+
} else {
|
|
161
|
+
// Form-level action
|
|
162
|
+
}
|
|
158
163
|
});
|
|
159
164
|
FormBuilder.renderForm(schema, prefillData);
|
|
160
165
|
```
|
package/dist/demo.js
CHANGED
|
@@ -19,9 +19,9 @@ const EXAMPLE_SCHEMA = {
|
|
|
19
19
|
},
|
|
20
20
|
maxSizeMB: 10,
|
|
21
21
|
actions: [
|
|
22
|
-
{
|
|
23
|
-
{
|
|
24
|
-
{
|
|
22
|
+
{ key: "retry", label: "А давай ещё разок" },
|
|
23
|
+
{ key: "enhance", label: "Улучшить качество" },
|
|
24
|
+
{ key: "crop", label: "Обрезать изображение" },
|
|
25
25
|
],
|
|
26
26
|
},
|
|
27
27
|
{
|
|
@@ -323,38 +323,6 @@ class InMemoryFileStorage {
|
|
|
323
323
|
// Initialize file storage
|
|
324
324
|
const fileStorage = new InMemoryFileStorage();
|
|
325
325
|
|
|
326
|
-
// Cache for action value -> label mapping for efficient lookup
|
|
327
|
-
let actionLabelMap = new Map();
|
|
328
|
-
|
|
329
|
-
// Build action value -> label mapping from schema for efficient lookup
|
|
330
|
-
function buildActionLabelMap(schema) {
|
|
331
|
-
const map = new Map();
|
|
332
|
-
|
|
333
|
-
function processElements(elements) {
|
|
334
|
-
if (!Array.isArray(elements)) return;
|
|
335
|
-
|
|
336
|
-
for (const element of elements) {
|
|
337
|
-
if (element.actions && Array.isArray(element.actions)) {
|
|
338
|
-
for (const action of element.actions) {
|
|
339
|
-
if (action.value && action.label) {
|
|
340
|
-
map.set(action.value, action.label);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
// Process nested group elements
|
|
346
|
-
if (element.elements && Array.isArray(element.elements)) {
|
|
347
|
-
processElements(element.elements);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (schema && schema.elements) {
|
|
353
|
-
processElements(schema.elements);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
return map;
|
|
357
|
-
}
|
|
358
326
|
|
|
359
327
|
// DOM element references
|
|
360
328
|
const el = {
|
|
@@ -429,16 +397,21 @@ function parseActions(actionsText) {
|
|
|
429
397
|
if (!action || typeof action !== "object") {
|
|
430
398
|
throw new Error(`Action at index ${i} must be an object`);
|
|
431
399
|
}
|
|
432
|
-
if (!action.
|
|
400
|
+
if (!action.key || typeof action.key !== "string") {
|
|
433
401
|
throw new Error(
|
|
434
|
-
`Action at index ${i} missing valid '
|
|
402
|
+
`Action at index ${i} missing valid 'key' property`,
|
|
435
403
|
);
|
|
436
404
|
}
|
|
437
405
|
if (!action.value || typeof action.value !== "string") {
|
|
438
406
|
throw new Error(`Action at index ${i} missing valid 'value' property`);
|
|
439
407
|
}
|
|
440
|
-
if
|
|
441
|
-
|
|
408
|
+
// related_field is optional - if not provided, action is form-level
|
|
409
|
+
if (action.related_field && typeof action.related_field !== "string") {
|
|
410
|
+
throw new Error(`Action at index ${i} has invalid 'related_field' property type`);
|
|
411
|
+
}
|
|
412
|
+
// Label is optional - will be resolved from schema or key
|
|
413
|
+
if (action.label && typeof action.label !== "string") {
|
|
414
|
+
throw new Error(`Action at index ${i} has invalid 'label' property type`);
|
|
442
415
|
}
|
|
443
416
|
}
|
|
444
417
|
|
|
@@ -480,43 +453,28 @@ function setupFormBuilder() {
|
|
|
480
453
|
});
|
|
481
454
|
|
|
482
455
|
// Action handler - display message when action button is clicked
|
|
483
|
-
//
|
|
484
|
-
FormBuilder.setActionHandler((
|
|
485
|
-
let actionLabel, actionValue, relatedField;
|
|
486
|
-
|
|
487
|
-
// Determine if this is the old system (1 param) or new system (2 params)
|
|
488
|
-
if (arguments.length === 1) {
|
|
489
|
-
// Old system: only value parameter
|
|
490
|
-
actionValue = relatedFieldOrValue;
|
|
491
|
-
actionLabel = actionLabelMap.get(actionValue) || actionValue;
|
|
492
|
-
relatedField = null;
|
|
493
|
-
} else {
|
|
494
|
-
// New system: related_field and value parameters
|
|
495
|
-
relatedField = relatedFieldOrValue;
|
|
496
|
-
actionValue = value;
|
|
497
|
-
actionLabel = `Action for ${relatedField}`;
|
|
498
|
-
}
|
|
499
|
-
|
|
456
|
+
// New system: value, key, related_field parameters
|
|
457
|
+
FormBuilder.setActionHandler((value, key, relatedField) => {
|
|
500
458
|
console.log("Action clicked:", {
|
|
501
|
-
|
|
502
|
-
|
|
459
|
+
value,
|
|
460
|
+
key,
|
|
503
461
|
relatedField,
|
|
504
|
-
|
|
462
|
+
type: relatedField ? "field-level" : "form-level",
|
|
505
463
|
});
|
|
506
464
|
|
|
507
465
|
// Show message to user (compatible with all environments)
|
|
508
466
|
if (typeof window !== "undefined" && window.alert) {
|
|
509
467
|
if (relatedField) {
|
|
510
468
|
window.alert(
|
|
511
|
-
`
|
|
469
|
+
`Field Action: "${key}" clicked for field "${relatedField}" with value: ${value}`,
|
|
512
470
|
);
|
|
513
471
|
} else {
|
|
514
|
-
window.alert(`
|
|
472
|
+
window.alert(`Form Action: "${key}" clicked with value: ${value}`);
|
|
515
473
|
}
|
|
516
474
|
} else {
|
|
517
475
|
console.log(
|
|
518
|
-
`Demo action: ${
|
|
519
|
-
relatedField ? ` for field: ${relatedField}` : "",
|
|
476
|
+
`Demo action: ${key} clicked with value: ${value}`,
|
|
477
|
+
relatedField ? ` for field: ${relatedField}` : " (form-level)",
|
|
520
478
|
);
|
|
521
479
|
}
|
|
522
480
|
});
|
|
@@ -553,8 +511,6 @@ function applyCurrentSchema() {
|
|
|
553
511
|
return false;
|
|
554
512
|
}
|
|
555
513
|
|
|
556
|
-
// Build action value -> label map for efficient lookup
|
|
557
|
-
actionLabelMap = buildActionLabelMap(schema);
|
|
558
514
|
|
|
559
515
|
// Set mode based on toggle
|
|
560
516
|
const isReadOnly = el.readOnlyToggle.checked;
|
|
@@ -801,25 +757,55 @@ el.clearActionsBtn.addEventListener("click", () => {
|
|
|
801
757
|
|
|
802
758
|
// Example external actions for demonstration
|
|
803
759
|
const EXAMPLE_ACTIONS = [
|
|
760
|
+
// Field-level actions using predefined labels from schema
|
|
761
|
+
{
|
|
762
|
+
related_field: "cover",
|
|
763
|
+
key: "retry",
|
|
764
|
+
value: "regenerate_cover_image", // Specific action value for handler
|
|
765
|
+
},
|
|
766
|
+
{
|
|
767
|
+
related_field: "cover",
|
|
768
|
+
key: "enhance",
|
|
769
|
+
value: "enhance_cover_quality", // Specific action value for handler
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
related_field: "cover",
|
|
773
|
+
key: "crop",
|
|
774
|
+
value: "auto_crop_cover", // Specific action value for handler
|
|
775
|
+
},
|
|
776
|
+
// Field-level actions with custom labels
|
|
804
777
|
{
|
|
805
778
|
related_field: "title[0]",
|
|
806
|
-
|
|
807
|
-
|
|
779
|
+
key: "generate",
|
|
780
|
+
value: "ai_generate_title",
|
|
781
|
+
label: "🤖 Generate Title", // Custom label overrides schema
|
|
808
782
|
},
|
|
809
783
|
{
|
|
810
784
|
related_field: "description",
|
|
811
|
-
|
|
812
|
-
|
|
785
|
+
key: "improve",
|
|
786
|
+
value: "ai_improve_description", // Key will be used as fallback label
|
|
813
787
|
},
|
|
814
788
|
{
|
|
815
789
|
related_field: "slides[0].title",
|
|
816
|
-
|
|
817
|
-
|
|
790
|
+
key: "optimize",
|
|
791
|
+
value: "ai_optimize_slide_title",
|
|
792
|
+
label: "🎯 Optimize Slide Title", // Custom label
|
|
818
793
|
},
|
|
794
|
+
// Form-level actions (no related_field)
|
|
819
795
|
{
|
|
820
|
-
|
|
821
|
-
value: "
|
|
822
|
-
label: "
|
|
796
|
+
key: "save-draft",
|
|
797
|
+
value: "save_form_draft",
|
|
798
|
+
label: "💾 Save Draft",
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
key: "preview",
|
|
802
|
+
value: "preview_infographic",
|
|
803
|
+
label: "👁️ Preview",
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
key: "export",
|
|
807
|
+
value: "export_json_data",
|
|
808
|
+
label: "📄 Export JSON",
|
|
823
809
|
},
|
|
824
810
|
];
|
|
825
811
|
|
package/dist/form-builder.js
CHANGED
|
@@ -290,50 +290,8 @@ function renderElement(element, ctx) {
|
|
|
290
290
|
}
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
state.config.readonly &&
|
|
296
|
-
element.actions &&
|
|
297
|
-
Array.isArray(element.actions) &&
|
|
298
|
-
element.actions.length > 0
|
|
299
|
-
) {
|
|
300
|
-
const actionsContainer = document.createElement("div");
|
|
301
|
-
actionsContainer.className = "mt-3 flex flex-wrap gap-2";
|
|
302
|
-
|
|
303
|
-
element.actions.forEach((action) => {
|
|
304
|
-
if (action.value && action.label) {
|
|
305
|
-
const actionBtn = document.createElement("button");
|
|
306
|
-
actionBtn.type = "button";
|
|
307
|
-
actionBtn.className =
|
|
308
|
-
"px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
|
|
309
|
-
actionBtn.textContent = action.label;
|
|
310
|
-
|
|
311
|
-
actionBtn.addEventListener("click", (e) => {
|
|
312
|
-
e.preventDefault();
|
|
313
|
-
e.stopPropagation();
|
|
314
|
-
|
|
315
|
-
if (
|
|
316
|
-
state.config.actionHandler &&
|
|
317
|
-
typeof state.config.actionHandler === "function"
|
|
318
|
-
) {
|
|
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
|
-
}
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
actionsContainer.appendChild(actionBtn);
|
|
332
|
-
}
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
wrapper.appendChild(actionsContainer);
|
|
336
|
-
}
|
|
293
|
+
// Actions are now only rendered via external actions system in renderExternalActions()
|
|
294
|
+
// element.actions are only used for label lookup, not direct rendering
|
|
337
295
|
|
|
338
296
|
return wrapper;
|
|
339
297
|
}
|
|
@@ -597,6 +555,23 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
597
555
|
}
|
|
598
556
|
}
|
|
599
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
|
+
|
|
600
575
|
function renderResourcePills(container, rids, onRemove) {
|
|
601
576
|
clear(container);
|
|
602
577
|
|
|
@@ -721,21 +696,7 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
721
696
|
slot.appendChild(img);
|
|
722
697
|
} else if (state.config.getThumbnail) {
|
|
723
698
|
// Use getThumbnail for uploaded files
|
|
724
|
-
|
|
725
|
-
img.className = "w-full h-full object-contain";
|
|
726
|
-
img.alt = meta.name;
|
|
727
|
-
|
|
728
|
-
const url = state.config.getThumbnail(rid);
|
|
729
|
-
if (url) {
|
|
730
|
-
img.src = url;
|
|
731
|
-
slot.appendChild(img);
|
|
732
|
-
} else {
|
|
733
|
-
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
734
|
-
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
735
|
-
<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"/>
|
|
736
|
-
</svg>
|
|
737
|
-
</div>`;
|
|
738
|
-
}
|
|
699
|
+
renderThumbnailForResource(slot, rid, meta);
|
|
739
700
|
} else {
|
|
740
701
|
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
741
702
|
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
@@ -985,21 +946,48 @@ function findFormElementByFieldPath(fieldPath) {
|
|
|
985
946
|
|
|
986
947
|
if (!state.formRoot) return null;
|
|
987
948
|
|
|
988
|
-
//
|
|
989
|
-
|
|
990
|
-
|
|
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;
|
|
991
954
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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
|
+
}
|
|
999
968
|
|
|
1000
|
-
for
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
+
}
|
|
1003
991
|
}
|
|
1004
992
|
|
|
1005
993
|
return null;
|
|
@@ -1008,14 +996,32 @@ function findFormElementByFieldPath(fieldPath) {
|
|
|
1008
996
|
function renderExternalActions() {
|
|
1009
997
|
if (!state.externalActions || !Array.isArray(state.externalActions)) return;
|
|
1010
998
|
|
|
999
|
+
// Group actions by related_field (null for form-level actions)
|
|
1000
|
+
const actionsByField = new Map();
|
|
1001
|
+
const formLevelActions = [];
|
|
1002
|
+
|
|
1011
1003
|
state.externalActions.forEach((action) => {
|
|
1012
|
-
if (!action.
|
|
1004
|
+
if (!action.key || !action.value) return;
|
|
1013
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) => {
|
|
1014
1020
|
// Find the form element for this related field
|
|
1015
|
-
const fieldElement = findFormElementByFieldPath(
|
|
1021
|
+
const fieldElement = findFormElementByFieldPath(fieldPath);
|
|
1016
1022
|
if (!fieldElement) {
|
|
1017
1023
|
console.warn(
|
|
1018
|
-
`External action: Could not find form element for field "${
|
|
1024
|
+
`External action: Could not find form element for field "${fieldPath}"`,
|
|
1019
1025
|
);
|
|
1020
1026
|
return;
|
|
1021
1027
|
}
|
|
@@ -1028,25 +1034,91 @@ function renderExternalActions() {
|
|
|
1028
1034
|
|
|
1029
1035
|
if (!wrapper) {
|
|
1030
1036
|
console.warn(
|
|
1031
|
-
`External action: Could not find wrapper for field "${
|
|
1037
|
+
`External action: Could not find wrapper for field "${fieldPath}"`,
|
|
1032
1038
|
);
|
|
1033
1039
|
return;
|
|
1034
1040
|
}
|
|
1035
1041
|
|
|
1036
|
-
//
|
|
1037
|
-
|
|
1042
|
+
// Remove any existing actions container
|
|
1043
|
+
const existingContainer = wrapper.querySelector(".external-actions-container");
|
|
1044
|
+
if (existingContainer) {
|
|
1045
|
+
existingContainer.remove();
|
|
1046
|
+
}
|
|
1038
1047
|
|
|
1039
1048
|
// Create actions container
|
|
1040
1049
|
const actionsContainer = document.createElement("div");
|
|
1041
1050
|
actionsContainer.className =
|
|
1042
|
-
"external-actions-container mt-
|
|
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;
|
|
1043
1097
|
|
|
1044
|
-
|
|
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) => {
|
|
1045
1111
|
const actionBtn = document.createElement("button");
|
|
1046
1112
|
actionBtn.type = "button";
|
|
1047
1113
|
actionBtn.className =
|
|
1048
|
-
"
|
|
1049
|
-
|
|
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;
|
|
1050
1122
|
|
|
1051
1123
|
actionBtn.addEventListener("click", (e) => {
|
|
1052
1124
|
e.preventDefault();
|
|
@@ -1056,14 +1128,58 @@ function renderExternalActions() {
|
|
|
1056
1128
|
state.config.actionHandler &&
|
|
1057
1129
|
typeof state.config.actionHandler === "function"
|
|
1058
1130
|
) {
|
|
1059
|
-
// Call with
|
|
1060
|
-
state.config.actionHandler(action.
|
|
1131
|
+
// Call with value, key, and null related_field for form-level actions
|
|
1132
|
+
state.config.actionHandler(action.value, action.key, null);
|
|
1061
1133
|
}
|
|
1062
1134
|
});
|
|
1063
1135
|
|
|
1064
1136
|
actionsContainer.appendChild(actionBtn);
|
|
1065
|
-
wrapper.appendChild(actionsContainer);
|
|
1066
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;
|
|
1067
1183
|
}
|
|
1068
1184
|
|
|
1069
1185
|
function showTooltip(tooltipId, button) {
|
package/dist/index.html
CHANGED
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
id="actionsTextarea"
|
|
297
297
|
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 font-mono text-sm resize-none flex-1"
|
|
298
298
|
spellcheck="false"
|
|
299
|
-
placeholder='[{"
|
|
299
|
+
placeholder='[{"key": "action-key", "value": "specific-value", "related_field": "fieldName", "label": "Button Label"}]'
|
|
300
300
|
></textarea>
|
|
301
301
|
<div
|
|
302
302
|
id="actionsErrors"
|