@dmitryvim/form-builder 0.1.34 → 0.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/demo.js +151 -10
- package/dist/elements.html +1 -22
- package/dist/elements.js +1 -1
- package/dist/form-builder.js +325 -621
- package/dist/index.html +66 -41
- package/package.json +3 -8
package/dist/form-builder.js
CHANGED
|
@@ -1,61 +1,10 @@
|
|
|
1
1
|
// Form Builder Library - Core API
|
|
2
|
-
|
|
3
|
-
// Object URL management for memory leak prevention
|
|
4
|
-
const objectUrlIndex = new Map();
|
|
5
|
-
|
|
6
|
-
function createObjectURL(file, resourceIdParam) {
|
|
7
|
-
// Revoke previous URL for this resource to prevent memory leaks
|
|
8
|
-
revokeObjectURL(resourceIdParam);
|
|
9
|
-
|
|
10
|
-
const url = URL.createObjectURL(file);
|
|
11
|
-
objectUrlIndex.set(resourceIdParam, url);
|
|
12
|
-
return url;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function revokeObjectURL(resourceIdParam) {
|
|
16
|
-
const url = objectUrlIndex.get(resourceIdParam);
|
|
17
|
-
if (url) {
|
|
18
|
-
URL.revokeObjectURL(url);
|
|
19
|
-
objectUrlIndex.delete(resourceIdParam);
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function revokeAllObjectURLs() {
|
|
24
|
-
for (const [, url] of objectUrlIndex.entries()) {
|
|
25
|
-
URL.revokeObjectURL(url);
|
|
26
|
-
}
|
|
27
|
-
objectUrlIndex.clear();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Helper function to create safe preview elements
|
|
31
|
-
function createPreviewElement(icon, fileName) {
|
|
32
|
-
const wrapper = document.createElement("div");
|
|
33
|
-
wrapper.className =
|
|
34
|
-
"aspect-video flex items-center justify-center text-gray-400";
|
|
35
|
-
|
|
36
|
-
const textCenter = document.createElement("div");
|
|
37
|
-
textCenter.className = "text-center";
|
|
38
|
-
|
|
39
|
-
const iconEl = document.createElement("div");
|
|
40
|
-
iconEl.className = "text-4xl mb-2";
|
|
41
|
-
iconEl.textContent = icon;
|
|
42
|
-
|
|
43
|
-
const nameEl = document.createElement("div");
|
|
44
|
-
nameEl.className = "text-sm";
|
|
45
|
-
nameEl.textContent = fileName || "";
|
|
46
|
-
|
|
47
|
-
textCenter.appendChild(iconEl);
|
|
48
|
-
textCenter.appendChild(nameEl);
|
|
49
|
-
wrapper.appendChild(textCenter);
|
|
50
|
-
|
|
51
|
-
return wrapper;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
2
|
// State management
|
|
55
3
|
const state = {
|
|
56
4
|
schema: null,
|
|
57
5
|
formRoot: null,
|
|
58
6
|
resourceIndex: new Map(),
|
|
7
|
+
externalActions: null, // Store external actions for the current form
|
|
59
8
|
version: "1.0.0",
|
|
60
9
|
config: {
|
|
61
10
|
// File upload configuration
|
|
@@ -181,7 +130,7 @@ function validateSchema(schema) {
|
|
|
181
130
|
}
|
|
182
131
|
|
|
183
132
|
// Form rendering
|
|
184
|
-
function renderForm(schema, prefill) {
|
|
133
|
+
function renderForm(schema, prefill, actions) {
|
|
185
134
|
const errors = validateSchema(schema);
|
|
186
135
|
if (errors.length > 0) {
|
|
187
136
|
console.error("Schema validation errors:", errors);
|
|
@@ -189,13 +138,13 @@ function renderForm(schema, prefill) {
|
|
|
189
138
|
}
|
|
190
139
|
|
|
191
140
|
state.schema = schema;
|
|
141
|
+
state.externalActions = actions || null;
|
|
142
|
+
|
|
192
143
|
if (!state.formRoot) {
|
|
193
144
|
console.error("No form root element set. Call setFormRoot() first.");
|
|
194
145
|
return;
|
|
195
146
|
}
|
|
196
147
|
|
|
197
|
-
// Clean up any existing object URLs before clearing form
|
|
198
|
-
revokeAllObjectURLs();
|
|
199
148
|
clear(state.formRoot);
|
|
200
149
|
|
|
201
150
|
const formEl = document.createElement("div");
|
|
@@ -214,11 +163,20 @@ function renderForm(schema, prefill) {
|
|
|
214
163
|
});
|
|
215
164
|
|
|
216
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
|
+
}
|
|
217
175
|
}
|
|
218
176
|
|
|
219
177
|
function renderElement(element, ctx) {
|
|
220
178
|
const wrapper = document.createElement("div");
|
|
221
|
-
wrapper.className = "mb-6";
|
|
179
|
+
wrapper.className = "mb-6 fb-field-wrapper";
|
|
222
180
|
|
|
223
181
|
const label = document.createElement("div");
|
|
224
182
|
label.className = "flex items-center mb-2";
|
|
@@ -238,17 +196,8 @@ function renderElement(element, ctx) {
|
|
|
238
196
|
const infoBtn = document.createElement("button");
|
|
239
197
|
infoBtn.type = "button";
|
|
240
198
|
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
svg.setAttribute("class", "w-4 h-4");
|
|
244
|
-
svg.setAttribute("fill", "currentColor");
|
|
245
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
246
|
-
|
|
247
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
248
|
-
path.setAttribute("d", "M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z");
|
|
249
|
-
|
|
250
|
-
svg.appendChild(path);
|
|
251
|
-
infoBtn.appendChild(svg);
|
|
199
|
+
infoBtn.innerHTML =
|
|
200
|
+
'<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>';
|
|
252
201
|
|
|
253
202
|
// Create tooltip
|
|
254
203
|
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -356,7 +305,7 @@ function renderElement(element, ctx) {
|
|
|
356
305
|
const actionBtn = document.createElement("button");
|
|
357
306
|
actionBtn.type = "button";
|
|
358
307
|
actionBtn.className =
|
|
359
|
-
"px-3 py-
|
|
308
|
+
"px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
|
|
360
309
|
actionBtn.textContent = action.label;
|
|
361
310
|
|
|
362
311
|
actionBtn.addEventListener("click", (e) => {
|
|
@@ -367,7 +316,15 @@ function renderElement(element, ctx) {
|
|
|
367
316
|
state.config.actionHandler &&
|
|
368
317
|
typeof state.config.actionHandler === "function"
|
|
369
318
|
) {
|
|
370
|
-
|
|
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
|
+
}
|
|
371
328
|
}
|
|
372
329
|
});
|
|
373
330
|
|
|
@@ -479,8 +436,8 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
479
436
|
reader.readAsDataURL(meta.file);
|
|
480
437
|
container.appendChild(img);
|
|
481
438
|
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
482
|
-
// Video file - use
|
|
483
|
-
const videoUrl = createObjectURL(meta.file
|
|
439
|
+
// Video file - use object URL for preview
|
|
440
|
+
const videoUrl = URL.createObjectURL(meta.file);
|
|
484
441
|
|
|
485
442
|
// Remove all conflicting handlers to prevent interference with video controls
|
|
486
443
|
container.onclick = null;
|
|
@@ -490,55 +447,39 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
490
447
|
container.parentNode.replaceChild(newContainer, container);
|
|
491
448
|
container = newContainer;
|
|
492
449
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const fallback = document.createTextNode("Your browser does not support the video tag.");
|
|
510
|
-
video.appendChild(source);
|
|
511
|
-
video.appendChild(fallback);
|
|
512
|
-
|
|
513
|
-
const buttonsContainer = document.createElement("div");
|
|
514
|
-
buttonsContainer.className = "absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1";
|
|
515
|
-
|
|
516
|
-
const deleteBtn = document.createElement("button");
|
|
517
|
-
deleteBtn.className = "bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn";
|
|
518
|
-
deleteBtn.textContent = t("removeElement");
|
|
519
|
-
|
|
520
|
-
const changeBtn = document.createElement("button");
|
|
521
|
-
changeBtn.className = "bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn";
|
|
522
|
-
changeBtn.textContent = "Change";
|
|
523
|
-
|
|
524
|
-
buttonsContainer.appendChild(deleteBtn);
|
|
525
|
-
buttonsContainer.appendChild(changeBtn);
|
|
526
|
-
wrapper.appendChild(video);
|
|
527
|
-
wrapper.appendChild(buttonsContainer);
|
|
528
|
-
container.appendChild(wrapper);
|
|
450
|
+
container.innerHTML = `
|
|
451
|
+
<div class="relative group h-full">
|
|
452
|
+
<video class="w-full h-full object-contain" controls preload="auto" muted>
|
|
453
|
+
<source src="${videoUrl}" type="${meta.type}">
|
|
454
|
+
Your browser does not support the video tag.
|
|
455
|
+
</video>
|
|
456
|
+
<div class="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity z-10 flex gap-1">
|
|
457
|
+
<button class="bg-red-600 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs delete-file-btn">
|
|
458
|
+
${t("removeElement")}
|
|
459
|
+
</button>
|
|
460
|
+
<button class="bg-gray-800 bg-opacity-75 hover:bg-opacity-90 text-white p-1 rounded text-xs change-file-btn">
|
|
461
|
+
Change
|
|
462
|
+
</button>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
`;
|
|
529
466
|
|
|
530
467
|
// Add click handlers to the custom buttons
|
|
531
|
-
changeBtn
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
468
|
+
const changeBtn = container.querySelector(".change-file-btn");
|
|
469
|
+
if (changeBtn) {
|
|
470
|
+
changeBtn.onclick = (e) => {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
if (deps?.picker) {
|
|
473
|
+
deps.picker.click();
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
537
477
|
|
|
538
|
-
deleteBtn
|
|
478
|
+
const deleteBtn = container.querySelector(".delete-file-btn");
|
|
479
|
+
if (deleteBtn) {
|
|
480
|
+
deleteBtn.onclick = (e) => {
|
|
539
481
|
e.stopPropagation();
|
|
540
|
-
// Clear the file
|
|
541
|
-
revokeObjectURL(resourceId);
|
|
482
|
+
// Clear the file
|
|
542
483
|
state.resourceIndex.delete(resourceId);
|
|
543
484
|
// Update hidden input
|
|
544
485
|
const hiddenInput = container.parentElement.querySelector(
|
|
@@ -554,55 +495,27 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
554
495
|
if (deps?.dragHandler) {
|
|
555
496
|
setupDragAndDrop(container, deps.dragHandler);
|
|
556
497
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
svg.setAttribute("fill", "currentColor");
|
|
566
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
567
|
-
|
|
568
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
569
|
-
path.setAttribute("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
|
-
|
|
571
|
-
svg.appendChild(path);
|
|
572
|
-
|
|
573
|
-
const textDiv = document.createElement("div");
|
|
574
|
-
textDiv.className = "text-sm text-center";
|
|
575
|
-
textDiv.textContent = t("clickDragText");
|
|
576
|
-
|
|
577
|
-
wrapper.appendChild(svg);
|
|
578
|
-
wrapper.appendChild(textDiv);
|
|
579
|
-
container.appendChild(wrapper);
|
|
498
|
+
container.innerHTML = `
|
|
499
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
500
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
501
|
+
<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"/>
|
|
502
|
+
</svg>
|
|
503
|
+
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
504
|
+
</div>
|
|
505
|
+
`;
|
|
580
506
|
};
|
|
507
|
+
}
|
|
581
508
|
} else {
|
|
582
|
-
// Non-image, non-video file
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const icon = document.createElement("div");
|
|
588
|
-
icon.className = "text-2xl mb-2";
|
|
589
|
-
icon.textContent = "📁";
|
|
590
|
-
|
|
591
|
-
const nameEl = document.createElement("div");
|
|
592
|
-
nameEl.className = "text-sm";
|
|
593
|
-
nameEl.textContent = fileName || "";
|
|
594
|
-
|
|
595
|
-
wrapper.appendChild(icon);
|
|
596
|
-
wrapper.appendChild(nameEl);
|
|
597
|
-
clear(container);
|
|
598
|
-
container.appendChild(wrapper);
|
|
509
|
+
// Non-image, non-video file
|
|
510
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">${
|
|
511
|
+
fileName
|
|
512
|
+
}</div></div>`;
|
|
599
513
|
}
|
|
600
514
|
|
|
601
515
|
// Add delete button for edit mode (except for videos which have custom buttons)
|
|
602
516
|
if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
|
|
603
517
|
addDeleteButton(container, () => {
|
|
604
|
-
// Clear the file
|
|
605
|
-
revokeObjectURL(resourceId);
|
|
518
|
+
// Clear the file
|
|
606
519
|
state.resourceIndex.delete(resourceId);
|
|
607
520
|
// Update hidden input
|
|
608
521
|
const hiddenInput = container.parentElement.querySelector(
|
|
@@ -611,29 +524,15 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
611
524
|
if (hiddenInput) {
|
|
612
525
|
hiddenInput.value = "";
|
|
613
526
|
}
|
|
614
|
-
// Clear preview and show placeholder
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
624
|
-
|
|
625
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
626
|
-
path.setAttribute("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");
|
|
627
|
-
|
|
628
|
-
svg.appendChild(path);
|
|
629
|
-
|
|
630
|
-
const textDiv = document.createElement("div");
|
|
631
|
-
textDiv.className = "text-sm text-center";
|
|
632
|
-
textDiv.textContent = t("clickDragText");
|
|
633
|
-
|
|
634
|
-
wrapper.appendChild(svg);
|
|
635
|
-
wrapper.appendChild(textDiv);
|
|
636
|
-
container.appendChild(wrapper);
|
|
527
|
+
// Clear preview and show placeholder
|
|
528
|
+
container.innerHTML = `
|
|
529
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
530
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
531
|
+
<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"/>
|
|
532
|
+
</svg>
|
|
533
|
+
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
534
|
+
</div>
|
|
535
|
+
`;
|
|
637
536
|
});
|
|
638
537
|
}
|
|
639
538
|
} else if (state.config.getThumbnail) {
|
|
@@ -646,50 +545,21 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
646
545
|
container.appendChild(img);
|
|
647
546
|
} else {
|
|
648
547
|
// Fallback to file icon
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
"flex flex-col items-center justify-center h-full text-gray-400";
|
|
653
|
-
|
|
654
|
-
const icon = document.createElement("div");
|
|
655
|
-
icon.className = "text-2xl mb-2";
|
|
656
|
-
icon.textContent = "🖼️";
|
|
657
|
-
|
|
658
|
-
const nameEl = document.createElement("div");
|
|
659
|
-
nameEl.className = "text-sm";
|
|
660
|
-
nameEl.textContent = fileName || "";
|
|
661
|
-
|
|
662
|
-
wrapper.appendChild(icon);
|
|
663
|
-
wrapper.appendChild(nameEl);
|
|
664
|
-
clear(container);
|
|
665
|
-
container.appendChild(wrapper);
|
|
548
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">🖼️</div><div class="text-sm">${
|
|
549
|
+
fileName
|
|
550
|
+
}</div></div>`;
|
|
666
551
|
}
|
|
667
552
|
} catch (error) {
|
|
668
553
|
console.warn("Thumbnail loading failed:", error);
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
"flex flex-col items-center justify-center h-full text-gray-400";
|
|
673
|
-
|
|
674
|
-
const icon = document.createElement("div");
|
|
675
|
-
icon.className = "text-2xl mb-2";
|
|
676
|
-
icon.textContent = "📁";
|
|
677
|
-
|
|
678
|
-
const nameEl = document.createElement("div");
|
|
679
|
-
nameEl.className = "text-sm";
|
|
680
|
-
nameEl.textContent = fileName || "";
|
|
681
|
-
|
|
682
|
-
wrapper.appendChild(icon);
|
|
683
|
-
wrapper.appendChild(nameEl);
|
|
684
|
-
clear(container);
|
|
685
|
-
container.appendChild(wrapper);
|
|
554
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">📁</div><div class="text-sm">${
|
|
555
|
+
fileName
|
|
556
|
+
}</div></div>`;
|
|
686
557
|
}
|
|
687
558
|
|
|
688
559
|
// Add delete button for edit mode
|
|
689
560
|
if (!isReadonly) {
|
|
690
561
|
addDeleteButton(container, () => {
|
|
691
|
-
// Clear the file
|
|
692
|
-
revokeObjectURL(resourceId);
|
|
562
|
+
// Clear the file
|
|
693
563
|
state.resourceIndex.delete(resourceId);
|
|
694
564
|
// Update hidden input
|
|
695
565
|
const hiddenInput = container.parentElement.querySelector(
|
|
@@ -698,35 +568,22 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
698
568
|
if (hiddenInput) {
|
|
699
569
|
hiddenInput.value = "";
|
|
700
570
|
}
|
|
701
|
-
// Clear preview and show placeholder
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
711
|
-
|
|
712
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
713
|
-
path.setAttribute("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");
|
|
714
|
-
|
|
715
|
-
svg.appendChild(path);
|
|
716
|
-
|
|
717
|
-
const textDiv = document.createElement("div");
|
|
718
|
-
textDiv.className = "text-sm text-center";
|
|
719
|
-
textDiv.textContent = t("clickDragText");
|
|
720
|
-
|
|
721
|
-
wrapper.appendChild(svg);
|
|
722
|
-
wrapper.appendChild(textDiv);
|
|
723
|
-
container.appendChild(wrapper);
|
|
571
|
+
// Clear preview and show placeholder
|
|
572
|
+
container.innerHTML = `
|
|
573
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
574
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
575
|
+
<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"/>
|
|
576
|
+
</svg>
|
|
577
|
+
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
578
|
+
</div>
|
|
579
|
+
`;
|
|
724
580
|
});
|
|
725
581
|
}
|
|
726
582
|
} else {
|
|
727
|
-
// No file and no getThumbnail config - fallback
|
|
728
|
-
|
|
729
|
-
|
|
583
|
+
// No file and no getThumbnail config - fallback
|
|
584
|
+
container.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400"><div class="text-2xl mb-2">🖼️</div><div class="text-sm">${
|
|
585
|
+
fileName
|
|
586
|
+
}</div></div>`;
|
|
730
587
|
}
|
|
731
588
|
|
|
732
589
|
// Add click handler for download in readonly mode
|
|
@@ -873,179 +730,76 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
873
730
|
img.src = url;
|
|
874
731
|
slot.appendChild(img);
|
|
875
732
|
} else {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
svg.setAttribute("class", "w-12 h-12");
|
|
882
|
-
svg.setAttribute("fill", "currentColor");
|
|
883
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
884
|
-
|
|
885
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
886
|
-
path.setAttribute("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");
|
|
887
|
-
|
|
888
|
-
svg.appendChild(path);
|
|
889
|
-
wrapper.appendChild(svg);
|
|
890
|
-
slot.appendChild(wrapper);
|
|
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>`;
|
|
891
738
|
}
|
|
892
739
|
} else {
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
svg.setAttribute("class", "w-12 h-12");
|
|
899
|
-
svg.setAttribute("fill", "currentColor");
|
|
900
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
901
|
-
|
|
902
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
903
|
-
path.setAttribute("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");
|
|
904
|
-
|
|
905
|
-
svg.appendChild(path);
|
|
906
|
-
wrapper.appendChild(svg);
|
|
907
|
-
slot.appendChild(wrapper);
|
|
740
|
+
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
741
|
+
<svg class="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
|
|
742
|
+
<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"/>
|
|
743
|
+
</svg>
|
|
744
|
+
</div>`;
|
|
908
745
|
}
|
|
909
746
|
} else if (meta && meta.type?.startsWith("video/")) {
|
|
910
747
|
if (meta.file && meta.file instanceof File) {
|
|
911
|
-
// Video file - use
|
|
912
|
-
const videoUrl = createObjectURL(meta.file
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const overlay = document.createElement("div");
|
|
929
|
-
overlay.className = "absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center";
|
|
930
|
-
|
|
931
|
-
const playButton = document.createElement("div");
|
|
932
|
-
playButton.className = "bg-white bg-opacity-90 rounded-full p-1";
|
|
933
|
-
|
|
934
|
-
const playIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
935
|
-
playIcon.setAttribute("class", "w-4 h-4 text-gray-800");
|
|
936
|
-
playIcon.setAttribute("fill", "currentColor");
|
|
937
|
-
playIcon.setAttribute("viewBox", "0 0 24 24");
|
|
938
|
-
|
|
939
|
-
const playPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
940
|
-
playPath.setAttribute("d", "M8 5v14l11-7z");
|
|
941
|
-
|
|
942
|
-
playIcon.appendChild(playPath);
|
|
943
|
-
playButton.appendChild(playIcon);
|
|
944
|
-
overlay.appendChild(playButton);
|
|
945
|
-
wrapper.appendChild(video);
|
|
946
|
-
wrapper.appendChild(overlay);
|
|
947
|
-
slot.appendChild(wrapper);
|
|
748
|
+
// Video file - use object URL for preview in thumbnail format
|
|
749
|
+
const videoUrl = URL.createObjectURL(meta.file);
|
|
750
|
+
slot.innerHTML = `
|
|
751
|
+
<div class="relative group h-full w-full">
|
|
752
|
+
<video class="w-full h-full object-contain" preload="metadata" muted>
|
|
753
|
+
<source src="${videoUrl}" type="${meta.type}">
|
|
754
|
+
</video>
|
|
755
|
+
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
756
|
+
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
757
|
+
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
758
|
+
<path d="M8 5v14l11-7z"/>
|
|
759
|
+
</svg>
|
|
760
|
+
</div>
|
|
761
|
+
</div>
|
|
762
|
+
</div>
|
|
763
|
+
`;
|
|
948
764
|
} else if (state.config.getThumbnail) {
|
|
949
765
|
// Use getThumbnail for uploaded video files
|
|
950
766
|
const videoUrl = state.config.getThumbnail(rid);
|
|
951
767
|
if (videoUrl) {
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const overlay = document.createElement("div");
|
|
968
|
-
overlay.className = "absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center";
|
|
969
|
-
|
|
970
|
-
const playButton = document.createElement("div");
|
|
971
|
-
playButton.className = "bg-white bg-opacity-90 rounded-full p-1";
|
|
972
|
-
|
|
973
|
-
const playIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
974
|
-
playIcon.setAttribute("class", "w-4 h-4 text-gray-800");
|
|
975
|
-
playIcon.setAttribute("fill", "currentColor");
|
|
976
|
-
playIcon.setAttribute("viewBox", "0 0 24 24");
|
|
977
|
-
|
|
978
|
-
const playPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
979
|
-
playPath.setAttribute("d", "M8 5v14l11-7z");
|
|
980
|
-
|
|
981
|
-
playIcon.appendChild(playPath);
|
|
982
|
-
playButton.appendChild(playIcon);
|
|
983
|
-
overlay.appendChild(playButton);
|
|
984
|
-
wrapper.appendChild(video);
|
|
985
|
-
wrapper.appendChild(overlay);
|
|
986
|
-
slot.appendChild(wrapper);
|
|
768
|
+
slot.innerHTML = `
|
|
769
|
+
<div class="relative group h-full w-full">
|
|
770
|
+
<video class="w-full h-full object-contain" preload="metadata" muted>
|
|
771
|
+
<source src="${videoUrl}" type="${meta.type}">
|
|
772
|
+
</video>
|
|
773
|
+
<div class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center">
|
|
774
|
+
<div class="bg-white bg-opacity-90 rounded-full p-1">
|
|
775
|
+
<svg class="w-4 h-4 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
776
|
+
<path d="M8 5v14l11-7z"/>
|
|
777
|
+
</svg>
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
</div>
|
|
781
|
+
`;
|
|
987
782
|
} else {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
svg.setAttribute("fill", "currentColor");
|
|
995
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
996
|
-
|
|
997
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
998
|
-
path.setAttribute("d", "M8 5v14l11-7z");
|
|
999
|
-
|
|
1000
|
-
svg.appendChild(path);
|
|
1001
|
-
|
|
1002
|
-
const nameDiv = document.createElement("div");
|
|
1003
|
-
nameDiv.className = "text-xs mt-1";
|
|
1004
|
-
nameDiv.textContent = meta?.name || "Video";
|
|
1005
|
-
|
|
1006
|
-
wrapper.appendChild(svg);
|
|
1007
|
-
wrapper.appendChild(nameDiv);
|
|
1008
|
-
slot.appendChild(wrapper);
|
|
783
|
+
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
784
|
+
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
785
|
+
<path d="M8 5v14l11-7z"/>
|
|
786
|
+
</svg>
|
|
787
|
+
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
788
|
+
</div>`;
|
|
1009
789
|
}
|
|
1010
790
|
} else {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
svg.setAttribute("fill", "currentColor");
|
|
1018
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1019
|
-
|
|
1020
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1021
|
-
path.setAttribute("d", "M8 5v14l11-7z");
|
|
1022
|
-
|
|
1023
|
-
svg.appendChild(path);
|
|
1024
|
-
|
|
1025
|
-
const nameDiv = document.createElement("div");
|
|
1026
|
-
nameDiv.className = "text-xs mt-1";
|
|
1027
|
-
nameDiv.textContent = meta?.name || "Video";
|
|
1028
|
-
|
|
1029
|
-
wrapper.appendChild(svg);
|
|
1030
|
-
wrapper.appendChild(nameDiv);
|
|
1031
|
-
slot.appendChild(wrapper);
|
|
791
|
+
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
792
|
+
<svg class="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
|
|
793
|
+
<path d="M8 5v14l11-7z"/>
|
|
794
|
+
</svg>
|
|
795
|
+
<div class="text-xs mt-1">${meta?.name || "Video"}</div>
|
|
796
|
+
</div>`;
|
|
1032
797
|
}
|
|
1033
798
|
} else {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
const iconDiv = document.createElement("div");
|
|
1039
|
-
iconDiv.className = "text-2xl mb-1";
|
|
1040
|
-
iconDiv.textContent = "📁";
|
|
1041
|
-
|
|
1042
|
-
const nameDiv = document.createElement("div");
|
|
1043
|
-
nameDiv.className = "text-xs";
|
|
1044
|
-
nameDiv.textContent = meta?.name || "File";
|
|
1045
|
-
|
|
1046
|
-
wrapper.appendChild(iconDiv);
|
|
1047
|
-
wrapper.appendChild(nameDiv);
|
|
1048
|
-
slot.appendChild(wrapper);
|
|
799
|
+
slot.innerHTML = `<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
800
|
+
<div class="text-2xl mb-1">📁</div>
|
|
801
|
+
<div class="text-xs">${meta?.name || "File"}</div>
|
|
802
|
+
</div>`;
|
|
1049
803
|
}
|
|
1050
804
|
|
|
1051
805
|
// Add remove button overlay (similar to file field)
|
|
@@ -1069,17 +823,8 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
1069
823
|
// Empty slot placeholder
|
|
1070
824
|
slot.className =
|
|
1071
825
|
"aspect-square bg-gray-100 border-2 border-dashed border-gray-300 rounded-lg flex items-center justify-center cursor-pointer hover:border-gray-400 transition-colors";
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
svg.setAttribute("class", "w-12 h-12 text-gray-400");
|
|
1075
|
-
svg.setAttribute("fill", "currentColor");
|
|
1076
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
1077
|
-
|
|
1078
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1079
|
-
path.setAttribute("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");
|
|
1080
|
-
|
|
1081
|
-
svg.appendChild(path);
|
|
1082
|
-
slot.appendChild(svg);
|
|
826
|
+
slot.innerHTML =
|
|
827
|
+
'<svg class="w-12 h-12 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><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"/></svg>';
|
|
1083
828
|
slot.onclick = () => {
|
|
1084
829
|
// Look for file input - check parent containers that have space-y-2 class
|
|
1085
830
|
let filesWrapper = container.parentElement;
|
|
@@ -1141,37 +886,14 @@ async function handleFileSelect(file, container, fieldName, deps = null) {
|
|
|
1141
886
|
});
|
|
1142
887
|
|
|
1143
888
|
// Create hidden input to store the resource ID
|
|
1144
|
-
|
|
1145
|
-
let parentElement = container.parentElement;
|
|
1146
|
-
if (!parentElement) {
|
|
1147
|
-
// Container is detached - find the current container by field name
|
|
1148
|
-
const form = document.querySelector('[data-form-builder]');
|
|
1149
|
-
if (form) {
|
|
1150
|
-
const existingInput = form.querySelector(`input[name="${fieldName}"]`);
|
|
1151
|
-
if (existingInput) {
|
|
1152
|
-
parentElement = existingInput.parentElement;
|
|
1153
|
-
// Update container reference to the current one in DOM
|
|
1154
|
-
const currentContainer = parentElement.querySelector('.file-preview-container');
|
|
1155
|
-
if (currentContainer) {
|
|
1156
|
-
container = currentContainer;
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
if (!parentElement) {
|
|
1163
|
-
console.warn('Could not find parent element for file field:', fieldName);
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
let hiddenInput = parentElement.querySelector(
|
|
889
|
+
let hiddenInput = container.parentElement.querySelector(
|
|
1168
890
|
'input[type="hidden"]',
|
|
1169
891
|
);
|
|
1170
892
|
if (!hiddenInput) {
|
|
1171
893
|
hiddenInput = document.createElement("input");
|
|
1172
894
|
hiddenInput.type = "hidden";
|
|
1173
895
|
hiddenInput.name = fieldName;
|
|
1174
|
-
parentElement.appendChild(hiddenInput);
|
|
896
|
+
container.parentElement.appendChild(hiddenInput);
|
|
1175
897
|
}
|
|
1176
898
|
hiddenInput.value = rid;
|
|
1177
899
|
|
|
@@ -1225,6 +947,125 @@ function addDeleteButton(container, onDelete) {
|
|
|
1225
947
|
container.appendChild(overlay);
|
|
1226
948
|
}
|
|
1227
949
|
|
|
950
|
+
// JSON path resolution for external actions (currently unused but kept for future use)
|
|
951
|
+
// eslint-disable-next-line no-unused-vars
|
|
952
|
+
function resolveFieldPath(path, formData) {
|
|
953
|
+
// Remove leading $input_data. prefix if present
|
|
954
|
+
const cleanPath = path.replace(/^\$input_data\./, "");
|
|
955
|
+
|
|
956
|
+
// Split path into segments, handling array notation
|
|
957
|
+
const segments = cleanPath.split(/[.[\]]/).filter(Boolean);
|
|
958
|
+
|
|
959
|
+
// Try to find the corresponding form element
|
|
960
|
+
return findElementByPath(segments, formData);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function findElementByPath(segments, data, currentPath = "") {
|
|
964
|
+
if (segments.length === 0) return currentPath;
|
|
965
|
+
|
|
966
|
+
const [head, ...tail] = segments;
|
|
967
|
+
|
|
968
|
+
// Check if this is an array index
|
|
969
|
+
const isArrayIndex = /^\d+$/.test(head);
|
|
970
|
+
|
|
971
|
+
if (isArrayIndex) {
|
|
972
|
+
// Array index case: build path like "fieldName[index]"
|
|
973
|
+
const newPath = currentPath ? `${currentPath}[${head}]` : `[${head}]`;
|
|
974
|
+
return findElementByPath(tail, data, newPath);
|
|
975
|
+
} else {
|
|
976
|
+
// Regular field name
|
|
977
|
+
const newPath = currentPath ? `${currentPath}.${head}` : head;
|
|
978
|
+
return findElementByPath(tail, data, newPath);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function findFormElementByFieldPath(fieldPath) {
|
|
983
|
+
// Try to find the form element that corresponds to the field path
|
|
984
|
+
// This looks for elements with name attributes that match the path pattern
|
|
985
|
+
|
|
986
|
+
if (!state.formRoot) return null;
|
|
987
|
+
|
|
988
|
+
// Try exact match first
|
|
989
|
+
let element = state.formRoot.querySelector(`[name="${fieldPath}"]`);
|
|
990
|
+
if (element) return element;
|
|
991
|
+
|
|
992
|
+
// Try with array notation variations
|
|
993
|
+
const variations = [
|
|
994
|
+
fieldPath,
|
|
995
|
+
fieldPath.replace(/\[(\d+)\]/g, "[$1]"), // normalize array notation
|
|
996
|
+
fieldPath.replace(/\./g, "[") +
|
|
997
|
+
"]".repeat((fieldPath.match(/\./g) || []).length), // convert dots to brackets
|
|
998
|
+
];
|
|
999
|
+
|
|
1000
|
+
for (const variation of variations) {
|
|
1001
|
+
element = state.formRoot.querySelector(`[name="${variation}"]`);
|
|
1002
|
+
if (element) return element;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return null;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function renderExternalActions() {
|
|
1009
|
+
if (!state.externalActions || !Array.isArray(state.externalActions)) return;
|
|
1010
|
+
|
|
1011
|
+
state.externalActions.forEach((action) => {
|
|
1012
|
+
if (!action.related_field || !action.value || !action.label) return;
|
|
1013
|
+
|
|
1014
|
+
// Find the form element for this related field
|
|
1015
|
+
const fieldElement = findFormElementByFieldPath(action.related_field);
|
|
1016
|
+
if (!fieldElement) {
|
|
1017
|
+
console.warn(
|
|
1018
|
+
`External action: Could not find form element for field "${action.related_field}"`,
|
|
1019
|
+
);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Find the wrapper element that contains the field using stable class
|
|
1024
|
+
let wrapper = fieldElement.closest(".fb-field-wrapper");
|
|
1025
|
+
if (!wrapper) {
|
|
1026
|
+
wrapper = fieldElement.parentElement;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
if (!wrapper) {
|
|
1030
|
+
console.warn(
|
|
1031
|
+
`External action: Could not find wrapper for field "${action.related_field}"`,
|
|
1032
|
+
);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// Check if we already added external actions to this wrapper
|
|
1037
|
+
if (wrapper.querySelector(".external-actions-container")) return;
|
|
1038
|
+
|
|
1039
|
+
// Create actions container
|
|
1040
|
+
const actionsContainer = document.createElement("div");
|
|
1041
|
+
actionsContainer.className =
|
|
1042
|
+
"external-actions-container mt-4 flex flex-wrap gap-2";
|
|
1043
|
+
|
|
1044
|
+
// Create action button
|
|
1045
|
+
const actionBtn = document.createElement("button");
|
|
1046
|
+
actionBtn.type = "button";
|
|
1047
|
+
actionBtn.className =
|
|
1048
|
+
"px-3 py-2 text-sm border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors";
|
|
1049
|
+
actionBtn.textContent = action.label;
|
|
1050
|
+
|
|
1051
|
+
actionBtn.addEventListener("click", (e) => {
|
|
1052
|
+
e.preventDefault();
|
|
1053
|
+
e.stopPropagation();
|
|
1054
|
+
|
|
1055
|
+
if (
|
|
1056
|
+
state.config.actionHandler &&
|
|
1057
|
+
typeof state.config.actionHandler === "function"
|
|
1058
|
+
) {
|
|
1059
|
+
// Call with both related_field and value for the new actions system
|
|
1060
|
+
state.config.actionHandler(action.related_field, action.value);
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
actionsContainer.appendChild(actionBtn);
|
|
1065
|
+
wrapper.appendChild(actionsContainer);
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1228
1069
|
function showTooltip(tooltipId, button) {
|
|
1229
1070
|
const tooltip = document.getElementById(tooltipId);
|
|
1230
1071
|
const isCurrentlyVisible = !tooltip.classList.contains("hidden");
|
|
@@ -1899,7 +1740,7 @@ function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1899
1740
|
removeBtn.type = "button";
|
|
1900
1741
|
removeBtn.className =
|
|
1901
1742
|
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
1902
|
-
removeBtn.
|
|
1743
|
+
removeBtn.innerHTML = "✕";
|
|
1903
1744
|
removeBtn.onclick = () => {
|
|
1904
1745
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1905
1746
|
if (container.children.length > minCount) {
|
|
@@ -2032,7 +1873,7 @@ function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
|
2032
1873
|
removeBtn.type = "button";
|
|
2033
1874
|
removeBtn.className =
|
|
2034
1875
|
"remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
|
|
2035
|
-
removeBtn.
|
|
1876
|
+
removeBtn.innerHTML = "✕ Remove";
|
|
2036
1877
|
removeBtn.onclick = () => {
|
|
2037
1878
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2038
1879
|
if (container.children.length > minCount) {
|
|
@@ -2171,7 +2012,7 @@ function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
2171
2012
|
removeBtn.type = "button";
|
|
2172
2013
|
removeBtn.className =
|
|
2173
2014
|
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2174
|
-
removeBtn.
|
|
2015
|
+
removeBtn.innerHTML = "✕";
|
|
2175
2016
|
removeBtn.onclick = () => {
|
|
2176
2017
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2177
2018
|
if (container.children.length > minCount) {
|
|
@@ -2320,7 +2161,7 @@ function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
2320
2161
|
removeBtn.type = "button";
|
|
2321
2162
|
removeBtn.className =
|
|
2322
2163
|
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2323
|
-
removeBtn.
|
|
2164
|
+
removeBtn.innerHTML = "✕";
|
|
2324
2165
|
removeBtn.onclick = () => {
|
|
2325
2166
|
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2326
2167
|
if (container.children.length > minCount) {
|
|
@@ -2385,11 +2226,7 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2385
2226
|
const emptyState = document.createElement("div");
|
|
2386
2227
|
emptyState.className =
|
|
2387
2228
|
"aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2388
|
-
|
|
2389
|
-
const textDiv = document.createElement("div");
|
|
2390
|
-
textDiv.className = "text-center";
|
|
2391
|
-
textDiv.textContent = t("noFileSelected");
|
|
2392
|
-
emptyState.appendChild(textDiv);
|
|
2229
|
+
emptyState.innerHTML = `<div class="text-center">${t("noFileSelected")}</div>`;
|
|
2393
2230
|
wrapper.appendChild(emptyState);
|
|
2394
2231
|
}
|
|
2395
2232
|
} else {
|
|
@@ -2449,14 +2286,8 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2449
2286
|
// Add upload text
|
|
2450
2287
|
const uploadText = document.createElement("p");
|
|
2451
2288
|
uploadText.className = "text-xs text-gray-600 mt-2 text-center";
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
uploadSpan.className = "underline cursor-pointer";
|
|
2455
|
-
uploadSpan.textContent = t("uploadText");
|
|
2456
|
-
uploadSpan.onclick = () => picker.click();
|
|
2457
|
-
|
|
2458
|
-
uploadText.appendChild(uploadSpan);
|
|
2459
|
-
uploadText.appendChild(document.createTextNode(" " + t("dragDropTextSingle")));
|
|
2289
|
+
uploadText.innerHTML = `<span class="underline cursor-pointer">${t("uploadText")}</span> ${t("dragDropTextSingle")}`;
|
|
2290
|
+
uploadText.querySelector("span").onclick = () => picker.click();
|
|
2460
2291
|
fileWrapper.appendChild(uploadText);
|
|
2461
2292
|
|
|
2462
2293
|
// Add hint
|
|
@@ -2515,29 +2346,14 @@ function handleInitialFileData(
|
|
|
2515
2346
|
}
|
|
2516
2347
|
|
|
2517
2348
|
function setEmptyFileContainer(fileContainer) {
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
svg.setAttribute("fill", "currentColor");
|
|
2527
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
2528
|
-
|
|
2529
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2530
|
-
path.setAttribute("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");
|
|
2531
|
-
|
|
2532
|
-
svg.appendChild(path);
|
|
2533
|
-
|
|
2534
|
-
const textDiv = document.createElement("div");
|
|
2535
|
-
textDiv.className = "text-sm text-center";
|
|
2536
|
-
textDiv.textContent = t("clickDragText");
|
|
2537
|
-
|
|
2538
|
-
wrapper.appendChild(svg);
|
|
2539
|
-
wrapper.appendChild(textDiv);
|
|
2540
|
-
fileContainer.appendChild(wrapper);
|
|
2349
|
+
fileContainer.innerHTML = `
|
|
2350
|
+
<div class="flex flex-col items-center justify-center h-full text-gray-400">
|
|
2351
|
+
<svg class="w-6 h-6 mb-2" fill="currentColor" viewBox="0 0 24 24">
|
|
2352
|
+
<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"/>
|
|
2353
|
+
</svg>
|
|
2354
|
+
<div class="text-sm text-center">${t("clickDragText")}</div>
|
|
2355
|
+
</div>
|
|
2356
|
+
`;
|
|
2541
2357
|
}
|
|
2542
2358
|
|
|
2543
2359
|
function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
@@ -2554,16 +2370,7 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
2554
2370
|
resultsWrapper.appendChild(filePreview);
|
|
2555
2371
|
});
|
|
2556
2372
|
} else {
|
|
2557
|
-
|
|
2558
|
-
const emptyDiv = document.createElement("div");
|
|
2559
|
-
emptyDiv.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2560
|
-
|
|
2561
|
-
const textDiv = document.createElement("div");
|
|
2562
|
-
textDiv.className = "text-center";
|
|
2563
|
-
textDiv.textContent = t("noFilesSelected");
|
|
2564
|
-
|
|
2565
|
-
emptyDiv.appendChild(textDiv);
|
|
2566
|
-
resultsWrapper.appendChild(emptyDiv);
|
|
2373
|
+
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected")}</div></div>`;
|
|
2567
2374
|
}
|
|
2568
2375
|
|
|
2569
2376
|
wrapper.appendChild(resultsWrapper);
|
|
@@ -2644,16 +2451,7 @@ function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
|
2644
2451
|
resultsWrapper.appendChild(filePreview);
|
|
2645
2452
|
});
|
|
2646
2453
|
} else {
|
|
2647
|
-
|
|
2648
|
-
const emptyDiv = document.createElement("div");
|
|
2649
|
-
emptyDiv.className = "aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
2650
|
-
|
|
2651
|
-
const textDiv = document.createElement("div");
|
|
2652
|
-
textDiv.className = "text-center";
|
|
2653
|
-
textDiv.textContent = t("noFilesSelected");
|
|
2654
|
-
|
|
2655
|
-
emptyDiv.appendChild(textDiv);
|
|
2656
|
-
resultsWrapper.appendChild(emptyDiv);
|
|
2454
|
+
resultsWrapper.innerHTML = `<div class="aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500"><div class="text-center">${t("noFilesSelected")}</div></div>`;
|
|
2657
2455
|
}
|
|
2658
2456
|
|
|
2659
2457
|
wrapper.appendChild(resultsWrapper);
|
|
@@ -2890,18 +2688,7 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
2890
2688
|
addBtn.type = "button";
|
|
2891
2689
|
addBtn.className =
|
|
2892
2690
|
"w-full py-2 px-4 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 flex items-center justify-center mt-3";
|
|
2893
|
-
|
|
2894
|
-
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2895
|
-
svg.setAttribute("class", "w-4 h-4 mr-2");
|
|
2896
|
-
svg.setAttribute("fill", "currentColor");
|
|
2897
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
2898
|
-
|
|
2899
|
-
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
2900
|
-
path.setAttribute("d", "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z");
|
|
2901
|
-
|
|
2902
|
-
svg.appendChild(path);
|
|
2903
|
-
addBtn.appendChild(svg);
|
|
2904
|
-
addBtn.appendChild(document.createTextNode(t("addElement")));
|
|
2691
|
+
addBtn.innerHTML = `<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>${t("addElement")}`;
|
|
2905
2692
|
groupWrap.appendChild(addBtn);
|
|
2906
2693
|
}
|
|
2907
2694
|
|
|
@@ -2909,16 +2696,7 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
2909
2696
|
if (state.config.readonly) return;
|
|
2910
2697
|
const n = countItems();
|
|
2911
2698
|
if (addBtn) addBtn.disabled = n >= max;
|
|
2912
|
-
|
|
2913
|
-
clear(left);
|
|
2914
|
-
const labelSpan = document.createElement("span");
|
|
2915
|
-
labelSpan.textContent = element.label || element.key;
|
|
2916
|
-
const countSpan = document.createElement("span");
|
|
2917
|
-
countSpan.className = "text-slate-500 dark:text-slate-400 text-xs";
|
|
2918
|
-
countSpan.textContent = `[${n} / ${max === Infinity ? "∞" : max}, min=${min}]`;
|
|
2919
|
-
left.appendChild(labelSpan);
|
|
2920
|
-
left.appendChild(document.createTextNode(" "));
|
|
2921
|
-
left.appendChild(countSpan);
|
|
2699
|
+
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-slate-500 dark:text-slate-400 text-xs">[${n} / ${max === Infinity ? "∞" : max}, min=${min}]</span>`;
|
|
2922
2700
|
};
|
|
2923
2701
|
|
|
2924
2702
|
if (pre && pre.length) {
|
|
@@ -2933,10 +2711,7 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
2933
2711
|
addBtn.addEventListener("click", () => addItem(null));
|
|
2934
2712
|
} else {
|
|
2935
2713
|
// In readonly mode, just show the label without count controls
|
|
2936
|
-
|
|
2937
|
-
const labelSpan = document.createElement("span");
|
|
2938
|
-
labelSpan.textContent = element.label || element.key;
|
|
2939
|
-
left.appendChild(labelSpan);
|
|
2714
|
+
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
2940
2715
|
}
|
|
2941
2716
|
}
|
|
2942
2717
|
|
|
@@ -2953,10 +2728,7 @@ function renderSingleGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
2953
2728
|
}
|
|
2954
2729
|
});
|
|
2955
2730
|
groupWrap.appendChild(itemsWrap);
|
|
2956
|
-
|
|
2957
|
-
const labelSpan = document.createElement("span");
|
|
2958
|
-
labelSpan.textContent = element.label || element.key;
|
|
2959
|
-
left.appendChild(labelSpan);
|
|
2731
|
+
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
2960
2732
|
}
|
|
2961
2733
|
|
|
2962
2734
|
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
@@ -2989,10 +2761,7 @@ function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
|
2989
2761
|
}
|
|
2990
2762
|
});
|
|
2991
2763
|
containerWrap.appendChild(itemsWrap);
|
|
2992
|
-
|
|
2993
|
-
const labelSpan = document.createElement("span");
|
|
2994
|
-
labelSpan.textContent = element.label || element.key;
|
|
2995
|
-
left.appendChild(labelSpan);
|
|
2764
|
+
left.innerHTML = `<span>${element.label || element.key}</span>`;
|
|
2996
2765
|
|
|
2997
2766
|
wrapper.appendChild(containerWrap);
|
|
2998
2767
|
}
|
|
@@ -3083,16 +2852,7 @@ function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
|
3083
2852
|
addBtn.disabled = currentCount >= max;
|
|
3084
2853
|
addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
3085
2854
|
}
|
|
3086
|
-
|
|
3087
|
-
clear(left);
|
|
3088
|
-
const labelSpan = document.createElement("span");
|
|
3089
|
-
labelSpan.textContent = element.label || element.key;
|
|
3090
|
-
const countSpan = document.createElement("span");
|
|
3091
|
-
countSpan.className = "text-sm text-gray-500";
|
|
3092
|
-
countSpan.textContent = `(${currentCount}/${max === Infinity ? "∞" : max})`;
|
|
3093
|
-
left.appendChild(labelSpan);
|
|
3094
|
-
left.appendChild(document.createTextNode(" "));
|
|
3095
|
-
left.appendChild(countSpan);
|
|
2855
|
+
left.innerHTML = `<span>${element.label || element.key}</span> <span class="text-sm text-gray-500">(${currentCount}/${max === Infinity ? "∞" : max})</span>`;
|
|
3096
2856
|
};
|
|
3097
2857
|
|
|
3098
2858
|
if (!state.config.readonly) {
|
|
@@ -3213,29 +2973,17 @@ function renderFilePreviewReadonly(resourceId, fileName) {
|
|
|
3213
2973
|
try {
|
|
3214
2974
|
const thumbnailUrl = state.config.getThumbnail(resourceId);
|
|
3215
2975
|
if (thumbnailUrl) {
|
|
3216
|
-
|
|
3217
|
-
img.src = thumbnailUrl;
|
|
3218
|
-
img.alt = actualFileName || "";
|
|
3219
|
-
img.className = "w-full h-auto";
|
|
3220
|
-
clear(previewContainer);
|
|
3221
|
-
previewContainer.appendChild(img);
|
|
2976
|
+
previewContainer.innerHTML = `<img src="${thumbnailUrl}" alt="${actualFileName}" class="w-full h-auto">`;
|
|
3222
2977
|
} else {
|
|
3223
2978
|
// Fallback to icon if getThumbnail returns null/undefined
|
|
3224
|
-
|
|
3225
|
-
previewContainer.appendChild(
|
|
3226
|
-
createPreviewElement("🖼️", actualFileName),
|
|
3227
|
-
);
|
|
2979
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3228
2980
|
}
|
|
3229
2981
|
} catch (error) {
|
|
3230
2982
|
console.warn("getThumbnail failed for", resourceId, error);
|
|
3231
|
-
|
|
3232
|
-
previewContainer.appendChild(
|
|
3233
|
-
createPreviewElement("🖼️", actualFileName),
|
|
3234
|
-
);
|
|
2983
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3235
2984
|
}
|
|
3236
2985
|
} else {
|
|
3237
|
-
|
|
3238
|
-
previewContainer.appendChild(createPreviewElement("🖼️", actualFileName));
|
|
2986
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🖼️</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3239
2987
|
}
|
|
3240
2988
|
} else if (isVideo) {
|
|
3241
2989
|
// Video preview - try getThumbnail for video URL
|
|
@@ -3243,76 +2991,34 @@ function renderFilePreviewReadonly(resourceId, fileName) {
|
|
|
3243
2991
|
try {
|
|
3244
2992
|
const videoUrl = state.config.getThumbnail(resourceId);
|
|
3245
2993
|
if (videoUrl) {
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
video.appendChild(
|
|
3262
|
-
document.createTextNode("Ваш браузер не поддерживает видео."),
|
|
3263
|
-
);
|
|
3264
|
-
|
|
3265
|
-
const overlay = document.createElement("div");
|
|
3266
|
-
overlay.className =
|
|
3267
|
-
"absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none";
|
|
3268
|
-
|
|
3269
|
-
const overlayContent = document.createElement("div");
|
|
3270
|
-
overlayContent.className = "bg-white bg-opacity-90 rounded-full p-3";
|
|
3271
|
-
|
|
3272
|
-
const svg = document.createElementNS(
|
|
3273
|
-
"http://www.w3.org/2000/svg",
|
|
3274
|
-
"svg",
|
|
3275
|
-
);
|
|
3276
|
-
svg.setAttribute("class", "w-8 h-8 text-gray-800");
|
|
3277
|
-
svg.setAttribute("fill", "currentColor");
|
|
3278
|
-
svg.setAttribute("viewBox", "0 0 24 24");
|
|
3279
|
-
|
|
3280
|
-
const path = document.createElementNS(
|
|
3281
|
-
"http://www.w3.org/2000/svg",
|
|
3282
|
-
"path",
|
|
3283
|
-
);
|
|
3284
|
-
path.setAttribute("d", "M8 5v14l11-7z");
|
|
3285
|
-
|
|
3286
|
-
svg.appendChild(path);
|
|
3287
|
-
overlayContent.appendChild(svg);
|
|
3288
|
-
overlay.appendChild(overlayContent);
|
|
3289
|
-
|
|
3290
|
-
wrapper.appendChild(video);
|
|
3291
|
-
wrapper.appendChild(overlay);
|
|
3292
|
-
|
|
3293
|
-
clear(previewContainer);
|
|
3294
|
-
previewContainer.appendChild(wrapper);
|
|
2994
|
+
previewContainer.innerHTML = `
|
|
2995
|
+
<div class="relative group">
|
|
2996
|
+
<video class="w-full h-auto" controls preload="auto" muted>
|
|
2997
|
+
<source src="${videoUrl}" type="${meta?.type || "video/mp4"}">
|
|
2998
|
+
Ваш браузер не поддерживает видео.
|
|
2999
|
+
</video>
|
|
3000
|
+
<div class="absolute inset-0 bg-black bg-opacity-20 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center pointer-events-none">
|
|
3001
|
+
<div class="bg-white bg-opacity-90 rounded-full p-3">
|
|
3002
|
+
<svg class="w-8 h-8 text-gray-800" fill="currentColor" viewBox="0 0 24 24">
|
|
3003
|
+
<path d="M8 5v14l11-7z"/>
|
|
3004
|
+
</svg>
|
|
3005
|
+
</div>
|
|
3006
|
+
</div>
|
|
3007
|
+
</div>
|
|
3008
|
+
`;
|
|
3295
3009
|
} else {
|
|
3296
|
-
|
|
3297
|
-
previewContainer.appendChild(
|
|
3298
|
-
createPreviewElement("🎥", actualFileName),
|
|
3299
|
-
);
|
|
3010
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3300
3011
|
}
|
|
3301
3012
|
} catch (error) {
|
|
3302
3013
|
console.warn("getThumbnail failed for video", resourceId, error);
|
|
3303
|
-
|
|
3304
|
-
previewContainer.appendChild(
|
|
3305
|
-
createPreviewElement("🎥", actualFileName),
|
|
3306
|
-
);
|
|
3014
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3307
3015
|
}
|
|
3308
3016
|
} else {
|
|
3309
|
-
|
|
3310
|
-
previewContainer.appendChild(createPreviewElement("🎥", actualFileName));
|
|
3017
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">🎥</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3311
3018
|
}
|
|
3312
3019
|
} else {
|
|
3313
3020
|
// Other file types
|
|
3314
|
-
|
|
3315
|
-
previewContainer.appendChild(createPreviewElement("📁", actualFileName));
|
|
3021
|
+
previewContainer.innerHTML = `<div class="aspect-video flex items-center justify-center text-gray-400"><div class="text-center"><div class="text-4xl mb-2">📁</div><div class="text-sm">${actualFileName}</div></div></div>`;
|
|
3316
3022
|
}
|
|
3317
3023
|
|
|
3318
3024
|
// File name
|
|
@@ -3478,8 +3184,6 @@ function saveDraft() {
|
|
|
3478
3184
|
|
|
3479
3185
|
function clearForm() {
|
|
3480
3186
|
if (state.formRoot) {
|
|
3481
|
-
// Clean up any existing object URLs before clearing form
|
|
3482
|
-
revokeAllObjectURLs();
|
|
3483
3187
|
clear(state.formRoot);
|
|
3484
3188
|
}
|
|
3485
3189
|
}
|