@dmitryvim/form-builder 0.1.37 → 0.1.39
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 +77 -74
- package/dist/form-builder.js +216 -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
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
const EXAMPLE_SCHEMA = {
|
|
6
6
|
version: "0.3",
|
|
7
7
|
title: "Asset Uploader with Slides",
|
|
8
|
+
actions: [
|
|
9
|
+
{
|
|
10
|
+
key: "test-missing",
|
|
11
|
+
label: "Другая потеря"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
key: "save-draft",
|
|
15
|
+
label: "Сохранить черновик"
|
|
16
|
+
}
|
|
17
|
+
],
|
|
8
18
|
elements: [
|
|
9
19
|
{
|
|
10
20
|
type: "file",
|
|
@@ -19,9 +29,9 @@ const EXAMPLE_SCHEMA = {
|
|
|
19
29
|
},
|
|
20
30
|
maxSizeMB: 10,
|
|
21
31
|
actions: [
|
|
22
|
-
{
|
|
23
|
-
{
|
|
24
|
-
{
|
|
32
|
+
{ key: "retry", label: "А давай ещё разок" },
|
|
33
|
+
{ key: "enhance", label: "Улучшить качество" },
|
|
34
|
+
{ key: "crop", label: "Обрезать изображение" },
|
|
25
35
|
],
|
|
26
36
|
},
|
|
27
37
|
{
|
|
@@ -323,38 +333,6 @@ class InMemoryFileStorage {
|
|
|
323
333
|
// Initialize file storage
|
|
324
334
|
const fileStorage = new InMemoryFileStorage();
|
|
325
335
|
|
|
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
336
|
|
|
359
337
|
// DOM element references
|
|
360
338
|
const el = {
|
|
@@ -429,16 +407,21 @@ function parseActions(actionsText) {
|
|
|
429
407
|
if (!action || typeof action !== "object") {
|
|
430
408
|
throw new Error(`Action at index ${i} must be an object`);
|
|
431
409
|
}
|
|
432
|
-
if (!action.
|
|
410
|
+
if (!action.key || typeof action.key !== "string") {
|
|
433
411
|
throw new Error(
|
|
434
|
-
`Action at index ${i} missing valid '
|
|
412
|
+
`Action at index ${i} missing valid 'key' property`,
|
|
435
413
|
);
|
|
436
414
|
}
|
|
437
415
|
if (!action.value || typeof action.value !== "string") {
|
|
438
416
|
throw new Error(`Action at index ${i} missing valid 'value' property`);
|
|
439
417
|
}
|
|
440
|
-
if
|
|
441
|
-
|
|
418
|
+
// related_field is optional - if not provided, action is form-level
|
|
419
|
+
if (action.related_field && typeof action.related_field !== "string") {
|
|
420
|
+
throw new Error(`Action at index ${i} has invalid 'related_field' property type`);
|
|
421
|
+
}
|
|
422
|
+
// Label is optional - will be resolved from schema or key
|
|
423
|
+
if (action.label && typeof action.label !== "string") {
|
|
424
|
+
throw new Error(`Action at index ${i} has invalid 'label' property type`);
|
|
442
425
|
}
|
|
443
426
|
}
|
|
444
427
|
|
|
@@ -480,43 +463,28 @@ function setupFormBuilder() {
|
|
|
480
463
|
});
|
|
481
464
|
|
|
482
465
|
// 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
|
-
|
|
466
|
+
// New system: value, key, related_field parameters
|
|
467
|
+
FormBuilder.setActionHandler((value, key, relatedField) => {
|
|
500
468
|
console.log("Action clicked:", {
|
|
501
|
-
|
|
502
|
-
|
|
469
|
+
value,
|
|
470
|
+
key,
|
|
503
471
|
relatedField,
|
|
504
|
-
|
|
472
|
+
type: relatedField ? "field-level" : "form-level",
|
|
505
473
|
});
|
|
506
474
|
|
|
507
475
|
// Show message to user (compatible with all environments)
|
|
508
476
|
if (typeof window !== "undefined" && window.alert) {
|
|
509
477
|
if (relatedField) {
|
|
510
478
|
window.alert(
|
|
511
|
-
`
|
|
479
|
+
`Field Action: "${key}" clicked for field "${relatedField}" with value: ${value}`,
|
|
512
480
|
);
|
|
513
481
|
} else {
|
|
514
|
-
window.alert(`
|
|
482
|
+
window.alert(`Form Action: "${key}" clicked with value: ${value}`);
|
|
515
483
|
}
|
|
516
484
|
} else {
|
|
517
485
|
console.log(
|
|
518
|
-
`Demo action: ${
|
|
519
|
-
relatedField ? ` for field: ${relatedField}` : "",
|
|
486
|
+
`Demo action: ${key} clicked with value: ${value}`,
|
|
487
|
+
relatedField ? ` for field: ${relatedField}` : " (form-level)",
|
|
520
488
|
);
|
|
521
489
|
}
|
|
522
490
|
});
|
|
@@ -553,8 +521,6 @@ function applyCurrentSchema() {
|
|
|
553
521
|
return false;
|
|
554
522
|
}
|
|
555
523
|
|
|
556
|
-
// Build action value -> label map for efficient lookup
|
|
557
|
-
actionLabelMap = buildActionLabelMap(schema);
|
|
558
524
|
|
|
559
525
|
// Set mode based on toggle
|
|
560
526
|
const isReadOnly = el.readOnlyToggle.checked;
|
|
@@ -801,25 +767,62 @@ el.clearActionsBtn.addEventListener("click", () => {
|
|
|
801
767
|
|
|
802
768
|
// Example external actions for demonstration
|
|
803
769
|
const EXAMPLE_ACTIONS = [
|
|
770
|
+
// Field-level actions using predefined labels from schema
|
|
771
|
+
{
|
|
772
|
+
related_field: "cover",
|
|
773
|
+
key: "retry",
|
|
774
|
+
value: "regenerate_cover_image", // Specific action value for handler
|
|
775
|
+
},
|
|
776
|
+
{
|
|
777
|
+
related_field: "cover",
|
|
778
|
+
key: "enhance",
|
|
779
|
+
value: "enhance_cover_quality", // Specific action value for handler
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
related_field: "cover",
|
|
783
|
+
key: "crop",
|
|
784
|
+
value: "auto_crop_cover", // Specific action value for handler
|
|
785
|
+
},
|
|
786
|
+
// Field-level actions with custom labels
|
|
804
787
|
{
|
|
805
788
|
related_field: "title[0]",
|
|
806
|
-
|
|
807
|
-
|
|
789
|
+
key: "generate",
|
|
790
|
+
value: "ai_generate_title",
|
|
791
|
+
label: "🤖 Generate Title", // Custom label overrides schema
|
|
808
792
|
},
|
|
809
793
|
{
|
|
810
794
|
related_field: "description",
|
|
811
|
-
|
|
812
|
-
|
|
795
|
+
key: "improve",
|
|
796
|
+
value: "ai_improve_description", // Key will be used as fallback label
|
|
813
797
|
},
|
|
814
798
|
{
|
|
815
799
|
related_field: "slides[0].title",
|
|
816
|
-
|
|
817
|
-
|
|
800
|
+
key: "optimize",
|
|
801
|
+
value: "ai_optimize_slide_title",
|
|
802
|
+
label: "🎯 Optimize Slide Title", // Custom label
|
|
818
803
|
},
|
|
804
|
+
// Form-level actions (no related_field)
|
|
819
805
|
{
|
|
820
|
-
|
|
821
|
-
value: "
|
|
822
|
-
label: "
|
|
806
|
+
key: "save-draft",
|
|
807
|
+
value: "save_form_draft",
|
|
808
|
+
label: "💾 Save Draft",
|
|
809
|
+
},
|
|
810
|
+
// Action with missing related field (should appear at bottom of form)
|
|
811
|
+
{
|
|
812
|
+
related_field: "non_existent_field",
|
|
813
|
+
key: "test-missing",
|
|
814
|
+
value: "test_missing_field_action",
|
|
815
|
+
label: "🔍 Test Missing Field Action",
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
key: "preview",
|
|
819
|
+
value: "preview_infographic",
|
|
820
|
+
label: "👁️ Preview",
|
|
821
|
+
},
|
|
822
|
+
{
|
|
823
|
+
key: "export",
|
|
824
|
+
value: "export_json_data",
|
|
825
|
+
label: "📄 Export JSON",
|
|
823
826
|
},
|
|
824
827
|
];
|
|
825
828
|
|
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,15 +996,36 @@ 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 trueFormLevelActions = [];
|
|
1002
|
+
const movedFormLevelActions = [];
|
|
1003
|
+
|
|
1011
1004
|
state.externalActions.forEach((action) => {
|
|
1012
|
-
if (!action.
|
|
1005
|
+
if (!action.key || !action.value) return;
|
|
1013
1006
|
|
|
1007
|
+
if (!action.related_field) {
|
|
1008
|
+
// True form-level action
|
|
1009
|
+
trueFormLevelActions.push(action);
|
|
1010
|
+
} else {
|
|
1011
|
+
// Field-level action
|
|
1012
|
+
if (!actionsByField.has(action.related_field)) {
|
|
1013
|
+
actionsByField.set(action.related_field, []);
|
|
1014
|
+
}
|
|
1015
|
+
actionsByField.get(action.related_field).push(action);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
// Render field-level actions
|
|
1020
|
+
actionsByField.forEach((actions, fieldPath) => {
|
|
1014
1021
|
// Find the form element for this related field
|
|
1015
|
-
const fieldElement = findFormElementByFieldPath(
|
|
1022
|
+
const fieldElement = findFormElementByFieldPath(fieldPath);
|
|
1016
1023
|
if (!fieldElement) {
|
|
1017
1024
|
console.warn(
|
|
1018
|
-
`External action: Could not find form element for field "${
|
|
1025
|
+
`External action: Could not find form element for field "${fieldPath}", treating as form-level actions`,
|
|
1019
1026
|
);
|
|
1027
|
+
// If field is not found, treat these actions as moved form-level actions
|
|
1028
|
+
movedFormLevelActions.push(...actions);
|
|
1020
1029
|
return;
|
|
1021
1030
|
}
|
|
1022
1031
|
|
|
@@ -1028,25 +1037,95 @@ function renderExternalActions() {
|
|
|
1028
1037
|
|
|
1029
1038
|
if (!wrapper) {
|
|
1030
1039
|
console.warn(
|
|
1031
|
-
`External action: Could not find wrapper for field "${
|
|
1040
|
+
`External action: Could not find wrapper for field "${fieldPath}"`,
|
|
1032
1041
|
);
|
|
1033
1042
|
return;
|
|
1034
1043
|
}
|
|
1035
1044
|
|
|
1036
|
-
//
|
|
1037
|
-
|
|
1045
|
+
// Remove any existing actions container
|
|
1046
|
+
const existingContainer = wrapper.querySelector(".external-actions-container");
|
|
1047
|
+
if (existingContainer) {
|
|
1048
|
+
existingContainer.remove();
|
|
1049
|
+
}
|
|
1038
1050
|
|
|
1039
1051
|
// Create actions container
|
|
1040
1052
|
const actionsContainer = document.createElement("div");
|
|
1041
1053
|
actionsContainer.className =
|
|
1042
|
-
"external-actions-container mt-
|
|
1054
|
+
"external-actions-container mt-3 flex flex-wrap gap-2";
|
|
1055
|
+
|
|
1056
|
+
// Find the corresponding schema element for label lookup
|
|
1057
|
+
const schemaElement = findSchemaElement(fieldPath);
|
|
1058
|
+
|
|
1059
|
+
// Create action buttons
|
|
1060
|
+
actions.forEach((action) => {
|
|
1061
|
+
const actionBtn = document.createElement("button");
|
|
1062
|
+
actionBtn.type = "button";
|
|
1063
|
+
actionBtn.className =
|
|
1064
|
+
"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";
|
|
1065
|
+
|
|
1066
|
+
// Resolve action label with priority:
|
|
1067
|
+
// 1. Use explicit label from action if provided
|
|
1068
|
+
// 2. Try to find label from schema element labels using key
|
|
1069
|
+
// 3. Fall back to using key as label
|
|
1070
|
+
const resolvedLabel = resolveActionLabel(action.key, action.label, schemaElement);
|
|
1071
|
+
actionBtn.textContent = resolvedLabel;
|
|
1072
|
+
|
|
1073
|
+
actionBtn.addEventListener("click", (e) => {
|
|
1074
|
+
e.preventDefault();
|
|
1075
|
+
e.stopPropagation();
|
|
1076
|
+
|
|
1077
|
+
if (
|
|
1078
|
+
state.config.actionHandler &&
|
|
1079
|
+
typeof state.config.actionHandler === "function"
|
|
1080
|
+
) {
|
|
1081
|
+
// Call with value, key, and related_field for the new actions system
|
|
1082
|
+
state.config.actionHandler(action.value, action.key, action.related_field);
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
actionsContainer.appendChild(actionBtn);
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
wrapper.appendChild(actionsContainer);
|
|
1090
|
+
});
|
|
1091
|
+
|
|
1092
|
+
// Render form-level actions at the bottom of the form
|
|
1093
|
+
const allFormLevelActions = [...trueFormLevelActions, ...movedFormLevelActions];
|
|
1094
|
+
if (allFormLevelActions.length > 0) {
|
|
1095
|
+
renderFormLevelActions(allFormLevelActions, trueFormLevelActions);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function renderFormLevelActions(actions, trueFormLevelActions = []) {
|
|
1100
|
+
if (!state.formRoot) return;
|
|
1101
|
+
|
|
1102
|
+
// Remove any existing form-level actions container
|
|
1103
|
+
const existingContainer = state.formRoot.querySelector(".form-level-actions-container");
|
|
1104
|
+
if (existingContainer) {
|
|
1105
|
+
existingContainer.remove();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Create form-level actions container
|
|
1109
|
+
const actionsContainer = document.createElement("div");
|
|
1110
|
+
actionsContainer.className =
|
|
1111
|
+
"form-level-actions-container mt-6 pt-4 border-t border-gray-200 flex flex-wrap gap-3 justify-center";
|
|
1043
1112
|
|
|
1044
|
-
|
|
1113
|
+
// Create action buttons
|
|
1114
|
+
actions.forEach((action) => {
|
|
1045
1115
|
const actionBtn = document.createElement("button");
|
|
1046
1116
|
actionBtn.type = "button";
|
|
1047
1117
|
actionBtn.className =
|
|
1048
|
-
"
|
|
1049
|
-
|
|
1118
|
+
"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";
|
|
1119
|
+
|
|
1120
|
+
// Check if this is a true form-level action (no related_field originally)
|
|
1121
|
+
const isTrueFormLevelAction = trueFormLevelActions.includes(action);
|
|
1122
|
+
|
|
1123
|
+
// Resolve action label with priority:
|
|
1124
|
+
// 1. Use explicit label from action if provided
|
|
1125
|
+
// 2. Try to find label from schema element labels using key (only for true form-level actions)
|
|
1126
|
+
// 3. Fall back to using key as label
|
|
1127
|
+
const resolvedLabel = resolveActionLabel(action.key, action.label, null, isTrueFormLevelAction);
|
|
1128
|
+
actionBtn.textContent = resolvedLabel;
|
|
1050
1129
|
|
|
1051
1130
|
actionBtn.addEventListener("click", (e) => {
|
|
1052
1131
|
e.preventDefault();
|
|
@@ -1056,14 +1135,66 @@ function renderExternalActions() {
|
|
|
1056
1135
|
state.config.actionHandler &&
|
|
1057
1136
|
typeof state.config.actionHandler === "function"
|
|
1058
1137
|
) {
|
|
1059
|
-
// Call with
|
|
1060
|
-
state.config.actionHandler(action.
|
|
1138
|
+
// Call with value, key, and null related_field for form-level actions
|
|
1139
|
+
state.config.actionHandler(action.value, action.key, null);
|
|
1061
1140
|
}
|
|
1062
1141
|
});
|
|
1063
1142
|
|
|
1064
1143
|
actionsContainer.appendChild(actionBtn);
|
|
1065
|
-
wrapper.appendChild(actionsContainer);
|
|
1066
1144
|
});
|
|
1145
|
+
|
|
1146
|
+
// Append to form root
|
|
1147
|
+
state.formRoot.appendChild(actionsContainer);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Helper function to resolve action label
|
|
1151
|
+
function resolveActionLabel(actionKey, externalLabel, schemaElement, isTrueFormLevelAction = false) {
|
|
1152
|
+
// 1. Try to find label from predefined actions in schema element using key (highest priority)
|
|
1153
|
+
if (schemaElement && schemaElement.actions && Array.isArray(schemaElement.actions)) {
|
|
1154
|
+
const predefinedAction = schemaElement.actions.find(a => a.key === actionKey);
|
|
1155
|
+
if (predefinedAction && predefinedAction.label) {
|
|
1156
|
+
return predefinedAction.label;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// 2. Try to find label from root-level schema actions (only for true form-level actions)
|
|
1161
|
+
if (isTrueFormLevelAction && state.schema && state.schema.actions && Array.isArray(state.schema.actions)) {
|
|
1162
|
+
const rootAction = state.schema.actions.find(a => a.key === actionKey);
|
|
1163
|
+
if (rootAction && rootAction.label) {
|
|
1164
|
+
return rootAction.label;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// 3. Use explicit label from external action if provided
|
|
1169
|
+
if (externalLabel) {
|
|
1170
|
+
return externalLabel;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// 4. Fall back to using key as label
|
|
1174
|
+
return actionKey;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// Helper function to find schema element by field path
|
|
1178
|
+
function findSchemaElement(fieldPath) {
|
|
1179
|
+
if (!state.schema || !state.schema.elements) return null;
|
|
1180
|
+
|
|
1181
|
+
let currentElements = state.schema.elements;
|
|
1182
|
+
let foundElement = null;
|
|
1183
|
+
|
|
1184
|
+
// Handle paths like 'a.b' or 'a[0].b' by looking for keys in sequence
|
|
1185
|
+
const keys = fieldPath.replace(/\[\d+\]/g, '').split('.').filter(Boolean);
|
|
1186
|
+
|
|
1187
|
+
for (const key of keys) {
|
|
1188
|
+
foundElement = currentElements.find(el => el.key === key);
|
|
1189
|
+
if (!foundElement) {
|
|
1190
|
+
return null; // Key not found at this level
|
|
1191
|
+
}
|
|
1192
|
+
if (foundElement.elements) {
|
|
1193
|
+
currentElements = foundElement.elements;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return foundElement;
|
|
1067
1198
|
}
|
|
1068
1199
|
|
|
1069
1200
|
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"
|