@dmitryvim/form-builder 0.1.31 → 0.1.34
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 +88 -11
- package/dist/elements.html +1151 -0
- package/dist/elements.js +488 -0
- package/dist/form-builder.js +1868 -298
- package/dist/index.html +29 -1
- package/package.json +9 -4
- package/dist/images/final_video.mp4 +0 -0
- package/dist/images/infographic_draft.jpg +0 -0
package/dist/form-builder.js
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
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
|
+
|
|
2
54
|
// State management
|
|
3
55
|
const state = {
|
|
4
56
|
schema: null,
|
|
@@ -142,12 +194,18 @@ function renderForm(schema, prefill) {
|
|
|
142
194
|
return;
|
|
143
195
|
}
|
|
144
196
|
|
|
197
|
+
// Clean up any existing object URLs before clearing form
|
|
198
|
+
revokeAllObjectURLs();
|
|
145
199
|
clear(state.formRoot);
|
|
146
200
|
|
|
147
201
|
const formEl = document.createElement("div");
|
|
148
202
|
formEl.className = "space-y-6";
|
|
149
203
|
|
|
150
204
|
schema.elements.forEach((element, _index) => {
|
|
205
|
+
// Skip rendering hidden elements
|
|
206
|
+
if (element.hidden) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
151
209
|
const block = renderElement(element, {
|
|
152
210
|
path: "",
|
|
153
211
|
prefill: prefill || {},
|
|
@@ -180,8 +238,17 @@ function renderElement(element, ctx) {
|
|
|
180
238
|
const infoBtn = document.createElement("button");
|
|
181
239
|
infoBtn.type = "button";
|
|
182
240
|
infoBtn.className = "ml-2 text-gray-400 hover:text-gray-600";
|
|
183
|
-
|
|
184
|
-
|
|
241
|
+
// Create SVG icon safely
|
|
242
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
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);
|
|
185
252
|
|
|
186
253
|
// Create tooltip
|
|
187
254
|
const tooltipId = `tooltip-${element.key}-${Math.random().toString(36).substr(2, 9)}`;
|
|
@@ -209,23 +276,44 @@ function renderElement(element, ctx) {
|
|
|
209
276
|
|
|
210
277
|
switch (element.type) {
|
|
211
278
|
case "text":
|
|
212
|
-
|
|
279
|
+
if (element.multiple) {
|
|
280
|
+
renderMultipleTextElement(element, ctx, wrapper, pathKey);
|
|
281
|
+
} else {
|
|
282
|
+
renderTextElement(element, ctx, wrapper, pathKey);
|
|
283
|
+
}
|
|
213
284
|
break;
|
|
214
285
|
|
|
215
286
|
case "textarea":
|
|
216
|
-
|
|
287
|
+
if (element.multiple) {
|
|
288
|
+
renderMultipleTextareaElement(element, ctx, wrapper, pathKey);
|
|
289
|
+
} else {
|
|
290
|
+
renderTextareaElement(element, ctx, wrapper, pathKey);
|
|
291
|
+
}
|
|
217
292
|
break;
|
|
218
293
|
|
|
219
294
|
case "number":
|
|
220
|
-
|
|
295
|
+
if (element.multiple) {
|
|
296
|
+
renderMultipleNumberElement(element, ctx, wrapper, pathKey);
|
|
297
|
+
} else {
|
|
298
|
+
renderNumberElement(element, ctx, wrapper, pathKey);
|
|
299
|
+
}
|
|
221
300
|
break;
|
|
222
301
|
|
|
223
302
|
case "select":
|
|
224
|
-
|
|
303
|
+
if (element.multiple) {
|
|
304
|
+
renderMultipleSelectElement(element, ctx, wrapper, pathKey);
|
|
305
|
+
} else {
|
|
306
|
+
renderSelectElement(element, ctx, wrapper, pathKey);
|
|
307
|
+
}
|
|
225
308
|
break;
|
|
226
309
|
|
|
227
310
|
case "file":
|
|
228
|
-
|
|
311
|
+
// Handle multiple files with file type using multiple property
|
|
312
|
+
if (element.multiple) {
|
|
313
|
+
renderMultipleFileElement(element, ctx, wrapper, pathKey);
|
|
314
|
+
} else {
|
|
315
|
+
renderFileElement(element, ctx, wrapper, pathKey);
|
|
316
|
+
}
|
|
229
317
|
break;
|
|
230
318
|
|
|
231
319
|
case "files":
|
|
@@ -236,6 +324,15 @@ function renderElement(element, ctx) {
|
|
|
236
324
|
renderGroupElement(element, ctx, wrapper, pathKey);
|
|
237
325
|
break;
|
|
238
326
|
|
|
327
|
+
case "container":
|
|
328
|
+
// Handle containers with multiple property like groups
|
|
329
|
+
if (element.multiple) {
|
|
330
|
+
renderMultipleContainerElement(element, ctx, wrapper, pathKey);
|
|
331
|
+
} else {
|
|
332
|
+
renderSingleContainerElement(element, ctx, wrapper, pathKey);
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
|
|
239
336
|
default: {
|
|
240
337
|
const unsupported = document.createElement("div");
|
|
241
338
|
unsupported.className = "text-red-500 text-sm";
|
|
@@ -382,8 +479,8 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
382
479
|
reader.readAsDataURL(meta.file);
|
|
383
480
|
container.appendChild(img);
|
|
384
481
|
} else if (meta.type && meta.type.startsWith("video/")) {
|
|
385
|
-
// Video file - use object URL for preview
|
|
386
|
-
const videoUrl =
|
|
482
|
+
// Video file - use managed object URL for preview
|
|
483
|
+
const videoUrl = createObjectURL(meta.file, resourceId);
|
|
387
484
|
|
|
388
485
|
// Remove all conflicting handlers to prevent interference with video controls
|
|
389
486
|
container.onclick = null;
|
|
@@ -393,39 +490,55 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
393
490
|
container.parentNode.replaceChild(newContainer, container);
|
|
394
491
|
container = newContainer;
|
|
395
492
|
|
|
396
|
-
container
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
493
|
+
// Create video container safely
|
|
494
|
+
clear(container);
|
|
495
|
+
|
|
496
|
+
const wrapper = document.createElement("div");
|
|
497
|
+
wrapper.className = "relative group h-full";
|
|
498
|
+
|
|
499
|
+
const video = document.createElement("video");
|
|
500
|
+
video.className = "w-full h-full object-contain";
|
|
501
|
+
video.controls = true;
|
|
502
|
+
video.preload = "auto";
|
|
503
|
+
video.muted = true;
|
|
504
|
+
|
|
505
|
+
const source = document.createElement("source");
|
|
506
|
+
source.src = videoUrl;
|
|
507
|
+
source.type = meta.type;
|
|
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);
|
|
412
529
|
|
|
413
530
|
// Add click handlers to the custom buttons
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
}
|
|
531
|
+
changeBtn.onclick = (e) => {
|
|
532
|
+
e.stopPropagation();
|
|
533
|
+
if (deps?.picker) {
|
|
534
|
+
deps.picker.click();
|
|
535
|
+
}
|
|
536
|
+
};
|
|
423
537
|
|
|
424
|
-
|
|
425
|
-
if (deleteBtn) {
|
|
426
|
-
deleteBtn.onclick = (e) => {
|
|
538
|
+
deleteBtn.onclick = (e) => {
|
|
427
539
|
e.stopPropagation();
|
|
428
|
-
// Clear the file
|
|
540
|
+
// Clear the file and revoke object URL
|
|
541
|
+
revokeObjectURL(resourceId);
|
|
429
542
|
state.resourceIndex.delete(resourceId);
|
|
430
543
|
// Update hidden input
|
|
431
544
|
const hiddenInput = container.parentElement.querySelector(
|
|
@@ -441,27 +554,55 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
441
554
|
if (deps?.dragHandler) {
|
|
442
555
|
setupDragAndDrop(container, deps.dragHandler);
|
|
443
556
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
557
|
+
// Create placeholder content safely
|
|
558
|
+
clear(container);
|
|
559
|
+
|
|
560
|
+
const wrapper = document.createElement("div");
|
|
561
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
562
|
+
|
|
563
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
564
|
+
svg.setAttribute("class", "w-6 h-6 mb-2");
|
|
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);
|
|
452
580
|
};
|
|
453
|
-
}
|
|
454
581
|
} else {
|
|
455
|
-
// Non-image, non-video file
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
582
|
+
// Non-image, non-video file - create elements safely
|
|
583
|
+
const wrapper = document.createElement("div");
|
|
584
|
+
wrapper.className =
|
|
585
|
+
"flex flex-col items-center justify-center h-full text-gray-400";
|
|
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);
|
|
459
599
|
}
|
|
460
600
|
|
|
461
601
|
// Add delete button for edit mode (except for videos which have custom buttons)
|
|
462
602
|
if (!isReadonly && !(meta && meta.type && meta.type.startsWith("video/"))) {
|
|
463
603
|
addDeleteButton(container, () => {
|
|
464
|
-
// Clear the file
|
|
604
|
+
// Clear the file and revoke object URL
|
|
605
|
+
revokeObjectURL(resourceId);
|
|
465
606
|
state.resourceIndex.delete(resourceId);
|
|
466
607
|
// Update hidden input
|
|
467
608
|
const hiddenInput = container.parentElement.querySelector(
|
|
@@ -470,15 +611,29 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
470
611
|
if (hiddenInput) {
|
|
471
612
|
hiddenInput.value = "";
|
|
472
613
|
}
|
|
473
|
-
// Clear preview and show placeholder
|
|
474
|
-
container
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
614
|
+
// Clear preview and show placeholder safely
|
|
615
|
+
clear(container);
|
|
616
|
+
|
|
617
|
+
const wrapper = document.createElement("div");
|
|
618
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
619
|
+
|
|
620
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
621
|
+
svg.setAttribute("class", "w-6 h-6 mb-2");
|
|
622
|
+
svg.setAttribute("fill", "currentColor");
|
|
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);
|
|
482
637
|
});
|
|
483
638
|
}
|
|
484
639
|
} else if (state.config.getThumbnail) {
|
|
@@ -491,21 +646,50 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
491
646
|
container.appendChild(img);
|
|
492
647
|
} else {
|
|
493
648
|
// Fallback to file icon
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
649
|
+
// Create elements safely
|
|
650
|
+
const wrapper = document.createElement("div");
|
|
651
|
+
wrapper.className =
|
|
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);
|
|
497
666
|
}
|
|
498
667
|
} catch (error) {
|
|
499
668
|
console.warn("Thumbnail loading failed:", error);
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
669
|
+
// Create elements safely
|
|
670
|
+
const wrapper = document.createElement("div");
|
|
671
|
+
wrapper.className =
|
|
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);
|
|
503
686
|
}
|
|
504
687
|
|
|
505
688
|
// Add delete button for edit mode
|
|
506
689
|
if (!isReadonly) {
|
|
507
690
|
addDeleteButton(container, () => {
|
|
508
|
-
// Clear the file
|
|
691
|
+
// Clear the file and revoke object URL
|
|
692
|
+
revokeObjectURL(resourceId);
|
|
509
693
|
state.resourceIndex.delete(resourceId);
|
|
510
694
|
// Update hidden input
|
|
511
695
|
const hiddenInput = container.parentElement.querySelector(
|
|
@@ -514,22 +698,35 @@ async function renderFilePreview(container, resourceId, options = {}) {
|
|
|
514
698
|
if (hiddenInput) {
|
|
515
699
|
hiddenInput.value = "";
|
|
516
700
|
}
|
|
517
|
-
// Clear preview and show placeholder
|
|
518
|
-
container
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
701
|
+
// Clear preview and show placeholder safely
|
|
702
|
+
clear(container);
|
|
703
|
+
|
|
704
|
+
const wrapper = document.createElement("div");
|
|
705
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
706
|
+
|
|
707
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
708
|
+
svg.setAttribute("class", "w-6 h-6 mb-2");
|
|
709
|
+
svg.setAttribute("fill", "currentColor");
|
|
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);
|
|
526
724
|
});
|
|
527
725
|
}
|
|
528
726
|
} else {
|
|
529
|
-
// No file and no getThumbnail config - fallback
|
|
530
|
-
container
|
|
531
|
-
|
|
532
|
-
}</div></div>`;
|
|
727
|
+
// No file and no getThumbnail config - fallback - create elements safely
|
|
728
|
+
clear(container);
|
|
729
|
+
container.appendChild(createPreviewElement("🖼️", fileName));
|
|
533
730
|
}
|
|
534
731
|
|
|
535
732
|
// Add click handler for download in readonly mode
|
|
@@ -580,8 +777,15 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
580
777
|
|
|
581
778
|
// Add click handler to each slot
|
|
582
779
|
slot.onclick = () => {
|
|
583
|
-
// Look for file input
|
|
584
|
-
|
|
780
|
+
// Look for file input - check parent containers that have space-y-2 class
|
|
781
|
+
let filesWrapper = container.parentElement;
|
|
782
|
+
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
783
|
+
filesWrapper = filesWrapper.parentElement;
|
|
784
|
+
}
|
|
785
|
+
// If no parent with space-y-2, container itself might be the wrapper
|
|
786
|
+
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
787
|
+
filesWrapper = container;
|
|
788
|
+
}
|
|
585
789
|
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
586
790
|
if (fileInput) fileInput.click();
|
|
587
791
|
};
|
|
@@ -598,8 +802,15 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
598
802
|
uploadLink.textContent = t("uploadText");
|
|
599
803
|
uploadLink.onclick = (e) => {
|
|
600
804
|
e.stopPropagation();
|
|
601
|
-
// Look for file input
|
|
602
|
-
|
|
805
|
+
// Look for file input - check parent containers that have space-y-2 class
|
|
806
|
+
let filesWrapper = container.parentElement;
|
|
807
|
+
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
808
|
+
filesWrapper = filesWrapper.parentElement;
|
|
809
|
+
}
|
|
810
|
+
// If no parent with space-y-2, container itself might be the wrapper
|
|
811
|
+
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
812
|
+
filesWrapper = container;
|
|
813
|
+
}
|
|
603
814
|
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
604
815
|
if (fileInput) fileInput.click();
|
|
605
816
|
};
|
|
@@ -662,76 +873,179 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
662
873
|
img.src = url;
|
|
663
874
|
slot.appendChild(img);
|
|
664
875
|
} else {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
876
|
+
// Create fallback placeholder safely
|
|
877
|
+
const wrapper = document.createElement("div");
|
|
878
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
879
|
+
|
|
880
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
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);
|
|
670
891
|
}
|
|
671
892
|
} else {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
893
|
+
// Create fallback placeholder safely
|
|
894
|
+
const wrapper = document.createElement("div");
|
|
895
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
896
|
+
|
|
897
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
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);
|
|
677
908
|
}
|
|
678
909
|
} else if (meta && meta.type?.startsWith("video/")) {
|
|
679
910
|
if (meta.file && meta.file instanceof File) {
|
|
680
|
-
// Video file - use object URL for preview in thumbnail format
|
|
681
|
-
const videoUrl =
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
911
|
+
// Video file - use managed object URL for preview in thumbnail format
|
|
912
|
+
const videoUrl = createObjectURL(meta.file, rid);
|
|
913
|
+
// Create video thumbnail safely
|
|
914
|
+
const wrapper = document.createElement("div");
|
|
915
|
+
wrapper.className = "relative group h-full w-full";
|
|
916
|
+
|
|
917
|
+
const video = document.createElement("video");
|
|
918
|
+
video.className = "w-full h-full object-contain";
|
|
919
|
+
video.preload = "metadata";
|
|
920
|
+
video.muted = true;
|
|
921
|
+
|
|
922
|
+
const source = document.createElement("source");
|
|
923
|
+
source.src = videoUrl;
|
|
924
|
+
source.type = meta.type;
|
|
925
|
+
|
|
926
|
+
video.appendChild(source);
|
|
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);
|
|
696
948
|
} else if (state.config.getThumbnail) {
|
|
697
949
|
// Use getThumbnail for uploaded video files
|
|
698
950
|
const videoUrl = state.config.getThumbnail(rid);
|
|
699
951
|
if (videoUrl) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
952
|
+
// Create video thumbnail safely
|
|
953
|
+
const wrapper = document.createElement("div");
|
|
954
|
+
wrapper.className = "relative group h-full w-full";
|
|
955
|
+
|
|
956
|
+
const video = document.createElement("video");
|
|
957
|
+
video.className = "w-full h-full object-contain";
|
|
958
|
+
video.preload = "metadata";
|
|
959
|
+
video.muted = true;
|
|
960
|
+
|
|
961
|
+
const source = document.createElement("source");
|
|
962
|
+
source.src = videoUrl;
|
|
963
|
+
source.type = meta.type;
|
|
964
|
+
|
|
965
|
+
video.appendChild(source);
|
|
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);
|
|
714
987
|
} else {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
988
|
+
// Create video placeholder safely
|
|
989
|
+
const wrapper = document.createElement("div");
|
|
990
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
991
|
+
|
|
992
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
993
|
+
svg.setAttribute("class", "w-8 h-8");
|
|
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);
|
|
721
1009
|
}
|
|
722
1010
|
} else {
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1011
|
+
// Create video placeholder safely
|
|
1012
|
+
const wrapper = document.createElement("div");
|
|
1013
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
1014
|
+
|
|
1015
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1016
|
+
svg.setAttribute("class", "w-8 h-8");
|
|
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);
|
|
729
1032
|
}
|
|
730
1033
|
} else {
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
1034
|
+
// Create file placeholder safely
|
|
1035
|
+
const wrapper = document.createElement("div");
|
|
1036
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
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);
|
|
735
1049
|
}
|
|
736
1050
|
|
|
737
1051
|
// Add remove button overlay (similar to file field)
|
|
@@ -755,11 +1069,27 @@ function renderResourcePills(container, rids, onRemove) {
|
|
|
755
1069
|
// Empty slot placeholder
|
|
756
1070
|
slot.className =
|
|
757
1071
|
"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";
|
|
758
|
-
slot
|
|
759
|
-
|
|
1072
|
+
// Create empty slot SVG safely
|
|
1073
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
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);
|
|
760
1083
|
slot.onclick = () => {
|
|
761
|
-
// Look for file input
|
|
762
|
-
|
|
1084
|
+
// Look for file input - check parent containers that have space-y-2 class
|
|
1085
|
+
let filesWrapper = container.parentElement;
|
|
1086
|
+
while (filesWrapper && !filesWrapper.classList.contains("space-y-2")) {
|
|
1087
|
+
filesWrapper = filesWrapper.parentElement;
|
|
1088
|
+
}
|
|
1089
|
+
// If no parent with space-y-2, container itself might be the wrapper
|
|
1090
|
+
if (!filesWrapper && container.classList.contains("space-y-2")) {
|
|
1091
|
+
filesWrapper = container;
|
|
1092
|
+
}
|
|
763
1093
|
const fileInput = filesWrapper?.querySelector('input[type="file"]');
|
|
764
1094
|
if (fileInput) fileInput.click();
|
|
765
1095
|
};
|
|
@@ -811,14 +1141,37 @@ async function handleFileSelect(file, container, fieldName, deps = null) {
|
|
|
811
1141
|
});
|
|
812
1142
|
|
|
813
1143
|
// Create hidden input to store the resource ID
|
|
814
|
-
|
|
1144
|
+
// Handle case where container might be detached due to video replacement
|
|
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(
|
|
815
1168
|
'input[type="hidden"]',
|
|
816
1169
|
);
|
|
817
1170
|
if (!hiddenInput) {
|
|
818
1171
|
hiddenInput = document.createElement("input");
|
|
819
1172
|
hiddenInput.type = "hidden";
|
|
820
1173
|
hiddenInput.name = fieldName;
|
|
821
|
-
|
|
1174
|
+
parentElement.appendChild(hiddenInput);
|
|
822
1175
|
}
|
|
823
1176
|
hiddenInput.value = rid;
|
|
824
1177
|
|
|
@@ -979,94 +1332,287 @@ function validateForm(skipValidation = false) {
|
|
|
979
1332
|
switch (element.type) {
|
|
980
1333
|
case "text":
|
|
981
1334
|
case "textarea": {
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1335
|
+
if (element.multiple) {
|
|
1336
|
+
// Handle multiple text/textarea fields
|
|
1337
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1338
|
+
const values = [];
|
|
1339
|
+
|
|
1340
|
+
inputs.forEach((input, index) => {
|
|
1341
|
+
const val = input?.value ?? "";
|
|
1342
|
+
values.push(val);
|
|
1343
|
+
|
|
1344
|
+
if (!skipValidation && val) {
|
|
1345
|
+
if (
|
|
1346
|
+
element.minLength !== null &&
|
|
1347
|
+
val.length < element.minLength
|
|
1348
|
+
) {
|
|
1349
|
+
errors.push(`${key}[${index}]: minLength=${element.minLength}`);
|
|
1350
|
+
markValidity(input, `minLength=${element.minLength}`);
|
|
1351
|
+
}
|
|
1352
|
+
if (
|
|
1353
|
+
element.maxLength !== null &&
|
|
1354
|
+
val.length > element.maxLength
|
|
1355
|
+
) {
|
|
1356
|
+
errors.push(`${key}[${index}]: maxLength=${element.maxLength}`);
|
|
1357
|
+
markValidity(input, `maxLength=${element.maxLength}`);
|
|
1358
|
+
}
|
|
1359
|
+
if (element.pattern) {
|
|
1360
|
+
try {
|
|
1361
|
+
const re = new RegExp(element.pattern);
|
|
1362
|
+
if (!re.test(val)) {
|
|
1363
|
+
errors.push(`${key}[${index}]: pattern mismatch`);
|
|
1364
|
+
markValidity(input, "pattern mismatch");
|
|
1365
|
+
}
|
|
1366
|
+
} catch {
|
|
1367
|
+
errors.push(`${key}[${index}]: invalid pattern`);
|
|
1368
|
+
markValidity(input, "invalid pattern");
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
} else {
|
|
1372
|
+
markValidity(input, null);
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// Validate minCount/maxCount constraints
|
|
1377
|
+
if (!skipValidation) {
|
|
1378
|
+
const minCount = element.minCount ?? 1;
|
|
1379
|
+
const maxCount = element.maxCount ?? 10;
|
|
1380
|
+
const nonEmptyValues = values.filter((v) => v.trim() !== "");
|
|
1381
|
+
|
|
1382
|
+
if (element.required && nonEmptyValues.length === 0) {
|
|
1383
|
+
errors.push(`${key}: required`);
|
|
1384
|
+
}
|
|
1385
|
+
if (nonEmptyValues.length < minCount) {
|
|
1386
|
+
errors.push(`${key}: minimum ${minCount} items required`);
|
|
1387
|
+
}
|
|
1388
|
+
if (nonEmptyValues.length > maxCount) {
|
|
1389
|
+
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
1390
|
+
}
|
|
993
1391
|
}
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1392
|
+
|
|
1393
|
+
return values;
|
|
1394
|
+
} else {
|
|
1395
|
+
// Handle single text/textarea field
|
|
1396
|
+
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
1397
|
+
const val = input?.value ?? "";
|
|
1398
|
+
if (!skipValidation && element.required && val === "") {
|
|
1399
|
+
errors.push(`${key}: required`);
|
|
1400
|
+
markValidity(input, "required");
|
|
1401
|
+
return "";
|
|
997
1402
|
}
|
|
998
|
-
if (
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1403
|
+
if (!skipValidation && val) {
|
|
1404
|
+
if (element.minLength !== null && val.length < element.minLength) {
|
|
1405
|
+
errors.push(`${key}: minLength=${element.minLength}`);
|
|
1406
|
+
markValidity(input, `minLength=${element.minLength}`);
|
|
1407
|
+
}
|
|
1408
|
+
if (element.maxLength !== null && val.length > element.maxLength) {
|
|
1409
|
+
errors.push(`${key}: maxLength=${element.maxLength}`);
|
|
1410
|
+
markValidity(input, `maxLength=${element.maxLength}`);
|
|
1411
|
+
}
|
|
1412
|
+
if (element.pattern) {
|
|
1413
|
+
try {
|
|
1414
|
+
const re = new RegExp(element.pattern);
|
|
1415
|
+
if (!re.test(val)) {
|
|
1416
|
+
errors.push(`${key}: pattern mismatch`);
|
|
1417
|
+
markValidity(input, "pattern mismatch");
|
|
1418
|
+
}
|
|
1419
|
+
} catch {
|
|
1420
|
+
errors.push(`${key}: invalid pattern`);
|
|
1421
|
+
markValidity(input, "invalid pattern");
|
|
1004
1422
|
}
|
|
1005
|
-
} catch {
|
|
1006
|
-
errors.push(`${key}: invalid pattern`);
|
|
1007
|
-
markValidity(input, "invalid pattern");
|
|
1008
1423
|
}
|
|
1424
|
+
} else if (skipValidation) {
|
|
1425
|
+
markValidity(input, null);
|
|
1426
|
+
} else {
|
|
1427
|
+
markValidity(input, null);
|
|
1009
1428
|
}
|
|
1010
|
-
|
|
1011
|
-
markValidity(input, null);
|
|
1012
|
-
} else {
|
|
1013
|
-
markValidity(input, null);
|
|
1429
|
+
return val;
|
|
1014
1430
|
}
|
|
1015
|
-
return val;
|
|
1016
1431
|
}
|
|
1017
1432
|
case "number": {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1433
|
+
if (element.multiple) {
|
|
1434
|
+
// Handle multiple number fields
|
|
1435
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1436
|
+
const values = [];
|
|
1437
|
+
|
|
1438
|
+
inputs.forEach((input, index) => {
|
|
1439
|
+
const raw = input?.value ?? "";
|
|
1440
|
+
if (raw === "") {
|
|
1441
|
+
values.push(null);
|
|
1442
|
+
markValidity(input, null);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
const v = parseFloat(raw);
|
|
1447
|
+
if (!skipValidation && !Number.isFinite(v)) {
|
|
1448
|
+
errors.push(`${key}[${index}]: not a number`);
|
|
1449
|
+
markValidity(input, "not a number");
|
|
1450
|
+
values.push(null);
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (!skipValidation && element.min !== null && v < element.min) {
|
|
1455
|
+
errors.push(`${key}[${index}]: < min=${element.min}`);
|
|
1456
|
+
markValidity(input, `< min=${element.min}`);
|
|
1457
|
+
}
|
|
1458
|
+
if (!skipValidation && element.max !== null && v > element.max) {
|
|
1459
|
+
errors.push(`${key}[${index}]: > max=${element.max}`);
|
|
1460
|
+
markValidity(input, `> max=${element.max}`);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const d = Number.isInteger(element.decimals ?? 0)
|
|
1464
|
+
? element.decimals
|
|
1465
|
+
: 0;
|
|
1466
|
+
markValidity(input, null);
|
|
1467
|
+
values.push(Number(v.toFixed(d)));
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// Validate minCount/maxCount constraints
|
|
1471
|
+
if (!skipValidation) {
|
|
1472
|
+
const minCount = element.minCount ?? 1;
|
|
1473
|
+
const maxCount = element.maxCount ?? 10;
|
|
1474
|
+
const nonNullValues = values.filter((v) => v !== null);
|
|
1475
|
+
|
|
1476
|
+
if (element.required && nonNullValues.length === 0) {
|
|
1477
|
+
errors.push(`${key}: required`);
|
|
1478
|
+
}
|
|
1479
|
+
if (nonNullValues.length < minCount) {
|
|
1480
|
+
errors.push(`${key}: minimum ${minCount} items required`);
|
|
1481
|
+
}
|
|
1482
|
+
if (nonNullValues.length > maxCount) {
|
|
1483
|
+
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return values;
|
|
1488
|
+
} else {
|
|
1489
|
+
// Handle single number field
|
|
1490
|
+
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
1491
|
+
const raw = input?.value ?? "";
|
|
1492
|
+
if (!skipValidation && element.required && raw === "") {
|
|
1493
|
+
errors.push(`${key}: required`);
|
|
1494
|
+
markValidity(input, "required");
|
|
1495
|
+
return null;
|
|
1496
|
+
}
|
|
1497
|
+
if (raw === "") {
|
|
1498
|
+
markValidity(input, null);
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
const v = parseFloat(raw);
|
|
1502
|
+
if (!skipValidation && !Number.isFinite(v)) {
|
|
1503
|
+
errors.push(`${key}: not a number`);
|
|
1504
|
+
markValidity(input, "not a number");
|
|
1505
|
+
return null;
|
|
1506
|
+
}
|
|
1507
|
+
if (!skipValidation && element.min !== null && v < element.min) {
|
|
1508
|
+
errors.push(`${key}: < min=${element.min}`);
|
|
1509
|
+
markValidity(input, `< min=${element.min}`);
|
|
1510
|
+
}
|
|
1511
|
+
if (!skipValidation && element.max !== null && v > element.max) {
|
|
1512
|
+
errors.push(`${key}: > max=${element.max}`);
|
|
1513
|
+
markValidity(input, `> max=${element.max}`);
|
|
1514
|
+
}
|
|
1515
|
+
const d = Number.isInteger(element.decimals ?? 0)
|
|
1516
|
+
? element.decimals
|
|
1517
|
+
: 0;
|
|
1026
1518
|
markValidity(input, null);
|
|
1027
|
-
return
|
|
1519
|
+
return Number(v.toFixed(d));
|
|
1028
1520
|
}
|
|
1029
|
-
const v = parseFloat(raw);
|
|
1030
|
-
if (!skipValidation && !Number.isFinite(v)) {
|
|
1031
|
-
errors.push(`${key}: not a number`);
|
|
1032
|
-
markValidity(input, "not a number");
|
|
1033
|
-
return null;
|
|
1034
|
-
}
|
|
1035
|
-
if (!skipValidation && element.min !== null && v < element.min) {
|
|
1036
|
-
errors.push(`${key}: < min=${element.min}`);
|
|
1037
|
-
markValidity(input, `< min=${element.min}`);
|
|
1038
|
-
}
|
|
1039
|
-
if (!skipValidation && element.max !== null && v > element.max) {
|
|
1040
|
-
errors.push(`${key}: > max=${element.max}`);
|
|
1041
|
-
markValidity(input, `> max=${element.max}`);
|
|
1042
|
-
}
|
|
1043
|
-
const d = Number.isInteger(element.decimals ?? 0)
|
|
1044
|
-
? element.decimals
|
|
1045
|
-
: 0;
|
|
1046
|
-
markValidity(input, null);
|
|
1047
|
-
return Number(v.toFixed(d));
|
|
1048
1521
|
}
|
|
1049
1522
|
case "select": {
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1523
|
+
if (element.multiple) {
|
|
1524
|
+
// Handle multiple select fields
|
|
1525
|
+
const inputs = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1526
|
+
const values = [];
|
|
1527
|
+
|
|
1528
|
+
inputs.forEach((input) => {
|
|
1529
|
+
const val = input?.value ?? "";
|
|
1530
|
+
values.push(val);
|
|
1531
|
+
markValidity(input, null);
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
// Validate minCount/maxCount constraints
|
|
1535
|
+
if (!skipValidation) {
|
|
1536
|
+
const minCount = element.minCount ?? 1;
|
|
1537
|
+
const maxCount = element.maxCount ?? 10;
|
|
1538
|
+
const nonEmptyValues = values.filter((v) => v !== "");
|
|
1539
|
+
|
|
1540
|
+
if (element.required && nonEmptyValues.length === 0) {
|
|
1541
|
+
errors.push(`${key}: required`);
|
|
1542
|
+
}
|
|
1543
|
+
if (nonEmptyValues.length < minCount) {
|
|
1544
|
+
errors.push(`${key}: minimum ${minCount} items required`);
|
|
1545
|
+
}
|
|
1546
|
+
if (nonEmptyValues.length > maxCount) {
|
|
1547
|
+
errors.push(`${key}: maximum ${maxCount} items allowed`);
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
return values;
|
|
1552
|
+
} else {
|
|
1553
|
+
// Handle single select field
|
|
1554
|
+
const input = scopeRoot.querySelector(`[name$="${key}"]`);
|
|
1555
|
+
const val = input?.value ?? "";
|
|
1556
|
+
if (!skipValidation && element.required && val === "") {
|
|
1557
|
+
errors.push(`${key}: required`);
|
|
1558
|
+
markValidity(input, "required");
|
|
1559
|
+
return "";
|
|
1560
|
+
}
|
|
1561
|
+
markValidity(input, null);
|
|
1562
|
+
return val;
|
|
1056
1563
|
}
|
|
1057
|
-
markValidity(input, null);
|
|
1058
|
-
return val;
|
|
1059
1564
|
}
|
|
1060
1565
|
case "file": {
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1566
|
+
if (element.multiple) {
|
|
1567
|
+
// Handle file with multiple property like files type
|
|
1568
|
+
// Find the files list by locating the specific file input for this field
|
|
1569
|
+
const fullKey = pathJoin(ctx.path, key);
|
|
1570
|
+
const pickerInput = scopeRoot.querySelector(
|
|
1571
|
+
`input[type="file"][name="${fullKey}"]`,
|
|
1572
|
+
);
|
|
1573
|
+
const filesWrapper = pickerInput?.closest(".space-y-2");
|
|
1574
|
+
const container = filesWrapper?.querySelector(".files-list") || null;
|
|
1575
|
+
|
|
1576
|
+
const resourceIds = [];
|
|
1577
|
+
if (container) {
|
|
1578
|
+
const pills = container.querySelectorAll(".resource-pill");
|
|
1579
|
+
pills.forEach((pill) => {
|
|
1580
|
+
const resourceId = pill.dataset.resourceId;
|
|
1581
|
+
if (resourceId) {
|
|
1582
|
+
resourceIds.push(resourceId);
|
|
1583
|
+
}
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Validate minCount/maxCount constraints
|
|
1588
|
+
if (!skipValidation) {
|
|
1589
|
+
const minFiles = element.minCount ?? 0;
|
|
1590
|
+
const maxFiles = element.maxCount ?? Infinity;
|
|
1591
|
+
|
|
1592
|
+
if (element.required && resourceIds.length === 0) {
|
|
1593
|
+
errors.push(`${key}: required`);
|
|
1594
|
+
}
|
|
1595
|
+
if (resourceIds.length < minFiles) {
|
|
1596
|
+
errors.push(`${key}: minimum ${minFiles} files required`);
|
|
1597
|
+
}
|
|
1598
|
+
if (resourceIds.length > maxFiles) {
|
|
1599
|
+
errors.push(`${key}: maximum ${maxFiles} files allowed`);
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
return resourceIds;
|
|
1604
|
+
} else {
|
|
1605
|
+
// Handle single file
|
|
1606
|
+
const input = scopeRoot.querySelector(
|
|
1607
|
+
`input[name$="${key}"][type="hidden"]`,
|
|
1608
|
+
);
|
|
1609
|
+
const rid = input?.value ?? "";
|
|
1610
|
+
if (!skipValidation && element.required && rid === "") {
|
|
1611
|
+
errors.push(`${key}: required`);
|
|
1612
|
+
return null;
|
|
1613
|
+
}
|
|
1614
|
+
return rid || null;
|
|
1068
1615
|
}
|
|
1069
|
-
return rid || null;
|
|
1070
1616
|
}
|
|
1071
1617
|
case "files": {
|
|
1072
1618
|
// For files, we need to collect all resource IDs
|
|
@@ -1110,6 +1656,77 @@ function validateForm(skipValidation = false) {
|
|
|
1110
1656
|
}
|
|
1111
1657
|
case "group": {
|
|
1112
1658
|
if (element.repeat && isPlainObject(element.repeat)) {
|
|
1659
|
+
const items = [];
|
|
1660
|
+
// Use full path for nested group element search
|
|
1661
|
+
const fullKey = pathJoin(ctx.path, key);
|
|
1662
|
+
const itemElements = scopeRoot.querySelectorAll(
|
|
1663
|
+
`[name^="${fullKey}["]`,
|
|
1664
|
+
);
|
|
1665
|
+
|
|
1666
|
+
// Extract actual indices from DOM element names instead of assuming sequential numbering
|
|
1667
|
+
const actualIndices = new Set();
|
|
1668
|
+
itemElements.forEach((el) => {
|
|
1669
|
+
const match = el.name.match(
|
|
1670
|
+
new RegExp(
|
|
1671
|
+
`^${fullKey.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\[(\\d+)\\]`,
|
|
1672
|
+
),
|
|
1673
|
+
);
|
|
1674
|
+
if (match) {
|
|
1675
|
+
actualIndices.add(parseInt(match[1]));
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
const sortedIndices = Array.from(actualIndices).sort((a, b) => a - b);
|
|
1680
|
+
|
|
1681
|
+
sortedIndices.forEach((actualIndex) => {
|
|
1682
|
+
const itemData = {};
|
|
1683
|
+
// Find the specific group item container for scoped queries - use full path
|
|
1684
|
+
const fullItemPath = `${fullKey}[${actualIndex}]`;
|
|
1685
|
+
const itemContainer =
|
|
1686
|
+
scopeRoot.querySelector(`[data-group-item="${fullItemPath}"]`) ||
|
|
1687
|
+
scopeRoot;
|
|
1688
|
+
element.elements.forEach((child) => {
|
|
1689
|
+
if (child.hidden) {
|
|
1690
|
+
// For hidden child elements, use their default value
|
|
1691
|
+
itemData[child.key] =
|
|
1692
|
+
child.default !== undefined ? child.default : "";
|
|
1693
|
+
} else {
|
|
1694
|
+
const childKey = `${fullKey}[${actualIndex}].${child.key}`;
|
|
1695
|
+
itemData[child.key] = validateElement(
|
|
1696
|
+
{ ...child, key: childKey },
|
|
1697
|
+
ctx,
|
|
1698
|
+
itemContainer,
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
});
|
|
1702
|
+
items.push(itemData);
|
|
1703
|
+
});
|
|
1704
|
+
return items;
|
|
1705
|
+
} else {
|
|
1706
|
+
const groupData = {};
|
|
1707
|
+
// Find the specific group container for scoped queries
|
|
1708
|
+
const groupContainer =
|
|
1709
|
+
scopeRoot.querySelector(`[data-group="${key}"]`) || scopeRoot;
|
|
1710
|
+
element.elements.forEach((child) => {
|
|
1711
|
+
if (child.hidden) {
|
|
1712
|
+
// For hidden child elements, use their default value
|
|
1713
|
+
groupData[child.key] =
|
|
1714
|
+
child.default !== undefined ? child.default : "";
|
|
1715
|
+
} else {
|
|
1716
|
+
const childKey = `${key}.${child.key}`;
|
|
1717
|
+
groupData[child.key] = validateElement(
|
|
1718
|
+
{ ...child, key: childKey },
|
|
1719
|
+
ctx,
|
|
1720
|
+
groupContainer,
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
});
|
|
1724
|
+
return groupData;
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
case "container": {
|
|
1728
|
+
if (element.multiple) {
|
|
1729
|
+
// Handle multiple containers like repeating groups
|
|
1113
1730
|
const items = [];
|
|
1114
1731
|
const itemElements = scopeRoot.querySelectorAll(`[name^="${key}["]`);
|
|
1115
1732
|
const itemCount = Math.max(
|
|
@@ -1119,35 +1736,64 @@ function validateForm(skipValidation = false) {
|
|
|
1119
1736
|
|
|
1120
1737
|
for (let i = 0; i < itemCount; i++) {
|
|
1121
1738
|
const itemData = {};
|
|
1122
|
-
// Find the specific
|
|
1739
|
+
// Find the specific container item container for scoped queries
|
|
1123
1740
|
const itemContainer =
|
|
1124
|
-
scopeRoot.querySelector(`[data-
|
|
1741
|
+
scopeRoot.querySelector(`[data-container-item="${key}[${i}]"]`) ||
|
|
1125
1742
|
scopeRoot;
|
|
1126
1743
|
element.elements.forEach((child) => {
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1744
|
+
if (child.hidden) {
|
|
1745
|
+
// For hidden child elements, use their default value
|
|
1746
|
+
itemData[child.key] =
|
|
1747
|
+
child.default !== undefined ? child.default : "";
|
|
1748
|
+
} else {
|
|
1749
|
+
const childKey = `${key}[${i}].${child.key}`;
|
|
1750
|
+
itemData[child.key] = validateElement(
|
|
1751
|
+
{ ...child, key: childKey },
|
|
1752
|
+
ctx,
|
|
1753
|
+
itemContainer,
|
|
1754
|
+
);
|
|
1755
|
+
}
|
|
1133
1756
|
});
|
|
1134
1757
|
items.push(itemData);
|
|
1135
1758
|
}
|
|
1759
|
+
|
|
1760
|
+
// Validate minCount/maxCount constraints
|
|
1761
|
+
if (!skipValidation) {
|
|
1762
|
+
const minItems = element.minCount ?? 0;
|
|
1763
|
+
const maxItems = element.maxCount ?? Infinity;
|
|
1764
|
+
|
|
1765
|
+
if (element.required && items.length === 0) {
|
|
1766
|
+
errors.push(`${key}: required`);
|
|
1767
|
+
}
|
|
1768
|
+
if (items.length < minItems) {
|
|
1769
|
+
errors.push(`${key}: minimum ${minItems} items required`);
|
|
1770
|
+
}
|
|
1771
|
+
if (items.length > maxItems) {
|
|
1772
|
+
errors.push(`${key}: maximum ${maxItems} items allowed`);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1136
1776
|
return items;
|
|
1137
1777
|
} else {
|
|
1138
|
-
const
|
|
1139
|
-
// Find the specific
|
|
1140
|
-
const
|
|
1141
|
-
scopeRoot.querySelector(`[data-
|
|
1778
|
+
const containerData = {};
|
|
1779
|
+
// Find the specific container container for scoped queries
|
|
1780
|
+
const containerContainer =
|
|
1781
|
+
scopeRoot.querySelector(`[data-container="${key}"]`) || scopeRoot;
|
|
1142
1782
|
element.elements.forEach((child) => {
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1783
|
+
if (child.hidden) {
|
|
1784
|
+
// For hidden child elements, use their default value
|
|
1785
|
+
containerData[child.key] =
|
|
1786
|
+
child.default !== undefined ? child.default : "";
|
|
1787
|
+
} else {
|
|
1788
|
+
const childKey = `${key}.${child.key}`;
|
|
1789
|
+
containerData[child.key] = validateElement(
|
|
1790
|
+
{ ...child, key: childKey },
|
|
1791
|
+
ctx,
|
|
1792
|
+
containerContainer,
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1149
1795
|
});
|
|
1150
|
-
return
|
|
1796
|
+
return containerData;
|
|
1151
1797
|
}
|
|
1152
1798
|
}
|
|
1153
1799
|
default:
|
|
@@ -1156,7 +1802,12 @@ function validateForm(skipValidation = false) {
|
|
|
1156
1802
|
}
|
|
1157
1803
|
|
|
1158
1804
|
state.schema.elements.forEach((element) => {
|
|
1159
|
-
|
|
1805
|
+
// Handle hidden elements - use their default value instead of reading from DOM
|
|
1806
|
+
if (element.hidden) {
|
|
1807
|
+
data[element.key] = element.default !== undefined ? element.default : "";
|
|
1808
|
+
} else {
|
|
1809
|
+
data[element.key] = validateElement(element, { path: "" });
|
|
1810
|
+
}
|
|
1160
1811
|
});
|
|
1161
1812
|
|
|
1162
1813
|
return {
|
|
@@ -1185,24 +1836,254 @@ function renderTextElement(element, ctx, wrapper, pathKey) {
|
|
|
1185
1836
|
wrapper.appendChild(textHint);
|
|
1186
1837
|
}
|
|
1187
1838
|
|
|
1188
|
-
function
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
1192
|
-
textareaInput.name = pathKey;
|
|
1193
|
-
textareaInput.placeholder = element.placeholder || "Введите текст";
|
|
1194
|
-
textareaInput.rows = element.rows || 4;
|
|
1195
|
-
textareaInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1196
|
-
textareaInput.readOnly = state.config.readonly;
|
|
1197
|
-
wrapper.appendChild(textareaInput);
|
|
1839
|
+
function renderMultipleTextElement(element, ctx, wrapper, pathKey) {
|
|
1840
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
1841
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1198
1842
|
|
|
1199
|
-
//
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1843
|
+
// Ensure minimum count
|
|
1844
|
+
const minCount = element.minCount ?? 1;
|
|
1845
|
+
const maxCount = element.maxCount ?? 10;
|
|
1846
|
+
|
|
1847
|
+
while (values.length < minCount) {
|
|
1848
|
+
values.push(element.default || "");
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
const container = document.createElement("div");
|
|
1852
|
+
container.className = "space-y-2";
|
|
1853
|
+
wrapper.appendChild(container);
|
|
1854
|
+
|
|
1855
|
+
function updateIndices() {
|
|
1856
|
+
const items = container.querySelectorAll(".multiple-text-item");
|
|
1857
|
+
items.forEach((item, index) => {
|
|
1858
|
+
const input = item.querySelector("input");
|
|
1859
|
+
if (input) {
|
|
1860
|
+
input.name = `${pathKey}[${index}]`;
|
|
1861
|
+
}
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function addTextItem(value = "", index = -1) {
|
|
1866
|
+
const itemWrapper = document.createElement("div");
|
|
1867
|
+
itemWrapper.className = "multiple-text-item flex items-center gap-2";
|
|
1868
|
+
|
|
1869
|
+
const textInput = document.createElement("input");
|
|
1870
|
+
textInput.type = "text";
|
|
1871
|
+
textInput.className =
|
|
1872
|
+
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
1873
|
+
textInput.placeholder = element.placeholder || "Enter text";
|
|
1874
|
+
textInput.value = value;
|
|
1875
|
+
textInput.readOnly = state.config.readonly;
|
|
1876
|
+
|
|
1877
|
+
itemWrapper.appendChild(textInput);
|
|
1878
|
+
|
|
1879
|
+
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
1880
|
+
|
|
1881
|
+
if (index === -1) {
|
|
1882
|
+
container.appendChild(itemWrapper);
|
|
1883
|
+
} else {
|
|
1884
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
updateIndices();
|
|
1888
|
+
return itemWrapper;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
function updateRemoveButtons() {
|
|
1892
|
+
if (state.config.readonly) return;
|
|
1893
|
+
const items = container.querySelectorAll(".multiple-text-item");
|
|
1894
|
+
const currentCount = items.length;
|
|
1895
|
+
items.forEach((item) => {
|
|
1896
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
1897
|
+
if (!removeBtn) {
|
|
1898
|
+
removeBtn = document.createElement("button");
|
|
1899
|
+
removeBtn.type = "button";
|
|
1900
|
+
removeBtn.className =
|
|
1901
|
+
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
1902
|
+
removeBtn.textContent = "✕";
|
|
1903
|
+
removeBtn.onclick = () => {
|
|
1904
|
+
const currentIndex = Array.from(container.children).indexOf(item);
|
|
1905
|
+
if (container.children.length > minCount) {
|
|
1906
|
+
values.splice(currentIndex, 1);
|
|
1907
|
+
item.remove();
|
|
1908
|
+
updateIndices();
|
|
1909
|
+
updateAddButton();
|
|
1910
|
+
updateRemoveButtons();
|
|
1911
|
+
}
|
|
1912
|
+
};
|
|
1913
|
+
item.appendChild(removeBtn);
|
|
1914
|
+
}
|
|
1915
|
+
const disabled = currentCount <= minCount;
|
|
1916
|
+
removeBtn.disabled = disabled;
|
|
1917
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
1918
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
function updateAddButton() {
|
|
1923
|
+
const existingAddBtn = wrapper.querySelector(".add-text-btn");
|
|
1924
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
1925
|
+
|
|
1926
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
1927
|
+
const addBtn = document.createElement("button");
|
|
1928
|
+
addBtn.type = "button";
|
|
1929
|
+
addBtn.className =
|
|
1930
|
+
"add-text-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
1931
|
+
addBtn.textContent = `+ Add ${element.label || "Text"}`;
|
|
1932
|
+
addBtn.onclick = () => {
|
|
1933
|
+
values.push(element.default || "");
|
|
1934
|
+
addTextItem(element.default || "");
|
|
1935
|
+
updateAddButton();
|
|
1936
|
+
updateRemoveButtons();
|
|
1937
|
+
};
|
|
1938
|
+
wrapper.appendChild(addBtn);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// Render initial items
|
|
1943
|
+
values.forEach((value) => addTextItem(value));
|
|
1944
|
+
updateAddButton();
|
|
1945
|
+
updateRemoveButtons();
|
|
1946
|
+
|
|
1947
|
+
// Add hint
|
|
1948
|
+
const hint = document.createElement("p");
|
|
1949
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
1950
|
+
hint.textContent = makeFieldHint(element);
|
|
1951
|
+
wrapper.appendChild(hint);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
function renderTextareaElement(element, ctx, wrapper, pathKey) {
|
|
1955
|
+
const textareaInput = document.createElement("textarea");
|
|
1956
|
+
textareaInput.className =
|
|
1957
|
+
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
1958
|
+
textareaInput.name = pathKey;
|
|
1959
|
+
textareaInput.placeholder = element.placeholder || "Введите текст";
|
|
1960
|
+
textareaInput.rows = element.rows || 4;
|
|
1961
|
+
textareaInput.value = ctx.prefill[element.key] || element.default || "";
|
|
1962
|
+
textareaInput.readOnly = state.config.readonly;
|
|
1963
|
+
wrapper.appendChild(textareaInput);
|
|
1964
|
+
|
|
1965
|
+
// Add hint
|
|
1966
|
+
const textareaHint = document.createElement("p");
|
|
1967
|
+
textareaHint.className = "text-xs text-gray-500 mt-1";
|
|
1202
1968
|
textareaHint.textContent = makeFieldHint(element);
|
|
1203
1969
|
wrapper.appendChild(textareaHint);
|
|
1204
1970
|
}
|
|
1205
1971
|
|
|
1972
|
+
function renderMultipleTextareaElement(element, ctx, wrapper, pathKey) {
|
|
1973
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
1974
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
1975
|
+
|
|
1976
|
+
// Ensure minimum count
|
|
1977
|
+
const minCount = element.minCount ?? 1;
|
|
1978
|
+
const maxCount = element.maxCount ?? 10;
|
|
1979
|
+
|
|
1980
|
+
while (values.length < minCount) {
|
|
1981
|
+
values.push(element.default || "");
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
const container = document.createElement("div");
|
|
1985
|
+
container.className = "space-y-2";
|
|
1986
|
+
wrapper.appendChild(container);
|
|
1987
|
+
|
|
1988
|
+
function updateIndices() {
|
|
1989
|
+
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
1990
|
+
items.forEach((item, index) => {
|
|
1991
|
+
const textarea = item.querySelector("textarea");
|
|
1992
|
+
if (textarea) {
|
|
1993
|
+
textarea.name = `${pathKey}[${index}]`;
|
|
1994
|
+
}
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function addTextareaItem(value = "", index = -1) {
|
|
1999
|
+
const itemWrapper = document.createElement("div");
|
|
2000
|
+
itemWrapper.className = "multiple-textarea-item";
|
|
2001
|
+
|
|
2002
|
+
const textareaInput = document.createElement("textarea");
|
|
2003
|
+
textareaInput.className =
|
|
2004
|
+
"w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-none";
|
|
2005
|
+
textareaInput.placeholder = element.placeholder || "Enter text";
|
|
2006
|
+
textareaInput.rows = element.rows || 4;
|
|
2007
|
+
textareaInput.value = value;
|
|
2008
|
+
textareaInput.readOnly = state.config.readonly;
|
|
2009
|
+
|
|
2010
|
+
itemWrapper.appendChild(textareaInput);
|
|
2011
|
+
|
|
2012
|
+
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
2013
|
+
|
|
2014
|
+
if (index === -1) {
|
|
2015
|
+
container.appendChild(itemWrapper);
|
|
2016
|
+
} else {
|
|
2017
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
updateIndices();
|
|
2021
|
+
return itemWrapper;
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
function updateRemoveButtons() {
|
|
2025
|
+
if (state.config.readonly) return;
|
|
2026
|
+
const items = container.querySelectorAll(".multiple-textarea-item");
|
|
2027
|
+
const currentCount = items.length;
|
|
2028
|
+
items.forEach((item) => {
|
|
2029
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
2030
|
+
if (!removeBtn) {
|
|
2031
|
+
removeBtn = document.createElement("button");
|
|
2032
|
+
removeBtn.type = "button";
|
|
2033
|
+
removeBtn.className =
|
|
2034
|
+
"remove-item-btn mt-1 px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm";
|
|
2035
|
+
removeBtn.textContent = "✕ Remove";
|
|
2036
|
+
removeBtn.onclick = () => {
|
|
2037
|
+
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2038
|
+
if (container.children.length > minCount) {
|
|
2039
|
+
values.splice(currentIndex, 1);
|
|
2040
|
+
item.remove();
|
|
2041
|
+
updateIndices();
|
|
2042
|
+
updateAddButton();
|
|
2043
|
+
updateRemoveButtons();
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
item.appendChild(removeBtn);
|
|
2047
|
+
}
|
|
2048
|
+
const disabled = currentCount <= minCount;
|
|
2049
|
+
removeBtn.disabled = disabled;
|
|
2050
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2051
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2052
|
+
});
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
function updateAddButton() {
|
|
2056
|
+
const existingAddBtn = wrapper.querySelector(".add-textarea-btn");
|
|
2057
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
2058
|
+
|
|
2059
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
2060
|
+
const addBtn = document.createElement("button");
|
|
2061
|
+
addBtn.type = "button";
|
|
2062
|
+
addBtn.className =
|
|
2063
|
+
"add-textarea-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2064
|
+
addBtn.textContent = `+ Add ${element.label || "Textarea"}`;
|
|
2065
|
+
addBtn.onclick = () => {
|
|
2066
|
+
values.push(element.default || "");
|
|
2067
|
+
addTextareaItem(element.default || "");
|
|
2068
|
+
updateAddButton();
|
|
2069
|
+
updateRemoveButtons();
|
|
2070
|
+
};
|
|
2071
|
+
wrapper.appendChild(addBtn);
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Render initial items
|
|
2076
|
+
values.forEach((value) => addTextareaItem(value));
|
|
2077
|
+
updateAddButton();
|
|
2078
|
+
updateRemoveButtons();
|
|
2079
|
+
|
|
2080
|
+
// Add hint
|
|
2081
|
+
const hint = document.createElement("p");
|
|
2082
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
2083
|
+
hint.textContent = makeFieldHint(element);
|
|
2084
|
+
wrapper.appendChild(hint);
|
|
2085
|
+
}
|
|
2086
|
+
|
|
1206
2087
|
function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
1207
2088
|
const numberInput = document.createElement("input");
|
|
1208
2089
|
numberInput.type = "number";
|
|
@@ -1224,6 +2105,124 @@ function renderNumberElement(element, ctx, wrapper, pathKey) {
|
|
|
1224
2105
|
wrapper.appendChild(numberHint);
|
|
1225
2106
|
}
|
|
1226
2107
|
|
|
2108
|
+
function renderMultipleNumberElement(element, ctx, wrapper, pathKey) {
|
|
2109
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
2110
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2111
|
+
|
|
2112
|
+
// Ensure minimum count
|
|
2113
|
+
const minCount = element.minCount ?? 1;
|
|
2114
|
+
const maxCount = element.maxCount ?? 10;
|
|
2115
|
+
|
|
2116
|
+
while (values.length < minCount) {
|
|
2117
|
+
values.push(element.default || "");
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
const container = document.createElement("div");
|
|
2121
|
+
container.className = "space-y-2";
|
|
2122
|
+
wrapper.appendChild(container);
|
|
2123
|
+
|
|
2124
|
+
function updateIndices() {
|
|
2125
|
+
const items = container.querySelectorAll(".multiple-number-item");
|
|
2126
|
+
items.forEach((item, index) => {
|
|
2127
|
+
const input = item.querySelector("input");
|
|
2128
|
+
if (input) {
|
|
2129
|
+
input.name = `${pathKey}[${index}]`;
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function addNumberItem(value = "", index = -1) {
|
|
2135
|
+
const itemWrapper = document.createElement("div");
|
|
2136
|
+
itemWrapper.className = "multiple-number-item flex items-center gap-2";
|
|
2137
|
+
|
|
2138
|
+
const numberInput = document.createElement("input");
|
|
2139
|
+
numberInput.type = "number";
|
|
2140
|
+
numberInput.className =
|
|
2141
|
+
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
2142
|
+
numberInput.placeholder = element.placeholder || "0";
|
|
2143
|
+
if (element.min !== undefined) numberInput.min = element.min;
|
|
2144
|
+
if (element.max !== undefined) numberInput.max = element.max;
|
|
2145
|
+
if (element.step !== undefined) numberInput.step = element.step;
|
|
2146
|
+
numberInput.value = value;
|
|
2147
|
+
numberInput.readOnly = state.config.readonly;
|
|
2148
|
+
|
|
2149
|
+
itemWrapper.appendChild(numberInput);
|
|
2150
|
+
|
|
2151
|
+
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
2152
|
+
|
|
2153
|
+
if (index === -1) {
|
|
2154
|
+
container.appendChild(itemWrapper);
|
|
2155
|
+
} else {
|
|
2156
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
updateIndices();
|
|
2160
|
+
return itemWrapper;
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
function updateRemoveButtons() {
|
|
2164
|
+
if (state.config.readonly) return;
|
|
2165
|
+
const items = container.querySelectorAll(".multiple-number-item");
|
|
2166
|
+
const currentCount = items.length;
|
|
2167
|
+
items.forEach((item) => {
|
|
2168
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
2169
|
+
if (!removeBtn) {
|
|
2170
|
+
removeBtn = document.createElement("button");
|
|
2171
|
+
removeBtn.type = "button";
|
|
2172
|
+
removeBtn.className =
|
|
2173
|
+
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2174
|
+
removeBtn.textContent = "✕";
|
|
2175
|
+
removeBtn.onclick = () => {
|
|
2176
|
+
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2177
|
+
if (container.children.length > minCount) {
|
|
2178
|
+
values.splice(currentIndex, 1);
|
|
2179
|
+
item.remove();
|
|
2180
|
+
updateIndices();
|
|
2181
|
+
updateAddButton();
|
|
2182
|
+
updateRemoveButtons();
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
item.appendChild(removeBtn);
|
|
2186
|
+
}
|
|
2187
|
+
const disabled = currentCount <= minCount;
|
|
2188
|
+
removeBtn.disabled = disabled;
|
|
2189
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2190
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
|
|
2194
|
+
function updateAddButton() {
|
|
2195
|
+
const existingAddBtn = wrapper.querySelector(".add-number-btn");
|
|
2196
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
2197
|
+
|
|
2198
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
2199
|
+
const addBtn = document.createElement("button");
|
|
2200
|
+
addBtn.type = "button";
|
|
2201
|
+
addBtn.className =
|
|
2202
|
+
"add-number-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2203
|
+
addBtn.textContent = `+ Add ${element.label || "Number"}`;
|
|
2204
|
+
addBtn.onclick = () => {
|
|
2205
|
+
values.push(element.default || "");
|
|
2206
|
+
addNumberItem(element.default || "");
|
|
2207
|
+
updateAddButton();
|
|
2208
|
+
updateRemoveButtons();
|
|
2209
|
+
};
|
|
2210
|
+
wrapper.appendChild(addBtn);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
// Render initial items
|
|
2215
|
+
values.forEach((value) => addNumberItem(value));
|
|
2216
|
+
updateAddButton();
|
|
2217
|
+
updateRemoveButtons();
|
|
2218
|
+
|
|
2219
|
+
// Add hint
|
|
2220
|
+
const hint = document.createElement("p");
|
|
2221
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
2222
|
+
hint.textContent = makeFieldHint(element);
|
|
2223
|
+
wrapper.appendChild(hint);
|
|
2224
|
+
}
|
|
2225
|
+
|
|
1227
2226
|
function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
1228
2227
|
const selectInput = document.createElement("select");
|
|
1229
2228
|
selectInput.className =
|
|
@@ -1250,6 +2249,131 @@ function renderSelectElement(element, ctx, wrapper, pathKey) {
|
|
|
1250
2249
|
wrapper.appendChild(selectHint);
|
|
1251
2250
|
}
|
|
1252
2251
|
|
|
2252
|
+
function renderMultipleSelectElement(element, ctx, wrapper, pathKey) {
|
|
2253
|
+
const prefillValues = ctx.prefill[element.key] || [];
|
|
2254
|
+
const values = Array.isArray(prefillValues) ? [...prefillValues] : [];
|
|
2255
|
+
|
|
2256
|
+
// Ensure minimum count
|
|
2257
|
+
const minCount = element.minCount ?? 1;
|
|
2258
|
+
const maxCount = element.maxCount ?? 10;
|
|
2259
|
+
|
|
2260
|
+
while (values.length < minCount) {
|
|
2261
|
+
values.push(element.default || element.options?.[0]?.value || "");
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
const container = document.createElement("div");
|
|
2265
|
+
container.className = "space-y-2";
|
|
2266
|
+
wrapper.appendChild(container);
|
|
2267
|
+
|
|
2268
|
+
function updateIndices() {
|
|
2269
|
+
const items = container.querySelectorAll(".multiple-select-item");
|
|
2270
|
+
items.forEach((item, index) => {
|
|
2271
|
+
const select = item.querySelector("select");
|
|
2272
|
+
if (select) {
|
|
2273
|
+
select.name = `${pathKey}[${index}]`;
|
|
2274
|
+
}
|
|
2275
|
+
});
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
function addSelectItem(value = "", index = -1) {
|
|
2279
|
+
const itemWrapper = document.createElement("div");
|
|
2280
|
+
itemWrapper.className = "multiple-select-item flex items-center gap-2";
|
|
2281
|
+
|
|
2282
|
+
const selectInput = document.createElement("select");
|
|
2283
|
+
selectInput.className =
|
|
2284
|
+
"flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500";
|
|
2285
|
+
selectInput.disabled = state.config.readonly;
|
|
2286
|
+
|
|
2287
|
+
// Add options
|
|
2288
|
+
(element.options || []).forEach((option) => {
|
|
2289
|
+
const optionElement = document.createElement("option");
|
|
2290
|
+
optionElement.value = option.value;
|
|
2291
|
+
optionElement.textContent = option.label;
|
|
2292
|
+
if (value === option.value) {
|
|
2293
|
+
optionElement.selected = true;
|
|
2294
|
+
}
|
|
2295
|
+
selectInput.appendChild(optionElement);
|
|
2296
|
+
});
|
|
2297
|
+
|
|
2298
|
+
itemWrapper.appendChild(selectInput);
|
|
2299
|
+
|
|
2300
|
+
// Remove buttons are managed centrally via updateRemoveButtons()
|
|
2301
|
+
|
|
2302
|
+
if (index === -1) {
|
|
2303
|
+
container.appendChild(itemWrapper);
|
|
2304
|
+
} else {
|
|
2305
|
+
container.insertBefore(itemWrapper, container.children[index]);
|
|
2306
|
+
}
|
|
2307
|
+
|
|
2308
|
+
updateIndices();
|
|
2309
|
+
return itemWrapper;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
function updateRemoveButtons() {
|
|
2313
|
+
if (state.config.readonly) return;
|
|
2314
|
+
const items = container.querySelectorAll(".multiple-select-item");
|
|
2315
|
+
const currentCount = items.length;
|
|
2316
|
+
items.forEach((item) => {
|
|
2317
|
+
let removeBtn = item.querySelector(".remove-item-btn");
|
|
2318
|
+
if (!removeBtn) {
|
|
2319
|
+
removeBtn = document.createElement("button");
|
|
2320
|
+
removeBtn.type = "button";
|
|
2321
|
+
removeBtn.className =
|
|
2322
|
+
"remove-item-btn px-2 py-1 text-red-600 hover:bg-red-50 rounded";
|
|
2323
|
+
removeBtn.textContent = "✕";
|
|
2324
|
+
removeBtn.onclick = () => {
|
|
2325
|
+
const currentIndex = Array.from(container.children).indexOf(item);
|
|
2326
|
+
if (container.children.length > minCount) {
|
|
2327
|
+
values.splice(currentIndex, 1);
|
|
2328
|
+
item.remove();
|
|
2329
|
+
updateIndices();
|
|
2330
|
+
updateAddButton();
|
|
2331
|
+
updateRemoveButtons();
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
item.appendChild(removeBtn);
|
|
2335
|
+
}
|
|
2336
|
+
const disabled = currentCount <= minCount;
|
|
2337
|
+
removeBtn.disabled = disabled;
|
|
2338
|
+
removeBtn.style.opacity = disabled ? "0.5" : "1";
|
|
2339
|
+
removeBtn.style.pointerEvents = disabled ? "none" : "auto";
|
|
2340
|
+
});
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
function updateAddButton() {
|
|
2344
|
+
const existingAddBtn = wrapper.querySelector(".add-select-btn");
|
|
2345
|
+
if (existingAddBtn) existingAddBtn.remove();
|
|
2346
|
+
|
|
2347
|
+
if (!state.config.readonly && values.length < maxCount) {
|
|
2348
|
+
const addBtn = document.createElement("button");
|
|
2349
|
+
addBtn.type = "button";
|
|
2350
|
+
addBtn.className =
|
|
2351
|
+
"add-select-btn mt-2 px-3 py-1 text-blue-600 border border-blue-300 rounded hover:bg-blue-50 text-sm";
|
|
2352
|
+
addBtn.textContent = `+ Add ${element.label || "Selection"}`;
|
|
2353
|
+
addBtn.onclick = () => {
|
|
2354
|
+
const defaultValue =
|
|
2355
|
+
element.default || element.options?.[0]?.value || "";
|
|
2356
|
+
values.push(defaultValue);
|
|
2357
|
+
addSelectItem(defaultValue);
|
|
2358
|
+
updateAddButton();
|
|
2359
|
+
updateRemoveButtons();
|
|
2360
|
+
};
|
|
2361
|
+
wrapper.appendChild(addBtn);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
// Render initial items
|
|
2366
|
+
values.forEach((value) => addSelectItem(value));
|
|
2367
|
+
updateAddButton();
|
|
2368
|
+
updateRemoveButtons();
|
|
2369
|
+
|
|
2370
|
+
// Add hint
|
|
2371
|
+
const hint = document.createElement("p");
|
|
2372
|
+
hint.className = "text-xs text-gray-500 mt-1";
|
|
2373
|
+
hint.textContent = makeFieldHint(element);
|
|
2374
|
+
wrapper.appendChild(hint);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
1253
2377
|
function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
1254
2378
|
if (state.config.readonly) {
|
|
1255
2379
|
// Readonly mode: use common preview function
|
|
@@ -1261,7 +2385,11 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
1261
2385
|
const emptyState = document.createElement("div");
|
|
1262
2386
|
emptyState.className =
|
|
1263
2387
|
"aspect-video bg-gray-100 rounded-lg flex items-center justify-center text-gray-500";
|
|
1264
|
-
|
|
2388
|
+
// Create empty state content safely
|
|
2389
|
+
const textDiv = document.createElement("div");
|
|
2390
|
+
textDiv.className = "text-center";
|
|
2391
|
+
textDiv.textContent = t("noFileSelected");
|
|
2392
|
+
emptyState.appendChild(textDiv);
|
|
1265
2393
|
wrapper.appendChild(emptyState);
|
|
1266
2394
|
}
|
|
1267
2395
|
} else {
|
|
@@ -1321,8 +2449,14 @@ function renderFileElement(element, ctx, wrapper, pathKey) {
|
|
|
1321
2449
|
// Add upload text
|
|
1322
2450
|
const uploadText = document.createElement("p");
|
|
1323
2451
|
uploadText.className = "text-xs text-gray-600 mt-2 text-center";
|
|
1324
|
-
|
|
1325
|
-
|
|
2452
|
+
// Create upload text content safely
|
|
2453
|
+
const uploadSpan = document.createElement("span");
|
|
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")));
|
|
1326
2460
|
fileWrapper.appendChild(uploadText);
|
|
1327
2461
|
|
|
1328
2462
|
// Add hint
|
|
@@ -1381,14 +2515,29 @@ function handleInitialFileData(
|
|
|
1381
2515
|
}
|
|
1382
2516
|
|
|
1383
2517
|
function setEmptyFileContainer(fileContainer) {
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
2518
|
+
// Create empty file container content safely
|
|
2519
|
+
clear(fileContainer);
|
|
2520
|
+
|
|
2521
|
+
const wrapper = document.createElement("div");
|
|
2522
|
+
wrapper.className = "flex flex-col items-center justify-center h-full text-gray-400";
|
|
2523
|
+
|
|
2524
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
2525
|
+
svg.setAttribute("class", "w-6 h-6 mb-2");
|
|
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);
|
|
1392
2541
|
}
|
|
1393
2542
|
|
|
1394
2543
|
function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
@@ -1405,7 +2554,16 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
1405
2554
|
resultsWrapper.appendChild(filePreview);
|
|
1406
2555
|
});
|
|
1407
2556
|
} else {
|
|
1408
|
-
|
|
2557
|
+
// Create empty state safely
|
|
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);
|
|
1409
2567
|
}
|
|
1410
2568
|
|
|
1411
2569
|
wrapper.appendChild(resultsWrapper);
|
|
@@ -1468,6 +2626,103 @@ function renderFilesElement(element, ctx, wrapper, pathKey) {
|
|
|
1468
2626
|
}
|
|
1469
2627
|
}
|
|
1470
2628
|
|
|
2629
|
+
function renderMultipleFileElement(element, ctx, wrapper, pathKey) {
|
|
2630
|
+
// Use the same logic as renderFilesElement but with minCount/maxCount from element properties
|
|
2631
|
+
const minFiles = element.minCount ?? 0;
|
|
2632
|
+
const maxFiles = element.maxCount ?? Infinity;
|
|
2633
|
+
|
|
2634
|
+
if (state.config.readonly) {
|
|
2635
|
+
// Readonly mode: render as results list
|
|
2636
|
+
const resultsWrapper = document.createElement("div");
|
|
2637
|
+
resultsWrapper.className = "space-y-4";
|
|
2638
|
+
|
|
2639
|
+
const initialFiles = ctx.prefill[element.key] || [];
|
|
2640
|
+
|
|
2641
|
+
if (initialFiles.length > 0) {
|
|
2642
|
+
initialFiles.forEach((resourceId) => {
|
|
2643
|
+
const filePreview = renderFilePreviewReadonly(resourceId);
|
|
2644
|
+
resultsWrapper.appendChild(filePreview);
|
|
2645
|
+
});
|
|
2646
|
+
} else {
|
|
2647
|
+
// Create empty state safely
|
|
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);
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
wrapper.appendChild(resultsWrapper);
|
|
2660
|
+
} else {
|
|
2661
|
+
// Edit mode: multiple file input with min/max validation
|
|
2662
|
+
const filesWrapper = document.createElement("div");
|
|
2663
|
+
filesWrapper.className = "space-y-2";
|
|
2664
|
+
|
|
2665
|
+
const filesPicker = document.createElement("input");
|
|
2666
|
+
filesPicker.type = "file";
|
|
2667
|
+
filesPicker.name = pathKey;
|
|
2668
|
+
filesPicker.multiple = true;
|
|
2669
|
+
filesPicker.style.display = "none"; // Hide default input
|
|
2670
|
+
if (element.accept?.extensions) {
|
|
2671
|
+
filesPicker.accept = element.accept.extensions
|
|
2672
|
+
.map((ext) => `.${ext}`)
|
|
2673
|
+
.join(",");
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
const filesContainer = document.createElement("div");
|
|
2677
|
+
filesContainer.className = "files-list space-y-2";
|
|
2678
|
+
|
|
2679
|
+
filesWrapper.appendChild(filesPicker);
|
|
2680
|
+
filesWrapper.appendChild(filesContainer);
|
|
2681
|
+
|
|
2682
|
+
const initialFiles = Array.isArray(ctx.prefill[element.key])
|
|
2683
|
+
? [...ctx.prefill[element.key]]
|
|
2684
|
+
: [];
|
|
2685
|
+
|
|
2686
|
+
// Add initial files to resource index
|
|
2687
|
+
addPrefillFilesToIndex(initialFiles);
|
|
2688
|
+
|
|
2689
|
+
const updateFilesDisplay = () => {
|
|
2690
|
+
renderResourcePills(filesContainer, initialFiles, (index) => {
|
|
2691
|
+
initialFiles.splice(index, 1);
|
|
2692
|
+
updateFilesDisplay();
|
|
2693
|
+
});
|
|
2694
|
+
|
|
2695
|
+
// Show count and min/max info
|
|
2696
|
+
const countInfo = document.createElement("div");
|
|
2697
|
+
countInfo.className = "text-xs text-gray-500 mt-2";
|
|
2698
|
+
const countText = `${initialFiles.length} file${initialFiles.length !== 1 ? "s" : ""}`;
|
|
2699
|
+
const minMaxText =
|
|
2700
|
+
minFiles > 0 || maxFiles < Infinity
|
|
2701
|
+
? ` (${minFiles}-${maxFiles} allowed)`
|
|
2702
|
+
: "";
|
|
2703
|
+
countInfo.textContent = countText + minMaxText;
|
|
2704
|
+
|
|
2705
|
+
// Remove previous count info
|
|
2706
|
+
const existingCount = filesWrapper.querySelector(".file-count-info");
|
|
2707
|
+
if (existingCount) existingCount.remove();
|
|
2708
|
+
|
|
2709
|
+
countInfo.className += " file-count-info";
|
|
2710
|
+
filesWrapper.appendChild(countInfo);
|
|
2711
|
+
};
|
|
2712
|
+
|
|
2713
|
+
// Set up drag and drop
|
|
2714
|
+
setupFilesDropHandler(filesContainer, initialFiles, updateFilesDisplay);
|
|
2715
|
+
|
|
2716
|
+
// Set up file picker
|
|
2717
|
+
setupFilesPickerHandler(filesPicker, initialFiles, updateFilesDisplay);
|
|
2718
|
+
|
|
2719
|
+
// Initial display
|
|
2720
|
+
updateFilesDisplay();
|
|
2721
|
+
|
|
2722
|
+
wrapper.appendChild(filesWrapper);
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
|
|
1471
2726
|
function addPrefillFilesToIndex(initialFiles) {
|
|
1472
2727
|
if (initialFiles.length > 0) {
|
|
1473
2728
|
initialFiles.forEach((resourceId) => {
|
|
@@ -1591,13 +2846,20 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
1591
2846
|
const item = document.createElement("div");
|
|
1592
2847
|
item.className =
|
|
1593
2848
|
"groupItem border border-dashed border-slate-300 dark:border-slate-600 rounded-lg p-3 mb-3 bg-blue-50/30 dark:bg-blue-900/10";
|
|
2849
|
+
const itemIndex = countItems();
|
|
2850
|
+
const fullPath = pathJoin(ctx.path, `${element.key}[${itemIndex}]`);
|
|
2851
|
+
// Add data-group-item attribute for validation scoping - use full path
|
|
2852
|
+
item.setAttribute("data-group-item", fullPath);
|
|
1594
2853
|
const subCtx = {
|
|
1595
|
-
path:
|
|
2854
|
+
path: fullPath,
|
|
1596
2855
|
prefill: prefillObj || {},
|
|
1597
2856
|
};
|
|
1598
|
-
element.elements.forEach((child) =>
|
|
1599
|
-
|
|
1600
|
-
|
|
2857
|
+
element.elements.forEach((child) => {
|
|
2858
|
+
// Skip rendering hidden child elements
|
|
2859
|
+
if (!child.hidden) {
|
|
2860
|
+
item.appendChild(renderElement(child, subCtx));
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
1601
2863
|
|
|
1602
2864
|
// Only add remove button in edit mode
|
|
1603
2865
|
if (!state.config.readonly) {
|
|
@@ -1628,7 +2890,18 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
1628
2890
|
addBtn.type = "button";
|
|
1629
2891
|
addBtn.className =
|
|
1630
2892
|
"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";
|
|
1631
|
-
|
|
2893
|
+
// Create add button content safely
|
|
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")));
|
|
1632
2905
|
groupWrap.appendChild(addBtn);
|
|
1633
2906
|
}
|
|
1634
2907
|
|
|
@@ -1636,7 +2909,16 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
1636
2909
|
if (state.config.readonly) return;
|
|
1637
2910
|
const n = countItems();
|
|
1638
2911
|
if (addBtn) addBtn.disabled = n >= max;
|
|
1639
|
-
|
|
2912
|
+
// Create elements safely
|
|
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);
|
|
1640
2922
|
};
|
|
1641
2923
|
|
|
1642
2924
|
if (pre && pre.length) {
|
|
@@ -1651,7 +2933,10 @@ function renderRepeatableGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
1651
2933
|
addBtn.addEventListener("click", () => addItem(null));
|
|
1652
2934
|
} else {
|
|
1653
2935
|
// In readonly mode, just show the label without count controls
|
|
1654
|
-
left
|
|
2936
|
+
clear(left);
|
|
2937
|
+
const labelSpan = document.createElement("span");
|
|
2938
|
+
labelSpan.textContent = element.label || element.key;
|
|
2939
|
+
left.appendChild(labelSpan);
|
|
1655
2940
|
}
|
|
1656
2941
|
}
|
|
1657
2942
|
|
|
@@ -1661,11 +2946,240 @@ function renderSingleGroup(element, ctx, itemsWrap, left, groupWrap) {
|
|
|
1661
2946
|
path: pathJoin(ctx.path, element.key),
|
|
1662
2947
|
prefill: ctx.prefill?.[element.key] || {},
|
|
1663
2948
|
};
|
|
1664
|
-
element.elements.forEach((child) =>
|
|
1665
|
-
|
|
1666
|
-
|
|
2949
|
+
element.elements.forEach((child) => {
|
|
2950
|
+
// Skip rendering hidden child elements
|
|
2951
|
+
if (!child.hidden) {
|
|
2952
|
+
itemsWrap.appendChild(renderElement(child, subCtx));
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
1667
2955
|
groupWrap.appendChild(itemsWrap);
|
|
1668
|
-
left
|
|
2956
|
+
clear(left);
|
|
2957
|
+
const labelSpan = document.createElement("span");
|
|
2958
|
+
labelSpan.textContent = element.label || element.key;
|
|
2959
|
+
left.appendChild(labelSpan);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
function renderSingleContainerElement(element, ctx, wrapper, pathKey) {
|
|
2963
|
+
// Same as renderSingleGroup but with updated naming
|
|
2964
|
+
const containerWrap = document.createElement("div");
|
|
2965
|
+
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
2966
|
+
containerWrap.setAttribute("data-container", pathKey);
|
|
2967
|
+
|
|
2968
|
+
const header = document.createElement("div");
|
|
2969
|
+
header.className = "flex justify-between items-center mb-4";
|
|
2970
|
+
|
|
2971
|
+
const left = document.createElement("div");
|
|
2972
|
+
left.className = "flex-1";
|
|
2973
|
+
|
|
2974
|
+
const itemsWrap = document.createElement("div");
|
|
2975
|
+
itemsWrap.className = "space-y-4";
|
|
2976
|
+
|
|
2977
|
+
containerWrap.appendChild(header);
|
|
2978
|
+
header.appendChild(left);
|
|
2979
|
+
|
|
2980
|
+
// Single object container
|
|
2981
|
+
const subCtx = {
|
|
2982
|
+
path: pathJoin(ctx.path, element.key),
|
|
2983
|
+
prefill: ctx.prefill?.[element.key] || {},
|
|
2984
|
+
};
|
|
2985
|
+
element.elements.forEach((child) => {
|
|
2986
|
+
// Skip rendering hidden child elements
|
|
2987
|
+
if (!child.hidden) {
|
|
2988
|
+
itemsWrap.appendChild(renderElement(child, subCtx));
|
|
2989
|
+
}
|
|
2990
|
+
});
|
|
2991
|
+
containerWrap.appendChild(itemsWrap);
|
|
2992
|
+
clear(left);
|
|
2993
|
+
const labelSpan = document.createElement("span");
|
|
2994
|
+
labelSpan.textContent = element.label || element.key;
|
|
2995
|
+
left.appendChild(labelSpan);
|
|
2996
|
+
|
|
2997
|
+
wrapper.appendChild(containerWrap);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function renderMultipleContainerElement(element, ctx, wrapper, _pathKey) {
|
|
3001
|
+
// Same as renderRepeatableGroup but with minCount/maxCount from element properties
|
|
3002
|
+
const containerWrap = document.createElement("div");
|
|
3003
|
+
containerWrap.className = "border border-gray-200 rounded-lg p-4 bg-gray-50";
|
|
3004
|
+
|
|
3005
|
+
const header = document.createElement("div");
|
|
3006
|
+
header.className = "flex justify-between items-center mb-4";
|
|
3007
|
+
|
|
3008
|
+
const left = document.createElement("div");
|
|
3009
|
+
left.className = "flex-1";
|
|
3010
|
+
|
|
3011
|
+
const right = document.createElement("div");
|
|
3012
|
+
right.className = "flex gap-2";
|
|
3013
|
+
|
|
3014
|
+
const itemsWrap = document.createElement("div");
|
|
3015
|
+
itemsWrap.className = "space-y-4";
|
|
3016
|
+
|
|
3017
|
+
containerWrap.appendChild(header);
|
|
3018
|
+
header.appendChild(left);
|
|
3019
|
+
if (!state.config.readonly) {
|
|
3020
|
+
header.appendChild(right);
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
const min = element.minCount ?? 0;
|
|
3024
|
+
const max = element.maxCount ?? Infinity;
|
|
3025
|
+
const pre = Array.isArray(ctx.prefill?.[element.key])
|
|
3026
|
+
? ctx.prefill[element.key]
|
|
3027
|
+
: null;
|
|
3028
|
+
|
|
3029
|
+
const countItems = () =>
|
|
3030
|
+
itemsWrap.querySelectorAll(":scope > .containerItem").length;
|
|
3031
|
+
|
|
3032
|
+
const createAddButton = () => {
|
|
3033
|
+
const add = document.createElement("button");
|
|
3034
|
+
add.type = "button";
|
|
3035
|
+
add.className =
|
|
3036
|
+
"px-3 py-1.5 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition-colors";
|
|
3037
|
+
add.textContent = t("addElement");
|
|
3038
|
+
add.onclick = () => {
|
|
3039
|
+
if (countItems() < max) {
|
|
3040
|
+
const idx = countItems();
|
|
3041
|
+
const subCtx = {
|
|
3042
|
+
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
3043
|
+
prefill: {},
|
|
3044
|
+
};
|
|
3045
|
+
const item = document.createElement("div");
|
|
3046
|
+
item.className =
|
|
3047
|
+
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
3048
|
+
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3049
|
+
|
|
3050
|
+
element.elements.forEach((child) => {
|
|
3051
|
+
// Skip rendering hidden child elements
|
|
3052
|
+
if (!child.hidden) {
|
|
3053
|
+
item.appendChild(renderElement(child, subCtx));
|
|
3054
|
+
}
|
|
3055
|
+
});
|
|
3056
|
+
|
|
3057
|
+
// Only add remove button in edit mode
|
|
3058
|
+
if (!state.config.readonly) {
|
|
3059
|
+
const rem = document.createElement("button");
|
|
3060
|
+
rem.type = "button";
|
|
3061
|
+
rem.className =
|
|
3062
|
+
"absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
3063
|
+
rem.textContent = "×";
|
|
3064
|
+
rem.onclick = () => {
|
|
3065
|
+
item.remove();
|
|
3066
|
+
updateAddButton();
|
|
3067
|
+
};
|
|
3068
|
+
item.style.position = "relative";
|
|
3069
|
+
item.appendChild(rem);
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
itemsWrap.appendChild(item);
|
|
3073
|
+
updateAddButton();
|
|
3074
|
+
}
|
|
3075
|
+
};
|
|
3076
|
+
return add;
|
|
3077
|
+
};
|
|
3078
|
+
|
|
3079
|
+
const updateAddButton = () => {
|
|
3080
|
+
const currentCount = countItems();
|
|
3081
|
+
const addBtn = right.querySelector("button");
|
|
3082
|
+
if (addBtn) {
|
|
3083
|
+
addBtn.disabled = currentCount >= max;
|
|
3084
|
+
addBtn.style.opacity = currentCount >= max ? "0.5" : "1";
|
|
3085
|
+
}
|
|
3086
|
+
// Create elements safely
|
|
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);
|
|
3096
|
+
};
|
|
3097
|
+
|
|
3098
|
+
if (!state.config.readonly) {
|
|
3099
|
+
right.appendChild(createAddButton());
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
// Pre-fill initial items
|
|
3103
|
+
if (pre && Array.isArray(pre)) {
|
|
3104
|
+
pre.forEach((prefillObj, idx) => {
|
|
3105
|
+
const subCtx = {
|
|
3106
|
+
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
3107
|
+
prefill: prefillObj || {},
|
|
3108
|
+
};
|
|
3109
|
+
const item = document.createElement("div");
|
|
3110
|
+
item.className =
|
|
3111
|
+
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
3112
|
+
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3113
|
+
|
|
3114
|
+
element.elements.forEach((child) => {
|
|
3115
|
+
// Skip rendering hidden child elements
|
|
3116
|
+
if (!child.hidden) {
|
|
3117
|
+
item.appendChild(renderElement(child, subCtx));
|
|
3118
|
+
}
|
|
3119
|
+
});
|
|
3120
|
+
|
|
3121
|
+
// Only add remove button in edit mode
|
|
3122
|
+
if (!state.config.readonly) {
|
|
3123
|
+
const rem = document.createElement("button");
|
|
3124
|
+
rem.type = "button";
|
|
3125
|
+
rem.className =
|
|
3126
|
+
"absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
3127
|
+
rem.textContent = "×";
|
|
3128
|
+
rem.onclick = () => {
|
|
3129
|
+
item.remove();
|
|
3130
|
+
updateAddButton();
|
|
3131
|
+
};
|
|
3132
|
+
item.style.position = "relative";
|
|
3133
|
+
item.appendChild(rem);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
itemsWrap.appendChild(item);
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
|
|
3140
|
+
// Ensure minimum items
|
|
3141
|
+
if (!state.config.readonly) {
|
|
3142
|
+
while (countItems() < min) {
|
|
3143
|
+
const idx = countItems();
|
|
3144
|
+
const subCtx = {
|
|
3145
|
+
path: pathJoin(ctx.path, `${element.key}[${idx}]`),
|
|
3146
|
+
prefill: {},
|
|
3147
|
+
};
|
|
3148
|
+
const item = document.createElement("div");
|
|
3149
|
+
item.className =
|
|
3150
|
+
"containerItem border border-gray-300 rounded-lg p-4 bg-white";
|
|
3151
|
+
item.setAttribute("data-container-item", `${element.key}[${idx}]`);
|
|
3152
|
+
|
|
3153
|
+
element.elements.forEach((child) => {
|
|
3154
|
+
// Skip rendering hidden child elements
|
|
3155
|
+
if (!child.hidden) {
|
|
3156
|
+
item.appendChild(renderElement(child, subCtx));
|
|
3157
|
+
}
|
|
3158
|
+
});
|
|
3159
|
+
|
|
3160
|
+
// Remove button - but disabled if we're at minimum
|
|
3161
|
+
const rem = document.createElement("button");
|
|
3162
|
+
rem.type = "button";
|
|
3163
|
+
rem.className =
|
|
3164
|
+
"absolute top-2 right-2 w-6 h-6 bg-red-500 text-white rounded-full text-xs hover:bg-red-600 transition-colors";
|
|
3165
|
+
rem.textContent = "×";
|
|
3166
|
+
rem.onclick = () => {
|
|
3167
|
+
if (countItems() > min) {
|
|
3168
|
+
item.remove();
|
|
3169
|
+
updateAddButton();
|
|
3170
|
+
}
|
|
3171
|
+
};
|
|
3172
|
+
item.style.position = "relative";
|
|
3173
|
+
item.appendChild(rem);
|
|
3174
|
+
|
|
3175
|
+
itemsWrap.appendChild(item);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
containerWrap.appendChild(itemsWrap);
|
|
3180
|
+
updateAddButton();
|
|
3181
|
+
|
|
3182
|
+
wrapper.appendChild(containerWrap);
|
|
1669
3183
|
}
|
|
1670
3184
|
|
|
1671
3185
|
// Common file preview rendering function for readonly mode
|
|
@@ -1699,17 +3213,29 @@ function renderFilePreviewReadonly(resourceId, fileName) {
|
|
|
1699
3213
|
try {
|
|
1700
3214
|
const thumbnailUrl = state.config.getThumbnail(resourceId);
|
|
1701
3215
|
if (thumbnailUrl) {
|
|
1702
|
-
|
|
3216
|
+
const img = document.createElement("img");
|
|
3217
|
+
img.src = thumbnailUrl;
|
|
3218
|
+
img.alt = actualFileName || "";
|
|
3219
|
+
img.className = "w-full h-auto";
|
|
3220
|
+
clear(previewContainer);
|
|
3221
|
+
previewContainer.appendChild(img);
|
|
1703
3222
|
} else {
|
|
1704
3223
|
// Fallback to icon if getThumbnail returns null/undefined
|
|
1705
|
-
previewContainer
|
|
3224
|
+
clear(previewContainer);
|
|
3225
|
+
previewContainer.appendChild(
|
|
3226
|
+
createPreviewElement("🖼️", actualFileName),
|
|
3227
|
+
);
|
|
1706
3228
|
}
|
|
1707
3229
|
} catch (error) {
|
|
1708
3230
|
console.warn("getThumbnail failed for", resourceId, error);
|
|
1709
|
-
previewContainer
|
|
3231
|
+
clear(previewContainer);
|
|
3232
|
+
previewContainer.appendChild(
|
|
3233
|
+
createPreviewElement("🖼️", actualFileName),
|
|
3234
|
+
);
|
|
1710
3235
|
}
|
|
1711
3236
|
} else {
|
|
1712
|
-
previewContainer
|
|
3237
|
+
clear(previewContainer);
|
|
3238
|
+
previewContainer.appendChild(createPreviewElement("🖼️", actualFileName));
|
|
1713
3239
|
}
|
|
1714
3240
|
} else if (isVideo) {
|
|
1715
3241
|
// Video preview - try getThumbnail for video URL
|
|
@@ -1717,34 +3243,76 @@ function renderFilePreviewReadonly(resourceId, fileName) {
|
|
|
1717
3243
|
try {
|
|
1718
3244
|
const videoUrl = state.config.getThumbnail(resourceId);
|
|
1719
3245
|
if (videoUrl) {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
3246
|
+
// Create video elements safely
|
|
3247
|
+
const wrapper = document.createElement("div");
|
|
3248
|
+
wrapper.className = "relative group";
|
|
3249
|
+
|
|
3250
|
+
const video = document.createElement("video");
|
|
3251
|
+
video.className = "w-full h-auto";
|
|
3252
|
+
video.controls = true;
|
|
3253
|
+
video.preload = "auto";
|
|
3254
|
+
video.muted = true;
|
|
3255
|
+
|
|
3256
|
+
const source = document.createElement("source");
|
|
3257
|
+
source.src = videoUrl;
|
|
3258
|
+
source.type = meta?.type || "video/mp4";
|
|
3259
|
+
|
|
3260
|
+
video.appendChild(source);
|
|
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);
|
|
1735
3295
|
} else {
|
|
1736
|
-
previewContainer
|
|
3296
|
+
clear(previewContainer);
|
|
3297
|
+
previewContainer.appendChild(
|
|
3298
|
+
createPreviewElement("🎥", actualFileName),
|
|
3299
|
+
);
|
|
1737
3300
|
}
|
|
1738
3301
|
} catch (error) {
|
|
1739
3302
|
console.warn("getThumbnail failed for video", resourceId, error);
|
|
1740
|
-
previewContainer
|
|
3303
|
+
clear(previewContainer);
|
|
3304
|
+
previewContainer.appendChild(
|
|
3305
|
+
createPreviewElement("🎥", actualFileName),
|
|
3306
|
+
);
|
|
1741
3307
|
}
|
|
1742
3308
|
} else {
|
|
1743
|
-
previewContainer
|
|
3309
|
+
clear(previewContainer);
|
|
3310
|
+
previewContainer.appendChild(createPreviewElement("🎥", actualFileName));
|
|
1744
3311
|
}
|
|
1745
3312
|
} else {
|
|
1746
3313
|
// Other file types
|
|
1747
|
-
previewContainer
|
|
3314
|
+
clear(previewContainer);
|
|
3315
|
+
previewContainer.appendChild(createPreviewElement("📁", actualFileName));
|
|
1748
3316
|
}
|
|
1749
3317
|
|
|
1750
3318
|
// File name
|
|
@@ -1910,6 +3478,8 @@ function saveDraft() {
|
|
|
1910
3478
|
|
|
1911
3479
|
function clearForm() {
|
|
1912
3480
|
if (state.formRoot) {
|
|
3481
|
+
// Clean up any existing object URLs before clearing form
|
|
3482
|
+
revokeAllObjectURLs();
|
|
1913
3483
|
clear(state.formRoot);
|
|
1914
3484
|
}
|
|
1915
3485
|
}
|